cromo The Cromo Guide
PDF IT

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:

cromo
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 a

b 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.

cromo
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:

cromo
i32 main() {
    string first = "hello"
    string second = first
    ret 0
}
error: values of type string are not copied implicitly

There 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:

cromo
i32 main() {
    string first = "hello"
    string second = move first
    println("{}", first)
    ret 0
}
error: 'first' has been moved and can no longer be used

With copy(), the value is duplicated and both variables stay usable:

cromo
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:

cromo
list<string> names() {
    list<string> result = ["Ada", "Grace"]
    ret result
}

i32 main() {
    list<string> all = names()
    all.add("Alan")
    println("{}", all.len())
    ret 0
}
3

Nothing 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:

cromo
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:

ModeDeclarationCallThe function
readlist<i32> xsf(numbers)reads the value; the caller keeps it
modifyref list<i32> xsf(ref numbers)modifies the caller's value
takemove list<i32> xsf(move numbers)becomes the owner; the caller loses it

#Read

A parameter without a word is read-only. The function cannot modify it:

cromo
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-only

A 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:

cromo
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
}
2

Forgetting the ref in the call is an error, not a silent copy:

cromo
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 argument

ref is also how a function gives more than one result:

cromo
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:

cromo
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:

cromo
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 both

When 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 move or duplicated with copy().
  • A parameter is read, modified with ref, or taken with move, 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.