Skip to content

fix: class async / async-generator method test262 parity - #4768

Merged
proggeramlug merged 1 commit into
mainfrom
class-async-methods-parity
Jun 7, 2026
Merged

fix: class async / async-generator method test262 parity#4768
proggeramlug merged 1 commit into
mainfrom
class-async-methods-parity

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

Brings the 8 in-class async-method / async-generator-method test262 dirs from 266/516 (51.6%) → 408/516 (79.1%) pass.

Regression-checked against a built-ins+language shard (0/12): +24 net fixed, 0 real regressions (the one shard blip — TypedArray/prototype/forEach/returns-undefined — passes in isolation on both this branch and the merge-base; load-induced flakiness, not from this change). The default-parameter and private-method fixes also lift the standalone language/statements/class/dstr dirs by +23 with zero regressions.

Dirs covered: language/{statements,expressions}/class/{async-method,async-method-static,async-gen-method,async-gen-method-static}.

Root causes fixed

  1. No [Symbol.asyncIterator] on async-generator instances. for await over a class async-gen method hung / yielded nothing — GetIterator(obj, async) couldn't resolve the iterator. Install [Symbol.asyncIterator] (returning this) on %AsyncGenerator.prototype%. (The sync %Generator.prototype% deliberately gets none — the sync for-of path doesn't bind implicit-this, so a this-returning thunk would break for (x of gen()).)

  2. Async yield* delegated through the sync iterator protocol with no awaitsyield* asyncIterable in an async generator produced nothing or threw "next is not a function". The yield* desugaring is now async-aware: uses GetAsyncIterator and awaits each delegated next() when the enclosing generator is async.

  3. GetIterator(obj, async) error semantics. A present-but-non-callable @@asyncIterator, or a callable one returning a non-Object, must throw TypeError — not fall back to the sync iterator. Rewrote js_get_async_iterator to the spec ordering.

  4. Missing trailing args padded with NaN instead of undefined in dynamic vtable / static-method dispatch. Default params lower to param === undefined ? <default> : param, so a hole padded with NaN never triggered the default (e.g. a detached C.prototype.method called with fewer args).

  5. Private methods never applied parameter defaults (direct or via a this.#m value): lower_private_method built param.default but never prepended the if (param === undefined) param = default prologue public methods emit. Fixes all private method kinds.

  6. Latent: small typed arrays are raw-alloc'd with no object GcHeader, so Object.isExtensible read garbage _reserved and non-deterministically reported false. Integer-indexed exotic objects are extensible by default; short-circuit TAs/buffers.

Files

  • perry-transform/src/generator/linearize.rs, lower.rs — async-aware yield* delegation
  • perry-runtime/src/object/global_this.rs%AsyncGenerator.prototype%[Symbol.asyncIterator]
  • perry-runtime/src/array/iterator.rs — spec GetIterator(obj, async)
  • perry-runtime/src/object/class_registry.rsundefined (not NaN) arg padding
  • perry-hir/src/lower_decl/private_members.rs — private-method default prologue
  • perry-runtime/src/object/object_ops_frozen.rs — TA/buffer isExtensible

Notes for the maintainer

  • Branch is based on cf56a6faf; current main has #4766 (dstr + dflt-params). Fix useEffect + setState panics with RefCell already borrowed on macOS ARM64 #4 (call_vtable_method NaN→undefined) is still needed on current main and shouldn't conflict.
  • Remaining failures here are mostly fine-grained async yield* execution-order tests (custom getter/thenable ordering) that conflict with Perry's synchronous-await-drain model, plus static-method this binding (a separate, broad pre-existing gap: static methods see this === undefined).
  • Kept scoped to the async-method state machine + shared dispatch; expect the usual rebase at merge.

Brings the 8 in-class async-method / async-generator-method test262 dirs
from 266/516 (51.6%) to 408/516 (79.1%) pass, zero regressions.

Root causes fixed:

1. Async-generator instances had no [Symbol.asyncIterator]. `for await` over
   a class async-gen method hung/yielded nothing (GetIterator(obj, async)
   could not resolve the iterator). Install [Symbol.asyncIterator] (returning
   `this`) on %AsyncGenerator.prototype% (global_this.rs). The sync
   %Generator.prototype% deliberately gets none — the sync for-of path does
   not bind implicit-this, so a this-returning thunk would break `for (x of
   gen())`.

2. Async `yield*` delegated through the SYNC iterator protocol with no awaits.
   `yield* asyncIterable` inside an async generator silently produced nothing
   or threw "next is not a function". Made the yield* desugaring async-aware
   (linearize.rs): use GetAsyncIterator and `await` each delegated
   next()/result when the enclosing generator is async (threaded via a
   thread-local set in lower.rs).

3. GetIterator(obj, async) error semantics. A present-but-non-callable
   @@asynciterator, or a callable one returning a non-Object, must throw
   TypeError (not fall back to the sync iterator). Rewrote js_get_async_iterator
   to follow the spec ordering (iterator.rs).

4. Missing trailing arguments were padded with NaN instead of `undefined` in
   the dynamic vtable / static-method dispatch (class_registry.rs
   call_vtable_method + call_static_method). Default parameters lower to
   `param === undefined ? <default> : param`, so a hole padded with NaN never
   triggered the default (e.g. `C.prototype.method` called with fewer args).

5. Private methods never applied parameter defaults at all (direct OR via a
   `this.#m` value): lower_private_method built `param.default` but never
   prepended the `if (param === undefined) param = default` prologue that
   public methods/constructors emit. Added it (private_members.rs). Fixes all
   private method kinds (sync/async/gen/async-gen) and async-private-gen dstr
   default tests.

6. Latent: small typed arrays are raw-alloc'd with no object GcHeader, so
   js_object_is_extensible read garbage `_reserved` and non-deterministically
   reported `false`. Integer-indexed exotic objects are extensible by default;
   short-circuit TAs/buffers to extensible (object_ops_frozen.rs).

Fixes #4 and #5 also lift the standalone language/statements/class/dstr
private-method and default-parameter dirs (+23 there) with zero regressions
across a built-ins+language regression shard.
@proggeramlug
proggeramlug force-pushed the class-async-methods-parity branch from 4faa461 to 3f65361 Compare June 7, 2026 20:23
@proggeramlug
proggeramlug merged commit eba26ac into main Jun 7, 2026
13 checks passed
@proggeramlug
proggeramlug deleted the class-async-methods-parity branch June 7, 2026 20:44
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.

useEffect + setState panics with RefCell already borrowed on macOS ARM64

1 participant