Skip to content

fix: yield* async-delegation test262 parity (next-capture + object-literal thenable await) - #4783

Merged
proggeramlug merged 1 commit into
mainfrom
yield-star-delegation-parity
Jun 8, 2026
Merged

fix: yield* async-delegation test262 parity (next-capture + object-literal thenable await)#4783
proggeramlug merged 1 commit into
mainfrom
yield-star-delegation-parity

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Builds on #4777 (now merged). Spec-aligns yield * async/sync delegation and fixes a core await/Promise.resolve gap that gated the delegated async-iterator protocol.

Results (test262, vs main @ #4777, 0 regressions)

suite before after
language/{expressions,statements}/async-generator 820 pass / 92.3% 838 pass / 94.4%
built-ins/Promise 347 pass / 60.5% 357 pass / 62.2% (thenable fix, bonus)
language/{expressions,statements}/generators 487 / 87.7% no regression

yield-star tests newly passing: yield-star-async-next, yield-star-next-then-get-abrupt, yield-star-next-then-returns-abrupt, yield-star-getiter-async-returns-null-throw (+ named- variants). Failure-set diff (mine − base) is empty for all three dirs.

Root causes fixed

  1. [[NextMethod]] captured once (generator/linearize.rs). The desugar re-read del_iter.next every loop iteration, re-running the iterator's get next accessor + an extra property read per step; spec (GetIterator) reads next once and reuses it. Factored the 3 near-identical desugar sites (yield* e, return yield* e, let x = yield* e) into one emit_yield_star_loop that captures __del_next = __del_iter.next once and calls __del_next.call(__del_iter, arg) (binds this for inherited/builtin next thunks), falling back to method dispatch when the captured value isn't callable (string/typed-array iterators expose no readable next).
  2. First inner pull passes explicit undefined (not argless). Spec inits received = NormalCompletion(undefined), so every inner next() — including the first — gets one arg (next args.length === 1).
  3. async-from-sync wrapper caches the sync [[NextMethod]] (array/iterator.rs). CreateAsyncFromSyncIterator reads next once; the wrapper re-read (and re-ran the getter) on every next().
  4. Object-literal thenables now assimilate in await/Promise.resolve (promise/combinators.rs). js_assimilate_thenable returned early for class_id == 0 objects ({ then(){} } / { get then(){} }), so await { then(r){r(v)} } resolved with the thenable itself. Routed through the existing property-based fallback (PromiseResolveThenableJob: Get(value,"then") + IsCallable), and pass the resolving functions to user then as NaN-boxed function values so typeof onFulfilled === "function" holds. Also fixes +10 built-ins/Promise tests.

The delegated yielded value is intentionally not awaited — current AsyncGeneratorYield does not await its operand (only the next() result is), so a delegated promise flows through un-unwrapped (yield-star-promise-not-unwrapped).

Regression check

  • cargo test -p perry-transform -p perry-runtime -p perry-hir -p perry-codegen: all real tests pass. Two perry-runtime unit tests (closure_name_and_length_ignore_plain_assignment, stream_constructors_expose_static_method_values) are pre-existing parallel-isolation flakes — both pass in isolation and on base; the same binary passes/fails depending only on whether a sibling test's test_clear_closure_side_tables() runs concurrently. Not caused by this change.
  • Effect-style sync yield* smoke (delegation through array / Set / generator / custom [Symbol.iterator] / two-way next(v)) matches Node.

Remaining yield* failures (out of scope; orthogonal pre-existing gaps)

  • return/throw delegation forwarding (yield-star-{async,sync}-{return,throw}, getiter-async-{return,throw}-method-is-null, *-notdone-iter-value-throws): the suspended yield* must dispatch the consumer's .return()/.throw() to the delegate (close + re-yield), needing resume-mode plumbing through the async-generator state machine (generator/lower.rs + object/async_generator_queue.rs).
  • GetIterator [Symbol.iterator] this-binding (yield-star-sync-next): calling the (getter-returned) [Symbol.iterator] binds the wrong this. Separate subsystem.
  • next-protocol edge validation (yield-star-next-{call-value-get-abrupt,non-object-ignores-then,not-callable-number-throw,then-non-callable-number-fulfillpromise}): IteratorNext result must be validated as Object (spec step iv) + abrupt propagation.

…al thenable await

Spec-aligns `yield *` async/sync delegation and fixes a core async/await
gap that blocked the delegated async-iterator protocol.

test262 (vs origin/async-generator-parity, 0 regressions):
- language/{expressions,statements}/async-generator: 820 -> 838 pass (92.3% -> 94.4%)
- built-ins/Promise: 347 -> 357 pass (60.5% -> 62.2%)  [thenable fix, bonus]
- language/{expressions,statements}/generators: no change, 0 regressions

Root causes addressed:

1. [[NextMethod]] captured once (perry-transform/generator/linearize.rs).
   The `yield*` desugar re-read `del_iter.next` on every loop iteration,
   re-running the iterator's `get next` accessor and an extra property read
   per step. Spec (GetIterator) reads `next` exactly once and reuses the
   captured method. Factored the 3 near-identical desugar sites (statement
   `yield* e`, `return yield* e`, `let x = yield* e`) into one
   `emit_yield_star_loop` helper that captures `__del_next = __del_iter.next`
   once and invokes `__del_next.call(__del_iter, arg)` (reads
   Function.prototype.call, not the iterator's getters; binds `this` for
   builtin/inherited `next` thunks). Falls back to method dispatch when the
   captured value isn't callable (string/typed-array iterators expose no
   readable `next`).

2. First inner pull passes an explicit `undefined`, not argless. Spec inits
   `received = NormalCompletion(undefined)`, so every inner `next()` — including
   the first — gets one argument (test262 yield-star-*-next assert
   `next args.length === 1`).

3. async-from-sync wrapper caches the sync `[[NextMethod]]`
   (perry-runtime/array/iterator.rs). CreateAsyncFromSyncIterator reads `next`
   once; the wrapper re-read (and re-ran the getter) on every `next()` call.

4. Object-literal thenables now assimilate in `await`/`Promise.resolve`
   (perry-runtime/promise/combinators.rs). `js_assimilate_thenable` returned
   early for class_id==0 objects (plain `{ then() {} }` / `{ get then() {} }`),
   so `await { then(r){r(v)} }` resolved with the thenable itself. Route those
   through the existing property-based fallback (spec PromiseResolveThenableJob:
   Get(value,"then") + IsCallable, independent of storage). Also pass the
   resolving functions to user `then` as NaN-boxed function values so
   `typeof onFulfilled === "function"` holds.

The delegated yielded value is intentionally NOT awaited: current
AsyncGeneratorYield does not await its operand (only the `next()` result is
awaited), so a delegated promise value flows through un-unwrapped
(yield-star-promise-not-unwrapped).
@proggeramlug
proggeramlug merged commit 5d2b1c2 into main Jun 8, 2026
13 checks passed
@proggeramlug
proggeramlug deleted the yield-star-delegation-parity branch June 8, 2026 10:09
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