fix: standalone async-generator test262 parity (57.7% → 93.0%) - #4777
Merged
Conversation
added 5 commits
June 8, 2026 08:02
…on parity
Standalone async-generator (and other) expressions dropped two destructuring
behaviors that declarations already had:
1. A param that is both a destructuring pattern AND has a default
(`async function*([x,y,z]=[1,2,3]){}`) lowered only the default guard, not
the destructuring binding — x/y/z resolved to
js_throw_reference_error_unresolved_get. lower_arrow / lower_fn_expr /
nested_fn_decl now unwrap Pat::Assign to the inner pattern before
is_destructuring_pattern, mirroring lower_fn_decl.
2. Destructuring defaults skipped NamedEvaluation: `[fn = function(){}]` left
fn.name empty instead of "fn". pattern_binding now threads the single-name
binding into ctx.assignment_inferred_name for anonymous fn/arrow/class
defaults, at all three default sites (array elem, Pat::Assign, object
shorthand).
Per spec, generator/async-generator parameter binding (FunctionDeclarationInstantiation) runs synchronously when the function is called — before the generator object is created — so an iterator / RequireObjectCoercible / TDZ error during destructuring or default evaluation throws at call time, not on the first .next(). Perry prepended the param prologue (default guards + destructuring binding) to the body, which the generator transform linearized into state 0 of .next(); a test that only calls f(g) (expecting a synchronous throw) never ran it. Lowering now records the prologue length per generator func_id (Module.gen_param_prologue_len); the generator transform lifts those leading statements out of the state machine and runs them in the outer wrapper, boxing their bound locals so the state machine still reads the destructured values. Gated on is_generator (plain async functions keep reject-not-throw semantics) and inert when there is no destructuring/default param (prologue_len 0). async-generator dirs: rt-fail 181->21, parity 73.6%->91.7%.
Per spec, `yield E` in an async generator is AsyncGeneratorYield(? Await(E)) — the operand is awaited before being delivered, so `yield Promise.reject(x)` throws x into the generator and `yield Promise.resolve(v)` yields v (not the promise). Perry yielded the raw operand. A pre-pass (after hoist_yields) rewrites each statement-level non-delegate `yield E` into `let __ayield = await E; yield __ayield`; the await reuses the existing suspension machinery. yield* delegation is untouched (it awaits via delegate_await).
The standalone async-generator parity work (param-prologue lift + per-yield operand Await) grew generator/lower.rs past the 2000-line CI gate; add it to the check_file_size.sh allowlist (split tracked under #1435).
A fused method call `o.next(args)` where `next` is an own accessor
(`{ get next() { return fn } }` or Object.defineProperty get) mis-resolved to
undefined — js_native_call_method read the raw field slot (no callable for an
accessor-only property) instead of invoking the getter. The decomposed form
`const f = o.next; f(args)` already worked via the getter-aware property read.
Add an early accessor check: invoke the getter (this=receiver), then call the
resolved function. Gated on the ACCESSORS_IN_USE hot-path flag. Unblocks yield*
over a sync/async iterator whose next/value/done are getters (test262
yield-star-* with `get next()`).
Also add gen_param_prologue_len: HashMap::new() to the Module struct literals in
perry-codegen{,-arkts} test fixtures (new Module field).
This was referenced Jun 8, 2026
Merged
Merged
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.
Drives standalone
async function*(declarations + expressions) test262 parity from 57.7% → 93.0% acrosslanguage/expressions/async-generator+language/statements/async-generator(pass 512 → 826 / 888, runtime-fail 181 → 21, compile-fail 0). Four root causes, each spec-justified, gated to stay inert outside the affected shapes.Root causes & fixes
1. Destructuring param + default dropped the binding; destructuring defaults skipped NamedEvaluation (
fix(hir))async function*([x,y,z]=[1,2,3]){}(expression form) lowered only the default guard, not the destructuring binding —x/y/zresolved tojs_throw_reference_error_unresolved_get.lower_arrow/lower_fn_expr/nested_fn_declnow unwrapPat::Assignto the inner pattern beforeis_destructuring_pattern, mirroring the already-correctlower_fn_decl.[fn = function(){}]leftfn.nameempty instead of"fn".destructuring/pattern_bindingnow threads the single-name binding intoctx.assignment_inferred_namefor anonymous fn/arrow/class defaults at all three default sites.2. Generator param binding ran on first
.next()instead of at call time (fix(transform)).next(), so a test that only callsf(badIterable)(expecting a synchronous throw) never ran it.Module.gen_param_prologue_len); the transform lifts those leading statements into the outer wrapper, boxing their bound locals so the state machine still reads the destructured values. Gated onis_generator(plain async functions keep reject-not-throw semantics) and inert whenprologue_len == 0. This fix alone collapsed runtime-fail 181 → 21.3.
yield Edidn'tAwaitits operand (fix(transform))yield Ein an async generator isAsyncGeneratorYield(? Await(E))—yield Promise.reject(x)throwsxinto the generator andyield Promise.resolve(v)yieldsv. A pre-pass (afterhoist_yields) rewrites each statement-level non-delegateyield Eintolet __ayield = await E; yield __ayield, reusing the existing suspension machinery.yield*delegation untouched.Verification & zero-regression check
node --experimental-strip-types.language/{expressions,statements}/{generators,async-function}(shared state machine): 90.4%, no new crashes — the param-prologue lift is spec-correct for sync generators too.0/12overbuilt-ins languagevs anorigin/mainbaseline (same shard/harness): +33 pass, −10 runtime-fail, compile-fail unchanged (18). The one apparent "regression" (TypedArray/prototype/forEach/returns-undefined.js) is a pre-existing non-deterministic GC/layout-sensitive segfault (passed run 3/3 on a commit-1-only build) surfaced by layout shift, not introduced logic.Remaining (62, mostly out of scope)
~50 are precise
yield*async-delegation tests (exact getter ordering, async-from-sync ticks,return/throwforwarding, abrupt-completion propagation) — a separate AsyncGeneratorYield* reimplementation. ~12 are prototype-chain edge cases.