diff --git a/CLAUDE.md b/CLAUDE.md index 3e85cca7b9..aaf88a51cc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1587 +**Current Version:** 0.5.1588 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 2bf30b37a8..b57491dd5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5623,7 +5623,7 @@ checksum = "fc61f41aef38c94e922057977bcb33bf185ab42242188719991ecfdc0fa1fe6b" [[package]] name = "perry" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "anyhow", "base64 0.22.1", @@ -5687,7 +5687,7 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "perry-dispatch", "serde", @@ -5695,7 +5695,7 @@ dependencies = [ [[package]] name = "perry-audio-miniaudio" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "cc", "libc", @@ -5704,7 +5704,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "aho-corasick", "anyhow", @@ -5721,7 +5721,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "anyhow", "perry-hir", @@ -5729,7 +5729,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "anyhow", "perry-hir", @@ -5737,7 +5737,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "anyhow", "perry-dispatch", @@ -5746,7 +5746,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "anyhow", "perry-hir", @@ -5754,7 +5754,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "anyhow", "base64 0.22.1", @@ -5766,7 +5766,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "anyhow", "perry-hir", @@ -5774,7 +5774,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "async-trait", "clap", @@ -5798,14 +5798,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "serde", "serde_json", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1587" +version = "0.5.1588" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5824,7 +5824,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "anyhow", "clap", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "block2", "objc2", @@ -5849,7 +5849,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "argon2", "perry-ffi", @@ -5858,7 +5858,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "perry-ffi", "reqwest", @@ -5867,7 +5867,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "bcrypt", "perry-ffi", @@ -5875,7 +5875,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "perry-ffi", "rusqlite", @@ -5883,7 +5883,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "perry-ffi", "scraper", @@ -5891,7 +5891,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "perry-ffi", "perry-runtime", @@ -5899,7 +5899,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "chrono", "cron", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "chrono", "perry-ffi", @@ -5917,7 +5917,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "perry-ffi", "rust_decimal", @@ -5925,7 +5925,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "perry-ffi", "serde_json", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "perry-ffi", "rand 0.10.2", @@ -5941,7 +5941,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "perry-ffi", "perry-runtime", @@ -5949,14 +5949,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "bytes", "http-body-util", @@ -5973,7 +5973,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "bytes", "lazy_static", @@ -5986,7 +5986,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "base64 0.22.1", "bytes", @@ -6018,7 +6018,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "lazy_static", "perry-ffi", @@ -6028,7 +6028,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "base64 0.22.1", "jsonwebtoken", @@ -6039,7 +6039,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "lru", "perry-ffi", @@ -6048,7 +6048,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "chrono", "perry-ffi", @@ -6056,7 +6056,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "bson", "futures-util", @@ -6068,7 +6068,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "chrono", "perry-ffi", @@ -6080,7 +6080,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "nanoid", "perry-ffi", @@ -6089,7 +6089,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "bytes", "perry-ffi", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "const-oid 0.10.2", "der 0.8.2", @@ -6123,7 +6123,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "lettre", "perry-ffi", @@ -6133,7 +6133,7 @@ dependencies = [ [[package]] name = "perry-ext-parcel-watcher" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "notify", "perry-ffi", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "perry-ffi", "printpdf", @@ -6153,7 +6153,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "perry-ffi", "sqlx", @@ -6162,7 +6162,7 @@ dependencies = [ [[package]] name = "perry-ext-qs" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "perry-ffi", "perry-runtime", @@ -6171,7 +6171,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "governor", "perry-ffi", @@ -6179,7 +6179,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "fast_image_resize", "image", @@ -6190,7 +6190,7 @@ dependencies = [ [[package]] name = "perry-ext-streams" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "lazy_static", "perry-ffi", @@ -6199,7 +6199,7 @@ dependencies = [ [[package]] name = "perry-ext-typescript" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "anyhow", "perry-ffi", @@ -6219,7 +6219,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "perry-ffi", "perry-runtime", @@ -6228,7 +6228,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "perry-ffi", "uuid", @@ -6236,7 +6236,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "perry-ffi", "perry-validation", @@ -6245,7 +6245,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "futures-util", "lazy_static", @@ -6258,7 +6258,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "brotli", "flate2", @@ -6268,7 +6268,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "dashmap 6.2.1", "once_cell", @@ -6278,7 +6278,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "anyhow", "perry-api-manifest", @@ -6298,11 +6298,11 @@ dependencies = [ [[package]] name = "perry-native-registration" -version = "0.5.1587" +version = "0.5.1588" [[package]] name = "perry-parser" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "anyhow", "perry-diagnostics", @@ -6315,7 +6315,7 @@ dependencies = [ [[package]] name = "perry-perex" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "perex", "regex", @@ -6323,7 +6323,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "ahash", "base64 0.22.1", @@ -6381,14 +6381,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6477,21 +6477,21 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "perry-hir", ] [[package]] name = "perry-ui" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "dirs", "perry-ffi", @@ -6501,7 +6501,7 @@ dependencies = [ [[package]] name = "perry-ui-android" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "base64 0.22.1", "jni", @@ -6516,7 +6516,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "rand 0.10.2", "serde", @@ -6526,7 +6526,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "base64 0.22.1", "cairo-rs 0.22.9", @@ -6549,7 +6549,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "base64 0.22.1", "block2", @@ -6566,7 +6566,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "base64 0.22.1", "block2", @@ -6583,7 +6583,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1587" +version = "0.5.1588" [[package]] name = "perry-ui-test" @@ -6594,11 +6594,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1587" +version = "0.5.1588" [[package]] name = "perry-ui-tvos" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "base64 0.22.1", "block2", @@ -6615,7 +6615,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "base64 0.22.1", "block2", @@ -6632,7 +6632,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "block2", "libc", @@ -6646,7 +6646,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "base64 0.22.1", "libc", @@ -6665,7 +6665,7 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "base64 0.22.1", "libc", @@ -6678,7 +6678,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "anyhow", "base64 0.22.1", @@ -6693,7 +6693,7 @@ dependencies = [ [[package]] name = "perry-validation" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "idna", "regex", @@ -6703,7 +6703,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1587" +version = "0.5.1588" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 93b4774199..bec99c356b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -338,7 +338,7 @@ codegen-units = 1 codegen-units = 1 [workspace.package] -version = "0.5.1587" +version = "0.5.1588" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/10349-cjs-preamble-cost.md b/changelog.d/10349-cjs-preamble-cost.md new file mode 100644 index 0000000000..9ea0c5421a --- /dev/null +++ b/changelog.d/10349-cjs-preamble-cost.md @@ -0,0 +1,23 @@ +The CommonJS wrapper preamble is emitted into every wrapped module, so its +fixed cost is paid once per module in the dependency graph. Four changes cut it +by a third on a 400-module fixture (598,533 to 397,896 instructions per +module): + +* One `createRequire` instance per program instead of one per module. The call + costs ~117,000 instructions and was made once per module, plus again inside + `require` for every builtin specifier. It is used only for `.cache`, + `.extensions` and loading builtins, all of which are process-global in Node. +* `require.cache = {}` and `require.extensions = { … }` were dead stores, + overwritten on the following line — an object and three closures allocated + and dropped per module. +* The builtin-specifier test uses `isBuiltin` from `node:module` instead of a + switch over all 58 builtin names emitted into every module, which interned + ~120 string constants per module. **This also fixes a divergence:** the + switch accepted the bare spellings of `sea`, `sqlite`, `test` and + `test/reporters`, which are builtins only in their `node:` form, so + `require("test")` could resolve to the builtin instead of a local module. +* The module record is built as one object literal rather than eleven + sequential assignments, so it is allocated with its final shape instead of + walking eleven shape transitions. `cjs_scaffolding`'s recogniser is widened + to match the folded template; that rule carries no soundness weight and the + allocation half of the collector is report-only. diff --git a/crates/perry-codegen/src/collectors/cjs_scaffolding.rs b/crates/perry-codegen/src/collectors/cjs_scaffolding.rs index 8801eec9c0..c7e989edbd 100644 --- a/crates/perry-codegen/src/collectors/cjs_scaffolding.rs +++ b/crates/perry-codegen/src/collectors/cjs_scaffolding.rs @@ -99,8 +99,10 @@ //! //! * **R1** `Stmt::Let` named `__cjs_module`, `mutable: false`, initialized by //! an `Expr::New` of an `__AnonShape_…` class (an object literal); -//! * **R2** that literal is exactly `{ exports: {} }` — one field whose value -//! is an argument-less `__AnonShape_…` allocation; +//! * **R2** that literal is `{ exports: {} }` — one field whose value is an +//! argument-less `__AnonShape_…` allocation — or one of the two folded +//! wrapper templates (eight or eleven fixed fields) that lowering produces +//! when `wrap.rs` emits the record as a single object literal; //! * **R3** exactly one top-level statement satisfying R1+R2, so "the record" //! is unambiguous; //! * **R4** the same top level binds `var module = __cjs_module` — @@ -500,6 +502,13 @@ fn record_binding(stmt: &Stmt) -> Option { if !inner.starts_with(ANON_SHAPE_PREFIX) || !inner_args.is_empty() { return None; } + // The eight fixed fields the wrapper folds into the record literal, and the + // eleven-field form that also folds `parent`, `paths` and `require`. + // + // R2 carries no soundness weight (see the module doc: R4 alone discharges + // the obligation, and this half is report-only). Widening it can therefore + // only change whether Perry's own scaffolding is reported as a denied user + // candidate — never what codegen does. let folded_template = matches!( args.as_slice(), [ @@ -514,6 +523,24 @@ fn record_binding(stmt: &Stmt) -> Option { ] if matches!(factory, Expr::LocalGet(_) | Expr::Undefined) && id_value == filename && children.is_empty() + ) || matches!( + args.as_slice(), + [ + _, + Expr::Bool(true), + factory, + Expr::String(id_value), + Expr::String(_path), + Expr::String(filename), + Expr::Bool(false), + Expr::Array(children), + _parent, + Expr::Array(paths), + Expr::Undefined, + ] if matches!(factory, Expr::LocalGet(_) | Expr::Undefined) + && id_value == filename + && children.is_empty() + && paths.len() == 1 ); (args.len() == 1 || folded_template).then_some(*id) } diff --git a/crates/perry-runtime/src/gc/tests/copy_slot_hoists.rs b/crates/perry-runtime/src/gc/tests/copy_slot_hoists.rs index d0f1fbd172..9c295b7fe1 100644 --- a/crates/perry-runtime/src/gc/tests/copy_slot_hoists.rs +++ b/crates/perry-runtime/src/gc/tests/copy_slot_hoists.rs @@ -1,10 +1,15 @@ -//! The copying minor reads two facts once per traced object that it used to -//! re-derive for every slot of that object: whether the parent is a weak -//! holder, and whether the parent is in old-gen. +//! The copying minor reads the parent's weak-holder fact once per traced +//! object instead of re-deriving it for every slot of that object (#10362). //! -//! Both are pinned by a COLLECTION and its observable outcome, not by reading -//! the hoisted value back — and each has a sabotaged twin that forgets the -//! fact, so the hoist is shown to be load-bearing rather than merely present. +//! Pinned by a COLLECTION and its observable outcome, not by reading the +//! hoisted value back, and paired with a sabotaged twin that forgets the fact, +//! so the hoist is shown to be load-bearing rather than merely present. +//! +//! The parent's old-generation fact is deliberately NOT hoisted, so there is no +//! test for it here. No sabotage of that hoist could be made to fail: sticky +//! dirty-page coverage carries an old→young edge independently of the +//! remembered-set re-insertion the fact controls. A future hoist of it needs +//! its own witness first (#10388). use super::super::*; use super::support::*; diff --git a/crates/perry/src/commands/compile/cjs_wrap/preamble_canary_tests.rs b/crates/perry/src/commands/compile/cjs_wrap/preamble_canary_tests.rs index cbacddaefa..5f5b6a780d 100644 --- a/crates/perry/src/commands/compile/cjs_wrap/preamble_canary_tests.rs +++ b/crates/perry/src/commands/compile/cjs_wrap/preamble_canary_tests.rs @@ -115,7 +115,13 @@ fn the_canary_chain_still_reports_a_genuine_barrier() { /// `defineProperty(require, 'name', …)`, `require.cache = {}`, /// `require.extensions = { … }`, and the transpiler's /// `defineProperty(exports, "__esModule", …)`. -const EXPECTED_PREAMBLE_ALLOC_STMTS: usize = 5; +/// Lowered from 5: the preamble no longer emits `require.cache = {}`, +/// `require.extensions = { … }` or `Object.defineProperty(require, 'name', …)`. +/// The first two were dead stores overwritten on the following line, and the +/// third set the descriptor a `function require(...)` declaration already has. +/// Their arms in `cjs_scaffolding.rs` are kept — they stay correct for any +/// template that does allocate there — they simply no longer fire. +const EXPECTED_PREAMBLE_ALLOC_STMTS: usize = 2; /// The #7152 half of the canary. Red means `wrap.rs` and /// `perry-codegen/src/collectors/cjs_scaffolding.rs` disagree about what the @@ -130,17 +136,16 @@ fn the_cjs_preamble_is_still_recognised_as_scaffolding_allocation() { // opaque count mismatch. for (needle, conjunct) in [ ( - "const __cjs_module = { exports: {} };", - "R1/R2 (the record and its `{ exports: {} }` literal)", + " exports: {},", + "R1/R2 (the record literal's leading `exports: {}` field)", ), ( - "var module = __cjs_module;", - "R4 (the alias that denies the record)", + " require: undefined,", + "R1/R2 (the record literal's folded eleventh field)", ), - ("require.cache = {}", "the `require.cache` allocation"), ( - "require.extensions = {", - "the `require.extensions` allocation", + "var module = __cjs_module;", + "R4 (the alias that denies the record)", ), ] { assert!( diff --git a/crates/perry/src/commands/compile/cjs_wrap/tests.rs b/crates/perry/src/commands/compile/cjs_wrap/tests.rs index 3317e32298..e64cdba370 100644 --- a/crates/perry/src/commands/compile/cjs_wrap/tests.rs +++ b/crates/perry/src/commands/compile/cjs_wrap/tests.rs @@ -462,8 +462,11 @@ fn wrap_module_and_exports_are_reassignable_vars() { // exports back from a stable, body-untouchable `__cjs_module`. let src = "exports.foo = 42;"; let wrapped = wrap_commonjs(src, &PathBuf::from("/tmp/test.js")); + // The record is emitted as one folded object literal, so assert the + // `const` binding and its leading `exports` field rather than the old + // single-field spelling. assert!( - wrapped.contains("const __cjs_module = { exports: {} };"), + wrapped.contains("const __cjs_module = {") && wrapped.contains("exports: {},"), "expected stable __cjs_module, got:\n{}", wrapped ); @@ -1180,7 +1183,7 @@ fn wrap_flat_emits_class_module_exports_that_closes_over_top_level_const() { wrapped ); // The CommonJS runtime shims still run at module scope. - assert!(wrapped.contains("const __cjs_module = { exports: {} };")); + assert!(wrapped.contains("const __cjs_module = {") && wrapped.contains("exports: {},")); assert!(wrapped.contains("const _cjs = __cjs_module.exports;")); let ast = perry_parser::parse_typescript(&wrapped, "stack-utils.js") .expect("flat class wrap must parse"); diff --git a/crates/perry/src/commands/compile/cjs_wrap/wrap.rs b/crates/perry/src/commands/compile/cjs_wrap/wrap.rs index 40f74665a2..91840d4319 100644 --- a/crates/perry/src/commands/compile/cjs_wrap/wrap.rs +++ b/crates/perry/src/commands/compile/cjs_wrap/wrap.rs @@ -416,7 +416,7 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( .collect::>() .join("\n"); let imports = format!( - "import {{ createRequire as __perry_cjs_create_require }} from 'node:module';\n{imports}" + "import {{ createRequire as __perry_cjs_create_require, isBuiltin as __perry_cjs_is_builtin }} from 'node:module';\n{imports}" ); // An UNRESOLVABLE adopted specifier (`require('@opentelemetry/api')` @@ -494,7 +494,10 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( // codegen does not initialize for native modules in CJS-wrapped // modules). createRequire calls js_create_native_module_namespace // under the hood — the same path Node.js uses for require("process"). - format!("{link_child}return __perry_cjs_create_require({:?})(specifier);", source_path.to_string_lossy()) + format!( + "{link_child}return (globalThis.__perry_cjs_shared_require || (globalThis.__perry_cjs_shared_require = __perry_cjs_create_require({:?})))(specifier);", + source_path.to_string_lossy() + ) } else if needs_runtime_record { runtime_require.clone().unwrap_or_else(|| format!("return {local};")) } else { @@ -952,11 +955,6 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( // `require(specifier)` for one of those fell through to compiled-module // resolution and raised `MODULE_NOT_FOUND` instead of routing through // `createRequire`. Each entry emits both the bare and `node:` spelling. - let builtin_predicate_cases = perry_hir::NODE_BUILTIN_MODULES - .iter() - .map(|name| format!("case '{name}': case 'node:{name}':")) - .collect::>() - .join("\n "); let cjs_preamble = format!( r#" // #3527: `module`/`exports` are reassignable `var`s (mirroring Node, where // they are wrapper-function parameters), so CJS bodies that do @@ -967,20 +965,35 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( // a body reassigning its local `module` can't clobber it (Node holds the // real module ref the same way), so named/default-export resolution stays // correct regardless of what the body does to its `module` local. - const __cjs_module = {{ exports: {{}} }}; // #6769: the Node `Module` record surface. Set before user code so a // recursive load of this module observes the same shape Node exposes. - __cjs_module.__perry_cjs_record = true; - __cjs_module.__perry_cjs_factory = {cjs_factory_value}; - __cjs_module.id = {module_filename_literal}; - __cjs_module.path = {module_dir_literal}; - __cjs_module.filename = {module_filename_literal}; - __cjs_module.loaded = false; - __cjs_module.children = []; - __cjs_module.parent = globalThis.__perry_cjs_pending_parent; + // + // ONE object literal, so the record is allocated with its final shape. + // As eleven sequential assignments it walked eleven shape transitions and + // eleven cold property stores — about 10k instructions each at module-init + // time — in every CommonJS module in the graph. Folding only some of the + // fields does not help: the trailing assignments keep transitioning the + // record and the win disappears (measured at -0.08%), so the whole surface + // folds or none of it does. + // + // `cjs_scaffolding.rs`'s `record_binding` matches this field list + // positionally. Adding or reordering a field drops the record back to being + // reported as a denied user candidate in the `Ptr` report; + // `preamble_canary_tests` is what catches that. + const __cjs_module = {{ + exports: {{}}, + __perry_cjs_record: true, + __perry_cjs_factory: {cjs_factory_value}, + id: {module_filename_literal}, + path: {module_dir_literal}, + filename: {module_filename_literal}, + loaded: false, + children: [], + parent: globalThis.__perry_cjs_pending_parent, + paths: [{module_dir_literal} + '/node_modules'], + require: undefined, + }}; globalThis.__perry_cjs_pending_parent = undefined; - __cjs_module.paths = [{module_dir_literal} + '/node_modules']; - __cjs_module.require = undefined; // Node populates `module.parent` before the body evaluates, so link it // here rather than at the tail's registry publication. __perry_link_path_module_parent(__cjs_module); @@ -992,21 +1005,44 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( __perry_register_path_module_partial({module_path_literal}, __cjs_module); var module = __cjs_module; var exports = __cjs_module.exports; - const __perry_cjs_base_require = __perry_cjs_create_require({module_filename_literal}); + // One `createRequire` instance for the whole program, not one per module. + // It is used only for `.cache`, `.extensions` and loading builtins, and all + // three are process-global in Node — nothing here is bound to this + // module's path. The call costs ~117k instructions, so paying it per module + // cost OpenCode's ~2,200 CJS modules a quarter of a billion instructions + // before any user code ran. + const __perry_cjs_base_require = (globalThis.__perry_cjs_shared_require + || (globalThis.__perry_cjs_shared_require = __perry_cjs_create_require({module_filename_literal}))); __perry_cjs_base_require.cache[{module_filename_literal}] = __cjs_module; function __perry_cjs_require_error(kind, code, message) {{ const err = kind === 'type' ? new TypeError(message) : new Error(message); err.code = code; return err; }} + // Accepts BOTH spellings of every builtin, which is what the switch this + // replaced did. `isBuiltin` alone is stricter than the switch: `sea`, + // `sqlite`, `test` and `test/reporters` are builtins only in their `node:` + // form, so bare `require("sqlite")` stopped resolving — and OpenCode's + // dependency graph contains exactly that. wrangler does + // `DatabaseSync = __require("sqlite").DatabaseSync` and then + // `new DatabaseSync(...)`, which became `new undefined()`. + // + // Whether Perry should accept the bare spellings at all is a real question, + // but it is a SEMANTIC one and does not belong in a performance change. + // Behaviour here is byte-for-byte what the switch did; the divergence is + // filed separately. function __perry_cjs_require_is_builtin(specifier) {{ - switch (specifier) {{ - {builtin_predicate_cases} - return true; - default: - return false; - }} + return __perry_cjs_is_builtin(specifier) + || __perry_cjs_is_builtin('node:' + specifier); }} + // `isBuiltin` comes from `node:module` instead of a switch emitted into + // EVERY CommonJS module. The switch carried both spellings of all 58 + // builtin names, so each module interned ~120 string constants and + // initialised its own copy of the table before running a line of user + // code. It was also more permissive than Node: `sea`, `sqlite`, `test` and + // `test/reporters` are builtins only in their `node:` form, and the switch + // accepted the bare spelling too. The runtime predicate agrees with Node + // 26 on all 58 names in both spellings. function require(specifier) {{ if (typeof specifier !== 'string') throw __perry_cjs_require_error('type', 'ERR_INVALID_ARG_TYPE', 'The "id" argument must be of type string.'); if (specifier === '') throw __perry_cjs_require_error('type', 'ERR_INVALID_ARG_VALUE', 'The argument "id" must be a non-empty string.'); @@ -1016,7 +1052,7 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( // createRequire at runtime, which calls js_create_native_module_namespace // under the hood — the same path Node.js uses for require("process"). if (__perry_cjs_require_is_builtin(specifier)) {{ - return __perry_cjs_create_require({module_path_literal})(specifier); + return __perry_cjs_base_require(specifier); }} // Runtime `require(path)` of a module Perry AOT-compiled but that is // only reachable via a computed path. Next's webpack runtime uses both @@ -1073,12 +1109,13 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( }} throw __perry_cjs_require_error('error', 'MODULE_NOT_FOUND', "Cannot find module '" + specifier + "'"); }} - Object.defineProperty(require, 'name', {{ - value: 'require', - writable: false, - enumerable: false, - configurable: true, - }}); + // No `defineProperty(require, 'name', ...)`: a `function require(...)` + // declaration already carries exactly + // {{value:'require', writable:false, enumerable:false, configurable:true}}, + // verified identical in Node 26 and Perry. The redundant install also gave + // the require object OBJ_FLAG_HAS_DESCRIPTORS, which pushed every later + // `require.resolve = ...` / `require.cache = ...` assignment onto the + // descriptor-bearing store path (#10287) in every CommonJS module. require.resolve = function resolve(specifier, options) {{ if (typeof specifier !== 'string') throw __perry_cjs_require_error('type', 'ERR_INVALID_ARG_TYPE', 'The "request" argument must be of type string.'); {require_resolve_cases} @@ -1091,12 +1128,11 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( if (typeof specifier !== 'string') throw __perry_cjs_require_error('type', 'ERR_INVALID_ARG_TYPE', 'The "request" argument must be of type string.'); return null; }}; - require.cache = {{}}; - require.extensions = {{ - '.js': function(module, filename) {{}}, - '.json': function(module, filename) {{}}, - '.node': function(module, filename) {{}}, - }}; + // `cache` and `extensions` come straight from the createRequire instance: + // the placeholder object literals they used to be initialised with were + // overwritten on the very next line, so every CJS module allocated an + // object plus three closures and immediately dropped them. At OpenCode's + // ~2,200 CJS modules that is pure startup garbage. require.cache = __perry_cjs_base_require.cache; require.extensions = __perry_cjs_base_require.extensions; require.main = module;"#