From 9c39aefdb643f2d767cc96d694b99da4f3496f2c Mon Sep 17 00:00:00 2001 From: Mick Cooney Date: Thu, 15 Oct 2026 20:32:49 +0100 Subject: [PATCH] test(r): pin the CFG edge structure and CDG labels for empty if arms Review finding: an empty then/else arm loses its cond-true/cond-false edge. Not reproducible as lost information: an empty `{}` arm (like a missing `else`) is the header's seq fall-through edge, the same convention as every other CFG visitor, and control dependence labels it as the complement of the sibling explicit arm. Probed on the current code: `if (c) {} else work()` gives c-cond-false->work() with CDG c->work():F, and `if (c) work() else {}` gives CDG c->work():T. Adds exact edge-structure and CDG-label tests (all passed on the unmodified visitor) and documents the fall-through convention in the visitor header. No behaviour change. --- gitnexus/src/core/ingestion/cfg/visitors/r.ts | 4 ++ gitnexus/test/unit/cfg/r-visitor.test.ts | 57 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/gitnexus/src/core/ingestion/cfg/visitors/r.ts b/gitnexus/src/core/ingestion/cfg/visitors/r.ts index a351a3d71..9f7d62d5f 100644 --- a/gitnexus/src/core/ingestion/cfg/visitors/r.ts +++ b/gitnexus/src/core/ingestion/cfg/visitors/r.ts @@ -21,6 +21,10 @@ * - break / next / return(…) -> `break` / `continue` / `return` * - straight-line -> `seq` * + * A fall-through arm (no `else`, or an empty `{}` arm) produces no block and is the + * header's `seq` edge to the join; control dependence labels it as the complement of + * the sibling arm's explicit `cond-true` / `cond-false`, so no branch sense is lost. + * * Every loop (including `repeat` and `while (TRUE)`) gets the structural * `header -> loopExit` `cond-false` edge, so EXIT stays reverse-reachable and * the post-dominator / control-dependence pass is never skipped for a function diff --git a/gitnexus/test/unit/cfg/r-visitor.test.ts b/gitnexus/test/unit/cfg/r-visitor.test.ts index d065af6b7..2e691c729 100644 --- a/gitnexus/test/unit/cfg/r-visitor.test.ts +++ b/gitnexus/test/unit/cfg/r-visitor.test.ts @@ -76,6 +76,63 @@ describe('R CfgVisitor — branches', () => { ]); }); + // An empty `{}` arm produces no block, so that arm is the fall-through edge from the + // header (`seq`), exactly like a missing `else`. The sibling arm keeps its explicit + // cond-true / cond-false kind and control dependence reads the fall-through arm as + // its complement, so no branch sense is lost (the same convention as every visitor). + it('empty then with a real else: cond-false arm plus a seq fall-through, CDG label F', () => { + const cfg = r.cfgOf(`f <- function(c) {\n if (c) {} else work()\n after()\n}\n`); + expect(shape(cfg)).toEqual([ + 'ENTRY -seq-> c', + 'after() -seq-> EXIT', + 'c -cond-false-> work()', + 'c -seq-> after()', + 'work() -seq-> after()', + ]); + expect(computeControlDependence(cfg).edges).toEqual([ + { controllerBlock: block(cfg, 'c'), dependentBlock: block(cfg, 'work()'), label: 'F' }, + ]); + }); + + it('real then with an empty else: cond-true arm plus a seq fall-through, CDG label T', () => { + const cfg = r.cfgOf(`f <- function(c) {\n if (c) work() else {}\n after()\n}\n`); + expect(shape(cfg)).toEqual([ + 'ENTRY -seq-> c', + 'after() -seq-> EXIT', + 'c -cond-true-> work()', + 'c -seq-> after()', + 'work() -seq-> after()', + ]); + expect(computeControlDependence(cfg).edges).toEqual([ + { controllerBlock: block(cfg, 'c'), dependentBlock: block(cfg, 'work()'), label: 'T' }, + ]); + }); + + it('both arms empty: a single seq edge and no control dependence (nothing differs)', () => { + for (const body of ['if (c) {} else {}', 'if (c) {}']) { + const cfg = r.cfgOf(`f <- function(c) {\n ${body}\n after()\n}\n`); + expect(shape(cfg)).toEqual(['ENTRY -seq-> c', 'after() -seq-> EXIT', 'c -seq-> after()']); + expect(computeControlDependence(cfg).edges).toEqual([]); + } + }); + + it('empty then followed by an else-if: the chain keeps its own explicit arms', () => { + const cfg = r.cfgOf(`f <- function(p, q) {\n if (p) {} else if (q) a()\n after()\n}\n`); + expect(shape(cfg)).toEqual([ + 'ENTRY -seq-> p', + 'a() -seq-> after()', + 'after() -seq-> EXIT', + 'p -cond-false-> q', + 'p -seq-> after()', + 'q -cond-true-> a()', + 'q -seq-> after()', + ]); + expect(computeControlDependence(cfg).edges).toEqual([ + { controllerBlock: block(cfg, 'p'), dependentBlock: block(cfg, 'q'), label: 'F' }, + { controllerBlock: block(cfg, 'q'), dependentBlock: block(cfg, 'a()'), label: 'T' }, + ]); + }); + it('else-if chain nests through the alternative', () => { const cfg = r.cfgOf(`f <- function(p, q) {\n if (p) a() else if (q) b() else c()\n}\n`); expect(shape(cfg)).toEqual([