Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
36b97c5
test(9466): gap fixture — same-name classes at different lexical depths
Sep 2, 2026
25e442e
test(9466): cover every block-kind scope — switch cases and loop bodies
Sep 2, 2026
c8434bb
test(9466): instanceof arms that actually discriminate — block bounda…
Sep 2, 2026
e76d3d5
fix(hir): disambiguate same-name classes per SCOPE, not per name (#9466)
Sep 2, 2026
4e46d14
test(9466): drop the loop-capture sub-arm — it discriminates a differ…
Sep 2, 2026
35d0c6f
test(9466): wording — the capture gap is reported, not yet filed
Sep 2, 2026
f572873
refactor(cjs-default): one shared table for the `<mod>.default` modul…
Sep 2, 2026
928eedd
test(gap): pin claude-code's MCP debug logger write shape (#9500)
Sep 2, 2026
8542bfa
test(gap): exec/execFile callbacks fire in completion order (#9500 pa…
Sep 2, 2026
7273b07
changelog: fragment for #9531 (#9500)
Sep 2, 2026
79754a4
fix(date): consume ISO parser tails (#9509)
Sep 2, 2026
6cd3f9e
fix(regex): spec-split fancy separators (#9438)
Sep 2, 2026
97e10f6
fix(codegen): strict `o.x += 1` on an inherited non-writable / access…
Sep 2, 2026
1da35b5
fix(codegen): root the Set receiver across the value in SetHas/SetDel…
Sep 2, 2026
f34ce01
changelog: #9532 Set receiver rooting fragment; bump 0.5.1521
Sep 2, 2026
df4bdd6
fix(runtime): inherit Error subclass names (#9440)
Sep 2, 2026
84e0269
fix(runtime): route #9511's raw ErrorHeader/StringHeader reads throug…
Sep 2, 2026
db9e145
fix(gates): split sloppy class-field stores from property_set.rs (fil…
Sep 2, 2026
9b7e1fa
fix: drop unused re-import after property_set split
Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions changelog.d/9438-fancy-regex-split.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
### Fixed

- **Regex separators that require fancy-regex now run the same
`RegExp.prototype[Symbol.split]` cursor algorithm as ordinary patterns.** The
old `find_iter` fallback emitted a trailing `""` after a zero-width match at
the end and discarded separator captures. The fancy lane now uses
`captures_from_pos` on the complete subject, performs the spec's sticky
`q`/`p` walk bounded by `q < size`, splices matched and unmatched captures,
and stops as soon as `limit` is reached.

Coverage includes lookbehind and lookahead separators, captures, start/end
matches, empty subjects, limits, and the multiline `^` / `$` forms rewritten
onto the fancy lane by #9427. The pre-existing runtime test that pinned the
incorrect trailing element now asserts Node's result.
59 changes: 59 additions & 0 deletions changelog.d/9466-scope-aware-class-disambiguation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
### Fixed

- **Same-name `class` declarations at different lexical depths are distinct
classes again — the inner body is no longer silently dropped.** Perry keeps
two same-named classes apart by registering the second under a uniquified key
(`M$0`); the key was minted once per **name** instead of once per **scope**, so
the third and every later `class M` aliased onto the second's ClassId. Whichever
body registered first won, and the program ran the wrong methods with no
diagnostic:

```ts
class M { v() { return "top"; } }
function h() {
class M { v() { return "outer"; } }
function h2() { class M { v() { return "inner"; } } return new M().v(); }
return [new M().v(), h2()].join(",");
}
console.log(new M().v(), h()); // node: top outer,inner perry: top outer,outer
```

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 saved and restored per body, so an enclosing body's
alias is live while the nested one lowers). A nested body declaring the same
name therefore took the early return and registered its `class X` under the
**outer** body's key. The map now carries the source span of the scope that
minted each alias, so the guard means "this scope already renamed it" — the
idempotence the guard existed for — and every nested scope mints its own.

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 ran the first's body:

```ts
{ class Blk { v() { return "b1"; } } console.log(new Blk().v()); } // b1
{ class Blk { v() { return "b2"; } } console.log(new Blk().v()); } // node b2, perry b1
```

`class` is block-scoped, so the scan is now bracketed at every `{ … }`-shaped
scope — bare block, `if` / `else` branch, loop body, `try` / `catch` /
`finally`, and `switch` — mirroring `register_block_forward_lexicals`
(#6062), which brackets the same boundary for TDZ names: record only what the
scope changed, undo exactly that, so an alias owned by an enclosing scope
survives.

This is an **identity** fix, not a naming one: each declaration now gets its own
ClassId, so `instanceof` across the shadowing boundary is correct in both
directions (an inner instance is not `instanceof` the outer class, and vice
versa), `Object.getPrototypeOf(inner) !== Outer.prototype`, and `class Sub
extends M` inside the inner scope extends the **inner** `M`. `.name` keeps
reporting the source name for all of them — the display-name override #9413
(PR #9465) installed on this exact path is what carries it, and every newly
minted alias goes through the same `lower_class_decl` site that records it.

Validated by `test-files/test_gap_9466_shadowed_class_identity.ts`, byte-identical
to `node --experimental-strip-types`.
96 changes: 96 additions & 0 deletions changelog.d/9495-strict-inherited-property-set-prototype-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
### Fixed

- **Strict `o.x += 1` against an INHERITED non-writable property, getter-only
accessor or setter now runs the prototype walk.**

```js
"use strict";
const proto = {};
Object.defineProperty(proto, "x", { value: 10, writable: false, configurable: true });
const a = Object.create(proto);
a.x += 1; // node: TypeError Perry: silent, created own a.x = 11

const g = {};
Object.defineProperty(g, "x", { get() { return 20; }, configurable: true });
const b = Object.create(g);
b.x += 1; // node: TypeError Perry: silent, created own b.x = 21

const calls = [];
const s = {};
Object.defineProperty(s, "x", { get() { return 30; }, set(v) { calls.push(v); }, configurable: true });
const d = Object.create(s);
d.x += 1; // node: setter runs with 31, no own property
// Perry: setter NEVER ran, created own d.x = 31
```

ES2024 §10.1.9.2 (`OrdinarySetWithOwnDescriptor`): when the receiver has no
own property, `[[Set]]` walks to the parent and the *parent's* descriptor
decides — a non-writable data property rejects, a getter-only accessor
rejects, a setter runs with the original receiver, and only a writable data
property (or the end of the chain) creates a new own property. `PutValue`
then throws on a rejection iff the reference is strict.

`o.x = v` was already right — it lowers to `Expr::PutValueSet` →
`js_put_value_set`, whose `ordinary_set_with_receiver` walks the chain — and
#9459 made the *sloppy* half of these spellings right as a side effect of
routing the sloppy `Expr::PropertySet` tail to that same entry. Only the
**strict** spellings that lower to `Expr::PropertySet` — compound and 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`) were still wrong. This is a missing prototype walk, not a
missing `Throw` flag: the opposite direction from #9422 and a different
defect from #9459.

Root cause: the strict tails ended in
`js_typed_feedback_object_set_field_by_name_fast` /
`js_typed_feedback_object_set_field_by_name` → `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 one of
these receivers fell to the slow branch — which appended an own property
without ever consulting the chain.

Fix: the strict tails now reach the same receiver-aware `[[Set]]` the sloppy
tails and the `=` lane use, `js_put_value_set(target, key, value, receiver,
strict)`, so the two modes are one tail distinguished by the `Throw` flag
alone. The typed-feedback `PropertySet` site moves with the store: it is
registered in both modes (it describes the store, not its strictness) and
observed by the pure-recording `js_typed_feedback_observe_property_set`,
compile-gated on `PERRY_TYPED_FEEDBACK` exactly as #7480 step 4 gates every
other recording helper — a default build emits the bare `js_put_value_set`
call and nothing else. Nothing was left for the old dispatching wrapper to
decide (the receiver-aware entry makes the fast-path choice itself), so the
#7480 "dispatching wrappers still emitted in a default build" gate is
re-pointed at the method-call dispatcher the same fixture emits, and asserted
as a *call* rather than a symbol (the symbol match was satisfied by the
`declare` line alone).

- `crates/perry-codegen/src/expr/property_set.rs` —
`lower_put_value_property_set_by_name` replaces the sloppy-only helper
and is the generic tail for both modes; `caller` / `arguments` keep their
`js_object_set_field_by_name` route (poisoned-accessor handling, unrelated
to either flag or walk). `emit_typed_feedback_property_set_observation`
carries the site.
- `crates/perry-codegen/src/expr/index_set.rs` —
`lower_object_index_set_put_value` replaces the sloppy-only helper on the
literal-string-key and string-typed-key object arms.
- `crates/perry-codegen/src/expr/typed_feedback.rs` —
`TypedFeedbackContract::put_value_set`.
- `test-files/test_gap_9495_strict_inherited_property_set.cts` — the three
inherited receivers plus a two-level chain, a class accessor on the chain
and a Proxy on the chain (receiver forwarded), across `+=`, `&&=`, `??=`
(short-circuit control), `for`-of heads, destructuring in statement and
expression position, `o["x"]`, `o[k]`, `o[anyKey]`, `[o[k]] = arr`, with
accepted-store controls (inherited writable data, new key beside an
inherited accessor, class-ref receiver) and the already-correct `=` /
`++` lanes — **both modes**, so "silent because sloppy" and "silent because
the walk never ran" are told apart by the setter-call log and `hasOwn`.
- `test-files/test_gap_9459_property_set_strictness.cts` — the strict
inherited twins are spelled `+=` now, as that file's comment promised.

Left as it was: `js_class_field_set_fallback` (the class-field arm's
guard-miss path) and `js_object_set_field_by_property_id` (the
computed-runtime-members class route) are still own-property stores; neither
is reachable for the receivers above without a class-typed variable holding
an `Object.create`d value, and each is its own lane. A DECLARED `static`
field losing `K.n += 1` in both modes (found while building the fixture) is
a static-slot lane defect, not a walk, and is filed as #9526.
17 changes: 17 additions & 0 deletions changelog.d/9509-date-parse-tail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
### Fixed

- **ISO-shaped date parsing now consumes the complete clock tail instead of
silently discarding it.** Space-separated `AM` / `PM`, the GMT family, and
V8's fixed `EST` / `EDT` / `CST` / `CDT` / `MST` / `MDT` / `PST` / `PDT`
table are applied to the parsed instant; the same zone words work on
date-only and partial `YYYY` / `YYYY-MM` forms. Missing day/month components,
repeated whitespace, numeric offsets, and parenthesized comments retain
Node's measured behavior.

Every other suffix must now make the parse fail. In particular,
`"2026-09-01 10:30GMT"`, `"...10:30EST"` and `"...10:30PM"` are Invalid
Date, matching Node, rather than plausible but wrong local instants produced
from the `HH:MM` prefix. The expanded #9449 parity fixture and focused
runtime tests cover both 12-hour boundaries, zone-plus-meridiem, all eight US
abbreviations, partial dates, date-only zone words, and invalid attached or
unknown tails with host-zone-independent assertions.
9 changes: 9 additions & 0 deletions changelog.d/9511-error-name-ownership.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
### Fixed

- **Error subclasses no longer expose their default `name` as an own,
enumerable property.** `name` now remains on the appropriate Error-family
prototype until user code explicitly assigns it, matching Node across
`JSON.stringify`, `Object.getOwnPropertyNames`, `Object.keys`, `for…in`,
object spread, property descriptors, and `util.inspect`. Error construction
also preserves Node's observable own-key order: `stack` precedes an optional
`message`.
25 changes: 25 additions & 0 deletions changelog.d/9531-cjs-default-table-mcp-logger-exec-order.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
**One shared table for the CommonJS-default module set; claude-code's MCP
debug logger shape and exec/execFile callback order pinned (#9500).**

The set of Node builtins whose `require()` / default import hands out a
distinct `<mod>.default` namespace was hand-maintained in five places (two of
them inside the HIR alone, already disagreeing on `ffi`, `inspector`,
`inspector/promises` and `wasi`); the method-call router's copy is the one
that drifted far enough to break `require('child_process').spawn` (#9485,
#9498). The table now lives once in `perry-dispatch`, built from one literal
per module, and the runtime's property-read and method-call paths, the
`default`-export resolver and the HIR's import lowering all derive from it.
Adding a module is one line; tests pin the table's shape, the HIR's
classification of every row, and the router test's list against the table in
both directions. No behaviour change.

Two fixtures pin the issue's other findings. The MCP debug logger's exact
write shape — the `using`-downlevel fs wrapper, the timer/dispose buffered
writer, the graceful-shutdown cleanup set and the `appendFileSync` → ENOENT →
`mkdirSync(recursive)` recovery arm that is the only code creating the log
tree — is byte-compared to node; it fails on a pre-#9491 build (the append
did not throw, so the tree was never created) and passes on main. The
exec/execFile callback order is pinned as what node guarantees — completion
order, whichever API launched the child or came first; the inverted order for
two instant `echo`s is a same-turn batch-delivery artefact node flips with
submission order, not a rule.
37 changes: 37 additions & 0 deletions changelog.d/9532-set-receiver-root-across-value.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
### Fixed

- **`s.has(makeKey())` / `s.delete(makeKey())` on a module-level `Set` no
longer read a moved receiver.** `Expr::SetHas` and `Expr::SetDelete`
(`expr/bigint_set.rs`) lowered the receiver, masked it to a raw `i64`
handle, *then* lowered the value expression, and consumed the handle after
— the #6970 shape their Map twins (`MapGet` / `MapHas` / `MapDelete`) were
fixed for, found live by #9522's audit of every Map/Set/WeakMap lowering
(#9523). A function-local Set was already covered — `root_reload` re-derives
a shadow-slot load and its unmask below every collection point — but a
module-level Set is a `@perry_global_*` load, which that pass deliberately
does not reload, so an evacuating minor inside the value's evaluation left
`js_set_has` a from-space header. Measured: the new gap fixture SIGSEGVs on
unfixed main where node prints `bad=0`; the from-space quarantine reports the
fault address as retired by the first minor with a last-known object of
`GC_TYPE_SET`.

Both arms now root the receiver in a `RootedGroup` before the value is
lowered and re-read it from the slot afterwards, exactly as the Map twins
do; when the value cannot collect nothing is pushed and the IR is unchanged.
`bigint_set.rs` joins the rooting migration ledger.

- **`this.field.set(a, 1).set(b, 2)` consumes the receiver `js_map_set`
returns.** `lower_call/property_get/map_set.rs`'s `"set"` arm called the
helper as `void` and returned the receiver box it had read *before* the
call. `js_map_set` returns the receiver as it stands after the insert — for
a `class X extends Map` instance the runtime roots the movable
`ObjectHeader` across the grow and hands back the relocated address — so the
chained call could dispatch on a from-space pointer. The arm now re-boxes
the returned pointer, as `Expr::MapSet` already did. Latent (the minor must
fire inside the first insert's grow); pinned by a chained-set fixture.

Fixtures: `test-files/test_gap_9523_set_receiver_roots_across_value.ts`
(fails on unfixed main, byte-identical to node fixed) and
`test_gap_9523_map_set_chain_returns_receiver.ts`; codegen contract in
`temp_root_coverage/set_receiver.rs`, sabotage-verified under both root
lowerings.
9 changes: 1 addition & 8 deletions crates/perry-codegen/src/codegen/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -978,17 +978,10 @@ pub(super) fn compile_method(
.cloned()
.map(|slot| ctx.block().load(DOUBLE, &slot))
.unwrap_or_else(|| undef_lit.clone());
let kind_idx = ctx.strings.intern(&pname_owned);
let kind_handle_global =
format!("@{}", ctx.strings.entry(kind_idx).handle_global);
let blk = ctx.block();
let kind_box = blk.load(DOUBLE, &kind_handle_global);
let kind_bits = blk.bitcast_double_to_i64(&kind_box);
let kind_raw =
blk.and(I64, &kind_bits, crate::nanbox::POINTER_MASK_I64);
blk.call_void(
"js_error_subclass_default_init",
&[(DOUBLE, &this_box), (DOUBLE, &msg_box), (I64, &kind_raw)],
&[(DOUBLE, &this_box), (DOUBLE, &msg_box)],
);
}
("".to_string(), 0)
Expand Down
Loading
Loading