The body of a for await loop runs as concurrent tasks, not one iteration at a time. Iterations interleave, and writes from one iteration are lost to another, under gc, rc and none.
let keep: number[] = [];
let log = "";
async function* numbers() { yield 7; yield 8; yield 9; }
async function main() {
for await (const x of numbers()) {
log += "<" + x;
keep.push(x);
log += ">";
}
print(log, keep.length);
}
Expected: <7><8><9> 3, every time.
Six JIT runs per model on main with #440 (PR #497) applied:
| Output |
gc |
rc |
none |
<7><8><9> 3 |
1 |
3 |
4 |
<7<8><9> 3 (interleaved) |
1 |
|
|
<7><9> 2, <8><9> 2, ><9> 2 (a push lost) |
4 |
|
|
<7> 3, <8> 3, <9> 3 (appends lost) |
|
3 |
1 |
| no output at all, exit 0 |
|
|
1 |
In TypeScript, the body of a for await runs one iteration at a time, in order: the next value is not requested until the body has finished.
Cause
MLIRGenImpl::mlirGen(ForStatement) (lib/TypeScript/MLIRGenStatements.cpp), when the loop has await:
- creates an
async.create_group before the loop;
- wraps each iteration's body in its own
async.execute, and adds its token to the group with async.add_to_group;
- waits for the whole group once after the loop, with
async.await_all.
So every iteration's body is started as a task without waiting for the previous one. The bodies run concurrently on the async runtime's threads, and race on whatever they share: here log, a string rebuilt by +=, and keep.push, which can reallocate the array.
Fix
Await each iteration's token before going on to the next iteration (async.await on the execute's token in the loop body, as await does since #440), and drop the group and the async.await_all. Then the next value is requested only after the body is done.
Found while working on #440 (listed there under "Also seen").
🤖 Generated with Claude Code
The body of a
for awaitloop runs as concurrent tasks, not one iteration at a time. Iterations interleave, and writes from one iteration are lost to another, under gc, rc and none.Expected:
<7><8><9> 3, every time.Six JIT runs per model on main with #440 (PR #497) applied:
<7><8><9> 3<7<8><9> 3(interleaved)<7><9> 2,<8><9> 2,><9> 2(a push lost)<7> 3,<8> 3,<9> 3(appends lost)In TypeScript, the body of a
for awaitruns one iteration at a time, in order: the next value is not requested until the body has finished.Cause
MLIRGenImpl::mlirGen(ForStatement)(lib/TypeScript/MLIRGenStatements.cpp), when the loop hasawait:async.create_groupbefore the loop;async.execute, and adds its token to the group withasync.add_to_group;async.await_all.So every iteration's body is started as a task without waiting for the previous one. The bodies run concurrently on the async runtime's threads, and race on whatever they share: here
log, a string rebuilt by+=, andkeep.push, which can reallocate the array.Fix
Await each iteration's token before going on to the next iteration (
async.awaiton the execute's token in the loop body, asawaitdoes since #440), and drop the group and theasync.await_all. Then the next value is requested only after the body is done.Found while working on #440 (listed there under "Also seen").
🤖 Generated with Claude Code