Fix async/generator IL & interpreter bugs (#727 #728 #721 #720)#786
Merged
Conversation
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).
This was referenced Jun 16, 2026
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.
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/continueout of atryemits invalid ILAsyncMoveNextEmitterlacked the protected-region-aware branching the generator emitters have, so abreak/continueleaving atryinside an async body emitted aBrout of the real IL exception region (BranchOutOfTry→InvalidProgramExceptionatAsyncMethodBuilderCore.Start).EmitBranchToLabelto emitLeavewhen inside a real IL exception block (ExceptionBlockDepth, incremented inEmitSimpleTryCatch).break/continue/returnas a segment-breaker inEmitTryBodyWithAwaitsso its jump lands at the top level (outside the mini IL try), mirroring the generator'sIsSegmentBreakerdesign.ContainsEscapingExit/ContainsEscapingExit2analysis (it lived in both the generator and async-generator emitters) into the sharedStatementEmitterBase.#728 — Labeled
for await...ofiterated synchronously (interpreter + compiled)outer: for await (...)ignoredStmt.ForOf.IsAsyncon the labeled path, throwing "requires an iterable" (interpreter) or failing to compile ("Label N has not been marked") while the unlabeled form worked.LabeledStatementhandler so a labeled loop runs through the async path (where for-await routes to the async iterator), and have the async loops drainTakePendingLoopLabels()sobreak/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.)AsyncMoveNextEmitter.EmitLabeledForOfdelegates toEmitForAwaitOfwhenIsAsync, threading the chain's labels into the loop scope.#721 — Redundant dead async state machine for method-nested async arrows
An
asyncarrow nested in an async class method got two async state machines: the live nested one and a dead standalone duplicate (DefineTopLevelAsyncArrowsran 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 asyncStmt.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 ILPrivate methods were emitted linearly into
__private_<name>regardless of kind — bareobjecton the stack for anasyncmethod declaredTask<object>(StackUnexpected), andyieldrejected.EmitPrivateMethodBodynow routes by kind through the same state-machine path as public methods:EmitAsyncMethodBodygeneralized withisInstanceMethod+ nullablefieldsField+ qualifiedcurrentClassName→ serves instance and static private async.static *m()) fails with "Yield not supported in this context" #692) all route through their state machines. Return types fixed viaResolvePrivateMethodReturnType; generator/async-generator stubs box from the method's actual IL signature (private params are allobjectslots).static async *m()) fails with "Yield not supported in this context" #761), not invalid IL.Merge integration
Branch based on
909ff2f2; merged currentorigin/main(15 commits) and reconciled with concurrent work touching the same subsystems — #692 (static generators), #704 (async chained labels → plurallabelNamesthreaded through the for-await fix), #675/#691/#559 (generator/async-generator try-flow machinery, which now consumes the sharedContainsEscapingExit). Because #692 landed static generator support, #720 also fixes static private generators.Follow-ups
break/continueout of a try-with-awaits doesn't run thefinally(the semantics half of Compiled (async function):break/continueout of atryemits invalid IL #727; the invalid-IL half is fixed here).static async *m()) fails with "Yield not supported in this context" #761 (static async generators), Interpreted async generator: body side effects run out of order vs for-await-of consumer (eager drain) #564/Interpreter: async generator leaks its closure when an await is nested inside a delegated expression (yield f(await x)) #752 (interpreter async-generator for-await eager drain).Testing
AsyncTryControlFlowTests,LabeledForAwaitTests,AsyncArrowNoDeadDuplicateTests(asserts the emitted state-machine count via reflection),PrivateAsyncGeneratorMethodTests.