On this page
Values that own, values that do not Borrowing: pointers and viewsown: taking a value on purpose
Scope exit
Reference counting: ref class
The rules on one card
Ownership
Every value in a Nexium program has exactly one owner, and the owner frees it when its scope ends. That one sentence replaces a garbage collector, and most of what a borrow checker does. This chapter is the rules, with the compiler's messages for the cases it rejects.
struct Token { kind: u8, text: String }
/// Parameters are borrowed: the caller keeps `words`.
fn longest(words: *List(String)) -> usize {
var best: usize = 0
for w in words.*{ if w.len > best { best = w.len } }
return best
}
/// `*mut` lets a function change what it is handed.
fn shout(words: *mut List(String)) {
for i in 0..words.len {
var up = String.new()
for c in words[i] { up.push_byte(if c >= 'a' and c <= 'z' { c - 32 } else { c }) }
words[i] = up
}
}
/// `own` takes the value: the caller gives it up, no copy is made.
fn make_token(kind: u8, own text: String) -> Token {
return Token{ .kind = kind, .text = text }
}
/// A reference-counted object: copies share it, the last one frees it.
ref class Node { value: i32, next: ?Node, back: ?weak Node }
fn main() {
// owning values move; the old name is unusable afterwards
var a = List(String).new()
a.append(String.from("ridge"))
a.append(String.from("col"))
let b = a // `a` is moved into `b`
println("{} words", .{b.len}) // fine; using `a` here would not compile
// a clone is a real copy
var c = b.clone()
c.append(String.from("summit"))
println("{} and {}", .{b.len, c.len})
// borrowing: pass a pointer, keep ownership
println("longest: {}", .{longest(&c)})
shout(&mut c)
println("{}", .{c[2]})
// views: a slice looks into storage it does not own
let text = String.from("base camp")
let word = text[0..4]
println("{} of {}", .{word, text})
// loop variables and `if let` bindings are views too: clone to keep one
var kept = List(String).new()
for w in c { kept.append(w.clone()) }
println("kept {}", .{kept.len})
// `own` parameters move the argument in
let name = String.from("ident")
let tok = make_token(1, name) // `name` is moved; `tok.text` owns it now
println("token {} {}", .{tok.kind, tok.text})
// reference counting
let first = Node{ .value = 1, .next = null, .back = null }
let second = Node{ .value = 2, .next = null, .back = @weak (first) }
first.next = second
let alias = first // the same object, count 2
println("{} -> {}", .{alias.value, alias.next.?.value})
if let w = second.back { if let up = w.upgrade() { println("back to {}", .{up.value}) } }
println("refs {}", .{@refCount(first)})
}
2 words
2 and 3
longest: 6
SUMMIT
base of base camp
kept 3
token 1 ident
1 -> 2
back to 1
refs 2
Values that own, values that do not#
List(T), String and Map(K, V) own a heap buffer. So does a struct that contains one, a tuple that contains one, an enum case that carries one. Everything else, numbers, bool, char, arrays of numbers, structs of numbers, is a plain value that is copied when assigned and owns nothing beyond its own bytes.
For an owning value, assignment is a move:
let b = a // `a` is moved into `b`
After this line b owns the list and a is a name that refers to nothing. Using a again is a compile error, and the message says so:
fn main() {
var names = List(String).new()
names.append(String.from("col"))
let moved = names
println("{} {}", .{moved.len, names.len})
}
error: use of `names` after it was moved
--> topo/code/ownership_fails.nx:5:35
|
5| println("{} {}", .{moved.len, names.len})
| ^
1 error(s)
The same happens when an owning value is passed by value to a function that takes own, stored into a field, appended to a list or put into a map: the value goes there, and the name that held it is done. Moves are tracked per branch: a value moved inside one if branch is still available in the other and counts as moved after the if.
When you need two, say so: b.clone() is a deep copy of the whole thing. Nothing is copied behind your back; every allocation in a program is one you can point at.
Borrowing: pointers and views#
A function should not have to own what it only wants to read. Parameters are borrowed: passing c to fn longest(words: *List(String)) lends it a pointer, the function reads through it, and the caller still owns c afterwards. A borrowed parameter cannot be moved out of, and cannot be changed unless the pointer is *mut:
fn shout(words: *mut List(String)) { // may change the list; the caller keeps it
&c makes a *List(String); &mut c makes a *mut List(String) and needs c to be a var. Reading a field or calling a method through either needs no special syntax; assigning a whole new value through a *mut T is p.* = value.
A slice is the other kind of borrow: text[0..4] is a view of four bytes of text, a pointer and a length, and it owns nothing. Slices are what almost every function takes ([]u8 for text, []T for a sequence), because an array, a List, a String and a piece of any of them all turn into one for free. Two rules keep views honest:
- A view into a local may not be returned from the function (rule R1): the local dies when the function returns and the view would point at freed memory. The compiler says:
fn first_word(text: []u8) -> []u8 {
let copy = String.from(text)
return copy[0..4]
}
fn main() { println("{}", .{first_word("base camp")}) }
error: this returns a slice into `copy`, a local that is released when the function returns (region rule R1)
--> topo/code/ownership_fails2.nx:3:5
|
3| return copy[0..4]
| ^
1 error(s)
- A loop variable in
for w in c, and the binding ofif let v = optover a stored optional, are views of the element: you may read them and clone them, not move them out. The message names the fix.
Views into parameters may be returned, because the caller owns their storage. Views stored into variables that outlive their storage, or kept across a List growing, are not caught by the 1.0 compiler; the roadmap's 1.2 closes those cases, and until then they are the one place where the programmer, not the compiler, keeps the promise. A debug build fills freed memory with a fixed byte so such a mistake fails loudly rather than quietly.
own: taking a value on purpose#
fn make_token(kind: u8, own text: String) -> Token {
return Token{ .kind = kind, .text = text }
}
own on a parameter says the function takes the value: the caller's name is moved, no copy is made, and the function may move it on (into the struct here) or let it drop when it returns. It is the right tool for constructors and for anything that stores what it is given. A function with an own parameter cannot be used as a function value, since the type of a function value does not say who owns the argument.
Scope exit#
An owning value is freed when the scope that owns it ends, in reverse order of declaration, and that is the only thing that happens automatically at scope exit. There are no destructors to write; defer (chapter 6) is for the actions that are not memory. Moving a value out of a scope (returning it, storing it) hands the duty to the new owner. If you want to see it, nx leaks program.nx runs a debug build that counts every allocation and reports what was still alive at exit; for every program in this book the answer is none.
Reference counting: ref class#
Sometimes a value really is shared: a node with several parents, a cache every part of the program reads. For those, ref class:
ref class Node { value: i32, next: ?Node, back: ?weak Node }
A ref class value is a reference to an object with a count. Copying it (let alias = first) retains, the count goes up, and the object is freed when the last reference is gone: deterministic, immediate, no tracing collector pausing anything. @refCount(x) reads the count. Fields are assigned through any reference (first.next = second), which is what makes the type a class and not a struct.
The one thing counting cannot do is a cycle. Two objects that point at each other keep each other alive forever, so a back edge is a weak reference: @weak(first) (or first.weak()) does not count, and w.upgrade() gives an ?Node that is null once the object is gone. nx leaks reports the cycles you forgot.
The effect system (chapter 15) tracks reference counting like everything else: a function that copies a reference has the refcounts effect, and a function that promises !refcounts cannot.
The rules on one card#
| you write | what happens |
|---|---|
let b = a (owning) | move; a is gone |
let b = a.clone() | deep copy; both live |
f(a) with fn f(x: List(T)) | borrow; a stays yours, f can only read |
f(&mut a) with fn f(x: *mut List(T)) | borrow; f may change it |
f(a) with fn f(own x: List(T)) | move; f keeps it |
a[i..j], a[..], for x in a | a view; do not outlive a, do not move out |
let r = obj (ref class) | retain; freed with the last reference |
@weak(obj) | no retain; upgrade() when you need it |
Next: collections.