What One Owner Is Worth

Functional code builds new values instead of changing old ones. The usual price is that every new value is a fresh allocation, and the old one is garbage for a collector to find later. March pays that price only when it has to. The question it asks at every rebuild is not “is this value immutable?” but “is anyone else holding it?”, and when the answer is no, the new value is written over the old one.

This page follows one small tree through that question four times. Then it shows the same question asked by the type checker instead of the runtime, as a linear or affine type, and finally the contract that makes the compiler keep a function allocation-free.

What the compiler finds

The tree has 31 cells: 16 leaves and 15 inner nodes. inc_leaves reads as if it builds a second tree:

fn inc_leaves(t : Tree) : Tree do
  match t do
  Leaf(n)    -> Leaf(n + 1)
  Node(l, r) -> Node(inc_leaves(l), inc_leaves(r))
  end
end

fn main(_c : Cap(IO.Console)) : Unit do
  let t = build(4, 1)
  let t2 = inc_leaves(t)
  println(int_to_string(sum_leaves(t2)))
end
The 31 cells of a binary tree, each with its reference count, and a second tree of copies growing beside it while a cursor walks the first.

The 31 cells as build left them. Each carries a count of how many references point at it. Every count is 1: the tree has one holder, the binding t.

inc_leaves(t) is the last thing main does with t. The call takes over the only reference, so when a match looks inside a Node, that cell is about to die, and the Node being built has the same shape. The compiler writes the new fields into the old cell.

Across the whole walk, inc_leaves allocates 0 cells. That number is read from the runtime's own allocation trace of this program, not estimated.

Now main reads the old tree again afterwards:

fn main(_c : Cap(IO.Console)) : Unit do
  let t = build(4, 1)
  let t2 = inc_leaves(t)
  println(int_to_string(sum_leaves(t2) - sum_leaves(t)))
end

Two holders, so the counts on the path climb to 2 as the walk descends, and nothing may be overwritten. inc_leaves allocates 31 cells: a complete second tree. The program is still correct; it is only slower. The fallback is automatic, and the compiler says where it happened:

`inc_leaves` builds a new copy instead of reusing `t` in place, because `t` is used again afterwards. [copy_shared]

What march --why-copies prints
-- WARNING: COPY --------------------------- shared.march

`inc_leaves` builds a new copy instead of reusing `t` in place, because `t` is used again afterwards. [copy_shared]
A function can update a value in place only when it holds the only reference to it; make this call the last use of `t` and `inc_leaves` reuses its memory instead of allocating.

27 |     let t2 = inc_leaves(t)
                             ^

    `t` is used again here, so it is still shared when `inc_leaves` runs:

28 |     println(int_to_string(sum_leaves(t2) - sum_leaves(t)))
                                                           ^
    run `march --explain copy_shared`

A holder need not be a variable. Here the tree is put in a Vault, March's shared table, before the transform, and inc_leaves(t) is again the last use in the code:

fn main(_c : Cap(IO.Console), _m : Cap(IO.Mut)) : Unit do
  let t = build(4, 1)
  let kept = Vault.new("trees")
  Vault.set(kept, "before", t)
  let t2 = inc_leaves(t)
  println(int_to_string(sum_leaves(t2)))
end

The static report sees the code and finds no second use:

No copies found: every value passed to a function that reuses it in place is its last use.

The runtime sees the count. The Vault holds a reference, so the walk copies all 31 cells anyway. That is the division of labour on this page: the compiler decides where reuse is possible, and the count decides whether it happens.

One owner also means no lock. The two halves of a uniquely held Node are disjoint, so each can be rewritten on its own task with nothing to synchronise:

fn inc_parallel(t : Tree) : Tree do
  match t do
  Leaf(n)    -> Leaf(n + 1)
  Node(l, r) ->
    match List.pmap_n([l, r], fn half -> inc_leaves(half), 2) do
    [left, right] -> Node(left, right)
    _ -> panic("two halves")
    end
  end
end

Two tasks, and inc_leaves allocates 0 cells. The list, the chunks and the task handles around it cost 11 small cells; the tree costs none.

