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
+117
View File
@@ -0,0 +1,117 @@
# 000 — Overview
- **Status:** Draft
- **Normative language:** `MUST`, `MUST NOT`, `SHOULD`, `SHOULD NOT`, and `MAY` are to be
interpreted as described in RFC 2119.
## 1. Purpose
libVCT (the **Vox C Transpiler Library**) is a compiler library written in D. Given a program's
IR, libVCT optimizes that IR, transpiles it to C17, and drives a system C compiler to produce an
object file. An alternate backend emits textual LLVM IR instead of C.
libVCT is a **library**, not a compiler. A frontend embeds it, builds IR through its builder API,
and receives either C source or LLVM IR back. libVCT never links object files into an executable;
that is the caller's job, with the single opt-in exception of `-mangled` mode
(see [010 — Driver & CLI](010-driver-cli.md)).
## 2. The two-stage IR thesis
libVCT is organized around **two IRs**:
- **HIR**: a high-level, tree-shaped AST. The frontend constructs it; libVCT annotates it *in
place* with **traits**. HIR performs discovery and small, cheap folds.
- **VIR**: a low-level SSA/CFG instruction IR. VIR performs the heavy transformations and
performs **no discovery**; it is a strict consumer of the traits HIR produced.
The design thesis: because HIR hands VIR a complete, annotated "dictionary" of facts, VIR can skip
analysis and go straight to transformation. HIR's cheap folds cascade and parallelize per function,
and the VIR that results is smaller and faster to optimize. The intended result is LLVM-class
codegen quality at materially lower compile time.
## 3. Goals
1. **Codegen quality is the headline.** Generated code quality takes priority over compile speed.
2. **Two-stage optimization.** HIR performs small folds and trait derivation; VIR performs large
transformations.
3. **Trait-driven VIR.** VIR never re-analyzes. Every fact it uses is an attribute, relationship, or
request supplied by HIR.
4. **Parallel, cascading HIR.** HIR passes run per function in parallel and cascade to a fixpoint.
5. **Compiler-friendly C.** The C backend always emits C written to be pattern-matched by GCC and
Clang.
6. **Deterministic output.** The same input and configuration produce byte-identical output,
regardless of thread count.
## 4. Non-goals (v1)
- **Linking.** libVCT produces object files; linking is the caller's job. The sole exception is
`-mangled` mode.
- **Windows / MSVC.** v1 is Linux/POSIX-first and supports GCC and Clang only.
- **LLVM bitcode.** The LLVM backend emits textual `.ll` only; the caller runs `llvm-as`.
- **LLVM debug metadata.** There is no `!llvm.dbg` in v1. Debug mapping exists on the C path via
`#line` directives.
- **A hand-authored VIR.** VIR is internal and intentionally hostile to hand-authoring. A textual
form exists solely for tools and round-trip tests; it is not a supported authoring surface.
- **Consuming LLVM IR or Vox's existing IR.** libVCT owns its IR definition.
## 5. 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 an Attribute, Requests, and Relationships 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 and down. |
| **Cascade** | The chain of HIR rewrites triggered by a single fold. |
| **Lowering** | The "middleman" that translates annotated HIR into VIR SSA. |
| **Remark** | An optimizer decision record emitted for diagnostics. |
## 6. Conformance
An implementation conforms to this specification if it satisfies every `MUST` and `MUST NOT`
requirement in every specification file listed in §8. `SHOULD` requirements may be violated only
with a documented, deliberate reason. `MAY` requirements are optional.
Where this specification and an implementation disagree, this specification is authoritative. Where
a later specification file refines an earlier one, the later file is authoritative for its subject.
## 7. Determinism guarantee
For a fixed `Config` and a fixed input, libVCT **MUST** produce byte-identical output on every run,
independent of thread count. This guarantee binds every stage: HIR optimization, lowering, VIR
optimization, and both backends. Determinism is a correctness property, not an optimization.
## 8. Specification index
| File | Subject |
|---|---|
| [000 — Overview](000-overview.md) | Purpose, thesis, goals, glossary, conformance, determinism. |
| [001 — Architecture](001-architecture.md) | Modules, data flow, public interfaces, boundaries. |
| [002 — Traits](002-traits.md) | Attributes, requests, relationships, representation, staleness. |
| [003 — HIR](003-hir.md) | HIR node model, builder contract, textual format, `defer`. |
| [004 — HIR Optimizer](004-hir-optimizer.md) | Pass framework, cascade, parallelism, comptime, budgets. |
| [005 — Lowering](005-lowering.md) | HIR → VIR translation, SSA construction, memory model. |
| [006 — VIR](006-vir.md) | VIR SSA/CFG model, type system, instructions, invariants. |
| [007 — VIR Optimizer](007-vir-optimizer.md) | Pass framework, analyses, pipeline, request handling. |
| [008 — C Backend](008-c-backend.md) | VIR → C17: out-of-SSA, compiler-friendly C, layout, ABI. |
| [009 — LLVM Backend](009-llvm-backend.md) | VIR → textual LLVM IR. |
| [010 — Driver & CLI](010-driver-cli.md) | Pipeline orchestration, flags, C compiler invocation. |
| [011 — Diagnostics](011-diagnostics.md) | Diagnostics service, source maps, channels, ICE policy. |
| [012 — Testing](012-testing.md) | Test layers, fixture harness, oracle, fuzzing, matrix. |
| [013 — Build & Packaging](013-build-packaging.md) | Toolchain, artifacts, API surfaces, memory ownership. |
| [014 — Worked Example](014-worked-example.md) | End-to-end trait/request walkthrough (normative). |
| [015 — Open Questions](015-open-questions.md) | Deferred items and future work. |
## 9. How to read this specification
Each file is a self-contained specification of one subject. Cross-references name the target file
directly (for example, "see [002 — Traits](002-traits.md)") so that the set can be read out of
order.
Specifications state **observable and required behavior** (what a component must do and what
guarantees it must uphold) rather than prescribing internal implementation. Where a component's
internal design is load-bearing for correctness (for example, the SSA construction algorithm in
[005 — Lowering](005-lowering.md)), the required properties are stated normatively and the named
algorithm is given as the reference method.
+94
View File
@@ -0,0 +1,94 @@
# 001 — Architecture
- **Status:** Draft
- **Normative language:** `MUST`, `MUST NOT`, `SHOULD`, `SHOULD NOT`, and `MAY` are to be
interpreted as described in RFC 2119.
## 1. Overview
libVCT is decomposed into modules with explicit responsibilities and boundaries. This file defines
the module map, the data flow between stages, the three public interfaces, and the boundaries that
carry the design's weight.
## 2. Modules
| Module | Responsibility | Allocation | Visibility |
|---|---|---|---|
| `vct.ir.hir` | HIR AST node types, the trait attachment point, 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 and queries | arena | **public** |
| `vct.comptime` | Comptime evaluator / constant-folding engine | arena | internal |
| `vct.hir.opt` | HIR pass framework and passes, parallel cascade | arena | internal |
| `vct.lower` | HIR → VIR lowering and SSA construction | arena | internal |
| `vct.vir.opt` | VIR pass framework and 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` | C compiler 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 and arena ownership, `Config` | GC + arena | **public** |
| `vct.test.hirbuild` | Fluent HIR test-builder harness | arena | **public** |
## 3. Data flow
The default pipeline is:
```
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)
```
Each stage consumes the previous stage's output and is specified independently:
[003 — HIR](003-hir.md), [004 — HIR Optimizer](004-hir-optimizer.md),
[005 — Lowering](005-lowering.md), [006 — VIR](006-vir.md),
[007 — VIR Optimizer](007-vir-optimizer.md), [008 — C Backend](008-c-backend.md), and
[009 — LLVM Backend](009-llvm-backend.md).
## 4. Public interfaces
libVCT exposes exactly three interfaces to embedders:
1. **HIR builder (frontend-facing).** Construct nodes, set types and values, attach
frontend-supplied traits (such as `is_static` and optimization hints), and finish a module. See
[003 — HIR](003-hir.md).
2. **Trait query (VIR-facing).** Read attributes and walk relationships. This is the **only**
channel through which VIR obtains facts. See [002 — Traits](002-traits.md).
3. **Backend (driver-facing).** Consume optimized VIR and emit C or textual LLVM IR. See
[008 — C Backend](008-c-backend.md) and [009 — LLVM Backend](009-llvm-backend.md).
The native D API is the full-feature surface. A thin `extern(C)` C API (opaque handles for
`Context`/`Module`/`Builder`/`Config`) is the primary v1 public surface and is a shim over the D
API; no logic lives in the shim. See [013 — Build & Packaging](013-build-packaging.md).
## 5. Load-bearing boundaries
- **`vct.traits` is the contract.** Both HIR and VIR depend on it. VIR depends on nothing else from
HIR except **node identity**. A change to the trait vocabulary is a cross-cutting change and is
versioned accordingly (see [013 — Build & Packaging](013-build-packaging.md)).
- **`vct.lower` is the SSA construction site.** Phi-nodes, dominance, and the memory model all land
there; it is the most algorithmically dense module. Its correctness invariants are specified in
[005 — Lowering](005-lowering.md) and [006 — VIR](006-vir.md).
## 6. Memory and ownership model
- A `Context` owns one or more **arenas**. An arena is the unit of reclamation; the default is one
arena per `Module`, freed wholesale.
- All IR and optimizer objects are **arena-owned and non-GC**. Passes mutate in place under epoch
guards (see [002 — Traits](002-traits.md)).
- **GC is permitted only on cold paths**: the driver, diagnostics, and CLI.
- The arena interface is exposed so embedders can supply their own backing memory; a default bump
allocator ships in-tree.
Full lifecycle, ownership, and threading rules are specified in
[013 — Build & Packaging](013-build-packaging.md).
## 7. Threading
libVCT is thread-safe when each compilation unit has its own `Context` and arenas. There is no
shared mutable global state. Intra-module parallelism is internal and bounded by the `-j` setting.
The determinism guarantee in [000 — Overview](000-overview.md) applies regardless of thread count.
+344
View File
@@ -0,0 +1,344 @@
# 002 — Traits
- **Status:** Draft
- **Normative language:** `MUST`, `MUST NOT`, `SHOULD`, `SHOULD NOT`, and `MAY` are to be
interpreted as described in RFC 2119.
## 1. Purpose
The **trait model** is the vocabulary libVCT uses to describe facts about IR nodes. It is the
contract between the two optimization halves of the library: HIR discovers and derives traits, and
VIR reads them and transforms accordingly. VIR performs no discovery of its own. Everything VIR
may rely on is an attribute, a request, or a relationship defined here.
## 2. Scope
This file specifies:
- the `Trait` union of an **Attribute**, a set of **Requests**, and a set of **Relationships**;
- the complete v1 attribute set and request catalog;
- relationship flags and relation attributes;
- the query API semantics a trait consumer may assume;
- the trait representation and staleness model;
- request conflict resolution.
Out of scope: how HIR derives attributes (see [004 — HIR Optimizer](004-hir-optimizer.md)), how
lowering copies traits onto VIR entities (see [005 — Lowering](005-lowering.md)), and how the VIR
optimizer acts on requests (see [007 — VIR Optimizer](007-vir-optimizer.md)).
Traits are **public**. Frontends embed trait vocabulary; frontends are not required to derive
traits themselves.
## 3. Definitions
| Term | Definition |
|---|---|
| **Node** | An arena-owned HIR entity carrying one `Trait`. |
| **Attribute** | A fact about a node (its type, value, mutability, and so on). |
| **Request** | An attribute-derived suggestion from HIR to VIR. Never ad-hoc. |
| **Relationship** | A directed, traceable edge from a node to another node. |
| **Suggested** | An attribute or request set by the frontend at build time. |
| **Derived** | A trait produced by the HIR optimizer. Authoritative. |
| **Epoch** | A monotonically increasing generation counter used to invalidate stale traits. |
| **Consumer** | Any component that reads traits. VIR is the primary consumer. |
The `Trait` is the union of the three sub-structures:
```d
struct Trait {
Attribute attr;
Request[] reqs;
Relation[] rels;
}
```
A trait attaches to exactly one node. Trait values are per-node and, after HIR optimization, are
complete for every node that lowering consumes.
## 4. Attribute model
Attributes are facts about a node. They share **one namespace with two origins**:
- **Suggested**: set by the frontend at build time through the builder API.
- **Derived**: produced by the HIR optimizer during the cascade. Derived attributes are
authoritative.
Every attribute carries an `attr_source` marker recording whether its current value is `suggested`
or `derived`.
### 4.1 Attribute validation rule
This rule is normative and applies to every suggested attribute:
1. When the frontend suggests an attribute, HIR **MUST** validate the suggestion against what it
can prove.
2. If the suggested value is wrong or unprovable-in-the-suggested-direction, HIR **MUST** emit a
warning, **MUST** overwrite the attribute with the correct value, and **MUST** mark
`attr_source = derived`.
3. The warning **MUST** be promoted to an error under `-Werror`. A frontend **MAY** opt out for a
specific category with `-Wno-error=<category>`, or for every category with a bare
`-Wno-error`.
4. A correct suggestion **MUST** be preserved and continue to report `attr_source = suggested`
until HIR derives a replacement value.
HIR **MUST NOT** silently accept a wrong suggestion. A consumer **MUST** treat derived attributes
as authoritative and **MUST NOT** re-derive them.
### 4.2 v1 attribute set
| Attribute | Meaning | Typical source |
|---|---|---|
| `ty` | The node's VIR type. | Both |
| `const_value` | Present if and only if the value is known at compile time. | Derived |
| `is_static` | Frontend-declared compile-time value (the frontend-facing `static` concept). Seeds the comptime evaluator. | Suggested |
| `is_comptime` | HIR-proven compile-time value. | Derived |
| `is_constant` | Value is constant; not necessarily compile-time-evaluable. | Derived |
| `is_used` | Node has at least one use. | Derived |
| `is_mutably_used` | Node is mutated through at least one use. | Derived |
| `is_addressed` | Address taken via pointer or reference. | Derived |
| `escapes` | Value escapes its defining scope. | Derived |
| `is_runtime_mutable` | Storage is written at runtime. | Derived |
| `may_change_at_runtime` | The observable value may differ between reads or executions (external state). Distinct from `is_runtime_mutable`. | Derived |
| `is_volatile` | Accesses are volatile: not eliminated, not reordered, and not promoted to SSA. | Suggested |
| `is_atomic` | Accesses are atomic and carry a declared memory ordering. | Suggested |
| `complex` | Frontend-provided; gates the `NoOptimize` request. | Suggested |
| `layout` | Pinned aggregate ABI: field offsets, total size, and alignment. | Suggested |
`is_runtime_mutable` describes storage writes; `may_change_at_runtime` describes externally
observable value change. They are independent and both **MUST** be tracked.
`is_static` is trusted by HIR as "this intends to be compile-time", but it is still validated: HIR
**MUST** verify the claim before marking `is_comptime`.
`is_volatile` and `is_atomic` force a value to memory; lowering and later passes **MUST NOT**
promote such a value to SSA (see [005 — Lowering](005-lowering.md)). The `layout` attribute pins
an aggregate's ABI; the C backend **MUST** emit explicit padding and static assertions for a
layout-pinned aggregate (see [008 — C Backend](008-c-backend.md)).
## 5. Request model
Requests are attribute-derived suggestions. A frontend suggesting a request that no attribute
supports is invalid and HIR **MUST** reject it with a diagnostic.
Each request has a **strength**:
- **Soft**: a suggestion VIR **MAY** decline.
- **Strong**: violating it is likely a bug. A capable consumer **MUST** honor it or report an
error with a `DenyReason` of `Illegality` or `ContradictsTrait` (see
[007 — VIR Optimizer](007-vir-optimizer.md)).
Every attempted request produces a `RequestResult`:
```d
struct RequestResult {
Request req;
bool accepted;
DenyReason reason;
string note; // one-line human-readable explanation
}
```
A denial **MUST** carry a `DenyReason` and a one-line `note`. On acceptance, `reason` is
unspecified and `note` **MAY** be empty.
### 5.1 DenyReason
| Enumerator | Meaning |
|---|---|
| `CostModel` | Profitable only under a different cost model. |
| `Illegality` | The 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` | The backend or target cannot express it. |
## 6. Request catalog
The v1 catalog contains **42** requests. Requests marked **Strong** are noted; all others are
**Soft**.
### 6.1 Inlining and call edges (10)
| Request | Strength | Meaning |
|---|---|---|
| `Inline` | Soft | Callee is a candidate for inlining. |
| `AlwaysInline` | **Strong** | Callee must be inlined. |
| `NoInline` | **Strong** | Callee must not be inlined. |
| `TailCall` | Soft | Prefer tail-call formation. |
| `MustTail` | **Strong** | Tail-call formation is required. |
| `NoTail` | **Strong** | Tail-call formation must not occur. |
| `Devirtualize` | Soft | Call target is expected to resolve to one target. |
| `ColdCall` | Soft | Call is unlikely; optimize for size. |
| `LikelyCall` | Soft | Call is likely executed. |
| `UnlikelyCall` | Soft | Call is unlikely executed. |
### 6.2 Loops (12)
| Request | Strength | Meaning |
|---|---|---|
| `Vectorize` | Soft | Vectorize the loop. |
| `Unroll(factor)` | Soft | Unroll by `factor`. |
| `Interleave(count)` | Soft | Interleave `count` iterations. |
| `Peel(count)` | Soft | Peel `count` iterations. |
| `Distribute` | Soft | Distribute loop bodies. |
| `Fuse` | Soft | Fuse adjacent loops. |
| `Jam` | Soft | Fuse loops by jamming. |
| `Unswitch` | Soft | Unswitch loop-invariant conditions. |
| `Rotate` | Soft | Rotate the loop. |
| `LICM` | Soft | Explicit request to hoist loop-invariant code (an action). |
| `LoopInvariant` | Soft | A proven invariant property VIR may exploit (a property). |
| `MustProgress` | Soft | The loop is guaranteed to make progress. |
`LICM` and `LoopInvariant` are distinct: one requests an action, the other asserts a property. Both
**MUST** be retained by HIR.
### 6.3 Memory effects (13)
| Request | Strength | Meaning |
|---|---|---|
| `NoAlias` | Soft | The value aliases no other relevant value. |
| `Restrict` | Soft | The value may be marked `restrict`. |
| `NonNull` | Soft | The value is not null. |
| `Align(n)` | Soft | The value is aligned to `n`. |
| `ReadOnly` | Soft | Region-level read-only fact. |
| `WriteOnly` | Soft | Region-level write-only fact. |
| `NoRead` | Soft | Primitive fact on an access edge: no read occurs. |
| `NoWrite` | Soft | Primitive fact on an access edge: no write occurs. |
| `NoCapture` | Soft | The value is not captured by a callee. |
| `Dereferenceable(n)` | Soft | At least `n` bytes are dereferenceable. |
| `Constant` | Soft | The value is immutable. |
| `NoUndef` | Soft | Reserved; meaningful only on the LLVM path. |
| `Prefetch` | Soft | Insert a prefetch hint. |
`NoRead`/`NoWrite` are **primitives on an access edge**; `ReadOnly`/`WriteOnly` are **region-level
facts** derived from them. All four are retained, and their scope difference is part of their
meaning.
### 6.4 Control flow, assumptions, and optimization control (7)
| Request | Category | Strength | Meaning |
|---|---|---|---|
| `HotPath` | Control flow | Soft | Block is likely executed. |
| `ColdPath` | Control flow | Soft | Block is unlikely executed. |
| `Unreachable` | Control flow | Soft | Execution never reaches this point. |
| `NoReturn` | Control flow | Soft | The call never returns. |
| `Assume(pred)` | Assumptions | Soft | `pred` holds at this point. |
| `Range(lo, hi)` | Assumptions | Soft | The value lies in `[lo, hi]`. |
| `NoOptimize` | Optimization control | **Strong** | The region must not be rewritten. Requested only when the frontend sets `complex`. |
HIR **MUST NOT** issue `NoOptimize` unless `complex` is set on the node. The resulting region is an
optimization barrier (see [005 — Lowering](005-lowering.md)).
## 7. Relationships
Relationships are directed edges that HIR leaves for the non-constant world. They are traceable
**up and down** to a terminal node.
### 7.1 Relationship flags
| Flag | Meaning |
|---|---|
| `is_offspring` | The edge cannot be traced up. |
| `is_ancestor` | The edge cannot be traced down. |
| `is_common` | Branch point; a consumer must choose a direction. |
| `has_siblings` | The node connects down to two or more nodes. |
| `is_apex` | The node participates in a cycle (`a → b → c → d → a`), usually from polymorphism. |
An apex **SHOULD** normally be ironed out by HIR. A residual apex after the HIR cascade typically
arises from polymorphism, and a consumer that walks into one **MUST** handle the cycle rather than
loop forever.
### 7.2 Relation attributes
Each relationship edge carries a small attribute set:
| Attribute | Meaning |
|---|---|
| `was_changed` | The related value changed since the relationship was recorded. |
| `is_mutable` | The related value is mutable. |
| `is_pointer` | The relationship passes through a pointer. |
## 8. Query API semantics
The trait query interface is the **only** channel through which VIR obtains facts. Its guaranteed
semantics are:
1. A query **MUST** return the trait values valid for the node's **current epoch**. A query against
a node whose trait is stale for the current epoch **MUST** fail rather than return stale data.
2. A query **MUST NOT** trigger analysis or re-derivation. It is a read of already-derived facts.
3. Reading `const_value` when absent **MUST** be reported as "not compile-time known" and **MUST
NOT** be interpreted as zero or any other value.
4. Walking a relationship in the available direction(s) **MUST** terminate at a terminal node or
report that the walk hit an apex cycle.
5. A consumer **MUST NOT** invent, weaken, or generalize a fact absent from the trait. VIR must not
assume a property that no trait states.
6. Query results **MUST** be deterministic: for a fixed module and epoch, the same query returns the
same result regardless of thread count.
## 9. Representation
Representation has two parts: an **embedded dense `Trait` struct** on every node is primary, so
common attributes are read without indirection; an **optional sparse side-channel** holds rare or
frontend-extensible attributes and **MAY** be absent. A consumer **MUST** observe the same logical
trait regardless of which part carries it. The dense struct is the reference representation; the
side-channel is an optimization, not a semantic difference.
## 10. Staleness
Traits are annotated in place, so a rewrite can invalidate dependent traits. Staleness is resolved
with **epoch/generation counters**:
1. Each node carries an **epoch**. A trait is valid only for the node's **current epoch**.
2. A rewrite that changes a trait **MUST** advance the affected node's epoch.
3. A rewrite that can affect a related node **MUST** mark that relationship dirty and propagate the
invalidation along the relationship edge.
4. A derived trait computed from a node whose epoch has advanced **MUST** be recomputed before it is
consumed.
5. Epoch counters **MUST** be sufficient to keep parallel HIR workers from reading stale traits.
This is why the model survives parallel passes.
## 11. Request conflict resolution
Requests from independent attributes can conflict. Resolution is normative:
1. **Strength ordering.** `Strong` beats `Soft`. When a Strong and a Soft request conflict, the
Strong request wins and the Soft request is discarded without a diagnostic.
2. **Conflicting Strongs.** When two **Strong** requests conflict, HIR **MUST** emit a diagnostic
and **MUST** reject the frontend suggestion. The derived, authoritative request wins.
3. **Canonical opposing pairs** are `{Inline, AlwaysInline}` against `{NoInline}`, and
`{TailCall, MustTail}` against `{NoTail}`. In the named pairs `Inline`/`NoInline` and
`TailCall`/`NoTail`, the `No*` member is Strong, so those resolve by rule 1; the corresponding
Strong-versus-Strong pairs (`AlwaysInline`/`NoInline`, `MustTail`/`NoTail`) resolve by rule 2.
4. A rejected suggestion **MUST NOT** silently disappear; the diagnostic and a remark **MUST**
record the rejection.
## 12. Invariants
- Every node carries exactly one `Trait`.
- Every `RequestResult` denial carries both a `DenyReason` and a non-empty `note`.
- A derived attribute is authoritative; consumers **MUST NOT** re-derive it.
- `const_value` is present if and only if `is_comptime` holds for the node.
- No consumer reads a trait whose epoch is not the node's current epoch.
- The trait vocabulary is versioned independently of the textual IR format (see
[013 — Build & Packaging](013-build-packaging.md)).
## 13. Example
For `y = x + 4` where HIR proves `x` is compile-time `4`, the node `x` carries `is_comptime`,
`is_constant`, and `const_value = 4` with `attr_source = derived`; the fold marks downstream nodes
`is_comptime` and issues an `Inline` request when the call is proven small. A frontend that had
suggested `is_comptime = false` on `x` receives a warning and sees HIR overwrite it. The full
walkthrough is normative in [014 — Worked Example](014-worked-example.md).
## 14. Cross-references
- [000 — Overview](000-overview.md)
- [001 — Architecture](001-architecture.md)
- [003 — HIR](003-hir.md)
- [004 — HIR Optimizer](004-hir-optimizer.md)
- [005 — Lowering](005-lowering.md)
- [007 — VIR Optimizer](007-vir-optimizer.md)
- [011 — Diagnostics](011-diagnostics.md)
- [013 — Build & Packaging](013-build-packaging.md)
- [014 — Worked Example](014-worked-example.md)
+255
View File
@@ -0,0 +1,255 @@
# 003 — HIR
- **Status:** Draft
- **Normative language:** `MUST`, `MUST NOT`, `SHOULD`, `SHOULD NOT`, and `MAY` are to be
interpreted as described in RFC 2119.
## 1. Purpose
HIR (High-level IR) is the tree-shaped abstract syntax that a frontend builds and libVCT annotates
in place. It is the discovery half of the two-stage IR: HIR establishes node identity, attaches
traits, and performs cheap folds. Lowering consumes annotated HIR and produces VIR (see
[005 — Lowering](005-lowering.md)).
This file specifies the HIR node model, the node kinds, node identity, the builder/emitter contract,
the `defer` statement, and the textual HIR format.
## 2. Scope
In scope: the HIR data model and its observable contract toward frontends, the HIR optimizer, and
lowering. Out of scope: the trait vocabulary itself (see [002 — Traits](002-traits.md)), the
optimizer's pass framework and cascade (see
[004 — HIR Optimizer](004-hir-optimizer.md)), and the HIR-to-VIR mapping (see
[005 — Lowering](005-lowering.md)).
A frontend that embeds libVCT interacts with HIR through the builder API and the trait query API.
The textual HIR format is a tool surface, not a frontend surface.
## 3. Node model
Key properties, all normative:
1. HIR is **tree-shaped**. Every node except the module root has exactly one parent. A node has an
ordered list of children whose meaning is determined by the node kind.
2. HIR is **arena-owned**. Nodes are allocated from the module's arena and are never individually
freed. An arena is reclaimed wholesale (see [001 — Architecture](001-architecture.md) and
[013 — Build & Packaging](013-build-packaging.md)).
3. HIR is **annotated in place**. Every node carries one embedded `Trait` (see
[002 — Traits](002-traits.md)) that the HIR optimizer mutates as it derives facts. There is no
separate side table for the common case.
4. HIR is **high-level**. It preserves structured control flow (`if`/`while`/`for`/`switch`/`block`)
and expressions; it is not SSA and has no phi-nodes.
5. Nodes are mutable during the HIR optimizer's cascade and become read-only to a consumer once
traits are epoch-valid. A frontend **MUST NOT** mutate a module after finishing it.
Every node exposes at least:
```d
struct HirNode {
NodeId id; // stable identity, unique within the module
NodeKind kind;
Span span; // optional frontend source span
Trait trait; // attribute + requests + relationships
NodeId[] children;
}
```
`span` **MAY** be absent. Absence **MUST** degrade diagnostics gracefully and **MUST NOT** affect
optimization correctness.
## 4. Node kinds
The following is the **reference node set** for v1. A frontend **MUST** build modules only from
these kinds. Additional kinds are reserved and require a specification change. A frontend **MAY**
use any subset of the set.
### 4.1 Structural kinds
| Kind | Description | Children / payload |
|---|---|---|
| `Module` | Root of one translation unit. | functions, globals |
| `Function` | A function definition or declaration. | params, body `Block` |
| `Param` | A function parameter. | none |
| `Block` | An ordered statement scope. | statements |
### 4.2 Declaration kinds
| Kind | Description | Children / payload |
|---|---|---|
| `GlobalVar` | A module-level storage object. | optional init expression |
| `LocalVar` | A local binding with optional initializer. | optional init expression |
A declaration **MUST** carry a `ty` attribute. A declaration whose initializer is compile-time
known **MUST** be eligible for `is_comptime`/`const_value` annotation by the optimizer.
### 4.3 Statement kinds
| Kind | Description | Children / payload |
|---|---|---|
| `ExprStmt` | Evaluate an expression for effect. | expression |
| `Return` | Return from the enclosing function. | optional value |
| `If` | Two-way branch. | condition, then-block, optional else-block |
| `While` | Pre-test loop. | condition, body |
| `For` | C-style loop. | optional init, optional condition, optional step, body |
| `Switch` | Multi-way branch. | subject, cases |
| `Case` | One switch arm. | optional value, body |
| `Break` | Exit the innermost loop or switch. | none |
| `Continue` | Continue the innermost loop. | none |
| `Defer` | Scope-exit action; see the `defer` section below. | deferred expression or statement |
### 4.4 Expression kinds
| Kind | Description | Children / payload |
|---|---|---|
| `Literal` | Integer, float, string, boolean, or null constant. | literal payload |
| `NameRef` | Reference to a declared name. | resolved target identity |
| `Unary` | Prefix or postfix unary operator. | operand |
| `Binary` | Binary operator. | left, right |
| `Assign` | Assignment to an lvalue. | target, value |
| `Call` | Function or intrinsic call. | callee, arguments |
| `Member` | Aggregate field access. | base, field index |
| `Index` | Pointer or array element access. | base, index |
| `Cast` | Type conversion. | operand, target type |
Every expression node **MUST** carry a `ty` attribute once HIR has validated it. `Literal` nodes
**MUST** additionally carry `const_value` when their value is representable in the type.
## 5. Node identity
1. Every node **MUST** have a `NodeId` that is unique within its `Module`.
2. Identity **MUST** be assigned deterministically from construction order, so that two runs over
the same input produce the same identity assignment.
3. Identity **MUST** remain stable across trait annotation and optimizer rewrites of the same node.
4. A deleted node's identity **MUST NOT** be reused for a different node.
5. VIR depends on HIR for **traits and node identity only** (see
[001 — Architecture](001-architecture.md)). Lowering records the mapping from HIR identity to
VIR entities (see [005 — Lowering](005-lowering.md)).
Identity is the join key that lets diagnostics, relationships, and the lowering map refer to the
same node across stages.
## 6. The `defer` statement
HIR carries `defer` as a **real high-level statement**. Its observable semantics:
1. A `Defer` node names a body that runs at **every scope exit** of the enclosing `Block`.
2. Scope exits include normal fall-through, `Break`, `Continue`, and `Return`.
3. Deferred bodies within one scope run in **reverse declaration order** (last declared runs first).
4. A backend **MUST NOT** ever observe a `Defer` node. Before VIR is produced, every `Defer` marker
**MUST** be erased with zero runtime residue. The behavior **MUST** be identical to writing the
deferred body textually at each exit point.
5. Expansion is performed by the HIR optimizer when HIR optimization is enabled. If HIR
optimization is disabled (`-O0`, see
[004 — HIR Optimizer](004-hir-optimizer.md)), **lowering itself MUST expand `defer` at every
scope exit** (see [005 — Lowering](005-lowering.md)).
6. Expansion **MUST** be deterministic. When a scope has multiple exits, the emitted bodies **MUST**
appear in a stable, specified order so that output is byte-identical across runs.
Because expansion is always resolved before VIR, `defer` costs nothing at runtime.
## 7. Builder and emitter contract
### 7.1 Frontend-facing builder
The builder is the frontend's only construction surface. It provides operations to create each
node kind, link children, set a node's type and value, and attach frontend-suggested traits
(including `is_static` and optimization hints). Its contract:
1. The builder **MUST** assign node identity per the node identity section above.
2. The builder **MUST** allow a frontend to construct a module without supplying derived
attributes. Derived facts are HIR's responsibility, not the frontend's.
3. The builder **MUST** accept a frontend-suggested attribute or request, record it with
`attr_source = suggested`, and defer validation to HIR.
4. `finish(module)` **MUST** validate every suggested trait. Validation follows the rule in
[002 — Traits](002-traits.md): on a wrong or unprovable suggestion HIR emits a warning (an error
under `-Werror`), **overwrites** the value with the correct one, and marks it `derived`. A
correct suggestion is preserved.
5. After `finish`, the builder **MUST** reject further structural mutation of that module by the
frontend. Optimization mutates in place internally, under epoch guards.
### 7.2 Trait attachment
The frontend attaches a `Trait` to each node. The trait is incomplete at build time: the frontend
supplies `ty`, `is_static`, and `complex` where it knows them, and the HIR optimizer fills in the
rest. A node lacking a derived attribute is not an error at build time; it becomes one only if
lowering finds the trait incomplete (see [005 — Lowering](005-lowering.md)).
HIR **MUST NOT** require a frontend to compute `is_used`, `escapes`, `is_addressed`,
`const_value`, or any other derived attribute.
### 7.3 Textual emitter
The textual emitter is a distinct, non-authoring surface (see the textual HIR format section). It
**MUST** be able to serialize a finished HIR module and to parse that serialization back into an
equivalent module.
## 8. Textual HIR format
HIR has a textual form used **solely for tools and round-trip tests**. It is explicitly **NOT** an
authoring surface: frontends embed the library and build HIR through the builder API. The format is
versioned independently of the library and of the trait vocabulary (see
[013 — Build & Packaging](013-build-packaging.md)).
The format carries, per node: kind, identity, child references, type, and the node's trait (with
each attribute's value and `attr_source`, its requests and strengths, and its relationships). The
reference round-trip property is:
```
print(parse(print(m))) == print(m)
```
A conforming implementation **MUST** satisfy this property for any module it can emit. Parsing
**MUST NOT** be required to accept hand-written input beyond what the emitter produces.
Illustrative fragment (shape only, not a complete grammar):
```
fn @main() -> i32 {
%v1: i32 = literal 4 ; attr: const_value=4 is_comptime=true (derived)
%v2: i32 = binary add %v1, 4 ; req: Inline (soft)
defer %v3
ret %v2
}
```
## 9. Invariants
- Every non-root node has exactly one parent; the module root has none.
- Every node has a unique `NodeId` within its module, and ids are not reused.
- Every node carries exactly one `Trait`.
- After `finish`, every suggested attribute has been validated and either preserved or overwritten.
- No `Defer` node survives into VIR.
- HIR is not SSA and **MUST NOT** be required to answer SSA questions.
- Frontend-supplied types and values are suggestions, never authoritative facts.
## 10. Example
A frontend builds:
```
fn main() -> int {
x = 4;
y = x + 4;
z = foo(x, y);
a = sqrt(z);
println(a);
}
```
It creates `Function`, `LocalVar`, `Binary`, `Call`, and `Return` nodes, sets each node's `ty`,
attaches `is_static` where known, and calls `finish`. HIR then validates the suggestions, derives
`const_value` for `x`, `y`, and the folded `sqrt` result, and issues an `Inline` request for
`foo` (see [004 — HIR Optimizer](004-hir-optimizer.md)). Content that `defer` would add is expanded
at scope exits, so the emitted VIR contains no defer marker. The full walkthrough is normative in
[014 — Worked Example](014-worked-example.md).
## 11. Cross-references
- [000 — Overview](000-overview.md)
- [001 — Architecture](001-architecture.md)
- [002 — Traits](002-traits.md)
- [004 — HIR Optimizer](004-hir-optimizer.md)
- [005 — Lowering](005-lowering.md)
- [011 — Diagnostics](011-diagnostics.md)
- [013 — Build & Packaging](013-build-packaging.md)
- [014 — Worked Example](014-worked-example.md)
+253
View File
@@ -0,0 +1,253 @@
# 004 — HIR Optimizer
- **Status:** Draft
- **Normative language:** `MUST`, `MUST NOT`, `SHOULD`, `SHOULD NOT`, and `MAY` are to be
interpreted as described in RFC 2119.
## 1. Purpose
The HIR optimizer is the **discovery and small-fold** stage. It validates frontend-suggested traits,
derives attributes and relationships, evaluates compile-time expressions, runs cheap folds to a
fixpoint, and expands `defer`. Its output is annotated HIR whose traits are complete and
epoch-valid, and which hands VIR a "dictionary" of facts.
The HIR optimizer **MUST NOT** perform heavy transformations. In particular, it **MUST NOT** inline
code (see the "HIR never inlines" requirement below).
## 2. Scope
In scope: the HIR pass framework, triggers, the worklist scheduler and cascade-to-fixpoint, per-
function parallelism and the module-level fixpoint, epoch-based staleness, the comptime evaluator,
`defer` expansion, optimization-level behavior, and default budgets.
Out of scope: the trait vocabulary (see [002 — Traits](002-traits.md)), the node model and builder
contract (see [003 — HIR](003-hir.md)), and the HIR-to-VIR translation (see
[005 — Lowering](005-lowering.md)).
## 3. Definitions
| Term | Definition |
|---|---|
| **Pass** | A unit of discovery or folding over one function's HIR. |
| **Trigger** | A record that a rewrite succeeded and may enable further work. |
| **Worklist** | The deduplicated queue of `(pass, node)` work items. |
| **Cascade** | The chain of rewrites triggered by one fold. |
| **Fixpoint** | The state in which the worklist drains with no new work. |
| **Epoch** | The per-node generation counter that invalidates stale traits. |
| **Step budget** | The bounded amount of work allowed before the scheduler stops. |
## 4. Pass framework
A pass is a named unit with the following shape:
```d
interface HirPass {
string name();
void run(HirFunction fn, HirContext ctx);
}
```
Normative requirements:
1. A pass **MUST** mutate the annotated AST **in place**. It **MUST NOT** rebuild the tree or
return a replacement.
2. A pass **MUST** be deterministic: for a fixed input and epoch, it produces the same mutations in
the same order regardless of thread count.
3. A pass **MUST** emit a **trigger** for every successful rewrite (see the triggers section).
4. A pass **MUST** respect the step budgets in the budgets section. On exhaustion it **MUST** stop
and leave the tree in a consistent, verifiable state.
5. A pass **MUST NOT** read a trait whose epoch is not the node's current epoch (see the
epoch-based staleness section).
## 5. Triggers
Successful rewrites emit triggers, which the scheduler uses to enqueue dependent work:
| Trigger | Meaning |
|---|---|
| `ConstantUnfolded` | A node's value became compile-time known. |
| `UsesReplaced` | Uses of a node were replaced, potentially making the node or its users constant or dead. |
| `NodeDeleted` | A node was removed; its former users may now be foldable. |
| `TraitChanged` | A trait changed and dependent traits may be stale. |
| `StaticDiscovered` | A frontend `is_static` claim was confirmed or a new compile-time value was found. |
A rewrite that changes no fact **MUST NOT** emit a trigger. Emitting a trigger for a no-op risks a
non-terminating cascade and is a correctness defect.
## 6. Worklist scheduler and cascade
### 6.1 Scheduler
1. The scheduler collects `(pass, node)` pairs into a **worklist**, **deduplicates** them, and runs
until the worklist drains to a **fixpoint**: a state in which no scheduled work produces a new
trigger.
2. Order of processing within one function **MUST** be deterministic, so cascade order is
reproducible.
3. A **per-function step budget** guarantees termination even if triggers keep firing. When the
budget is exhausted the scheduler **MUST** stop scheduling further passes for that function and
report the exhaustion.
4. The scheduler **MUST NOT** schedule work for a node whose governing function is not yet ready in
the current phase.
### 6.2 Canonical cascade
The reference cascade for a fold:
```
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. The cascade is the
mechanism by which one fold can make a whole subtree constant; it **MUST** reach the same fixpoint
regardless of the order in which independent items are processed.
## 7. Parallelism and determinism
1. HIR passes **MUST** parallelize **per function**. Independent function trees may be processed
concurrently.
2. Within one function, passes **MUST** run **sequentially**, so cascade order is deterministic.
3. Cross-function effects, such as cross-module constant propagation and requests that depend on
callees, **MUST** use a **module-level fixpoint**:
- run per-function passes in parallel;
- run a deterministic module phase;
- re-enqueue only the functions whose inputs changed.
4. The module phase **MUST** be deterministic: it processes functions in a stable order and folds
their results in a stable order.
5. The determinism guarantee of [000 — Overview](000-overview.md) binds the HIR optimizer: for a
fixed `Config` and input, output **MUST** be byte-identical independent of thread count and of
`-j`.
## 8. Epoch-based staleness
1. Each node carries an epoch counter. A trait is valid only for the node's **current epoch**.
2. A rewrite that changes a trait **MUST** advance the affected node's epoch.
3. A rewrite that can affect a related node **MUST** mark the relationship dirty and propagate
invalidation along the relationship edge.
4. A pass in a parallel worker **MUST NOT** consume a trait that became stale after the worker read
it. Epoch checks **MUST** detect this.
5. A stale derived trait **MUST** be recomputed before it is consumed. Recomputing it **MUST** be
part of the cascade, not a silent fallback to a default.
This model is what makes parallel HIR passes safe without a global lock.
## 9. Comptime evaluator
The comptime evaluator is an interpreter over HIR subgraphs. It computes compile-time values and
marks them on the nodes.
### 9.1 Eligibility
An HIR subgraph is eligible for comptime evaluation **only if** all of the following hold:
1. It has **no side effects**.
2. It involves **primitives only** (no aggregate or opaque operations that cannot be evaluated
element-wise).
3. It performs **no global mutation**.
4. It performs **no I/O**.
5. It stays within the loop and recursion budgets (see the budgets section below).
If any condition fails, the evaluator **MUST** bail out cleanly and defer the computation to VIR.
A bailout **MUST NOT** mark any partial result as compile-time known.
### 9.2 Outputs
On successful evaluation, the evaluator writes, in order:
1. `const_value`: the computed value.
2. `is_comptime` and `is_constant`: the proven properties.
`const_value` **MUST** be present if and only if `is_comptime` holds.
### 9.3 Seeding
The frontend `is_static` attribute (the frontend-facing `static` concept) **seeds** the evaluator:
HIR treats `is_static` as the claim "this is compile-time" and **MUST** validate the claim before
setting `is_comptime`. A false `is_static` claim is handled by the attribute validation rule of
[002 — Traits](002-traits.md): warning, overwrite, mark derived.
## 10. `defer` expansion
1. When HIR optimization is enabled, the optimizer **MUST** expand every `defer` body at every
scope-exit path and erase the `defer` marker. This leaves zero runtime residue.
2. Expansion **MUST** obey the ordering and determinism rules of [003 — HIR](003-hir.md).
3. When HIR optimization is disabled (`-O0`), the optimizer does not expand `defer`; lowering
performs the same expansion. Either way, no `Defer` node reaches VIR.
## 11. Optimization-level behavior
| Level | HIR behavior | VIR |
|---|---|---|
| `-O0` | Traits validated only. | off |
| `-O1` | Builtin folds and discovery. | off |
| `-O2` | Full cascade plus comptime evaluation. | **on** |
| `-O3` | `-O2` plus special AST transforms (loop-unfold/vectorize, then constant-unfold, then comptime). | on |
| `-Ofast` | `-O3` plus `-march=native` and fast-math. | on |
| `-Oz` | Size-tuned discovery and folds. | on |
Compiler-friendly C is emitted at **all** optimization levels. `-O3` only adds more aggressive AST
transforms; it does not change the trait contract.
## 12. HIR never inlines
HIR **MUST NOT** inline code. It proves that a call is small and eligible, then issues an `Inline`
request (see [002 — Traits](002-traits.md)). Inlining is a VIR/IPA transform (see
[007 — VIR Optimizer](007-vir-optimizer.md)). This separation keeps HIR's work cheap and
parallelizable.
## 13. Default budgets
All budgets are configurable through the API and CLI. The defaults are:
| Budget | Default |
|---|---|
| `comptime.max_iterations` | `4096` |
| `comptime.max_recursion_depth` | `256` |
| `comptime.max_steps` | `1000000` |
| `comptime.max_aggregate_elements` | `65536` |
| `hir.max_rewrites_per_function` | `100000` |
| `hir.max_rewrites_per_module` | `1000000` |
A budget exhaustion is a normal, reported outcome; it **MUST NOT** be treated as a crash and
**MUST NOT** produce partially annotated output that lowering would accept as complete.
## 14. Invariants
- The worklist drains to a fixpoint or stops at an explicit budget: the scheduler never loops
forever.
- Every successful rewrite emits exactly the triggers needed to re-enqueue affected work.
- A no-op rewrite emits no trigger.
- Every pass validates the epochs of traits it reads.
- After HIR optimization, every trait consumed by lowering is complete and epoch-valid.
- HIR performs no inlining.
- No `Defer` node survives HIR optimization when HIR optimization is enabled.
- Output is deterministic for a fixed `Config` and input, independent of thread count.
## 15. Example
For `y = x + 4` with `x = 4`:
1. The comptime evaluator folds `x` to `const_value = 4`, marking `is_comptime` and `is_constant`.
2. `ConstantUnfolded` triggers the fold of `y` to `8`.
3. `foo(x, y)` is proven small; HIR issues a soft `Inline` request instead of inlining it.
4. The `sqrt` call folds to a floating-point constant.
5. The worklist drains; the module reaches a fixpoint with `println` left as the only real call.
The normative end-to-end walkthrough is [014 — Worked Example](014-worked-example.md).
## 16. Cross-references
- [000 — Overview](000-overview.md)
- [001 — Architecture](001-architecture.md)
- [002 — Traits](002-traits.md)
- [003 — HIR](003-hir.md)
- [005 — Lowering](005-lowering.md)
- [007 — VIR Optimizer](007-vir-optimizer.md)
- [011 — Diagnostics](011-diagnostics.md)
- [013 — Build & Packaging](013-build-packaging.md)
- [014 — Worked Example](014-worked-example.md)
+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.
+298
View File
@@ -0,0 +1,298 @@
# 006 — VIR
- **Status:** Draft
- **Normative language:** `MUST`, `MUST NOT`, `SHOULD`, `SHOULD NOT`, and `MAY` are to be
interpreted as described in RFC 2119.
## 1. Purpose
VIR (Vox IR) is the low-level instruction IR of libVCT. It is an SSA form over a control-flow
graph. Every optimization above the backends operates on VIR, and both the C backend and the LLVM
backend consume VIR. Lowering produces it (see [005 — Lowering](005-lowering.md)); the VIR
optimizer transforms it (see [007 — VIR Optimizer](007-vir-optimizer.md)).
VIR is internal and is intended to be machine-generated. A textual form exists for tools,
dumps, and round-trip tests, but it is not a supported authoring surface: programs are built
through the HIR builder or as fixtures for tests (see [003 — HIR](003-hir.md),
[012 — Testing](012-testing.md)).
## 2. Scope
This file specifies the VIR SSA/CFG model, the VIR type system, the instruction set, and the
well-formedness invariants every VIR module must satisfy. It does not specify how VIR is produced
(see [005 — Lowering](005-lowering.md)), how it is optimized (see
[007 — VIR Optimizer](007-vir-optimizer.md)), or how it is emitted (see
[008 — C Backend](008-c-backend.md), [009 — LLVM Backend](009-llvm-backend.md)).
## 3. SSA/CFG model
A **VIR module** contains functions. A **function** has a name, a signature, and a non-empty list
of basic blocks; one block is the **entry block**. Every function is either a definition or a
declaration. A declaration has a signature and no blocks.
A **basic block** has a label, unique within its function, and an ordered list of instructions
ending in exactly one **terminator**. The terminator is the last instruction of the block.
An **SSA value** has a type and is defined by exactly one instruction. Values are referenced by
name. Following the LLVM convention, SSA names written `%name` are numbered values; unnumbered
temporaries are assigned deterministic numbers at print time.
The **CFG** is the directed graph whose nodes are blocks and whose edges are taken from
terminators. Edge `b -> c` means c is a **successor** of b and b is a **predecessor** of c. The
entry block has no predecessors.
**Domination.** Block `a` dominates block `b` if every path from the entry block to `b` passes
through `a`. Domination is reflexive; `a` strictly dominates `b` when `a` and `b` differ. An
instruction `d` dominates a use `u` when the block containing `d` dominates the block containing
`u`, or `d` and `u` are in the same block and `d` appears before `u`.
**SSA form.** Every value has exactly one definition, and every use is dominated by its definition.
The only relaxation is the `Phi` instruction, whose operands are conceptually evaluated at the end
of the corresponding predecessor.
**Def-use.** Each use of a value is a `(user-instruction, operand-index)` pair. The def-use graph
is the union of those pairs over all instructions. Passes that rewrite an operand MUST update both
sides of the edge.
## 4. Type system
VIR types are strictly typed. There are no implicit conversions; a conversion is an explicit cast
instruction.
| Type | Meaning |
|---|---|
| `void` | No value. The result of effect-only calls and functions. |
| `iN` | An `N`-bit two's-complement integer, `N >= 1`. `i1` is boolean. |
| `f32`, `f64` | IEEE-754 binary32 and binary64. `f16`, `f80`, `f128` MAY be supported. |
| `ptr<T>` | A pointer to `T`. |
| `[N x T]` | An array of `N` elements of type `T`. |
| `{T1, ..., Tn}` | A struct with the listed field types in order. A packed variant MAY exist. |
| `union {T1, ...}` | A union of the listed member types. |
| `fn(T1, ...) -> R` | A function type: parameter types and return type `R`. A variadic variant MAY exist. |
- **V-1.** `iN` MUST accept any width `N >= 1`; the implementation MUST NOT restrict integers to a
fixed set of power-of-two widths.
- **V-2.** `void` MUST NOT be used as the type of a value. It is valid only as a function result
type and as the result type of a call that produces no value.
- **V-3.** Aggregate types (`[N x T]`, struct, union) are first-class: they MAY be operands and
results of `load`, `store`, `phi`, `select`, and calls.
- **V-4.** Pointer element types are used for typing `load`, `store`, and indexed `gep`. They are
a convenience for the C backend; the LLVM backend emits opaque `ptr` and relies on the
instructions' operand types for correctness (see [009 — LLVM Backend](009-llvm-backend.md)).
## 5. Instruction set
An instruction has an opcode, zero or more operands, an optional result type and value, and
optional flags. The catalog below is normative for opcode meaning; the exact textual spelling is
specified by the dump format and is not an authoring interface.
### 5.1 Terminators
| Opcode | Syntax (conceptual) | Meaning |
|---|---|---|
| `br` | `br %target` | Unconditional edge. |
| `condbr` | `condbr %cond, %iftrue, %iffalse` | Conditional edge on an `i1` value. |
| `switch` | `switch %value, %default [ case ... ]` | Multi-way edge on an integer value. |
| `ret` | `ret void` / `ret %value` | Return from the function. |
| `unreachable` | `unreachable` | Marks a point that cannot be reached at runtime. |
- **V-5.** Every block MUST end in exactly one terminator.
- **V-6.** A `condbr` condition MUST have type `i1`.
- **V-7.** A `switch` value MUST have integer type; each case constant MUST fit that type.
- **V-8.** A `ret` operand MUST match the function's declared return type, and a void function
MUST use `ret void`.
### 5.2 Memory instructions
| Opcode | Syntax (conceptual) | Meaning |
|---|---|---|
| `alloca` | `%p = alloca T [, align A]` | Allocate `T` on the stack; result type is `ptr<T>`. |
| `load` | `%v = load T, ptr<T> %p [, align A] [, volatile]` | Read a `T` from memory. |
| `store` | `store T %v, ptr<T> %p [, align A] [, volatile]` | Write a `T` to memory. |
| `gep` | `%p = gep T, ptr<T> %base, idx...` | Compute a pointer to an indexed element or field. |
| `memcpy` | `call ...` | Copy memory; also allowed as a dedicated opcode. |
| `memset` | `call ...` | Fill memory; also allowed as a dedicated opcode. |
| `memmove` | `call ...` | Overlapping copy; also allowed as a dedicated opcode. |
- **V-9.** A `load` of type `T` MUST take a pointer of type `ptr<T>`; a `store` MUST agree on `T`.
- **V-10.** A `gep` MUST index into aggregates by **field index**, not by byte offset. The first
index traverses the pointer; subsequent indices traverse arrays and struct fields.
- **V-11.** A `volatile` load or store MUST NOT be reordered with respect to any other volatile or
atomic access, and MUST NOT be eliminated even when its result is unused.
### 5.3 Arithmetic and bitwise instructions
| Category | Opcodes |
|---|---|
| Integer binary | `add`, `sub`, `mul`, `udiv`, `sdiv`, `urem`, `srem` |
| Integer bitwise | `and`, `or`, `xor` |
| Shifts | `shl`, `lshr`, `ashr` |
| Floating binary | `fadd`, `fsub`, `fmul`, `fdiv`, `frem` |
| Floating unary | `fneg` |
- **V-12.** Integer arithmetic MAY carry `nsw` (no signed wrap) and `nuw` (no unsigned wrap)
flags. A result on which overflow occurs under an asserted flag is poison; an implementation
that does not model poison MUST NOT emit such a flag unless the overflow is provably impossible.
- **V-13.** Floating-point instructions MAY carry the fast-math flags `nnan`, `ninf`, `nsz`,
`arcp`, `contract`, `afn`, and `reassoc`. A flag MUST only be set when the corresponding trait or
`Config` licenses it. Fast-math flags MUST NOT be applied at optimization levels below `-Ofast`
unless explicitly configured.
- **V-14.** `udiv`/`sdiv`/`urem`/`srem` by zero is undefined behavior. An implementation that
models this MUST NOT fold such a division to an arbitrary value.
### 5.4 Comparison instructions
| Opcode | Predicates |
|---|---|
| `icmp` | `eq`, `ne`, `slt`, `sle`, `sgt`, `sge`, `ult`, `ule`, `ugt`, `uge` |
| `fcmp` | `oeq`, `one`, `olt`, `ole`, `ogt`, `oge`, `ord`, `uno`, `ueq`, `une`, `ult`, `ule`, `ugt`, `uge`, `true`, `false` |
- **V-15.** The result of `icmp` and `fcmp` MUST have type `i1`.
- **V-16.** Integer comparison predicates beginning with `s` are signed; those beginning with `u`
are unsigned.
### 5.5 Conversion instructions
| Opcode | Meaning |
|---|---|
| `trunc` | Narrow an integer. |
| `zext`, `sext` | Widen an integer, zero- or sign-extending. |
| `fptrunc`, `fpext` | Narrow or widen a float. |
| `fptoui`, `fptosi` | Float to integer. |
| `uitofp`, `sitofp` | Integer to float. |
| `ptrtoint`, `inttoptr` | Pointer/integer conversion. |
| `bitcast` | Reinterpret bits; requires equal size and no pointer-to-pointer element change in the C backend. |
- **V-17.** A conversion MUST be explicit. There MUST be no implicit widening, narrowing, or
signedness change anywhere else in the instruction set.
### 5.6 Other instructions
| Opcode | Syntax (conceptual) | Meaning |
|---|---|---|
| `phi` | `%v = phi T [ v1, %b1 ], [ v2, %b2 ], ...` | SSA merge at a join. |
| `select` | `%v = select %cond, %a, %b` | Choose `%a` or `%b` on an `i1`. |
| `call` | `%v = call R @callee(args...) [flags]` | Call a function or intrinsic. |
| `atomicrmw` | `%v = atomicrmw op, ptr<T> %p, %x ordering` | Atomic read-modify-write, result is the old value. |
| `cmpxchg` | `%v = cmpxchg ptr<T> %p, %expected, %new ordering` | Atomic compare-and-exchange. |
| `fence` | `fence ordering` | Memory ordering fence. |
- **V-18.** A `phi` has one operand per predecessor, each a `(value, predecessor-block)` pair. Its
result type MUST equal each operand's type.
- **V-19.** A `select` condition MUST have type `i1`, and its two value operands MUST share a type
that equals the result type.
- **V-20.** A `call` result value MAY be absent when the callee returns `void`. Call-site attributes
(`Inline`, `NoInline`, `TailCall`, `MustTail`, `NoTail`, `ColdCall`, `NoReturn`, and the
memory-effect requests) MUST be carried on the call and read from its trait, not re-derived.
- **V-21.** Non-atomic accesses MUST NOT be reordered across an atomic access or a fence in a way
that violates the ordering the access declares.
### 5.7 Intrinsics
A VIR intrinsic is a well-known callee referenced by a `call` instruction. The v1 intrinsic set is
fixed:
| Intrinsic | Signature | Meaning |
|---|---|---|
| `memcpy` | `(ptr<T>, ptr<T>, iN) -> void` | Copy `N` bytes between non-overlapping regions. |
| `memset` | `(ptr<T>, i8, iN) -> void` | Fill `N` bytes with the given byte value. |
| `memmove` | `(ptr<T>, ptr<T>, iN) -> void` | Copy `N` bytes between possibly overlapping regions. |
| `sqrt` | `(f32) -> f32`, `(f64) -> f64` | IEEE-754 square root. |
| `fabs` | `(f32) -> f32`, `(f64) -> f64` | Absolute value. |
| `ctpop` | `(iN) -> iN` | Count of set bits. |
| `fshl` | `(iN, iN, iN) -> iN` | Funnel shift left. |
| `sadd_sat`, `uadd_sat`, `ssub_sat`, `usub_sat` | `(iN, iN) -> iN` | Saturating add and subtract. |
| `sadd_overflow`, `uadd_overflow`, `ssub_overflow`, `usub_overflow`, `smul_overflow`, `umul_overflow` | `(iN, iN) -> {iN, i1}` | Wrapping result plus an overflow flag. |
`memcpy`, `memset`, and `memmove` MAY also appear as dedicated opcodes (§5.2); the other intrinsics
appear only as calls.
- **V-25.** An intrinsic `call` MUST use the canonical name in the table above and operands that
match its signature. Its result type follows the signature; the overflow intrinsics return the
struct `{iN, i1}`.
- **V-26.** The intrinsic names are reserved. An implementation MUST recognize them as intrinsics
rather than as user-defined callees, and MUST NOT allow a module to redefine them.
## 6. Well-formedness invariants
A VIR module is **well-formed** when it satisfies all of the following. The verifier checks them
after lowering and, in debug builds, after every pass (see [012 — Testing](012-testing.md)).
- **WF-1 (single definition).** Every SSA value is defined exactly once in its function.
- **WF-2 (def dominates use).** Every use is dominated by its definition, with the phi exception:
an operand `(v, P)` of a phi requires `v` to dominate `P`, or to be the phi's own result when `P`
is a loop back-edge predecessor.
- **WF-3 (termination).** Every basic block ends in exactly one terminator, and no terminator
appears before the end of a block.
- **WF-4 (phi arity).** The number of phi operands equals the number of the phi block's
predecessors, and each named predecessor is an actual predecessor.
- **WF-5 (CFG consistency).** For every successor edge `b -> c` from a terminator, `c` lists `b`
as a predecessor. The entry block has no predecessors.
- **WF-6 (operand types).** Every instruction's operands and result conform to §4 and §5.
- **WF-7 (return type).** Every `ret` agrees with the function's declared return type.
- **WF-8 (function isolation).** No instruction references a value defined in another function.
- **WF-9 (declared callees).** Every `call` names a function that is either defined in the module
or declared in it.
- **WF-10 (alloca result).** An `alloca` of `T` produces a `ptr<T>`, and every `load`/`store`
through it agrees on `T`.
- **WF-11 (dead blocks).** A block unreachable from the entry block MAY remain, but it MUST itself
be well-formed.
Verification policy:
- **V-22.** In debug builds, the verifier MUST run after lowering and after every optimization pass.
A violation MUST assert.
- **V-23.** In release builds, verification MUST be available behind a flag (the driver's
`--verify`) and MUST NOT run by default. A violation detected in release MUST raise an
internal-compiler-error diagnostic as specified in [011 — Diagnostics](011-diagnostics.md).
- **V-24.** An implementation MUST NOT "repair" a detected violation silently.
## 7. Textual form
The textual form prints functions, blocks, instructions, and types in a stable order. It exists
for dumps, tooling, and round-trip tests. The round-trip property is
`print(parse(print(m))) == print(m)` (see [012 — Testing](012-testing.md)). The parser is not a
supported frontend: it MAY assume the input was produced by the printer.
## 8. Example
```
define i32 @sum(i32 %n) {
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:
%s.mid = add i32 %s, %i
br %loop.latch
loop.latch:
%i.next = add i32 %i, 1
%s.next = add i32 %s.mid, 0
br %loop.header
loop.exit:
ret i32 %s
}
```
The two phis are required because `loop.header` has two predecessors, `entry` and `loop.latch`.
WF-4 requires both phis to carry exactly two operands, and WF-2 requires each operand to dominate
its named predecessor.
## 9. Cross-references
- [000 — Overview](000-overview.md): determinism, non-goals (no hand-authored VIR).
- [001 — Architecture](001-architecture.md): VIR module placement and visibility.
- [002 — Traits](002-traits.md): the only channel by which VIR receives facts.
- [005 — Lowering](005-lowering.md): constructs and validates VIR.
- [007 — VIR Optimizer](007-vir-optimizer.md): transforms VIR under the invariants here.
- [008 — C Backend](008-c-backend.md): out-of-SSA and C emission.
- [009 — LLVM Backend](009-llvm-backend.md): 1:1 VIR to LLVM mapping.
- [011 — Diagnostics](011-diagnostics.md): internal-compiler-error policy.
- [012 — Testing](012-testing.md): verifier, round-trip, and differential tests.
+311
View File
@@ -0,0 +1,311 @@
# 007 — VIR Optimizer
- **Status:** Draft
- **Normative language:** `MUST`, `MUST NOT`, `SHOULD`, `SHOULD NOT`, and `MAY` are to be
interpreted as described in RFC 2119.
## 1. Purpose
The VIR optimizer is the heavy-transformation half of libVCT. It consumes lowered VIR and applies
large, whole-function and whole-module transformations: inlining, scalar and memory
optimization, loop transformation, vectorization, control-flow cleanup, and code-generation
preparation. It is where the design's codegen-quality goal is realized.
The VIR optimizer is a **strict trait consumer**. It discovers nothing. Every fact that enables or
forbids a transform is an attribute or relationship left by HIR, or a Request that HIR issued.
When a fact is not present in a trait, the optimizer MUST assume the conservative answer and move
on.
## 2. Scope
This file specifies the optimizer contract, the pass framework, the analysis set and its
invalidation discipline, the default `-O2` pipeline, request handling, alias analysis, region
barriers, budgets, and the rules that relate optimization level to aggression.
It does not specify VIR itself (see [006 — VIR](006-vir.md)), how VIR was produced (see
[005 — Lowering](005-lowering.md)), or how optimized VIR is emitted (see
[008 — C Backend](008-c-backend.md), [009 — LLVM Backend](009-llvm-backend.md)).
## 3. Definitions and model
| Term | Meaning |
|---|---|
| **VIR pass** | A named transform over a function or a module. |
| **Pass manager** | The driver that schedules passes and owns analyses. |
| **Analysis** | A derived fact about a function or module, cached and invalidated. |
| **Preserved set** | The analyses a pass declares it does not invalidate. |
| **Request** | An attribute-derived suggestion from HIR, with a strength (see [002 — Traits](002-traits.md)). |
| **Action request** | A Request that asks for a transform (`Inline`, `Unroll(factor)`, `LICM`, ...). It yields a `RequestResult` when the responsible pass runs. |
| **Property request** | A proven property the optimizer MAY exploit (`LoopInvariant`, `Range`, `Assume`, ...). It is consumed as a fact and does not by itself produce a `RequestResult`. |
| **DenyReason** | The machine-readable reason a request was declined. |
| **Region barrier** | An opaque region produced by lowering for `complex`/`NoOptimize` code. |
| **Remark** | A per-decision record emitted for diagnostics. |
## 4. Normative requirements
### 4.1 Contract
- **O-1.** The input MUST be well-formed VIR (see [006 — VIR](006-vir.md)) with complete,
epoch-valid traits.
- **O-2.** The output MUST be well-formed VIR that is still in SSA form. The optimizer MUST NOT
break any invariant in [006 — VIR](006-vir.md).
- **O-3.** The optimizer MUST be a strict trait consumer. A transform MUST be enabled by an
attribute, a relationship, or a Request. The optimizer MUST NOT assume a fact that is absent from
traits, and MUST NOT infer one from program structure.
- **O-4.** The optimizer MUST run only at `-O2`, `-O3`, `-Ofast`, and `-Oz`. At `-O0` and `-O1` it
MUST NOT run at all (see §4.9).
- **O-5.** The optimizer MUST be deterministic. For fixed input and `Config`, it MUST produce
byte-identical output regardless of thread count, per [000 — Overview](000-overview.md). Where a
pass has a choice that affects output (for example, the order of two independent transforms), the
pass MUST fix the order deterministically.
- **O-6.** The optimizer MUST preserve the observable behavior of the program. A transform that
would change behavior is legal only when the traits license that change (for example, fast-math
flags).
- **O-7.** The optimizer MUST terminate on every input. Budgets under §4.8 guarantee this.
### 4.2 Pass framework
A pass is a named unit of work. A function pass runs on one function; a module (IPA) pass runs on
the whole module. Passes mutate VIR in place.
```
interface VirPass {
string name();
void run(VirModule m, PassContext ctx); // module pass
}
```
- **O-8.** Every pass MUST expose a stable name. The name MUST be usable as a `-f<name>` /
`-fno-<name>` toggle (see §4.9).
- **O-9.** A pass MUST declare the analyses it preserves before it runs. The pass manager uses the
declaration to invalidate only the analyses the pass might destroy (see §4.3).
- **O-10.** A pass MUST update the def-use graph for every operand it rewrites (see
[006 — VIR](006-vir.md)).
- **O-11.** A pass MUST NOT run when a required analysis cannot be built, when a region barrier
forbids it (see §4.7), or when a request that gates it is denied.
- **O-12.** A pass that reads a trait MUST ensure the trait is epoch-valid. If the epoch advanced
since it read the trait, it MUST re-read rather than reuse the stale value.
### 4.3 Analyses and invalidation
The pass manager owns a cache of analyses. Analyses are built lazily on first request and reused
until invalidated.
| Analysis | Subject |
|---|---|
| `DominatorTree` | Immediate dominators and the dominator tree of each function. |
| `PostDominatorTree` | Immediate post-dominators, used by control-flow transforms. |
| `LoopInfo` | Natural loops, headers, latches, preheaders, and nesting. |
| `AliasInfo` | May-alias answers synthesized from traits (§4.6). |
| `DefUse` | The def-use graph over SSA values. |
| `CallGraph` | Caller/callee edges and call-site metadata. |
| `RangeInfo` | Lightweight value ranges, seeded by `Range`/`Assume` traits. |
- **O-13.** Invalidation MUST be fine-grained. The pass manager MUST invalidate exactly the
analyses that a pass does not declare preserved. It MUST NOT invalidate all analyses as a
default. An invalidated analysis is rebuilt lazily on next request.
- **O-14.** A pass MUST NOT read an analysis after the pass manager has invalidated it; it MUST
request a fresh build.
- **O-15.** An analysis whose result depends on traits MUST be invalidated when a trait epoch
advances in a way that can change that result. The epoch counter is the mechanism (see
[002 — Traits](002-traits.md)).
- **O-16.** `RangeInfo` MUST be lightweight. It MUST be seeded by `Range` and `Assume` traits and
MUST NOT implement a full scalar-evolution analysis. A pass MUST NOT expect exact ranges.
- **O-17.** `AliasInfo` MUST NOT guess. Its answers are defined in §4.6.
### 4.4 Default `-O2` pipeline
At `-O2` the pass manager runs the following canonical pipeline. The order is fixed; the pipeline
is not reordered adaptively by requests. Requests gate and parameterize passes, they do not resequence
them.
| Stage | Passes |
|---|---|
| 1. Canonicalize | `mem2reg`, `SROA`, `instcombine`, `simplifyCFG`, `early-CSE`, `DCE` |
| 2. Scalar | `GVN`, `SCCP`, `LICM`, `indvars`, `reassociation` |
| 3. IPA | inliner (gated by `Inline`/`AlwaysInline`/`NoInline`/`ColdCall`/`LikelyCall`), global `DCE`, `IPSCCP`, function-attribute propagation |
| 4. Loops | `unroll`/`interleave`/`peel`/`rotate`/`unswitch`/`distribute`/`fuse`/`jam`, each gated by its Request |
| 5. Vectorize | loop and SLP vectorization, gated by `Vectorize`, `LoopInvariant`, `Range`, `NoAlias`, `Restrict` |
| 6. Memory | alias-driven `DSE`, GEP simplification, load widening |
| 7. Control flow | block layout (`HotPath`/`ColdPath`), tail-call formation (`TailCall`/`MustTail`/`NoTail`), unreachable pruning, jump threading |
| 8. Codegen prep | backend-directed canonicalization; output remains SSA |
- **O-18.** At `-O2` the optimizer MUST run all eight stages in the order shown, subject to pass
toggles, region barriers, and request denials.
- **O-19.** Stage 3 (IPA) MUST be driven by the call-edge requests. A callee carrying `AlwaysInline`
MUST be inlined where legal. A callee carrying `NoInline` MUST NOT be inlined.
- **O-20.** Stage 4 loop transforms MUST each be gated by their own Request. For example, `unroll`
runs only under an `Unroll(factor)` request, and `LICM` hoists only under an `LICM` request or
when a `LoopInvariant` property licenses it.
- **O-21.** Stage 5 vectorization MUST be gated by `Vectorize` and MUST consult `NoAlias`,
`Restrict`, `Range`, and `LoopInvariant` before reordering memory operations.
- **O-22.** Stage 8 MUST NOT perform out-of-SSA translation and MUST NOT lower `Phi`
instructions. Out-of-SSA is a backend concern (see [008 — C Backend](008-c-backend.md)).
### 4.5 Request handling
Every action request that the responsible pass evaluates MUST produce a `RequestResult`.
```
struct RequestResult {
Request req;
bool accepted;
DenyReason reason;
string note; // one-line human-readable explanation
}
```
A denial MUST carry a `DenyReason` and a one-line `note`.
| DenyReason | Meaning |
|---|---|
| `CostModel` | Profitable only under a different cost model. |
| `Illegality` | The transform would be incorrect. |
| `AlreadyDone` | No-op; the goal already holds. |
| `Unsupported` | The optimizer does not implement it yet. |
| `ContradictsTrait` | Conflicts with a stronger fact. |
| `TooLarge` | Exceeds a size or count budget. |
| `NoTargetSupport` | The backend or target cannot express the result. |
- **O-23.** When a pass evaluates a request, it MUST record a `RequestResult` with a `DenyReason`
and a one-line `note` on denial. It MUST NOT deny silently.
- **O-24.** A `Soft` request MAY be denied for any `DenyReason`. A denied `Soft` request MUST NOT
produce an error; it SHOULD produce a remark.
- **O-25.** A `Strong` request denied with `Illegality` or `ContradictsTrait` MUST raise an
internal-compiler-error diagnostic (see [011 — Diagnostics](011-diagnostics.md)). A `Strong`
request should be legally satisfiable; failing to satisfy it indicates a compiler inconsistency.
This rule applies only when the target is capable of the transform; a capability gap MUST instead
be reported as `NoTargetSupport`.
- **O-26.** A `Strong` request denied with `Unsupported` or `NoTargetSupport` MUST produce a warning,
not an error, unless the target genuinely cannot express semantics the request requires for
correctness or ABI. Such a semantic-capability failure (for example, `MustTail` on GCC; see
[008 — C Backend](008-c-backend.md)) MUST be a hard error.
- **O-27.** A `Strong` request denied for any other reason (`CostModel`, `AlreadyDone`, `TooLarge`)
MUST NOT be a hard error and SHOULD produce a warning.
- **O-28.** The optimizer MUST NOT drop a request without a `RequestResult`, and MUST NOT flip a
request to accepted when it did not perform the transform.
- **O-29.** `LoopInvariant` is a proven property and `LICM` is an explicit hoist request. Both MUST
be retained and handled distinctly: a pass MAY exploit `LoopInvariant` without an `LICM` request,
and a pass MUST attempt the hoist when `LICM` is requested.
- **O-30.** `NoRead`/`NoWrite` are access-edge primitives; `ReadOnly`/`WriteOnly` are region-level
facts. A pass MUST respect the scope difference when it consumes either form.
### 4.6 Alias analysis
`AliasInfo` answers may-alias queries. It is synthesized from traits and provenance only.
- **O-31.** `AliasInfo` MUST derive its answers from `NoAlias`, `Restrict`, `NoCapture`,
`Dereferenceable`, `ReadOnly`, and `WriteOnly` traits, together with the provenance relationships
HIR left on the values.
- **O-32.** When no trait establishes a relationship between two memory references, `AliasInfo`
MUST answer `MayAlias`. It MUST NOT infer `NoAlias` from program structure, from the fact that
two allocas are distinct, or from any fact not present in traits.
- **O-33.** A pass MAY use a `NoAlias` answer only for the exact pair and scope the trait licenses.
It MUST NOT generalize an answer to other pairs.
- **O-34.** A region barrier is an alias barrier: no reference inside one is known not to alias a
reference outside it unless a trait says so (see §4.7).
### 4.7 Region barriers
- **O-35.** Lowering marks `complex`/`NoOptimize` code with a region-level barrier (see
[005 — Lowering](005-lowering.md)). VIR passes MUST treat the region as opaque.
- **O-36.** A pass MUST NOT rewrite, reorder, delete, hoist, or sink an instruction across a region
barrier. It MUST NOT propagate a value or a fact into or out of the region.
- **O-37.** Memory and alias analyses MUST model a barrier region as a single opaque,
side-effecting operation.
- **O-38.** A pass that cannot make progress because of a barrier MUST skip the affected code
without error and MUST NOT force a transform through it.
### 4.8 Budgets and termination
- **O-39.** The optimizer MUST honor `vir.max_iterations` (per-pass or per-analysis iteration cap)
and `vir.max_pipeline_rounds` (number of times the pipeline may re-run). Both MUST be
configurable through the API and CLI and MUST have finite defaults.
- **O-40.** When a budget is exhausted, the optimizer MUST stop and return the current, well-formed
VIR. It MUST NOT loop indefinitely and MUST NOT return malformed VIR.
- **O-41.** Budget exhaustion SHOULD be reported as a warning and MAY be reported as a remark. It
MUST NOT be an internal compiler error.
The default budgets are:
| Budget | Default |
|---|---|
| `vir.max_iterations` | `100000` |
| `vir.max_pipeline_rounds` | `4` |
An implementation MUST use these defaults unless the caller overrides them through the API or CLI.
The values are finite (O-39) and are chosen at the same order of magnitude as the HIR budgets (see
[004 — HIR Optimizer](004-hir-optimizer.md)); `vir.max_pipeline_rounds` bounds how many times the
eight-stage pipeline of §4.4 may be re-run.
### 4.9 Optimization levels and pass toggles
| Level | VIR optimization | Behavior |
|---|---|---|
| `-O0` | off | None. Only lowering's own translation. |
| `-O1` | off | None. HIR folds only. |
| `-O2` | on | The eight-stage default pipeline of §4.4. |
| `-O3` | on | The `-O2` pipeline with raised aggression (see below). |
| `-Ofast` | on | `-O3` plus fast-math and `-march=native`. |
| `-Oz` | on | The `-O2` pipeline tuned for size. |
- **O-42.** The optimizer MUST be entirely disabled at `-O0` and `-O1`. In particular, `mem2reg`,
`SROA`, and `DCE` MUST NOT run at those levels.
- **O-43.** `-O3` MUST raise aggression relative to `-O2`: higher inlining thresholds, larger
unroll/interleave factors, and wider vectorization factors, all still gated by their requests.
- **O-44.** `-Ofast` MUST enable fast-math flags and `-march=native`. Fast-math flags MUST NOT be
set at any lower level unless the caller explicitly configured them.
- **O-45.** `-Oz` MUST be size-first: most unrolling and vectorization MUST be disabled, inlining
limited to very small callees, while canonicalization and dead-code elimination remain enabled.
- **O-46.** Every pass MUST be individually controllable through a `-f<name>` / `-fno-<name>`
toggle. A disabled pass MUST be omitted from the pipeline rather than run and then reversed.
### 4.10 Diagnostics and remarks
- **O-47.** A pass MUST emit its decisions through the diagnostics service; it MUST NOT print
directly (see [011 — Diagnostics](011-diagnostics.md)).
- **O-48.** Remarks MUST be off by default and MUST be enabled by `--remarks`; they MUST be
enabled automatically at `-O3`.
- **O-49.** A remark that reports a request decision SHOULD include the request, the subject, the
outcome, and the one-line note. Example:
`VIR: denied Inline on @foo, CostModel (callee 2.4x size budget)`
## 5. Invariants
The output of the optimizer MUST satisfy all of the following.
1. **Well-formed VIR.** All invariants in [006 — VIR](006-vir.md) hold after every pass.
2. **Still SSA.** The output is in SSA form; `Phi` instructions remain; no out-of-SSA lowering
happens here.
3. **Trait fidelity.** No transform may invent a fact. Every fact used is traceable to a trait.
4. **Barrier respect.** No transform crosses a region barrier.
5. **Request accountability.** Every evaluated action request has a `RequestResult`.
6. **Termination.** The pipeline terminates within its configured budgets.
7. **Determinism.** Fixed input and `Config` produce byte-identical output.
## 6. Example
Given a callee annotated `AlwaysInline` and a call site annotated `NoInline`:
- The call-edge requests conflict. HIR resolves strong conflicts before lowering (see
[002 — Traits](002-traits.md)), so lowering should not present both to the optimizer. If a
contradiction still reaches the optimizer, the inliner denies the `Strong` request with
`ContradictsTrait`, which is an internal compiler error under O-25.
Given a callee with a `ColdCall` request and a size estimate above the inline threshold:
- The inliner denies `Inline` with `CostModel` and records a `RequestResult` with the note
`callee 3.1x size budget`. No error is raised; a remark is emitted when remarks are enabled.
## 7. Cross-references
- [000 — Overview](000-overview.md): determinism guarantee and conformance.
- [001 — Architecture](001-architecture.md): `vct.vir.opt` module boundary.
- [002 — Traits](002-traits.md): requests, strengths, conflict rules, epochs.
- [004 — HIR Optimizer](004-hir-optimizer.md): who produces the traits the optimizer consumes.
- [005 — Lowering](005-lowering.md): who produces the VIR and the region barriers.
- [006 — VIR](006-vir.md): the IR and its invariants.
- [008 — C Backend](008-c-backend.md), [009 — LLVM Backend](009-llvm-backend.md): out-of-SSA on the C path and 1:1 mapping on the LLVM path.
- [011 — Diagnostics](011-diagnostics.md): internal-compiler-error policy and remarks.
- [012 — Testing](012-testing.md): verifier and differential testing.
+280
View File
@@ -0,0 +1,280 @@
# 008 — C 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 C backend (`vct.backend.c`) translates optimized VIR into C17 source. It is the
default backend: the driver hands each translation unit to a system C compiler that
produces an object file. The backend's target is **compiler-friendly C**, meaning the
emitted text is shaped so that GCC and Clang recognize it and optimize it well. libVCT
does not try to pre-optimize away everything the C compiler can do itself.
## 2. Scope
This file specifies the backend's contract and the required shape of its output:
translation-unit granularity, out-of-SSA lowering, control-flow reconstruction,
trait-to-C mappings, type and layout emission, name mangling, intrinsic selection,
debug line mapping, and ABI guarantees. The VIR input is specified in
[006 — VIR](006-vir.md); the pipeline that feeds the backend is specified in
[007 — VIR Optimizer](007-vir-optimizer.md) and [010 — Driver & CLI](010-driver-cli.md).
## 3. Definitions
| Term | Meaning |
|---|---|
| TU | Translation unit: one C17 source file. |
| Out-of-SSA | Conversion of SSA values into C variables with explicit assignments on every control-flow path. |
| Critical edge | A CFG edge whose source has multiple successors and whose destination has multiple predecessors. |
| Reducible CFG | A control-flow graph in which every cycle has exactly one loop header. |
| Loop header | The unique block by which control enters a loop. |
| `layout` attribute | The attribute that pins an aggregate's ABI: field offsets, total size, and alignment. |
| Opaque barrier call | A `noinline` function whose body the C compiler cannot inspect, used to stop optimization across a region. |
## 4. Model
The backend is a pure translation stage. It consumes optimized VIR that satisfies the
SSA invariants of [006 — VIR](006-vir.md) and emits C17. It performs no optimization
and no discovery; every fact it uses is read from traits (see
[002 — Traits](002-traits.md)).
Emission is **per function**. A VIR module with N functions produces N independent C
translation units. Each TU carries one function definition plus the declarations it
needs. That independence is what lets emission run in parallel, and it is what lets
the driver concatenate the results in a deterministic order.
## 5. Normative requirements
### 5.1 Translation-unit structure
**R1.** The backend MUST emit exactly one C17 translation unit per VIR function.
**R2.** Emitting one function's TU MUST NOT depend on emitting any other function's TU.
Two functions emitted in any order MUST yield identical per-function text.
**R3.** Each TU MUST be emitted in two passes: first all forward declarations, then all
definitions. Every declaration MUST precede the definition that uses it.
**R4.** The backend MUST assign each function a stable emission index derived only from
module content, so the driver can order TUs deterministically (see
[010 — Driver & CLI](010-driver-cli.md)).
**R5.** For a fixed module and `Config`, the concatenation of per-function TUs MUST be
byte-identical across runs and independent of thread count (see
[000 — Overview](000-overview.md)).
### 5.2 Out-of-SSA
**R6.** The backend MUST convert SSA to C using the algorithm of Boissinot, Brisk,
Caron, and Ferrand, "Revisiting Out-of-SSA Translation for Correctness, Code Quality,
and Efficiency" (CGO 2009), or an algorithm observably equivalent to it. The
conversion MUST handle the lost-copy and swap problems.
**R7.** The backend MUST split all critical edges **before** running out-of-SSA
conversion.
**R8.** After conversion, every SSA value MUST map to exactly one C variable or
expression, and every `Phi` MUST lower to an assignment on each incoming edge, or to a
semantically equivalent construct.
### 5.3 Control-flow reconstruction
**R9.** For reducible CFGs, the backend MUST reconstruct structured C control flow using
`if`/`else`, `while`, `for`, `do`, and `switch` wherever the shape is recognized.
**R10.** For irreducible CFGs, or for regions where no structured shape is recognized,
the backend MUST fall back to `goto` and labels. The fallback MUST preserve semantics.
This is a correctness requirement, not a quality preference.
**R11.** Structured reconstruction SHOULD be preferred over `goto` fallback whenever a
recognized shape exists, because it improves downstream optimization quality.
### 5.4 Compiler-friendly C
**R12.** The backend MUST map traits to C qualifiers, hints, and pragmas as specified
in Table 1. When a trait is absent, the corresponding construct MUST NOT be emitted.
**Table 1. Trait to C mapping**
| Trait | C construct | Notes |
|---|---|---|
| `Restrict`, `NoAlias` | `restrict` | On pointer parameters and local pointers. |
| `ReadOnly`, `NoWrite` | `const` | On the pointee type of a pointer or reference parameter. |
| `NoWrite` | `__attribute__((pure))` | Function level; reads allowed, writes not allowed. |
| `NoRead` + `NoWrite` | `__attribute__((const))` | Function level; no memory access. |
| `NoRead` | No portable C17 construct | No GCC/Clang attribute means "does not read"; omit it. |
| `NonNull` | `nonnull` attribute or `__builtin_unreachable` guard | |
| `NoReturn` | `_Noreturn` | |
| `ColdPath` | `__attribute__((cold))` | |
| `HotPath` | `__attribute__((hot))` | |
| `LikelyCall` | `__builtin_expect(cond, 1)` | On the branch condition. |
| `UnlikelyCall`, `ColdCall` | `__builtin_expect(cond, 0)` | On the branch condition. |
| `AlwaysInline` | `static inline` plus `always_inline` attribute | |
| `NoInline` | `__attribute__((noinline))` | |
| `Align(n)` | `_Alignas(n)` and/or `__builtin_assume_aligned` | |
| `Assume(pred)` | `__builtin_assume(pred)` (Clang) | |
| `Unreachable` | `__builtin_unreachable()` | |
| `Vectorize` | `#pragma GCC ivdep` or `#pragma clang loop vectorize(enable)` | Selected by probed compiler dialect. |
| `NoOptimize`, `complex` | Optimization barrier, see R14 | |
**R13.** The backend MUST select between GCC and Clang spellings of pragmas and
builtins from the compiler dialect probed once by the driver (see
[010 — Driver & CLI](010-driver-cli.md)). When the probed compiler does not support a
construct, the backend MUST omit it rather than emit something that fails to compile,
unless the corresponding request is `Strong`, in which case the denial rules of
[007 — VIR Optimizer](007-vir-optimizer.md) apply.
**R14 (Optimization barrier).** For a region marked `complex` or carrying a `NoOptimize`
request, the backend MUST emit a compiler barrier: a memory fence
(`__asm__ __volatile__("" ::: "memory")` on GCC and Clang) followed by a call to an
opaque function marked `noinline`. The call MUST be a real call that the C compiler
cannot see through.
### 5.5 Types and layout
**R15.** The backend MUST map VIR types to C types as specified in Table 2.
**Table 2. VIR type to C type**
| VIR type | C type |
|---|---|
| Signed integer with a C17 fixed-width type | `intN_t` |
| Unsigned integer with a C17 fixed-width type | `uintN_t` |
| `f32` | `float` |
| `f64` | `double` |
| Pointer `T*` | `T *` |
| Struct | `struct` |
| Union | `union` |
| Array `[N x T]` | `T[N]` |
| Function type | Function declarator or function pointer |
| `void` | `void` |
**R16.** An integer width with no C17 fixed-width type (any width other than 8, 16, 32,
or 64 bits) MUST be diagnosed as unsupported on the C path unless the probed compiler
provides an N-bit integer type, in which case the backend MAY use it.
**R17.** Aggregate GEPs MUST use field indices, never byte offsets. The backend MUST
let the C compiler choose struct layout unless the `layout` attribute pins it.
**R18.** When the `layout` attribute pins an aggregate's ABI, the backend MUST emit explicit
padding members to force the specified offsets and total size, and MUST emit
`_Static_assert` checks on `sizeof` and on `offsetof` for every pinned field.
### 5.6 Naming
**R19.** Exported names MUST be preserved exactly as the frontend supplied them.
Internal names MUST be sanitized into valid C identifiers and uniquified
deterministically.
**R20.** Sanitization and uniquification MUST NOT produce an identifier that collides
with a C keyword, a standard library identifier, or a compiler-reserved identifier. The
reserved-word and collision prefix policy of Table 3 MUST be applied.
**Table 3. Reserved identifier prefix policy for internal names**
| Category | Action |
|---|---|
| C keywords (`int`, `while`, ...) | Prefix with `vct_` |
| C17 standard library names (`memcpy`, `printf`, ...) | Prefix with `vct_` |
| Implementation-reserved names (`_x`, `__x`, `_[A-Z]...`) | Prefix with `vct_` |
| Compiler builtins (`__builtin_*`) | Prefix with `vct_` |
| Post-mangle collisions | Append a stable numeric suffix |
**R21.** The disambiguator used for uniquification MUST be derived only from
deterministic module content. It MUST NOT depend on allocation addresses, hash-table
iteration order, or thread scheduling.
### 5.7 Intrinsics
**R22.** The backend MUST lower the VIR intrinsics of [006 — VIR](006-vir.md) to the matching
`__builtin_*` function where GCC and Clang provide one.
**R23.** For intrinsics with no portable builtin, the backend MUST emit a call to a
portable helper with a stable name. A helper definition MUST be emitted per function,
deterministically, with internal linkage, into every TU whose function references it, so
that per-function text remains a pure function of that function's VIR (R2, I2). A helper
definition MUST NOT be emitted once per module or into a shared, module-level TU.
### 5.8 Debug mapping
**R24.** When debug mapping is enabled, the backend MUST emit `#line` directives that map
emitted C lines to frontend source positions, using the source map registered with the
diagnostics service (see [011 — Diagnostics](011-diagnostics.md)).
**R25.** Debug mapping MUST be flag-gated: enabled when debug mode is on, disabled in
release. When disabled, the backend MUST NOT emit any `#line` directive.
### 5.9 ABI
**R26.** Exported functions MUST match the platform C ABI. Internal functions MUST be
emitted `static`.
**R27.** Struct-by-value arguments and returns MUST follow the C compiler's ABI,
including any constraint introduced by the `layout` attribute.
**R28.** A `MustTail` request MUST be honored with `[[clang::musttail]]` when the probed
compiler is Clang. When the probed compiler is GCC, a `MustTail` request MUST be a hard
error (see [011 — Diagnostics](011-diagnostics.md)). This is the semantic-capability
exception of O-26 in [007 — VIR Optimizer](007-vir-optimizer.md): GCC cannot express the
required tail-call guarantee, so the `Strong` request is fatal rather than a warning.
**R29.** The backend MUST NOT emit implementation-defined constructs beyond those
explicitly permitted here. In particular it MUST NOT emit byte-offset aggregate
accesses, alignment-violating pointer casts, or identifiers outside the reserved prefix
policy.
## 6. Invariants
- **I1.** Each emitted TU is a valid C17 translation unit given the shared declarations.
- **I2.** Per-function text, including any helper definitions it carries, is a pure function of
that function's VIR and the traits it references.
- **I3.** Concatenated output is byte-identical for a fixed input and `Config`, at any
thread count.
- **I4.** Every definition is preceded by a declaration.
- **I5.** No two emitted identifiers collide after mangling.
- **I6.** Every layout-pinned aggregate satisfies its static assertions at compile time.
- **I7.** Every structured-to-`goto` fallback preserves the CFG's reachability and
semantics.
## 7. Examples
A `Phi` merging `%a` and `%b` on the true and false edges of a conditional branch lowers
to a temporary assigned on each edge before out-of-SSA bookkeeping:
```c
int vct_phi0; /* out-of-SSA temporary for a Phi */
if (cond) { vct_phi0 = a; } else { vct_phi0 = b; }
return vct_phi0;
```
A `NoOptimize` region emits a fence followed by an opaque call:
```c
__asm__ __volatile__("" ::: "memory");
vct_opaque_barrier(x);
```
A layout-pinned struct emits padding plus compile-time checks:
```c
struct vct_pair {
int32_t lo;
int32_t vct_pad0;
int64_t hi;
};
_Static_assert(sizeof(struct vct_pair) == 16, "layout");
_Static_assert(offsetof(struct vct_pair, hi) == 8, "layout");
```
## 8. Cross-references
- [000 — Overview](000-overview.md) for determinism and conformance.
- [001 — Architecture](001-architecture.md) for module boundaries.
- [002 — Traits](002-traits.md) for the trait vocabulary consumed here.
- [006 — VIR](006-vir.md) for the input IR and its invariants.
- [007 — VIR Optimizer](007-vir-optimizer.md) for the pass pipeline.
- [009 — LLVM Backend](009-llvm-backend.md) for the alternate backend.
- [010 — Driver & CLI](010-driver-cli.md) for compiler probing and TU concatenation.
- [011 — Diagnostics](011-diagnostics.md) for error reporting and `#line`.
+267
View File
@@ -0,0 +1,267 @@
# 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](006-vir.md); the optimizer that runs before this backend is specified in
[007 — VIR Optimizer](007-vir-optimizer.md). The C backend is specified in
[008 — C Backend](008-c-backend.md).
## 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](008-c-backend.md)).
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](000-overview.md)).
### 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](008-c-backend.md)).
### 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](008-c-backend.md): 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](002-traits.md)); 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](006-vir.md) 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](010-driver-cli.md)).
**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`:
```llvm
; 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:
```llvm
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](000-overview.md) for determinism and conformance.
- [001 — Architecture](001-architecture.md) for module boundaries.
- [002 — Traits](002-traits.md) for the trait vocabulary consumed here.
- [006 — VIR](006-vir.md) for the input IR and its invariants.
- [007 — VIR Optimizer](007-vir-optimizer.md) for the pass pipeline.
- [008 — C Backend](008-c-backend.md) for the default C path.
- [010 — Driver & CLI](010-driver-cli.md) for the `-target` flag and mode selection.
- [011 — Diagnostics](011-diagnostics.md) for error reporting.
+223
View File
@@ -0,0 +1,223 @@
# 010 — Driver & CLI
- **Status:** Draft
- **Normative language:** `MUST`, `MUST NOT`, `SHOULD`, `SHOULD NOT`, and `MAY` are to
be interpreted as described in RFC 2119.
## 1. Purpose
libVCT splits orchestration across two layers. The driver (`vct.driver`) is the
library-level orchestrator: it takes emitted C translation units, invokes the system C
compiler, and collects object files. The CLI (`vct.cli`) is a thin wrapper that parses
command-line flags into a `Config`, drives the whole pipeline, formats diagnostics, and
sets exit codes. The CLI holds no optimizer state and adds no compilation logic of its
own.
## 2. Scope
This file specifies the driver/CLI role split, the pipeline order, the full flag
surface, the exact C compiler invocation, compiler discovery and probing, error
handling, determinism, `-mangled` mode, and object-file emission and merging. The
compiler-facing backends are specified in [008 — C Backend](008-c-backend.md) and
[009 — LLVM Backend](009-llvm-backend.md). Diagnostics, remarks, and exit codes are
specified in [011 — Diagnostics](011-diagnostics.md).
## 3. Definitions
| Term | Meaning |
|---|---|
| `Config` | One struct holding opt level, target, budgets, pass toggles, warning policy, compiler selection, and mode. |
| TU | Translation unit: one emitted C file. |
| `cc` | The discovered system C compiler. |
| APE | Actually Portable Executable, the output of cosmopolitan tooling. |
| Fat LTO object | An object that carries both native machine code and LTO bitcode. |
| Response file | A file listing arguments for a linker or compiler, referenced by `@file`. |
## 4. Model
`vct.cli` is GC-allocated and cold-path only. It converts `argv` into a `Config`, runs
the pipeline, forwards every diagnostic to the configured consumer, and returns an exit
code. `vct.driver` owns process invocation. Any embedder that has already constructed a
`Config` can call the driver directly and skip the CLI entirely.
The driver also owns the `-llvm` path, which returns textual LLVM IR and never touches
`cc`, and the `-mangled` path, which switches to the cosmopolitan toolchain.
## 5. Normative requirements
### 5.1 Role split
**R1.** `vct.cli` MUST be a thin wrapper. It MUST NOT contain optimization, lowering, or
backend logic, and it MUST NOT hold optimizer state.
**R2.** `vct.cli` MUST be a pure function from `argv` to `Config` plus pipeline
invocation. The library entry point MUST accept a `Config`; programmatic callers MUST be
able to bypass the CLI.
**R3.** `vct.driver` MUST own all invocation of the system C compiler.
**R4.** `vct.cli` MUST be GC-allocated and MUST be usable on the cold path only.
### 5.2 Pipeline
**R5.** The default pipeline order MUST be: read input, then HIR optimization if enabled,
then lowering, then VIR optimization if enabled, then the C backend, then the driver.
```
read input
-> (HIR opt if enabled)
-> lower
-> (VIR opt if enabled)
-> backend.c -> driver -> .o
```
**R6.** `-S` MUST stop after C emission and MUST NOT invoke `cc`.
**R7.** `-emit-llvm` and `-llvm` MUST stop after LLVM IR emission and MUST NOT invoke
`cc`.
### 5.3 Flag surface
**R8.** The CLI MUST accept the flags listed in Table 1.
**Table 1. v1 flag surface**
| Group | Flags | Meaning |
|---|---|---|
| Optimize | `-O0` `-O1` `-O2` `-O3` `-Ofast` `-Oz` | Optimization level. |
| Target | `-march=<arch>`, `-mcpu=<cpu>`, `-target <triple>` | Target selection; `-march`/`-mcpu` forward to `cc` as cflags. |
| Modes | `-S`, `-emit-llvm`, `-llvm`, `-mangled` | Stop point or alternate mode. |
| Toolchain | `-cc=<gcc\|clang>`, `-cflags="..."`, `-j<N>`, `-save-temps` | Compiler selection, extra flags, parallelism, temp retention. |
| LTO | `-flto[=full\|thin]`, `-ffat-lto-objects` | Link-time optimization control. |
| Debug | `-g`, `--dump-hir`, `--dump-vir`, `--verify`, `--time-passes`, `--stats` | Debug output and instrumentation. |
| Output | `-o <path>` | Output path. |
| Warnings | `-Wall`, `-Werror`, `-Wextra`, `-Wno-error[=<category>]`, `-w` | Warning policy. |
| Diagnostics | `--remarks`, `--diagnostics=json` | Remark and diagnostic channel selection. |
| Pass toggles | `-f<pass>`, `-fno-<pass>` | Enable or disable an individual pass. |
**R9.** `-Wall`, `-Werror`, and `-Wextra` MUST be on by default. A bare `-Wno-error` MUST
disable every promotion, `-Wno-error=<category>` MUST disable the promotion of that
category, and `-w` MUST silence warnings.
**R10.** `-Werror` MUST apply to the C source that libVCT generates, not only to
frontend input.
**R11.** Every pass MUST be exposed as both `-f<pass>` and `-fno-<pass>`.
### 5.4 C compiler invocation
**R12.** For each emitted TU, the driver MUST invoke the compiler as:
```
cc -std=c17 -c <tu.c> -o <tu.o> <cflags> <march> -Wall -Werror -Wextra <g> <flto>
```
where `<g>` is `-g` when debug is enabled, and `<flto>` is the LTO flag when LTO is
requested.
**R13.** The driver MUST run one compiler process per TU, with concurrency bounded by
`-j`.
**R14.** The driver MUST discover the compiler via the `$CC` environment variable first,
then by searching `PATH`, unless `-cc=<gcc|clang>` forces a selection.
**R15.** The driver MUST probe the discovered compiler's version and dialect exactly
once per run. The probed dialect MUST drive the backend's choice of pragmas and builtins
(see [008 — C Backend](008-c-backend.md)).
**R16.** The driver MUST NOT pass `-march`/`-mcpu` values to `cc` that the probed
compiler does not accept; it MUST diagnose an unsupported target value.
### 5.5 Error handling
**R17.** The driver MUST capture the compiler's stderr.
**R18.** The driver MUST use the `#line` directives emitted by the C backend (see
[008 — C Backend](008-c-backend.md)) to map C compiler errors back to VIR and frontend
source positions, then re-emit them through the diagnostics service (see
[011 — Diagnostics](011-diagnostics.md)).
**R19.** A compiler failure MUST produce a diagnostic and MUST cause a nonzero exit.
**R20.** The driver MUST NOT discard compiler diagnostics silently.
### 5.6 Determinism
**R21.** The driver MUST order TUs and flags deterministically. For a fixed `Config` and
input, the sequence of compiler invocations and their argument order MUST be identical
across runs and independent of thread count.
**R22.** The driver MUST place temporary files under a GC-managed temporary directory.
**R23.** The temporary directory MUST be cleaned up after the run unless `-save-temps`
is given.
### 5.7 `-mangled` mode
**R24.** `-mangled` MUST switch the toolchain to `cosmocc` and the cosmopolitan libc.
**R25.** `-mangled` MUST produce an Actually Portable Executable.
**R26.** `-mangled` is the one mode in which libVCT links. In every other mode libVCT
produces object files and leaves linking to the caller (see
[000 — Overview](000-overview.md)).
### 5.8 Object emission and merging
**R27.** In the default path, the driver MUST produce object files only. It MUST NOT link
a final executable.
**R28.** When a single `-o` path is requested and the module produced multiple
per-function objects, the driver MUST merge them with a relocatable link
(`ld -r -o <out> <tu>.o ...`).
**R29.** The driver MUST emit, for the caller, a recommended link line or response file
listing the produced objects and any needed libraries.
## 6. Invariants
- **I1.** A fixed `Config` and input produce byte-identical outputs and an identical
sequence of compiler invocations.
- **I2.** `vct.cli` holds no optimizer state.
- **I3.** The `-llvm` path never spawns `cc`.
- **I4.** The temporary directory is removed unless `-save-temps` is set.
- **I5.** Flag assembly order is stable.
- **I6.** Every captured compiler error is re-emitted through the diagnostics service.
- **I7.** `-mangled` is the only mode that links.
## 7. Examples
Default compile of a single source to an object:
```
vct -O2 -march=native -cc=clang -j8 -o out.o input.vir
```
Stop after emitting C, without invoking `cc`:
```
vct -O2 -S -o out.c input.vir
```
Emit textual LLVM IR instead of C:
```
vct -O3 -emit-llvm -target x86_64-unknown-linux-gnu -o out.ll input.vir
```
Produce a portable executable:
```
vct -O2 -mangled -o app.com input.vir
```
## 8. Cross-references
- [000 — Overview](000-overview.md) for determinism and the no-linking rule.
- [001 — Architecture](001-architecture.md) for module boundaries.
- [002 — Traits](002-traits.md) for the trait vocabulary.
- [007 — VIR Optimizer](007-vir-optimizer.md) for pass toggles and the pipeline.
- [008 — C Backend](008-c-backend.md) for `#line` and compiler dialect use.
- [009 — LLVM Backend](009-llvm-backend.md) for `-target` and the `-llvm` path.
- [011 — Diagnostics](011-diagnostics.md) for diagnostics, remarks, and exit codes.
- [013 — Build & Packaging](013-build-packaging.md) for `Config` and the C API.
+246
View File
@@ -0,0 +1,246 @@
# 011 — Diagnostics
- **Status:** Draft
- **Normative language:** `MUST`, `MUST NOT`, `SHOULD`, `SHOULD NOT`, and `MAY` are to
be interpreted as described in RFC 2119.
## 1. Purpose
libVCT has a single central diagnostics service (`vct.diag`). Every module emits
diagnostics through it; no module prints directly to a stream. This gives embedders one
uniform surface for errors, warnings, notes, optimizer remarks, and internal compiler
errors, and it lets the CLI present them in several formats without every module knowing
which format is active.
## 2. Scope
This file specifies the diagnostics service, the `Diagnostic` structure and its
severity and code vocabulary, source maps, output channels, optimizer remarks, the
internal-compiler-error policy, and process exit codes. The pipeline that produces these
diagnostics is specified in [010 — Driver & CLI](010-driver-cli.md); the backends that
consume source maps are specified in [008 — C Backend](008-c-backend.md) and
[009 — LLVM Backend](009-llvm-backend.md).
## 3. Definitions
| Term | Meaning |
|---|---|
| Diagnostic | One emitted message with severity, code, span, and optional notes and suggestions. |
| Severity | Error, Warning, Note, Remark, or Ice. |
| `DiagCode` | A stable, namespaced identifier such as `VCT1002`. |
| Span | A source range: file, line, column, and length. |
| Source map | The association of HIR nodes with frontend source locations, resolved for VIR through the lowering map. |
| Consumer | A `DiagnosticConsumer` callback that receives emitted diagnostics. |
| ICE | Internal compiler error. |
## 4. Model
The service is **emit-only** from the perspective of compilation modules. A module
builds a `Diagnostic` and hands it to the service. The service forwards it to the
configured consumer. Fatal conditions (an error diagnostic, or an ICE) stop the
pipeline at the next safe boundary; they never let compilation continue on invalid
state.
The service is GC-allocated, consistent with the cold path rule in
[001 — Architecture](001-architecture.md).
```d
struct Diagnostic {
Severity severity; // Error | Warning | Note | Remark | Ice
DiagCode code; // stable namespaced, e.g. VCT1002
string message;
Span primary;
Span[] notes;
Suggestion[] suggestions;
}
```
## 5. Normative requirements
### 5.1 Central service
**R1.** Every module MUST emit diagnostics through the central service. No module MUST
print to standard output or standard error directly.
**R2.** The service MUST be GC-allocated.
**R3.** The service MUST forward each emitted diagnostic to the configured consumer in
emission order.
**R4.** An `Error` or `Ice` diagnostic MUST prevent further compilation on the invalid
path. The pipeline MUST stop at the next safe boundary after such a diagnostic.
### 5.2 Diagnostic structure
**R5.** Every diagnostic MUST carry a `Severity` and a `DiagCode`. A diagnostic with no
code MUST NOT be emitted.
**R6.** `Severity` MUST be one of the values in Table 1.
**Table 1. Severity levels**
| Severity | Meaning |
|---|---|
| Error | Compilation cannot proceed correctly. |
| Warning | Suspicious but continuable. |
| Note | Supporting context attached to another diagnostic. |
| Remark | An optimizer decision record. |
| Ice | Internal compiler error. |
**R7.** A `DiagCode` MUST be a stable, namespaced identifier in the form `VCT` followed
by digits. Once published, a code's meaning MUST NOT change and the code MUST NOT be
renumbered.
**R8.** A diagnostic MUST carry a primary `Span` whenever a source location is known, and it MAY
carry additional note spans and suggestions. When no location can be resolved, the diagnostic MUST
carry no location instead (see R12).
**R9.** A `Note` diagnostic MUST be attached to a parent diagnostic rather than emitted
as a standalone top-level message.
### 5.3 Source maps
**R10.** The frontend MUST be able to register a `SourceLocation` (file, line, column,
length) against any HIR node.
**R11.** For VIR entities, the service MUST resolve source locations through the
lowering map that associates HIR nodes with the VIR entities they became (see
[005 — Lowering](005-lowering.md)).
**R12.** When a location cannot be resolved, the service MUST degrade gracefully to a
diagnostic with no location. It MUST NOT fabricate a location.
**R13.** The C backend MUST emit `#line` directives from the registered source map (see
[008 — C Backend](008-c-backend.md)), and the driver MUST re-map compiler errors through
it (see [010 — Driver & CLI](010-driver-cli.md)).
### 5.4 Channels
**R14.** The service MUST expose a single canonical `DiagnosticConsumer` callback for
embedders.
**R15.** The CLI MUST provide a human-readable channel that enables color when standard
error is a terminal.
**R16.** The CLI MUST provide a JSON channel selected by `--diagnostics=json`.
**R17.** The JSON channel MUST use a versioned schema. The schema version MUST appear in
the output, and the schema MUST be stable within a major version.
**R18.** A single diagnostic MUST render equivalently in content across all channels; the
channels differ only in presentation.
Example JSON shape:
```json
{
"version": 1,
"diagnostics": [
{
"severity": "error",
"code": "VCT1002",
"message": "missing trait at lowering",
"file": "input.vox",
"line": 12,
"column": 5,
"length": 3,
"notes": []
}
]
}
```
### 5.5 Optimizer remarks
**R19.** Optimizer remarks MUST be off by default and MUST be enabled by `--remarks`.
**R20.** Optimizer remarks MUST be enabled automatically at `-O3`.
**R21.** Every remark MUST be tied to a specific trait-pipeline decision, naming the
request or attribute that drove it.
Examples:
```
HIR: replaced 3 uses of node#412 (comptime 64)
VIR: denied Inline on @foo - CostModel (callee 2.4x size budget)
```
### 5.6 Internal compiler error policy
**R22.** The following conditions MUST be treated as internal compiler errors:
- a missing or contradictory trait encountered at lowering (see
[005 — Lowering](005-lowering.md));
- a VIR invariant violation detected in a debug build (see [006 — VIR](006-vir.md));
- a `Strong` request denied for reason `Illegality` or `ContradictsTrait` (see
[007 — VIR Optimizer](007-vir-optimizer.md)).
**R23.** In release builds, an internal compiler error MUST return a failure result and
emit an `Ice` diagnostic that requests a module dump.
**R24.** In debug builds, an internal compiler error MUST assert.
**R25.** An internal compiler error MUST NEVER be silently continued past.
### 5.7 Warning policy
**R26.** The trait-mismatch warning raised when a suggested trait conflicts with the
derived value MUST be promoted to an error under `-Werror`.
**R27.** A frontend MUST be able to opt out of warning promotions: `-Wno-error=<category>` MUST
disable the promotion of that category, and a bare `-Wno-error` MUST disable every promotion.
### 5.8 Exit codes
**R28.** The CLI MUST exit with the codes in Table 2.
**Table 2. Exit codes**
| Code | Meaning |
|---|---|
| 0 | Success. |
| 1 | Diagnostics present. |
| 2 | Usage or configuration error. |
| 3 | Internal compiler error. |
**R29.** Exit code 3 MUST take precedence over exit code 1 when an ICE has been emitted.
## 6. Invariants
- **I1.** Every emitted diagnostic has a severity and a `DiagCode`.
- **I2.** No compilation module writes directly to a stream.
- **I3.** Diagnostics are emitted and rendered in a deterministic order for a fixed
input and `Config`.
- **I4.** An ICE never resolves to a success exit code.
- **I5.** A location is either resolved or omitted; it is never invented.
- **I6.** The JSON channel's schema version is always present.
## 7. Examples
A frontend registers a location and a later lowering failure reports it:
```d
diag.registerLocation(node, SourceLocation("input.vox", 12, 5, 3));
// later, during lowering:
emit(Diagnostic(Severity.Error, DiagCode(1002),
"missing trait at lowering", span));
```
A `MustTail` request on GCC becomes a hard error rather than a silent fallback:
```
error[VCT1044]: MustTail requires [[clang::musttail]]; current compiler is GCC
--> input.vox:7:5
```
## 8. Cross-references
- [000 — Overview](000-overview.md) for conformance and determinism.
- [001 — Architecture](001-architecture.md) for the cold-path GC rule.
- [002 — Traits](002-traits.md) for the trait vocabulary and mismatch handling.
- [005 — Lowering](005-lowering.md) for the lowering map and ICE conditions.
- [006 — VIR](006-vir.md) for the invariants checked in debug.
- [007 — VIR Optimizer](007-vir-optimizer.md) for request denials and `DenyReason`.
- [008 — C Backend](008-c-backend.md) for `#line` emission.
- [010 — Driver & CLI](010-driver-cli.md) for `--remarks`, `--diagnostics`, and exit codes.
+203
View File
@@ -0,0 +1,203 @@
# 012 — Testing
- **Status:** Draft
- **Normative language:** `MUST`, `MUST NOT`, `SHOULD`, `SHOULD NOT`, and `MAY` are to be
interpreted as described in RFC 2119.
## 1. Purpose
This file defines how an implementation demonstrates conformance. It specifies the test layers,
the fixture strategy, the structural verifier, the differential oracle, the fuzzing obligations,
and the CI guarantees. It states **what must be tested and what must hold**, not how to write the
test code.
## 2. Scope
The obligations here apply to every stage: the HIR optimizer, lowering, the VIR optimizer, both
backends, the driver, and the diagnostics service. The determinism guarantee from
[000 — Overview](000-overview.md) is tested here, but it is defined there.
## 3. Definitions
| Term | Meaning |
|---|---|
| **Fixture** | A fully constructed, trait-complete HIR module used as input to a test. |
| **Fixture layer** | The harness that constructs fixtures without a frontend. |
| **Layer** | One category of test with a distinct input source and success criterion. |
| **Verifier** | The component that checks SSA well-formedness and trait consistency. |
| **Oracle** | A trusted implementation used to judge another implementation's output. |
| **Round-trip property** | `print(parse(print(m))) == print(m)` for a module `m`. |
| **FileCheck test** | A test whose expectation is expressed as ordered pattern directives. |
| **Regression corpus** | The checked-in set of input IR plus expected directives. |
| **Matrix** | The cross-product of compiler, optimization level, and backend under test. |
## 4. Core constraint: VIR cannot be tested without a valid HIR fixture
VIR is a strict consumer of traits and performs no discovery (see [006 — VIR](006-vir.md) and
[007 — VIR Optimizer](007-vir-optimizer.md)). A VIR module cannot be authored meaningfully by
hand, because its facts come from HIR. Testing therefore starts from HIR.
1. Every component that consumes traits **MUST** be exercised through HIR fixtures that carry
complete, epoch-valid traits. A test **MUST NOT** assert behavior on VIR whose traits are
incomplete or stale, except a test that deliberately targets the verifier or the
internal-compiler-error path.
2. `vct.test.hirbuild` **MUST** be the fixture layer for HIR optimization, lowering, VIR
optimization, and both backends.
3. `vct.test.hirbuild` **MUST** construct modules, functions, nodes, types, values, and traits
without a frontend, and the modules it produces **MUST** pass the verifier.
## 5. Test layers
| Layer | Input | Success criterion | Required in v1 |
|---|---|---|---|
| Unit | D `unittest` per module | Function-level assertions | Yes |
| Fixture | HIR builder modules | Component behaves as specified on annotated HIR | Yes |
| Round-trip | Textual IR | `print(parse(print(m))) == print(m)` | Yes |
| FileCheck | IR plus directives | Ordered pattern directives match | Yes |
| Verifier | Any IR at any stage | SSA and traits are consistent | Yes |
| Differential | VIR plus host harness | Interpreter and compiled object agree | Yes |
| Fuzz | Random HIR and traits | See the properties below | Yes |
| Matrix | Regression corpus | All matrix configurations pass | Yes |
| Performance | Benchmark programs | Codegen quality and compile time tracked | Yes |
### 5.1 Unit tests
4. Each module **MUST** ship D `unittest` blocks covering its public and internal entry points.
### 5.2 HIR test-builder harness
5. The harness **MUST** produce input that is indistinguishable, for the consuming stage, from a
frontend-produced module. It **MUST** allow a test to set or suggest traits explicitly so that
trait validation, overwrite, and staleness behavior can be tested.
6. Tests for lowering and VIR optimization **MUST** build their HIR through the harness and run
the HIR optimizer first, so VIR sees the same complete annotation it sees in production.
### 5.3 Textual round-trip
7. For every module in the regression corpus, the textual form **MUST** satisfy
`print(parse(print(m))) == print(m)`. Parsing followed by printing **MUST** be lossless with
respect to the printed form.
8. The textual form exists for tools and round-trip tests. It is not a supported authoring
surface (see [000 — Overview](000-overview.md)); tests **MUST NOT** rely on hand-authored
textual VIR by itself as a source of traits.
### 5.4 FileCheck-style matching
9. A test **MUST** express its expected output using `CHECK`, `CHECK-NOT`, and `CHECK-NEXT`
directives against captured stage output.
10. The matcher **MUST** enforce directive order: `CHECK` lines match in sequence, `CHECK-NEXT`
requires a match on the line immediately following the previous match, and `CHECK-NOT`
requires that its pattern does **not** occur between the surrounding matches.
11. CI **MUST** fail a test when a `CHECK-NOT` pattern matches. Exact golden files **SHOULD NOT**
be used, with the sole exception of byte-exact determinism comparisons.
### 5.5 Structural verifier
12. A structural verifier **MUST** check SSA well-formedness and trait consistency. It **MUST**
run after every pass in debug builds, and it **MUST** be invocable on demand through
`--verify` in release builds (see [010 — Driver & CLI](010-driver-cli.md)).
13. The verifier **MUST** check at least these SSA properties, as required by
[006 — VIR](006-vir.md):
- every use is dominated by its definition;
- every basic block is terminated;
- every phi node has exactly one operand per predecessor block;
- every value has a single definition.
14. The verifier **MUST** check trait consistency: traits are complete for the current epoch, and
no two retained traits contradict one another (see [002 — Traits](002-traits.md)).
15. A failed verification **MUST** be an internal compiler error: it **MUST** assert in debug
builds and **MUST** produce an ICE diagnostic plus a failure result in release builds (see
[011 — Diagnostics](011-diagnostics.md)). It **MUST NOT** be silently ignored.
### 5.6 Differential testing
16. A VIR reference interpreter **MUST** be built in v1. It is the differential oracle.
17. For each differential test, the interpreter **MUST** run the same VIR that the backend is
given, the compiled object **MUST** be run on the same inputs, and the two observable results
**MUST** match. Observable results include defined output and exit status; unobservable
internal state is excluded.
18. Where the LLVM path is exercised, the C path and the LLVM path **SHOULD** be compared
differentially for equivalent observable behavior.
### 5.7 Fuzzing
19. Random-HIR and random-trait fuzzers **MUST** exist in v1.
20. Every fuzz input **MUST** satisfy all of these properties:
- the pipeline terminates within its configured budget;
- the verifier reports clean after every pass;
- emitted C, and emitted LLVM IR when the LLVM path is active, compiles;
- for deterministic programs, the interpreter and the compiled object agree.
21. A crash, a verifier failure, non-termination, or non-compiling output **MUST** be treated as
a bug and **MUST** block merge.
### 5.8 Test matrix
22. CI **MUST** test at least GCC and Clang. Each compiler **MUST** be tested across `-O0`,
`-O1`, `-O2`, `-O3`, `-Ofast`, and `-Oz`, and across the C backend and the LLVM backend where
the backend applies.
### 5.9 Performance benchmarks
23. Benchmarks **MUST** track both codegen quality and compile time. Compile time is measured
through `--time-passes` and `--stats` (see [010 — Driver & CLI](010-driver-cli.md)).
24. Because codegen quality is the headline goal (see [000 — Overview](000-overview.md)), a
regression in generated-code quality **MUST** be surfaced even when compile time improves.
## 6. Determinism testing
25. Every pipeline run in CI **MUST** be executed at least twice with identical `Config` and
identical input, and the two outputs **MUST** be byte-identical.
26. CI **MUST** vary the thread count across runs, including at least one run with `-j1` and one
with `-j` greater than one, to demonstrate independence from thread count.
27. The determinism guarantee itself is defined in [000 — Overview](000-overview.md). A
nondeterministic result **MUST** be treated as a correctness bug even if the outputs are
semantically equivalent.
## 7. Regression corpus
28. The regression corpus **MUST** store input IR paired with expected FileCheck patterns.
29. CI **MUST** run the full matrix over the corpus. A failing corpus entry **MUST** block merge.
30. A bug fix **MUST** add a corpus entry that fails before the fix and passes after it.
## 8. Invariants
- A green suite **MUST** mean that every requirement in this file held for the tested revision.
- The verifier **MUST** be sound: it **MUST NOT** report clean on malformed SSA or on
contradictory traits. A false negative is itself a bug.
- The differential oracle **MUST** be conservative: disagreement between the interpreter and the
compiled object **MUST** be reported as a failure, not explained away.
## 9. Examples
A round-trip test asserts:
```
assert(print(parse(print(m))) == print(m));
```
A FileCheck test names the stage output it inspects and lists its directives:
```
; RUN: vct --dump-vir --verify %s | FileCheck %s
; CHECK: define i32 @main(
; CHECK-NEXT: entry:
; CHECK-NOT: phi
; CHECK: ret i32 0
```
The first `CHECK` matches the function header, `CHECK-NEXT` requires the entry label on the very
next line, `CHECK-NOT` asserts that no phi node appears before the return, and the final `CHECK`
matches the return instruction.
## 10. Cross-references
- [000 — Overview](000-overview.md) for the determinism guarantee.
- [002 — Traits](002-traits.md) for trait completeness, epochs, and the verifier's trait checks.
- [004 — HIR Optimizer](004-hir-optimizer.md) for the cascade under test.
- [005 — Lowering](005-lowering.md) for SSA output invariants.
- [006 — VIR](006-vir.md) for the SSA properties the verifier checks.
- [007 — VIR Optimizer](007-vir-optimizer.md) for request results under test.
- [008 — C Backend](008-c-backend.md) and [009 — LLVM Backend](009-llvm-backend.md) for the
emitted forms checked by FileCheck and the matrix.
- [010 — Driver & CLI](010-driver-cli.md) for `--verify`, `--time-passes`, and `--stats`.
- [011 — Diagnostics](011-diagnostics.md) for the ICE policy the verifier feeds.
- [013 — Build & Packaging](013-build-packaging.md) for how `vct.test.hirbuild` ships.
+185
View File
@@ -0,0 +1,185 @@
# 013 — Build & Packaging
- **Status:** Draft
- **Normative language:** `MUST`, `MUST NOT`, `SHOULD`, `SHOULD NOT`, and `MAY` are to be
interpreted as described in RFC 2119.
## 1. Purpose
This file specifies how libVCT is built, what it produces, what it exposes to embedders, and who
owns memory across the API boundary. It also fixes the configuration, threading, versioning, and
determinism contracts that the rest of the specification depends on.
## 2. Scope
Covers the toolchain, the build system, the shipped artifacts, the two API surfaces, module
visibility, arena ownership, the `Config` struct, the threading contract, versioning, and the
determinism guarantee. It does not restate stage semantics; those live in the stage files.
## 3. Definitions
| Term | Meaning |
|---|---|
| **C API** | The `extern(C)` surface exposed to non-D callers. |
| **D API** | The native D surface; the implementation itself. |
| **Shim** | The thin translation layer implementing the C API over the D API. |
| **Arena** | A region of memory reclaimed as a unit, not per object. |
| **Bump allocator** | The default in-tree allocator that serves requests by advancing a pointer. |
| **Cold path** | Code that runs at most a few times per compilation: driver, diagnostics, CLI. |
## 4. Toolchain
1. The library **MUST** be written in D.
2. Release builds **MUST** use LDC2, chosen for generated-code quality.
3. Debug builds **MUST** use DMD, chosen for compile and iteration speed.
4. libVCT **MUST** have zero runtime dependencies beyond its own code. It **MUST NOT** require a
third-party library at build time or at load time.
5. A D compiler is required to build libVCT. A system C compiler is required only when the C path
is used, and it **MUST** be discovered at runtime (see [010 — Driver & CLI](010-driver-cli.md)).
Prebuilt binaries **MUST NOT** require a D compiler at load time.
## 5. Build system
6. The build system **MUST** be xmake. A single `xmake.lua` **MUST** drive every target.
7. The build system **MUST NOT** be dub.
8. The `release` profile **MUST** select LDC2. The `debug` profile **MUST** select DMD.
9. Building libVCT **MUST NOT** require an external package manager.
## 6. Artifacts
10. A conforming build **MUST** produce all of the following:
| Artifact | Kind | Purpose |
|---|---|---|
| `libvct.a` | static library | Static linking of the full library. |
| `libvct.so` | shared library | Dynamic linking; embedders load it at runtime. |
| `vct` | executable | The command-line driver (see [010 — Driver & CLI](010-driver-cli.md)). |
| `vctc.h` | C header | The declarations of the C API. |
| `vct.test.hirbuild` | D module | The fixture harness, shipped publicly (see [012 — Testing](012-testing.md)). |
11. The static and shared libraries **MUST** expose the same symbols and behavior; choosing one
over the other **MUST NOT** change observable results.
## 7. Public API surfaces
12. The **C API** **MUST** be the primary public surface in v1. It **MUST** be declared
`extern(C)` so that C, C++, and other FFI callers can use it without a D toolchain.
13. The C API **MUST** represent library state through opaque handles for `Context`, `Module`,
`Builder`, and `Config`. Callers **MUST NOT** depend on the layout of these types, and the
header **MUST NOT** expose their internal fields.
14. The C API **MUST** provide operations to build IR, run the pipeline, and query diagnostics.
15. The **D API** **MUST** be the full-feature implementation surface. The C API is a shim over
it.
16. The shim **MUST NOT** contain logic beyond argument marshalling, error translation, and
handle conversion. Every behavior reachable through the C API **MUST** be implemented in the
D API, so that every C API call exercises the D API.
17. An implementation **MUST NOT** place optimizer, lowering, or backend logic in the header or
the shim.
An illustrative header shape (not a complete declaration set):
```c
typedef struct vct_context vct_context;
typedef struct vct_module vct_module;
typedef struct vct_builder vct_builder;
typedef struct vct_config vct_config;
vct_context *vct_context_create(const vct_config *cfg);
void vct_context_destroy(vct_context *ctx);
```
## 8. Module visibility
18. The following modules **MUST** be public: `vct.ir.hir`, `vct.traits`, `vct.context`, the
entry points of `vct.backend.c` and `vct.backend.llvm`, `vct.driver`, `vct.diag`, and
`vct.test.hirbuild`.
19. The following modules **MUST** be internal: `vct.ir.vir`, `vct.lower`, `vct.hir.opt`,
`vct.vir.opt`, and the internals of `vct.comptime`.
20. A public module **MUST NOT** expose internal types in its public signatures in a way that
forces embedders to depend on internal layout. The trait vocabulary and node identity cross
this boundary and are versioned accordingly (see section 12).
## 9. Memory ownership and lifecycle
21. A `Context` **MUST** own one or more arenas. An arena is the unit of reclamation. The default
is one arena per `Module`, freed wholesale.
22. All IR and optimizer objects **MUST** be arena-owned and non-GC. Passes **MUST** mutate them
in place under epoch guards (see [002 — Traits](002-traits.md)).
23. Garbage collection **MUST** be permitted only on cold paths: the driver, diagnostics, and the
CLI. The optimizer, lowering, and backends **MUST NOT** depend on the GC for IR objects.
24. The `Context` **MUST** accept an `ArenaAllocator` interface so embedders can supply their own
backing memory. The interface **MUST** provide exactly these operations:
- `allocate(size, alignment)` returns uninitialized storage owned by the arena; callers MUST
NOT assume zeroed storage;
- `reset()` returns the arena to its initial state so it can be reused;
- `destroy()` releases the arena's backing memory.
25. A default bump allocator **MUST** ship in-tree and **MUST** be the allocator used when the
embedder supplies none.
26. Ownership rules:
- Objects allocated into a `Context` or arena live until that arena is reset or destroyed.
- A caller **MUST NOT** free an individual arena-owned object. Freeing happens only through
`reset` or `destroy`.
- A pointer or handle returned across the API **MUST** remain valid until the owning arena is
reset or destroyed.
- Use of an object after its arena has been reset or destroyed is undefined, and the
implementation **MUST NOT** be required to detect it.
## 10. Configuration
27. A single `Config` **MUST** carry every setting that affects output. At minimum it **MUST**
carry:
| Field group | Contents |
|---|---|
| Optimization level | `-O0`, `-O1`, `-O2`, `-O3`, `-Ofast`, `-Oz`. |
| Target | `-march`, `-mcpu`, `-target <triple>`. |
| Budgets | comptime and HIR budgets (see [004 — HIR Optimizer](004-hir-optimizer.md)) and VIR budgets (see [007 — VIR Optimizer](007-vir-optimizer.md)). |
| Pass toggles | enable and disable for every pass exposed as `-f` and `-fno-`. |
| Warning policy | `-Wall`, `-Wextra`, `-Werror`, `-Wno-error[=<category>]`, `-w`. |
| C compiler selection | `-cc=gcc` or `-cc=clang`, or automatic discovery. |
| Mode | `-S`, `-emit-llvm`/`-llvm`, or `-mangled`. |
28. The CLI **MUST** be a pure function from `argv` to a `Config`. The library **MUST** take a
`Config` and **MUST NOT** read process arguments itself.
29. Programmatic callers **MUST** be able to bypass the CLI and construct a `Config` directly.
30. The determinism guarantee (section 13) is stated relative to a fixed `Config`.
## 11. Threading
31. libVCT **MUST** be thread-safe when each compilation unit has its own `Context` and arenas.
32. libVCT **MUST NOT** hold shared mutable global state.
33. Intra-module parallelism is internal and **MUST** be bounded by the `-j` setting. The
observed output **MUST NOT** depend on `-j` (see [000 — Overview](000-overview.md)).
## 12. Versioning
34. The library **MUST** follow semantic versioning.
35. The textual IR format **MUST** carry its own version, independent of the library version.
36. The trait vocabulary **MUST** carry its own version. Adding an attribute or request is a
backward-compatible change; changing the meaning of an existing one is a major change.
37. Experimental passes **MUST** be gated behind feature flags so that default behavior stays
stable within a minor version.
## 13. Determinism guarantee
38. For a fixed `Config` and a fixed input, libVCT **MUST** produce byte-identical output on every
run, independent of thread count. This binds HIR optimization, lowering, VIR optimization,
and both backends. It is a correctness property, not an optimization.
39. Determinism **MUST** be demonstrated in CI as specified in [012 — Testing](012-testing.md).
## 14. Invariants
- The C API and the D API **MUST** expose the same behavior; a divergence is a bug in the shim.
- An arena **MUST** be reclaimable as a unit; no arena-owned object may outlive its arena.
- A stage **MUST NOT** depend on shared mutable global state.
- The same `Config` and input **MUST** yield the same bytes regardless of `-j`.
## 15. Cross-references
- [000 — Overview](000-overview.md) for goals, non-goals, and the determinism guarantee.
- [001 — Architecture](001-architecture.md) for the module map and interfaces.
- [002 — Traits](002-traits.md) for epochs and the trait vocabulary version.
- [004 — HIR Optimizer](004-hir-optimizer.md) for budget names and defaults.
- [007 — VIR Optimizer](007-vir-optimizer.md) for VIR budget names and defaults.
- [010 — Driver & CLI](010-driver-cli.md) for flag semantics and C compiler discovery.
- [012 — Testing](012-testing.md) for determinism tests and the fixture harness.
+230
View File
@@ -0,0 +1,230 @@
# 014 — Worked Example
- **Status:** Draft
- **Normative language:** `MUST`, `MUST NOT`, `SHOULD`, `SHOULD NOT`, and `MAY` are to be
interpreted as described in RFC 2119.
## 1. Purpose
This file walks one small program from the HIR builder through the comptime cascade, trait
derivation, request handling, lowering, VIR optimization, and C emission. It is **normative for
trait and request semantics**: a conforming implementation **MUST** reach the trait states,
request outcomes, and observable result described here. Where a stage file gives the general
rule, this file gives the concrete instance that fixes its meaning.
## 2. Scope
The example covers the HIR optimizer, the trait vocabulary, lowering, the VIR optimizer's
handling of one request, and the C backend. It does not restate the full grammar of HIR or VIR,
and it is not a conformance test by itself. It is the reference walkthrough that other files cite.
## 3. The 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);
}
```
A frontend builds this through the HIR builder. The builder calls are illustrative; their shapes
show how nodes and values enter HIR:
```d
auto m = ModuleBuilder("foo");
auto fn = m.function("foo", [intTy, intTy], intTy);
auto body = fn.block();
body.ret(body.add(fn.param(0), fn.param(1)));
auto main_ = m.function("main", [], intTy);
auto b = main_.block();
auto x = b.local("x", b.literal(4));
auto y = b.local("y", b.add(x, b.literal(4)));
auto z = b.local("z", b.call("foo", x, y));
auto a = b.local("a", b.call("sqrt", z));
b.expr(b.call("println", a));
```
The frontend **MAY** suggest traits while building. HIR validates each suggestion and overwrites a
wrong value with the derived one, promoting the mismatch to an error under `-Werror` (see
[002 — Traits](002-traits.md)).
## 4. Rules exercised
| Rule | Statement | Defined in |
|---|---|---|
| **R1** | When every operand of a pure primitive is `is_comptime`, the evaluator computes `const_value` and sets `is_comptime` and `is_constant`. | [004 — HIR Optimizer](004-hir-optimizer.md) |
| **R2** | Before substituting a constant for a node, HIR **MUST** prove the node is not addressed and not runtime-mutable. | [002 — Traits](002-traits.md), [004 — HIR Optimizer](004-hir-optimizer.md) |
| **R3** | A `is_comptime` node with `const_value = v` has every use replaced by the literal `v`. | [004 — HIR Optimizer](004-hir-optimizer.md) |
| **R4** | A node with no remaining uses and no side effects **MUST** be deleted. | [004 — HIR Optimizer](004-hir-optimizer.md) |
| **R5** | Every node whose operands became constant **MUST** be re-enqueued on the worklist. | [004 — HIR Optimizer](004-hir-optimizer.md) |
| **R6** | HIR **MUST NOT** inline. It proves smallness and issues an `Inline` request to VIR. | [004 — HIR Optimizer](004-hir-optimizer.md), [007 — VIR Optimizer](007-vir-optimizer.md) |
| **R7** | Every request **MUST** yield a `RequestResult`; an accepted request needs no reason, and a denial **MUST** carry a `DenyReason` and a one-line note. | [002 — Traits](002-traits.md) |
| **R8** | Each VIR entity **MUST** carry the `Trait` of its source HIR node; relationship endpoints are rewritten through the `LoweringMap`. | [005 — Lowering](005-lowering.md) |
| **R9** | A scalar that is not addressed, not escaping, and not runtime-mutable **MUST** lower to a pure SSA value. | [005 — Lowering](005-lowering.md) |
| **R10** | A comptime value **MUST** be emitted as a literal, not recomputed. | [008 — C Backend](008-c-backend.md) |
## 5. The canonical cascade shape
The cascade is the chain of rewrites triggered by one fold. Its canonical textbook shape uses
`4 * 16`:
```
n = 4 * 16
-> const 64 (R1; trigger ConstantUnfolded)
-> mark is_comptime / is_constant / const_value = 64
-> prove not addressed / not runtime-mutable (R2)
-> replace all uses with literal 64 (R3; trigger UsesReplaced)
-> node dead -> delete (R4; trigger NodeDeleted)
-> downstream nodes now constant -> enqueue (R5; trigger TraitChanged)
```
Each hop emits a trigger. The worklist scheduler dedupes `(pass, node)` pairs and runs to a
fixpoint, bounded by the per-function and per-module rewrite budgets (see
[004 — HIR Optimizer](004-hir-optimizer.md)). Step 6 of the walkthrough below is exactly this
shape applied to `y = x + 4`, and the same shape recurs for `z`, `a`, and the argument of
`println`.
## 6. Worked cascade for `foo` / `main`
The steps below run per function, in deterministic order. Each step names the rule it exercises.
1. **Build.** `main` binds `x` to the literal `4`. The literal already carries
`const_value = 4`; HIR sets `is_comptime = true` and `is_constant = true` (R1).
2. **Prove `x`.** HIR proves `x` is not addressed and not runtime-mutable: `is_addressed = false`,
`is_runtime_mutable = false`, `escapes = false`, `is_mutably_used = false` (R2).
3. **Replace uses of `x`.** Every use of `x` becomes the literal `4`. Trigger `UsesReplaced`
(R3).
4. **Delete `x`.** `x` has no uses and no side effects; it is deleted. Trigger `NodeDeleted`
(R4).
5. **Re-enqueue.** `y = x + 4` is now `y = 4 + 4`, so `y` is enqueued. Trigger `TraitChanged`
(R5).
6. **Fold `y`.** The evaluator computes `4 + 4 = 8`, and sets `y.const_value = 8`,
`y.is_comptime = true`, `y.is_constant = true`. Trigger `ConstantUnfolded` (R1).
7. **Prove and replace `y`.** R2 holds for `y`; R3 rewrites its uses to `8`; R4 deletes `y`.
8. **Re-enqueue `z`.** `z = foo(x, y)` is now `z = foo(4, 8)`; `z` is enqueued (R5).
9. **Fold `z`.** `foo` is pure, non-recursive, and its body evaluates with primitives only, so
the evaluator interprets the call with arguments `4` and `8` and computes `12`. `z` becomes
`is_comptime = true`, `is_constant = true`, `const_value = 12` (R1). This is constant
evaluation, not inlining: HIR computes a value and does not rewrite `foo` into `main`
(R6).
10. **Request inlining.** Separately, HIR proves `foo` is small and issues an `Inline` request on
the call edge. HIR does not perform the inlining (R6).
11. **Prove, replace, delete `z`.** R2 holds; R3 rewrites the use in `a = sqrt(z)` to
`a = sqrt(12)`; R4 deletes `z`.
12. **Fold `a`.** `sqrt` is a primitive and its argument is `is_comptime`, so the evaluator
computes a `double` result: `a.const_value = 3.4641016151377544`, `a.is_comptime = true`,
`a.is_constant = true` (R1). The other float arithmetic in this example is exact; the square
root is not, and the field holds the IEEE 754 binary64 value of `sqrt(12)`, printed with the
shortest decimal string that round-trips.
13. **Replace and delete `a`.** R3 rewrites the argument of `println` to
`3.4641016151377544`; R4 deletes `a`.
14. **Collapse.** What remains of `main` is `println(3.4641016151377544)`. The `foo` function now
has no call site. HIR does not delete functions; that is a VIR/IPA concern.
The worklist drains when no node changes, which here is after step 14.
## 7. Traits at completion
The derived traits that survive the cascade are:
| Node | `ty` | `const_value` | `is_comptime` | `is_constant` | `is_addressed` | `is_runtime_mutable` | `escapes` |
|---|---|---|---|---|---|---|---|
| `x` (deleted) | `int` | `4` | true | true | false | false | false |
| `y` (deleted) | `int` | `8` | true | true | false | false | false |
| `z` (deleted) | `int` | `12` | true | true | false | false | false |
| `a` (deleted) | `double` | `3.4641016151377544` | true | true | false | false | false |
| `println` arg | `double` | `3.4641016151377544` | true | true | false | false | false |
| `foo` | function | absent | false | false | false | false | false |
`foo.is_used` falls to `false` once its only call is folded. `is_constant` without `is_comptime`
is possible in general; every value in this example is comptime.
## 8. Requests and their results
| Request | Target | Strength | Requested because | Result |
|---|---|---|---|---|
| `Inline` | call edge of `foo` in `main` | Soft | `foo` proven small (R6) | denied: `AlreadyDone` (call folded in HIR; no live call remains) (R7) |
The `Inline` request is soft, so VIR **MAY** decline it; here it declines with `AlreadyDone`
because HIR already folded the only call site, so no live call remains to inline. A denial
**MUST** carry a `DenyReason` and a one-line note, for example
`denied Inline on @foo, reason CostModel, note "callee exceeds size budget"`. No conflicting
`Strong` request is issued in this example, and `NoInline` **MUST NOT** be requested for `foo`.
Recording the request as accepted would violate O-28 of
[007 — VIR Optimizer](007-vir-optimizer.md), which forbids marking a request accepted when no
transform was performed. VIR removes the now-unused `foo` through global dead-code elimination.
## 9. Lowering to VIR
Lowering is a pure translation. It builds a `LoweringMap` from HIR nodes to VIR entities and
copies each node's `Trait` onto its VIR entity (R8). Each local here satisfies R9, so it lowers
to a pure SSA value rather than an `alloca`. An illustrative VIR listing for the optimized
`main`, plus the still-present `foo`, is:
```
define i32 @foo(i32 %x, i32 %y) {
entry:
%s = add i32 %x, %y
ret i32 %s
}
define i32 @main() {
entry:
%r = call i32 @println(double 3.4641016151377544)
ret i32 %r
}
```
The textual form is shown for clarity only. It is not an authoring surface (see
[000 — Overview](000-overview.md)); the same module can be expressed with the builder or as
opaque binary structures.
VIR optimization resolves the `Inline` request (denied `AlreadyDone`), runs global DCE, and
leaves the call to `println`. It does not re-derive traits; every fact it uses came from HIR (see
[007 — VIR Optimizer](007-vir-optimizer.md)).
## 10. Emitted C
The C backend emits C17. Because the argument is comptime, it is emitted as a literal (R10), and
the irreducible shape collapses to a single call:
```c
extern int println(double);
int main(void) {
return println(3.4641016151377544);
}
```
The exact spelling of the float literal is implementation-defined as long as it round-trips to
the same binary64 value; the value `3.4641016151377544` is normative.
## 11. Invariants
- A conforming implementation **MUST** derive the trait values in section 7 from the section 3
input.
- A conforming implementation **MUST** issue an `Inline` request for `foo` and **MUST NOT**
inline it in HIR.
- A conforming implementation **MUST** reach the observable behavior of `println` called once
with the binary64 value of `sqrt(12)`, whatever its internal pipeline order.
- Running the example twice with the same `Config` **MUST** produce byte-identical output (see
[000 — Overview](000-overview.md)).
## 12. Cross-references
- [000 — Overview](000-overview.md) for the two-stage thesis and the determinism guarantee.
- [002 — Traits](002-traits.md) for attributes, requests, `RequestResult`, and `DenyReason`.
- [004 — HIR Optimizer](004-hir-optimizer.md) for the cascade, triggers, and budgets.
- [005 — Lowering](005-lowering.md) for the `LoweringMap`, SSA construction, and the memory
model behind R9.
- [006 — VIR](006-vir.md) for the SSA and CFG shapes used in section 9.
- [007 — VIR Optimizer](007-vir-optimizer.md) for request handling and global DCE.
- [008 — C Backend](008-c-backend.md) for emission and literal formatting.
+159
View File
@@ -0,0 +1,159 @@
# 015 — Open Questions
- **Status:** Draft
- **Normative language:** `MUST`, `MUST NOT`, `SHOULD`, `SHOULD NOT`, and `MAY` are to be
interpreted as described in RFC 2119.
## 1. Purpose
This file records the capabilities that v1 deliberately defers. Each item is stated as an open
question with the reason for deferral and the work needed to resolve it. The purpose is to
separate "not yet designed" from "required and specified", so that readers and implementors do
not mistake a gap for a guarantee.
## 2. Scope and conformance status
Everything in this file is **out of scope for v1**. These items are **not** conformance
requirements, and they add no behavior an implementation must provide.
1. An implementation **MUST NOT** be judged non-conforming for omitting any item in this file.
2. An implementation **MAY** implement any item here. Doing so **MUST NOT** change the behavior
required by the stage files for the features that are in scope.
3. The absence statements already in [000 — Overview](000-overview.md), such as the absence of
LLVM debug metadata on the LLVM path, describe the v1 scope. They **MUST NOT** be read as
forbidding a future extension.
4. When an item is resolved, it **MUST** be promoted into a stage file with its own normative
requirements; it **MUST NOT** remain only here.
## 3. Summary
| ID | Open question | v1 status | Would touch |
|---|---|---|---|
| OQ1 | LLVM debug info (`!llvm.dbg`) | Deferred | [009 — LLVM Backend](009-llvm-backend.md) |
| OQ2 | Windows and MSVC support | Out of scope | [008 — C Backend](008-c-backend.md), [010 — Driver & CLI](010-driver-cli.md) |
| OQ3 | LLVM bitcode emission | Deferred | [009 — LLVM Backend](009-llvm-backend.md) |
| OQ4 | Incremental and cached compilation | Out of scope | [010 — Driver & CLI](010-driver-cli.md), [013 — Build & Packaging](013-build-packaging.md) |
| OQ5 | Cross-compilation beyond passthrough | Future work | [009 — LLVM Backend](009-llvm-backend.md), [010 — Driver & CLI](010-driver-cli.md) |
## 4. OQ1: LLVM debug information
**Question.** Should the LLVM backend emit `!llvm.dbg` metadata so that debuggers can map machine
state back to frontend source?
**v1 behavior.** No. The LLVM backend emits no debug metadata. Debug mapping exists only on the C
path, through `#line` directives (see [008 — C Backend](008-c-backend.md)).
**Why deferred.** The C path already provides source mapping for the v1 target, and LLVM debug
metadata is a large, version-sensitive surface. Emitting it correctly needs `DICompileUnit`,
`DIFile`, `DISubprogram`, `DILocation`, and scope chain construction, plus a verifier pass that
checks the metadata graph. That is a subsystem of its own, and getting it half-right produces
misleading debuggers, which is worse than no debug info.
**What resolution requires.**
- Define how VIR source locations resolve through the `LoweringMap` and the registered source
map (see [011 — Diagnostics](011-diagnostics.md)).
- Lower those locations to LLVM metadata on the LLVM 18+ schema, including lexical block scopes.
- Add a verification step over the metadata graph, and test it with `llvm-dwarfdump` and a
debugger round-trip in CI.
- Decide whether debug metadata is emitted only under `-g` and how it interacts with the
determinism guarantee.
## 5. OQ2: Windows and MSVC support
**Question.** Should libVCT support Windows as a host or target, and MSVC as the C compiler?
**v1 behavior.** No. v1 is Linux/POSIX-first and supports GCC and Clang only (see
[000 — Overview](000-overview.md)).
**Why deferred.** The C backend leans on GCC and Clang constructs: `__builtin_expect`,
`__builtin_assume_aligned`, `__builtin_assume`, `__asm__` fences, `#[[]]` attributes, and
`#pragma` loop hints. MSVC lacks most of these and spells the rest differently. Beyond the C
surface, the driver's compiler discovery, response-file handling, path rules, and ABI
(`dllexport`/`dllimport`, COFF) all differ. Cosmopolitan mode covers a subset of Windows
execution but not MSVC as a backend compiler.
**What resolution requires.**
- Define an MSVC dialect layer: per-construct mappings for hints, attributes, fences, and
pragmas, with a portable fallback where no equivalent exists.
- Extend the driver with `cl.exe` discovery, argument syntax, and error parsing.
- Specify the Windows ABI surface, including exported symbol decoration and the C runtime.
- Add a Windows CI matrix, which also forces decisions about the xmake build on Windows.
## 6. OQ3: LLVM bitcode emission
**Question.** Should the LLVM backend be able to emit bitcode (`.bc`) directly instead of
textual `.ll`?
**v1 behavior.** No. The backend emits textual `.ll` only, and the caller runs `llvm-as` (see
[009 — LLVM Backend](009-llvm-backend.md)). libVCT never links LLVM.
**Why deferred.** The two obvious routes both carry cost or conflict. Linking LLVM would add a
large native dependency and contradict the zero-runtime-dependency rule (see
[013 — Build & Packaging](013-build-packaging.md)). Emitting the LLVM bitstream by hand would
require implementing and version-tracking the bitstream container format, which is stable but
large and not the intended value of the project. Textual output keeps the backend a pure
translator.
**What resolution requires.**
- Choose a route: an optional LLVM-linked build, a bundled bitstream writer, or a supported
external `llvm-as` invocation driven by the library.
- If the library invokes `llvm-as`, specify discovery and failure handling the way the C compiler
is handled in [010 — Driver & CLI](010-driver-cli.md).
- Keep textual emission available, since it is the format the round-trip and FileCheck tests use
(see [012 — Testing](012-testing.md)).
## 7. OQ4: Incremental and cached compilation
**Question.** Should libVCT cache compiled objects keyed by a content hash so that repeated
compilations skip work?
**v1 behavior.** No. Content-hash `.o` caching is out of scope. Every invocation of the pipeline
compiles from the input it is given.
**Why deferred.** Caching is only safe when the cache key captures everything that affects
output. The deterministic-output guarantee makes caching feasible, but it also raises the bar:
a stale hit would silently violate determinism. The key must cover the input, the full `Config`,
the library version, the trait vocabulary version, the textual IR format version, and the chosen
C compiler and its version. The storage, eviction, and concurrency design is a separate feature.
**What resolution requires.**
- Specify the cache key and prove that no input affecting output is omitted.
- Specify cache storage, invalidation, and locking, including concurrent invocations.
- Expose the feature through the CLI and the API without making it the default.
- Test that a cache hit and a cache miss produce byte-identical output.
## 8. OQ5: Cross-compilation
**Question.** Should libVCT support cross-compilation to targets other than the host?
**v1 behavior.** Partial. `-target <triple>` passes through to the toolchain, and `-mangled`
mode produces an Actually Portable Executable, but there is no general cross-compilation support.
Compiler discovery is host-oriented, and there is no sysroot or header management.
**Why deferred.** Real cross-compilation needs a target model, not just a flag: target data
layout for the LLVM backend, an ABI definition for the C backend, discovery of a target C
compiler, and a sysroot with target headers and libraries. The v1 pipeline assumes a host
compiler that already knows its target.
**What resolution requires.**
- Define the supported target matrix and the meaning of a target triple in `Config`.
- Derive the LLVM target data layout and triple, and validate the module against it.
- Specify cross C compiler discovery, sysroot layout, and header resolution.
- Define the target ABI surface for the C backend and test it on at least one non-host target.
## 9. Cross-references
- [000 — Overview](000-overview.md) for the v1 non-goals these questions extend.
- [008 — C Backend](008-c-backend.md) for the GCC and Clang constructs behind OQ2 and OQ5.
- [009 — LLVM Backend](009-llvm-backend.md) for the emission surface behind OQ1, OQ3, and OQ5.
- [010 — Driver & CLI](010-driver-cli.md) for compiler discovery behind OQ2, OQ3, OQ4, and OQ5.
- [011 — Diagnostics](011-diagnostics.md) for source maps behind OQ1.
- [012 — Testing](012-testing.md) for the regression and determinism obligations new features
must inherit.
- [013 — Build & Packaging](013-build-packaging.md) for the dependency and versioning rules
these features must respect.