From 1a1810a10331c69f5260a5efd7ac989c04637ceb Mon Sep 17 00:00:00 2001 From: Eric San Date: Thu, 25 Jun 2026 16:49:51 +0800 Subject: [PATCH 1/2] fix(cfg): catch and post-labeled-break segments wrongly unreachable (#57) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 `, 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. --- src/code_path.zig | 79 +++++++++++++++++++++++++++++++++++++++-- src/event_resolver.zig | 8 +++++ src/parser.zig | 38 ++++++++++++++++++-- src/scope_events.zig | 4 +++ tests/semantic_test.zig | 55 ++++++++++++++++++++++++++++ 5 files changed, 179 insertions(+), 5 deletions(-) diff --git a/src/code_path.zig b/src/code_path.zig index 5b204a4..da727b4 100644 --- a/src/code_path.zig +++ b/src/code_path.zig @@ -347,6 +347,15 @@ const LoopContext = struct { prev_break_target_is_switch: bool = false, }; +/// Break target for a labeled NON-loop statement (`A: { … break A; … }`). Loops +/// and switches have their own break handling; this covers the remaining case so +/// `break