The Loup Freestanding Book — contents
  1. Chapter 1: What Is Freestanding Mode?
  2. Chapter 2: Building a Bare-Metal Binary
  3. Chapter 3: Integer Types and Wrapping Arithmetic
  4. Chapter 4: Functions and Control Flow
  5. Chapter 5: Structs and Layout Control
  6. Chapter 6: Enums and Pattern Matching
  7. Chapter 7: Classes and Allocators
  8. Chapter 8: Interfaces and Generics
  9. Chapter 9: Error Handling with Result
  10. Chapter 10: Ownership Verbs
  11. Chapter 11: Borrow and Lifetimes
  12. Chapter 12: Unique Ownership
  13. Chapter 13: Memory Primitives
  14. Chapter 14: Inline Assembly
  15. Chapter 15: MMIO with Volatile
  16. Chapter 16: Interrupts and Naked Functions
  17. Chapter 17: Entry Points and Panic
  18. Chapter 18: Sections and the Linker
  19. Chapter 19: Multicore and Atomics
  20. Chapter 20: The Available Standard Library Subset
  21. Chapter 21: Example — x86_64 Kernel Stub
  22. Chapter 22: Testing and Deploying Firmware
  23. Chapter 23: Memory Safety and Kernel Development Patterns
  24. Chapter 24: synchronized Classes and Lock Ordering

Chapter 12: Unique Ownership

A freestanding image has no reference counts, so "who owns this object, and when is it done?" must be answered at compile time. unique is how Loup answers it: a unique value has exactly one owner at a time, ownership moves on assignment, and the compiler rejects every use of a binding whose value has moved on (Language Reference §3-2.4.7). Moves cost nothing at run time — there is no count to adjust.

This chapter covers unique class and unique bindings, moves, takes, moving out of borrows, loops, closures, structs that hold unique values, and del.

unique class and unique bindings

A unique class is a class whose instances always have a single owner. A plain class can also be held uniquely by declaring the binding unique let or unique var:

@mode(freestanding)

unique class PageFrame {
    pub var number: uint64;
    new(number: uint64) { this.number = number; }

    reads func address(): uint64 {
        return this.number * 4096u64
    }
}

class Mailbox {
    pub var pending: uint32 = 0;
}

func setUp(): uint64 {
    let frame = PageFrame(0x42u64)       # unique because the class is
    unique let box = Mailbox()           # unique because the binding is
    unique var scratch = Mailbox()
    scratch = Mailbox()                  # a unique var may be rebound
    return frame.address() + (box.pending as uint64) + (scratch.pending as uint64)
}

Listing 12-1: A unique class and unique bindings

unique applies to class values only. Structs, enums and primitives are values already — each copy is independent — so unique let p = Point(x = 1) is E_UNIQUE_NOT_CLASS: 'unique' can only be applied to class types, not 'Point'.

Moves

Assigning a unique value to another binding moves it; the old binding is dead:

This code does not compile
@mode(freestanding)

unique class PageFrame {
    pub var number: uint64;
    new(number: uint64) { this.number = number; }
}

func mapKernelPage(): uint64 {
    let frame = PageFrame(0x42u64)
    let mapped = frame                  # meant: "look at the same frame"
    return frame.number * 4096u64
}
error[E_USE_AFTER_MOVE]: E_USE_AFTER_MOVE: binding 'frame' was moved and cannot be used again — `unique class` instances and move-only structs have single ownership
  --> src/main.lpfree:11:12
   |
11 |     return frame.number * 4096u64
   |            ^^^^^
   |

In a hosted mode let mapped = frame would make a second reference to the same object. A unique object cannot have two, so the assignment transfers ownership to mapped. If you only need to look at the frame, borrow it instead — pass it to a reads parameter, which leaves the caller's binding intact:

func frameAddress(frame: PageFrame): uint64 {   # an implicit `reads` borrow
    return frame.number * 4096u64
}

func clear(edits frame: PageFrame): void {      # an exclusive borrow
    frame.number = 0u64
}

func mapKernelPage(): uint64 {
    let frame = PageFrame(0x42u64)
    let address = frameAddress(frame)           # borrowed, still ours
    clear(edits: frame)                         # borrowed exclusively, still ours
    return address + frame.number
}

Listing 12-2: Borrowing a unique value leaves the owner in place

Because a unique value has no aliases, an edits borrow of it needs no run-time or compile-time alias search: exclusivity is already proven.

takes — moving into a function

A takes parameter receives ownership; the call site writes takes: and the caller's binding is consumed (chapter 10). Returning a unique value moves it out to the caller. Together they express hand-offs precisely:

var nextFree: uint64 = 0x100
var freed: uint64 = 0

func allocFrame(): PageFrame {
    let f = PageFrame(nextFree)
    nextFree = nextFree + 1u64
    return f                                   # ownership moves to the caller
}

func freeFrame(takes f: PageFrame): void {
    freed = freed + 1u64                       # the allocator owns it now
}

func cycle(): uint64 {
    let f = allocFrame()
    let number = f.number
    freeFrame(takes: f)
    return number
}

Listing 12-3: Ownership flowing out of an allocator and back

Moves inside loops

A move inside a loop body happens again on the next iteration, so the checker reports the second iteration's use (ruling LOOPMV-1):

This code does not compile
@mode(freestanding)

unique class PageFrame {
    pub var number: uint64;
    new(number: uint64) { this.number = number; }
}

var freeFrames: uint64 = 0

func freeFrame(takes f: PageFrame): void {
    freeFrames = freeFrames + 1u64
}

