Skip to content

Merge train: #9527 #9531 #9532 #9533 #9534(#9495) #9511 - #9544

Merged
proggeramlug merged 19 commits into
mainfrom
land-train71
Sep 2, 2026
Merged

Merge train: #9527 #9531 #9532 #9533 #9534(#9495) #9511#9544
proggeramlug merged 19 commits into
mainfrom
land-train71

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Lands the validated train: #9527 (scope-aware class ClassIds, #9466), #9531 (shared cjs-default table + MCP logger/exec-order pins, #9500), #9532 (Set receiver rooting + js_map_set receiver, #9523 — version-bump hunks stripped per convention), #9533 (date tails + fancy-regex split, #9509/#9438), #9534's #9495 commit (strict inherited-property-set prototype walk; its stacked #9459/#9460 commits are already on main), and #9511 (inherit Error subclass names, #9440) with its 16 raw-handle sites converted to the rooting combinators.

Train-only additions: property_set.rs file-cap split (sloppy class-field stores → child module) and an unused-import fix.

Validation (whole train as one tree): release build green; RUST_TEST_THREADS=1 perry-runtime 2986+ green; perry-codegen/hir/dispatch green; perry-stdlib 123/123 single-threaded (2 known parallel-globals flakes at default threads, #1444 class); all lint gates green; 11/11 train gap fixtures byte-identical to node.

Rebase-merge to preserve per-commit authorship of the original PR authors.

Summary by CodeRabbit

  • Bug Fixes
    • Improved regex-based String.prototype.split() compatibility, including captures, zero-width matches, limits, and anchors.
    • Corrected ISO date parsing for time zones, AM/PM, offsets, partial dates, and invalid trailing text.
    • Fixed inherited-property behavior for strict and compound assignments.
    • Corrected Error subclass names, property ordering, and inspection output.
    • Preserved Map chaining and Set operations during garbage collection.
    • Distinguished same-name classes in nested scopes.
    • Improved CommonJS built-in imports and child-process callback ordering.

Ralph Küpper and others added 19 commits September 2, 2026 15:51
Demonstrates the aliasing on unfixed origin/main: three depths, sibling
module-top blocks, sibling blocks in a function, sibling functions,
if/else + try/catch/finally + loop bodies, a shadowed class captured in a
closure called after its block exits, instanceof across the shadowing
boundary, .name (#9413 regression guard), and subclassing a shadowed class.

Claude-Session: https://claude.ai/code/session_014knX724SYDogwzsXybCGxp
The rename scan was missing from ALL {}-shaped scopes, not just bare
blocks, so the fixture now names each kind: bare block, if/else, loop
body, try/catch/finally, and both switch forms (a bare case
statement-list, which shares one switch block scope, and a braced case,
which is its own). Two loop arms pin the span-keyed semantics: one
declaration site is ONE class across iterations, its closures outlive
the loop, and a per-iteration capture still gets three environments.

Claude-Session: https://claude.ai/code/session_014knX724SYDogwzsXybCGxp
…ry + third depth

Arm 4's instanceof rows sit at TWO-scope depth, which the name-keyed
disambiguation already handled, so they pass before and after: they guard
the fix but do not demonstrate the gap. These two do — a block-scoped
class (never lowered at all before the fix, so its instances were
instances of the OUTER class) and a third-depth one (aliased onto the
second's ClassId).

Claude-Session: https://claude.ai/code/session_014knX724SYDogwzsXybCGxp
Same-name `class` declarations at different lexical depths aliased onto
one ClassId and the inner body was silently dropped — wrong code, no
diagnostic.

Two defects, one symptom.

1. The "already renamed" guard was per NAME, not per SCOPE.
   `maybe_rename_colliding_class` returned early on
   `class_renames.contains_key(name)`. But `class_renames` is INHERITED by
   nested bodies — it is snapshotted and restored per body, so an enclosing
   body's alias is live while a nested one lowers. A nested body declaring
   the same name therefore took that early return and registered its
   `class X` under the OUTER body's key. Third and later occurrences shared
   one ClassId; whichever body lowered first won.

   The map value now carries the source span of the scope that minted the
   alias, so the guard means "THIS scope already renamed it" — the
   idempotence the guard existed for — while every nested scope mints its
   own. That span key is also what makes it safe to hook the scan at more
   than one funnel: a function body is scanned twice (Phase-1.5, then
   `lower_block_stmt`) and the matching key makes the second a no-op.
   Without it the second alias would strand that body's end-of-body capture
   re-registration on a stale key — the 2026-07-02 audit P0 that
   `capture_rereg_renamed_class.rs` guards.

2. Block scopes never ran the disambiguation scan at all. Only function
   bodies did, so two sibling `{ class Blk { … } }` blocks shared one
   ClassId and the second was never lowered. Measured on unfixed main, ALL
   of these ran the outer class: bare blocks (module-level and in-function),
   `if`/`else` branches, loop bodies, `try`/`catch`/`finally`, and both
   switch forms.

   `class` is block-scoped, so `enter_class_rename_scope` /
   `exit_class_rename_scope` now bracket every `{ … }`-shaped scope,
   deliberately mirroring `register_block_forward_lexicals` (#6062), which
   brackets the same boundary for TDZ names: record only what this scope
   changed, undo exactly that, so an alias owned by an enclosing scope
   survives. `lower_block_stmt` is the funnel `rebind_nested_forward_scope_lets`
   already documents for those scopes; the strict-mode branch of
   `lower_block_stmt_scoped` bypasses it, and switch case statement-lists
   are not `BlockStmt`s, so both take the bracket explicitly.

This is an identity fix, not a naming one: each declaration gets its own
ClassId, so `instanceof` across the shadowing boundary is right in both
directions, `Object.getPrototypeOf` disagrees with the outer prototype, and
`class Sub extends M` inside the inner scope extends the INNER `M`. `.name`
keeps reporting the source name — the #9413 (PR #9465) display-name
override lives on the same `lower_class_decl` site every new alias flows
through, so it composes for free.

Fixture: test-files/test_gap_9466_shadowed_class_identity.ts.

Claude-Session: https://claude.ai/code/session_014knX724SYDogwzsXybCGxp
…ent bug

`class Cp { v(){ return "cp" + i } }` in a loop body prints cp3,cp3,cp3
instead of cp0,cp1,cp2 — but that reproduces with NO name shadowing
anywhere and is byte-identical before and after this fix, so it is the
class-capture snapshot mechanism (one RegisterClassCaptures per class,
refreshed at assignments and returns; a loop body has neither), not class
identity. Filed separately; the fixture keeps discriminating one thing.

Claude-Session: https://claude.ai/code/session_014knX724SYDogwzsXybCGxp
…e set (#9500)

The set of Node builtins whose CommonJS `module.exports` is a distinct
`<mod>.default` namespace was hand-maintained in five places: the runtime's
`cjs_default_base_module` and `cjs_default_namespace_name` tables, the
`cjs_default_export_value` match arm, the method-call router's list (which
drifted far enough to break `require('child_process').spawn` — #9485/#9498),
and the HIR's `is_cjs_style_native_default_import`, itself duplicated in
`module_decl/native_default_import.rs` and `lower_expr/helpers.rs` with the
two copies already disagreeing (`ffi`, `inspector`, `inspector/promises`,
`wasi` missing from the latter).

Move the table to `perry-dispatch` (the crate both perry-hir and
perry-runtime already depend on) as `CJS_DEFAULT_NAMESPACE_MODULES`, built
by a macro from one literal per module so the two spellings cannot disagree,
and derive every consumer from it:

- runtime: `cjs_default_base_module` / `cjs_default_namespace_name` become
  views over the table; the `cjs_default_export_value` wildcard arm is a
  guard on `has_cjs_default_namespace` (the explicit arms before it — the
  callable/plain-namespace defaults — keep winning, so behaviour is
  unchanged);
- HIR: one predicate, derived from the table plus the spelled-out
  differences (`events` and the `sys`/`path/posix`/`path/win32` aliases are
  CJS-style; `node-pty`/`process`/`repl`/`sea` stay on the namespace-object
  default), used by both former call sites;
- tests pin the table's shape, the HIR classification of every row, and
  that the router test's spelled-out list equals the table in both
  directions.

perry-runtime gains perry-dispatch as a regular dependency (it was
build-only); the crate has no dependencies of its own.

Claude-Session: https://claude.ai/code/session_0184JRgBs978K6X7qKJFB4Hp
De-minified from the cc bundle: the `using`-downlevel fs wrapper (error
stashed by a catch-block `var`, re-thrown from `finally`), the 1 s-timer /
size / dispose buffered writer, the cleanup set awaited by graceful
shutdown before `process.exit`, and the `try { appendFileSync } catch {
mkdirSync(recursive); appendFileSync }` recovery arm that is the only code
creating the log directory tree. Byte-compared to node.

Claude-Session: https://claude.ai/code/session_0184JRgBs978K6X7qKJFB4Hp
…rt 2)

The issue's inverted exec→execFile order for two instant echos is a same-turn
batch-delivery artefact (node itself flips it with submission order); the
property both engines actually guarantee — a child that finishes first calls
back first, whichever API launched it — is what this pins.

Claude-Session: https://claude.ai/code/session_0184JRgBs978K6X7qKJFB4Hp
…or property walks the prototype chain (#9495)

The strict `Expr::PropertySet` tail (`o.x += 1`, logical assignment, `for`-of
heads, expression-position destructuring targets) and the strict object-by-name
arms of `Expr::IndexSet` (`o["x"] += 1`, `o[k] += 1`) ended in
`js_typed_feedback_object_set_field_by_name{,_fast}` -> `js_object_set_field_by_name`,
an OWN-property store. The shape-transition fast path inside it already declines
any receiver whose prototype is not the ordinary one, so every inherited
non-writable data property, getter-only accessor and setter fell to the slow
branch, which appended an own property without consulting the chain: no
TypeError, the setter never ran, and an own property materialised where ES2024
§10.1.9.2 creates none.

Route both tails through the receiver-aware `[[Set]]` that `o.x = v` and (since
#9459) the sloppy spellings already use -- `js_put_value_set(target, key, value,
receiver, strict)` -- so the two modes are one tail distinguished by the Throw
flag alone. `caller`/`arguments` keep their `js_object_set_field_by_name` route
(poisoned-accessor handling). The typed-feedback `PropertySet` site moves with
the store: registered in both modes, observed by the pure-recording
`js_typed_feedback_observe_property_set` under PERRY_TYPED_FEEDBACK only, per
#7480 step 4; a default build emits the bare `js_put_value_set` call. The #7480
"dispatching wrappers still emitted in a default build" gate is re-pointed at
the property-get and method-call dispatchers the same fixture emits, as calls.

Fixture: test_gap_9495_strict_inherited_property_set.cts (both modes; 18 strict
lines diverged on the unfixed branch, byte-identical after), and the strict
inherited twins in test_gap_9459_property_set_strictness.cts are spelled `+=`.
Found and filed separately: #9526 (declared static field loses `K.n += 1`).
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 88fa6d7a-75e9-4873-80e7-1631502e595f

📥 Commits

Reviewing files that changed from the base of the PR and between ad8fcaa and 9b7e1fa.

📒 Files selected for processing (63)
  • changelog.d/9438-fancy-regex-split.md
  • changelog.d/9466-scope-aware-class-disambiguation.md
  • changelog.d/9495-strict-inherited-property-set-prototype-walk.md
  • changelog.d/9509-date-parse-tail.md
  • changelog.d/9511-error-name-ownership.md
  • changelog.d/9531-cjs-default-table-mcp-logger-exec-order.md
  • changelog.d/9532-set-receiver-root-across-value.md
  • crates/perry-codegen/src/codegen/method.rs
  • crates/perry-codegen/src/expr/bigint_set.rs
  • crates/perry-codegen/src/expr/index_set.rs
  • crates/perry-codegen/src/expr/property_set.rs
  • crates/perry-codegen/src/expr/property_set/sloppy_class_field.rs
  • crates/perry-codegen/src/expr/this_super_call.rs
  • crates/perry-codegen/src/expr/typed_feedback.rs
  • crates/perry-codegen/src/lower_call/new_error_init.rs
  • crates/perry-codegen/src/lower_call/property_get/map_set.rs
  • crates/perry-codegen/src/rooting/mod.rs
  • crates/perry-codegen/src/runtime_decls/objects.rs
  • crates/perry-codegen/src/temp_root_coverage/mod.rs
  • crates/perry-codegen/src/temp_root_coverage/set_receiver.rs
  • crates/perry-codegen/tests/native_proof_regressions.rs
  • crates/perry-codegen/tests/typed_feedback.rs
  • crates/perry-codegen/tests/typed_shape_descriptors.rs
  • crates/perry-dispatch/src/cjs_default_modules.rs
  • crates/perry-dispatch/src/lib.rs
  • crates/perry-hir/src/lower/context.rs
  • crates/perry-hir/src/lower/expr_function.rs
  • crates/perry-hir/src/lower/lower_expr/helpers.rs
  • crates/perry-hir/src/lower/lowering_context.rs
  • crates/perry-hir/src/lower/module_decl.rs
  • crates/perry-hir/src/lower/module_decl/native_default_import.rs
  • crates/perry-hir/src/lower/stmt.rs
  • crates/perry-hir/src/lower_decl/block.rs
  • crates/perry-hir/src/lower_decl/body_stmt.rs
  • crates/perry-hir/src/lower_decl/mod.rs
  • crates/perry-runtime/Cargo.toml
  • crates/perry-runtime/src/builtins/formatting.rs
  • crates/perry-runtime/src/builtins/formatting/errors.rs
  • crates/perry-runtime/src/date/parse.rs
  • crates/perry-runtime/src/date/tests.rs
  • crates/perry-runtime/src/object/class_constructors.rs
  • crates/perry-runtime/src/object/class_meta_registry.rs
  • crates/perry-runtime/src/object/descriptors.rs
  • crates/perry-runtime/src/object/field_get_set/accessors.rs
  • crates/perry-runtime/src/object/global_this/fetch_globals.rs
  • crates/perry-runtime/src/object/mod.rs
  • crates/perry-runtime/src/object/native_module.rs
  • crates/perry-runtime/src/object/native_module_dispatch.rs
  • crates/perry-runtime/src/regex.rs
  • crates/perry-runtime/src/regex/tests.rs
  • crates/perry-runtime/src/string/mod.rs
  • crates/perry-runtime/src/string/split.rs
  • test-files/test_gap_9410_error_subclass_stack.ts
  • test-files/test_gap_9438_fancy_regex_split.ts
  • test-files/test_gap_9440_error_name_ownership.ts
  • test-files/test_gap_9459_property_set_strictness.cts
  • test-files/test_gap_9466_shadowed_class_identity.ts
  • test-files/test_gap_9495_strict_inherited_property_set.cts
  • test-files/test_gap_9500_exec_callback_completion_order.ts
  • test-files/test_gap_9500_mcp_debug_logger_shape.ts
  • test-files/test_gap_9523_map_set_chain_returns_receiver.ts
  • test-files/test_gap_9523_set_receiver_roots_across_value.ts
  • test-files/test_gap_date_iso_datetime_local_9449.ts

📝 Walkthrough

Walkthrough

This PR updates Perry runtime and compiler behavior for fancy-regex splitting, scoped class identity, inherited property assignment, receiver rooting, Error construction and formatting, CommonJS default namespaces, date parsing, and related regression coverage.

Changes

Fancy-regex splitting

Layer / File(s) Summary
Capture-aware split implementation
crates/perry-runtime/src/string/*, crates/perry-runtime/src/regex*, test-files/test_gap_9438_fancy_regex_split.ts
Fancy-regex splitting now preserves captures, handles zero-width matches, applies limits, and follows boundary behavior.

Scope-aware class identity

Layer / File(s) Summary
Scoped class rename tracking
crates/perry-hir/src/lower/...
Class aliases now use lexical scope keys and are restored across blocks, functions, and switch cases.
Class identity regression coverage
test-files/test_gap_9466_shadowed_class_identity.ts
Tests cover nested and sibling classes, closures, inheritance, switches, loops, and instanceof.

Property assignment and receiver rooting

Layer / File(s) Summary
Receiver-aware property stores
crates/perry-codegen/src/expr/{property_set.rs,index_set.rs,typed_feedback.rs}, test-files/test_gap_9495_strict_inherited_property_set.cts
By-name and string-key stores now use receiver-aware [[Set]] semantics with strict-mode flags and typed-feedback observations.
Set receiver rooting
crates/perry-codegen/src/expr/bigint_set.rs, crates/perry-codegen/src/temp_root_coverage/*, test-files/test_gap_9523_set_receiver_roots_across_value.ts
Set receivers are rooted across collecting value evaluation and re-read afterward.
Map.set receiver return
crates/perry-codegen/src/lower_call/property_get/map_set.rs, test-files/test_gap_9523_map_set_chain_returns_receiver.ts
Map.set now returns the post-call receiver pointer for safe chaining after relocation.

Error construction and formatting

Layer / File(s) Summary
Error subclass construction
crates/perry-codegen/src/{codegen/method.rs,expr/this_super_call.rs,lower_call/new_error_init.rs}, crates/perry-runtime/src/object/*
Error subclasses keep name inherited, capture stack before message, root values across allocations, and use a two-argument default initializer.
Error metadata and formatting
crates/perry-runtime/src/builtins/formatting*, crates/perry-runtime/src/object/{class_meta_registry.rs,descriptors.rs}
Error prototype lookup, own-key enumeration, inspect formatting, subclass headlines, and property quoting were updated.

CommonJS default namespaces and async fixtures

Layer / File(s) Summary
Shared CJS default table
crates/perry-dispatch/src/*, crates/perry-hir/src/lower/module_decl/*, crates/perry-runtime/src/object/native_module*
A shared module table now drives runtime namespace resolution and HIR import classification.
Async and logger regression fixtures
test-files/test_gap_9500_*
Tests cover exec completion order and MCP logger buffering, cleanup, directory recovery, and output.

Date parsing tails

Layer / File(s) Summary
Date tail parsing
crates/perry-runtime/src/date/*, test-files/test_gap_date_iso_datetime_local_9449.ts
ISO-shaped parsing now consumes clock tails, named zones, offsets, AM/PM, partial dates, and invalid suffixes.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • PerryTS/perry#5874: Covers related runtime and code-generation behavior for instanceof, Error subclasses, namespace imports, and object properties.

Suggested labels: type:bug

Suggested reviewers: thehypnoo

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch land-train71

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant