Quartz v5.25

Borrowing & Mutation

TL;DR: Quartz prevents memory safety bugs at compile time with zero annotation burden. Most users will never encounter the borrow checker — it only activates when you use & or &mut.


The Problem

Memory safety bugs — use-after-free, dangling pointers, data races from aliased mutation — are the #1 source of security vulnerabilities in systems code. Chrome, Microsoft, and Android all report 65-70% of their security bugs come from this single class of error.

The root cause is aliased mutation: two references to the same data where at least one can write. One reference reads stale data, or two writers corrupt each other.

The Rule

Many readers OR one writer. Never both.

Quartz enforces this at compile time with zero runtime cost.


Two Kinds of Borrows

&x — Shared (Read-Only) Borrow

Multiple shared borrows can coexist. Nobody can write.

var p = Point { x: 3, y: 4 }
a = point_sum(&p)     # OK — read-only
b = point_sum(&p)     # OK — multiple readers fine

&mut x — Exclusive (Mutable) Borrow

Only one exclusive borrow at a time. The borrower can write back to the original.

var p = Point { x: 0, y: 0 }
set_x(&mut p, 42)
# p.x is now 42 — mutation went through the borrow

The Rules

There are exactly five rules. If you remember these, you know the entire borrow checker.

1. Multiple & is fine — multiple &mut is not

var x = 42
a = &x       # OK
b = &x       # OK — multiple shared borrows

var y = 42
a = &mut y   # OK
b = &mut y   # ERROR (QZ1205) — only one exclusive borrow allowed

2. & and &mut cannot coexist

var x = 42
a = &x       # shared borrow
b = &mut x   # ERROR (QZ1206) — conflicts with shared borrow

var y = 42
a = &mut y   # exclusive borrow
b = &y       # ERROR (QZ1206) — conflicts with exclusive borrow

3. Cannot mutate while borrowed

var x = 42
p = &x
x = 99       # ERROR (QZ1209) — x is borrowed, can't mutate it

This applies to all mutation forms: =, +=, field assign, index assign.

4. Cannot return a borrow

def bad(): Int
  var x = 42
  return &x   # ERROR (QZ1210) — x dies when bad() returns
end

The variable is destroyed when the function returns. The borrow would dangle.

This also applies indirectly — returning a binding that holds a borrow:

def also_bad(): Int
  var x = 42
  var r = &x
  return r    # ERROR (QZ1210) — r holds a borrow of x
end

5. Cannot store a borrow in a struct field

struct Holder
  ptr: Int
end

var x = 42
h = Holder { ptr: &x }  # ERROR (QZ1211) — borrow may outlive source

The struct could be passed around, outliving the variable it borrows from.

This also applies indirectly — storing a binding that holds a borrow:

var x = 42
var r = &x
h = Holder { ptr: r }   # ERROR (QZ1211) — r holds a borrow of x

Ephemeral Borrows

Borrows passed as function arguments are ephemeral — they’re released when the call returns.

def peek(r: &Int): Int = 0

var x = 42
peek(&x)        # shared borrow created, then released
var p = &mut x   # OK — the shared borrow is gone

This is the common case. Most borrows are ephemeral and never cause issues.

Stored borrows (assigned to a variable) live until their last use (NLL-lite):

var x = 42
var p = &x       # stored borrow
var y = p + 1    # last use of p — borrow released here
x = 99           # OK — p's borrow has expired

If the borrow is used after the mutation, it’s still an error:

var x = 42
var p = &x
x = 99           # ERROR (QZ1209) — p is still live (used below)
var y = p + 1    # last use of p

Reassignment Releases Borrows

If you reassign a variable that holds a borrow, the old borrow is released:

var x = 42
var p = &x       # p borrows x (shared)
p = 0            # p reassigned — borrow on x released
var q = &mut x   # OK — x is no longer borrowed

Borrows Inside Loops

Borrows created inside a loop body are scoped to that iteration. They don’t leak across iterations or persist after the loop:

var x = 42
var i = 0
while i < 3
  var r = &x     # borrow created each iteration
  var y = r + 1  # last use — NLL releases it
  i = i + 1
end
x = 99           # OK — no borrow persists after the loop

Borrows created before a loop are unaffected — they keep their existing lifetime:

var x = 42
var r = &x       # borrow created before loop
while i < 3
  i = i + 1
end
var y = r + 1    # OK — r is still live

Lifetime Diagnostics

Borrow errors include dual-span information — both the error site and the borrow creation site:

error[QZ1209]: Cannot mutate 'x' while it is borrowed by 'r' (borrow created at line 3, live until line 5)
 --> file.qz:4:3

For dangling reference errors:

error[QZ1218]: Dangling borrow — 'r' borrows 'inner' which is going out of scope (borrow created at line 5)

&mut Requires var

You can only create a mutable borrow of a mutable binding:

x = 42           # immutable (no var)
p = &mut x       # ERROR (QZ1208) — x is not mutable

var y = 42       # mutable
p = &mut y       # OK

Error Code Reference

CodeErrorMeaning
QZ1205Cannot create exclusive borrowVariable is already &mut borrowed
QZ1206Conflicting borrowsMixing & and &mut on the same variable
QZ1208Cannot &mut immutable bindingTarget must be declared with var
QZ1209Cannot mutate while borrowedAny assign/compound-assign while a borrow exists
QZ1210Cannot return borrow of localLocal dies at function exit; borrow would dangle
QZ1211Cannot store borrow in structStruct may outlive the borrowed variable

All error codes support quartz --explain QZ1209 for detailed help.


What This Doesn’t Cover

Quartz’s borrow checker is deliberately simpler than Rust’s. These patterns are not supported and don’t need to be:

PatternWhy Not
Borrowing through containers (Vec, Map)Would need generic lifetime params
Borrow splitting (different struct fields)Not worth the complexity
Named lifetime annotations ('a)NLL-lite covers the common cases without annotations

For the rare cases that need these patterns, use CPtr and FFI.


Who Needs to Know This

Most users: nobody. The borrow checker is silent for code that doesn’t use & or &mut. It only activates when you explicitly create borrows.

Systems programmers passing data by reference will encounter these rules. The errors are clear, the fixes are straightforward, and there’s nothing to annotate.

Coming from Rust? You already know this, minus the lifetimes. That’s the whole difference.