Control Flow
Turning statements into a graph you can reason about: basic blocks, edges, natural loops, dominators and the dominance frontier that SSA construction needs.
Once the statements are instructions, the `if` is gone. What remains is a directed graph: blocks of straight-line code as nodes, the ways control can pass between them as edges. Every question about "when does this run" becomes a question about paths.
A maximal run of instructions with one way in and one way out. If the first instruction executes, all of them do — and that single guarantee is what makes the block, rather than the instruction, the unit every analysis is written against.
An `if` becomes two blocks and a join. A `while` becomes three blocks and an edge that points backwards. Then there is the edge nobody expects: the one from a two-way branch straight into a merge, which has no safe place to put anything — and which AtlasLang reports rather than guesses at.
The `while` was destroyed by lowering, so the optimizer has to find the loop again in the graph. A back edge `n -> h` where `h` dominates `n` is a loop; the body is `h` plus everything that reaches `n` without going through `h`. That is a definition, not a heuristic.
A dominates B if every path from the entry to B goes through A — so if B runs, A has already run. Loops make the graph cyclic, so this cannot be computed in one traversal: the algorithm iterates until a full pass changes nothing, and the second pass is not optional.
Every block has exactly one immediate dominator, so the relation is a tree — and it is a different tree from the CFG, drawn on the same nodes. Draw it separately, because the edges mean something the CFG edges do not, and half the confusion about dominance comes from overlaying them.
The frontier of A is the set of blocks where A stops being guaranteed — the first blocks reachable from A that A does not dominate. That is exactly the set of places where a definition in A might not be the one that arrives, which is exactly where a phi node goes.