The Loup Native Book — contents
  1. Chapter 1: Getting started
  2. Chapter 2: Native mode overview
  3. Chapter 3: Variables and numerics
  4. Chapter 4: Functions
  5. Chapter 5: Control flow
  6. Chapter 6: Collections
  7. Chapter 7: Structs
  8. Chapter 8: Enums and pattern matching
  9. Chapter 9: Classes and ARC
  10. Chapter 10: Interfaces
  11. Chapter 11: Generics
  12. Chapter 12: Error handling
  13. Chapter 13: Modules and packages
  14. Chapter 14: Template fences
  15. Chapter 15: Ownership verbs
  16. Chapter 16: borrow, excl borrow and noescape
  17. Chapter 17: unique ownership
  18. Chapter 18: Lifetimes and regions
  19. Chapter 19: @unsafe and calling C
  20. Chapter 20: Concurrency with OS threads
  21. Chapter 21: Async on native
  22. Chapter 22: Desktop UI
  23. Chapter 23: SIMD and GPU
  24. Chapter 24: Building and distributing
  25. Chapter 25: Memory safety patterns

Chapter 17: unique ownership

Reference counting lets any number of bindings share a class instance. That flexibility has two costs: a count in every object and an atomic increment or decrement whenever a reference is copied or dropped. For values that only ever have one owner — a connection, a file, a buffer being filled, a job moving through a queue — unique removes both. A unique value has exactly one owner at a time, the compiler tracks every move, and a native binary keeps no reference count for it.

This chapter covers unique let and unique var bindings, unique class, how unique values move through functions, loops, collections and closures, consume for handing a value over to ARC, and move-only structs. The normative text is the Language Reference §3-2.4.7 and §1-8.11.

Where unique is available:

Feature Modes
unique let / unique var bindings, unique var struct fields every mode
unique class host.native and freestanding (E_UNIQUE_CLASS_ARC_BINDING elsewhere, chapter 2)
consume (downgrade to ARC) host.native and host.dual (E_CONSUME_NOT_AVAILABLE elsewhere)

unique let and moves

unique let binds a class instance to a single owner. Assigning it to another binding moves it: the new binding owns the instance and the old one can no longer be used.

@mode(host.native)

class Connection {
    pub let addr: string;
    new(addr: string) { this.addr = addr; }
}

func describe(c: Connection): string {
    return i"connection to {$ c.addr $}";
}

func main(): void {
    unique let primary = Connection("db1:5432");
    println(describe(primary));     # a reads borrow: primary is still ours
    unique let current = primary;   # move
    println(describe(current));
}

Listing 17-1: A unique let binding, borrowed and then moved

connection to db1:5432
connection to db1:5432

Passing primary to describe does not move it: a parameter without a verb is a reads borrow (chapter 15), and the caller keeps ownership. Only an assignment, a takes argument, an insertion into a collection, or a closure capture moves a unique value.

Mistake: using a value after moving it

This code does not compile
@mode(host.native)

class Connection {
    pub let addr: string;
    new(addr: string) { this.addr = addr; }
}

func main(): void {
    unique let primary = Connection("db1:5432");
    let current = primary;
    println(current.addr);
    println(primary.addr);
}
main.lphc:12:13: error[E_USE_AFTER_MOVE]: E_USE_AFTER_MOVE: binding 'primary' was moved and cannot be used again — `unique class` instances and move-only structs have single ownership

Note that current is a plain let, and the assignment still moves: a binding initialized from a unique binding is itself unique (ruling UNQ-2). The only way to get an ordinary shared reference out of a unique value is the explicit consume, covered below. After the move, use current.

unique var is the reassignable form. Assigning a fresh value replaces the old one, which a native binary frees on the spot:

func main(): void {
    unique var conn = Connection("db1:5432");
    println(conn.addr);
    conn = Connection("db2:5432");    # the first connection is released here
    println(conn.addr);
}

Listing 17-2: Replacing the value of a unique var

db1:5432
db2:5432

Note. A unique let binding of an ordinary class is still treated as an ARC receiver when you call a modifier method on it, so the call needs a safe { } block or a safe func (chapter 15). A unique class instance, below, does not.

unique class

