14 — Ownership, Borrows, and Drop¶
Mighty enforces single-owner semantics for non-Copy values. The compiler tracks every binding's ownership state through the body of each fn / handler / lambda, and reports moves, borrows, and drops with matching diagnostics in the MT3001..MT3099 range.
This chapter walks the rules with worked examples. See
spec §7 and
docs/internals/borrowck.md for the full
reference. Run mty explain MT3001 (etc.) for the
Cause/Example/Fix/Spec block on any specific code.
Ownership and move¶
A non-Copy value has exactly one owner. Reassigning moves the value:
Without the explicit move keyword Mighty does NOT silently move the
value — assignment requires being clear about intent. (Calling a fn that
takes a non-Copy value also moves; see "Calls and parameters" below.)
What can be reused freely (Copy types)¶
Primitives, shared references, raw pointers, function pointers, and
tuples/arrays of Copy values are implicitly copyable. No move required;
no MT3001 risk.
Immutable borrows: &T¶
A shared borrow lets you read the value without consuming it. Many shared borrows can coexist:
let buf = String("data")
let r1 = &buf
let r2 = &buf
log_len(r1)
log_len(r2)
// r1 and r2 decay at end of scope; buf is owned again
Borrows deactivate at the last use of the borrower binding (NLL, v0.3 / A55). Programs that were once rejected for lexical reasons now work — for example:
let mut buf = String("data")
let r = &buf
log_len(r) // r's last use; the shared borrow ends here
let m = &mut buf // OK — no live shared borrow
push(m, "!")
Pre-v0.3 this would have errored with MT3004 because r's borrow was
"live" until the end of the enclosing block. v0.3 sees that r is
last used at log_len(r), so the borrow ends there.
Mutable borrows: &mut T¶
At most one mutable borrow may exist at a time. While it lives, no shared borrow may coexist:
let mut buf = String("data")
let m = &mut buf
push(m, "!")
// m decays here; you can now read or mutably borrow buf again
Errors you might trip:
MT3004 mut_borrow_while_shared— created&mutwhile&was liveMT3005 shared_borrow_while_mut— created&while&mutwas liveMT3006 two_mut_borrows— created a second&mutMT3013 mut_borrow_of_immut_local— used&muton aletwithoutmut
Field-level borrows (v0.3 / A54)¶
Borrows of disjoint fields of the same struct don't conflict:
struct Pair { a: String, b: String, }
let mut s = Pair { a: String("x"), b: String("y") }
let ra = &mut s.a // borrow of place s.a
let rb = &s.b // borrow of place s.b — disjoint
push(ra, "!")
log_len(rb) // both succeed
The checker tracks borrows at Place granularity — a rooted
projection path like s.a or arr[_]. Two borrows conflict iff
their Places overlap (one is a prefix of the other). v0.3 truncates
projection chains at depth 1, so &s.a.x and &s.a.y still conflict
(folded to &s.a); v0.4 will deepen.
Moves through references (v0.3 / A56)¶
Dereferencing a reference does NOT transfer ownership. For a non-Copy
type, this errors with MT3009 move_out_of_ref:
For Copy types (primitives, references, function pointers), *r is
just a load:
Fix: clone, take ownership, or work with the borrow directly.
Calls and parameters¶
Non-Copy arguments are moved into the fn unless the parameter type is
&T / &mut T:
fn take(s: String) {} // takes ownership
fn read(s: &String) {} // reads via shared borrow
fn fill(s: &mut String) {} // writes via mutable borrow
let owned = String("x")
read(&owned) // shared borrow; owned still usable
take(move owned) // ownership transferred; owned is now Moved
Drop and scope exit¶
Owned non-Copy values are dropped (deterministically) at end of their
scope. Slice 4 records this as drop intent in an internal DropPlan;
real codegen of .drop() calls arrives in a later slice. From the
language perspective the contract is: when you no longer own a value at
scope end, no leak; when you do own one, its Drop runs.
Arenas: scope-bound allocation¶
Values created inside an arena block may not escape the arena's scope
unless they are Copy or you explicitly move them out:
fn turn(input: Str) -> Lowered!ParseErr {
arena turn {
let toks = tokenize(input)
let ast = parse(toks)?
lower(ast) // OK — lower returns a fresh non-arena value
}
}
Trying to return an arena-local binding directly is MT3010 arena_escape:
To return an arena-local value, copy it (if Copy) or restructure the computation so the arena's tail is a derived value.
Cross-agent messages: Sendable¶
Arguments to !Msg(args) (send) or ?Msg(args) (ask) must be
Sendable: Copy ∨ owned-String/Bytes ∨ Sendable
tuples/arrays/structs. References and raw pointers can't cross agent
boundaries.
Pass owned data, copies, or convert to a Sendable form first.
Quick reference¶
| Symptom | Code | Fix |
|---|---|---|
| Used a moved local | MT3001 | Don't reuse, or clone before the move |
| Borrowed after move | MT3003 | Same |
&mut while & is live |
MT3004 | Reorder, narrow scope, or use a fresh borrow |
& while &mut is live |
MT3005 | Same |
Two &mut to same value |
MT3006 | Sequence them; only one mut borrow at a time |
| Moved a borrowed value | MT3008 | Move only after the borrow ends |
| Moved out of a reference | MT3009 | Clone or borrow; don't *ref a non-Copy value |
| Arena-local escapes | MT3010 | Copy out, or restructure to return a derived val |
| Cross-agent arg is not Sendable | MT3011 | Pass owned data; don't ship references |
&mut x but x not mut |
MT3013 | let mut x = ... |
Assigned to non-mut local |
MT3014 | let mut x = ... |
| Used un-initialized binding | MT3015 | Initialise the binding before its first read |
Try it¶
There is no single 14_ownership.mty example file — the ownership
machinery is exercised by every program. To experiment, paste any
snippet above into scratch.mty and run:
Borrow conformance is also covered by the
tests/conformance/
suite, which runs as part of cargo test.
Next¶
Continue to 15 — Traits. For the full borrow-checker
implementation notes, see
docs/internals/borrowck.md.