Skip to content
volt
GitHub

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.

  • 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.