Skip to content

fix(cfg): catch and post-labeled-break segments wrongly unreachable (#57) - #67

Merged
ericsssan merged 2 commits into
mainfrom
fix/57-cfg-reachability
Jun 25, 2026
Merged

ericsssan merged 2 commits into
mainfrom
fix/57-cfg-reachability

Conversation

@ericsssan

Copy link
Copy Markdown
Owner

Fixes #57.

Two segment-reachability bugs in code_path.zig caused no-unreachable false positives. The JS runner reconstructs reachability from the serialized seg_reachable table, so node_reachable (a separate loop-only pass) masked these — they're only visible in the segment graph.

Bug 2 — catch unreachable when the try body ends with return

try { bar(); return; } catch (err) { return err; }   // catch wrongly marked dead

first_throwable_called was set only by explicit throw and return <expr>. A bare throwable call (bar()) + an argless return set it for neither, so the catch was marked dead. Fix: a new throwable event — the parser emits it for Call/Member/New/Yield expressions evaluated in a try body (gated on a new in_try_body flag; a single predictable bool check in addNode), and the CFG's makeFirstThrowablePathInTryBlock makes the catch reachable from the try entry. try_context is now saved/cleared across enterCodePath/exitCodePath, so a throwable in a nested function does NOT make the outer catch reachable.

Bug 1 — break <label> to a labeled block

A: { break A; } foo();   // foo() wrongly marked unreachable

makeBreakLabeled only walked loop_context, so a break to a labeled block found no target. Fix: a LabelBreakContext for non-loop labels — label_open (non-loop) pushes it, label_close pops and merges the break targets with the body's normal exit into the post-label segment. Labeled loops are unchanged.

Validation (against the serialized seg_reachable)

  • Catch reachable for call / yield / member-write throwables; still dead for an empty try (try { return; }) and for a throwable in a nested function — matching ESLint.
  • foo() reachable after A: { break A; }; still dead after a block that always returns; labeled-loop break unchanged.
  • Full suite green; TS conformance 17910/17913 · 1210/1223; babel 1928/1928 · 1548/1548; test262 3966/3966 · 1389/1389; semantic sweep 0 crashes, scope/symbol/ref/diagnostic totals byte-identical to main (the change alters only segment reachability, which the sweep doesn't tally).

)

Two segment-reachability bugs in code_path.zig caused no-unreachable false
positives (the JS runner reconstructs reachability from the serialized
seg_reachable table, so node_reachable — a separate loop-only pass — masked them).

Bug 2 (try/catch): `first_throwable_called` was set only by `throw` and
`return <expr>`, so `try { bar(); return; } catch {}` (a bare throwable call +
argless return) marked the catch dead. Add a `throwable` event: the parser emits
it for Call/Member/New/Yield expressions evaluated in a try body (gated on a new
`in_try_body` flag — a single predictable bool check in addNode), and the CFG's
new makeFirstThrowablePathInTryBlock makes the catch reachable from the try
entry. try_context is now saved/cleared across enterCodePath/exitCodePath so a
throwable in a nested function does NOT make the outer catch reachable.

Bug 1 (labeled break): makeBreakLabeled only walked loop_context, so `break A`
to a labeled BLOCK (`A: { break A; } foo()`) found no target and foo() was
disconnected. Add a LabelBreakContext for non-loop labels: label_open(non-loop)
pushes it, label_close pops and merges the break targets with the body's normal
exit into the post-label segment. Labeled loops are unchanged.

Validated against the serialized seg_reachable: catch reachable for call/yield/
member, still dead for an empty try and for nested-function throwables; foo()
reachable after a labeled-block break, still dead after a returning block;
labeled-loop break unchanged. Full suite green; TS conformance 17910/17913 ·
1210/1223; babel 1928/1928 · 1548/1548; semantic sweep 0 crashes with
byte-identical scope/symbol/ref/diagnostic totals.
Per review: errdefer-restore `in_try_body` so a recoverable parse error inside a
try body doesn't leak the flag into error-recovered code (harmless — the CFG
drops the spurious throwable events — but cleaner).

Tests: the `A: { if (x) break A; bar(); }` case was a false guard (the `bar()`
fall-through made foo reachable even on buggy main); replace with the
`{ if (x) break A; return; }` form where the break is the only path to foo
(0 pre-fix → 1 post-fix). Add nested-label (`A: B: { break A; }` / `break B`),
nested-block break, member-READ and `new` throwable cases — distinct code paths
the original tests didn't discriminate.
@ericsssan
ericsssan merged commit d897a53 into main Jun 25, 2026
2 checks passed
@ericsssan
ericsssan deleted the fix/57-cfg-reachability branch June 25, 2026 09:21
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.

cfg: catch block (and post-labeled-break statement) wrongly marked unreachable

1 participant