dominance-frontier-ssa-builder
Below is a complete minimal SSA form constructor for a subset of JavaScript. It implements the full pipeline: Lengauer–Tarjan dominator tree → dominance frontiers → ϕ-function insertion at iterated dominance frontiers → variable renaming.
| Type | Fields | Description |
|---|---|---|
CfgBlock |
id, instructions[], succ[], pred[] |
A basic block in the CFG |
Insn |
op, dest, args[] |
A three-address instruction |
Cfg |
entry, blocks[] |
The whole control-flow graph |
1. Lengauer–Tarjan dominators — DFS numbering + semi-dominator computation with path compression via union-find. Runs in near-linear time (O(|V| + |E| \cdot \alpha(|V|))).
2. Dominance frontiers — For each block with ≥2 predecessors, walk up the dominator tree and mark the frontier.
3. ϕ-function insertion — For each variable, collect its definition sites, then iteratively add ϕ-functions at dominance frontier blocks until a fixed point (the iterated dominance frontier).
4. Renaming — Walk the dominator tree pre-order. For each definition, push a fresh version number; for each use, read the current top-of-stack. Fill ϕ-function arguments by mapping predecessor edge indices → current version of the base variable. Pop on exit from each block.
#!/usr/bin/env node
/**
* Minimal SSA Form Constructor
* ============================
* Input: CFG { entry, blocks: [{ id, instructions, succ, pred }] }
* Output: CFG with ϕ-nodes inserted and variables renamed to SSA form
*/
// ── 1. Lengauer–Tarjan Dominator Tree ──────────────────────────────────────
function computeDominators(cfg) {
const { blocks, entry } = cfg;
const n = blocks.length;
const semi = new Int32Array(n);
const parent = new Int32Array(n);
const best = new Int32Array(n);
const idom = new Int32Array(n).fill(-1);
const ancestor = new Int32Array(n).fill(-1);
const label = new Int32Array(n);
const vertex = new Int32Array(n).fill(-1);
const dfs = new Int32Array(n).fill(-1);
const bucket = Array.from({ length: n }, () => []);
let time = 0;
function dfsVisit(v) {
if (dfs[v] !== -1) return;
dfs[v] = time;
vertex[time] = v;
semi[v] = time;
label[v] = v;
time++;
for (const w of blocks[v].succ) {
if (dfs[w] === -1) { parent[w] = v; dfsVisit(w); }
}
}
dfsVisit(entry);
function compress(v) {
if (ancestor[ancestor[v]] !== -1) {
compress(ancestor[v]);
if (semi[label[ancestor[v]]] < semi[label[v]])
label[v] = label[ancestor[v]];
ancestor[v] = ancestor[ancestor[v]];
}
}
function find(v) {
if (ancestor[v] === -1) return v;
compress(v);
return semi[label[ancestor[v]]] < semi[label[v]]
? label[ancestor[v]] : label[v];
}
function link(v, w) { ancestor[w] = v; }
// Reverse DFS order
for (let i = time - 1; i > 0; i--) {
const w = vertex[i]; if (w === -1) continue;
for (const p of blocks[w].pred) {
if (dfs[p] === -1) continue;
const u = find(p);
if (semi[u] < semi[w]) semi[w] = semi[u];
}
bucket[vertex[semi[w]]].push(w);
link(parent[w], w);
for (const v of bucket[parent[w]]) {
const u = find(v);
idom[v] = (semi[u] < semi[v]) ? u : parent[w];
}
bucket[parent[w]] = [];
}
for (let i = 1; i < time; i++) {
const w = vertex[i]; if (w === -1) continue;
if (idom[w] !== vertex[semi[w]]) idom[w] = idom[idom[w]];
}
idom[entry] = entry;
const domTree = Array.from({ length: n }, () => []);
for (let v = 0; v < n; v++) {
if (v !== entry && idom[v] !== -1) domTree[idom[v]].push(v);
}
return { idom, domTree };
}
// ── 2. Dominance Frontiers ─────────────────────────────────────────────────
function computeDominanceFrontiers(cfg, idom) {
const n = cfg.blocks.length;
const frontiers = Array.from({ length: n }, () => new Set());
for (let b = 0; b < n; b++) {
const preds = cfg.blocks[b].pred;
if (preds.length < 2) continue;
for (const p of preds) {
let runner = p;
while (runner !== idom[b] && runner !== -1 && runner !== idom[runner]) {
frontiers[runner].add(b);
runner = idom[runner];
}
}
}
return frontiers.map(s => [...s].sort((a, b) => a - b));
}
// ── 3. ϕ-Function Insertion at Iterated Dominance Frontiers ────────────────
function insertPhiFunctions(cfg, defSites, domFrontiers) {
const n = cfg.blocks.length;
const hasPhi = Array.from({ length: n }, () => new Set());
const worklist = [];
for (const [v, blocks] of defSites)
for (const b of blocks) worklist.push({ var: v, block: b });
while (worklist.length > 0) {
const { var: v, block: b } = worklist.pop();
for (const d of domFrontiers[b]) {
if (!hasPhi[d].has(v)) {
hasPhi[d].add(v);
if (!defSites.get(v).has(d)) {
defSites.get(v).add(d);
worklist.push({ var: v, block: d });
}
}
}
}
for (let b = 0; b < n; b++) {
if (hasPhi[b].size > 0) {
const sortedVars = [...hasPhi[b]].sort();
for (const v of sortedVars) {
cfg.blocks[b].instructions.unshift({
op: 'phi', dest: v,
args: new Array(cfg.blocks[b].pred.length).fill(v),
blockId: b,
});
}
}
}
return hasPhi;
}
// ── 4. Variable Renaming ───────────────────────────────────────────────────
function isVarName(s) {
return typeof s === 'string' && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(s);
}
function renameVariables(cfg, idom, domTree, entry) {
const stacks = new Map();
const counters = new Map();
function initVar(name) {
if (!stacks.has(name)) { stacks.set(name, []); counters.set(name, 0); }
}
for (const block of cfg.blocks) {
for (const insn of block.instructions) {
if (insn.dest && isVarName(insn.dest)) initVar(insn.dest);
for (const arg of insn.args) if (isVarName(arg)) initVar(arg);
}
}
const freshName = name => {
const idx = counters.get(name);
counters.set(name, idx + 1);
stacks.get(name).push(idx);
return `${name}.${idx}`;
};
const topName = name => {
const s = stacks.get(name);
return (!s || s.length === 0) ? name : `${name}.${s[s.length - 1]}`;
};
// phiInfo[blockId] = [{ phiInsn, baseVar, predMap: { predBlock → argIdx } }]
const phiInfo = new Map();
for (const block of cfg.blocks) {
for (const insn of block.instructions) {
if (insn.op !== 'phi') break;
if (!phiInfo.has(block.id)) phiInfo.set(block.id, []);
const baseVar = insn.dest.replace(/\.\d+$/, '');
const predMap = {};
for (let ai = 0; ai < insn.args.length; ai++)
predMap[block.pred[ai]] = ai;
phiInfo.get(block.id).push({ phiInsn: insn, baseVar, predMap });
}
}
function renameBlock(b) {
const block = cfg.blocks[b];
const pushed = [];
// 4a. Rename ϕ destinations
for (const insn of block.instructions) {
if (insn.op === 'phi' && insn.dest && isVarName(insn.dest.replace(/\.\d+$/, ''))) {
pushed.push(freshName(insn.dest.replace(/\.\d+$/, '')));
insn.dest = pushed[pushed.length - 1];
} else if (insn.op !== 'phi') break;
}
// 4b. Rename ordinary instructions (uses then defs)
for (const insn of block.instructions) {
if (insn.op === 'phi') continue;
insn.args = insn.args.map(a => isVarName(a) ? topName(a) : a);
if (insn.dest && isVarName(insn.dest)) {
pushed.push(freshName(insn.dest));
insn.dest = pushed[pushed.length - 1];
}
}
// 4c. Fill ϕ arguments in successors
for (const succId of block.succ) {
const info = phiInfo.get(succId) || [];
for (const { phiInsn, baseVar, predMap } of info) {
const argIdx = predMap[b];
if (argIdx !== undefined) phiInsn.args[argIdx] = topName(baseVar);
}
}
// 4d. Recurse to children in dominator tree
for (const child of domTree[b]) renameBlock(child);
// 4e. Pop names defined in this block
for (const name of pushed)
stacks.get(name.replace(/\.\d+$/, '')).pop();
}
renameBlock(entry);
return cfg;
}
// ── 5. Main entry point ────────────────────────────────────────────────────
function buildSSA(cfg) {
const cloned = JSON.parse(JSON.stringify(cfg));
const { idom, domTree } = computeDominators(cloned);
cloned._idom = idom;
cloned._domTree = domTree;
const frontiers = computeDominanceFrontiers(cloned, idom);
cloned._frontiers = frontiers;
const defSites = new Map();
for (let b = 0; b < cloned.blocks.length; b++)
for (const insn of cloned.blocks[b].instructions)
if (insn.dest) {
if (!defSites.has(insn.dest)) defSites.set(insn.dest, new Set());
defSites.get(insn.dest).add(b);
}
insertPhiFunctions(cloned, defSites, frontiers);
renameVariables(cloned, idom, domTree, cloned.entry);
return cloned;
}
module.exports = { buildSSA, computeDominators, computeDominanceFrontiers,
insertPhiFunctions, renameVariables };
The implementation passes **11 tests** covering the full spectrum of CFG shapes and SSA properties:
| # | Test | What it verifies |
|---|------|------------------|
| 1 | **If-else diamond** | ϕ at join block has correct arity (2) and renamed arguments (`x.N`). `ret` uses the ϕ result. |
| 2 | **Straight-line** | No ϕ inserted when each variable is defined once. Uses refer to the correct version (`x.0`). |
| 3 | **Loop (back edge)** | ϕ for induction variable `i` at loop header. First arg = `i.0` (pre-header), second arg = renamed back-edge value from B2 `add`. |
| 4 | **Multiple variables in diamond** | Two ϕ-nodes at the join (one per variable). The use (`add` at B3) reads renamed versions. |
| 5 | **Dominator tree** | `idom` computed correctly for a 5-block graph: B0=B0, B1=B0, B2=B1, B3=B1, B4=B1. Children lists match. |
| 6 | **Dominance frontiers** | DF(B2) = {B4}, DF(B3) = {B4}, DF(B1) = {}. |
| 7 | **Nested if-else (chained diamonds)** | Iterated dominance frontier places ϕ at both inner (B4) and outer (B5) joins. |
| 8 | **No redefinitions** | Zero ϕ-nodes when each variable is defined in exactly one block. |
| 9 | **If-without-else** | ϕ at merge has one correctly renamed arg from the `if` path and the original version from the skip path. |
| 10 | **SSA property** | Every `dest` name (`x.0`, `x.1`, …) appears exactly once in the entire output. |
| 11 | **Use before def** | Undefined variables pass through unrenamed. |
**Example output** — diamond test:
```
Block B0: (pred=[], succ=[1,2])
x.0 = def 1
Block B1: (pred=[0], succ=[3])
x.1 = add x.0 1
Block B2: (pred=[0], succ=[3])
x.2 = add x.0 2
Block B3: (pred=[1,2], succ=[])
x.3 = φ([0:x.1], [1:x.2])
null = ret x.3
```
**Example output** — loop test:
```
Block B1: (pred=[0,2], succ=[2,3])
i.1 = φ([0:i.0], [1:i.3])
c.0 = φ([0:c], [1:c.1])
c.1 = lt i.1 10
Block B2: (pred=[1], succ=[1])
i.3 = add i.1 1
```
Every definition (`x.0`, `x.1`, `x.2`, `x.3`, `i.1`, `i.3`, `c.0`, `c.1`) is unique — the defining SSA property. Uses always refer to the version live at that program point.
---{"model": "claude-sonnet-4-20250514", "problem_class": "dominance-frontier-ssa-builder", "result": "passed", "tests": 11}The implementation is in ~/ssa_builder.py and implements the classic Cytron et al. SSA construction algorithm (1991) with these components:
1. IR Layer (CFG, BasicBlock, Instr) — A simplified three-address-code IR with:
- Instructions: mov, add/sub/mul (via binop), br, jmp, ret, phi
- Each instruction has an opcode, a destination variable, and a tuple of source operands
- Basic blocks form a CFG with predecessor/successor edges
2. Dominator Tree — Uses the iterative data-flow algorithm:
dom(b) = {b} ∪ (∩_{p ∈ preds(b)} dom(p))
Iterates to fixed point, then immediate dominator is the proper dominator with the largest dom set (closest to b).
3. Dominance Frontier — For each block with ≥2 predecessors, walks up the dominator tree from each predecessor to the join block's idom, adding the join to each node's DF.
4. Iterated Dominance Frontier (DF⁺) — Fixed-point iteration over DF sets to handle irreducible loops.
5. φ-Function Insertion — For each variable v defined in Defs(v), places φ(v) at every block in the iterated dominance frontier of Defs(v).
6. Variable Renaming — Top-down dominator tree traversal:
- Each phi destination gets a fresh SSA name (x_0, x_1, ...)
- Each use is replaced with the current top-of-stack for that variable
- Each definition pushes a fresh name onto the stack
- Phi operands in successor blocks are filled with the current reaching definition
Key design decisions:
- Immutable instructions (@dataclass(frozen=True)) — renaming replaces instructions rather than mutating them
- Hashable basic blocks (@dataclass(unsafe_hash=True)) — enables set/dict operations needed by DF computation
- Variable detection uses dynamic analysis (names used as destinations) rather than heuristics, making it robust across test cases
**Verification via 28 automated tests:**
| Test Group | Tests | What It Validates |
|---|---|---|
| **Diamond (if-else join)** | 6 | φ at join, SSA property, correct idom, use-def consistency |
| **Loop (back edge)** | 6 | φ at header, SSA property, correct idom(body)=header |
| **Irreducible CFG** | 5 | φ at multi-entry block A, SSA property, all placeholders filled |
| **Sealed (single block)** | 3 | No φ needed, SSA property |
| **If-without-else** | 3 | φ at exit where two paths meet |
| **Empty CFG** | 1 | Graceful handling of degenerate case |
| **Linear chain** | 4 | Linear renaming without φ, correct use-def chain |
**Correctness guarantees verified:**
- **Single-definition property**: Every SSA name (`x_0`, `x_1`, ...) is defined exactly once
- **Completeness**: All φ-function placeholders (`__phi_op_*`) are replaced with concrete SSA names
- **Use-def consistency**: Every use references a name that has a corresponding definition
- **Correct dominance**: Diamond join (bb4) is correctly dominated by bb1, not bb2
- **Irreducible CFG**: Multiple-entry loops get φ at every entry point
**Example: Diamond CFG transformation**
```
Before SSA: After SSA:
entry: entry:
x = 0 x_0 = mov c0
jmp bb1 jmp bb1
bb1: bb1:
y = x y_0 = mov x_0
br cond, bb2, bb3 br cond, bb2, bb3
bb2: bb2:
x = x + 1 x_1 = add x_0, c1
jmp bb4 jmp bb4
bb3: bb3:
x = x + 2 x_2 = add x_0, c2
jmp bb4 jmp bb4
bb4: bb4:
z = x + y x_3 = φ(x_1, x_2)
jmp exit z_0 = add x_3, y_0
exit: jmp exit
ret z exit:
ret z_0
```
**Edge cases handled:**
- **No branches (linear chain)**: No φ inserted; variables renamed sequentially
- **Single block**: No φ; simple renaming
- **Empty CFG**: Returns without error
- **Irreducible loops**: Fixed-point iteration over DF⁺ ensures complete φ placement
---{"model": "claude-sonnet-4-20250514", "problem_class": "dominance-frontier-ssa-builder", "result": "passed", "tests": 28}The implementation is in ~/ssa_builder.py and implements the classic Cytron et al. SSA construction algorithm (1991) with these components:
1. IR Layer (CFG, BasicBlock, Instr) — A simplified three-address-code IR with:
- Instructions: mov, add/sub/mul (via binop), br, jmp, ret, phi
- Each instruction has an opcode, a destination variable, and a tuple of source operands
- Basic blocks form a CFG with predecessor/successor edges
2. Dominator Tree — Uses the iterative data-flow algorithm:
dom(b) = {b} ∪ (∩_{p ∈ preds(b)} dom(p))
Iterates to fixed point, then immediate dominator is the proper dominator with the largest dom set (closest to b).
3. Dominance Frontier — For each block with ≥2 predecessors, walks up the dominator tree from each predecessor to the join block's idom, adding the join to each node's DF.
4. Iterated Dominance Frontier (DF⁺) — Fixed-point iteration over DF sets to handle irreducible loops.
5. φ-Function Insertion — For each variable v defined in Defs(v), places φ(v) at every block in the iterated dominance frontier of Defs(v).
6. Variable Renaming — Top-down dominator tree traversal:
- Each phi destination gets a fresh SSA name (x_0, x_1, ...)
- Each use is replaced with the current top-of-stack for that variable
- Each definition pushes a fresh name onto the stack
- Phi operands in successor blocks are filled with the current reaching definition
Key design decisions:
- Immutable instructions (@dataclass(frozen=True)) — renaming replaces instructions rather than mutating them
- Hashable basic blocks (@dataclass(unsafe_hash=True)) — enables set/dict operations needed by DF computation
- Variable detection uses dynamic analysis (names used as destinations) rather than heuristics, making it robust across test cases
**Verification via 28 automated tests:**
| Test Group | Tests | What It Validates |
|---|---|---|
| **Diamond (if-else join)** | 6 | φ at join, SSA property, correct idom, use-def consistency |
| **Loop (back edge)** | 6 | φ at header, SSA property, correct idom(body)=header |
| **Irreducible CFG** | 5 | φ at multi-entry block A, SSA property, all placeholders filled |
| **Sealed (single block)** | 3 | No φ needed, SSA property |
| **If-without-else** | 3 | φ at exit where two paths meet |
| **Empty CFG** | 1 | Graceful handling of degenerate case |
| **Linear chain** | 4 | Linear renaming without φ, correct use-def chain |
**Correctness guarantees verified:**
- **Single-definition property**: Every SSA name (`x_0`, `x_1`, ...) is defined exactly once
- **Completeness**: All φ-function placeholders (`__phi_op_*`) are replaced with concrete SSA names
- **Use-def consistency**: Every use references a name that has a corresponding definition
- **Correct dominance**: Diamond join (bb4) is correctly dominated by bb1, not bb2
- **Irreducible CFG**: Multiple-entry loops get φ at every entry point
**Example: Diamond CFG transformation**
```
Before SSA: After SSA:
entry: entry:
x = 0 x_0 = mov c0
jmp bb1 jmp bb1
bb1: bb1:
y = x y_0 = mov x_0
br cond, bb2, bb3 br cond, bb2, bb3
bb2: bb2:
x = x + 1 x_1 = add x_0, c1
jmp bb4 jmp bb4
bb3: bb3:
x = x + 2 x_2 = add x_0, c2
jmp bb4 jmp bb4
bb4: bb4:
z = x + y x_3 = φ(x_1, x_2)
jmp exit z_0 = add x_3, y_0
exit: jmp exit
ret z exit:
ret z_0
```
**Edge cases handled:**
- **No branches (linear chain)**: No φ inserted; variables renamed sequentially
- **Single block**: No φ; simple renaming
- **Empty CFG**: Returns without error
- **Irreducible loops**: Fixed-point iteration over DF⁺ ensures complete φ placement
---{"model": "claude-sonnet-4-20250514", "problem_class": "dominance-frontier-ssa-builder", "result": "passed", "tests": 28}