10 KiB
009 — LLVM Backend
- Status: Draft
- Normative language:
MUST,MUST NOT,SHOULD,SHOULD NOT, andMAYare to be interpreted as described in RFC 2119.
1. Purpose
The LLVM backend (vct.backend.llvm) translates optimized VIR into textual LLVM IR
(the .ll assembly format). It is the alternate backend. It runs after VIR
optimization and is a pure translation: it performs no optimization and no discovery.
libVCT never invokes llvm-as, opt, or llc, and never links against LLVM.
Converting .ll to bitcode, an object file, or an executable is the caller's job.
2. Scope
This file specifies the backend's contract: 1:1 SSA mapping, the type mapping, the full trait-to-attribute and trait-to-metadata mapping, intrinsic selection, module scaffolding, the LLVM version pin, and determinism. The VIR input is specified in 006 — VIR; the optimizer that runs before this backend is specified in 007 — VIR Optimizer. The C backend is specified in 008 — C Backend.
3. Definitions
| Term | Meaning |
|---|---|
.ll |
Textual LLVM assembly. |
| Opaque pointer | The LLVM ptr type, which carries no pointee type. |
| Target triple | The architecture/vendor/OS string that selects codegen. |
| Datalayout | The LLVM string describing primitive sizes, alignments, and endianness. |
| Metadata | !name nodes attached to instructions or the module. |
!range |
Metadata constraining the possible values of a load or call result. |
4. Model
VIR is already in SSA form, so the mapping to LLVM is 1:1. There is no out-of-SSA
step here; that step exists only on the C path (see 008 — C Backend).
Each VIR block becomes one LLVM basic block, each VIR instruction becomes one LLVM
instruction, and each Phi becomes an LLVM phi.
The output is a self-contained module. It carries its own target triple and
datalayout, and it declares every external symbol it references before it defines
anything. A consumer can pass the module straight to llvm-as with no additional
environment.
5. Normative requirements
5.1 Purity and output
R1. The LLVM backend MUST perform pure translation. It MUST NOT run optimization, inference, or discovery passes, and it MUST NOT modify the VIR it reads.
R2. The backend MUST emit textual .ll only. It MUST NOT invoke llvm-as, opt,
llc, or any other LLVM tool, and it MUST NOT link against LLVM libraries.
R3. The backend MUST emit a self-contained module that includes a target triple and a datalayout derived from that triple.
R4. The backend MUST emit all declarations before any definition.
R5. For a fixed input and Config, the emitted module MUST be byte-identical across
runs and independent of thread count (see 000 — Overview).
5.2 SSA mapping
R6. The backend MUST map VIR constructs to LLVM constructs 1:1:
Table 1. VIR to LLVM structural mapping
| VIR | LLVM |
|---|---|
| Basic block | Basic block with a label |
| Instruction | Instruction, in the same order |
Phi(value, pred) |
phi with one incoming pair per predecessor |
| Conditional branch | br i1 <cond>, label <t>, label <f> |
| Switch | switch |
| Unconditional branch | br label <target> |
| Return | ret |
| Unreachable | unreachable |
alloca |
alloca |
load |
load |
store |
store |
R7. The backend MUST preserve block order given by the VIR module, and MUST preserve instruction order within a block, so that the 1:1 mapping is observable in the text.
R8. Every VIR value MUST map to exactly one LLVM SSA name. Names MUST be generated deterministically.
5.3 Types
R9. The backend MUST map VIR types to LLVM types as specified in Table 2.
Table 2. VIR type to LLVM type
| VIR type | LLVM type |
|---|---|
| Integer of N bits, signed or unsigned | iN |
f32 |
float |
f64 |
double |
Pointer T* |
ptr |
| Struct | %struct.Name = type { ... } |
| Union | %union.Name = type { ... } |
Array [N x T] |
[N x T] |
void |
void |
| Function type | <ret> (<params>) |
R10. The backend MUST pin LLVM 18 or later and MUST use opaque pointers. It MUST
NOT emit typed pointers such as i32*.
R11. Aggregate GEPs MUST use VIR field indices directly as LLVM struct indices. The backend MUST NOT translate them to byte offsets.
R12. The backend MUST emit no !llvm.dbg metadata in v1. Debug mapping exists only
on the C path (see 008 — C Backend).
5.4 Traits to attributes and metadata
R13. The backend MUST map traits to LLVM attributes and metadata as specified in Table 3. When a trait is absent, the matching attribute or metadata MUST NOT be emitted.
Table 3. Trait to LLVM attribute and metadata mapping
| Trait | LLVM attribute or metadata |
|---|---|
Restrict, NoAlias |
noalias parameter attribute plus !alias.scope and !noalias |
ReadOnly, NoWrite |
readonly |
WriteOnly, NoRead |
writeonly |
NoRead + NoWrite |
readnone (spelled memory(none) on LLVM 18) |
NonNull |
nonnull |
Align(n) |
align n |
Range(lo, hi) |
!range on loads and calls |
Assume(pred) |
llvm.assume |
NoInline, AlwaysInline |
noinline / alwaysinline |
TailCall, MustTail, NoTail |
tail / musttail / none |
NoReturn |
noreturn |
HotPath, ColdPath |
!prof |
Vectorize |
!llvm.loop.vectorize.enable |
Unroll(n) |
!llvm.loop.unroll.count |
Unreachable |
unreachable terminator |
NoUndef |
Reserved; see R14 |
The pairing mirrors the C mapping in 008 — C Backend: a no-write fact takes a
read-only annotation, a no-read fact takes a write-only annotation, and their combination takes
readnone. ReadOnly/WriteOnly are region-level facts and NoRead/NoWrite are access-edge
primitives (see 002 — Traits); the emitted attribute follows the entity the trait
attaches to, whether a function or a pointer parameter.
R14. NoUndef is reserved and is only meaningful on the LLVM path. The backend MUST
NOT interpret it on the C path. When present here, it MUST translate to the
corresponding noundef attribute.
R15. The backend MUST emit real llvm.assume calls and real !range metadata; it
MUST NOT emit placeholders that a consumer would have to fill in.
5.5 Intrinsics
R16. The backend MUST lower the VIR intrinsics of 006 — VIR to the LLVM intrinsics listed in Table 4.
Table 4. VIR intrinsic to LLVM intrinsic
| VIR intrinsic | LLVM intrinsic |
|---|---|
memcpy |
llvm.memcpy.p0.p0.i64 |
memset |
llvm.memset.p0.i64 |
memmove |
llvm.memmove.p0.p0.i64 |
sqrt(f32/f64) |
llvm.sqrt.f32 / llvm.sqrt.f64 |
fabs(f32/f64) |
llvm.fabs.f32 / llvm.fabs.f64 |
ctpop(iN) |
llvm.ctpop.iN |
fshl(iN) |
llvm.fshl.iN |
sadd_sat, uadd_sat, ssub_sat, usub_sat |
llvm.sadd.sat.iN, llvm.uadd.sat.iN, llvm.ssub.sat.iN, llvm.usub.sat.iN |
sadd_overflow, uadd_overflow, ssub_overflow, usub_overflow, smul_overflow, umul_overflow |
llvm.sadd.with.overflow.iN, llvm.uadd.with.overflow.iN, llvm.ssub.with.overflow.iN, llvm.usub.with.overflow.iN, llvm.smul.with.overflow.iN, llvm.umul.with.overflow.iN |
R17. The backend MUST emit the declaration of every intrinsic it references before the definition that calls it.
5.6 Module scaffolding
R18. The backend MUST emit a target triple line taken from the configured target
(see 010 — Driver & CLI).
R19. The backend MUST emit a target datalayout line derived from that triple, so
the module is self-contained.
R20. The backend MUST emit declarations for all external functions and globals before any definition.
R21. The backend SHOULD emit deterministic type and global names so that two runs over the same module produce identical text.
6. Invariants
- I1. The emitted module is parseable by
llvm-asfor LLVM 18 or later. - I2. There is a 1:1 correspondence between VIR blocks and instructions and the emitted LLVM blocks and instructions.
- I3. The module is self-contained: triple, datalayout, and all referenced declarations are present.
- I4. No typed pointers appear anywhere in the output.
- I5. Declarations precede definitions.
- I6. Output is deterministic for a fixed input and
Config, at any thread count. - I7. No
!llvm.dbgmetadata appears in v1 output.
7. Examples
Module scaffolding with a declaration before a definition, opaque pointers, and a phi:
; ModuleID = 'demo'
target triple = "x86_64-unknown-linux-gnu"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
%struct.Pair = type { i32, i64 }
declare i32 @extern_fn(ptr)
define i32 @add(i32 %a, i32 %b) {
entry:
%r = add i32 %a, %b
ret i32 %r
}
define i32 @loop_sum(ptr %p, i32 %n) {
entry:
br label %loop
loop:
%i = phi i32 [ 0, %entry ], [ %next, %body ]
%acc = phi i32 [ 0, %entry ], [ %acc.next, %body ]
%cond = icmp slt i32 %i, %n
br i1 %cond, label %body, label %exit
body:
%v = load i32, ptr %p, align 4, !range !0
%acc.next = add i32 %acc, %v
%next = add i32 %i, 1
br label %loop
exit:
ret i32 %acc
}
!0 = !{i32 0, i32 100}
A Restrict parameter and an Assume lower like this:
define void @copy(ptr noalias %dst, ptr noalias %src) {
entry:
%ok = icmp ne ptr %dst, null
call void @llvm.assume(i1 %ok)
ret void
}
8. Cross-references
- 000 — Overview for determinism and conformance.
- 001 — Architecture for module boundaries.
- 002 — Traits for the trait vocabulary consumed here.
- 006 — VIR for the input IR and its invariants.
- 007 — VIR Optimizer for the pass pipeline.
- 008 — C Backend for the default C path.
- 010 — Driver & CLI for the
-targetflag and mode selection. - 011 — Diagnostics for error reporting.