17 KiB
005 — Lowering
- Status: Draft
- Normative language:
MUST,MUST NOT,SHOULD,SHOULD NOT, andMAYare to be interpreted as described in RFC 2119.
1. Purpose
Lowering translates annotated HIR into VIR. It is the bridge between the frontend's tree and the SSA/CFG form that the optimizer and both backends consume. It turns structured control flow into basic blocks and terminators, turns variables into SSA values or stack slots according to their traits, and records the correspondence between every source node and the VIR entities it produced.
Lowering is a pure translation. It discovers nothing and proves nothing. Every type, value, mutability fact, aliasing property, and call hint it needs is already present as a trait that the HIR optimizer left on the tree (see 004 — HIR Optimizer). When a required fact is absent or contradicts another, lowering does not guess: it reports an internal compiler error.
2. Scope
This file specifies the lowering contract, the translation of structured HIR constructs into a
control-flow graph, SSA construction, the trait-driven hybrid memory model, the Phi instruction
shape, the LoweringMap, region optimization barriers, the defer fallback, the VIR types that
lowering emits, and the invariants the output must satisfy.
It does not specify the HIR node model (see 003 — HIR), the trait vocabulary (see 002 — Traits), VIR instruction semantics (see 006 — VIR), or the optimizer passes (see 007 — VIR Optimizer).
3. Definitions and model
| Term | Meaning |
|---|---|
| Annotated HIR | An HIR tree on which every relevant node carries a complete trait set. |
| Epoch-valid trait | A trait whose recorded epoch equals the node's current epoch. Stale traits are not readable. A trait is invalidated when a rewrite makes it stale (see 002 — Traits). |
| Lowering unit | One HIR function. Lowering runs independently per function. |
| Basic block | A maximal straight-line sequence of VIR instructions with one entry and one terminator. |
| Sealed block | A block for which all predecessors are known. A block with a pending back-edge is unsealed. |
| Current-definition map | Per-block mapping from a source variable to the SSA value most recently written to it on the path into that point. |
| LoweringMap | The bidirectional correspondence between HIR nodes and the VIR entities produced from them. |
Lowering consumes HIR and produces VIR. It reads only the trait contract and HIR node identity (see 001 — Architecture). It MUST NOT read HIR structure to recover a fact that the trait model already carries.
4. Normative requirements
4.1 Contract
- L-1. The input MUST be annotated HIR whose traits are complete and epoch-valid.
- L-2. The output MUST be a VIR module in which every function is well-formed SSA (see 006 — VIR).
- L-3. Lowering MUST NOT perform discovery. It MUST NOT infer types, constant values, mutability, aliasing, or call effects that are not stated by traits.
- L-4. Lowering MUST validate trait completeness before it translates a function. A required trait that is missing, stale, or self-contradictory MUST raise an internal-compiler-error diagnostic (see 011 — Diagnostics). Lowering MUST NOT substitute a default, a guess, or a conservative fallback for a missing trait.
- L-5. Lowering MUST be a pure function of the annotated HIR and the
Config. The same input andConfigMUST produce byte-identical VIR regardless of thread count, per the determinism guarantee in 000 — Overview. - L-6. Lowering MUST be parallelizable per function. Two functions MUST NOT share mutable
lowering state. The only cross-function state is read-only, namely the module's trait graph and
the
LoweringMapfor already-lowered functions where a relationship endpoint crosses functions. - L-7. Emission order within a function MUST be deterministic. Where a construct admits
multiple equivalent orderings (for example, the order of predecessor operands in a
Phi), the order MUST be fixed by a documented rule, not by hash iteration.
4.2 Structured control flow to CFG
Lowering is a streaming recursive interpreter over the HIR tree. Structured constructs become blocks connected by terminators. Lowering MUST emit blocks and terminators, not a flat instruction stream with implicit jumps.
| HIR construct | Required VIR emission |
|---|---|
Statement sequence, block |
Instructions appended to the current block in source order. |
if (c) T else E |
condbr c, %then, %else; a %then block, an %else block, and a %join block. Values live past the construct receive a Phi in %join. |
while (c) B |
A %header block that evaluates c, ending in condbr c, %body, %exit; a %body block; a %latch block ending in br %header; an %exit block. |
for (init; c; step) B |
An %init block; a %header block for c; a %body block; a %step block that runs step then branches to %header; an %exit block. |
switch (v) |
A switch terminator whose case targets and default target are the arm entry blocks. |
break |
br to the exit block of the innermost enclosing loop or switch. |
continue |
br to the latch or step block of the innermost enclosing loop. |
return e |
ret of the lowered value of e; ret void when the function returns void. |
| End of a void function | ret void. |
| End of a non-void function without an explicit return | unreachable. |
- L-8. Every basic block MUST be terminated by exactly one terminator.
- L-9. A block that is not reachable by fallthrough MAY remain unreachable from the entry block as long as it is well-formed; unreachable blocks MAY persist until dead-code elimination removes them (see 006 — VIR, WF-11).
- L-10. The entry block MUST have no predecessors.
4.3 Expression translation
Expressions lower to temporaries and instructions.
- L-11. Each HIR expression that produces a runtime value MUST lower to exactly one VIR value: either an existing constant or a newly emitted instruction result. Aggregate-producing expressions MAY lower to a memory location instead.
- L-12. Operands MUST be lowered before the instruction that consumes them, in source order (left to right for binary operators, receiver before arguments for calls).
- L-13. A HIR constant that is comptime must lower to a VIR constant operand and MUST NOT produce a runtime instruction.
- L-14. A call MUST attach the call-site traits (
Inline,AlwaysInline,NoInline,TailCall,MustTail,NoTail,ColdCall,LikelyCall,UnlikelyCall, and the memory-effect requests) to the emitted call entity. Lowering MUST NOT decide whether to inline; that decision belongs to the VIR optimizer (see 007 — VIR Optimizer). - L-15. Reading a variable MUST follow the representation chosen in §4.5: a pure SSA read from
the current-definition map, or an explicit
loadfrom its stack slot.
4.4 SSA construction
SSA MUST be constructed with the sealed-block algorithm of Braun, Buchwald, Hack, Leißa, Mallon, and Zwinkau, "Simple and Efficient Construction of Static Single Assignment Form" (CC 2013). That algorithm is the reference method; the requirements below are the observable properties it guarantees.
- L-16. Construction MUST be a single pass interleaved with the streaming translation. Lowering MUST NOT run a separate dominance-frontier computation followed by iterated phi placement.
- L-17. Lowering MUST maintain a current-definition map per block. A write records the new value for the variable in the current block. A read consults the current block first.
- L-18. On a read for which the current block has no definition, lowering MUST:
- if the block is sealed, recurse to its predecessors, place a
Phiat the nearest join, and return the phi result; - if the block is not sealed, create an incomplete phi placeholder for the variable in that block and return it; the placeholder is resolved when the block is sealed.
- if the block is sealed, recurse to its predecessors, place a
- L-19. Lowering MUST apply trivial-phi elimination: a phi whose operands all resolve to the same value MUST be replaced by that value, and a phi that references only itself MUST be removed. This keeps construction linear in practice.
- L-20. Sealing a block MUST fill each incomplete phi with one operand per predecessor, each operand obtained by reading the variable in that predecessor, and MUST then recursively seal successors whose predecessors are now all known.
- L-21. A loop header MUST NOT be sealed until its back-edge has been emitted. Sealing early is the one mistake that makes the algorithm produce unsound phis.
- L-22. Phi operand order MUST follow a deterministic predecessor order (for example, ascending block identifier), so that output is reproducible.
4.5 Trait-driven hybrid memory model
Lowering MUST choose each value's representation from its traits alone.
| Condition | Representation |
|---|---|
ty is scalar AND NOT is_addressed AND NOT escapes AND NOT is_runtime_mutable |
Pure SSA value held in a register. |
is_addressed OR escapes OR is_runtime_mutable OR the type is an aggregate |
Stack slot: an alloca plus explicit load/store. |
is_volatile or is_atomic holds |
Forced to memory, never promoted to SSA. |
- L-23. A scalar that is not addressed, does not escape, and is not runtime-mutable MUST be kept as a pure SSA value.
- L-24. A value that is addressed, escapes, is runtime-mutable, or has aggregate type MUST be
given a stack slot with explicit
load/store. - L-25. A value carrying
is_volatileoris_atomicMUST be forced to memory and MUST NOT be promoted to SSA at any point, including by later optimizations. - L-26. Lowering's choice is not final for values that carry neither
is_volatilenoris_atomic.mem2regandSROAMAY promote a memory value back to SSA when traits confirm that the promotion is safe (see 007 — VIR Optimizer). Such a promotion MUST preserve the program's observable behavior. - L-27.
is_runtime_mutablemeans the storage is written at runtime.may_change_at_runtimemeans the observable value may differ between reads because of external state. Lowering MUST consultis_runtime_mutablefor the representation decision and MUST NOT conflate the two.
4.6 Phi
VIR has an explicit, LLVM-style Phi instruction.
- L-28. A
PhiMUST record one operand per CFG predecessor of its block. Each operand MUST be a(value, predecessor-block)pair. - L-29. The number of operands MUST equal the number of predecessors, and each named predecessor MUST be an actual predecessor of the phi's block.
Example:
%loop.header:
%i = phi i32 [ 0, %entry ], [ %i.next, %loop.latch ]
4.7 LoweringMap and trait transfer
- L-30. Lowering MUST build a
LoweringMapfrom each HIR node to the one or more VIR entities produced from it, and from each VIR entity back to its source HIR node. - L-31. Every VIR entity MUST carry its own
Traitinstance, populated from the source HIR node's trait. Attributes and requests MUST be transferred; the VIR optimizer reads them through the trait contract and does not consult HIR. - L-32. Every HIR relationship endpoint MUST be rewritten to the corresponding VIR entity via
the
LoweringMap, so that VIR relationship walks terminate in VIR entities without touching HIR. - L-33. The
LoweringMapMUST be total on HIR nodes that produce a VIR entity and MUST be stable for the lifetime of the module. The diagnostics service uses it to resolve VIR locations back to source (see 011 — Diagnostics). - L-34. After lowering, VIR MUST depend on the trait contract and HIR node identity only. It MUST NOT depend on HIR structure.
4.8 Region optimization barriers
- L-35. A HIR node marked
complexor carrying theNoOptimizerequest MUST be lowered inside a region-level optimization barrier. The barrier covers the node and the region it dominates as defined by the source construct, and is recorded on the VIR entities in that region. - L-36. VIR passes MUST NOT rewrite, reorder, delete, or hoist across a region barrier, and MUST treat the region as an opaque, side-effecting operation for alias and memory-effect analyses. Backends MAY emit a compiler fence for it (see 008 — C Backend).
4.9 Defer fallback
- L-37. When HIR optimization is disabled (
-O0), HIR does not expanddefer, and lowering MUST expand each deferred call at every scope-exit path and then erase thedefermarker. Expansion order MUST follow the reverse order of registration at each exit path. - L-38. When HIR optimization is enabled, HIR has already expanded
defer; lowering MUST NOT expand it a second time. A residualdefermarker reaching lowering when HIR optimization is on MUST be treated as an internal inconsistency under L-4.
4.10 VIR type system overview
The ty attribute already holds the node's VIR type (see 002 — Traits). It
is authoritative: lowering MUST read it and MUST NOT recompute a type from HIR structure.
| VIR type | Meaning |
|---|---|
void |
No value; the result type of effect-only calls and functions. |
iN |
An N-bit integer. N MUST be at least 1. i1 is the boolean type. |
f32, f64 |
IEEE-754 binary32 and binary64. Implementations MAY support f16, f80, f128. |
ptr<T> |
A pointer to T. The LLVM backend maps this to opaque ptr (see 009 — LLVM Backend). |
[N x T] |
A fixed-size array of N elements of T. |
{T1, T2, ...} |
A struct with the named field types. |
union {T1, ...} |
A union of the named member types. |
fn(T1, ...) -> R |
A function type with parameter types and return type R. |
- L-39. Aggregate values are first-class: they MAY be loaded, stored, passed, returned, and
named by a
Phi. - L-40. Aggregate indexing MUST use field indices, not byte offsets. The C backend leaves
aggregate layout to the C compiler except where the
layoutattribute pins an ABI (see 008 — C Backend).
5. Invariants
The VIR module released by lowering MUST satisfy all of the following. They are checked by the verifier after lowering and, in debug builds, after every subsequent pass.
- Single definition. Every SSA value has exactly one defining instruction.
- Def dominates use. Every use is dominated by its definition. A phi operand used in
predecessor
PMUST either dominatePor be the phi result itself (the loop case). - Terminated blocks. Every basic block ends in exactly one terminator.
- Phi arity. Every
Phihas one operand per predecessor, and each named predecessor is real. - CFG consistency. Every successor edge from a block has a matching predecessor edge on the target, and the entry block has no predecessors.
- Type consistency. Every instruction's operands and result match the VIR type system.
- LoweringMap total. Every value-producing HIR node maps to at least one VIR entity, and every VIR entity maps back to a source HIR node.
- Determinism. Re-lowering the same input produces byte-identical VIR.
6. Example
Source:
while (i < n) {
s = s + a[i];
i = i + 1;
}
Lowered shape (values elided):
entry:
br %loop.header
loop.header:
%i = phi i32 [ 0, %entry ], [ %i.next, %loop.latch ]
%s = phi i32 [ 0, %entry ], [ %s.next, %loop.latch ]
%cmp = icmp slt i32 %i, %n
condbr %cmp, %loop.body, %loop.exit
loop.body:
%addr = gep [4 x i32] %a, 0, %i
%elem = load i32 %addr
%s.mid = add i32 %s, %elem
br %loop.latch
loop.latch:
%s.next = add i32 %s.mid, 0
%i.next = add i32 %i, 1
br %loop.header
loop.exit:
ret void
%i and %s are scalars that are neither addressed, escaping, nor runtime-mutable, so they are
pure SSA values; their merge points in loop.header are phis. If s were addressed by a pointer,
lowering would instead allocate a slot for it and emit load/store around each access.
7. Cross-references
- 000 — Overview: determinism guarantee and conformance.
- 001 — Architecture: module boundaries; lowering is the SSA construction site.
- 002 — Traits: attribute, request, and relationship vocabulary; epochs.
- 003 — HIR: the tree lowering consumes, including
defer. - 004 — HIR Optimizer: who produces the annotated HIR.
- 006 — VIR: the target IR, its instructions, and its invariants.
- 007 — VIR Optimizer: who consumes lowered VIR.
- 008 — C Backend, 009 — LLVM Backend: consumers of VIR.
- 011 — Diagnostics: internal-compiler-error policy and source mapping.