C+
Ownership & safety · View as Markdown
v0.0.27 is a macOS / AppKit release. That is the supported path. Other platforms are not recommended; wait for a later version.

The borrow checker

The rule is one line: aliasing XOR mutability. At any point in the program, a place has either any number of shared borrows, or exactly one exclusive borrow, never both.

var v: vec::Vec[i32] = vec::new::[i32]();
v.push(1);
let n: usize = v.len();                     // shared borrow — fine
let p: i32 = vec::at_copy::[i32](v, 0 as usize);  // shared borrow — fine
v.push(2);                                  // exclusive — but no live shared borrow now; fine

Because shared borrows can read but not mutate, and the single exclusive borrow is the only one allowed to mutate, two threads or two code paths can never hold a writer and a reader of the same place at once. That is why data races do not compile.

The common diagnostics

  • E0372: move out of a borrowed value (including moving a Vec while a slice of it is live).
  • E0381: exclusive use of a place that is already borrowed (a field write, a ref argument, or a mutating method while a view or loan is live).
  • E0383: read while exclusively borrowed.
  • E0370 family: overlapping incompatible borrows.
  • E0513: a view of a dying owner escapes the function (returned directly, or inside a returned aggregate).
  • E0514: a value dies while a scope or promoted raw pointer still holds a view of it.
  • E0516: a view stored through a raw pointer (*p = v, or (*p).f = v) is undeclared flow at the raw seam.

Views are tracked by shape

A str or T[] is a borrow written as a value. The checker follows the shape, not a method-name allowlist, through every form it can leak:

  • Returning t.view(), as_str / as_slice, or any user method that returns str / T[] from a borrowing receiver is E0513 when the owner is a local.
  • A take parameter or take this is a dying root: a returned view of it is E0513.
  • return head(local) where head(x) -> str returns a view of its parameter is rooted at local.
  • let s = mk().view(); (a view of a statement-scoped temporary) is rejected; passing such a view as a direct call argument stays legal.
  • Storing a view of a frame-dying owner into a static or a ref out-parameter is rejected.
  • A view written into a field or index, produced by an if / block / match expression, or moved out by a struct destructure pins its owner (E0372 / E0381).
  • A slice of a Vec[T] (or any generic Drop type) pins it: moving it is E0372, append while the slice is live is E0381. A read-only method alongside a live view stays legal.
  • A raw-pointer-returning function whose computed return flow carries a view-capable parameter has that flow promoted to a return borrow, so the caller ties the pointer to the argument's owner. #[keeps(nothing)] opts a declared boundary out.

A mutating method call on a borrowed place was already refused; a plain field write, and passing the place as a ref argument, now are too.

The fix is almost always a scope boundary

When two borrows conflict, the fix is usually to end one before the other begins, by introducing a scope so they do not co-exist:

{
    let r: i32 = vec::at_copy::[i32](v, 0 as usize);
    #println(r);
}                            // shared borrow ends here
v.push(99);                  // exclusive borrow now fine

The borrow checker reasons within a function boundary. For the full picture of what it enforces versus what it trusts you to uphold (views and raw pointers), see Ownership.

Next

Continue with error handling.