Skip to content

chore: merge train 226 (v0.5.1605) - #10751

Merged
proggeramlug merged 13 commits into
mainfrom
train226r
Sep 19, 2026
Merged

proggeramlug merged 13 commits into
mainfrom
train226r

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

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

PR Change
#10747 fix(codegen): skip the local_types refresh for box-captured locals
#10749 fix(compile): publish require.main only for the entry module
#10649 fix(runtime): dispatch node:stream super() through any bound-export heritage shape
#10679 refactor(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 undefined in its declaring scope, once Array.prototype had ever had an indexed property. Surfaced as a GC-named gap fixture; it is not a GC bug. PreallocateBoxes registers the id up front, so the local's one real Let hits lower_let's redeclaration branch, where 0ed806587c had added ctx.local_types.insert(id, refined_ty). That records the value's type while the slot holds a box pointer, so local_types readers lower reads as raw local loads instead of js_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 uses ctx.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_10488 at position 46 and test_gap_10727 at 59 — the two sides of the trade the revert couldn't satisfy, green in the same invocation.

#10749require.main === module was true in every compiled CJS module

So 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_wrap hoists a static require('./relative') into an ESM import, and ESM evaluates static-import dependencies before the importing module's own top-level code. So a dependency reading require.main at top level would see nothing published. A placeholder object is published from main() before any __init and 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=2 for that slot, PERRY_GC_VERIFY_EVACUATION=1 clean. 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 --check on 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 against 053b9ccac4, and whichever landed second would have been wrong.

Worth recording the mechanism, because MERGEABLE does not catch it: on #10691 the branch's earlier resolution and train 225's uuid removal had each independently rewritten workspace_members to 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_shape at 578, no perry-ext-axios entries 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-targets under -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. lint completed its full 6-of-6 compile tier with nothing outside the known-red public-baseline step.

Issues resolved

Closes #10727
Closes #10735
Closes #10448

Ralph Küpper and others added 13 commits September 19, 2026 19:23
#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.
@proggeramlug
proggeramlug merged commit 91a566c into main Sep 19, 2026
22 of 24 checks passed
@proggeramlug
proggeramlug deleted the train226r branch September 19, 2026 19:11
@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 99fd914f-c91f-4a0c-bf3c-01c61a507b4e

📥 Commits

Reviewing files that changed from the base of the PR and between 053b9cc and f61b4cc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (69)
  • CLAUDE.md
  • Cargo.toml
  • changelog.d/10649-stream-subclass-heritage.md
  • changelog.d/10679-axios-native-binding-removal.md
  • changelog.d/10747-box-captured-local-types.md
  • changelog.d/10749-require-main-entry-only.md
  • changelog.d/PENDINGCS-compile-smoke-known.md
  • crates/perry-api-manifest/src/emit.rs
  • crates/perry-api-manifest/src/entries.rs
  • crates/perry-api-manifest/src/entries/part_4.rs
  • crates/perry-codegen/src/codegen/entry.rs
  • crates/perry-codegen/src/lower_call/native/native_runtime_branch.rs
  • crates/perry-codegen/src/lower_call/options/fetch.rs
  • crates/perry-codegen/src/runtime_decls/objects.rs
  • crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs
  • crates/perry-codegen/src/runtime_decls/stdlib_ffi/third_party.rs
  • crates/perry-codegen/src/stmt/let_stmt.rs
  • crates/perry-ext-axios/Cargo.toml
  • crates/perry-ext-axios/src/lib.rs
  • crates/perry-hir/src/destructuring/var_decl/native_new.rs
  • crates/perry-hir/src/js_transform/local_natives.rs
  • crates/perry-hir/src/lower/expr_call/globals.rs
  • crates/perry-hir/src/lower/module_decl.rs
  • crates/perry-hir/src/lower/stmt.rs
  • crates/perry-hir/tests/axios_response_property_lowering.rs
  • crates/perry-hir/tests/unimplemented_api_check.rs
  • crates/perry-runtime/src/closure/mod.rs
  • crates/perry-runtime/src/closure/v8_stubs.rs
  • crates/perry-runtime/src/gc/mod.rs
  • crates/perry-runtime/src/gc/tests/cjs_main_module.rs
  • crates/perry-runtime/src/gc/tests/mod.rs
  • crates/perry-runtime/src/module_require.rs
  • crates/perry-runtime/src/object/global_this/fetch_globals.rs
  • crates/perry-stdlib/Cargo.toml
  • crates/perry-stdlib/src/axios.rs
  • crates/perry-stdlib/src/common/dispatch/property_dispatch.rs
  • crates/perry-stdlib/src/lib.rs
  • crates/perry-ui-android/src/stdlib_stubs.rs
  • crates/perry/src/commands/compile/cjs_wrap/parcel_watcher_tests.rs
  • crates/perry/src/commands/compile/cjs_wrap/preamble_canary_tests.rs
  • crates/perry/src/commands/compile/cjs_wrap/tests.rs
  • crates/perry/src/commands/compile/cjs_wrap/wrap.rs
  • crates/perry/src/commands/compile/collect_modules.rs
  • crates/perry/src/commands/compile/optimized_libs/freshness.rs
  • crates/perry/src/commands/sandbox_profile.rs
  • crates/perry/src/commands/stdlib_features.rs
  • crates/perry/well_known_bindings.toml
  • docs/api/perry.d.ts
  • docs/native-libraries.md
  • docs/src/api/reference.md
  • docs/src/cli/commands.md
  • docs/src/native-libraries/governance.md
  • scripts/gc_runtime_root_holders.json
  • test-files/gap_10448_stream_subclass_heritage_helper.cjs
  • test-files/gap_10735_require_main_cli_guard.cjs
  • test-files/gap_10735_require_main_deep.cjs
  • test-files/gap_10735_require_main_dep.cjs
  • test-files/gap_10735_require_main_dep2.cjs
  • test-files/gap_10735_require_main_esm_dep.cjs
  • test-files/test_ffi_surface_runtime_core.ts
  • test-files/test_ffi_surface_stdlib_core.ts
  • test-files/test_gap_10448_stream_subclass_heritage.ts
  • test-files/test_gap_10727_captured_array_local_proto_index.ts
  • test-files/test_gap_10735_require_main_entry.cts
  • test-files/test_gap_10735_require_main_esm_entry.ts
  • test-files/test_issue_340_axios_response_props.ts
  • test-parity/known_failures.json
  • tests/release/packages/axios-get/package.json
  • workspace-architecture.json
 ________________________________________________________________
< I like what you did here. I don't like *that* you did it here. >
 ----------------------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment