Merge train: #9527 #9531 #9532 #9533 #9534(#9495) #9511 - #9544
Conversation
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`).
…ete; return js_map_set's receiver (#9523)
…h the rooting combinators
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (63)
📝 WalkthroughWalkthroughThis 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. ChangesFancy-regex splitting
Scope-aware class identity
Property assignment and receiver rooting
Error construction and formatting
CommonJS default namespaces and async fixtures
Date parsing tails
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: Suggested reviewers: ✨ 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 |
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
String.prototype.split()compatibility, including captures, zero-width matches, limits, and anchors.Errorsubclass names, property ordering, and inspection output.Mapchaining andSetoperations during garbage collection.