Skip to content
volt
GitHub

Volt

A systems language that shows its work.

Volt has no runtime. It gives you ownership instead of a garbage collector, errors as values, templates and compile-time code, and it compiles to C you can read or to native code through LLVM.

main.volt
use std::io;

struct rect {
    w: f64;
    h: f64;
}

fn area(r: rect) -> f64 {
    return r.w * r.h;
}

fn main() -> void {
    val r: rect = { w: 3.0, h: 4.5 };
    std::println("area {}", area(r));
}
struct v_rect {
    double w;
    double h;
};

// area (main.volt:8)
static double v_area(v_rect r) {
    return r.w * r.h;
}
define internal double @v_area(double %0, double %1) #0 {
  %3 = alloca { double, double }, align 8
  %4 = alloca double, align 8
  br label %5

5:                                                ; preds = %2
  store double %0, ptr %3, align 1
  %6 = getelementptr inbounds nuw i8, ptr %3, i64 8
  store double %1, ptr %6, align 1
  %7 = getelementptr inbounds nuw { double, double }, ptr %3, i32 0, i32 0
  %8 = load double, ptr %7, align 8
  %9 = getelementptr inbounds nuw { double, double }, ptr %3, i32 0, i32 1
  %10 = load double, ptr %9, align 8
  %11 = fmul double %8, %10
  ret double %11
terminal
$ voltc run main.volt
area 13.5

Nothing runs underneath

Volt programs carry no machinery you didn't ask for. What a line does is what it says, and the C it becomes is there to read.

  • Garbage collectornone
  • Exceptions and unwindingnone
  • Virtual tablesnone
  • Runtime type informationnone
  • Hidden allocationsnone

Read some Volt

Every program here is real: the site's tests compile and run each one and check what it prints.

use std::io;

error parse_error { EMPTY, BAD_DIGIT }

// E!T: an error from set E, or a T. No exceptions, no hidden control flow
fn parse(s: str) -> parse_error!i32 {
    if (s.len == 0) {
        return parse_error::EMPTY;
    }
    var n = 0;
    for (ch) in s {
        if (ch < '0' || ch > '9') {
            return parse_error::BAD_DIGIT;
        }
        n = n * 10 + @cast<i32>(ch - '0');
    }
    return n;
}

fn main() -> !void {
    val a = try parse("42");            // try passes an error up
    val b = parse("4x") catch -1;       // catch handles it
    val c: i32? = null;                 // T?: a value, or null
    std::println("{} {} {}", a, b, c ?? 7);
    val d = parse("") catch |e| {
        std::println("failed: {}", e);
        return;                         // leave, or give a value
    };
    std::println("{}", d);
}

Errors are values

A function that can fail returns E!T: an error from the set E, or a T. try passes an error up, catch handles it. Optionals (T?) say a value may be missing, and ?? gives a fallback. Nothing is thrown, nothing unwinds.

42 -1 7
failed: EMPTY
Read the guide
use std::io;

struct buffer {
    name: str;
    data: std::vec<u8>;             // owned: deleted with the buffer
}

// runs when a buffer's owner goes out of scope; never called by hand
attach fn delete(this: buffer&) -> void {
    std::println("freeing {}", this.name);
}

// `copy x` calls this: deep copies are explicit
attach fn copy(this: buffer&) -> buffer {
    return { name: "copy", data: copy this.data };
}

fn fill(var b: buffer) -> buffer {  // takes ownership, gives it back
    b.data.push(7) catch @panic("out of memory");
    return b;
}

fn main() -> void {
    val a: buffer = { name: "a", data: {} };
    val b = fill(a);                // `a` moved: using it now is a compile error
    val c = copy b;                 // a deep copy: a second owner
    std::println("{} {}", b.data.len, c.data.len);
}                                   // c, then b: deleted in reverse order

Ownership without a collector

Every value has one owner and is deleted when that owner's scope ends, in reverse order. Passing a value moves it (using it afterwards is a compile error); copy makes a second owner.

1 1
freeing copy
freeing a
Read the guide
use std::io;

// templates, like C++: one copy per type, checked when instantiated
<T: type>
fn largest(xs: T[..]) -> T {
    var best = xs[0];
    for (x) in xs {
        if (x > best) {
            best = x;
        }
    }
    return best;
}

// traits are constraints; as a type, a tagged union of every type
// that attaches them. No vtables, no heap
trait shape {
    fn area(this) -> f64;
}

struct circle { r: f64; }
struct square { side: f64; }

attach shape -> circle {
    fn area(this) -> f64 { return 3.14159 * this.r * this.r; }
}

attach shape -> square {
    fn area(this) -> f64 { return this.side * this.side; }
}

fn main() -> void {
    val xs: i32[] = { 3, 9, 4 };
    std::println("{}", largest(xs[..]));
    val c: circle = { r: 1.0 };
    val q: square = { side: 2.0 };
    val shapes: shape[] = { c, q };
    for (s&) in shapes {
        std::println("{}", s.area());
    }
}

Templates and closed traits

Generic functions and structs are templates: one copy per set of arguments. A trait used as a type is a tagged union of every type that attaches it, so a call is a switch, never a vtable.

9
3.14159
4
Read the guide
use std::io;

// runs while compiling: the result is a constant in the program
comptime fn fib(n: i32) -> i32 {
    var a = 0;
    var b = 1;
    for (i) in 0..n {
        val next = a + b;
        a = b;
        b = next;
    }
    return a;
}

// a pack of types: the loop unrolls, each arg has its own type
<Args: type...>
fn show(args: Args...) -> void {
    comptime for (arg) in args {
        std::print("{}:{} ", @typeinfo(@typeof(arg)).short_name, arg);
    }
    std::println("");
}

