Give every tensor value one definition while tracking storage separately
Read SSA definitions, predecessor-selected values, dominance, and tensor-versus-buffer semantics without treating unique names as proof of non-aliasing.
On this page
The question for today
What you’ll understand
- Follow tensor definitions and uses through straight-line and branching regions.
- Explain predecessor selection using block arguments or phi nodes.
- Separate logical value identity from mutable backing storage.
An intermediate representation needs names whose meaning survives transformations. Static single assignment gives each SSA value one definition, making use-def relationships explicit. That is a statement about values in the representation, not a promise that every name owns separate memory. Tensor-level IR can describe immutable logical results and later reuse storage when analysis permits it. Buffer-level IR can contain two distinct pointer values that refer to the same bytes. Keeping these layers separate prevents a deceptively tidy graph from hiding a mutation dependency.
Rename results so dependencies remain visible
For a straight-line region, write x0 for the input, x1 for twice x0, and x2 for x1 plus one. A later reassignment in source code becomes another definition rather than changing what x1 means. This makes a transformation's obligations visible: replacing x2 requires preserving the values reaching its users. For a tensor, shape and element type attach to the value as part of its type or metadata. Matching shapes does not make two definitions interchangeable when they compute different contents.
Definitions also obey scope and dominance rules appropriate to their region. In an ordinary control-flow SSA region, a value used after a branch must be available along the executed incoming path. Moving a definition from one arm into a shared successor can leave the other arm with no valid definition. The LLVM 20.1.0 verifier checks well-formedness conditions, including dominance; it does not prove that an otherwise well-formed rewrite computes the same function. A legal-looking name is only the beginning of a correctness argument.
A merge selects the value from the incoming path
Suppose a condition chooses either x0+1 or twice x0. The shared successor needs the result produced by the arm that actually ran. LLVM commonly expresses this with a phi instruction; MLIR control-flow regions use block arguments supplied by predecessor terminators. Neither notation means both alternatives were executed, averaged, or concatenated. A transformation that speculatively executes both arms additionally needs permission to perform every operation that previously ran only conditionally, including any potential effects or invalid accesses.
For a matrix-valued merge, incoming tensor types must satisfy the operation's type rules, but equal type does not erase path identity. If both arms yield shape [2,2], the selected contents can still differ in every entry. A compiler must preserve the predecessor-to-value association when rewriting control flow. This is especially important for loop-carried values: each iteration receives a new logical state, even when lowering later implements that state with a small fixed set of buffers.
Logical immutability still permits carefully justified reuse
Consider tensor t0=[[1,2],[3,4]] and t1 obtained by replacing its upper-left entry with nine. A tensor operation can define t1 while t0 remains the original logical value. If a later operation reads t0[0,0], in-place reuse that overwrites its only available storage would incorrectly return nine instead of one. If no old contents are needed and the buffer is writable, reuse may be valid. The analysis must inspect actual uses and aliasing, rather than infer safety from different SSA names.
The network lab supplies a small pure expression whose intermediate definitions can be named on paper. Reset, set both input x=1 and weight w=2, and name the product, biased logit, and sigmoid result before Play or Step. Predict product two, logit 1.5, and output about 0.8176. Reset and set both x=1,w=1 to compare the new definitions. The instrument evaluates one scalar expression; it neither constructs SSA blocks nor allocates aliased tensors, so it cannot certify a proposed buffer-reuse transformation.
Work it through
Select a matrix result from one branch
Let x0=[[1,2],[3,4]]. The left arm defines xL=x0+1 elementwise, the right arm defines xR=2x0, and the merge receives the right arm. A later result is z=xmerge+x0. Use exact integer arithmetic for this example.
Compute the two possible values separately: xL=[[2,3],[4,5]] and xR=[[2,4],[6,8]]. These are possible path results, not a requirement to execute both arms.
The right predecessor supplies xR to the merge, so xmerge=[[2,4],[6,8]]. Choosing xL would preserve shape but violate the incoming-path association.
Add the original x0 to obtain z=[[3,6],[9,12]]. The original input remains available as a logical value; any storage-reuse plan must preserve the contents needed by this final use.
The valid result is [[3,6],[9,12]]. A structural verifier can catch invalid definitions or types, but testing this path association is a separate semantic obligation.
The living notebook
Explore the model
Interactive lab · network
Name the definitions of one pure computation
Fresh Reset → set both x=1,w=2: predict product 2, logit 1.5 and sigmoid ≈0.8176, then Play or Step. Reset and set both x=1,w=1 for comparison. The scalar network has no SSA builder, branching interpreter, or storage-alias model.
- Product · wx
- 1.2
- Logit · wx − 0.5
- 0.7
- Sigmoid output
- 0.6682
One inspectable sigmoid unit, σ(wx − 0.5). Playback traces the computation graph. There is no training, attention mechanism or hidden deep network. Parameter changes pause playback and recompute the model at the current elapsed time. Reset restores both parameters and returns time to zero. Playback starts paused. Adjust either parameter, then choose Play or Step.
Look forExplicit definitions clarify dependencies, while storage safety needs additional analysis.
Pause & reason
Check your understanding
Your turn
Try it yourself
A tensor operation creates t1 by changing t0[0,0] from one to nine, and a later operation returns both t1 and the original t0[0,0]. May lowering simply overwrite the only buffer holding t0 before that read?
Take a moment to make a prediction or work it out on paper. Open a hint when you need one.
Give me a hint
Write the required pair of logical results first. Ask whether overwriting preserves the value consumed by the later extraction.
Compare with the solution
The required results are the changed tensor t1 and scalar one from t0. An overwrite followed by reading that same changed location would return nine, so this simple lowering is incorrect. A copy, an appropriately scheduled preserved read, or another proven arrangement could satisfy the contract. Distinct names t0 and t1 do not themselves choose among those implementations or prove that the original bytes are no longer needed.
Keep the idea
Take it with you
- SSA gives values single definitions and makes their uses inspectable.
- Block arguments and phi nodes preserve predecessor-specific value selection.
- Logical tensor immutability and mutable storage reuse are different analysis layers.
Read further
Original sources and deeper explanations to follow at your own pace.
Optional progress, saved in this browser when available. Revisit any lesson at any time.
Lesson not marked complete.