Borrowing¶
Status: DECIDED and SHIPPED. The reference subsystem is part of the ownership model (PRs #306, #309, #313, #320, #325, #336, #342, #343 — 2026-08-15 and 2026-08-16). Every mechanism in this document is enforced and has tests.
Sections 1 to 5 are SUPERSEDED by
docs/design/borrow-model.md(ruled 2026-08-16, issue #354). Borrow by default made an unmarked parameter a borrow for every kind of callee, sopeekandpokeare no longer the only two ways to say "do not take this". Read that document for the mode rule and this one for the borrow MECHANISMS — where a reference type may appear, the six ways a borrow is created, and the gate behind each rule. Two changes ripple through the text below: the&is gone from both words, and the sixth mechanism (a method parameter) is now simply a parameter, in any callable.
docs/design/ownership-conventions.md is the normative spec for ownership. This document
describes the borrow half only, and does not repeat it.
1. The two words¶
| spelling | meaning | how many at a time |
|---|---|---|
peek T |
read-only borrow | many |
poke T |
read-write borrow | one, and no peek with it |
A borrow names storage that something else owns. The owner keeps the value and frees it. Four rules follow from that one sentence, and everything below is one of them:
- You cannot write through a read-only borrow. The write lands on a private copy, or on storage the owner no longer holds.
- You cannot give a borrow to a position that takes ownership. The value would get a
second owner, and both would free it. The escape is
.clone(). - The owner is frozen while a borrow of it lives. A mutation, a free, a rebind or a move of the owner would leave the borrow pointing at storage the owner no longer holds. This is Rust's E0502.
- A borrow cannot leave the function that makes it. Sushi has no lifetimes, so nothing can relate a borrow to its owner in the caller.
2. Where a reference type may appear¶
The grammar's ?type rule is recursive and universal, so peek T / poke T parses in
every type position. Semantics defines three:
- a function parameter —
fn f(peek T x); - a parameter inside a function type —
fn(peek i32) -> i32, and the lambda|peek i32 x|that satisfies it; - a
letbinding —let poke T x = <place>/let peek T x = <place>(#409, built 2026-09-03): a block-scoped borrow binding, mechanism 3b below.
Every other position is a registered rejection at the declaration: CE2415
(struct field), CE2416 (enum payload), CE2417 (return type), CE2418 (nested reference),
CE2419 (generic type argument), CE2420 (extension or perk-impl target), CE5003 (FFI
signature), CE0114 (variadic element). One walk — contains_reference in
semantics/type_predicates.py — backs the five CE24xx codes. Its carve-out for
FunctionType.param_types is load-bearing: it keeps the second supported position legal
wherever a function type appears. The position table and the reason for each rejection are
in ownership-conventions.md §8.5.
3. The mechanisms¶
One surface vocabulary, six mechanisms. Each has its own extent and its own diagnostics.
| # | mechanism | spelling | extent | write through it | consume it |
|---|---|---|---|---|---|
| 1 | call-site borrow | f(peek x), f(poke y) |
one statement | the callee's mode decides | the argument is not consumed |
| 2 | reference parameter | fn f(poke T x) |
the function body | peek: CE2408; poke: in place |
CE2411 |
| 3 | let binding of a read |
let v = c.get(0)?? |
the enclosing block | not gated (§8) | CE2411 |
| 4 | pattern value binding | E.V(p), foreach(n in ...) |
the arm or the loop body | CE2414 | CE2411 |
| 5 | pattern reference binding | foreach(poke r in xs.iter()), Own(poke x), E.V(poke p) |
the arm or the loop body | poke: in place; peek: CE2408 |
CE2411 |
| 6 | method parameter, self included |
extend T m(H h) |
the method body | CE2421 (receiver), CE2422 (by value) | CE2411 |
| 7 | pattern TAKING binding | E.V(nom p) |
the arm | in place -- the arm owns it | yes: it is the owner |
Seven now, and row 7 is the only one that is not a borrow at all. It is listed here because it shares the surface vocabulary and because it is what row 4's diagnostic offers as a way out.
1 — the call-site borrow is tracked by counters that are cleared at the end of every
statement. Therefore the exclusivity rules have jurisdiction inside one statement only: a
second poke is CE2403, and a mixed pair is CE2407. A poke of a poke parameter —
forwarding it whole (inner(poke cur)) or by field (set_port(poke cfg.port)) — is
legal and silent: the borrow ends with the statement, and the callee can neither store
the reference nor write outside its declared mode. (CW2409 warned on the whole-parameter
form until 0.11.x; it was retired because it marked the mandated composition idiom while
guarding nothing the errors do not.) A move and a borrow of one owner in one statement is
CE2401, in either argument order. Two statements are two borrows, and are unaffected.
2 — the reference parameter is the position the subsystem is built for. It carries its
full ReferenceType in the borrow state, which is what makes the write gate answerable
(§5). A borrow is created at a USE site, so a reference parameter is forwarded as
f(peek v), never as bare f(v).
3 — the let binding of a read inherits BORROWED provenance: a field read
(h.inner), an index (rows[i]) and a container get-out (c.get(0)??, own.get()) all
borrow. The owner is frozen until the end of the block that declares the binding
(CE2412).
3b — the reference-typed let (#409) binds a POINTER into a place the owner keeps:
let poke T x = <place> writes through, let peek T x = <place> reads through. The place
is a name, a member or index chain off one, or an Own@(T).get() (the payload's cell);
a temporary is CE2404, a constant CE2400. The state carries the full
ReferenceType, so the write gates answer by construction (CE2408 through a peek
binding or out of a peek owner; CE2411 on a consuming use), the owner is frozen exactly
as in mechanism 3, and the binding is released at block exit. Two rules are its own:
one poke binding of an owner at a time (CE2403), and a peek beside a live poke,
or the reverse, is CE2407 — the two codes acquire_borrow gives an argument list,
here at the binding instead of the statement. bind_let_reference in
passes/borrow/bindings.py is the one seam. CE2413, which refused the form while it was
untracked, is retired.
4 — the pattern value binding is compiled as a copy of the payload, so a write through
it could never reach the owner. The copy is SHALLOW: it holds the owner's descriptor and
points at the owner's heap, which is why a rebind of the binding itself (n := 99) is
CE2414 as well (#590). It was legal until then, on the reading that it re-initializes a
local — but the store frees what sits in the slot, and what sits in the slot is a value the
scrutinee still owns. A plain payload only hid it. The three escapes are the binding's own
modes plus the copy: poke to write through to the owner, nom to take the payload where
the match owns its scrutinee, .clone() for a value of your own — and note that a plain
let off the binding borrows again, so the copy has to be asked for. Each binding has a
scope of its own — an arm binding no longer replaces an outer local of the same name (#337).
5 — the pattern reference binding (#300) binds a POINTER into the owner's storage, so
r.n := 5 and p.push(9) mutate in place. It registers with its full ReferenceType, so
every rule above applies by construction. The match half rests on the enum payload layout
{i32 tag, [K x i64] data}, whose naturally aligned payload offsets come from one
authority (TypeSizing.payload_field_offsets). Four fences: an iterable whose items have
no address is CE2423 (a range, HashMap.entries()); a reference binding in a NESTED
match pattern is CE2424 (extraction walks through temporary copies there); a temporary
scrutinee is CE2404; and a poke binding out of a peek owner is CE2408, out of a
constant CE2400. The temporary fence was LIFTED by HANDLES.md ruling R11: a match parks a
scrutinee it owns in a slot for the whole statement, so the pointer has storage to aim at.
CE2404 now means a read THROUGH a live owner, which still has none.
7 — the pattern taking binding (HANDLES.md ruling R11) is not a borrow: E.V(nom p)
moves the payload out and the arm becomes its owner, so it is registered for its own free
through register_owning_value, the complete registry router. What stops the second free
is the SCRUTINEE'S drop flag, cleared at the head of the taking arm, so whether the match
still owns its value at the end is a run-time fact -- exactly the #414 mechanism a
conditional move of an ordinary local uses. Three fences: the match must OWN its scrutinee
(CE2432, and match nom r: is how a local is handed over); an arm takes the variant
whole (CE2433); and Own(nom x) is refused because the heap cell would leak
(CE2434). docs/design/borrow-model.md section 10b is the normative half.
6 — the method parameter. Every parameter of an extension or perk method is a borrow,
self included, and the caller keeps ownership (the #298 ruling). There is no string
carve-out (#338). The mutable form is an opt-in FIRST parameter:
extend Counter bump(poke self) ~:
self.n := self.n + 1
return ~
The receiver then arrives by pointer, so the write reaches the caller's value. peek self
states the read-only default. A perk declares the mode in its signature, and the
implementation must match it (CE4004). A receiver parameter anywhere else is
CE2425. The full ruling is in ownership-conventions.md §8.6.
4. Coercion¶
poke T coerces to peek T at a call site — a safe downgrade of a borrow that is
passed once. The coercion is a property of the position, not of the type pair, so it does
NOT apply to a stored function type: fn(peek T) and fn(poke T) are different types
in both directions (CE2002). Without that invariance, one indirection defeats the write
gate (#335). The single coercion site is semantics/passes/types/compatibility.py;
tests/references/test_borrow_coercion_matrix.sushi and its two rejection companions pin
every position where the coercion does and does not apply.
5. One gate for each rule¶
A write cannot reach through a read-only receiver. Six kinds, three write shapes each
(a mutating method under the receiver, a field assignment, a poke borrow of it), plus the
indexed assignment, which routes through the same gate:
| kind | code | escape |
|---|---|---|
match / foreach value binding |
CE2414 | bind it poke to write through, or nom to take it; otherwise .clone(), mutate, store back (.share() for a resource) |
peek reference |
CE2408 | declare the parameter poke |
| method receiver | CE2421 | poke self |
| by-value method parameter | CE2422 | declare the parameter poke T |
let-borrow binding |
CE2426 | write to the owner; or .clone(), mutate, store back |
unbound chained borrow (o.get().items) |
CE2429 | .clone(), mutate, rebuild the owner (o := Own.alloc(h)); or a nested Own(poke ...) binding where the Own sits in an enum |
A rebind of the NAME (v := ...) is the gate's other position, and it splits the table
(#590). A name with storage of ITS OWN may be rebound — the store lands in that storage and
reaches exactly what it names — so a by-value parameter and a by-value receiver stay
rebindable, which is what test_rebind_borrow_param.sushi has pinned since #326. A name
that is a VIEW of another value's storage may not, because the store frees a value the
owner still holds: the match/foreach binding (CE2414), the let-borrow (CE2426) and
the peek reference (CE2408). The position is one field on the row —
ReadOnlyReceiver.refuses_a_rebind — and a rebind=True argument to the gate, rather than
a check beside it: the check that stood beside it covered peek alone, which is how a
match binding came to be rebindable.
The first five kinds are a TABLE (READONLY_RECEIVERS) behind one dispatcher
(reject_readonly_write) with four call sites, so a new state-keyed kind is one row and not
a new walk — the fifth one (#344) cost exactly that. The codes stay separate because each
carries its own escape, and rows four and five show why that convention earns its keep: a
match/foreach binding is a private DEEP copy, so its write is only lost, while a
let-borrow shares the owner's DATA, so its write is lost AND a reallocating one frees the
owner's buffer. Same rule, different first answer.
The sixth kind keys on SHAPE, not on state (ruled 2026-08-20, #352; the live bug was
407). The other five are each a NAMED thing carrying a BorrowState; an unbound chained¶
receiver has no name to hold state on. The rule is structural: a write receiver must
reach its root — a NAME — through member and index steps only. Whatever else the walk
stops at — a method call, a ??, a plain call, an inline constructor — yields a
temporary copy, so the write would land on the copy and be lost
(o.get().items.push(9) printed the old length, exit 0, leak-clean).
chain_call_boundary (reads.py) finds the boundary, deliberately INVERTED — "a Name
is fine" rather than a list of boundary kinds — so a new expression kind is rejected,
never silently writable. The same dispatcher checks it BEFORE the state table, because
the boundary answer is the more precise one whatever the root's mode is. The boundary
span is the second location of the relational diagnostic. Three consequences:
- The READ through the same chain stays legal and leak-clean (#280; the receiver is a registered scope temporary).
- A FRESH temporary is rejected too (
make().items.push(9)): the statement discards the value, so the write is dead either way. Swift rejects the same shape. One rule, no fresh/borrowed split. - The
poke-borrow shape of this kind was already gated — CE2404, "expression has no stable address" — so the kind covers the mutating method, the field assignment and the indexed assignment (which was a CE0000 ICE until this gate).
A borrow cannot be consumed. The ownership table's (BORROWED, MOVE) cell rejects, and
that is the whole implementation: type_class_of derefs a reference to its referent, so
all twelve consuming positions answer the same way with no per-sink work. The code is CE2411
and the escape is .clone(), which is total over types.
An owner is frozen while a borrow of it lives. CE2412, reported NLL-style: the owner is invalidated at the change, and the error is reported at the next read of the borrow.
6. Where reference-ness lives in the compiler¶
- Grammar / AST. Two spellings of one concept:
ReferenceTypein a type position, and aBorrowexpression at a use site. typecheckregisters a reference parameter with its fullReferenceType, then UNWRAPS it at every mention. That gives borrow transparency — it is whyp.len()works — at the price that no inferred type downstream can answer "is this a borrow?".borrowis therefore the only layer that can ask, because it holds the borrow state. All three callable kinds — plain function, extension method, perk method — go through one entry point (_check_callable), so a relational diagnostic renders its second location in a method body like anywhere else.- The ownership seam needs no reference arm: the rejection happens in the borrow pass, before codegen. An unstamped consuming use in the backend is CE0129, which is fatal on purpose.
- The backend keys every deref on
variable_types, which is saved and restored per function (#332).
7. What keeps this total¶
Each gate turns the next occurrence of its bug class into a red test:
| gate | what it pins |
|---|---|
test_borrow_dispatch_is_total.py |
an arm for every Expr node (CE0125) |
test_scope_dispatch_is_total.py |
the same for the scope pass (CE0130) |
test_peek_write_gate_is_total.py |
every member of _MUTATING_METHODS |
test_readonly_receiver_matrix.py |
every kind x shape cell of §5, the shape-keyed sixth kind included, and the rebind position per kind |
test_borrow_flag_lifecycle.py |
every BorrowState flag x flow event |
test_ownership_table.py |
the 3x2 table, reference rows included |
test_consuming_use_coverage.py |
nothing bypasses the backend seam |
test_method_body_diagnostics.py |
all three callable kinds render the same evidence |
test_enum_payload_layout.py |
the payload offsets mechanism 5 depends on |
tests/references/ |
the behaviour corpus, ~50 programs |
8. Two questions about a let-borrow, not one¶
Mechanism 3 — a let binding of a read — is the kind that shows the two rules are
complementary rather than alternatives:
- May I change the OWNER while the binding lives? CE2412, answered NLL-style.
- May I write THROUGH the binding? CE2426, the §5 gate's fifth row.
Until #344 the second question had no answer, because the CE2414 row excluded this kind
by pointing at CE2412. let i32[] v = h.items followed by v.push(9) compiled: the write
was lost from the owner's view, a push that forced a reallocation freed the owner's buffer
(a double free plus a read of released memory), and v.destroy() did the same directly. A
field assignment and a poke borrow of the binding were ungated the same way.
The row keys on is_let_borrow, not on borrows_from is not None. An owner with no
BorrowState — a temporary, as in let v = make()??.items — records no owner name, and
the borrows_from spelling would have handed that case to CE2414, which tells the author
their let is a match binding. The temporary's buffer is just as real.
The sixth kind (CE2429) has only ONE of the two questions. A write THROUGH the chain is the §5 gate's answer; there is no owner-side question, because the chain names no binding the owner could invalidate — the temporary copy is freed at scope exit like any other unbound owning temporary.
9. Not designed¶
Each of these is a rejection today, and each is lifted separately when its feature is designed:
- Lifetimes. Nothing relates a borrow to the value it names, which is why a borrow cannot be returned (CE2417) or stored in data (CE2415, CE2416, CE2419).
- A reference binding in a nested match pattern (CE2424). A TAKING binding is legal there: nothing points into the scrutinee, so the temporary-copy hazard does not apply.
- A
nombinding insideOwn(...)(CE2434), and a per-slot take of one payload out of a variant (CE2433). - A scrutinee-side spelling for a mutable binding (Rust's
match &mut x). Sushi marks the binding instead.