From 98452e302f46f032f3d20f6ffd13289106b1fe9b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 10:19:05 +0000 Subject: [PATCH 1/2] Initial plan From bd50f37400bec8e26e3d6ce4fd6b48344eff1ce0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 10:40:47 +0000 Subject: [PATCH 2/2] Fix: Jint returns cached first Promise result when awaiting inside a for...of loop Co-authored-by: lahma <171892+lahma@users.noreply.github.com> --- Jint.Tests/Runtime/AsyncTests.cs | 52 +++++++++++++++++++ .../Statements/JintForInForOfStatement.cs | 24 ++++++++- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/Jint.Tests/Runtime/AsyncTests.cs b/Jint.Tests/Runtime/AsyncTests.cs index dce83366f1..f820d2596e 100644 --- a/Jint.Tests/Runtime/AsyncTests.cs +++ b/Jint.Tests/Runtime/AsyncTests.cs @@ -1917,6 +1917,58 @@ async function main() { Assert.Equal(4950, result.AsInteger()); } + // ======================================================================== + // For-of loop with await inside body + // ======================================================================== + + [Fact] + public void AwaitInsideForOfLoopShouldReturnDistinctResults() + { + // Verifies that each await inside a for-of loop returns a distinct result based on + // the current iteration value, not a cached result from a prior iteration. + var engine = new Engine(); + var result = engine.Evaluate(""" + function fetch_async(url) { + return new Promise((resolve) => { + resolve(`response for ${url}`); + }); + } + (async function() { + var text = ""; + text += await fetch_async("a") + "\n"; + text += await fetch_async("b") + "\n"; + text += await fetch_async("c") + "\n"; + var urls = ["d", "e", "f"]; + for (let url of urls) { + text += await fetch_async(url) + "\n"; + } + return text; + })() + """); + result = result.UnwrapIfPromise(); + Assert.Equal("response for a\nresponse for b\nresponse for c\nresponse for d\nresponse for e\nresponse for f\n", result.AsString()); + } + + [Fact] + public void AwaitInsideForOfLoopShouldWorkWithVarBinding() + { + // Test with var (not let) binding in for-of loop + var engine = new Engine(); + var result = engine.Evaluate(""" + (async function() { + var results = []; + var items = [1, 2, 3]; + for (var item of items) { + var r = await Promise.resolve(item * 10); + results.push(r); + } + return results.join(","); + })() + """); + result = result.UnwrapIfPromise(); + Assert.Equal("10,20,30", result.AsString()); + } + class TestAsyncClass { private readonly ConcurrentBag _values = new(); diff --git a/Jint/Runtime/Interpreter/Statements/JintForInForOfStatement.cs b/Jint/Runtime/Interpreter/Statements/JintForInForOfStatement.cs index 06efea6272..e130e0d956 100644 --- a/Jint/Runtime/Interpreter/Statements/JintForInForOfStatement.cs +++ b/Jint/Runtime/Interpreter/Statements/JintForInForOfStatement.cs @@ -241,6 +241,15 @@ private Completion BodyEvaluation( { while (true) { + // Clear completed awaits cache when starting a new iteration for async functions. + // This prevents stale cached await results from prior iterations being reused. + // Only clear when NOT resuming mid-iteration (resuming needs the cache intact). + var asyncFnLoop = engine.ExecutionContext.AsyncFunction; + if (asyncFnLoop is not null && !asyncFnLoop._isResuming) + { + asyncFnLoop._completedAwaits?.Clear(); + } + DeclarativeEnvironment? iterationEnv = null; JsValue nextValue; @@ -467,8 +476,7 @@ private Completion BodyEvaluation( return new Completion(status, nextValue, context.LastSyntaxElement); } - // Before executing body, save state in case of yield (generators only, not async functions) - // ForOfSuspendData is generator-specific for sync for-of loops + // Before executing body, save state in case of yield/await suspension. var generator = engine.ExecutionContext.Generator; if (generator is not null) { @@ -478,6 +486,18 @@ private Completion BodyEvaluation( data.IterationEnv = iterationEnv; } + // For async functions with sync iterators, save state so that if an await + // in the body suspends execution, we can resume at the correct iteration + // without restarting the whole loop from scratch. + var asyncFnBody = engine.ExecutionContext.AsyncFunction; + if (iteratorKind == IteratorKind.Sync && asyncFnBody is not null) + { + var asyncData = asyncFnBody.Data.GetOrCreate(this, iteratorRecord); + asyncData.AccumulatedValue = v; + asyncData.CurrentValue = nextValue; + asyncData.IterationEnv = iterationEnv; + } + var result = stmt.Execute(context); // Clear current value after successful body execution (not suspended)