fn main() -> void {
    val table: i32 = fib(20);
    show(table, 2.5, true, "volt");
}

Code that runs while compiling

comptime functions, comptime if, for and match, types as values and @typeinfo. Here fib runs in the compiler, and a pack of arguments unrolls into one print per type.

i32:6765 f64:2.5 bool:true str:volt
Read the guide
use std::io;
use { "geometry.hpp" } as cpp;       // C++, by the extension: libclang reads it
use { "math.h" } as c;               // C headers too

fn main() -> void {
    var path = cpp::geo::Path::new();  // a constructor
    path.add({ x: 0.0, y: 0.0 });      // methods, C++ layouts
    path.add({ x: 3.0, y: 4.0 });
    val len = path.length();
    val top = cpp::geo::clamp<i32>(12, 0, 10);  // a function template
    std::println("{} {} {}", len, top, c::floor(2.7));
}

C and C++ headers, imported

A use line reads real C headers, and C++ ones through libclang: classes with their layouts, constructors, methods, overloads and templates. Rust, Zig, Swift, Kotlin, Go, Java, C#, Python, TypeScript and JavaScript code imports the same way.

5 10 2
Read the guide

Code that writes code

A comptime function can build a type and declare the functions that go with it. Each of these replaces code you'd otherwise keep in step by hand, and the site's tests run every one.

A struct of arrays
One vector per field of any struct, and the push that fills them.
An enum and its name table
One list of names gives the enum, name() and parse().
Getters and an argument parser
A get_ for each field, and --name value read into a struct by each field's type.
A struct from a schema file
@embed reads a JSON schema while compiling, and the struct comes from it.
Register blocks from a spec
Volatile reads and writes for each of a device's registers, from names and offsets.
A state machine from a table
The states and next(event) from a table of moves.
Code from the fns attached to any type
Fns attached to i32 or a library's types, and a dispatcher from whatever cmd_ fns a type has; one call attaches 1560.

Measured against C

Each program runs in C, C++, Rust and Zig, and in Volt through both backends. A bar is Volt's time as a share of the C program's, and the line marks C. Where Volt is faster, it's for a reason you can read: free lists in the default allocator, a radix sort for integers, Ryu for printing floats.

The programs, the machine and every number
ProgramC backendLLVM
bigint0.85×0.84×
binary_trees0.47×0.40×
btree0.97×0.96×
closures1.12×0.98×
crc321.01×1.01×
csv0.47×0.57×
dijkstra0.96×0.95×
errors0.97×0.99×
fannkuch1.07×1.07×
fasta1.01×1.01×
fft1.01×1.12×
fib1.00×0.97×
hashmap0.86×0.85×
heap0.88×0.88×
huffman0.76×0.75×
json0.74×0.68×
knucleotide0.95×0.95×
levenshtein0.79×0.80×
lexer0.85×0.82×
life1.01×1.11×
lru_cache0.92×0.94×
lz771.15×1.13×
mandelbrot1.00×1.00×
matmul1.03×1.04×
nbody1.00×1.02×
nqueens1.02×1.01×
print0.05×0.05×
raytrace0.99×0.88×
revcomp1.07×1.21×
sha2561.04×1.00×
shapes0.72×0.75×
sieve1.01×1.00×
sort0.09×0.11×
spectral_norm0.97×0.98×
strings0.88×0.85×
sudoku1.06×1.02×
vec_grow1.05×0.97×
vm_interp0.87×1.05×
wordfreq1.08×1.09×

Calls everything, and everything calls it

A use line reads another language's code like a header; the extension says which language. And a Volt library builds as a shared library, with bindings bolt writes for each of these.

  • Cuse { "stdio.h" } as c;
  • C++use { "shapes.hpp" } as shapes;
  • Rustuse { "geom.rs" } as geom;
  • Ziguse { "fastmath.zig" } as fm;
volt
  • CC++Rust
  • ZigGoPython
  • JavaScriptTypeScriptLua
  • RubyJavaKotlin
  • C#DartSwift
A runnable example for each language

What's in it

No runtime
No collector, no scheduler, no vtables, no exceptions. A Volt program is the code you wrote and the standard library you called.
Two backends
Readable C that any C compiler builds, or native code through LLVM. A library from one links with a program from the other.
Checked
Out-of-bounds indexing, null unwraps and reading the wrong variant stop the program in every build: with a message and the line in debug, a trap in release. Debug builds check overflow and double frees too.
Ownership
Deletes run at scope end, moves are tracked, copies are explicit, and box<T> owns heap memory. No annotations.
Comptime
Run code in the compiler, pass types around as values, build types and the functions on them, and read any type's shape with @typeinfo.
Async
Stackless frames of known size, driven with async, resume and await. No heap and no executor.
bolt
The build tool: packages, workspaces, git dependencies, features, profiles, tests, benchmarks and a profiler.
Tooling
A language server built on the compiler, a VS Code extension, and diagnostics that show the code and suggest the fix.
Self-hosted
voltc is written in Volt and builds itself, byte for byte, through both backends.
Bare metal
--target builds for a microcontroller with no C compiler and no C library: start code from voltc, std's runtime in Volt.

Write your first program

Build the compiler, then let bolt make a package and run it.

$ bolt new hello && cd hello && bolt run
     Created binary (application) package `hello`
   Compiling std (~/volt/std)
   Compiling hello v0.1.0 (bin "hello")
    Finished `dev` profile [unoptimized] target(s) in 0.66s
     Running `target/debug/hello`
hello from hello