Skip to content

fix(hir): statement-semantics test262 tail (v2)#4861

Merged
proggeramlug merged 2 commits into
mainfrom
language-statements-v2-parity
Jun 10, 2026
Merged

fix(hir): statement-semantics test262 tail (v2)#4861
proggeramlug merged 2 commits into
mainfrom
language-statements-v2-parity

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

language/statements/* test262 parity 83.1% → 92.8% (1037 → 1158 pass of 1248 judged, +121), zero regressions — verified per-round across 8 incremental sweeps against the branch-point baseline, plus a final broad --shard 0/12 gate over built-ins language (main 89.4% → branch 89.7%, +8 bonus fixes outside the target dirs, 0 regressions).

Per-directory failures before → after: function 56→19, for-of 41→18, try 20→6, variable 17→2, for-in 16→8, with 12→4, switch 10→3, for 9→4, while 5→1 (let/const/labeled/for-await-of: remaining failures are the deferred TDZ/async categories below).

Root causes fixed

  1. var hoisting out of nested blocks (~30): module-level lowering never pre-registered vars declared inside blocks/loops/try, so reads before/after the block threw ReferenceError. New pre-register pass + an explicit undefined-initialised slot Let at entry — codegen materialises local storage at the first Stmt::Let for an id, so a read compiled earlier baked in an undefined constant and never observed later writes (while(1){if(c)break; var c=1} looped forever). Same fix in the function-body hoist pass. Also dropped an expr_assign guard that made x = x before a hoisted top-level var x throw.

  2. switch dispatch (8): case clauses written after default: were unreachable — the if-tower chained through the default's unconditional jump. Default is now excluded from the test chain. Case comparison goes through a new js_switch_strict_equals (string content compare incl. SSO, IEEE numeric compare so case NaN never matches and int32-boxed 1 equals raw 1.0, bit identity otherwise) — the old path coerced numbers to property-key strings, so switch(1) matched case '1'.

  3. for-of live iteration (12): typed arrays were snapshotted via ArrayFrom (mutations during the loop invisible) — now iterated live through the typed-array accessors. The proven-array index loop's length-hoist peephole fired on the desugar's __arr_N holder (an alias of the user local), so arr.push() in the body couldn't invalidate it; the peephole is now skipped for desugared holders.

  4. strict-mode eval early errors + fn property poisoning (~33): literal direct-eval bodies are parsed and scanned for strict violations (assigning/binding/naming eval/arguments, duplicate params; strictness from the calling context, the source's own directive, function-body directives, and nested new Function(p, body) literal bodies) and throw SyntaxError at the eval call, ahead of the const-fold. fn.caller/fn.arguments assignment now throws for all closures and function declarations; 'caller' in fn no longer runs the poisoned getter.

  5. for-in / for-of head targets + catch params (~16): member-expression heads (for (x.y of …)), parenthesised idents, bare-ident heads (assign the outer binding, don't mint a shadow), destructuring-assignment heads, and destructuring decl heads with defaults/rest/nested patterns — the previous inline walks silently skipped non-Ident pattern elements, leaving body references unbound (the test262 scope-* probe closures). All now routed through shared predefine_for_head / for_head_binding_stmts / emit_for_of_pattern_binding. Destructured catch params bound the same way. const heads throw TypeError on body assignment.

  6. try/catch/finally (3): a throw escaping a catch body skipped the finally. The catch body now runs under its own setjmp frame; the finally runs and its own abrupt completion supersedes the pending exception.

  7. with statement (6): implicit globals minted by with-set fallbacks are module-scoped HOLE-sentinel slots — the binding materialises only if the fallback actually fires at runtime (when the env owns the property, the write goes there), so a post-with read throws ReferenceError iff the global never came to exist (fixes S13.2.2_A19 and 12.10-0-7 simultaneously). var x = v inside with PutValues through the env when it owns x. HasBinding probes after RHS evaluation, matching V8.

  8. inliner parameter aliasing (cross-cutting correctness bug): a callee body that writes a parameter (function f(a){ a++ }) had the param substituted with the caller's LocalGet, so f(x) mutated the caller's x. Mutated params are now always materialised as a fresh copy Let (new collect_mutated_local_ids).

Plus: iterator-protocol next()/return() results validated (non-object → TypeError) in the lazy for-of loop.

Files

  • perry-hir: lower/lower_module_fn.rs, lower/stmt.rs, lower/stmt_loops.rs, lower/for_head.rs (new), lower/expr_assign.rs, lower/lower_expr.rs, lower/expr_call/{mod,intrinsics}.rs, lower/context.rs, lower/lowering_context.rs, lower_decl/{block,body_stmt,mod}.rs, destructuring/{var_decl,var_decl_sources(new),mod}.rs
  • perry-codegen: stmt/{switch_stmt,try_stmt,loops}.rs, expr/instance_misc1.rs, runtime_decls/strings.rs
  • perry-runtime: object/{field_get_set,field_set_by_name,with_env}.rs, value/nanbox.rs, symbol.rs, error.rs
  • perry-transform: inline/{call_inliner,closure_analysis,mod}.rs

Validation

  • Per-round failure-SET diffs (not %) against the branch-point baseline after every change batch: 0 regressions at each of 8 rounds.
  • Final broad gate: --shard 0/12 over built-ins language (2645 judged), main vs branch built from the same commit on the same box: 281 → 273 failures, 0 regressions, 8 bonus fixes.
  • cargo test green for perry-hir / perry-codegen / perry-transform; perry-runtime has one pre-existing failure on main (date::tests::test_full_year_setters_revive_invalid_date_only), unrelated.
  • cargo fmt --check + scripts/check_file_size.sh pass (split for_head.rs / var_decl_sources.rs out of files that crossed 2000 LOC).

Deferred (pre-existing categories, unchanged)

Runtime TDZ for let/const (~20 tests), named-function-expression self-binding, mapped arguments aliasing, live Map iterators (map-expand/contract), IteratorClose on uncaught body throw, let xCls = class x {} NamedEvaluation (.name must be x), for-await-of async-ordering diffs.

No version bump / changelog per maintainer flow — metadata folded in at merge.

Ralph Küpper added 2 commits June 9, 2026 22:52
language/statements parity 83.1% -> 92.8% (1037 -> 1158 of 1248 judged,
+121, zero regressions vs the branch-point baseline). Root causes, by
cluster:

1. var hoisting out of nested blocks (~30 tests): module-level lowering
   never pre-registered `var`s declared inside blocks/loops/try at module
   scope, so reads before/after the block threw ReferenceError. New
   pre-register pass walks compound statements and emits an
   undefined-initialised slot Let up front (codegen materialises local
   storage at the first Stmt::Let; reads compiled earlier baked in an
   `undefined` constant and never saw later writes — `while(1){if(c)break;
   var c=1}` looped forever). Same slot-Let fix applied to the function-body
   hoist pass. Also removed an expr_assign guard that made `x = x` before a
   hoisted top-level `var x` throw.

2. switch dispatch (8): cases written after `default:` were never tested —
   the if-tower chained tests through the default's unconditional jump.
   Default is now excluded from the test chain (entered only after every
   case test fails). Case comparison routed through a new
   js_switch_strict_equals (string content compare incl. SSO, IEEE numeric
   compare so NaN never matches and int32-boxed == raw double, bit identity
   otherwise) — the old js_get_string_pointer_unified path coerced numbers
   to property-key strings, making `switch(1)` match `case '1'`.

3. for-of live iteration (12): typed arrays were snapshotted via ArrayFrom
   (mutations during the loop invisible) — now iterated live through the
   typed-array accessors. The proven-array index loop's length-hoist
   peephole fired on the desugar's `__arr_N` holder, an alias of the user
   local, so `arr.push()` in the body couldn't invalidate it — peephole now
   skipped for desugared holders (spec reads length live).

4. strict-mode eval early errors (~30): literal direct-eval bodies are now
   parsed and scanned for strict violations (assign/bind/name eval or
   arguments, dup params; strictness from calling context or the source's
   own directive, incl. function-body directives and nested
   `new Function(p, body)` literal bodies) and throw SyntaxError at the
   eval call, ahead of the const-fold. fn.caller/fn.arguments now poisoned
   on set for all closures AND function declarations; `'caller' in fn`
   no longer runs the throwing getter.

5. for-in/for-of head targets (~14): member-expression heads
   (`for (x.y of …)`), parenthesised idents, bare-ident assignment heads
   (write the OUTER binding, not a fresh local), destructuring-assignment
   heads, and destructuring DECL heads with defaults/rest/nested patterns
   (previous inline walks silently skipped non-Ident elements — the
   scope-* probe closures read unbound locals). Catch params with
   destructuring patterns bound the same way. const heads now throw
   TypeError on body assignment.

6. try/finally (3): a throw escaping a CATCH body now runs the finally
   (whose own abrupt completion supersedes) via a dedicated setjmp frame
   around the catch body.

7. with-statement (6): implicit globals created by with-set fallbacks are
   now module-scoped HOLE-sentinel slots — materialised only if the
   fallback actually fires at runtime (the env owning the property takes
   the write), so post-with reads throw ReferenceError iff the binding
   never materialised. `var x = v` inside `with` PutValues through the env
   when it owns `x`. HasBinding probe ordered after RHS evaluation to
   match V8.

8. inliner param aliasing (cross-cutting): a body that WRITES a parameter
   (`function f(a){a++}`) had the param substituted with the caller's
   LocalGet, mutating the caller's variable. Mutated params are now always
   materialised as a fresh copy Let (new collect_mutated_local_ids).

Also: iterator-protocol next()/return() results validated (non-object →
TypeError) in the lazy for-of loop.

Deferred (tracked, pre-existing categories): runtime TDZ for let/const,
named-function-expression self-binding, mapped arguments aliasing, live
Map iterators, iterator close on uncaught body throw.
@proggeramlug proggeramlug merged commit a2a40d3 into main Jun 10, 2026
13 checks passed
@proggeramlug proggeramlug deleted the language-statements-v2-parity branch June 10, 2026 03:08
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