Getter-aware iterator reads, IteratorClose, and timeout-safe iteration (interpreter)#982
Merged
Merged
Conversation
… iteration The interpreter's iterator protocol (EnumerateWithIteratorProtocol) read the iterator result's `done`/`value` via a raw field accessor that bypassed accessor getters, never checked the VM timeout token, and never performed IteratorClose. Three ECMA-262 fixes (interpreter-only; compiled mode emits its own iterator IL): 1. Read `done`/`value` through Get() (getter-aware, prototype-walking), reading `value` only when not done (7.4.4/7.4.5). A result with a throwing `value`/`done` accessor (Test262 poisoned-iterator tests) previously never fired the getter, so `done` stayed falsy and the loop spun forever. 2. Honor `_vmTimeoutToken` in the loop. A non-terminating iterator under the Test262 harness left a CPU-pegged background (orphan) thread after the runner returned Timeout; the accumulating orphans starved later tests and made the interpreted baseline flaky under load. The check unwinds the enumerator so the thread exits. 3. IteratorClose (7.4.6): wrap the loop in try/finally so an abandoned iteration (for-of break/throw, a spread or Array.from element callback throwing) invokes the iterator's return(). `iteratorDone` suppresses the close on normal exhaustion. Array.from(items, mapFn) now applies mapfn DURING iteration (lazy foreach) instead of materializing the whole source first, so a throwing mapfn surfaces immediately (no infinite-iterator hang), triggers IteratorClose, and observes mid-iteration mutations (23.1.2.1 step 6.g). Test262 interpreted baseline: +6 (4 Timeout->Pass, 2 Fail->Pass), 0 regressions. Full xUnit suite 14410/0; +3 iterator regression tests.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Background
Started as an investigation into the long-"unexplained" interpreted Test262 baseline flake. Finding: the baseline is fully deterministic in isolation — 7 consecutive runs (default 6 workers + oversubscribed 24 workers on 12 cores) are byte-identical to each other and to the committed baseline. The per-realm work (#981) already removed the cross-worker state race; that was never this flake.
The flake is load-induced timeout flips: workers are long-lived subprocesses pulling from a shared queue (non-deterministic test→worker assignment), and under heavy concurrent machine load tests near the 15s timeout flip Pass↔Timeout depending on scheduling. The amplifier was a real interpreter bug (below): timed-out infinite-loop tests leaked CPU-pegged orphan threads that accumulated and starved later tests.
Changes (interpreter-only — compiled mode emits its own iterator IL)
Interpreter.EnumerateWithIteratorProtocolhad three gaps, all fixed here:Getter-aware
done/valuereads. It read the iterator result'sdone/valuevia a raw field accessor that bypassed accessor getters (_getters). A result with a throwingvalue/doneaccessor (Test262 poisoned-iterator tests) never fired the getter, sodonestayed falsy and the loop spun forever. Now reads viaGet()(getter-aware, prototype-walking), readingvalueonly when not done (ECMA-262 §7.4.4/§7.4.5).Timeout-safe iteration. The
while(true)never checked the VM timeout token, so a non-terminating iterator under the Test262 harness left a background (orphan) thread spinning at 100% CPU after the runner returned Timeout. The accumulating orphans were the flake amplifier. Now checks_vmTimeoutTokeneach step and unwinds the enumerator so the thread exits. (No-op in the CLI, where the token is non-cancellable.)IteratorClose (§7.4.6). The loop is wrapped in
try/finallyso an abandoned iteration —for-ofbreak/throw, or a spread /Array.fromelement callback throwing — invokes the iterator'sreturn(). AniteratorDoneflag suppresses the close on normal exhaustion. Generalizes to all iterator consumers (for-of, spread,yield*, destructuring).Array.from(items, mapFn)(ArrayStaticBuiltIns) now appliesmapFnduring iteration (lazyforeach) instead of materializing the whole source via.ToList()first, so a throwingmapFnsurfaces immediately (no infinite-iterator hang), triggers IteratorClose, and observes mid-iteration mutations (ECMA-262 §23.1.2.1 step 6.g).Results
Test262 interpreted baseline: +6, 0 regressions
Array/from/iter-get-iter-val-err.js: Timeout → PassArray/from/iter-map-fn-err.js: Timeout → Passcall/spread-err-mult-err-itr-value.js: Timeout → Passcall/spread-err-sngl-err-itr-value.js: Timeout → PassObject/fromEntries/iterator-not-closed-for-throwing-done-accessor.js: Fail → PassArray/from/elements-updated-after.js: Fail → PassFull xUnit suite 14410 / 0. Added 3 interpreter-only iterator regression tests (all use finite/breakable iterators so they terminate regardless of the fix — no hang risk; the throwing/non-terminating shapes are guarded by the bounded Test262 baseline). Compiled baseline untouched.
Notes / follow-ups
Interpreter.Statements.csis mostly re-indentation for thetry/finally; the logic change is ~40 lines.