A unique class can only exist in single-owner form. Every binding of one is unique, whether or not it says so, and there is no reference-counted version of it. Use it for types that must never be shared: file handles, locks, device mappings, builders that are filled and then handed off.

@mode(host.native)

unique class Batch {
    var items: list[int] = [];

    pub modifier func add(x: int): void {
        this.items.push(x);
    }

    pub func total(): int {
        var sum = 0;
        for (x in this.items) {
            sum = sum + x;
        }
        return sum;
    }

    pub func count(): int {
        return this.items.length();
    }
}

func fill(edits b: Batch, n: int): void {
    for (i in 1..n + 1) {
        b.add(i * 10);
    }
}

func report(b: Batch): void {
    println(i"{$ b.count() $} items, total {$ b.total() $}");
}

func submit(takes b: Batch): int {
    return b.total();
}

func main(): void {
    let batch = Batch();
    batch.add(5);
    fill(edits: batch, 3);
    report(batch);
    let sent = submit(takes: batch);
    println(i"submitted {$ sent $}");
}

Listing 17-3: A unique class passed by reads, edits and takes

4 items, total 65
submitted 65

Three things differ from an ordinary class:

  • batch.add(5) needs no safe { } block. The compiler knows batch is the only reference, so nothing else can be changing the object at the same time.
  • fill changes the batch through edits without being a safe func.
  • submit(takes: batch) is a real move. In a native binary nothing is counted; ownership passes to submit, which frees the batch when it returns.

Mistake: using a value after takes

This code does not compile
@mode(host.native)

unique class Batch {
    pub var size: int = 0;
}

func submit(takes b: Batch): int {
    return b.size;
}

func main(): void {
    let batch = Batch();
    let sent = submit(takes: batch);
    println(i"{$ sent $} sent, {$ batch.size $} left");
}
main.lphc:14:35: error[E_USE_AFTER_TAKE]: E_USE_AFTER_TAKE: binding 'batch' was consumed by a `takes` parameter and cannot be used again

Read what you need before the call (let left = batch.size;). Since Loup 1.24 only takes consumes; reads, edits and shares arguments leave the binding usable.

Moves inside loops

A move inside a loop body happens on every iteration, so the second iteration would use a value the first one already gave away:

This code does not compile
@mode(host.native)

unique class Job {
    pub let id: int;
    new(id: int) { this.id = id; }
}

func run(takes j: Job): void {
    println(i"running job {$ j.id $}");
}

func main(): void {
    let job = Job(1);
    for (round in 0..3) {
        run(takes: job);
    }
}
main.lphc:15:20: error[E_USE_AFTER_TAKE]: E_USE_AFTER_TAKE: binding 'job' was consumed by a `takes` parameter in a previous iteration of the loop and cannot be used again

The message says which iteration is the problem (ruling LOOPMV-1). Either create the value inside the loop, or give a var a fresh value on every path back to the top of the loop:

func main(): void {
    var job = Job(1);
    for (next in 2..4) {
        run(takes: job);
        job = Job(next);     # re-initialized before the next iteration
    }
    run(takes: job);
}

Listing 17-4: Re-initializing a moved var inside the loop

running job 1
running job 2
running job 3

del and finalize

del ends a unique value's life at once: it runs the class's finalize() method and frees the instance, and the binding cannot be used afterwards.

unique class TempFile {
    pub let path: string;

    new(path: string) {
        this.path = path;
        println(i"create {$ path $}");
    }

    func finalize(): void {
        println(i"remove {$ this.path $}");
    }
}

func main(): void {
    let scratch = TempFile("/tmp/build-42");
    println(i"writing to {$ scratch.path $}");
    del scratch;
    println("done");
}

Listing 17-5: del runs finalize() immediately

create /tmp/build-42
writing to /tmp/build-42
remove /tmp/build-42
done

Using the binding after del is E_USE_AFTER_DEL: use of binding 'scratch' after del— the binding is uninitialized once it has been passed todel (§1-6.11).

Note. In a native binary, a unique value that goes out of scope without del is finalized and freed at the end of its scope. loup run does not do this: the interpreter calls finalize() only for an explicit del. Use del where cleanup must happen at a known point on every delivery path. Chapter 9 covers finalize() and del for ordinary classes.

