Ownership, References, and Borrowing
Shape gives you Rust-level memory safety with Python-level ergonomics. The compiler enforces strict aliasing rules at compile time — no runtime overhead, no garbage collector pauses, no data races.
Binding Forms and Ownership
Section titled “Binding Forms and Ownership”Shape has three binding keywords with distinct ownership semantics:
| Keyword | Mutable? | Ownership | When to use |
|---|---|---|---|
let | No | Unique | Default. Immutable bindings. |
let mut | Yes | Unique | Mutable locals, accumulators, loop counters. |
var | Yes | Shared (copy-on-write) | Shared mutable state, closures. |
let x = 42 // immutable, uniquely ownedlet mut count = 0 // mutable, uniquely owned — direct mutationvar shared = [1, 2, 3] // mutable, shared — copy-on-write on mutationlet and let mut — unique ownership
Section titled “let and let mut — unique ownership”let and let mut bindings are uniquely owned. There is exactly one owner at any time, and assignment transfers ownership (a “move”):
let a = [1, 2, 3]let b = a // MOVE — a is invalidated// print(a) // ERROR: use of moved valueBecause there is only one owner, let mut mutations happen directly with zero overhead — no coordination or copying required:
let mut items = [1, 2, 3]items.push(4) // Direct mutation — single owner, no copy neededvar — shared ownership
Section titled “var — shared ownership”var bindings use shared ownership. Multiple bindings can reference the same data, and mutations use copy-on-write: the data is cloned before mutation only when it has multiple references.
var data = [1, 2, 3]let alias = data // Both reference the same datadata.push(4) // Copy-on-write: data is cloned, then mutated// (SURFACE: v0.3 G.1 (b)-class §3.B.1 — `var` + alias + `.push` CoW path// currently SEGFAULTs under JIT and produces a dev-mode auto-print of an// internal future-ptr under VM. Documented contract preserved; gating// item tracked at `docs/cluster-audits/v0.3-g1-step1-fundamentals.md`.)var is essential when you need to share mutable state across closures or when multiple parts of your code reference the same value. For single-owner cases, prefer let or let mut for better performance.
Move and Clone
Section titled “Move and Clone”Non-Copy types (strings, arrays, objects) follow ownership rules:
let a = [1, 2, 3]let b = a // MOVE — a is invalidated// print(a) // ERROR: use of moved value
let c = [4, 5, 6]let d = clone c // CLONE — c stays validprint(c) // OK: c is still validCopy types (int, number, bool, none) are implicitly copied — no borrow tracking needed.
References
Section titled “References”A reference borrows a value without taking ownership.
Creating References
Section titled “Creating References”let x = 42let r = &x // shared reference to xlet mut arr = [1, 2, 3]let m = &mut arr // exclusive (mutable) reference to arrReference Rules
Section titled “Reference Rules”Two rules govern all borrowing:
- Any number of shared borrows (
&) can coexist. - At most one exclusive borrow (
&mut), and it cannot overlap with any shared borrow.
While a value is borrowed:
- The owner cannot be written while any borrow (shared or exclusive) is active.
- The owner cannot be read while an exclusive borrow is active.
let mut x = 10let r1 = &x // shared borrowlet r2 = &x // OK: multiple shared borrows// let m = &mut x // ERROR B0001: can't take &mut while & existsprint(r1) // last use of r1, r2let m = &mut x // OK: shared borrows ended (NLL)References as Function Parameters
Section titled “References as Function Parameters”The most common use of references is passing values to functions:
fn read_first(&arr) { return arr[0]}
fn append(&mut arr, value) { arr.push(value)}
let mut data = [1, 2, 3]let first = read_first(&data) // shared borrow for the callappend(&mut data, 4) // exclusive borrow for the callInferred Reference Mode
Section titled “Inferred Reference Mode”When you don’t annotate a parameter, the compiler infers the mode from usage:
fn total(items) { // compiler sees items is only read → infers &items (shared) var sum = 0 for item in items { sum = sum + item } return sum}The LSP shows the inferred mode as an inlay hint: items: &[int].
Auto-Referencing for Method Calls
Section titled “Auto-Referencing for Method Calls”Method calls create temporary borrows automatically:
let mut arr = [1, 2, 3]arr.push(4) // desugars to (&mut arr).push(4)arr.len() // desugars to (&arr).len()Reference Capabilities and Limits
Section titled “Reference Capabilities and Limits”References support several advanced patterns:
- Disjoint field borrows work:
&mut obj.aand&mut obj.bcan coexist because the MIR solver tracks places at field granularity. - Index borrowing is supported:
&arr[0]creates a reference into the array. - References in local containers: parameter borrows can be stored in local containers if the borrow provably outlives the container.
return &xfor locals: detected by the MIR solver (no hard-coded rejection) — the solver proves the reference would escape the referent’s region.
References are scoped borrows — they cannot escape their referent’s region:
// ERROR: MIR solver detects reference would escape local's regionfn bad() { let x = 42 return &x // x would be destroyed, reference would dangle}
// OK: return the VALUE through a referencefn read_val(&x) { return x // returns the dereferenced value, not the reference}Task Boundary Rules
Section titled “Task Boundary Rules”References interact with concurrency boundaries:
- Detached tasks: all references (shared and exclusive) are rejected across detached task boundaries.
- Structured tasks: only exclusive (
&mut) references are rejected; shared (&) references are allowed because they are truly immutable and scope-bounded.
Non-Lexical Lifetimes (NLL)
Section titled “Non-Lexical Lifetimes (NLL)”Shape uses non-lexical lifetimes — a borrow ends at its last use, not at the end of its scope:
let mut data = [1, 2, 3]let r = &data // shared borrow startsprint(r[0]) // last use of r — borrow ENDS heredata.push(4) // OK: no active borrowsWithout NLL, data.push(4) would fail because r is still “in scope.” NLL eliminates these false positives.
Borrow Error Codes
Section titled “Borrow Error Codes”| Code | Meaning | Common Fix |
|---|---|---|
| B0001 | Borrow conflict (& + &mut on same variable) | Reorder code so borrows don’t overlap |
| B0002 | Write to owner while borrowed | Move the write after the last borrow use |
| B0003 | Reference escapes its scope | Return an owned value instead |
| B0004 | Unexpected & on non-reference parameter | Remove the & — parameter is by-value |
Every error includes the conflict location, a note pointing to the first borrow, and a suggested fix with a code diff.
Concurrency Rules
Section titled “Concurrency Rules”Shape replaces Rust’s Send/Sync traits with three simple rules:
| What crosses a task boundary | Structured task | Detached task | Why |
|---|---|---|---|
| Owned value (move or clone) | Yes | Yes | No aliasing possible |
&T (shared reference) | Yes | No | Structured: scope-bounded; Detached: could outlive referent |
&mut T (exclusive reference) | No | No | Would create aliased mutation |
let mut data = [1, 2, 3]let snapshot = clone data // clone must be at let-init position (see Grammar)async let result = process(snapshot) // OK: detached child gets owned snapshotasync let view = read_only(&data) // OK: shared ref in structured childasync let bad = modify(&mut data) // ERROR C0001: exclusive ref across taskFor shared mutable state across tasks, use Mutex<T>, Atomic<T>, or channels.
Quick Reference
Section titled “Quick Reference”// Immutable bindinglet x = value
// Mutable bindinglet mut x = value
// Auto-inferred bindingvar x = value
// Shared referencelet r = &xfn f(¶m) { ... }
// Exclusive referencelet m = &mut xfn g(&mut param) { ... }
// Explicit movelet y = move x
// Explicit clonelet y = clone xSee Ownership Deep Dive for the full specification including the MIR, Datafrog solver, repair engine, and concurrency primitives.