Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions Jint.Tests/Runtime/AsyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> _values = new();
Expand Down
24 changes: 22 additions & 2 deletions Jint/Runtime/Interpreter/Statements/JintForInForOfStatement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)
{
Expand All @@ -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<ForOfSuspendData>(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)
Expand Down