Mako compiles to C, then to native machine code via clang. There is no garbage
collector. Memory is managed through ownership (hold/share) and arena
allocation. This chapter explains how Mako keeps your programs both fast and
safe, and the tools you have when you need to push further in either direction.
By default, mako build produces a debug binary with -O0 -g -- fast compile
times, full debug symbols, and all runtime safety checks enabled. When you are
ready to ship:
mako build --release main.mko -o bin/app
The --release flag tells the backend to compile with -O3 -flto:
-O3 enables aggressive optimizations: inlining, vectorization, loop
unrolling, dead code elimination, and constant propagation.-flto (link-time optimization) lets the optimizer see across translation
units, eliminating unused functions and inlining across module boundaries.You can measure where time is spent:
mako build --time main.mko # prints frontend + backend + link durations
mako profile main.mko --json # structured output for CI dashboards
A typical release binary for a small service is under 200 KB on arm64.
Mako uses an incremental compilation cache by default. Object files are stored
in .mako/cache/ and reused when source has not changed.
| Flag / Environment Variable | Meaning |
|---|---|
| (default) | Incremental on, cache at .mako/cache/ |
--no-incremental |
Bypass the object cache entirely |
-j N / MAKO_JOBS |
Number of parallel clang invocations |
MAKO_CACHE |
Override the cache directory path |
Example: building a workspace with 8 parallel jobs:
mako build -j 8 . --release
On a cold build the frontend (lex, parse, typecheck, C codegen) is typically under 100 ms. The clang backend dominates. Incremental builds skip unchanged translation units entirely.
All slice and array accesses are bounds-checked at runtime in both debug and release builds. An out-of-bounds index aborts the program immediately with a message indicating the file, line, index, and length:
abort: index 5 out of bounds (len 3) at main.mko:12
This prevents buffer overflows from ever becoming exploitable. The cost of a bounds check is typically a single comparison and branch -- modern CPUs predict the taken (in-bounds) path with near-zero overhead.
In extremely hot loops where profiling shows the bounds check is measurable, you
can use unsafe_index:
fn sum_hot(xs: []int) -> int {
let mut total = 0
let n = len(xs)
let mut i = 0
while i < n {
unsafe {
total = total + unsafe_index(xs, i)
}
i = i + 1
}
return total
}
unsafe_index skips the bounds check. It must appear inside an unsafe block.
If you pass an invalid index, behavior is undefined -- there is no safety net.
Guideline: Only use unsafe_index when you have profiled and confirmed the
bounds check is the bottleneck. In most code, the checked path is free.
Mako enforces ownership at compile time through hold and share bindings. The
checker runs during mako check and prevents use-after-move, double-free, and
aliasing violations without any runtime cost.
A hold binding has exclusive ownership. When the value is rebound or passed
to a function, ownership transfers and the original binding is dead:
fn consume(s: string) {
print(s)
}
fn main() {
hold let x = "hello"
consume(x) // x moved into consume
// print(x) // COMPILE ERROR: use of moved value `x`
}
For struct values, individual fields can be moved independently:
struct Pair {
left: string
right: string
}
fn main() {
hold let p = Pair { left: "a", right: "b" }
let l = p.left // moves only `left`
print(p.right) // `right` still usable
// print(p.left) // COMPILE ERROR: field already moved
}
Primitive types (int, int64, int32, int8, uint64, byte, float64,
bool) are Copy. A hold binding of a Copy type can be read multiple times
without consuming it:
fn main() {
hold let n = 42
print_int(n) // fine
print_int(n) // still fine -- int is Copy
}
share creates an immutable borrow. While a share is live, the source cannot
be mutated:
fn main() {
let mut x = 10
share let s = share_int(x)
print_int(share_get(s))
// x = 20 // COMPILE ERROR: cannot mutate while share is live
share_drop(s)
x = 20 // fine now
}
The checker uses control-flow-graph analysis to determine precisely when a share ends. A share that is last used on line 5 does not block mutations on line 7, even within the same scope (mid-scope drop).
The move checker does not use lexical scopes to determine when a binding is live. Instead, it traces actual control flow through the program.
fn main() {
hold let x = "data"
if some_condition() {
consume(x) // moves x on this path
} else {
// x not moved here
}
// After the if: x MAY be moved (moved on one arm)
// print(x) // COMPILE ERROR
}
If x is moved on all non-diverging branches, it is dead after the join. If moved on no branches, it remains live.
A branch that always returns, breaks, or continues does not contribute to the join point:
fn process(data: string) -> int {
hold let x = data
if len(x) == 0 {
return -1 // diverges -- this arm's move state is irrelevant
}
// x is still live here because the only non-diverging path did not move it
print(x)
return 0
}
The checker iterates loop bodies to detect moves that could be reached on a second iteration:
fn main() {
hold let x = "once"
let mut i = 0
while i < 3 {
// print(x) // COMPILE ERROR: loop-carried move
i = i + 1
}
}
An always-break loop body does not trigger the second-pass check:
fn main() {
hold let x = "data"
while true {
print(x) // OK: always breaks, never re-enters
break
}
}
The checker recognizes constant booleans. if false { ... } is dead code and
does not affect move state:
fn main() {
hold let x = "kept"
if false {
consume(x) // dead -- does not move x
}
print(x) // fine
}
Arenas provide bump-pointer allocation: many allocations, one bulk free at scope exit. There is no per-object free and no fragmentation within the arena's lifetime.
fn handle_request(fd: int) {
arena a {
// All allocations below come from the arena
let mut buf = make([]byte, 0, 4096)
let mut headers = make([]string, 0, 16)
let body = read_body(fd, buf)
let response = process(body, headers)
send_response(fd, response)
}
// One free here -- no GC pause, no per-allocation overhead
}
Inside an arena block, make allocates from that arena:
struct Point {
x: int
y: int
}
fn main() {
arena a {
let mut xs = make([]Point, 2, 4)
xs[0] = Point { x: 1, y: 2 }
xs[1] = Point { x: 3, y: 4 }
xs = append(xs, Point { x: 5, y: 6 })
print_int(len(xs)) // 3
print_int(xs[2].x) // 5
}
}
Arenas are not appropriate for data that must outlive a scope. For long-lived
data, use normal allocations with hold ownership.
The unsafe keyword marks code where the compiler's safety guarantees are
suspended. Today this means:
unsafe_index(slice, i) -- unchecked bounds access.fn main() {
let xs = [10, 20, 30]
unsafe {
let v = unsafe_index(xs, 1)
print_int(v) // 20
}
}
Unsafe blocks are syntactically visible and greppable. Code review tools and
mako lint flag them. The goal is that 99.9% of code never needs unsafe.
unsafe_index
should make it obvious why the index is valid (e.g., a preceding length
check or loop bound).unsafe_index without profiling
evidence that bounds checking is the bottleneck.Mako provides primitives for handling sensitive data (API keys, passwords, tokens) that should not linger in memory:
fn main() {
let key = secret_from_str("sk-live-abc123xyz")
// ... use key for authentication ...
secret_drop(key) // zeroes the memory before freeing
}
secret_from_str(s) copies the string into a dedicated allocation and
returns an opaque handle.secret_drop(handle) overwrites the allocation with zeroes, then frees it.
The original string s is a normal Mako string (GC-free but not zeroed);
secret_from_str exists so you can control the lifetime of the sensitive
copy.This prevents secrets from persisting in freed memory where they could be exposed by a crash dump, memory inspector, or allocation reuse.
When comparing secrets (tokens, HMACs, password hashes), a naive == leaks
information through timing differences. Mako provides const_eq:
fn verify_token(got: string, want: string) -> bool {
return const_eq(got, want) == 1
}
const_eq always examines every byte of both strings, regardless of where they
differ. It returns 1 for equal, 0 for not equal. Use this for any
security-sensitive comparison.
The HTTP builtins include http_header_ok which rejects header names or values
containing CR/LF characters:
fn safe_set_header(name: string, val: string) -> bool {
if http_header_ok(name, val) == 1 {
// safe to use
return true
}
return false
}
This prevents HTTP response splitting attacks at the API level.
For concurrent workloads that need shared mutable state, Mako provides CMap --
a concurrent hashmap with lock-free reads and per-stripe spinlock writes. It can
be shared across crew tasks without channels, mutexes, or ownership annotations:
fn main() {
let counters = cmap_new()
crew t {
let _ = t.kick(increment(counters, "a"))
let _ = t.kick(increment(counters, "b"))
let _ = t.kick(increment(counters, "a"))
}
print_int(cmap_incr(counters, "a", 0)) // 2
print_int(cmap_incr(counters, "b", 0)) // 1
}
fn increment(m: CMap, key: string) -> int {
let _ = cmap_incr(m, key, 1)
return 0
}
CMap is safe by construction: the runtime uses 512 stripes with FNV-1a hashing to minimize contention, and reads are lock-free. For most concurrent key-value patterns, prefer CMap over channel-based coordination.
For distributed services, Mako provides two safety primitives that protect
systems from overload and cascading failures (runtime/mako_cloud.h).
The RateLimiter type implements a token-bucket algorithm. It prevents callers
from exceeding a defined request rate:
fn main() {
// Allow 100 requests/second with a burst of 10
let rl = ratelimit_new(100, 10)
let mut allowed = 0
let mut rejected = 0
for _ in range 20 {
if ratelimit_allow(rl) == 1 {
allowed = allowed + 1
} else {
rejected = rejected + 1
}
}
print_int(allowed) // up to 10 (burst)
print_int(rejected) // remainder
print_int(ratelimit_remaining(rl))
ratelimit_free(rl)
}
Use rate limiters at API boundaries to prevent abuse and ensure fair resource sharing between clients.
The CircuitBreaker type prevents repeated calls to a failing downstream
service. After a threshold of failures, the breaker opens and rejects requests
immediately (fail-fast), protecting both the caller and the downstream:
fn call_downstream(cb: CircuitBreaker) -> int {
if breaker_allow(cb) == 0 {
return -1 // circuit open, fail fast
}
// Attempt the call...
let success = 0 // simulate failure
if success == 1 {
breaker_success(cb)
} else {
breaker_failure(cb)
}
return success
}
fn main() {
// Open after 5 failures; wait 30s before half-open; allow 3 probes
let cb = breaker_new(5, 30000, 3)
for _ in range 10 {
let _ = call_downstream(cb)
}
// After 5 failures, state transitions to open
print_int(breaker_state(cb)) // 1 (open)
breaker_reset(cb)
print_int(breaker_state(cb)) // 0 (closed)
breaker_free(cb)
}
Circuit breaker states: - 0 (closed): Normal operation. Failures are counted. - 1 (open): Too many failures. All requests rejected immediately. - 2 (half-open): After timeout expires, a limited number of probe requests are allowed through. If they succeed, the breaker closes; if they fail, it reopens.
These primitives complement the memory-safety guarantees with operational safety for networked services.
Mako's session management and authentication toolkit (runtime/mako_security.h)
extends the safety-by-default philosophy to web security:
Constant-time comparisons everywhere. All token, password, session, and
CSRF comparisons use const_eq internally. Functions like
auth_session_cookie, auth_check_bearer, auth_check_basic,
auth_token_check, and csrf_check are immune to timing attacks by
construction. Application code never needs to implement its own comparison
logic.
Secure cookie defaults. cookie_make produces cookies with HttpOnly
(prevents JavaScript access), SameSite=Lax (mitigates cross-site request
forgery), and Path=/. There is no "insecure cookie" API to misuse.
Cryptographic session IDs. session_id_new generates 16 bytes of
cryptographic randomness (via mako_random_bytes), formatted as 32 hex
characters. This provides 128 bits of entropy, making session ID guessing
computationally infeasible.
Secret memory wiping. Signing keys and API tokens can be stored via
secret_from_str and explicitly zeroed with secret_drop, preventing
sensitive material from lingering in freed memory where crash dumps or
allocation reuse could expose it.
HMAC-SHA256 signed tokens. auth_token_sign produces tamper-evident
tokens using HMAC-SHA256. Verification via auth_token_check is constant-time.
| Risk | Prevention |
|---|---|
| Use-after-move | CFG NLL + hold checker at compile time |
| Buffer overflow | Bounds checks on every index (release too) |
| Orphan threads | crew structured concurrency -- cancel/join |
| Data races | Channels + crew isolation + CMap (thread-safe by design) |
| Header injection | http_header_ok rejects CR/LF |
| Secret residue | secret_from_str + secret_drop zeroing |
| Timing side-channel | const_eq constant-time comparison (all auth/session/CSRF functions) |
| Session hijacking | Crypto-random session IDs (128-bit entropy), HttpOnly cookies |
| CSRF | csrf_token / csrf_check with constant-time verify; SameSite=Lax cookies |
| SQL injection | Parameterized queries only (sqlite_query_int_params) |
Pre-size slices and maps. make([]T, 0, n) avoids repeated growth
copies. make(map[K]V, n) avoids rehashing.
Use arenas for request-scoped work. One bulk free is cheaper than many individual frees.
Prefer hold over share. Unique ownership has zero runtime cost.
Shared references add reference counting overhead.
Avoid string copies in hot paths. Use str_builder for concatenation.
Use []byte when building output incrementally.
Measure with now_ns. The monotonic nanosecond clock is the right tool
for benchmarking:
fn main() {
let start = now_ns()
// ... work ...
let elapsed = now_ns() - start
print_int(elapsed)
}
black_box in benchmarks. Prevents the optimizer from eliminating
work that has no observable side effect:fn main() {
let start = now_ns()
let mut sum = 0
for i in range 1000000 {
sum = sum + i
}
let _ = black_box(sum)
print_int(now_ns() - start)
}
mako profile main.mko --json reports
frontend, backend, and run time. Focus on what the profile shows.For catching memory and threading bugs that slip past static analysis:
mako build --sanitize=address path.mko # AddressSanitizer: buffer overruns, use-after-free
mako build --sanitize=thread path.mko # ThreadSanitizer: data races
These add runtime instrumentation. Binaries are slower but report precise diagnostics on violation. Use them in CI test runs.
[package] systems = true MarkerIn mako.toml, a package can declare:
[package]
name = "mykernel"
systems = true
This opts into stricter rules: if a future optional GC mode is added, this
package will never use it. The hold/share ownership rules remain fully
enforced with no runtime weakening.
Mako achieves speed through direct compilation to C with -O3 -flto, arena
allocation, zero-cost ownership, and no garbage collector. It achieves safety
through compile-time move analysis, runtime bounds checks, structured
concurrency, and explicit unsafe opt-out. The two goals reinforce each other:
the ownership system eliminates the need for a GC (speed) while preventing
use-after-free (safety). Bounds checks prevent overflows (safety) with negligible
performance cost on modern hardware (speed).
Next: Cross-platform and WASI.