truncateLoopStacks method
Restores every loop stack to depths, forgetting the loops that are
removed — so a loop entered again later starts from scratch instead of
resuming a stale iterator or list.
Implementation
void truncateLoopStacks(LoopStackDepths depths) {
for (final node in loopNodeStack.skip(depths.nodes)) {
forInIteratorMap.remove(node);
loopEntryDepths.remove(node);
}
for (final node in awaitForNodeStack.skip(depths.awaitFor)) {
loopEntryDepths.remove(node);
}
// SCE16: an await-for being left owns a live subscription. Cancelling it is
// what stops the producer — an `async*` generator resumes at its `yield`
// and runs its `finally` blocks, and an infinite stream stops arriving.
// Leaving it uncancelled is how a `break` used to mean nothing to the
// stream.
//
// NOT awaited, because every caller of this is a synchronous exit path in
// the state machine (break, continue, loop end, an exception unwinding).
// The cancel still happens promptly — it is the ORDERING against code after
// the loop that is not guaranteed, which is why a test that observes a
// generator's `finally` has to await a turn first.
for (final iterator in awaitForIteratorStack.skip(depths.awaitFor)) {
unawaited(iterator.cancel().catchError((Object _) {}));
}
_shrink(loopNodeStack, depths.nodes);
_shrink(loopEnvironmentStack, depths.environments);
_shrink(loopInitializedStack, depths.initialized);
_shrink(awaitForNodeStack, depths.awaitFor);
_shrink(awaitForIteratorStack, depths.awaitFor);
_shrink(awaitForHasElementStack, depths.awaitFor);
if (awaitForNodeStack.isEmpty) {
currentAwaitForList = null;
currentAwaitForIndex = null;
}
}