Ownership
Ownership is how Cromo manages memory and every other resource without a garbage collector and without pointers. The rules are few, the compiler checks them, and once learned they make the cost of every line visible.
#Every value has one owner
A value belongs to the variable, the field or the element of a list that holds it: its owner. When the owner ends, the value ends with it. For a local variable, that happens at the end of the block where it is declared, in the reverse order of the declarations.
A struct can say what to do when it ends, with a destructor: a method named ~ followed by the name of the struct. The program below uses one to show when each value ends:
struct Log {
string name
Log(move string label) {
name = move label
println("open {}", name)
}
~Log() {
println("close {}", name)
}
}
void inner() {
Log b = Log("b")
println("inner")
}
i32 main() {
Log a = Log("a")
inner()
Log d = Log("d")
println("end of main")
ret 0
}open a
open b
inner
close b
open d
end of main
close d
close ab ends when inner returns; d and a at the end of main, the last declared first. This is RAII, as in C++: a struct that owns a file closes it in its destructor, one that owns a connection closes the connection, and the code that uses them cannot forget it, even when an exception goes through. A string and a list free their memory in the same way.
#Simple values are copied
Numbers, bool and the structs made only of them are simple types: they are copied, as in C.
struct Point {
f64 x
f64 y
}
i32 main() {
Point p
p.x = 1.5
Point q = p
q.x = 9.0
println("{} {}", p.x, q.x)
ret 0
}1.5 9#Values that own resources are moved
A string, a list, a map and the structs that contain them, or that have a destructor, own something that a copy would have to duplicate. Cromo never duplicates it without saying so. Assigning one to another variable does not compile:
i32 main() {
string first = "hello"
string second = first
ret 0
}error: values of type string are not copied implicitlyThere are two ways to say what you mean. With move, the value passes to the new owner, without copying anything, and the old variable can no longer be used:
i32 main() {
string first = "hello"
string second = move first
println("{}", first)
ret 0
}error: 'first' has been moved and can no longer be usedWith copy(), the value is duplicated and both variables stay usable:
i32 main() {
string first = "hello"
string second = first.copy()
second += "!"
println("{} {}", first, second)
ret 0
}hello hello!A struct has its copy() generated by the compiler, when all its fields can be copied and it has no destructor of its own.
The compiler follows every path of the function: a variable moved only in one branch of an if cannot be used after the if either.
#Temporaries and returned values
A value without a name, as the result of a call or a literal, is moved without writing move. So is a local variable returned with ret:
list<string> names() {
list<string> result = ["Ada", "Grace"]
ret result
}
i32 main() {
list<string> all = names()
all.add("Alan")
println("{}", all.len())
ret 0
}3Nothing is copied here: the list built in names becomes all.
#Moving a field
Moving a field, or an element of a list, leaves the default value in its place, so the struct stays usable:
struct Message {
string text
i32 priority
}
i32 main() {
Message m
m.text = "urgent"
string taken = move m.text
println("[{}] [{}]", taken, m.text)
ret 0
}[urgent] []#Passing values to functions
A parameter is passed in one of three ways, and the call repeats the word of the declaration, so that reading a call is enough to know what happens to its arguments:
| Mode | Declaration | Call | The function |
|---|---|---|---|
| read | list<i32> xs | f(numbers) | reads the value; the caller keeps it |
| modify | ref list<i32> xs | f(ref numbers) | modifies the caller's value |
| take | move list<i32> xs | f(move numbers) | becomes the owner; the caller loses it |
#Read
A parameter without a word is read-only. The function cannot modify it:
void addOne(list<i32> numbers) {
numbers.add(1)
}
i32 main() {
list<i32> values
addOne(values)
ret 0
}error: cannot call 'add', which is not const
parameter 'numbers' cannot be modified: parameters are read-onlyA read parameter is never copied: a large value is passed by its address, a small one, up to 16 bytes, by copy, because that is faster. The compiler chooses, and it can, because the value cannot change during the call. In C++ terms, a read parameter is a const T& that you never have to write.
#Modify: ref
With ref, the function works on the caller's value. The call writes ref too:
void addOne(ref list<i32> numbers) {
numbers.add(1)
}
i32 main() {
list<i32> values
addOne(ref values)
addOne(ref values)
println("{}", values.len())
ret 0
}2Forgetting the ref in the call is an error, not a silent copy:
void addOne(ref list<i32> numbers) {
numbers.add(1)
}
i32 main() {
list<i32> values
addOne(values)
ret 0
}error: parameter 'numbers' is 'ref': write 'ref' before the argumentref is also how a function gives more than one result:
bool find(list<string> items, string wanted, ref i64 position) {
for (i64 i = 0; i < items.len(); i++) {
if (items[i] == wanted) {
position = i
ret true
}
}
ret false
}
i32 main() {
list<string> planets = ["Mercury", "Venus", "Earth"]
i64 at
if (find(planets, "Earth", ref at)) {
println("found at {}", at)
}
ret 0
}found at 2#Take: move
A move parameter receives the ownership of the value. The function can keep it, for example in a list, without copying it:
struct Library {
list<string> titles
void add(move string title) {
titles.add(move title)
}
}
i32 main() {
Library library
string book = "The C Programming Language"
library.add(move book)
library.add("Structure and Interpretation of Computer Programs")
println("{} books", library.titles.len())
ret 0
}2 books#References live only during a call
ref exists only for parameters. There are no references in variables, in fields or in returned values: a reference cannot outlive the object it refers to, because it ends with the call. This single rule replaces the lifetimes and the borrow checker of other languages.
When a program needs to point to another object, as in a graph, it keeps an index into a list instead of a pointer.
#Exclusivity
During a call, an object passed with ref cannot be reached through any other argument of the same call. Otherwise a function could see a value change under its feet:
void swap(ref i32 a, ref i32 b) {
i32 t = a
a = b
b = t
}
i32 main() {
i32 x = 1
swap(ref x, ref x)
ret 0
}error: 'x' overlaps 'x' in the same call, and the call can modify bothWhen the compiler cannot know, as in swap(ref v[i], ref v[j]), the program checks it while it runs, and a call with i equal to j is a panic. The same rule allows the compiler to tell LLVM that two ref parameters never overlap, which makes the code faster, as restrict in C.
#const
A const variable cannot be modified, and const is deep: the strings and lists inside a const value are const too. A method marked const does not modify its object, as chapter 5 shows; only const methods can be called on a read parameter.
#Summary
- Every value has one owner, and ends with it: destructors release the resources.
- Simple values are copied; the others are moved with
moveor duplicated withcopy(). - A parameter is read, modified with
ref, or taken withmove, and the call says which. - References exist only during a call: no lifetimes to write, no dangling pointers.
- Two arguments of a call that modifies them are never the same object.