Skip to content

Fix compiled chained-label for-loop hang (#580) and optional-chain string-method await over-eval (#627)#715

Merged
nickna merged 3 commits into
mainfrom
wrk/dazzling-chatterjee-defb8e
Jun 16, 2026
Merged

Fix compiled chained-label for-loop hang (#580) and optional-chain string-method await over-eval (#627)#715
nickna merged 3 commits into
mainfrom
wrk/dazzling-chatterjee-defb8e

Conversation

@nickna

@nickna nickna commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Fixes two compiled-mode parity bugs and documents a third as a deliberate deferral.

#580 — chained label on a for re-ran the initializer on continue <outerLabel> (compiled hang)

a: b: for (…) with continue to the outer label of the chain hung in compiled mode: only the innermost label was handed to the loop, so the outer one fell into the legacy "mark continue target before the loop" path, ahead of the for initializer, which then re-ran forever. The interpreter already handled it.

Fix: a compiled loop scope now carries a set of label names (IReadOnlyList<string> LabelNames) instead of a single string?. EmitLabeledStatement flattens the label chain (new UnwrapLabelChain helper) and, when it bottoms out in a loop, parks every label via a pending list that the loop drains at EnterLoop — mirroring the interpreter's _pendingLoopLabels. FindLabeledLoop matches by membership. Threaded through CompilationContext, StatementEmitterBase, ILEmitter, and the sync/async generator emitters (for-of uses a drain-early-pass-many IReadOnlyList<string> EnterLoop overload; while/do-while/for-in adopt via the pending default).

Tests: converted the interpreter-only ChainedLabels_For_ContinueOuter to both-modes and added continue-inner, break-outer, triple-chain, and for-of-chain (all both-modes).

#627 — optional-chain string-method over-evaluated an awaited argument (compiled)

For o?.substring(await x, n) where o is a non-string object lacking substring, the compiled await-safe path spilled the argument before the isinst string split (the #614 emit-once position), so the await ran even though the chain short-circuits to undefined. The interpreter short-circuits without evaluating the argument.

Fix: a dedicated await-safe helper (EmitOptionalChainStringMethodAwaitSafe) resolves the dispatch and null-checks the generic fn before evaluating the argument (short-circuiting first), then evaluates it once in a shared block both live dispatches reach — re-testing isinst string after the spill (the receiver local survives the suspension; a plain bool flag would reset on MoveNext re-entry). Preserves the #614 emit-once invariant and the pre-existing HasOptionalInChain quirk (returns undefined rather than throwing — that's shared by both modes). The non-await path keeps the baseline, which already short-circuits before its inline args.

Tests: new RunOptionalChainObserve helper (top-level flag-setter folded into the awaited arg — compiled supports neither a nested function nor a nested async function inside the async body) covering missing-on-non-string (ran=false), the slice sibling (ran=false), and a string receiver (ran=true).

#650 — for(let) mutate-and-restore capture — assessed and deferred (no code change)

for (let i …) { i=i+10; g.push(()=>i); i=i-10 } yields 10,11,12 compiled vs 0,1,2 interp/node. Compiled snapshots the loop var at closure-creation time; the spec wants the per-iteration binding's end-of-body value. The correct fix is per-iteration reference-capture cells (the compiled analog of CreatePerIterationEnvironment) — a 5-area rework across all three variable resolvers + the for-loop emitters + closure capture + the analyzer, and it reintroduces boxing. Disproportionate and regression-prone for this exotic, low-priority edge, so it stays deferred (with an actionable design note added to the issue). The interpreted-only regression test is retained.

Follow-up issues filed

Testing

Full suite green except ClusterModuleTests.Fork_WorkersShareHttpPort, a known pre-existing flake (HTTP-port race under parallel load, unrelated to these changes) that passes in isolation.

nickna added 3 commits June 15, 2026 20:38
…erLabel> (compiled)

A chain of labels on a loop (a: b: for) used to hand only the innermost label
to the loop; the outer label fell into the legacy 'mark continue before the
loop' path, so 'continue <outer>' re-ran the for initializer forever (compiled
hang). Loop scopes now carry a set of label names: EmitLabeledStatement flattens
the chain and parks every label, and the loop drains them all so continue/break
to any of them runs the loop's own step. Mirrors the interpreter's
_pendingLoopLabels. Adds both-modes tests (continue outer/inner, break outer,
triple chain, for-of chain). Async state-machine chained labels tracked in #704.
…uit (compiled await parity)

For an optional-chain call to a dispatchable-string-method name (substring,
charAt, ...) on a non-string receiver lacking it, with an awaited argument, the
compiled await-safe path spilled the args before the isinst-string split, so the
await ran even though the chain short-circuits to undefined. The interpreter
short-circuits without evaluating the arg. Now a dedicated await-safe helper
resolves the dispatch and null-checks the generic fn BEFORE evaluating the arg
(short-circuiting first), then evaluates it once in a shared block both live
dispatches reach, re-testing isinst string after (recv survives the suspension).
Restores interpreter/compiled parity; keeps the pre-existing HasOptionalInChain
quirk (undefined rather than TypeError) in both modes.
@nickna nickna merged commit 6b714a7 into main Jun 16, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant