From 5d284b8f5f53ef923dbfc9e643741def2cbf65df Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Sun, 14 Jun 2026 21:19:01 +0000 Subject: [PATCH] fix(cfg): wire Swift multi-catch throw edges to every handler (#2195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit visitDo routed the protected body's throw edge only to handlerEntries[0], so a do { try r() } catch A {} catch {} left the 2nd..Nth catch handlers UNREACHABLE from ENTRY — orphaned blocks whose error bindings + def/use facts were stranded in a dead component (a soundness gap for idiomatic Swift typed multi-catch). Swift tries the catch clauses in order and the thrown type is unknown at CFG time, so every protected block may reach ANY clause: edge each protected block to every handlerEntry. Found by the per-language CFG/CDG verification swarm (reproduced: 2-catch=1, 3-catch=3 unreachable blocks). swift suite 26 passed, tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- gitnexus/src/core/ingestion/cfg/visitors/swift.ts | 10 +++++++--- gitnexus/test/unit/cfg/swift-visitor.test.ts | 11 +++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/gitnexus/src/core/ingestion/cfg/visitors/swift.ts b/gitnexus/src/core/ingestion/cfg/visitors/swift.ts index 19bb85a32..e19279c05 100644 --- a/gitnexus/src/core/ingestion/cfg/visitors/swift.ts +++ b/gitnexus/src/core/ingestion/cfg/visitors/swift.ts @@ -740,10 +740,14 @@ class SwiftCfgWalk { const bodyRes = bodyNode ? this.visitSeq(this.statementsOf(bodyNode)) : null; this.handlers.pop(); - // Conservative exceptional edges: every protected-region block → the handler. - if (catchBlocks.length > 0) { + // Conservative exceptional edges: every protected-region block → EACH catch + // handler. Swift tries the catch clauses in order until one matches; the + // thrown type is unknown at CFG time, so any protected block may reach ANY + // clause. Edging only the first handler orphaned the 2nd..Nth catch blocks + // (unreachable from ENTRY, stranding their error bindings + def/use facts). + if (handlerEntries.length > 0) { for (let b = protectedStart; b < this.builder.blockCount; b++) { - this.builder.edge(b, doHandler, 'throw'); + for (const handler of handlerEntries) this.builder.edge(b, handler, 'throw'); } } diff --git a/gitnexus/test/unit/cfg/swift-visitor.test.ts b/gitnexus/test/unit/cfg/swift-visitor.test.ts index 3944875e7..7a46bb9ed 100644 --- a/gitnexus/test/unit/cfg/swift-visitor.test.ts +++ b/gitnexus/test/unit/cfg/swift-visitor.test.ts @@ -238,6 +238,17 @@ describe('Swift CfgVisitor — do/catch (error handling)', () => { expect(reaches(cfg, thr, block(cfg, 'done()'))).toBe(false); expect(reachable(cfg, block(cfg, 'done()'))).toBe(true); // via the if false branch }); + + it('multi-catch: EVERY catch handler is reachable from ENTRY (#2195)', () => { + // The protected body can throw an error matching ANY clause, so the 2nd..Nth + // catch must not be orphaned — the bug routed the throw edge only to the + // first handler, leaving later handlers unreachable from ENTRY. + const cfg = swift.cfgOf(`func f() { do { try r() } catch A { ha() } catch { hb() } }`); + expect(reachable(cfg, block(cfg, 'ha()'))).toBe(true); + expect(reachable(cfg, block(cfg, 'hb()'))).toBe(true); + // the protected `try r()` reaches both handlers. + expect(reaches(cfg, block(cfg, 'try r()'), block(cfg, 'hb()'))).toBe(true); + }); }); describe('Swift CfgVisitor — defer (LIFO scope-exit)', () => {