Ownership
Volt has no garbage collector. Every value has one owner (a variable, a field, an element), and when the owner’s scope ends the value is deleted: the compiler inserts the call. Deletes run in reverse order of declaration, and a struct’s fields are deleted after the struct’s own hook, last field first. Assigning a new value deletes the one it replaces.
The delete hook
Section titled “The delete hook”A type that holds a resource attaches fn delete(this: T&). It runs automatically, exactly once;
calling it by hand is an error.
use std::io;
struct file { name: str;}
attach fn delete(this: file&) -> void { std::println("closing {}", this.name);}
fn main() -> void { val a: file = { name: "a.txt" }; { val b: file = { name: "b.txt" }; std::println("inner scope ends"); } std::println("main ends");}// expect: inner scope ends// expect: closing b.txt// expect: main ends// expect: closing a.txtTypes made of owned parts (a struct with a std::vec field, say) need no hook: each field is
deleted when the struct is.
Temporaries
Section titled “Temporaries”A value nobody keeps, like the result of make("a") below, is a temporary. It’s deleted at the end
of the statement it’s in, so make("a").view() can be used anywhere in that statement, even when
the result points into it (word().as_str() as an argument). In a val or var’s initializer,
it lives as long as the variable instead, and so does one in a break value that the initializer’s
block gives. A defer’s statement keeps its own until it’s done.
use std::io;
struct note { text: str;}
attach fn delete(this: note&) -> void { std::println("delete {}", this.text);}
attach fn view(this: note&) -> str { return this.text;}
fn make(t: str) -> note { return { text: t };}
fn main() -> void { std::println("{}", make("a").view()); // deleted after this statement val kept = make("b").view(); // deleted when kept goes out of scope std::println("{} still here", kept);}// expect: a// expect: delete a// expect: b still here// expect: delete bUsing an owned value by value (passing it, returning it, assigning it) moves it: the new place
owns it, and the old one is no longer usable. move x says so explicitly, but it’s never needed to
return a local or parameter: return out; hands out to the caller without copying it. The compiler tracks moves
through branches and loops, and using a moved value is a compile error:
use std::io;
fn consume(v: std::vec<i32>) -> void {}
fn main() -> void { val v: std::vec<i32> = {}; consume(v); std::println("{}", v.len);}// error: 'v' was moved earlierA variable moved inside a loop needs a new value before the loop goes around, or the next pass
would find nothing there: val c = move a; a = move b; b = c; is fine in a loop, eat(move a); on its
own is an error. Values that own nothing, like numbers and plain structs, are copied instead of moved.
Only a whole variable moves. An owned field, element, or value behind a reference can’t move out
of its place (the error says to copy it); a container hands an element out through a method,
such as a vec’s pop or remove.
To lend a value without giving it up, pass a reference:
use std::io;
fn total(v: std::vec<i32>&) -> i32 { // borrows: the caller keeps v var sum = 0; for (x) in v.items() { sum += x; } return sum;}
fn main() -> !void { var v: std::vec<i32> = {}; try v.push(2); try v.push(3); std::println("{} {}", total(&v), v.len);}// expect: 5 2A reference never owns anything: nothing is deleted through a T&.
Copies
Section titled “Copies”copy x makes a second, independent value. For a type with a delete hook, copying needs a
copy hook too, or both copies would delete the same resource:
use std::io;
struct token { id: i32;}
attach fn delete(this: token&) -> void { std::println("revoke {}", this.id);}
attach fn copy(this: token&) -> token { return { id: this.id + 100 };}
fn main() -> void { val a: token = { id: 1 }; val b = copy a; std::println("{} {}", a.id, b.id);}// expect: 1 101// expect: revoke 101// expect: revoke 1A copy hook can only read this. std’s containers attach copy, so copy v on a vec copies
its elements, and copy b on a box (below) is a new box holding a copy of the value.
box: owning heap memory
Section titled “box: owning heap memory”std::mem::box<T> owns one value on the heap. T::new(value) makes one (allocation can fail, so
it’s an error union); a box is used like a T&, and deleting the box deletes the value and frees
the memory.
use std::io;
struct node { value: i32; next: std::mem::box<node>?;}
fn sum(n: node&) -> i32 { if (n.next) { // narrows the field: a box, used like a node& return n.value + sum(n.next); } return n.value;}
fn main() -> !void { val tail = try node::new({ value: 3, next: null }); val head: node = { value: 1, next: try node::new({ value: 2, next: tail }) }; std::println("{}", sum(&head));}// expect: 6box is an ordinary std type: @attributes([@owns("ptr")]) is what makes a struct an owning
pointer, and a library can make its own.
A box<T>? is as small as the box: a live box’s pointer is never null, so null means none, and
node above is 16 bytes, like the C struct with a next pointer. That holds for any @owns
struct whose other fields take no space, such as a box using the default allocator. A box from an
arena carries the arena’s pointer, so its optional adds a flag. (An allocator that returned null
for a successful allocation would make that box read as none.)
Deletes handle memory and anything with a hook. For other cleanup (unlocking, logging, closing a C
handle) use defer, which runs when the scope ends, and errdefer, which runs only when it ends by
returning an error. See Errors.
Checks
Section titled “Checks”Debug builds stop the program on a double free. --leak-check makes a debug build exit with
code 102 if any allocation was never freed:
voltc run app.volt --leak-check