consume: handing a value over to ARC

Sometimes a value starts with one owner and later needs to be shared — a connection that is configured exclusively and then placed in a pool. consume converts a unique binding into an ordinary reference-counted reference. It is one-way: after it, the compiler no longer tracks uniqueness.

class Connection {
    pub let addr: string;
    pub var retries: int = 0;
    new(addr: string) { this.addr = addr; }
}

safe func configure(edits c: Connection): void {
    c.retries = 3;
}

func main(): void {
    unique let conn = Connection("db1:5432");
    configure(edits: conn);                   # exclusive setup
    let pooled: Connection = consume conn;    # now an ordinary ARC reference
    let alias = pooled;                       # sharing is allowed again
    println(i"{$ alias.addr $} retries={$ pooled.retries $}");
}

Listing 17-6: Configuring a value exclusively, then sharing it

db1:5432 retries=3

Mistake: a reserved word as the binding name

The specification's own consume example names its result shared. That does not compile, because shared is a reserved word (it declares shared class, chapter 20):

This code does not compile
@mode(host.native)

class Connection {
    pub let addr: string;
    new(addr: string) { this.addr = addr; }
}

func main(): void {
    unique let conn = Connection("db1:5432");
    let shared: Connection = consume conn;
}
main.lphc:10:9: error[E0001]: `shared` is a reserved word (§1-3.4) and cannot name a binding
  note: rename the binding — e.g. `shared_value`

local and template are reserved for the same kind of reason. Pick another name, as Listing 17-6 does.

Mistakes with consume

consume only applies to a unique binding of an ordinary class:

This code does not compile
@mode(host.native)

class Connection {
    pub let addr: string;
    new(addr: string) { this.addr = addr; }
}

func main(): void {
    let conn = Connection("db1:5432");
    let pooled: Connection = consume conn;
}
main.lphc:10:30: error[E_CONSUME_NON_UNIQUE]: E_CONSUME_NON_UNIQUE: 'consume' can only be applied to 'unique' bindings, but 'conn' is not unique

conn is already an ordinary reference, so there is nothing to convert: drop the consume. The opposite case, a unique class, cannot be converted at all, because no reference-counted form of the class exists:

This code does not compile
@mode(host.native)

unique class Lock {
    pub let name: string;
    new(name: string) { this.name = name; }
}

func main(): void {
    let lock = Lock("db");
    let pooled = consume lock;
}
main.lphc:10:18: error[E_UNIQUE_CLASS_CONSUME]: E_UNIQUE_CLASS_CONSUME: `consume` on unique class binding 'Lock' is not allowed — unique class instances cannot be downgraded to ARC
main.lphc:10:18: error[E_CONSUME_NON_UNIQUE]: E_CONSUME_NON_UNIQUE: 'consume' can only be applied to 'unique' bindings, but 'lock' is not unique

The first message is the real one. If a type must sometimes be shared, make it an ordinary class and put unique on the bindings that need it.

Known gap. The specification says that using a binding after consume is E_USE_AFTER_MOVE. Loup 1.25.1 does not report it: println(conn.addr) after consume conn compiles. Treat consume as the binding's last use.

Move-only structs

A struct field may be declared unique var. The struct then owns that value, and the whole struct becomes move-only: assigning it moves it instead of copying it, because a copy would give the unique value two owners.

@mode(host.native)

class Handle {
    pub let fd: int;
    new(fd: int) { this.fd = fd; }
}

struct Resource {
    name: string;
    unique var handle: Handle;
}

func reopen(edits r: Resource, takes h: Handle): void {
    r.handle = h;           # the old handle is released
}

func close(takes r: Resource): int {
    return r.handle.fd;
}

func main(): void {
    var res = Resource(name = "db", handle = Handle(3));
    let fresh = Handle(7);
    reopen(edits: res, takes: fresh);
    println(i"{$ res.name $} -> fd {$ res.handle.fd $}");
    println(i"closed fd {$ close(takes: res) $}");
}

Listing 17-7: A move-only struct with a unique var field

db -> fd 7
closed fd 7

Mistake: copying a move-only struct

This code does not compile
@mode(host.native)

