Skip to content

Compiled: or await over an async generator inside an async ARROW function throws InvalidCastException (lowered as sync for-of) #645

@nickna

Description

@nickna

Summary

In compiled mode, a for await (… of asyncGen()) loop placed inside an async arrow function (async () => { … }) crashes at runtime with:

System.InvalidCastException: Unable to cast object of type '<g>d__0' to type 'System.Collections.IEnumerable'.
   at <>c__AsyncArrow_0.MoveNext()

The async-arrow MoveNext emitter appears to lower for await as a synchronous for-of (it casts the async-generator state machine to System.Collections.IEnumerable) instead of async iteration. The same loop works when it lives in an async function declaration, and works in both forms under the interpreter.

Repro

async function* g() { yield 1; yield 2; }

(async () => {
  for await (const x of g()) console.log(x);   // compiled: InvalidCastException
})();

Contrast — these all work:

// async FUNCTION wrapper instead of arrow — works compiled + interpreted
async function* g() { yield 1; yield 2; }
async function run() { for await (const x of g()) console.log(x); }
run();

The arrow form works under the interpreter (dotnet run script.ts); it only fails when compiled (--compile).

Impact / how found

Found while implementing #635 (parser support for async function () {} expressions). With #635, an async generator function expression const ag = async function* () { … } now parses; GeneratorArrowLifter lifts it to a top-level async function* declaration, and any compiled consumer that uses for await inside an async arrow hits this bug. The bug itself is independent of #635 — it reproduces with a hand-written async function* declaration and no expression syntax (above).

Likely area

The async-arrow state-machine MoveNext emitter's handling of Stmt.ForOf with IsAsync = true — it likely falls through to the synchronous for-of path (emitting an IEnumerable cast / GetEnumerator) rather than the async-iterator path used by the async-function-declaration emitter. Compare the for await emission in the async function MoveNext emitter (which is correct) with the async arrow one.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions