Safety checks
Debug builds (the default) check the mistakes C leaves undefined, and stop the program with a message and exit code 101:
| Check | Example |
|---|---|
| integer overflow | i32 max + 1 |
| division by zero | x / y or x % y with y 0 |
| shifting too far | x << n on an i32 with n 32 or more |
| out-of-bounds indexing | xs[10] on a 3-element array |
| unwrapping null | *p on a null pointer, .value on an empty optional |
| unwrapping an error | .value on an error union holding an error |
| reading another variant’s payload | s.CIRCLE when s holds a SQUARE |
| double free | deleting the same memory twice through a bad @cast, @read or C code |
| freeing with the wrong size | a.free<T>(p, n) with another n than the block was allocated with |
| resuming a finished frame | resume f after the async fn returned, or await f twice |
@panic |
always |
use std::io;// exit: 101
fn main() -> void { val xs: i32[] = { 1, 2, 3 }; var i = 0; while (true) { std::println(xs[i]); // traps when i reaches 3 i++; }}The message names the place: app.volt:8:22: panic: index 3 out of bounds (len 3).
Release builds
Section titled “Release builds”--release (bolt: --release or a profile with optimize = true) builds with optimization and
without most of these checks: integer arithmetic wraps, a division by zero isn’t caught, and a
shift by at least the type’s width shifts by the amount modulo the width. What would read memory as
the wrong thing stays checked: indexing and slicing, reading a payload of the variant a value
doesn’t hold, and .value on a null optional or an error union holding an error. Each is one
compare and a trap instruction: no call and no message, so a bare-metal build still makes no hidden
calls. A failed check stops the program with a signal (SIGILL on x86-64, SIGTRAP on arm64), and
output still buffered is lost, as with C’s abort; run the debug build to see where. A finished
frame resumed or awaited again, and @panic, still stop with their message. A function with
@attributes([@unchecked]) has no bounds checks in its body, for a hot loop known to stay in range.
Code that wants wrapping in every mode uses +%, -% and *%.
The optimizer removes a check it can prove passes, such as xs[i] in for (i) in 0..xs.len over a
slice or array (that loop then vectorizes as it would in C), so most loops pay nothing. Over the
benchmarks, the checks release keeps cost under 3%
(geomean, LLVM backend), paid by code that indexes with values it computes: a bytecode interpreter’s
stack pointer costs it the most, 1.7x its unchecked time. std skips the check in a few internals
where the index is in range by construction: sifting a heap, filling a vec in resize, and
writing a number’s digits.
--leak-check makes a debug build count allocations: if any is still live when the program ends,
it exits with code 102.
Freed memory
Section titled “Freed memory”A debug build’s allocator fills a freed block with 0xDD bytes and keeps it from being reused
while it is among the last 65536 blocks or 64 MiB freed, so a view into storage that moved or was
freed reads 0xDD bytes, not some newer value’s. When a block leaves that quarantine its bytes are
checked: one written through a stale view panics with write after free (exit 101), and
--leak-check checks the blocks still held when the program ends. A block bigger than the whole
quarantine is freed at once. Release builds free memory straight away and check none of this.
What the compiler checks
Section titled “What the compiler checks”Some mistakes never compile: using a moved value, moving inside a loop without a new value before
the next pass, copying a type with a delete hook but no copy hook, a match that misses a case,
a null T&, writing through a reference to a
val, a statement that computes a value
and drops it (x == 1;; a call’s result may be dropped), and format strings that don’t match their
arguments.
What the compiler warns about
Section titled “What the compiler warns about”Some mistakes with references are warnings: the program still builds, and the warning says what to change. Volt has no lifetimes to write and no borrow errors; it looks at each function on its own.
-
A view used after its container changed. A slice,
str, reference or iterator a method gave out of a local container (xs.items(),name.as_str(),m.get(k)) looks into the container’s storage. A call that may move or free that storage (push,insert,reserve,append,clear,removeand the like, marked@invalidatesin std) leaves the view looking at what may be gone:val s = xs.items();xs.push(2) catch @panic("out of memory");std::println(s.len); // warning: 's' looks into 'xs', whose items may have moved or been// freed since: get 's' again after the changeA read on any path after the change warns, a loop’s next pass included. Changing a container inside a
forloop over its items (for (y) in xs.items(),this.a.b.items(), or straight over a C++ container,for (y) in this.v) warns at the change, a new value for it or for a container it’s a field of (this.a = ...) included: the loop goes on over the old items. A loop over a copy, a range or an array (read through its place each round) doesn’t. The container may be a local, a parameter or a field of one (this.items.items()): a change tothis.items, or tothisas a whole, reaches the views into it, and one tothis.bleaves a view ofthis.aalone. Giving the container a new value (xs = ys,this.items = ...,*this = other) is a change too when the old value had something to delete; a field written (xs.n = 2), plain data given a new value and a reference pointed elsewhere aren’t. A closure held in a local and called by its name is followed both ways: one that captured a view warns where it’s called after the view’s container changed, and one whose body changes a container it captured (|xs&| () { xs.push(1) ... }) makes that change at each call. What isn’t followed: the same container reached through another name (val r = &this.items; r.push(1)), and a closure stored in a field, returned, passed to a function or captured by another closure that calls it. -
A reference to a local, returned in a value. Returning
&nitself, or a literal holding it, is an error. Returning a local that holds a reference to one of the function’s own locals (a struct with{ x: &n }, av.x = &n, a pointerval r = &n) warns: what it refers to is gone once the function returns.