Nothing above was written for the optimiser. Two mechanisms did the work, and the Memory Model names them. Perceus is deterministic reference counting: the compiler inserts each increment and decrement where ownership changes, so a value is freed at its last use, at a point you can see. FBIP, “functional but in-place”, is the reuse rule on top of it: a dying value with a count of 1 gives its cell to the same-shaped value being built. Any other value is copied, and the copy is never wrong, only slower. The compiler shows its decisions as editor hints (♻ in-place, ⧉ copied, ⚡ stack-allocated), and march --why-copies lists every copy with its reason.

What the type guarantees

The runtime learns that a value has one owner by counting. The type checker can be told instead. A linear value must be used exactly once; an affine value at most once. Both forbid using it twice. Only linear forbids forgetting it. The compiler tracks the value as a token that each use consumes.

A short March function with a token beside the line where a linear value is bound, consumed at its one use, and marked where a second use is refused.

The same tree, declared linear. The token is minted at the binding and consumed by inc_leaves, which takes a linear parameter and so promises to consume what it is given. This compiles.

fn inc_leaves(linear t : Tree) : Tree do
  match t do
  Leaf(n)    -> Leaf(n + 1)
  Node(l, r) -> Node(inc_leaves(l), inc_leaves(r))
  end
end

fn main() : Int do
  linear let t : Tree = Node(Leaf(1), Node(Leaf(2), Leaf(3)))
  sum_leaves(inc_leaves(t))
end

The sharing program from above, with the same declaration. The second use has no token to consume. Where the runtime copied, the type checker refuses:

The linear value `t` is used more than once here. [linear_used_twice]

What the compiler prints
-- LINEARITY ------------------------- linear_twice.march

The linear value `t` is used more than once here. [linear_used_twice]
Linear values must be consumed exactly once — they cannot be copied or ignored.

22 |     sum_leaves(t2) - sum_leaves(t)
                                     ^

    `t` was already consumed here:

21 |     let t2 = inc_leaves(t)
                             ^
    run `march --explain linear_used_twice`

A token that reaches the end of its scope unconsumed is the other error. For a tree it is only a wasted value; for a file handle or a session endpoint it is a leak, and linear is how March makes "you must close this" a type.

The linear value `t` was never used. [linear_never_used]

affine keeps half the rule. A branch may leave the token unused:

fn maybe_sum(t : affine Tree, wanted : Bool) : Int do
  if wanted do sum_leaves(t) else 0 end
end

It is the right word for a permission or a one-shot token: something you may decide not to use, but must never use twice.

The other half still holds.

fn twice(t : affine Tree) : Int do
  sum_leaves(t) + sum_leaves(t)
end

The affine value `t` is used more than once here. [linear_used_twice]

Two consequences follow from the type knowing what the runtime had to count.

  • Branches must agree. A linear value consumed in one branch of an if or match must be consumed in every branch that returns; the early Err return of let? is a branch too.
  • The count disappears. The Memory Model puts it as “A linear value is the strongest FBIP case”: one owner by the type, not by inference, so the compiler drops the reference-count traffic where the type settles it and frees the value at its last use. Linearity is erased before code generation; nothing checks it again at run time.

Some types are linear without the keyword. The stdlib’s Handle and the fixed buffer RingBuf are always_linear: every operation consumes the value and hands it back, which is what lets a buffer be written in place without anyone else seeing it. A session endpoint is tracked as affine plus a rule that it must be closed once the protocol reaches End; walking away from a channel mid-protocol is not refused today. Linear Types covers all of it, including linear record fields and what a closure may capture.

Making it stick

Reuse is something the compiler found, and a later edit can lose it: read the old value once more, store it somewhere, add a field that changes a type’s representation. A contract turns the finding into a check.

An allocation ledger: one row per function with a bar for the cells it allocates, and a line at zero that a contracted function may not cross.

The compiler already knows which functions came out allocation-free. Asked, it names them:

`inc_leaves` is verified allocation-free; add @[no_alloc] to keep it that way.

That is one line of march --compile --report-contracts, and forge fix --contracts applies every such line.

With the attribute on, the check is made on the compiled form, after reuse and stack promotion have been decided, and it is made every build:

@[no_alloc]
fn inc_leaves(t : Tree) : Tree do
  match t do
  Leaf(n)    -> Leaf(n + 1)
  Node(l, r) -> Node(inc_leaves(l), inc_leaves(r))
  end
end

Reintroduce an allocation, here or in anything inc_leaves calls, and the build fails instead of the benchmark.

