Mako has no garbage collector. Memory safety is achieved through ownership
rules (hold/share) and region-based allocation (arena). This guide
explains when to use each strategy.
Regular let bindings are the simplest form. They work like stack values:
let x = 42 // immutable
let mut y = 10 // mutable
y = y + 1
For most local computation, this is all you need.
hold marks a binding as move-semantics. Once moved, the original is gone:
hold let x = 7
hold let y = x // x is moved into y
print_int(y) // 7
// print_int(x) // COMPILE ERROR: use of moved value `x`
Moving into a function call also consumes the binding:
fn consume(n: int) -> int {
return n * 2
}
hold let val = 42
print_int(consume(val))
// print_int(val) // COMPILE ERROR: moved into consume
hold let mut x = 7
x = 9 // allowed -- still owned
print_int(x) // 9
Move individual fields while keeping the rest usable:
struct Point { x: int y: int }
hold let p = Point { x: 1, y: 2 }
let px = p.x // moves only x
print_int(p.y) // y still usable
// print_int(p.x) // COMPILE ERROR: x already moved
Integer and bool types are Copy -- they can be re-read after a hold binding
without consuming:
hold let n = 42
let a = n
let b = n // fine -- int is Copy
print_int(a + b) // 84
share creates a read-only shared reference. While a share exists, the
original cannot be mutated:
hold let a = 1
share let s = share_int(a)
print_int(share_get(s)) // 1
// a = 5 // COMPILE ERROR: cannot mutate while shared
share_drop(s)
// Now a is free again (if mut)
Rules enforced at compile time:
share let is always immutable (no share let mut)share_drop, block exit, or last use (NLL)| Situation | Use |
|---|---|
| Value has one owner, passed linearly | hold |
| Multiple readers, no mutation needed | share |
| Short-lived local computation | plain let |
| Large allocation, bounded lifetime | arena |
Prefer hold (unique ownership) whenever possible. It has zero overhead and
gives the compiler maximum freedom to optimize.
An arena allocates many objects cheaply and frees them all at once when the scope exits:
fn main() {
arena a {
let msg = arena_text(a, "hello arena")
print(msg)
let xs = arena_ints(a, 4) // 4 ints, zeroed
print_int(len(xs)) // 4
let stamp = arena_stamp(a, 99)
print_int(stamp) // 99
}
// Everything in `a` freed here -- one deallocation for the whole region
print("arena done")
}
Inside an arena block, make allocates from the arena automatically:
arena a {
let mut s = make([]int, 3, 8) // backed by arena memory
s[0] = 10
s[1] = 20
s = append(s, 30) // grows within the arena
print_int(len(s)) // 4
}
struct Point { x: int y: int }
arena a {
let mut pts = make([]Point, 0, 4)
pts = append(pts, Point { x: 1, y: 2 })
pts = append(pts, Point { x: 3, y: 4 })
print_int(pts[0].x) // 1
}
The compiler tracks ownership through branches and loops:
hold let x = 7
if some_condition() {
let y = x // moves x on this branch
} else {
print_int(x) // x still alive on this branch
}
// x may or may not be moved here -- compiler tracks both paths
Moves in a loop iteration are checked per-iteration:
hold let x = 42
while condition() {
// Cannot move x here -- would be used-after-move on next iteration
print_int(x) // read of Copy type is fine
}
Use defer to ensure cleanup runs when a scope exits:
fn process() {
let fd = open_resource()
defer close_resource(fd)
// ... work with fd ...
// close_resource runs automatically on return
}
Defers execute in LIFO (last-in, first-out) order.
fn handle_request(fd: int) {
arena req {
let mut buf = make([]byte, 0, 4096)
let c = http_accept(fd)
if c < 0 { return }
let path = http_path(c)
let body = http_body(c)
if str_eq(path, "/data") {
let _ = http_respond_json(c, 200, body)
} else {
let _ = http_respond(c, 404, "not found\n")
}
let _ = http_close(c)
}
// All request memory freed in one shot
}
| Mechanism | Overhead | Lifetime | Use case |
|---|---|---|---|
let |
None (stack) | Lexical scope | Local computation |
hold |
None (move) | Until moved | Linear resource passing |
share |
RC seed | Until drop/end | Multiple readers |
arena |
Bump alloc | Block scope | Batch/request work |