Skip to content

Interpreter: rewrite async generator as a lazy coroutine (#690/#717/#752/#734)#773

Merged
nickna merged 2 commits into
mainfrom
wrk/sweet-hoover-bb5ab2
Jun 16, 2026
Merged

Interpreter: rewrite async generator as a lazy coroutine (#690/#717/#752/#734)#773
nickna merged 2 commits into
mainfrom
wrk/sweet-hoover-bb5ab2

Conversation

@nickna

@nickna nickna commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Summary

Rewrites the interpreter's async generator from the eager full-body drain model to a lazy, main-thread C# async coroutine backed by an ECMA-262 §27.6.3 request queue. This single rewrite closes four open interpreter issues that all stemmed from the eager-drain model, and removes a class of environment-leak workarounds.

Why a coroutine, not a worker thread

The synchronous generator (SharpTSGenerator) suspends a background thread at each yield. That isn't viable for async generators: the interpreter is single-threaded — a custom SynchronizationContext routes every async continuation back to one event-loop thread — and a worker thread would race the event loop on the shared _interpreter.Environment. So the async generator body now runs as an ordinary interpreter async execution (ExecuteBlockAsync) on that event-loop thread and suspends at each yield by handing the value to the driving request and awaiting the next one.

Reusing the real async execution path is what fixes #717 and #752 essentially for free — a for await inside the body and an await nested in a yielded expression are handled by the same code that handles them in any async function.

Issues fixed

Issue Before (interpreter) After
#690 concurrent next() Runtime Error: Only instances and objects have properties FIFO request queue → 1 false 2 false
#717 for await…of inside an async generator Runtime Error: Cannot iterate over non-iterable value drives the async-iterator protocol natively
#752 yield f(await x) closure leak outer binding reads back undefined (undefined 2) environment preserved across the nested await (T 2)
#734 async generator expression capturing a block-scoped binding Error: Undefined variable 'n' runs natively; compiler still errors cleanly

How it works

Tests

Notes for the reviewer

nickna added 2 commits June 16, 2026 10:52
/#734)

Replace the interpreter's eager-drain SharpTSAsyncGenerator with a lazy,
main-thread C# async coroutine plus an ECMA-262 §27.6.3 request queue. The
body now runs as an ordinary interpreter async execution (ExecuteBlockAsync)
on the single event-loop thread and suspends at each yield by handing the
value to the driving request and awaiting the next one. A worker thread (as
the synchronous generator uses) is impossible here: the interpreter is
single-threaded and a worker would race the event loop on the shared
interpreter environment.

Reusing the real async execution path fixes several issues at once:

- #690: two next() calls before the first settles are serviced FIFO via the
  request queue instead of racing a half-populated drain.
- #717: a for await...of INSIDE an async generator drives the async-iterator
  protocol natively instead of throwing "Cannot iterate over non-iterable".
- #752: an await nested in a yielded expression (yield f(await x)) preserves
  the ambient environment like any async function, so the caller's bindings
  no longer read back undefined.
- #734: an async generator function expression that closes over a block-scoped
  binding runs natively (new SharpTSAsyncArrowGeneratorFunction; the
  GeneratorArrowLifter leaves it in place). The compiler still reports a clean
  "Yield not supported in this context" error.

Mechanism: new Interpreter.CurrentAsyncGenerator identifies the active
generator; Expr.Yield in the async path routes through EvaluateYieldAsync ->
OnYieldAsync. AwaitPreservingEnvironment now also saves/restores
CurrentAsyncGenerator so interleaved generators never see each other's
binding. for await...of early exit (break / return / throw) calls
CloseAsyncIteratorOnEarlyExit (AsyncIteratorClose) so a suspended generator
runs its finally blocks. return()/throw() reuse the synchronous generator's
GeneratorResume / GeneratorReturnException injection, so finally blocks run.
A re-entrant next() during the body's initial synchronous segment rejects
with a catchable TypeError instead of deadlocking (#542 analog).

Promotes 7 previously CompiledOnly async-generator tests to run in the
interpreter too, and adds #734 coverage. Full suite: 13200 passing.

Gaps filed: #770 (labeled loop containing await/yield has no async handler --
pre-existing, exposed here), #771 (deep re-entrancy stall).
…fo[])

origin/main currently does not compile: the two instance/private-method call
sites of EmitGeneratorFunctionDCInit (ILCompiler.AsyncGenerators.cs and
ILCompiler.Generators.cs) pass methodBuilder.GetParameters() — a
ParameterInfo[] — to a parameter declared as Type[]?. This is a latent
mismatch from the interaction of the #720 (async/generator private methods,
which deliberately box from the method's ACTUAL IL signature) and #724/#725
(function display-class seeding) changes after both landed.

Accept ParameterInfo[]? and box value types via .ParameterType, leaving a
private method's all-object slots unboxed. Behavior is unchanged; this only
makes the tree compile. Unrelated to the async-generator coroutine rewrite in
the preceding commit — committed separately so it is easy to drop if main is
fixed independently.
@nickna nickna force-pushed the wrk/sweet-hoover-bb5ab2 branch from 9021132 to 3564041 Compare June 16, 2026 18:06
@nickna nickna merged commit e8f3990 into main Jun 16, 2026
3 checks passed
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.

Interpreter: for await…of INSIDE an async generator throws 'Cannot iterate over non-iterable value'

1 participant