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:
@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):
@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):
@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):
@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
delreturn 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()atdeland at the end of awithblock;deferblocks do not run in 1.25 (chapter 4). Calldel, or atakesmethod such asrelease(), 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 classandunique let/unique varbindings give a class value exactly one owner;uniqueon a non-class type isE_UNIQUE_NOT_CLASS.- Assignment moves a
uniquevalue (E_USE_AFTER_MOVE); borrow withreads/editsparameters to keep ownership. takesmoves into a function (E_USE_AFTER_TAKEafterwards, including on the next loop iteration); returning moves out.- Only
takesparameters may move their value onward (E_MOVE_OUT_OF_BORROW). - Closures capture
uniquebindings by moving them; capture copied fields instead. - A struct with a
unique varfield is move-only. delrunsfinalize()and ends the binding; release resources explicitly rather than at scope exit;consumeis unavailable.
Next: memory primitives.