fix(hir): statement-semantics test262 tail (v2)#4861
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/12gate overbuilt-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
varhoisting out of nested blocks (~30): module-level lowering never pre-registeredvars declared inside blocks/loops/try, so reads before/after the block threw ReferenceError. New pre-register pass + an explicit undefined-initialised slotLetat entry — codegen materialises local storage at the firstStmt::Letfor an id, so a read compiled earlier baked in anundefinedconstant 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 anexpr_assignguard that madex = xbefore a hoisted top-levelvar xthrow.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 newjs_switch_strict_equals(string content compare incl. SSO, IEEE numeric compare socase NaNnever matches and int32-boxed1equals raw1.0, bit identity otherwise) — the old path coerced numbers to property-key strings, soswitch(1)matchedcase '1'.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_Nholder (an alias of the user local), soarr.push()in the body couldn't invalidate it; the peephole is now skipped for desugared holders.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 nestednew Function(p, body)literal bodies) and throw SyntaxError at the eval call, ahead of the const-fold.fn.caller/fn.argumentsassignment now throws for all closures and function declarations;'caller' in fnno longer runs the poisoned getter.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 test262scope-*probe closures). All now routed through sharedpredefine_for_head/for_head_binding_stmts/emit_for_of_pattern_binding. Destructuredcatchparams bound the same way.constheads throw TypeError on body assignment.try/catch/finally (3): a throw escaping a
catchbody skipped thefinally. The catch body now runs under its own setjmp frame; the finally runs and its own abrupt completion supersedes the pending exception.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 = vinsidewithPutValues through the env when it ownsx. HasBinding probes after RHS evaluation, matching V8.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'sLocalGet, sof(x)mutated the caller'sx. Mutated params are now always materialised as a fresh copyLet(newcollect_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}.rsperry-codegen:stmt/{switch_stmt,try_stmt,loops}.rs,expr/instance_misc1.rs,runtime_decls/strings.rsperry-runtime:object/{field_get_set,field_set_by_name,with_env}.rs,value/nanbox.rs,symbol.rs,error.rsperry-transform:inline/{call_inliner,closure_analysis,mod}.rsValidation
--shard 0/12overbuilt-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 testgreen 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.shpass (splitfor_head.rs/var_decl_sources.rsout of files that crossed 2000 LOC).Deferred (pre-existing categories, unchanged)
Runtime TDZ for let/const (~20 tests), named-function-expression self-binding, mapped
argumentsaliasing, live Map iterators (map-expand/contract), IteratorClose on uncaught body throw,let xCls = class x {}NamedEvaluation (.name must bex), for-await-of async-ordering diffs.No version bump / changelog per maintainer flow — metadata folded in at merge.