OxL Reference

Step 7. Ownership and borrowing

Why#

In languages without a garbage collector the programmer is responsible for freeing memory; in languages with one you pay with pauses and unpredictability. OxL takes a third way: every value has exactly one owner; when the owner goes out of scope the value is destroyed. This single rule gives no leaks, no double frees and no data races between tasks - and it is all checked at compile time. There are no explicit lifetimes (as in Rust): the check is simpler (§7.2).

Three rules:

  1. Assignment, passing to a function and returning move the value (Copy types - numbers, str, bool - are simply copied).
  2. &x is a shared borrow (read only), &var x an exclusive one (read and write). While a &var exists there are no other references; while a & exists you cannot modify.
  3. x.clone() is an explicit independent copy.

Example#

oxl✓ checked in CI
fn total(xs: List<i32>) -> i32  pure {  // a by-value parameter: the function becomes the owner
  var s = 0
  for x in xs { s += x }
  s
}

fn count(xs: &List<i32>) -> usize  pure => xs.len()  // &T reads without taking ownership

fn addOne(xs: &var List<i32>) {  // &var T is an exclusive mutable borrow
  xs.push(1)
}

fn main() io {
  var xs = [10, 20, 30]
  print("{count(&xs)}")  // &xs lends the value; xs stays ours
  addOne(&var xs)  // &var must be written at the call site
  let copy = xs.clone()  // clone makes an independent copy
  print("{total(xs)}")  // xs is moved into total; it cannot be used afterwards
  print("{copy.len()} {copy}")
}
Output
3
61
4 [10, 20, 30, 1]

Walkthrough#

  • total(xs: List<i32>) - a by-value parameter: by calling total(xs) you gave away the list. Afterwards xs cannot be used.
  • count(xs: &List<i32>) and the call count(&xs) - the function only reads and takes nothing.
  • addOne(&var xs) - &var is written explicitly at the call site: the reader sees at once that the function may change the argument.
  • clone() is expensive (it copies the data), so it never happens implicitly.

Common mistakes#

Use after a move:

oxl✓ checked in CI
fn main() {
  let xs = [1, 2, 3]
  let ys = xs  // the value moved into ys
  print(xs.len())  // error: xs no longer owns the list
}
error
use-after-move

The diagnostic use-after-move also points at the place of the move. Fixes: pass &xs, make xs.clone() or return the passed value from the function.

Modification while reading:

oxl✓ checked in CI
fn main() {
  var xs = [1, 2, 3]
  let first = &xs
  xs.push(4)  // cannot mutate while &xs is alive
  print(first.len())
}
error
borrow-conflict

borrow-conflict: while first holds a borrow of the list, push is forbidden - otherwise the pointer could dangle after the memory is reallocated. A borrow lives until its last use, so it is often enough to swap lines. References cannot be stored in fields of data or passed to other tasks (borrow-escape): for shared data use freeze(x) - an immutable value that may be shared between tasks (§7.3).

Remember#

  • One owner; move by default; & to read, &var to modify, clone to copy.
  • Copy types (numbers, bool, str, bytes) do not fear moves.
  • Ownership errors are compile errors, not runtime crashes.

Next: effects.

See also

OxL 1.0.0-beta · Documentation version 1.0.0-beta© OxwaldLink · generated from the Standard, the std sources and the guides