class Handle {
    pub let fd: int;
    new(fd: int) { this.fd = fd; }
}

struct Resource {
    name: string;
    unique var handle: Handle;
}

func main(): void {
    var primary = Resource(name = "db", handle = Handle(3));
    var backup = primary;
    println(primary.name);
}
main.lphc:16:13: error[E_USE_AFTER_MOVE]: E_USE_AFTER_MOVE: binding 'primary' was moved and cannot be used again — `unique class` instances and move-only structs have single ownership

An ordinary struct would have been copied. This one moved, because it holds a unique field. Build a second resource with its own handle if you need two.

Mistake: moving a field out of a borrowed struct

A function that receives a move-only struct as a borrow may read its fields but not move them out:

This code does not compile
@mode(host.native)

unique class Socket {
    pub let port: int;
    new(port: int) { this.port = port; }
}

struct Pending {
    name: string;
    unique var socket: Socket;
}

func accept(p: Pending): Socket {
    println(i"accepting {$ p.name $}");
    return p.socket;
}

func main(): void {
    let p = Pending(name = "web", socket = Socket(8080));
    let s = accept(p);
    println(s.port);
}
main.lphc:15:12: error[E_MOVE_OUT_OF_BORROW]: E_MOVE_OUT_OF_BORROW: cannot move `p.socket` out of the borrowed parameter `p` — only a `takes` parameter owns its value (§1-8.11); declare it `takes p`

accept hands the socket to its caller, so it must own the struct (ruling MOVEB-1). Declare func accept(takes p: Pending): Socket and call it as accept(takes: p); the program then prints accepting web and 8080.

The field rules, all checked:

  • a unique field must be unique var (E_UNIQUE_FIELD_LET for unique let);
  • unique applies only to class types (E_UNIQUE_NOT_CLASS: 'unique' can only be applied to class types, not 'int');
  • an ordinary class may not have a unique field, because the class itself can be shared (E_UNIQUE_CLASS_FIELD); a unique class may.

⚠ Planned. consume r.handle, downgrading one field of a move-only struct while keeping the rest, is described in earlier editions but not implemented: Loup 1.25.1 reports E_CONSUME_NON_UNIQUE for it.

Collections of unique values

A list can hold unique values (ruling UNIQCOLL-1). The collection becomes the owner, and the compiler tracks what goes in and comes out:

  • inserting (push, an element assignment, a list literal) moves the value in;
  • pop and remove move a value out;
  • indexing, get, first, last and a for loop variable borrow an element in place.
@mode(host.native)

unique class Job {
    pub let id: int;
    new(id: int) { this.id = id; }
}

func main(): void {
    var queue: list[Job] = [];
    queue.push(Job(1));
    queue.push(Job(2));
    queue.push(Job(3));
    for (j in queue) {
        println(i"queued {$ j.id $}");
    }
    println(i"next up: {$ queue[0].id $}");
    let last = queue.pop();
    println(i"took job {$ last.id $}, {$ queue.length() $} left");
}

Listing 17-8: A queue that owns its jobs

queued 1
queued 2
queued 3
next up: 1
took job 3, 2 left

Taking an element out by indexing, or copying the collection, would leave two owners, so all of these are refused:

This code does not compile
@mode(host.native)

unique class Job {
    pub let id: int;
    new(id: int) { this.id = id; }
}

func main(): void {
    var queue: list[Job] = [Job(1), Job(2)];
    let first = queue[0];
    let copy = queue.clone();
    let head = queue[0:1];
}
main.lphc:10:17: error[E_MOVE_OUT_OF_BORROW]: E_MOVE_OUT_OF_BORROW: cannot move `queue[…]` out of the collection `queue` — an element accessed in place is a borrow (§1-8.11); move it out with `remove` or `pop`
main.lphc:11:16: error[E_MOVE_OUT_OF_BORROW]: E_MOVE_OUT_OF_BORROW: cannot move the elements of `queue` out through `clone()` — a collection holding a `unique` value is move-only (§1-8.11); move elements out with `remove` or `pop`, iterate with `for` (a borrow)
main.lphc:12:16: error[E_MOVE_OUT_OF_BORROW]: E_MOVE_OUT_OF_BORROW: cannot move the elements of `queue` out through a slice — a collection holding a `unique` value is move-only (§1-8.11); move elements out with `remove` or `pop`, iterate with `for` (a borrow)