The bare form means no allocation at all. The check runs on the compiled form, where describe has been inlined into width_bare, so it names the allocation where it ended up:

fn describe(i : Int) : String do
  "item " ++ int_to_string(i)
end

@[no_alloc]
fn width_bare(i : Int) : Int do
  String.byte_size(describe(i))
end

In `width_bare`: `int_to_string` allocates.

What the compiler prints
-- CONTRACT ------------------------- contract_bare.march

`width_bare` is marked @[no_alloc] but allocates. [no_alloc]
  In `width_bare`: `int_to_string` allocates.

10 |   fn width_bare(i : Int) : Int do
          ^^^^^^^^^^

A tick, a frame, a request handler: code that allocates freely and frees all of it before returning has a property the bare form cannot say. transient asks where a value ends up, not whether one was made:

@[no_alloc(transient)]
fn width(i : Int) : Int do
  String.byte_size(describe(i))
end

This compiles. It fails if the function returns something it allocated, stores one into an object it did not allocate, or hands one to an actor, a Vault, a task or foreign code.

Some values never reach the heap at all. A variant with one constructor of two to four Int, Float or Bool fields lives in registers, so forward passes. Storing it into a field boxes it, and the contract says so:

@[no_alloc]
fn forward(yaw : Float, pitch : Float) : Vec3 do
  let cp = Math.cos(pitch)
  Vec3(0.0 -. Math.sin(yaw) *. cp, Math.sin(pitch), 0.0 -. Math.cos(yaw) *. cp)
end

@[no_alloc]
fn record(yaw : Float, pitch : Float, id : Int) : Hit do
  Hit(forward(yaw, pitch), id)
end

In `record`: constructor `Hit` is allocated here.

One limit to hold on to. The contract is about the function's own code. Give inc_leaves a caller that keeps the old tree and the build still passes, while at run time the walk allocates every cell again (5 on the 5-cell tree in that program). The copy belongs to the caller, and --why-copies is the tool that points at it.

What the contract cannot yet say is "at most N". A parser that allocates a few cells per token is not allocation-free, but its count is worth pinning. @[alloc_bound(N)] and a per-activation form for recursive functions are designed and not shipped; the design is in the repository, with its open item.

What this does not do

  • The static report is a view of the code, not of the run. --why-copies found no copy in the Vault program, and the run copied every cell. The runtime check is what keeps the program correct either way.
  • Reuse needs the same shape. A Leaf cannot be rewritten into a Node; a rebuild of a different size class allocates.
  • Linearity is checked, then forgotten. No runtime cost, and no runtime protection: a value that crosses into foreign code is on its own.
  • A Vault makes a value shared by definition. Reads need no capability, and the first write to the table is the second holder.
  • The counts here come from a 31-cell tree compiled with the allocation trace on. The trace names each site; it does not change what is allocated.
  • @[alloc_bound(N)] is a design. Nothing on this page measured it.

The reference chapters go further: Memory Model, Linear Types, Value Representation and the cap no_alloc section of Capabilities.

How the numbers on this page were measured

Every count, excerpt and compiler message above is generated, not typed. One script regenerates them all, and this page was last generated at commit 87553b42a on 2026-11-06:

scripts/gen-ownership-data.py
  • Programs: every excerpt is cut from a file under docs/assets/explainer/ownership/programs/; the whole file is what was compiled.
  • Allocation counts: each tree program compiled with march --rc-trace --compile and run with MARCH_TRACE_GC=1; the count is the number of march_alloc events whose site is inside the named function, folded from trace/gc/gc.jsonl the way scripts/gc-trace-report.py folds it.
  • Copy reports: march --why-copies on the same files. Recursion warnings that the tree functions also raise are left out of the quoted text.
  • Linear, affine and contract messages: march --check and march --compile on each program, with the exit code recorded.
  • The candidate line: march --compile --report-contracts on the owned program.
  • Quoted claims: that a linear value is the strongest reuse case, that linearity is erased, and that an endpoint is tracked as affine, are phrases pinned in specs/lang/memory-model.md and specs/lang/linear-types.md; the generator fails if they move. The allocation-bound design is specs/2026-11-04-alloc-bound-design.md, with its open item at specs/todos/2026-11-04-alloc-bound-contract.md.
  • The heap figure: a simulation of the walk, seeded (20261106). Its counts are the measured ones; the order and timing of the walk are drawn, not recorded.