In an SSA intermediate form where every name is assigned exactly once, what does a phi node at a control-flow merge do?
answer
- one assignment per name, statically
- renaming is easy on a straight line
- merges break it
- operands paired with incoming edges
- removed as edge copies, never executed
basics
~20 sA phi node defines a new version of a value at a merge point, selecting among the versions that arrive on each incoming edge. It preserves the single-assignment rule where two branches would otherwise both assign the same name.
solid answer
~50 sStatic single assignment renames values so that each name is assigned by exactly one instruction: an assignment to `x` on one branch becomes `x1`, an assignment on the other becomes `x2`. At the block where the branches merge, code after the merge must read whichever version actually arrived — but naming either one would be wrong, and reusing `x` would break the rule. A **phi node** at the head of the merge block resolves this: `x3 = phi(x1 from the then-edge, x2 from the else-edge)` defines a third version whose value is chosen by the edge control came in on. Phi is a notation, not something a processor runs. When the compiler leaves SSA on the way to machine code, each phi becomes ordinary copies placed on the incoming edges — a copy of `x1` into `x3`'s location at the end of the then-block, and of `x2` at the end of the else-block.
code
pseudocode · 15 lines// before renaming
if cond goto L_then else goto L_else
L_then: x = 1
goto L_join
L_else: x = 2
goto L_join
L_join: y = x + 1
// after renaming into single-assignment form
L_then: x1 = 1
goto L_join
L_else: x2 = 2
goto L_join
L_join: x3 = phi(x1 from L_then, x2 from L_else)
y1 = x3 + 1go deeper
Recall that some compilers rename each assignment to a fresh version of the variable, so that a name points at exactly one place where its value was produced.
Explain the renaming and why a point where two branches rejoin cannot be renamed without help, then write the merge node with one operand per incoming edge.
Show why the form pays off in practice — provenance by lookup rather than by analysis — and say what happens to the merge nodes on the way to machine code, since no processor runs one.
Judge whether the form is worth it for your compiler: the discipline speeds many rewrites but constrains the representation, adds a destruction step, and leaves aliased storage outside the guarantee.
## The invariant SSA buys **Static single assignment (SSA)** is a discipline on an intermediate representation: **every name is the destination of exactly one instruction in the whole procedure**. It is achieved by renaming. Each time the source assigns to a variable, the renamer produces a fresh version — `x1`, `x2`, `x3` — and each read is rewritten to the version that reaches it. The payoff is stated in one line: **for any operand, the instruction that produced it is found by looking up the name, with no dataflow analysis to reconstruct it.** On a representation where names are reassigned, "what value does this read see?" depends on which paths reach this point and what each of them last wrote; it is an analysis. Under SSA it is a lookup. Every rewrite that has to know where a value came from — and most do — starts from cheaper ground. The word **static** matters. The rule is about the *program text*: one assignment per name in the code. It is not a claim about runtime. A loop body executes its instructions many times, and the storage behind a name is written on each iteration. ## Why merges need something extra Renaming works effortlessly along a straight line. It breaks at a **merge**: a block with more than one predecessor. ``` if cond: x = 1 else: x = 2 y = x + 1 ``` After renaming the branches, one holds `x1 = 1` and the other `x2 = 2`. The read in `y = x + 1` must see one of them, and which one is not known until the program runs. Three non-solutions: - name `x1` — wrong on the else path; - assign both branches into one name — breaks single assignment, which is the entire value of the form; - leave the read referring to "either" — then a name has no single definition and the lookup property is gone. ## The phi node The answer is a pseudo-instruction at the **head of the merge block**: ``` x3 = phi(x1 from L_then, x2 from L_else) ``` It defines exactly one new name, `x3`, and its operands are paired with the predecessor edges. Its meaning: *the value is the operand associated with the edge along which control actually arrived.* The single-assignment rule survives, and every use after the merge refers to `x3`. Properties worth knowing: - Phi nodes sit **at the top of a block**, before any ordinary instruction. - A block's phis are understood to take effect **together, on entry**, not one after another. Ordering them against each other would be meaningless, since they all read values from the predecessor. - A phi has **one operand per predecessor edge** — three predecessors, three operands. - Phis are needed only where two different definitions genuinely converge. Compilers place them by computing, for each definition, the blocks where its influence first meets another's, and repeating until the set stops growing. Inserting phis everywhere would be correct but wasteful. ## Leaving SSA No machine has a phi instruction. On the way down, the compiler **destroys SSA**: each phi is replaced by ordinary copies placed **on the incoming edges** — at the end of the then-block, copy `x1` into the location for `x3`; at the end of the else-block, copy `x2`. Every path then writes the merged location before reaching the merge, and no selection is needed at runtime. Many of those copies disappear when the versions end up sharing storage anyway. ## Limits — where the tidy story stops | Holds for | Not automatic for | |---|---| | Values held in SSA names | Locations reachable through more than one path, such as aliased memory | | Scalar quantities the compiler can rename freely | Storage whose identity is not known statically | This is the caveat a strong candidate volunteers. SSA's lookup property covers **the values that were renamed**. A store into a location that some other name might also reach cannot be renamed away by the same trick; compilers either keep such locations out of SSA or model memory itself with a parallel scheme. "SSA means you never have to do a dataflow analysis again" is too strong. ## What interviewers listen for The answer that lands states the invariant, shows why a merge forces the issue, gives a phi with edge-paired operands, and closes with the fact that phis are removed as copies rather than executed. A candidate who describes phi as a runtime test on which branch was taken has the mechanism backwards — that would cost a check the compiler has gone out of its way to avoid.
- Why is a phi node placed at the head of its block rather than anywhere inside it?Its operands are values as they exist on the incoming edges, so it must take effect on entry, before any instruction of the block has run. All phis in a block are therefore understood to happen together at that point, which is also why ordering them relative to each other is meaningless.
- What does a loop header's phi look like?A loop header has two predecessors — the edge entering the loop and the back edge — so its phi has two operands: the version defined before the loop, and the version defined at the end of the body. That single node is how a loop-carried value is expressed without breaking single assignment.
- Does SSA make dataflow analysis unnecessary?It makes one particular question — which instruction defined this operand — a lookup rather than an analysis, and that simplifies many rewrites. Questions about memory locations that several names may reach, and about values that flow through such locations, still need analysis.
- What can go wrong when phis are turned back into copies?If several phis in one block swap values between locations, replacing them with copies in sequence can clobber a value another phi still needs. The copies represent a simultaneous assignment, so the destruction step must order them carefully, and sometimes introduce an extra temporary to break a cycle.
Think of a document store where an edit never overwrites a file but saves a new numbered version. At a point where two editors' chains rejoin, you need a note on the desk saying which version came in through which door — otherwise a later reader has no single file to open.
saying these in an interview costs you the question
- Describes a phi node as a runtime test of which branch ran
- Says SSA means values never change while the program runs
- Thinks phi nodes are executed in order within a block
- Gives a phi fewer operands than the block has predecessors
- Claims SSA removes the need for any analysis of memory