Skip to content

Fix async/generator IL & interpreter bugs (#727 #728 #721 #720)#786

Merged
nickna merged 6 commits into
mainfrom
wrk/bold-sinoussi-bf5a06
Jun 16, 2026
Merged

Fix async/generator IL & interpreter bugs (#727 #728 #721 #720)#786
nickna merged 6 commits into
mainfrom
wrk/bold-sinoussi-bf5a06

Conversation

@nickna

@nickna nickna commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Fixes four compiled/interpreter async & generator bugs. Each fix ships with cross-mode (interpreter + compiled) parity tests and, where the bug was invalid IL, an IL-verification test.

#727 — Compiled async function: break/continue out of a try emits invalid IL

AsyncMoveNextEmitter lacked the protected-region-aware branching the generator emitters have, so a break/continue leaving a try inside an async body emitted a Br out of the real IL exception region (BranchOutOfTryInvalidProgramException at AsyncMethodBuilderCore.Start).

  • Override EmitBranchToLabel to emit Leave when inside a real IL exception block (ExceptionBlockDepth, incremented in EmitSimpleTryCatch).
  • Treat an escaping break/continue/return as a segment-breaker in EmitTryBodyWithAwaits so its jump lands at the top level (outside the mini IL try), mirroring the generator's IsSegmentBreaker design.
  • De-dup: hoist the verbatim-duplicated ContainsEscapingExit/ContainsEscapingExit2 analysis (it lived in both the generator and async-generator emitters) into the shared StatementEmitterBase.

#728 — Labeled for await...of iterated synchronously (interpreter + compiled)

outer: for await (...) ignored Stmt.ForOf.IsAsync on the labeled path, throwing "requires an iterable" (interpreter) or failing to compile ("Label N has not been marked") while the unlabeled form worked.

  • Interpreter: register an async LabeledStatement handler so a labeled loop runs through the async path (where for-await routes to the async iterator), and have the async loops drain TakePendingLoopLabels() so break/continue <label> match. (This also fully resolves the async-function case of Interpreter: labeled loop containing await/yield fails in async functions and async generators #770 and a labeled-break-out-of-async-try interpreter bug.)
  • Compiled: AsyncMoveNextEmitter.EmitLabeledForOf delegates to EmitForAwaitOf when IsAsync, threading the chain's labels into the loop scope.

#721 — Redundant dead async state machine for method-nested async arrows

An async arrow nested in an async class method got two async state machines: the live nested one and a dead standalone duplicate (DefineTopLevelAsyncArrows ran before class-method state machines registered their arrows). Skip an arrow that an enclosing async function/method's state machine will claim, detected by walking the enclosing-callable chain (all-async up to an async Stmt.Function). Removes the dead duplicate (2→1 type for the direct case, 4→2 for nested async arrows) with no effect on any working case (arrows behind a sync arrow/method, or genuinely top-level, still get their needed standalone builder).

#720 — Compiled async/generator private methods (async #m, *#m, async *#m) emit invalid IL

Private methods were emitted linearly into __private_<name> regardless of kind — bare object on the stack for an async method declared Task<object> (StackUnexpected), and yield rejected. EmitPrivateMethodBody now routes by kind through the same state-machine path as public methods:

Merge integration

Branch based on 909ff2f2; merged current origin/main (15 commits) and reconciled with concurrent work touching the same subsystems — #692 (static generators), #704 (async chained labels → plural labelNames threaded through the for-await fix), #675/#691/#559 (generator/async-generator try-flow machinery, which now consumes the shared ContainsEscapingExit). Because #692 landed static generator support, #720 also fixes static private generators.

Follow-ups

Testing

  • New: AsyncTryControlFlowTests, LabeledForAwaitTests, AsyncArrowNoDeadDuplicateTests (asserts the emitted state-machine count via reflection), PrivateAsyncGeneratorMethodTests.
  • Full suite green: 13257 passed, 0 failed.
  • Conformance suites (Test262 / TypeScriptConformance) not run — they are not in CI and their baselines are managed separately; the changes are covered by the main suite + targeted parity/IL-verification tests.

nickna added 6 commits June 16, 2026 00:41
AsyncMoveNextEmitter lacked the protected-region-aware branching the
generator emitters have, so a break/continue leaving a try inside an
async function body emitted a Br out of the real IL exception region
(BranchOutOfTry -> InvalidProgramException at AsyncMethodBuilderCore.Start).

- Override EmitBranchToLabel to emit Leave when inside a real IL exception
  block (ExceptionBlockDepth), incremented in EmitSimpleTryCatch.
- Treat an escaping break/continue/return as a segment-breaker in
  EmitTryBodyWithAwaits so its jump lands at the top level (outside the
  mini IL try), mirroring the generator's IsSegmentBreaker design.
- Hoist the duplicated ContainsEscapingExit/ContainsEscapingExit2 analysis
  (verbatim in the generator and async-generator emitters) into the shared
  StatementEmitterBase base so all three suspension-aware emitters use one copy.

Adds AsyncTryControlFlowTests (cross-mode parity + IL-validity facts).
…piled)

A 'for await...of' carrying a label (outer: for await (...)) was iterated
synchronously: the Stmt.ForOf.IsAsync flag never reached the async-iteration
lowering on the labeled path. It threw 'for...of requires an iterable'
(interpreter) or failed to compile ('Label N has not been marked', from the
unconsumed reserved await states) while an unlabeled for-await worked.

Interpreter: register an async LabeledStatement handler
(ExecuteLabeledStatementAsyncVT) so a labeled loop in async code runs through
the async path (where for-await routes to the async iterator), and have the
async loops drain TakePendingLoopLabels() so break/continue <label> match.
This also fixes labeled break/continue out of an async try across nested loops.

Compiled: AsyncMoveNextEmitter.EmitLabeledForOf delegates to EmitForAwaitOf
when f.IsAsync, threading the label into the loop scope.

Adds LabeledForAwaitTests (cross-mode parity + IL-validity).
…arrows

An async arrow nested in an async class method got two async state machines:
the live nested one (<>__outer) the method invokes, and a redundant standalone
one (<>captured_*) that is never invoked. DefineTopLevelAsyncArrows ran before
class-method state machines registered their arrows, so a method's arrows
looked 'unclaimed' and got a dead standalone builder.

Skip an arrow that an enclosing async function/method state machine will claim,
detected by walking the enclosing-callable chain: all-async up to an async
Stmt.Function means claimed. An arrow behind a sync arrow/method, or a genuinely
top-level one, is not claimed and still gets its needed standalone builder.

Removes the dead duplicate (2->1 type for the direct case, 4->2 for nested async
arrows) with no effect on any working case. Adds AsyncArrowNoDeadDuplicateTests
asserting the emitted state-machine count via reflection.
A compiled async/generator ES2022 private method (async #m, *#m, async *#m)
was emitted linearly into __private_<name> regardless of kind — leaving a bare
object on the stack for a method declared to return Task<object> (StackUnexpected
invalid IL) and rejecting yield ('Yield not supported'). The interpreter handled
both correctly.

EmitPrivateMethodBody now routes by method kind through the same state-machine
path as public methods:
- EmitAsyncMethodBody generalized with isInstanceMethod + nullable fieldsField +
  qualified currentClassName, so it serves instance AND static private async.
- EmitGeneratorMethodBody / EmitAsyncGeneratorMethodBody take a qualified
  currentClassName for nested private-member access (instance).
- Private-method return types fixed (IEnumerable/IAsyncEnumerable for
  generator/async-generator) via ResolvePrivateMethodReturnType.
- Generator/async-generator instance stubs box from the method's ACTUAL IL
  signature (methodBuilder.GetParameters()), not AST-resolved types, since a
  private method's params are all object slots (mirrors EmitAsyncStubMethod).
- State-machine type names use the mangled builder name to avoid '#' in __private_.

Covers instance async/generator/async-generator and static async private methods.
Static private generators/async generators stay on the linear path and report the
existing clean 'Yield not supported' error (the public static-generator gap, #762),
not invalid IL. Adds PrivateAsyncGeneratorMethodTests.
Reconcile the four fixes (#727 #728 #721 #720) with concurrent work merged after
this branch's base (notably #692 static generators, #704 async chained labels,
#675/#691/#559 generator/async-generator try-flow machinery):

- ILCompiler.Generators.cs: combine #692's isInstanceMethod/static-stub generalization
  of EmitGeneratorMethodBody with this branch's currentClassName param (private methods).
  Apply the actual-IL-signature param boxing (methodBuilder.GetParameters()) to the new
  EmitGeneratorStaticStubMethod too, so private STATIC generators (object slots) don't
  StackUnexpected.
- Because #692 added static generator support, #720 now also routes static private
  generators through the (static-capable) generator state machine — EmitPrivateMethodBody
  routes them, ResolvePrivateMethodReturnType gives them IEnumerable<object>. Only static
  ASYNC generators remain on the linear path (clean 'Yield not supported', #761).
- AsyncMoveNextEmitter for-await: thread #704's plural chain labels (labelNames) through
  EmitForAwaitOf for the labeled for-await fix (#728), instead of the single labelName.
- ContainsEscapingExit extraction (#727) lands in StatementEmitterBase; the merged
  generator/async-generator try-flow machinery uses the shared copy (no duplicate).
…cTests (#727)

The EmitBranchToLabel override added for #727 (Leave instead of Br when a
break/continue leaves a real IL exception block) must be listed in the
StateMachineEmitters_OnlyOverrideAllowedMethods allowlist, like the generator
and async-generator emitters already are.
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