Files
libvct-spec/spec/005-lowering.md
T

17 KiB

005 — Lowering

  • Status: Draft
  • Normative language: MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are 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 and Config MUST 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 LoweringMap for 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 load from 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:
    1. if the block is sealed, recurse to its predecessors, place a Phi at the nearest join, and return the phi result;
    2. 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.
  • 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_volatile or is_atomic MUST 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_volatile nor is_atomic. mem2reg and SROA MAY 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_mutable means the storage is written at runtime. may_change_at_runtime means the observable value may differ between reads because of external state. Lowering MUST consult is_runtime_mutable for the representation decision and MUST NOT conflate the two.

4.6 Phi

VIR has an explicit, LLVM-style Phi instruction.

  • L-28. A Phi MUST 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 LoweringMap from 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 Trait instance, 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 LoweringMap MUST 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 complex or carrying the NoOptimize request 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 expand defer, and lowering MUST expand each deferred call at every scope-exit path and then erase the defer marker. 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 residual defer marker 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 layout attribute 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.

  1. Single definition. Every SSA value has exactly one defining instruction.
  2. Def dominates use. Every use is dominated by its definition. A phi operand used in predecessor P MUST either dominate P or be the phi result itself (the loop case).
  3. Terminated blocks. Every basic block ends in exactly one terminator.
  4. Phi arity. Every Phi has one operand per predecessor, and each named predecessor is real.
  5. CFG consistency. Every successor edge from a block has a matching predecessor edge on the target, and the entry block has no predecessors.
  6. Type consistency. Every instruction's operands and result match the VIR type system.
  7. 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.
  8. 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