Backends
The checker turns each function into a small typed IR, and a backend turns the IR into code. voltc has two, and they’re interchangeable: layouts and calling conventions match, so a library built by one links into a program built by the other.
The C backend writes C that a person can read: named after the Volt it comes from, with a comment
per function saying where it was declared. voltc emit-c app.volt -o out/ writes it as files:
| File | What’s in it |
|---|---|
volt.h |
the prelude, and every type and function declaration |
NAME.c |
one per Volt source file: its functions |
glue.c |
generated helpers (deleting and copying values, error names), when there are any |
runtime.c |
the small runtime: the allocator wrapper and argv |
program.c |
includes them all, so cc out/program.c builds the program |
Only functions the program can reach, and export fns, are emitted. Here is a function and what it
becomes:
struct point { x: i32; y: i32; }
fn add(a: point, b: point) -> point { return { x: a.x + b.x, y: a.y + b.y };}// add (point.volt:3)static v_point v_add(v_point a, v_point b) { int32_t _s1; int32_t _s2;
_s1 = volt_add_i32(a.x, b.x, "point.volt:4:17"); _s2 = volt_add_i32(a.y, b.y, "point.volt:4:31"); return (v_point){ .x = _s1, .y = _s2 };}volt_add_i32 is the overflow check (a plain + in --release builds). The C compiler is
$CC (cc by default), with -O2 for release builds. The C is GNU C, not ISO C: it uses
statement expressions, __int128 and zero-length arrays, which gcc and clang (Apple’s, and clang on
Windows) take and MSVC’s cl doesn’t, so cl isn’t a C compiler voltc supports.
An f128 is the C compiler’s __float128, or long double where that is binary128 (aarch64
Linux). Where C has neither (arm64 macOS, whose long double is a double), it’s 16 bytes of bits
and each operation on it a call to the soft quad routines LLVM calls there too (std::softfloat’s,
under compiler-rt’s names): the C writes every f128 operation as a volt_q_* macro, a plain
operator or cast where the type is native. --cc -DVOLT_SOFT_F128 takes the soft path anywhere.
% on an f128 goes through a double in C.
The LLVM backend generates native code through LLVM’s C API, with no C compiler in between (cc
still links the program and compiles the tiny C runtime). voltc emit-llvm prints the IR.
It’s the default on x86-64 and aarch64 (Linux, macOS, FreeBSD). It lowers Windows x64’s calling
convention too, but C stays the default there until voltc itself runs on Windows; --backend c or
--backend llvm chooses either way. C’s static inline functions from headers are reached through
pointers that a small generated C unit exports.
A header struct with members Volt can’t read (a bitfield, a _Complex, a long double) gets its
layout from libclang: each field Volt reads sits at C’s offset, in a struct of C’s size and
alignment. C passes some of these by value in a way their readable fields can’t tell (a _Complex
goes in floating-point registers, a bitfield shares an eightbyte with a float), so a call of a
header function taking or giving one by value goes through a C shim in that unit that takes the
structs by pointer, and C passes them. What a shim can’t cover builds the program through C
instead, with a note naming the line: a call through a C function pointer, a Volt fn used as one
(C may call it), an exported or extern "C" fn, a variadic argument or a function declared
without a header taking or giving such a struct by value, and a struct libclang can’t lay out or
aligned to more than 16 bytes. --backend llvm makes those an error instead.
Debug builds carry DWARF debug information: a breakpoint goes on a Volt line (break main.volt:12),
stepping goes statement by statement, and a debugger shows parameters and locals by their Volt
names, with structs, str, arrays and pointers laid out as Volt lays them out.
How they’re kept in step
Section titled “How they’re kept in step”- The golden test suite runs every program through both backends.
- The compiler builds itself through each: the bootstrap check requires the LLVM-built compiler to produce the same C and the same LLVM IR as the C-built one.
- Libraries built by one backend are linked into programs built by the other, in both directions.
See Internals for how the backends work.