12 Borrowing — ref and mut_ref
What to know first
mut slicefield is a place to read and to writeLooking back
In chapter 9, what rejected writing to an element of a slice without mut? And how does immutability of a name made with let differ from immutability of a slice’s elements?
A. It was rejected with E-TYPE-MUT. let forbids putting a different value into the name, while whether elements can be written is decided by the slice’s type (slice or mut slice). This chapter covers references that borrow a single value that is not a slice, and the rules for when borrows overlap.
The need for this chapter, and its context
By the end of this chapter
ref t and mut_ref t and read them with deref. You will see that a read borrow cannot write and that write permission only narrows in one direction. You will meet the exclusivity rule — borrows of one value are “many readers or one writer” — and the rule that a reference to a local cannot leave its op. You will also understand why mut ref slice is rejected.The questions this chapter answers
- How do you make a null reference?
12.1 Two kinds of borrow#
A reference points to a value that lives elsewhere. There are only two kinds, and the name is the permission.
ref t— a read borrow. Many can hold one at the same time. It cannot change the value.mut_ref t— a write borrow. There can be only one for a value at a time, and no read borrows meanwhile.
Think of a library book. Many people can read it at once, but to write corrections in it you must borrow it alone, and nobody reads it meanwhile — otherwise a reader would see half-made corrections.
examples/ch12/borrow.low
module borrow .
rem run: use_sum 7
rem run: use_bump 7
struct big do
a u64 .
b u64 .
c u64 .
d u64 .
end
fn total input v ref big . output u64 .
requires le (field v a) 1000 .
requires le (field v b) 1000 .
requires le (field v c) 1000 .
requires le (field v d) 1000 .
do
return add (add (field v a) (field v b)) (add (field v c) (field v d)) .
end
fn use_sum input x u64 . output u64 .
requires le x 1000 .
do
let v big be make big do a x . b 2 . c 3 . d 4 . end .
return add (total (ref v)) (total (ref v)) .
end
proc bump input p mut_ref u64 . output u64 .
do
set p (add (deref p) 1) .
return deref p .
end
proc use_bump input start u64 . output u64 .
requires lt start 1000 .
do
var n u64 be start .
let r u64 be bump (mut_ref n) .
return add n r .
end
Output
$ lowentc --run use_sum borrow.low 7
use_sum(7) = 32
$ lowentc --run use_bump borrow.low 7
use_bump(7) = 16
totalborrows the four-fieldbigwithref bigto read it without copying.field v areads a field through the reference.use_sumborrows the same value twice withref v. Read borrows may be many.bumptakes amut_ref u64and changes the caller’s value withset p …. The value a reference points to is read withderef p.use_bumplendsvar nasmut_ref n. Afterbumpreturns,nis 8.
What matters is that the borrower writes ref x or mut_ref n at the call site. Someone reading the call sees from that place alone that this call can change n.
Q. How do you make a null reference?
A. You cannot. A reference always points to a live value. To express “points to nothing”, use an option (chapter 11). Then the code that checks before taking out shows in the source, and taking out without checking stops.
12.2 Write permission only narrows#
Writing through a read borrow is rejected.
examples/ch12/ref_write.low
module ref_write .
rem expect: E-TYPE-REF
proc reset input p ref u64 . output u64 .
do
set p 0 .
return 0 .
end
Output
$ lowentc --check ref_write.low
ref_write.low:6:0 E-TYPE-REF: write through a shared ref (declare mut_ref)
The other direction is blocked too. Passing a value received only for reading to a position that takes mut, mut_ref or owned is rejected with E-TYPE-ARGMUT. Something writable can be passed on for reading, but something read-only cannot be passed to a writable position. Whoever received a value for reading must be able to trust that it does not change while they look at it, and that trust holds only when nobody gains write permission behind their back.
12.3 Many readers or one writer#
The trouble is overlapping borrows of the same value. Two overlapping write borrows are rejected.
examples/ch12/excl.low
module excl .
rem expect: E-EXCL
proc set_both input a mut_ref u64 . input b mut_ref u64 . output void .
do
set a 1 .
set b 2 .
end
proc confused output u64 .
do
var n u64 be 0 .
set_both (mut_ref n) (mut_ref n) .
return n .
end
Output
$ lowentc --check excl.low
13:0 E-EXCL: exclusivity violation: overlapping borrow/owner access (readers-XOR-writer)
The body of set_both believes a and b are different values. Lending the same n twice breaks that belief, and whether 1 or 2 remains depends on the order of writes. Overlapping read and write is rejected too.
examples/ch12/stale.low
module stale .
rem expect: E-EXCL
proc look_then_write output u64 .
do
var n u64 be 0 .
let r ref u64 be ref n .
set n 5 .
return deref r .
end
Output
$ lowentc --check stale.low
8:0 E-EXCL: exclusivity violation: overlapping borrow/owner access (readers-XOR-writer)
Writing 5 to n while r holds a read borrow of n means whoever reads through r cannot tell when the value they saw changed. These two shapes — another flow changing a value at the same time, and a value changing while being read — are defects hard to find by reading the source, so a rule removes them.
Laying the borrows of one value n out in time makes the rule visible at a glance.
time → ① ② ③ ④ ⑤
ref r1 n ├─────────────────┤ two reads may overlap
ref r2 n ├─────────────────┤
mut_ref w n ├────────┤ a write only when alone
──────────────────────────────────────────────────────────────
✘ ref r n ├──────────────────────────┤
set n 5 ● the value changes while read → refused
✘ mut_ref a n ├──────────────┤
mut_ref b n ├──────────────┤ two writes overlap → refusedA common misconception. The exclusivity rule only matters for multithreaded programs
memcpy is undefined for overlapping buffers. But once the rule holds within one flow, the absence of data races when the work is split into several flows follows, and that is where the proofs of Part VII lean (chapter 45).12.4 A borrow cannot outlive what it borrows#
Returning a reference to a local from its op is rejected.
examples/ch12/escape.low
module escape .
rem expect: E-ESCAPE
fn leak output ref u32 .
do
let here u32 be 42 .
return ref here .
end
Output
$ lowentc --check escape.low
7:0 E-ESCAPE: reference to a local escapes the op (dangling)
If this code were translated, the returned reference would point at a value already gone, and nobody knows what reading it would give. Lowent knows this not by running it but at translation time. A borrow cannot leave the block that opened it (E-BORROW-ESCAPE), and a borrow still alive after the lent value has moved is rejected too (E-EXCL-MOVED).
If a value must leave the op, return the value rather than a reference, or put it in storage the caller passed. If you need storage that outlives the caller, use a region (chapter 18).
12.5 There is no mut ref slice#
Slices do not take mut ref.
examples/ch12/mref_slice.low
module mref_slice .
rem expect: E-MREF-SLICE
proc shrink input p mut ref slice u8 . output u64 .
do
return 0 .
end
Output
$ lowentc --check mref_slice.low
mref_slice.low:4:0 E-MREF-SLICE: `mut ref slice` gives nothing that `mut slice` does not — the elements are already writable through a plain `mut slice` (the descriptor is copied, the bytes are shared). The ONLY thing it adds is replacing the caller's descriptor, which silently changes the length behind the caller's back. Say the new slice with a RETURN VALUE instead (SPEC-004 §4.4a: the windows through which someone else can change your local are listed, and this one is closed)
Passing a mut slice already makes the elements writable. The slice value {start, length} is copied, but the bytes it points to are the same. So what mut ref slice adds is exactly one thing — secretly changing the length and start of the caller’s slice. Then the caller’s loop has its length change even though its own code contains no assignment. Return the new slice instead.
examples/ch12/head.low
module head .
rem run: first_two [9,8,7,6]
fn take_front input p slice u8 . input n u64 . output slice u8 .
requires le n (len p) .
do
return subslice p 0 n .
end
fn first_two input xs slice u8 . output u64 .
requires ge (len xs) 2 .
do
let front slice u8 be take_front xs 2 .
return len front .
end
Output
$ lowentc --run first_two head.low [9,8,7,6]
first_two([9,8,7,6]) = 2
arg0 (written) = [9,8,7,6]
take_front returns a new slice pointing at the first n elements. That the length changed shows in the return value and in let front.
12.6 Common mistakes#
Counter-example. Forgetting mut_ref at the call site
examples/ch12/mistake_nomutref.low
module mistake_nomutref .
rem expect: E-TYPE-ARG
proc bump input p mut_ref u64 . output u64 .
do
set p (add (deref p) 1) .
return deref p .
end
proc use_bump input start u64 . output u64 .
requires lt start 1000 .
do
var n u64 be start .
rem ✘ `mut_ref` is missing at the call --- the number 7 is passed, not a reference
let r u64 be bump n .
return add n r .
end
Output
$ lowentc --check mistake_nomutref.low
mistake_nomutref.low:15:0 E-TYPE-ARG: this op takes a BORROW here (`ref` / `mut_ref`), and a plain value was passed. Take the borrow at the call — `mut_ref <name>` — so the reader sees where the callee may write. It used to pass `--check` and stop at run time with `E-VM-TYPE: deref needs a reference`
bump expects “a reference to a number”, but bump n passes the number 7 itself. C++ reference parameters need no mark at the call, but Lowent makes you write mut_ref n so the call alone tells you what may change. This edition’s tool does not reject the omission at translation time; at run time deref meets something that is not a reference and stops (recorded as a defect in the development repository). When the stop says “deref needs a reference”, look for a missing mut_ref or ref at the call site.
Counter-example. Using a reference as a number without deref
examples/ch12/mistake_noderef.low
module mistake_noderef .
rem expect: E-TYPE-REFVAL
fn twice input p ref u64 . output u64 .
do
rem ✘ `p` is not a number but a reference to one --- read it with `deref p`
return add p p .
end
fn double input x u64 . output u64 .
requires le x 1000 .
do
let n u64 be x .
return twice (ref n) .
end
Output
$ lowentc --check mistake_noderef.low
mistake_noderef.low:7:0 E-TYPE-REFVAL: a BORROW is being used where a number is expected. `ref t` / `mut_ref t` names a place, not the value in it — read it with `deref <name>` (and write through it with `set <name> …`). It used to pass `--check` and stop at run time with `E-VM-TYPE`, which blamed the arithmetic instead of the missing read
A reference is where a value lives, not the value. Each read spells deref p, making “read the borrowed value here” visible. In C, dropping the * turns the code into address arithmetic; Lowent has no address arithmetic, so a wrong value never comes out quietly. This edition’s tool does not report it at translation time, though, and stops at run time instead (recorded as a defect). The fixed line is add (deref p) (deref p).
Counter-example. Lending a let name for writing
examples/ch12/mistake_letmutref.low
module mistake_letmutref .
rem expect: E-TYPE-ARGMUT
proc bump input p mut_ref u64 . output void .
do
set p (add (deref p) 1) .
end
proc use_bump input start u64 . output u64 .
requires lt start 1000 .
do
rem ✘ a name made with `let` is lent for writing
let n u64 be start .
bump (mut_ref n) .
return n .
end
Output
$ lowentc --check mistake_letmutref.low
mistake_letmutref.low:14:0 E-TYPE-ARGMUT: a WRITE borrow (`mut_ref`) was taken of a name that cannot be written: a `let` binding (or a shared input). `let` says the value does not change (§6.5.1) — if a borrow could change it, the reader who checked that name once would be wrong, and the borrow makes the change invisible at the call site. Bind it with `var`, or take a read borrow (`ref`)
let promises “the value of this name does not change”. Lending such a name with mut_ref breaks that promise, so it is refused with E-TYPE-ARGMUT. A borrow changes the value invisibly at the call site, which is exactly what would cost let its worth: that checking the name once is enough. Make a value that must change a var from the start, so the reader learns from the declaration that it changes somewhere.
A common misconception. A variable passed as input can be changed by the op it is passed to
examples/ch12/copyparam.low
module copyparam .
rem run: use_inc 7
proc inc input x u64 . output u64 .
requires lt x 1000 .
do
rem `x` is a copy of the caller's value --- nothing done here reaches the caller's variable
var y u64 be x .
set y (add y 1) .
return y .
end
proc use_inc input start u64 . output u64 .
requires lt start 1000 .
do
var n u64 be start .
let r u64 be inc n .
rem `n` is still 7 --- lend it with `mut_ref n` to let it change
return n .
end
Output
$ lowentc --run use_inc copyparam.low 7
use_inc(7) = 7
A plain value parameter (input x u64) receives a copy of the caller’s value. Whatever inc does inside, n in use_inc stays
- Some languages pass large values by reference behind your back and blur this line; in Lowent the only way to change a caller’s
value is mut_ref, and that mark stays at the call site.
A common misconception. Within one op you may lend a value with mut_ref only once
examples/ch12/seqborrow.low
module seqborrow .
rem run: use_twice 7
proc bump input p mut_ref u64 . output void .
requires lt (deref p) 1000 .
do
set p (add (deref p) 1) .
end
proc use_twice input start u64 . output u64 .
requires lt start 900 .
do
var n u64 be start .
rem the first borrow comes back when this call ends --- so the next line may borrow again
bump (mut_ref n) .
bump (mut_ref n) .
return n .
end
Output
$ lowentc --run use_twice seqborrow.low 7
use_twice(7) = 9
The exclusivity rule forbids borrows that overlap in time. A mut_ref n passed to one call comes back when that call ends, so the next line may borrow again. What is rejected is a shape like set_both (mut_ref n) (mut_ref n), where two borrows live inside a single call.
12.7 This chapter’s syntax at a glance#
| Shape | Meaning | Why |
|---|---|---|
input v ref big . | receive a read borrow | no copy of a large value, and a promise not to change it |
input p mut_ref u64 . | receive a write borrow | the only way to change a caller’s value |
total (ref v) · bump (mut_ref n) | lend at the call site | the call alone shows what may change |
deref p | read the value a reference points to | a value and its place are never mixed up |
set p <value> . | write where a reference points (mut_ref) | through a read borrow it is E-TYPE-REF |
field v a | read a field through a reference | same spelling for a reference to a struct |
two mut_ref of one value · ref plus a write | overlapping borrows — rejected (E-EXCL) | many readers or one writer |
| one storage in a written and a read position | only when the callee allows it with inplace <written> <read> . and the ranges are the same — else E-EXCL-INPLACE | a body that reads and writes element by element is right only on the same range |
the body of an op that declares inplace o a | write o only after every read of a, or read and write element by element with one index — else E-INPLACE-UNPROVEN | the processor checks the declaration against the body |
using an old view after calling an op that declares invalidates <input> on its storage | rejected (E-VIEW-INVALIDATED) — take the view again | after a release, a growth or a rewind the view points at someone else’s place |
return ref here . | return a reference to a local — rejected | never point at a value that is gone |
mut ref slice | does not exist — rejected (E-MREF-SLICE) | no hidden length change — return a new slice |
Table 12.1 — Borrowing syntax — shape · meaning · why it looks this way
Recap
ref t is a read borrow and mut_ref t a write borrow, and the borrower writes ref x or mut_ref n at the call site. The value a reference points to is read with deref. Write permission only narrows. Borrows of one value are many readers or one writer, and a borrow cannot outlive the lent value. mut ref slice is rejected; return the new slice.