Files
libvct-spec/spec/009-llvm-backend.md

10 KiB

009 — LLVM Backend

  • Status: Draft
  • Normative language: MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are 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-as for 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.dbg metadata 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