Skip to content
volt
GitHub

How the backends work

cgen.volt writes the IR as GNU C11. Types come first (every struct, union, tagged enum and slice type, in dependency order), then declarations, then the functions. Names are readable: a Volt function geo::area becomes v_geo__area, locals keep their names (renamed only when they’d clash with a C keyword, a macro from the C library, or another name), and each function starts with a comment naming its Volt declaration.

Checked arithmetic becomes calls to the prelude’s volt_add_i32 and friends, which use the C compiler’s overflow builtins; release builds use plain wrapping arithmetic.

emit-c -o DIR splits the output into volt.h, one .c per source file, glue.c and runtime.c, with a program.c that includes them.

lgen.volt builds an LLVM module through the llvm-c API. Layouts are computed to match C’s exactly, and every function uses the target’s C calling convention: System V on x86-64, AAPCS64 on aarch64 (with Apple’s variant, where the caller extends small ints), or Windows x64. Each has its own rules for passing and returning structs in registers or memory, and tests/abi.rs checks them against clang: VOLT_TRIPLE=aarch64-unknown-linux-gnu voltc emit-llvm lowers for another host, and each export fn in tests/abi/sigs.volt has to get the signature clang gives the same C. On aarch64, C and Volt also call each other under qemu-aarch64. A small C unit is still compiled for the runtime and for C headers’ static inline functions, which the LLVM code reaches through pointers.

A few rules keep the LLVM output correct:

  • aggregates are copied as plain memory (a tagged union’s storage is an integer-and-bytes type, never one member’s struct, or another member’s bytes would be lost);
  • zero-filling a large aggregate uses memset;
  • struct layouts from C headers that Volt can only partly read come from libclang, as the C backend’s do.

Every hosted program is compiled with a small runtime: today the C prelude and runtime in runtime/. It is moving into Volt one function at a time. On bare metal (--target) there’s no C at all already: std/bare.volt has the runtime functions the compiler’s code calls (panics, bounds, printing, the heap) and the ones LLVM calls (memcpy, 64-bit division on 32-bit cores), and voltc’s start code (voltc/src/target.volt) replaces C’s crt0. A function in std marked @runtime("volt_print_f64") is std’s Volt version of that runtime function: both compilers call it wherever they’d call the C one, and both backends can inline it. The C version stays for programs built without std (--no-std).

Printing text goes to the runtime as bytes and a length (volt_put), with no printf format, and C strings and error names print through std’s volt_print_cstr. Format specs are in Volt too: padding counts UTF-8 characters, integers of any base and width (i128 included) are divided in 32-bit pieces, so 32-bit cores need no 128-bit division, and a float with a precision ({:.3}, {:.5e}) is written from its exact decimal expansion, a big integer for the whole part and one digit per multiplication by 10 for the fraction, rounded half to even as printf does. A test checks it against snprintf on random values. Printing numbers is in Volt (std/fmt.volt): integers by a digit loop, and floats by Ryu (Ulf Adams, PLDI 2018), the shortest text that reads back as the same value, found by exact integer arithmetic on the bits. Ryu replaced up to 17 rounds of snprintf and strtod per float (the print benchmark measures it). Its tables are generated with exact big-integer arithmetic. Reading floats is in Volt as well (std/text.volt, for parse_float and JSON numbers), in three tries, as Go’s strconv does: a number under 2^52 times or over a power of ten up to 10^22 is exact in f64 arithmetic (Clinger); otherwise the first 19 digits times a 128-bit power of ten, whose high bits are the answer unless the power’s error could reach the rounding bit (Eisel and Lemire); otherwise the digits as a big decimal, shifted by powers of two until 53 bits can be taken and rounded once. A test compares it with strtod, bit for bit, on random doubles’ texts and on the exact midpoints between neighbours. It’s about 9 ns a number for short ones and 23 ns for 17 digits, against glibc strtod’s 28 and 53.

Both backends produce the same symbols, layouts, error codes and calling conventions, so objects from one link with the other: the bootstrap check builds libraries with each backend and links them into programs built by the other.