Basics
Variables
Section titled “Variables”val declares a name whose value can’t change: it can’t be reassigned, and nothing changes it
through a reference either
(references to a val). var declares one
that can. The type comes from the value, or is written after a colon. A var declared without a
value starts at zero.
use std::io;
fn main() -> void { val answer = 42; // i32: an integer literal's default type val ratio = 0.5; // f64 var count: u8 = 1; count += 2; var buffer: u8[4]; // zeroed buffer[0] = count; std::println("{} {} {} {}", answer, ratio, count, buffer[0]);}// expect: 42 0.5 3 3Function parameters are vals too; fn f(var x: i32) gives the function its own mutable copy.
| Type | What it is |
|---|---|
i8 i16 i32 i64 i128, isize |
signed integers |
u8 u16 u32 u64 u128, usize |
unsigned integers (usize for sizes and indexes) |
f16 f32 f64 f128 |
floating point |
bool |
true or false |
str |
UTF-8 text: a pointer and a length, not null-terminated |
cstr |
a null-terminated C string (string literals convert to it) |
void, never |
no value; never returns (@panic, exit, an endless loop) |
T[N], T[] |
an array; T[] takes its length from the initializer |
T[..] |
a slice: a view into an array (pointer and length) |
(A, B) |
a tuple |
T? |
an optional |
E!T |
an error union |
T&, T* |
a reference and a pointer |
fn(A) -> R |
a function value |
type |
a type, as a value at compile time |
f128 is IEEE binary128. Most CPUs have no instructions for it, so its arithmetic is calls to
soft-float routines: the C runtime’s (libgcc’s or compiler-rt’s), and where there is none with them
(aarch64 macOS, bare metal and WebAssembly) Volt’s own std::softfloat, exported under the same
names. % is always
std::softfloat’s: the exact remainder, as C’s fmod gives it, on every target.
An f128 constant has all of binary128’s precision. A literal like 0.1 is read straight to the
nearest binary128, not through an f64, and val THIRD: f128 = 1.0 / 3.0; is folded at compile
time in binary128, rounding each step as the same operation at run time would.
Type aliases
Section titled “Type aliases”type name = T; gives a type another name. It’s the same type, not a new one: a meters is an
f64 wherever an f64 goes. An alias can be generic, taking its arguments where it’s used, and
in a package it’s the package’s own unless marked public, like any other declaration.
use std::io;
type meters = f64;<T: type>type list = std::vec<T>;
fn total(a: meters, b: meters) -> meters { return a + b;}
fn main() -> void { var xs: list<i32> = {}; xs.push(3); std::println("{} {}", total(1.5, 2.0), xs.len);}// expect: 3.5 1An alias can’t be defined in terms of itself (type a = b; type b = a; is an error).
Literals
Section titled “Literals”use std::io;
fn main() -> void { val big = 1_000_000; val mask = 0xFF; val bits = 0b1010; val perms = 0o755; val sci = 1.25e1; val letter = 'a'; // a u8 val text = "tab\there"; // escapes: \n \t \r \0 \\ \" \' \xNN \u{...} std::println("{} {} {} {} {} {}", big, mask, bits, perms, sci, letter); std::println("{}", text);}// expect: 1000000 255 10 493 12.5 97// expect: tab hereA literal takes the type its place wants. On its own, an integer is an i32 (an i64, then an
i128, when it doesn’t fit), a float an f64, and a character its code point: a u8 up to 255, a
u32 past it. Integer literals go up to u128’s maximum (2^128 - 1); one past i128’s maximum
needs a u128 place (val all: u128 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; or @cast<u128>(...)).
Multi-line and raw strings
Section titled “Multi-line and raw strings”""" starts a string on the next line, and a """ on a line of its own ends it. The closing
quotes’ indentation comes off every line, so the text sits indented with the code around it; the
lines are joined with \n, without one after the last (leave an empty line before the closing
quotes for that). Escapes work as in "...", and \""" puts three quotes in. r"..." and
r"""...""" are raw: a backslash is just a backslash.
use std::io;
fn main() -> void { val page = """ <ul> <li>one</li> </ul> """; std::println(page); std::println(r"C:\temp\new"); std::println(r""" \d+\.\d+ "quoted" """);}// expect: <ul>// expect: <li>one</li>// expect: </ul>// expect: C:\temp\new// expect: \d+\.\d+ "quoted"Operators
Section titled “Operators”The arithmetic, comparison, logical and bitwise operators are C’s. What’s different:
- Integer overflow panics in debug builds and wraps in
--releasebuilds (dividing by zero panics in debug builds too: see Safety).+%,-%and*%always wrap, for hashes and checksums; on two literals they wrap in the type the value gets (val b: u8 = 134 *% 115;is 50). - A shift by at least the type’s width (
x >> 64on au64) panics in debug builds;--releasebuilds shift by the amount modulo the width. x++andx--are statements, not expressions.&,|and^bind tighter than comparisons:x & 1 == 1is(x & 1) == 1, not C’sx & (1 == 1).asbinds tighter than arithmetic:n as i64 * 2convertsn, then multiplies.- A value converts by itself where nothing can be lost: an integer to a wider one of the same
signedness (
u32tou64,i8toi32), an unsigned one to a wider signed one (u32toi64),usizeandu64either way on a 64-bit target (andisizeandi64),u32andusizeeither way on a 32-bit one (usizeis as wide as a pointer), an integer to a float that holds all its values (i32oru32tof64,u16tof32), and a float to a wider one (f32tof64). Soxs.lenpasses as au64, andn * 1.5with ani32nis anf64. x as Tconverts only when nothing can be lost (widening an integer or a float, aboolto an integer,T&toT*orvoid*; any integer to a float, rounding when it must, and a plain enum to an integer); anything else is a compile error.@cast<T>(x)converts anything to anything, unchecked: narrowing (u64tou32) and changing sign take it.a ?? bis the value of optionala, orbwhen it’s null.==and!=on a struct (or an enum with payloads) call itseq(other): one it attaches, likestd::string’s (attach operator ==is another way to write it), or a derived one. A type without one can’t be compared. Structs and enums get the other operators the same way: see Operators.eqtakes one side by reference, so both sides can’t be temporaries (make() == make(): store one in a variable first). Arrays, tuples and slices don’t take==; a slice’seq(&other)compares it element by element.a..banda..=bare ranges (exclusive and inclusive).- Operands and arguments are evaluated left to right.
place = valueevaluates the value first, then the place;place += valueevaluates the place first, since it reads it. - A statement that only computes a value (
x + 1;,a == b;) is an error, since it does nothing. When it looks like a missing=, the message says so:x + 1;suggests+=,a == b;suggests=.
use std::io;
fn main() -> void { val small: u8 = 200; val wide = small as i32; // fine: every u8 fits in an i32 val back = @cast<u8>(wide + 100); // unchecked: 300 becomes 44 val hash: u32 = 4000000000 *% 3; // wraps on purpose std::println("{} {} {}", wide, back, hash > 0);}// expect: 200 44 trueControl flow
Section titled “Control flow”if and while take a bool in parentheses (or an optional or a pointer, which they check for
null and narrow), and their bodies always have braces (an
if without them is a value, below).
use std::io;
fn main() -> void { var n = 27; var steps = 0; while (n != 1) { if (n % 2 == 0) { n /= 2; } else { n = 3 * n + 1; } steps++; } std::println("{}", steps);}// expect: 111if (val v = e) and while (val x = e) bind the value of an optional or an error union when it
has one; see Binding in if and while.
if as a value
Section titled “if as a value”Without braces, if (c) a else b is an expression: its value is a when c holds, else b. The
else is required, else if chains, and the arms are expressions, as a
match’s are; they take their type from where the value goes.
A condition narrows an optional in the arm as it does in a statement if, and the binding forms work
too. An arm can also leave (else return 0), or be a block that leaves (else { ...; return; }).
use std::io;
fn sign(x: i32) -> str { return if (x < 0) "negative" else if (x == 0) "zero" else "positive";}
fn main() -> void { val name: str? = "volt"; val len = if (val n = name) n.len else 0; val carry: u32 = if (len > 3) 1 else 0; std::println("{} {} {}", sign(-5), len, carry);}// expect: negative 4 1Choosing a value this way compiles to a branch-free select where it can, as C’s ?: does; an if
statement that assigns in both branches may compile to a jump instead, which costs more when the
condition is hard to predict.
for walks a range, an array, a slice or a str (its bytes). A second name gets the index.
for (x&) binds each element by reference instead of copying it, so the loop can change it.
use std::io;
fn main() -> void { var scores: i32[] = { 70, 85, 90 }; for (s&) in scores { *s += 5; } for (s, i) in scores { std::print("{}:{} ", i, s); } std::println(""); for (i) in 0..=3 { std::print("{} ", i); } std::println("");}// expect: 0:75 1:90 2:95// expect: 0 1 2 3Iterators
Section titled “Iterators”A type that attaches next(this: T&) -> X? is an iterator: for calls next until it returns
null, binding each value. next can return a pointer, X*, instead: each element then binds as
an X&, so the loop can change what it points at, and collections iterate without copying. An iterator in a var is advanced by the loop; a val one is copied
first, and a temporary (like m.iter()) lives until the loop ends. The index, labels, break, continue and accumulators
work as they do for arrays.
use std::io;
struct countdown { n: i32;}
attach fn next(this: countdown&) -> i32? { if (this.n == 0) { return null; } this.n -= 1; return this.n + 1;}
fn main() -> void { val c: countdown = { n: 3 }; for (x, i) in c { std::print("{}{}", x, i); } std::println("");}// expect: 302112loop, labels and break values
Section titled “loop, labels and break values”loop repeats until a break. break can carry a value, which makes loop and labelled blocks
expressions. A label is :name before a loop or a block; break :name and continue :name use it.
use std::io;
fn main() -> void { var x = 1; val first = loop { if (x * x > 50) { break x; } x++; }; val sign = :pick { if (first < 0) { break :pick "negative"; } break :pick "positive"; }; :rows for (r) in 0..3 { for (c) in 0..3 { if (c > r) { continue :rows; } std::print("{}{} ", r, c); } } std::println("| {} {}", first, sign);}// expect: 00 10 11 20 21 22 | 8 positiveA for can also build a value: after the iterable, [ var name: T = start ] declares an
accumulator, and the loop’s value is what it holds at the end.
use std::io;
fn main() -> void { val sum = for (v) in 1..=10 [ var acc: i32 = 0 ] { acc += v; }; std::println("{}", sum);}// expect: 55=> expr after the iterable (before any accumulator) replaces each element with a value worked
out from it and the index, before the body runs:
use std::io;
fn main() -> void { val words: str[] = { "volt", "is", "fast" }; val letters = for (w) in words => w.len [ var n: usize = 0 ] { n += w; }; std::println("{}", letters);}// expect: 10Arrays, slices and ranges
Section titled “Arrays, slices and ranges”An array’s length is part of its type, and an array type reads left to right: u8[2][3] is three
u8[2]s. Slicing an array (a[1..3], a[1..] and a[..3] to an end, or a[..] for all of it)
gives a T[..] that points into it without copying. Indexing is bounds-checked, in --release
builds too (as a trap).
use std::io;
fn sum(xs: i32[..]) -> i32 { var total = 0; for (x) in xs { total += x; } return total;}
fn main() -> void { val primes: i32[] = { 2, 3, 5, 7, 11 }; val middle = primes[1..4]; val evens: i32[] = 0..5; // a range fills an array: 0 1 2 3 4 std::println("{} {} {} {}", primes.len, sum(primes[..]), sum(middle), evens[4]);}// expect: 5 28 15 4{} is an array of zeros (what a var without an initializer holds), and {x; n} is n copies of
x, which runs once; n is a constant, the array’s length. Both work wherever an array is
expected, struct field defaults and compile time included:
use std::io;
struct frame { data: u8[16] = {}; tag: u8[2] = { 0xFF; 2 };}
fn main() -> void { val f: frame = {}; val ones: i32[] = { 1; 4 }; std::println("{} {} {}", f.data[15], f.tag[1], ones.len);}// expect: 0 255 4Tuples
Section titled “Tuples”Tuples group values without declaring a struct. Destructure them with val (a, b) = ..., or reach
into them with .0, .1. Their elements can have names.
use std::io;
fn min_max(xs: i32[..]) -> (i32, i32) { var lo = xs[0]; var hi = xs[0]; for (x) in xs { if (x < lo) { lo = x; } if (x > hi) { hi = x; } } return (lo, hi);}
fn main() -> void { val data: i32[] = { 4, -2, 9 }; val (lo, hi) = min_max(data[..]); val point: (x: i32, y: i32) = (3, 4); std::println("{} {} {} {}", lo, hi, point.x, point.1);}// expect: -2 9 3 4