Skip to content

Interpreter: labeled loop containing await/yield fails in async functions and async generators #770

@nickna

Description

@nickna

Summary

In the interpreter, a labeled iteration statement (label: for/while/... {}) that contains an await or yield fails, because labeled statements have no async execution handler: RegistryDispatch.DispatchStmtAsync falls back to the synchronous ExecuteLabeledStatement, which runs the loop body via the sync Execute/Evaluate path. That path cannot suspend, so an await inside throws "'await' can only be used inside async functions" and a yield inside throws a raw YieldException.

Repro

// await inside a labeled loop in a plain async function
async function f() {
  outer: for (let i = 0; i < 2; i++) {
    for (let j = 0; j < 2; j++) {
      const x = await Promise.resolve(i * 10 + j);
      console.log("v" + x);
      if (j === 0) break outer;
    }
  }
}
f();
// Interpreter: Runtime Error: 'await' can only be used inside async functions.
// Node: v0
// yield inside a labeled loop in an async generator
async function* g() {
  outer: for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
      try { yield i * 10 + j; if (j === 1) break outer; } finally { console.log("fin" + i + j); }
    }
  }
}
async function main() { for await (const v of g()) console.log("v" + v); }
main();
// Interpreter: fin00 then "Runtime Error: ...YieldException..."
// Node: v0, fin00, v1, fin01

Compiled mode is correct for both (it has a full async-generator/await state machine). The plain (non-labeled) sibling loops work in the interpreter — only the labeled form falls back to sync.

Cause / fix direction

Two pieces are needed:

  1. An async handler for Stmt.LabeledStatement (an ExecuteLabeledStatementAsync mirroring the sync ExecuteLabeledStatement but running the inner statement via ExecuteStatementAsync).
  2. Labeled break/continue adoption in the async loop handlers (ExecuteForAsyncVT, ExecuteWhileAsyncVT, ExecuteDoWhileAsyncVT, ExecuteForOfAsync, ExecuteForInAsync). These currently only honor unlabeled break/continue (TargetLabel == null); they do not drain _pendingLoopLabels / carry LabelNames the way the sync loops do (Compiled: chained label on a for re-runs initializer on continue <outerLabel> (interpreter handles it) #580). Adding (1) without (2) would regress labeled loops that today work via the sync fallback (a labeled continue would propagate out of the loop instead of advancing it).

Notes

  • Pre-existing; surfaced while rewriting the interpreter async generator to a lazy coroutine (which runs the body through the real async execution path). The compiled-only AsyncGeneratorTryFinallyTests.LabeledBreakToOuterLoop_RunsInterveningFinally / LabeledContinueToOuterLoop_RunsInterveningFinally cover the compiled side; the interpreter side is this gap.

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