docs: add libVCT public specification set (000-015)

This commit is contained in:
2026-09-13 15:56:53 -04:00
parent 591fdf3107
commit 765c91dabc
16 changed files with 3769 additions and 0 deletions
+304
View File
@@ -0,0 +1,304 @@
# 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](004-hir-optimizer.md)). 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](003-hir.md)), the trait vocabulary (see
[002 — Traits](002-traits.md)), VIR instruction semantics (see [006 — VIR](006-vir.md)), or the
optimizer passes (see [007 — VIR Optimizer](007-vir-optimizer.md)).
## 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](002-traits.md)). |
| **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](001-architecture.md)). 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](006-vir.md)).
- **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](011-diagnostics.md)). 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](000-overview.md).
- **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](006-vir.md), 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](007-vir-optimizer.md)).
- **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](007-vir-optimizer.md)). 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](011-diagnostics.md)).
- **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](008-c-backend.md)).
### 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](002-traits.md)). 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](009-llvm-backend.md)). |
| `[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](008-c-backend.md)).
## 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
- [000 — Overview](000-overview.md): determinism guarantee and conformance.
- [001 — Architecture](001-architecture.md): module boundaries; lowering is the SSA construction site.
- [002 — Traits](002-traits.md): attribute, request, and relationship vocabulary; epochs.
- [003 — HIR](003-hir.md): the tree lowering consumes, including `defer`.
- [004 — HIR Optimizer](004-hir-optimizer.md): who produces the annotated HIR.
- [006 — VIR](006-vir.md): the target IR, its instructions, and its invariants.
- [007 — VIR Optimizer](007-vir-optimizer.md): who consumes lowered VIR.
- [008 — C Backend](008-c-backend.md), [009 — LLVM Backend](009-llvm-backend.md): consumers of VIR.
- [011 — Diagnostics](011-diagnostics.md): internal-compiler-error policy and source mapping.