diff --git a/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts index 882013369..459af8751 100644 --- a/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts @@ -441,7 +441,8 @@ export class GeneratorDynamicDispatch { * back. Every other iterator, and every early exit (return() through the * protocol), keeps the dynamic path. The awaits are the protocol's: a * `for await` over an async generator awaits next()'s promise, and over a - * sync generator awaits once for the result and once for the value. */ + * sync generator awaits once for the result and, reached through + * Symbol.iterator, once for the value. */ private stepUntypedLoops(lowerer: Lowerer, functions: readonly IrFunction[]): boolean { if (this.generators.size === 0 || lowerer.untypedLoops.size === 0) return false; // The loop reads next through the getter only once it exists. @@ -659,8 +660,15 @@ export class GeneratorDynamicDispatch { type: DYN, loc, }; - // for await over a sync generator: the result awaits once, then the - // value (the final one too), as the async-from-sync iterator does. + // for await over a sync generator: the result awaits once. When the + // loop reached it through Symbol.iterator, the async-from-sync iterator + // also awaits the value (the final one too); a sync generator that an + // object's Symbol.asyncIterator returns passes the value as it is, as + // the protocol does. + const fromSync: IrExpr = + site.sync === null + ? { kind: "boolLit", value: false, type: BOOL, loc } + : varRef(site.sync, BOOL, loc); return { kind: "seqExpr", stmts: [ @@ -672,13 +680,27 @@ export class GeneratorDynamicDispatch { kind: "ternary", cond: doneRef, then: { - kind: "seqExpr", - stmts: [{ kind: "exprStmt", expr: awaitDyn(boxed), loc }], - result: dynUndefinedExpr(loc), + kind: "ternary", + cond: fromSync, + then: { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: awaitDyn(boxed), loc }], + result: dynUndefinedExpr(loc), + type: DYN, + loc, + }, + else_: dynUndefinedExpr(loc), + type: DYN, + loc, + }, + else_: { + kind: "ternary", + cond: fromSync, + then: awaitDyn(boxed), + else_: boxed, type: DYN, loc, }, - else_: awaitDyn(boxed), type: DYN, loc, }, diff --git a/tests/corpus/8380-untyped-generator-loops.mjs b/tests/corpus/8380-untyped-generator-loops.mjs index 2a477ca10..a6dd20ebe 100644 --- a/tests/corpus/8380-untyped-generator-loops.mjs +++ b/tests/corpus/8380-untyped-generator-loops.mjs @@ -56,6 +56,12 @@ async function main() { }, }; console.log("async custom", show(await collectAsync(asyncCustom))); + // Symbol.asyncIterator returning a sync generator: each result is awaited, + // its value is not, so a yielded promise reaches the loop as a promise. + const viaAsyncIterator = { [Symbol.asyncIterator]: () => mixed() }; + const seen = []; + for await (const x of viaAsyncIterator) seen.push(x instanceof Promise ? `promise ${await x}` : x); + console.log("async iterator sync", show(seen)); // A generator that has started keeps its position; one that finished stays done. const started = numbers(5);