func freeRange(count: int): void {
    let frame = PageFrame(0x100u64)
    var i = 0
    while (i < count) {
        freeFrame(takes: frame)
        i = i + 1
    }
}
error[E_USE_AFTER_TAKE]: E_USE_AFTER_TAKE: binding 'frame' was consumed by a `takes` parameter in a previous iteration of the loop and cannot be used again
  --> src/main.lpfree:18:26
   |
18 |         freeFrame(takes: frame)
   |                          ^^^^^
   |

Each iteration needs its own value. Create it inside the loop:

func freeRange(first: uint64, count: int): void {
    var i = 0
    while (i < count) {
        let frame = PageFrame(first + (i as uint64))
        freeFrame(takes: frame)
        i = i + 1
    }
}

Listing 12-4: A fresh unique value per iteration

Only takes parameters own their value

A parameter without takes is a borrow. Returning it, storing it, or passing it on to a takes parameter would move a value the function does not own (ruling MOVEB-1):

This code does not compile
@mode(freestanding)

unique class PageFrame {
    pub var number: uint64;
    new(number: uint64) { this.number = number; }
}

# Returns the frame a lookup found.
func pick(frame: PageFrame): PageFrame {
    return frame
}
error[E_MOVE_OUT_OF_BORROW]: E_MOVE_OUT_OF_BORROW: cannot move `frame` out of the borrowed parameter `frame` — only a `takes` parameter owns its value (§1-8.11); declare it `takes frame`
  --> src/main.lpfree:10:12
   |
10 |     return frame
   |            ^^^^^
   |

The fix is in the message: func pick(takes frame: PageFrame): PageFrame, called as pick(takes: f). The same rule covers a receiver: a reads or edits method cannot move out of this or out of one of its fields.

Closures capture by moving

A closure that captures a unique binding — or any binding whose value holds one — moves it into the closure, whether or not the closure escapes (ruling CAPMV-1):

This code does not compile
@mode(freestanding)

unique class PageFrame {
    pub var number: uint64;
    new(number: uint64) { this.number = number; }
}

func describe(): uint64 {
    let frame = PageFrame(0x42u64)
    let address = func(): uint64 {
        return frame.number * 4096u64
    }
    let a = address()
    return frame.number
}
error[E_USE_AFTER_MOVE]: E_USE_AFTER_MOVE: binding 'frame' was moved and cannot be used again — `unique class` instances and move-only structs have single ownership
  --> src/main.lpfree:14:12
   |
14 |     return frame.number
   |            ^^^^^
   |

Capture what the closure needs, not the owner: copy the plain value out first.

func describe(): uint64 {
    let frame = PageFrame(0x42u64)
    let number = frame.number                 # a uint64 copy
    let address = func(): uint64 {
        return number * 4096u64
    }
    let a = address()
    return a + frame.number
}

Listing 12-5: Capturing a copied field instead of the unique owner

Structs that hold unique values

A struct field may hold a unique value, declared unique var. Such a struct becomes move-only: copying it would duplicate the owner, so let b = a moves it (§1-10.3.2):

struct Mapping {
    var virt: uint64;
    unique var frame: PageFrame;
}

func mapOne(virt: uint64, number: uint64): Mapping {
    return Mapping(virt = virt, frame = PageFrame(number))
}

func physical(m: Mapping): uint64 {
    return m.frame.number * 4096u64
}

func demo(): uint64 {
    let m = mapOne(0xFFFF_8000u64, 7u64)
    let p = physical(m)                       # borrowed: m is still ours
    let moved = m                             # moved: m is now dead
    return p + moved.virt
}

Listing 12-6: A move-only struct holding a unique field

A struct field must use unique var, not unique let (E_UNIQUE_FIELD_LET), and a plain class may not have unique fields — only a unique class or a struct can hold one (E_UNIQUE_CLASS_FIELD).

Collections are a hosted feature (chapter 13 explains why freestanding code should not use growable lists), but the same rules apply to them: inserting a unique value consumes it, indexing borrows, and pop/remove move it out (ruling UNIQCOLL-1).

del and the end of ownership

del x ends ownership early: it runs the class's finalize() method, and any later use of x is E_USE_AFTER_DEL (chapter 7 shows the diagnostic). In freestanding, keep two things in mind:

  • The Language Reference also has del return the object's memory to the @deallocator. Like allocation itself, that half is planned, not implemented in 1.25 (chapter 7).
  • Do not rely on implicit cleanup at the end of a scope. The Language Reference promises finalize() at del and at the end of a with block; defer blocks do not run in 1.25 (chapter 4). Call del, or a takes method such as release(), at the point where the resource really is done.

consume, which turns a unique value into an ARC reference in host.native, does not exist in freestanding: there is no ARC to hand the value to (E_CONSUME_NOT_AVAILABLE, chapter 7). When several parts of a kernel need the same object, keep one owner in a long-lived place — a module-level table — and lend borrows to the rest; or count references yourself with atomic intrinsics (chapter 23).

Summary

  • unique class and unique let/unique var bindings give a class value exactly one owner; unique on a non-class type is E_UNIQUE_NOT_CLASS.
  • Assignment moves a unique value (E_USE_AFTER_MOVE); borrow with reads/edits parameters to keep ownership.
  • takes moves into a function (E_USE_AFTER_TAKE afterwards, including on the next loop iteration); returning moves out.
  • Only takes parameters may move their value onward (E_MOVE_OUT_OF_BORROW).
  • Closures capture unique bindings by moving them; capture copied fields instead.
  • A struct with a unique var field is move-only.
  • del runs finalize() and ends the binding; release resources explicitly rather than at scope exit; consume is unavailable.

Next: memory primitives.