| Field | Value |
|---|---|
| Status | Accepted |
| Depends on | RFC-0005 |
| Algorithm reference | Rust NLL RFC 2094; Polonius datalog engine (design inspiration) |
| MLIR reference | mlir/include/mlir/Analysis/DataFlow/ (dataflow framework) |
The borrow checker is a pipeline of five MLIR analysis passes that run sequentially on the ownership HIR after construction and before any lowering. All five passes are read-only analyses — they emit diagnostics but do not transform IR. If any diagnostic is emitted, compilation halts before lowering begins.
HIR (own + task dialect, unverified)
│
▼
Pass 1: Liveness analysis
│ live-in / live-out sets per block
▼
Pass 2: Region inference
│ concrete region spans for all borrows
▼
Pass 3: Aliasing constraint check
│ Rule A, B, C (see below)
▼
Pass 4: Move validity check
│ linearity + conditional move detection
▼
Pass 5: Send / Sync check
│ task.spawn capture validation
▼
HIR (verified — safe to lower)
Computes live-in and live-out sets per basic block for all !own.val SSA values.
Uses backward dataflow over the CFG.
Formal definition:
live-out(B) = ⋃ live-in(S) for all successors S of B
live-in(B) = use(B) ∪ (live-out(B) − def(B))
where:
use(B)= set of!own.valSSA values used in B before being defined in Bdef(B)= set of!own.valSSA values defined in B (by consuming ops)
A value V is live at a program point p if there exists a path from p to a
consuming use of V that does not pass through another consuming use of V.
Complexity: O(N × B) where N = number of !own.val defs, B = number of basic blocks.
Output: A LivenessInfo object attached to each function, queried by subsequent passes.
Assigns a concrete region (a span of program points) to every borrow.ref and
borrow.mut op. Uses the NLL algorithm from Rust RFC 2094.
Algorithm:
- For each borrow op, compute the initial region: the minimal span from the borrow op to its last use (using liveness from Pass 1).
- Propagate outlives constraints:
- If borrow
R1is returned from a block that is in regionR2, thenR1 ⊇ R2 - If borrow
R1flows into a phi node that feeds regionR2, thenR1 ⊇ R2
- If borrow
- Solve constraints via union-find over block ranges until fixed point.
Constraint types generated:
| Constraint | Meaning |
|---|---|
R ⊆ live(V) |
Borrow region must not outlive the source !own.val |
R ∩ drop(V) = ∅ |
Borrow region must not cross a block where source is moved/dropped |
R_mut ∩ R_any = ∅ |
Mutable borrow region must not overlap any other borrow on same source |
Output: A RegionMap mapping each borrow op to its solved region span.
Verifies three aliasing rules at every program point using the regions from Pass 2.
For any !own.val V, at any program point p:
|{ R : R is borrow.mut region of V, p ∈ R }| ≤ 1
For any !own.val V, at any program point p:
(∃ R_mut : p ∈ R_mut, R_mut is borrow.mut of V)
⟹ (∀ R_ref : R_ref is borrow.ref of V, p ∉ R_ref)
For any own.drop(V) at point p:
∀ R : R is borrow region of V → p ∉ R
Violation diagnostic format:
error: cannot borrow `<name>` as mutable because it is also borrowed as shared
--> src/file.ts:<line>:<col>
|
<line> | <source excerpt — shared borrow>
| ^^^^ shared borrow of `<name>` occurs here
<line> | <source excerpt — mutable borrow>
| ^^^^ mutable borrow occurs here
|
note: shared borrow is active until line <line>
Verifies that every !own.val SSA value is consumed exactly once, and that no consuming
use occurs after the value has already been consumed.
Detected directly from the SSA use-def chain: if an !own.val has more than one
consuming use, this is a use-after-move error.
error: use of moved value `<name>`
--> src/file.ts:<line>:<col>
|
<line> | process(data); // data moved here
| ^^^^ value moved here
<line> | log(data); // use after move
| ^^^^ value used after move
If a value V is moved in one branch of a conditional but not the other, the compiler
cannot statically guarantee it is alive at the join point. This is a warning (not an
error) and triggers drop flag insertion (RFC-0008):
warning: value `<name>` may be moved in some branches
--> src/file.ts:<line>:<col>
note: drop flag inserted at join point <line>:<col>
The borrow checker marks these values in the LivenessInfo object so the drop insertion
pass (RFC-0008) can emit conditional own.drop ops.
Runs as a verifier on the task.spawn op rather than a separate pass. Fires whenever a
task.spawn op is constructed in the IR.
For each captured !own.val<T, send, sync> operand of task.spawn:
- If
send = false→ compile error identifying the non-Send type and spawn location
error: captured value `<name>` cannot be sent between threads safely
--> src/file.ts:<line>:<col>
|
<line> | task.spawn(() => { use(data); });
| ^^^^ `Buffer` is not `Send`
|
help: annotate `Buffer` with `@send` if it is safe to move across threads
!borrow<T> (shared ref) is not Send unless T is @sync.
!borrow.mut<T> (exclusive ref) is never Send.
All borrow checker diagnostics follow Clang's structured diagnostic format:
- Primary message — one sentence, names the construct
- Source excerpt — file, line, column, caret
- One or more notes — point to related locations
- Optional suggestion — concrete fix-it when the fix is unambiguous
Diagnostics are emitted via the MLIR DiagnosticEngine, which supports both human-readable
and JSON (--error-format json) output for LSP integration (RFC-0010).