From fcec1a341d1d7a8506a1bd8fa0d8ab1fea70b6b2 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Mon, 15 Jun 2026 20:51:40 -0700 Subject: [PATCH] Fix #672: reject top-level / non-async `for await...of` in the type checker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A `for await...of` drives the async-iterator protocol and is only valid in an async context, exactly like an `await` expression or `await using`. The type checker flagged those two (CheckAwait / CheckUsingDeclaration) but never checked the `await` modifier on `Stmt.ForOf`, so a top-level `for await` — or one inside a plain function — slipped through type checking and was then run as an ordinary synchronous `for...of`. Iterating an async generator that way fails at runtime in both modes with a misleading "not iterable" error. Reject it in VisitForOf with the same `'await' is only valid inside an async function.` diagnostic (TS1308) the await-expression and `await using` paths already use. SharpTS does not support top-level await, so this is consistent with the existing up-front rejection of top-level `await` expressions, instead of silently degrading to a sync loop. Adds both-mode regression tests for the top-level and non-async-function cases. --- .../SharedTests/AsyncGeneratorTests.cs | 36 +++++++++++++++++++ TypeSystem/TypeChecker.Statements.cs | 12 +++++++ 2 files changed, 48 insertions(+) diff --git a/SharpTS.Tests/SharedTests/AsyncGeneratorTests.cs b/SharpTS.Tests/SharedTests/AsyncGeneratorTests.cs index ad0ed02c..67a6ca7b 100644 --- a/SharpTS.Tests/SharedTests/AsyncGeneratorTests.cs +++ b/SharpTS.Tests/SharedTests/AsyncGeneratorTests.cs @@ -1,4 +1,5 @@ using SharpTS.Tests.Infrastructure; +using SharpTS.TypeSystem.Exceptions; using Xunit; namespace SharpTS.Tests.SharedTests; @@ -329,6 +330,41 @@ async function main() { Assert.Equal("first: 1\nfirst: 2\nsecond: 1\nsecond: 2\n", output); } + // #672: a top-level `for await...of` drives the async-iterator protocol, which requires an async + // context. SharpTS does not support top-level await, so — like a top-level `await` expression — + // it must be rejected by the type checker rather than silently degrading to a synchronous + // `for...of` (which then throws a misleading 'not iterable' runtime error in both modes). + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ForAwaitOf_TopLevel_RejectedByTypeChecker(ExecutionMode mode) + { + var source = """ + async function* g() { yield 1; yield 2; } + for await (const x of g()) console.log("top", x); + """; + + var ex = Assert.Throws(() => TestHarness.Run(source, mode)); + Assert.Contains("'await' is only valid inside an async function.", ex.Message); + } + + // #672: `for await...of` inside a non-async function is likewise an await outside an async + // context and must be rejected (TS conformance), not run as a synchronous `for...of`. + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ForAwaitOf_InNonAsyncFunction_RejectedByTypeChecker(ExecutionMode mode) + { + var source = """ + async function* g() { yield 1; yield 2; } + function notAsync() { + for await (const x of g()) console.log(x); + } + notAsync(); + """; + + var ex = Assert.Throws(() => TestHarness.Run(source, mode)); + Assert.Contains("'await' is only valid inside an async function.", ex.Message); + } + #endregion #region Async Generator .return() and .throw() Tests diff --git a/TypeSystem/TypeChecker.Statements.cs b/TypeSystem/TypeChecker.Statements.cs index c60c1e11..4642e014 100644 --- a/TypeSystem/TypeChecker.Statements.cs +++ b/TypeSystem/TypeChecker.Statements.cs @@ -825,6 +825,18 @@ internal VoidResult VisitFor(Stmt.For stmt) internal VoidResult VisitForOf(Stmt.ForOf stmt) { + // `for await...of` drives the async-iterator protocol, so — like an `await` expression or + // `await using` — it is only valid inside an async function. SharpTS does not support + // top-level await, so a top-level (or otherwise non-async-context) `for await` is rejected + // here rather than silently degrading to a synchronous `for...of`, which would then fail at + // runtime with a misleading 'not iterable' error (#672). Mirrors CheckAwait / CheckUsingDeclaration. + if (stmt.IsAsync && !_inAsyncFunction) + { + throw new TypeCheckException( + "'await' is only valid inside an async function.", + stmt.Variable.Line, tsCode: "TS1308"); + } + TypeInfo iterableType = CheckExpr(stmt.Iterable); // Now that generator yield-type inference draws from the `yield` / `yield*` operands (#548), the