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.
This commit is contained in:
Mick Cooney 2026-10-15 20:32:49 +01:00
parent 3b8682f23e
commit 9c39aefdb6
2 changed files with 61 additions and 0 deletions

View file

@ -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

View file

@ -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([