Getting Started
OverviewLanguage GuideFull Reference
Book
Table of ContentsIntroductionPrefaceGetting StartedLanguage TourOwnershipErrorsConcurrencyStdlibNetworkingDataPackagesSpeed & SafetyCross-PlatformToolingCookbookAppendix
Reference
Standard LibraryKeywordsPerformanceSecurityBuilt-in FunctionsStatusDebuggingABI
How-To
Getting StartedHTTP APIsErrorsPackagesConcurrencyMemoryWASITestingRelease Builds
Project
RoadmapVisionChangelogContributing

Memory Management

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.

Default bindings

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 -- unique ownership

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

Mutable hold

hold let mut x = 7
x = 9                   // allowed -- still owned
print_int(x)            // 9

Partial moves on structs

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

Copy types

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 -- shared read access

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:

When to use hold vs share

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.

Arenas -- region-based allocation

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")
}

Arena slices

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
}

Arena structs

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
}

When to use arenas

Control flow and ownership

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
}

Defer for cleanup

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.

Complete example: request handler with arena

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
}

Summary

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

Next steps

Edit this page on GitHub Report an issue