Each message names the replacement. Use queue.remove(0) to take the first job out, and read fields in place (queue[0].id) when you only need to look. Inserting a value is a move too: after queue.push(job), using job is E_USE_AFTER_TAKE. A collection holding unique values is itself move-only, so let other = queue; moves the whole queue.

Closures capture by moving

A closure that uses a unique value captures it by moving it in (ruling CAPMV-1). The closure body only borrows it, so the closure can be called many times, but the enclosing function cannot use the value afterwards:

This code does not compile
@mode(host.native)

unique class Logger {
    pub let name: string;
    new(name: string) { this.name = name; }
    pub func log(msg: string): void { println(i"[{$ this.name $}] {$ msg $}"); }
}

func main(): void {
    let logger = Logger("app");
    let report = func(msg: string): void {
        logger.log(msg);
    };
    report("started");
    logger.log("finished");
}
main.lphc:15:5: error[E_USE_AFTER_MOVE]: E_USE_AFTER_MOVE: binding 'logger' was moved and cannot be used again — `unique class` instances and move-only structs have single ownership

The capture is a move even though the closure never leaves main, and even when the closure is passed straight to a function. Route every use through the closure, or keep the closure's work separate from the value's other uses:

func main(): void {
    let logger = Logger("app");
    let report = func(msg: string): void {
        logger.log(msg);
    };
    report("started");
    report("finished");
}

Listing 17-9: After the capture, the closure owns the logger

[app] started
[app] finished

A closure may not capture a unique value that the function only borrows: capturing parameter l of func useIt(l: Logger) is E_MOVE_OUT_OF_BORROW, with the same advice to declare it takes l.

Moving a unique value into a thread works the same way, with one more rule: the captured type must be Sendable (chapter 20). A unique class that declares implements Sendable can be moved into a Thread.spawn closure, and using it afterwards is E_USE_AFTER_MOVE.

When to use unique

Reach for unique when a value has one owner at every moment and you want the compiler to prove it: resources with a finalize, buffers being built, jobs moving through a pipeline. Keep ordinary classes for data that many parts of the program read, for object graphs, and for anything you store in several places. Mixing is normal: unique while a value is being set up or handed along, consume at the point where it becomes shared.

Error codes

Code Meaning
E_USE_AFTER_MOVE a binding used after it was moved by assignment or closure capture
E_USE_AFTER_TAKE a binding used after a takes argument or an insertion into a collection, including "in a previous iteration of the loop"
E_USE_AFTER_DEL a binding used after del
E_MOVE_OUT_OF_BORROW moving a unique value out of a borrowed parameter, a collection element or a slice/clone
E_CONSUME_NON_UNIQUE consume of a binding that is not unique
E_UNIQUE_CLASS_CONSUME consume of a unique class instance
E_CONSUME_NOT_AVAILABLE consume outside host.native and host.dual
E_UNIQUE_CLASS_ARC_BINDING unique class outside host.native and freestanding
E_UNIQUE_NOT_CLASS unique on a non-class type
E_UNIQUE_FIELD_LET a unique let field
E_UNIQUE_CLASS_FIELD a unique field in a class that is not a unique class

E_UNIQUE_NOT_NATIVE, which older editions listed, was removed in v1.17: unique bindings work in every mode.

Summary

  • unique let / unique var give a class instance a single owner; assignment, takes, insertion into a collection and closure capture move it, and a later use is an error.
  • A unique class has no shared form; its modifier methods need no safe context, and it is available in host.native and freestanding.
  • Moves inside loops, moves out of borrowed parameters and moves out of collection elements are all caught (E_USE_AFTER_TAKE "in a previous iteration", E_MOVE_OUT_OF_BORROW).
  • consume converts a unique binding of an ordinary class into an ARC reference, one way; using the binding afterwards is not yet reported.
  • A struct with a unique var field is move-only; collections of unique values are move-only too.
  • del runs finalize() at once; under loup run that is the only time finalize() runs.
  • Chapter 18 introduces lifetimes, which let borrows outlive a single call.