13 KiB
008 — C Backend
- Status: Draft
- Normative language:
MUST,MUST NOT,SHOULD,SHOULD NOT, andMAYare 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; the pipeline that feeds the backend is specified in 007 — VIR Optimizer and 010 — Driver & CLI.
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 and emits C17. It performs no optimization and no discovery; every fact it uses is read from traits (see 002 — Traits).
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).
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).
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). 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 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 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).
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). This is the semantic-capability
exception of O-26 in 007 — VIR Optimizer: 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-
gotofallback 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:
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:
__asm__ __volatile__("" ::: "memory");
vct_opaque_barrier(x);
A layout-pinned struct emits padding plus compile-time checks:
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 for determinism and conformance.
- 001 — Architecture for module boundaries.
- 002 — Traits for the trait vocabulary consumed here.
- 006 — VIR for the input IR and its invariants.
- 007 — VIR Optimizer for the pass pipeline.
- 009 — LLVM Backend for the alternate backend.
- 010 — Driver & CLI for compiler probing and TU concatenation.
- 011 — Diagnostics for error reporting and
#line.