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.
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$ 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: EMPTYRead 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 orderOwnership 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 aRead 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 4Read 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 guideuse 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 guideMeasured 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 numberCalls 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.
- C
use { "stdio.h" } as c; - C++
use { "shapes.hpp" } as shapes; - Rust
use { "geom.rs" } as geom; - Zig
use { "fastmath.zig" } as fm;
- CC++Rust
- ZigGoPython
- JavaScriptTypeScriptLua
- RubyJavaKotlin
- C#DartSwift
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