chore: merge train 226 (v0.5.1605) - #10751
Merged
Merged
Conversation
#10488 added a `ctx.local_types` refresh to the redeclaration branch of `lower_let` so `is_numeric_expr` and `static_type_of` would stop disagreeing about a hoisted `var`. A captured local reaches that same branch without any redeclaration in the source: `Stmt::PreallocateBoxes` registers the id up front, so its one real `Stmt::Let` finds `ctx.locals` already populated and lands there. For such an id the refined type describes the VALUE while the slot holds a box pointer. The `local_types` readers then lower reads as raw local loads instead of `js_box_get_bits`, so the declaring scope read `undefined` while a closure over the same binding, holding the box directly, still saw the real value -- `peek() === dest` was false. It fixed that desync for hoisted `var` and introduced a new one for box-captured locals. Skip the refresh when `ctx.boxed_vars` holds the id; a hoisted `var` is unboxed unless separately captured, so #10488 keeps its fix. Reached only once `Array.prototype` has carried an indexed property, which arms the monotone array-index deopt and routes element stores through the generic runtime-key path. The trigger is rare; an array local captured by a closure is ordinary code.
Perry's CJS preamble emitted \`require.main = module;\` unconditionally in
every compiled CommonJS module, so \`require.main === module\` was trivially
true everywhere, not just in the entry point. The standard CommonJS "am I
the entry?" idiom (\`if (require.main === module) { ... }\`) therefore took
its CLI/direct-run branch in every dependency that used it, merely because
it was imported.
The compiler already knows which module is the compile-time entry (the
same \`ctx.entry_canonical\` comparison \`import.meta.main\` uses). Thread
that through cjs_wrap's preamble: the entry module publishes its own
\`module\` record once, before running any of its own \`require()\` calls,
into a new per-heap runtime global (\`js_set_cjs_main_module\`); every other
CJS module reads that value back (\`js_get_cjs_main_module\`) instead of
assigning its own local \`module\`. An ESM entry never publishes, so
\`require.main\` correctly stays \`undefined\` for CJS dependencies it
imports, matching Node.
Fixes #10735
The first fix (publishing the entry's module record from its own preamble)
was insufficient: cjs_wrap hoists a statically-known require('./relative')
into an ESM import, and ESM import evaluation runs a module's static-import
dependencies before the importing module's own top-level code. So a CJS
entry's own dependencies initialize BEFORE the entry's own preamble by the
time codegen assembles main() -- a dependency reading require.main at its
own top level (the require.main === module CLI-guard idiom) would still see
whatever the entry had not yet published.
Fixed by publishing a placeholder object as the shared "main module" from
main() itself (js_bootstrap_cjs_main_module_placeholder), before ANY
module's __init runs -- gated on collectors::is_cjs_wrapped_module so an ESM
entry never does this. The entry's own preamble later reclaims that exact
object (js_get_cjs_main_module) and fills in its real fields in place, so
object identity survives even for a dependency that captured require.main
before the entry's own code ran.
Added test-files/test_gap_10735_require_main_entry.cts and
test_gap_10735_require_main_esm_entry.ts, both verified byte-for-byte
against Node 26.5.1 and proven to fail on a pristine baseline.
…he PASS1_MARKED census pin entry.rs and collect_modules.rs crossed the 2000-line cap after the #10735 require.main fix; trimmed comments (no code changes) to fit under it. gc_runtime_root_holders.json's PASS1_MARKED entry pins a SHA-256 of gc/mod.rs; the #10735 fix's new reg_scanner! registration there changed the file, so the pin needed a re-audit. Added: a scanner registration adds a root SOURCE for the mutable-root walks and runs during root scanning, before mark propagation completes -- it does not execute between census_pass1_if_armed and census_take_if_armed_at_full_sweep_start, so neither census boundary moved. Updated the pinned hash to match.
…_DIAG Diagnostic-only addition (PERRY_GC_DIAG=1) proving the placeholder's identity guarantee empirically rather than only by argument: scan_cjs_main_module_root_mut now counts and logs each time visit_nanbox_u64_slot actually rewrites the cached bits (the placeholder moved this cycle). Distinguishes "the object happened never to move" from "it moved and the cache followed it" -- exactly the question a generic per-cycle moved-object counter can't answer for one specific holder. Verified under PERRY_GC_SCHEDULE_SEED=42 PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_PROTECT_FROMSPACE_DEPTH=800 PERRY_GC_SCHEDULE_ALLOC_KB=0 PERRY_GC_SCHEDULE_RATE=1 PERRY_GC_VERIFY_EVACUATION=1 on a fixture allocating between publication (main(), before any module __init) and reclamation (the entry's own preamble): retired_set=#8005, copying_minors=8006, moved_objects=8888, total_rewrites=2 for this slot specifically, PERRY_GC_VERIFY_EVACUATION did not panic, and every identity assertion (module-to-module and entry-to-dependency) held true throughout.
…on move A diagnostic counter nothing else reads rots silently: a future refactor that stops rewriting the cache would report total_rewrites=0 forever while looking perfectly healthy. This forces a real evacuating collection (via the same thread-local test overrides gc::tests::evacuation uses, not env vars, which would race every other test sharing the process) and asserts both that the placeholder's address changed and that the rewrite counter tracked it -- converting the diagnostic into a witness per CLAUDE.md's "a gate must assert its subject was live" rule. Lives in gc/tests/ rather than module_require.rs because a real evacuating minor needs CopyingNurseryTestGuard's preflight setup (generated write barriers reporting active, the conservative-full-scan test default turned off), which is private to that tree. The guard also clears the thread's scanner registry, so the test re-registers scan_cjs_main_module_root_mut explicitly before collecting. Added two minimal pub(crate) test accessors to module_require.rs for the cross-module read.
…eritage shape
Generalizes js_fetch_or_value_super (crates/perry-runtime/src/object/global_this/fetch_globals.rs)
to recognize Readable/Writable/Duplex/Transform reached through a local alias, namespace member,
indirect subclass, or CJS destructured require('stream') -- the same pattern #10621/#10634 already
fixed for AsyncResource/AsyncLocalStorage. PassThrough is deliberately left unhandled (separate,
deeper HIR-level gap; see code comment).
Fixes #10448
… source Deletes the perry-ext-axios crate, perry-stdlib's axios.rs shim, the js_axios_* FFI surface, and every NATIVE_MODULES/manifest/HIR/codegen special-case that existed only to route the native binding. A plain `import axios from "axios"` with no perry.compilePackages entry now resolves and compiles the real npm package (and its transitive deps) from source instead. Must not merge before #10673 (agent-base namespace/export= fallback fix) -- axios's https-proxy-agent dependency needs it to compile.
Renamed the axios-removal changeset fragment now that the PR number is known, and pointed PENDINGCS-compile-smoke-known.md's dangling placeholder reference at it.
This was referenced Sep 19, 2026
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (69)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This was referenced Sep 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merge train 226 — four PRs validated together as one tree, released as v0.5.1605.
Trains land as their own PR, so the source PRs are closed, not merged, and their close-keywords never fire. Issues resolved are listed at the bottom.
Contents
fix(codegen): skip thelocal_typesrefresh for box-captured localsfix(compile): publishrequire.mainonly for the entry modulefix(runtime): dispatchnode:streamsuper()through any bound-export heritage shaperefactor(stdlib): remove the axios native binding, compile the real package from source#10747 — a months-latent correctness bug, root-caused to one line
An array local captured by a nested closure read back
undefinedin its declaring scope, onceArray.prototypehad ever had an indexed property. Surfaced as a GC-named gap fixture; it is not a GC bug.PreallocateBoxesregisters the id up front, so the local's one realLethitslower_let's redeclaration branch, where0ed806587chad addedctx.local_types.insert(id, refined_ty). That records the value's type while the slot holds a box pointer, solocal_typesreaders lower reads as raw local loads instead ofjs_box_get_bits.Found by six stamp-verified bisect builds over merge train 219; subject-matter reasoning pointed at the wrong commit three times. A plain revert was measured and rejected — it fixes this and reds
test_gap_10488, moving the failure rather than clearing it. The guard usesctx.boxed_vars, already in scope.Full 871-fixture sweep: 864 pass, 7 pre-existing (six snapshot entries in their recorded state, plus #10730).
test_gap_10488at position 46 andtest_gap_10727at 59 — the two sides of the trade the revert couldn't satisfy, green in the same invocation.#10749 —
require.main === modulewas true in every compiled CJS moduleSo the standard "am I being run directly?" guard fired on a plain library import. Real instance:
import dotenv from "dotenv"(18.0.1) printed dotenv's CLI usage banner and exited 1.Two parts, the second non-obvious:
cjs_wraphoists a staticrequire('./relative')into an ESM import, and ESM evaluates static-import dependencies before the importing module's own top-level code. So a dependency readingrequire.mainat top level would see nothing published. A placeholder object is published frommain()before any__initand reclaimed by the entry, preserving identity.That placeholder is a heap object cached in a thread-local, held live across all module initialization. It is registered (
COVERED [core/T], no exemption needed) and verified moving, not merely surviving: 8006 forced collections, 8888 moved objects,total_rewrites=2for that slot,PERRY_GC_VERIFY_EVACUATION=1clean. A permanent test asserts the rewrite counter is non-zero under forced evacuation, so it is a witness rather than a diagnostic that can rot.Instruction count is reported as not resolvable above noise, with the naive +0.28% figure explicitly disproven by a 20-vs-40-dependency differential that came back with the wrong sign.
Counts re-derived on the assembled tree, not carried
workspace_architecture.py --checkon this tree: 78 members / externalize=29 / keep=44. Re-derived here rather than taken from either PR's recorded value — #10679 and #10691 both correctly recorded 78/29/44 against053b9ccac4, and whichever landed second would have been wrong.Worth recording the mechanism, because
MERGEABLEdoes not catch it: on #10691 the branch's earlier resolution and train 225's uuid removal had each independently rewrittenworkspace_membersto the identical value, so the rebase produced zero conflict markers and textually identical content — while that agreed number was stale. Two sides agreeing is normally evidence; here it is the failure. The defence cannot be "check whether the sides disagree"; it has to be re-derivation.Also green on the assembled tree: ledger 376 rows / 326 providers,
unrooted_local_shapeat 578, noperry-ext-axiosentries in the regenerated lock.Validation
Assembled on
053b9ccac4; source heads asserted unchanged since assembly — this train was re-rolled once when #10649's head moved mid-validation, caught before the cycle was spent. All nine cheap gates,cargo check --workspace --all-targetsunder-D warnings, the release build of all five pinned artifacts, every unit suite, and an 8-area gap sweep with zero unexplained regressions and every area asserted to have run a non-zero number of tests.lintcompleted its full 6-of-6 compile tier with nothing outside the known-red public-baseline step.Issues resolved
Closes #10727
Closes #10735
Closes #10448