Fix #430/#645: compiled for await...of inside an async arrow throws InvalidCastException#677
Merged
Conversation
…InvalidCastException `AsyncArrowMoveNextEmitter` had no `EmitForOf` override, so a compiled `for await...of` loop fell through to the synchronous for-of path in `StatementEmitterBase` and cast the async-generator state machine to `IEnumerable` — throwing `InvalidCastException` at runtime. The interpreter handled the same program correctly. Extract the async-iterator lowering (previously private to `AsyncMoveNextEmitter`) into `StatementEmitterBase.EmitForAwaitOf` so all three async state-machine emitters share one implementation instead of growing a third divergent copy: - `AsyncMoveNextEmitter` / `AsyncArrowMoveNextEmitter` dispatch to the shared base method from their `EmitForOf` override when `f.IsAsync`. - `AsyncGeneratorMoveNextEmitter` keeps a `protected override` (its own error-propagating + strict-done variant for the async-generator consumer). Loop-variable binding now routes through the `DeclareLoopVariable` / `EmitStoreLoopVariable` hooks instead of `Ctx.Locals` directly: the async arrow's resolver reads its own private `_locals` map (via its `RegisterLoopLocal` override), so a binding registered in `Ctx.Locals` read back as `null` in the arrow body. Covers the `Symbol.asyncIterator` protocol, the `$IAsyncGenerator` fast path, break/cleanup, captured variables, and nested async arrows; emitted IL passes ILVerify. Adds 4 regression tests (both modes) and registers the two new emitter overrides in `EmitterSyncTests.AllowedOverrides`. The residual "async generator whose body awaits a not-yet-settled promise yields no output" case from #430's comment is the pre-existing blocking- GetResult event-loop limitation tracked by #631, not the arrow bug. Filed follow-ups #672 (top-level for-await mis-run as sync) and #673 (captured-var store in a nested async arrow emits unverifiable IL), both pre-existing.
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 #430 and #645 (same root cause).
Problem
In compiled mode, a
for await...ofover an async generator inside an async arrow function crashed:AsyncArrowMoveNextEmitterhad noEmitForOfoverride, so the loop fell through to the synchronous for-of path inStatementEmitterBaseand cast the async-generator state machine toIEnumerable. The interpreter handled the same program correctly, and the async function form already worked.Fix
The correct async-iterator lowering lived only inside
AsyncMoveNextEmitter(with a divergent, more-limited copy inAsyncGeneratorMoveNextEmitter). Rather than add a third copy, I extracted it intoStatementEmitterBase.EmitForAwaitOfso all three async state-machine emitters share one implementation:AsyncMoveNextEmitter/AsyncArrowMoveNextEmitterdispatch to the shared base method from theirEmitForOfoverride whenf.IsAsync.AsyncGeneratorMoveNextEmitterkeeps aprotected override(it needs its own error-propagating + strict-done variant for the async-generator-consumer case).Loop-variable binding now routes through the
DeclareLoopVariable/EmitStoreLoopVariablehooks instead ofCtx.Localsdirectly — the async arrow resolver reads its own private_localsmap (via itsRegisterLoopLocaloverride), so a binding registered inCtx.Localsread back asnullin the arrow body.Validation
$IAsyncGeneratorpath, customSymbol.asyncIterator,break/cleanup (iterator.return()), captured-variable accumulation, and nested-arrow (captured-outer-state-machine) contexts.SharpTS.Testspass (incl. theEmitterSyncTestsoverride guard, updated for the two new overrides).AsyncArrowFunctionTests(both interpreter + compiled).Out of scope (filed separately)
setTimeout) promise yields no compiled output" case from Compiler: for await...of over an async generator inside an async arrow emits synchronous IEnumerable iteration (InvalidCastException) #430's comment is the pre-existing blocking-GetResult/ event-loop limitation already tracked by Compiled async generator hangs on a not-yet-settled (genuinely-async) await #631 (reproduces with a plain async-function driver too — not arrow-specific).for await...ofis accepted then mis-run as sync for-of (top-level await is otherwise rejected) #672 — top-levelfor awaitis accepted by the type checker then mis-run as a sync for-of (both modes).StackUnexpected); pre-existing, independent of for-await.Note: the local
SharpTS.Test262compiled-baseline host aborts with the known environmental0x80131506CLR crash — confirmed identical on cleanorigin/main(stash comparison), so unrelated to this change.