33 KiB
libVCT — Vox C Transpiler Library: Architecture Specification
- Status: Draft (pending final review)
- Date: 2026-09-13
- Scope: Whole-library architecture — component boundaries and interfaces. Subsystem internals are deferred to later specs.
- Normative language:
MUST,SHOULD,MAYper RFC 2119.
1. Overview
libVCT is an LLVM-inspired compiler library written in D. It consumes an IR, optimizes it, and transpiles it into C17, then drives a system C compiler to produce an object file. An alternate backend emits textual LLVM IR instead of C.
The library is organized around a two-stage IR:
- HIR — a high-level, tree-shaped AST that is the frontend's output and is annotated in place with traits. HIR does discovery and small, cheap folds.
- VIR — a low-level SSA/CFG instruction IR. VIR does the heavy lifting and performs no discovery; it is a strict consumer of HIR-provided traits.
The design thesis: because HIR hands VIR a fully annotated "dictionary" of facts, VIR can skip analysis and go straight to transformation. HIR's cheap, cascading folds are parallelizable; the resulting VIR is simpler and faster to optimize. The net goal is LLVM-class codegen quality with materially lower compile time.
1.1 Goals
- Codegen quality is the headline. Generated code quality takes priority over compile speed.
- Two-stage optimization. HIR performs small folds and trait derivation; VIR performs large transformations.
- Trait-driven VIR. VIR never re-analyzes; every fact it uses is an attribute, relationship, or request supplied by HIR.
- Parallel, cascading HIR. HIR passes run per-function in parallel and cascade to a fixpoint.
- Compiler-friendly C. The C backend always emits C written to be pattern-matched by GCC/Clang.
- Deterministic output. Same input + same config ⇒ byte-identical output, regardless of thread count.
1.2 Non-goals (v1)
- Linking. libVCT produces object files; linking is the caller's job. The sole exception is
-mangled(cosmopolitan) mode. - Windows / MSVC. Linux/POSIX-first; gcc and clang only.
- LLVM bitcode. The LLVM backend emits textual
.llonly; the caller runsllvm-as. - LLVM debug metadata. No
!llvm.dbgin v1; debug mapping exists on the C path via#line. - A user-facing VIR. VIR is internal and intentionally hostile to hand-authoring.
- Consuming LLVM IR or Vox's existing IR. libVCT owns its IR definition.
1.3 Glossary
| Term | Meaning |
|---|---|
| HIR | High-level IR: the frontend AST, annotated in place with traits. |
| VIR | Vox IR: a low-level SSA/CFG instruction IR. |
| Trait | The union of Attribute, Request, and Relationship attached to a node. |
| Attribute | A fact about a node (type, value, mutability, ...). |
| Request | An attribute-derived suggestion from HIR to VIR. |
| Relationship | A directed edge between nodes, traceable up/down. |
| Cascade | The chain of HIR rewrites triggered by one fold. |
| Lowering | The "middleman" that translates annotated HIR into VIR SSA. |
| Remark | An optimizer decision record emitted for diagnostics. |
2. Component Architecture
2.1 Modules
| Module | Responsibility | Allocation | Visibility |
|---|---|---|---|
vct.ir.hir |
HIR AST node types, Trait model, builder/emitter API | arena | public |
vct.ir.vir |
VIR SSA/CFG types, VIR type system, instruction set | arena | internal |
vct.traits |
Attribute / Request / Relationship definitions + queries | arena | public |
vct.comptime |
Comptime evaluator / constant-folding engine | arena | internal |
vct.hir.opt |
HIR pass framework + passes, parallel cascade | arena | internal |
vct.lower |
HIR → VIR lowering + SSA construction | arena | internal |
vct.vir.opt |
VIR pass framework + heavy passes | arena | internal |
vct.backend.c |
VIR → C17 emitter (out-of-SSA, compiler-friendly C) | arena + GC | public entry |
vct.backend.llvm |
VIR → textual LLVM IR emitter | arena + GC | public entry |
vct.driver |
cc invocation, flag assembly, .o emission, LTO, cosmopolitan |
GC | public |
vct.cli |
Thin command-line wrapper | GC | public |
vct.diag |
Diagnostics, source maps, error reporting | GC | public |
vct.context |
Context / arena ownership, Config |
GC + arena | public |
vct.test.hirbuild |
Fluent HIR test-builder harness | arena | public |
2.2 Data flow
frontend --build--> HIR
--hir.opt (parallel, cascading)--> annotated HIR
--lower--> VIR (SSA/CFG)
--vir.opt--> optimized VIR
--backend.c--> C17 source --driver--> .o (default path)
optimized VIR --backend.llvm--> textual LLVM IR (caller links LLVM)
2.3 Public interfaces
- HIR builder (frontend-facing). Construct nodes, set types/values, attach frontend-supplied traits (
static, hints), and finish a module. - Trait query (VIR-facing). Read attributes and walk relationships. This is the only channel through which VIR obtains facts.
- Backend (driver-facing). Consume optimized VIR and emit C (or textual LLVM IR).
2.4 Load-bearing boundaries
vct.traitsis the contract. Both HIR and VIR depend on it. VIR depends on nothing else from HIR except node identity.vct.loweris the SSA construction site. Phi-nodes, dominance, and the memory model all land here; it is the most algorithmically dense module.
3. The Trait Model
A Trait is the union of three sub-structures hung on HIR nodes, and the entire vocabulary VIR may reason with:
struct Trait {
Attribute attr;
Request[] reqs;
Relation[] rels;
}
Traits are exposed publicly. A frontend MAY suggest traits to HIR; HIR validates suggestions and fills in correct values. Derived traits are authoritative and VIR trusts them blindly.
3.1 Attributes
Attributes are facts about a node, with two origins in one namespace:
- Suggested — set by the frontend at build time. HIR validates; on mismatch it emits a warning (promoted to error under
-Werror) and overwrites with the correct value. - Derived — produced by the HIR optimizer during the cascade. Authoritative.
Every attribute carries an attr_source bit recording whether it was suggested or derived.
v1 attribute set:
| Attribute | Meaning |
|---|---|
ty |
The node's VIR type. |
const_value |
Present iff the value is known at compile time. |
is_static |
Frontend-declared compile-time value. |
is_comptime |
HIR-proven compile-time value. |
is_constant |
Value is constant (not necessarily comptime). |
is_used |
Node has at least one use. |
is_mutably_used |
Node is mutated through at least one use. |
is_addressed |
Address taken by pointer/reference. |
escapes |
Value escapes its defining scope. |
is_runtime_mutable |
May change at runtime. |
may_change_at_runtime |
Alias of intent: value is not frozen. |
complex |
Frontend-provided; gates the NoOptimize request. |
3.2 Requests
Requests are attribute-derived suggestions from HIR to VIR — never ad-hoc. Each has a strength:
Soft— a suggestion VIR MAY decline.Strong— violating it is likely a bug (e.g.,NoInline,MustTail,AlwaysInline,NoOptimize).
Every request produces a RequestResult:
struct RequestResult {
Request req;
bool accepted;
DenyReason reason;
string note; // one-line human-readable explanation
}
A denial MUST carry a DenyReason enum (for tooling) and a one-line note.
enum DenyReason {
CostModel, // profitable only under a different cost model
Illegality, // transform would be incorrect
AlreadyDone, // no-op: the goal already holds
Unsupported, // VIR does not implement it yet
ContradictsTrait, // conflicts with a stronger fact
TooLarge, // exceeds a size budget
NoTargetSupport, // backend/target cannot express it
}
3.2.1 Request catalog (v1)
Inlining / call edges: Inline; AlwaysInline (Strong); NoInline (Strong); TailCall; MustTail (Strong); NoTail (Strong); Devirtualize; ColdCall; LikelyCall; UnlikelyCall.
Loops: Vectorize; Unroll(factor); Interleave(count); Peel(count); Distribute; Fuse; Jam; Unswitch; Rotate; LICM (action); LoopInvariant (property); MustProgress.
Memory effects: NoAlias; Restrict; NonNull; Align(n); ReadOnly; WriteOnly; NoRead; NoWrite; NoCapture; Dereferenceable(n); Constant; NoUndef (reserved); Prefetch.
Control flow: HotPath; ColdPath; Unreachable; NoReturn.
Assumptions: Assume(pred); Range(lo, hi).
Optimization control: NoOptimize (Strong; requested only when the frontend sets complex on the node).
3.2.2 Request conflict rules
StrongbeatsSoft.- Two conflicting
Strongrequests (e.g.,InlinevsNoInline,TailCallvsNoTail) ⇒ HIR emits a diagnostic and rejects the frontend suggestion. LoopInvariant(a proven property VIR may exploit) andLICM(an explicit request to hoist) are distinct and both retained.NoRead/NoWriteare primitives on an access edge;ReadOnly/WriteOnlyare region-level facts derived from them. All four are retained, with the scope difference documented.
3.3 Relationships
Relationships are directed edges HIR leaves for the non-constant world. They are traceable up and down to a terminal node.
Flags:
| Flag | Meaning |
|---|---|
is_offspring |
Cannot trace up. |
is_ancestor |
Cannot trace down. |
is_common |
Branch point; VIR must choose a direction. |
has_siblings |
Connects down to 2+ nodes. |
is_apex |
Part of a cycle (a → b → c → d → a), usually from polymorphism. |
Relation attributes (small set): was_changed; is_mutable; is_pointer.
An apex SHOULD normally be ironed out by HIR; residual apexes typically arise from polymorphism.
3.4 Representation & staleness
- Representation (resolved): an embedded dense
Traitstruct on every node is primary, plus an optional sparse side-channel for rare / frontend-extensible attributes. - Staleness (resolved): epoch/generation counters with dirty propagation along relationships. A trait is valid only for the node's current epoch. This survives parallel HIR passes.
4. HIR Optimizer
Role: discovery + small folds + trait derivation. HIR hands VIR a "dictionary".
4.1 Pass framework
interface HirPass {
string name();
void run(HirFunction fn, HirContext ctx);
}
Passes mutate the annotated AST in place. Successful rewrites emit triggers:
enum Trigger {
ConstantUnfolded,
UsesReplaced,
NodeDeleted,
TraitChanged,
StaticDiscovered,
}
A worklist scheduler collects (pass, node) pairs, dedupes, and runs to a fixpoint. A per-function step budget guarantees termination.
4.2 The cascade
Canonical chain:
4 * 16
-> const 64
-> mark is_comptime / is_constant / const_value = 64
-> prove not addressed / not runtime-mutable
-> replace all uses with literal 64
-> node dead -> delete
-> downstream nodes now constant -> enqueue
Each hop emits a trigger; the worklist drains until nothing new is provable. HIR never inlines — it only proves smallness and issues an Inline request to VIR.
4.3 Parallelism & determinism
- Parallelize per function (independent HIR trees).
- Within a function, passes run sequentially so cascade order is deterministic.
- Cross-function effects (inlining, global constant propagation) use a module-level fixpoint: parallel per-function passes → deterministic module phase → re-enqueue only affected functions.
- Epoch counters prevent stale trait reads in parallel workers.
4.4 Comptime evaluator
An interpreter over HIR subgraphs. Eligible only if: no side effects, primitives only, no global mutation, no I/O, and within loop/recursion budgets. Otherwise it bails and defers to VIR. Outputs const_value, then is_comptime + is_constant.
The frontend static attribute seeds the evaluator. HIR trusts static as "this is compile-time" but still validates it.
Budgets (configurable; defaults):
| Budget | Default |
|---|---|
comptime.max_iterations |
4096 |
comptime.max_recursion_depth |
256 |
comptime.max_steps |
1,000,000 |
comptime.max_aggregate_elements |
65,536 |
hir.max_rewrites_per_function |
100,000 |
hir.max_rewrites_per_module |
1,000,000 |
All are overridable via the API and CLI.
4.5 Defer
HIR carries a real high-level defer statement. The optimizer inlines the deferred call at every scope-exit path and then erases the defer marker — zero runtime tax.
4.6 Optimization-level behavior
| Level | HIR | VIR |
|---|---|---|
-O0 |
traits validated only | off |
-O1 |
builtin folds + discovery | off |
-O2 |
full cascade + comptime | on |
-O3 |
+ special AST transforms (loop-unfold/vectorize → constant-unfold → comptime) | on |
-Ofast |
= -O3 + -march=native + fast-math |
on |
-Oz |
size-tuned | on |
Compiler-friendly C is emitted at ALL optimization levels. -O3 merely adds more aggressive AST transforms.
5. Lowering (HIR → VIR)
Role: the "middleman". Pure translation + SSA construction; no discovery.
5.1 Contract
- Input: annotated HIR with complete, epoch-valid traits.
- Output: VIR — basic blocks, phi-nodes, def-use, SSA.
- Lowering validates trait completeness. A missing or contradictory trait raises an internal-compiler-error diagnostic (see §11).
- Deterministic; parallelizable per function.
5.2 Form
Lowering is a streaming recursive interpreter over the HIR tree ("tcc-for-VIR"):
- Structured HIR (
if/while/for/switch/block) → blocks + terminators (br/condbr/switch/ret/unreachable). - Expressions → temporaries + instructions.
5.3 SSA construction
SSA is built with the Braun et al. sealed-block algorithm: one pass, on-the-fly phi insertion, no separate dominance-frontier pass. A loop header is sealed once its back-edge is emitted.
5.4 Memory model (trait-driven hybrid)
| Condition | Representation |
|---|---|
scalar ∧ ¬is_addressed ∧ ¬escapes ∧ ¬is_runtime_mutable |
pure SSA value |
| addressed / escaping / runtime-mutable / aggregate | alloca + explicit load/store |
| volatile / atomic | forced memory, never promoted |
VIR's mem2reg/SROA MAY promote memory back to SSA when traits confirm safety.
5.5 Phi
VIR has an explicit LLVM-style Phi instruction whose operands are (value, predecessor-block) pairs.
5.6 Identity & traits
A LoweringMap (HIR node → VIR entity(ies)) is built during lowering. Each VIR entity carries its own Trait, populated from the source HIR node. HIR Relationship endpoints are rewritten to VIR entities via the map. VIR depends only on vct.traits + node identity.
5.7 Optimization barriers
complex/NoOptimize nodes are lowered inside a region-level optimization barrier. VIR passes MUST NOT rewrite across it.
5.8 Defer fallback
If HIR optimization is off (-O0/-O1), lowering itself expands defer at every scope exit.
5.9 VIR type system
More minimal than HIR but still fleshed out: integers with width, floats, pointers, aggregates, function types, void.
5.10 Output invariants
Well-formed SSA:
- every use is dominated by its def,
- every block is terminated,
- phi arity equals predecessor count,
- a single def per value.
Verified in debug builds (§12).
6. VIR Optimizer
Role: the heavy-lifting half. Reads traits only, transforms SSA, performs no discovery. Runs at -O2/-O3/-Ofast/-Oz.
6.1 Contract
- Input: lowered VIR (well-formed SSA/CFG) + complete traits.
- Output: optimized VIR, still in SSA.
- Strict trait consumer: a transform is enabled by an attribute/relationship or requested by a Request. VIR MUST NOT assume a fact absent from traits.
6.2 Framework
interface VirPass {
string name();
void run(VirModule m, PassContext ctx);
}
Analyses: DominatorTree, PostDominatorTree, LoopInfo, AliasInfo, DefUse, CallGraph, RangeInfo. Passes declare which analyses they preserve; the manager invalidates the rest (fine-grained, not invalidate-all). Epoch counters catch stale trait reads.
6.3 Default pipeline (-O2)
A module pass manager interleaving function passes and IPA:
- Canonicalize:
mem2reg,SROA,instcombine,simplifyCFG,early-CSE,DCE. - Scalar:
GVN,SCCP,LICM,indvars,reassociation. - IPA: inliner (driven by
Inline/AlwaysInline/NoInline/ColdCall/LikelyCall), globalDCE,IPSCCP, function-attribute propagation. - Loops:
unroll/interleave/peel/rotate/unswitch/distribute/fuse/jam, each gated by its Request. - Vectorize: loop + SLP, gated by
Vectorize/LoopInvariant/Range/NoAlias/Restrict. - Memory: alias-driven
DSE, GEP simplification, load widening. - Control flow: block layout (
HotPath/ColdPath), tail-call formation (TailCall/MustTail/NoTail), unreachable pruning, jump threading. - Codegen prep: canonicalize for the backend; remains SSA.
-O3/-Ofast raise aggression. -Oz is size-first (most unrolling/vectorization off).
6.4 Requests
Each Request yields a RequestResult (§3.2). Denials carry a DenyReason + one-line note.
6.5 Alias analysis
Synthesized from NoAlias/Restrict/NoCapture/Dereferenceable/ReadOnly/WriteOnly + provenance — no guessing.
6.6 Barriers
Region optimization barriers (complex/NoOptimize, §5.7) are opaque; passes skip across them.
6.7 Budgets
vir.max_iterations, vir.max_pipeline_rounds (configurable).
6.8 Resolutions
- A Strong-request denial is a hard error when the backend is capable and the reason is
Illegality/ContradictsTrait; it is a warning forUnsupported/NoTargetSupport. - The pipeline is a fixed canonical pipeline (LLVM-style), not adaptive request-driven ordering.
- Inlining is a VIR/IPA transform (HIR only requests it).
RangeInfois lightweight, seeded byRange/Assumetraits — no full ScalarEvolution.- Analysis invalidation is fine-grained (preserved-analyses sets).
7. C Backend (vct.backend.c)
Role: VIR → C17.
7.1 Contract
- Input: optimized VIR (still SSA, traits attached).
- Output: one C17 translation unit per function.
- Emission is per-function independent → parallel; the driver concatenates deterministically.
- Two-pass emission: forward declarations, then definitions.
7.2 Out-of-SSA
Boissinot et al., "Revisiting Out-of-SSA Translation" — handles the lost-copy and swap problems. Critical edges are split before out-of-SSA.
7.3 Compiler-friendly C
- Structured control-flow reconstruction (
if/else/while/for/do/switch) for reducible CFGs;gotofallback for irreducible. - Qualifiers from traits:
restrict(Restrict/NoAlias),const(ReadOnly/NoWrite),_Noreturn(NoReturn),cold/hot(ColdPath/HotPath),static inline/noinline. - Hints:
__builtin_expect;__builtin_assume_aligned/_Alignas(Align);__builtin_unreachable(Unreachable);__builtin_assume(clang,Assume);#pragma GCC ivdep/#pragma clang loop vectorize(enable)(Vectorize);pure/const(NoRead/NoWrite). NoOptimize/complexbarrier: a compiler fence —__asm__ __volatile__("" ::: "memory")plus an opaquenoinlinecall.
7.4 Types & layout
intN_t/uintN_t, float/double, T*, struct/union/arrays, function pointers, void. VIR aggregate GEPs use field indices, not byte offsets; the C compiler picks layout. An explicit layout trait pins the ABI (emits padding + _Static_assert).
7.5 Naming
Deterministic mangling. Exported names preserved; internal names sanitized and uniquified. A reserved-word/collision prefix table.
7.6 Intrinsics
Mostly __builtin_* (GCC/Clang), plus a small set of portable fallback helpers for what builtins do not cover.
7.7 Debug mapping
#line directives back to frontend source, flag-gated (on for debug builds, off for release). The frontend registers its source map with vct.diag.
7.8 ABI
Exported functions match the C ABI; internal functions are static; struct-by-value follows the C compiler's ABI. MustTail requires [[clang::musttail]] on Clang; on GCC a MustTail request is a hard error.
8. LLVM Backend (vct.backend.llvm)
Role: VIR → textual LLVM IR, post VIR optimization. Pure translation; no optimization or discovery. Output is a self-contained .ll module. libVCT never invokes llvm-as/opt/llc or links LLVM — that is the frontend's job. Deterministic per-function emission with stable concatenation.
8.1 SSA mapping
VIR is already SSA → 1:1 mapping: instruction → instruction, block → block, Phi → phi, terminators, alloca/load/store. No out-of-SSA (the opposite of the C backend).
8.2 Types
iN integers, float/double, opaque ptr (not typed), struct/array (VIR field indices → LLVM struct indices), void, function types.
8.3 Traits → LLVM attributes/metadata
| Trait | LLVM |
|---|---|
Restrict/NoAlias |
noalias + !alias.scope/!noalias |
ReadOnly/WriteOnly/NoRead/NoWrite |
readonly/writeonly |
NonNull |
nonnull |
Align(n) |
align n |
Range |
!range on loads |
Assume |
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 |
NoUndef |
reserved; only meaningful here |
8.4 Intrinsics
memcpy/memset/memmove, llvm.sqrt.*, llvm.fabs.*, llvm.ctpop.*, llvm.fshl.*, saturating/overflow ops.
8.5 Module scaffolding
Target triple (from -target) + derived target datalayout, so the module is self-contained. Declarations precede definitions.
8.6 Resolutions
- Pin LLVM 18+ only (opaque pointers, current metadata).
- No
!llvm.dbgin v1; debug only on the C path via#line. - Emit text
.llonly; the caller runsllvm-as. - Emit real
llvm.assume/!range.
9. Driver & CLI
9.1 Role split
vct.driver— library-level orchestrator: takes emitted C TUs, invokes the C compiler, collects.o. Owns the-llvmpath (returns LLVM IR, never touchescc) and the-mangledpath.vct.cli— thin wrapper: parses flags intoConfig, drives the whole pipeline (read IR → HIR opt → lower → VIR opt →backend.c→ driver), formats diagnostics, sets exit codes. GC-allocated; holds no optimizer state.
9.2 Pipeline
read input
-> (HIR opt if enabled)
-> lower
-> (VIR opt if enabled)
-> backend.c -> driver -> .o
-S stops after C; -emit-llvm/-llvm stop after LLVM IR.
9.3 Flag surface (v1)
| Group | Flags |
|---|---|
| Optimize | -O0 -O1 -O2 -O3 -Ofast -Oz |
| Target | -march=<arch> (→ cflags), -mcpu=<cpu>, -target <triple> |
| Modes | -S (C only), -emit-llvm/-llvm, -mangled (cosmopolitan) |
| Toolchain | -cc=<gcc|clang> (default auto), -cflags="...", -j<N>, -save-temps |
| LTO | -flto[=full|thin], -ffat-lto-objects |
| Debug | -g, --dump-hir, --dump-vir, --verify, --time-passes, --stats |
| Output | -o <path> |
| Warnings | -Wall -Werror -Wextra always on; -Wno-error escape hatch; -w to silence |
| Diagnostics | --remarks, --diagnostics=json |
| Pass toggles | every pass exposed as -f<pass> / -fno-<pass> |
9.4 cc invocation
cc -std=c17 -c <tu.c> -o <tu.o> <cflags> <march> -Wall -Werror -Wextra <g> <flto>
One process per TU, bounded by -j. The compiler is discovered via $CC then PATH; its version/dialect is probed once and drives the backend's pragma/builtin choices (§7.3, §7.6).
9.5 Error handling
cc stderr is captured. #line directives (§7.7) map C errors back to VIR/source; they are re-emitted via vct.diag. A cc failure produces a diagnostic and a nonzero exit.
9.6 Determinism
Stable TU ordering and flag ordering. Temp files live under a GC-managed temp dir (cleaned unless -save-temps).
9.7 -mangled
Switches the toolchain to cosmocc/cosmopolitan libc and produces an Actually Portable Executable. This mode does link — the opt-in exception to "caller links".
9.8 Resolutions
- Driver compiles to
.oonly; a single-omerges per-function objects with anld -rrelocatable link; a recommended link line / response file is emitted for the caller.-mangledis the sole mode producing a final executable. - The CLI also reads a textual HIR/VIR format for testing/tools/round-trips; frontends still embed the library.
- Every pass is exposed as a
-f/-fnotoggle. -Wall -Werror -Wextradefault;-Wno-errorescape hatch;-Werrorapplies to libVCT-generated C.
10. Diagnostics & Source Maps (vct.diag)
10.1 Central service
One central diagnostics service; every module emits through it, nothing prints directly. GC-allocated.
struct Diagnostic {
Severity severity; // Error | Warning | Note | Remark | Ice
DiagCode code; // stable namespaced, e.g. VCT1002
string message;
Span primary;
Span[] notes;
Suggestion[] suggestions;
}
10.2 Source maps
The frontend registers SourceLocation{file,line,col,len} against HIR nodes; VIR resolves via the LoweringMap (§5.6). Degradation to no-location is graceful. The C backend emits #line (§7.7); cc errors are parsed and re-mapped (§9.5).
10.3 Channels
DiagnosticConsumercallback (canonical for embedded use).- Human-readable CLI (color when tty).
- JSON (
--diagnostics=json, versioned schema, shipped in v1).
10.4 Optimizer remarks
--remarks (off by default; automatically enabled at -O3) emits per-decision remarks tied to the trait pipeline, e.g.:
HIR: replaced 3 uses of node#412 (comptime 64)VIR: denied Inline on @foo — CostModel (callee 2.4x size budget)
10.5 ICE policy
Internal compiler errors include: missing/contradictory trait at lowering (§5.1), VIR invariant violated in debug (§5.10), and a Strong request denied for Illegality/ContradictsTrait (§6.8).
- Release: return a failure result + ICE diagnostic with a module-dump request.
- Debug: assert.
Never silently continue.
10.6 Exit codes
| Code | Meaning |
|---|---|
| 0 | success |
| 1 | diagnostics present |
| 2 | usage/config error |
| 3 | ICE |
10.7 Resolutions
- The trait-mismatch warning is promoted to error under
-Werror(frontends can opt out with-Wno-error=). - ICE in release = diagnostic + error result.
- Remarks: both opt-in
--remarksand automatically enabled at-O3. - Ship a versioned JSON diagnostics format in v1.
11. Testing & Verification
11.1 Core constraint
VIR is a strict trait consumer and cannot be tested without a valid HIR fixture. The test strategy is anchored on a fixture layer.
11.2 Layers
- Unit tests — D
unittestper module. - HIR test-builder harness (
vct.test.hirbuild) — a fluent builder constructing HIR modules + traits without a frontend. This is the fixture layer for HIR-opt, lowering, VIR-opt, and both backends. - Textual round-trip — property:
print(parse(print(m))) == print(m). - FileCheck-style tests —
CHECK/CHECK-NOT/CHECK-NEXTdirectives. - Verifier — structural SSA (§5.10) + trait consistency, via
--verify; asserted after every pass in debug. - Differential — the VIR reference interpreter vs the compiled
.o; the C path vs the LLVM path. - Fuzz — random HIR/trait combinations; property: terminates, verifier clean, output compiles.
- Matrix — gcc + clang ×
-O0..-Ofast/-Oz× C/LLVM. - Perf benchmarks — codegen quality is the headline; compile time tracked via
--time-passes/--stats.
11.3 VIR reference interpreter
Approved as v1 scope. It is the differential oracle: the same VIR is run through the interpreter and through the compiled .o, and results are compared.
11.4 Determinism
Every pipeline run is executed twice in CI; output must be byte-identical.
11.5 Regression corpus
A corpus of input IR + expected FileCheck patterns; CI runs the full matrix.
11.6 Resolutions
- FileCheck-style matcher (not exact golden files).
- Build the VIR reference interpreter in v1 as a differential oracle.
- Verifier always-on in debug builds;
--verifyopt-in in release. - Random-HIR + random-trait fuzzers in v1.
12. Build, Packaging & Public API Lifecycle
12.1 Language & toolchain
Written in D. Primary compiler: LDC2; DMD supported as a secondary build target. Zero runtime dependencies — only a D compiler to build, and a system C compiler discovered at runtime for the C path.
12.2 Build system
dub for normal development, plus a plain build.d/Makefile path for no-dub builds. Both MUST produce identical artifacts.
12.3 Artifacts
libvct.a(static) andlibvct.so(shared).vctCLI binary.vctc.h— C API header.vct.test.hirbuild— shipped publicly.
12.4 API surfaces
- Native D API (primary, full feature set).
- Thin
extern(C)C API (v1): opaque handles forContext/Module/Builder/Config, functions to build IR, run the pipeline, and query diagnostics. No logic lives in the shim.
12.5 Module visibility
- Public:
vct.ir.hir,vct.traits,vct.context,vct.backend.centry,vct.backend.llvmentry,vct.driver,vct.diag,vct.test.hirbuild. - Internal:
vct.ir.vir,vct.lower,vct.hir.opt,vct.vir.opt,vct.comptimeinternals.
12.6 Memory ownership & lifecycle
- A
Contextowns one or more arenas; an arena is the unit of reclamation. Default: one arena perModule, freed wholesale. - All IR and optimizer objects are arena-owned and non-GC; passes mutate in place under epoch guards.
- GC is permitted only on cold paths (driver, diagnostics, CLI).
Contextaccepts anArenaAllocatorinterface so embedders can supply backing memory.
12.7 Config
One struct holds opt level, target (-march/-mcpu/-target), all budgets, pass toggles, warning policy, cc selection, and mode (-S/-llvm/-mangled). The CLI is a pure function from argv to Config; the library takes a Config. Programmatic callers bypass the CLI.
12.8 Threading contract
Thread-safe when each compilation unit has its own Context/arenas; no shared mutable global state. Intra-module parallelism is internal and bounded by -j.
12.9 Versioning
- Library semver.
- Textual IR format version — separate and independently versioned.
- Trait vocabulary version — adding attributes/requests is backward-compatible; changing semantics is a major bump. Experimental passes are gated behind feature flags.
12.10 Determinism guarantee
Same Config + same input ⇒ byte-identical output, independent of thread count.
12.11 Resolutions
- C API in v1 (thin shim).
- Custom
ArenaAllocatorhooks exposed. - LDC2 primary + DMD secondary.
dub+ no-dub build path.
13. Worked End-to-End Example
This example is normative for trait/request semantics.
Input program:
fn foo(x: int, y: int) -> int {
return x + y;
}
fn main() -> int {
x = 4;
y = x + 4;
z = foo(x, y);
a = sqrt(z);
println(a);
}
The HIR cascade proceeds as follows:
- HIR comptime-evaluates
x = 4→is_comptime,is_constant,const_value = 4. y = x + 4unfolds toy = 8→is_comptime.foo(x, y)is proven small → HIR issues anInlinerequest;zbecomes8 + ...constant → comptime.a = sqrt(z)→a = sqrt(8)comptime →a = 3.4641016151377544.- The whole program collapses to
println(3.4641016151377544).
VIR then optimizes the println call. The emitted C is effectively a single call with the folded constant. This demonstrates trait propagation HIR → VIR and the request lifecycle (Inline accepted).
14. Open Questions & Future Work
- Debug info on the LLVM path (
!llvm.dbg) — deferred past v1. - Windows / MSVC support — out of scope for v1.
- LLVM bitcode emission — deferred; caller runs
llvm-as. - Incremental / cached compilation — content-hash
.ocaching is out of scope for v1. - Cross-compilation targets beyond
-targetpassthrough and cosmopolitan — future work.