Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Ownership and borrowing

Vx has no garbage collector and no mandatory reference counting. Memory is managed by ownership, checked at compile time, in the same family as Rust’s model.

Owning and borrowing

A binding owns its value. Passing it to a function by value moves it, and the original binding is no longer usable:

#![allow(unused)]
fn main() {
let v = make_buffer();
consume(v);      // v is moved
// reading v here is an error
}

Borrow instead of moving to keep the original alive:

#![allow(unused)]
fn main() {
fn total(v : &Vec<i32>) -> i32 { /* ... */ }
fn append(v : &mut Vec<i32>, x : i32) { /* ... */ }
}

&T is a shared borrow, &mut T is an exclusive one. The usual rule applies: any number of shared borrows, or exactly one exclusive borrow, never both at once. The checker tracks variance and regions, so a borrow cannot outlive what it points into.

Linear values

Some values are linear: they must be consumed exactly once, and the checker enforces it. Device buffers are the motivating case. A buffer that has been handed off to an accelerator has left your control, and reading it again is a use-after-move — reported as a compile error rather than as corrupted data.

This is stronger than an ordinary move check. A linear value cannot be quietly dropped either, because dropping a device allocation without releasing it is a leak the runtime cannot detect for you.

Boxing

Recursive types must be boxed. Box<T> is a heap allocation with a single owner:

#![allow(unused)]
fn main() {
import std::box;

struct Node {
    value: i32,
    next: Box<Node>,
}
}

The requirement is not an oversight — it is what lets the compiler give every nominal type a size without solving a fixpoint across module boundaries, which in turn is what lets the frontend compile modules in parallel with no shared state.

Raw pointers

*const T and *mut T are raw pointers, and they opt out of all of the above. Because of that, the operations that can go wrong with them require unsafe:

  • dereferencing one
  • indexing through one
  • reading a field through one
  • calling an unsafe fn

A function that takes a caller’s raw pointer and dereferences it is itself declared unsafe fn, so the obligation is visible in the signature rather than buried in the body.

unsafe fn read_first(p : *const i32) -> i32 {
    return *p;
}

fn main() -> i32 {
    let x : i32 = 42;
    unsafe {
        return read_first(&x);
    }
}

Keep unsafe blocks small. The point of the annotation is that the region a human has to verify by hand is written down and searchable.