Lowent Manual←↑→

12 Borrowing — ref and mut_ref

What to know first

chapter 9, Sequences · elements can be written only through a mut slice
chapter 10, Aggregates · field is a place to read and to write

Looking 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

Copying a big value every time you pass it is slow, and passing a pointer leaves you unable to tell who changes the value and when. Lowent puts borrowing between the two. Borrowers for reading may be many; a borrower for writing must be alone. That rule is the foundation of memory safety without a garbage collector, and the proof that there are no data races in concurrency (Part VII) stands on it too. Before memory (Part V), learn the shape of borrowing.

By the end of this chapter

You will learn to borrow values with 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

  1. 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.

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

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            → refused

A common misconception. The exclusivity rule only matters for multithreaded programs

The two examples above contain no threads. Even within one flow, when two names point at the same value, a write through one breaks what the other believes. That is also why C’s 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

  1. 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#

ShapeMeaningWhy
input v ref big .receive a read borrowno copy of a large value, and a promise not to change it
input p mut_ref u64 .receive a write borrowthe only way to change a caller’s value
total (ref v) · bump (mut_ref n)lend at the call sitethe call alone shows what may change
deref pread the value a reference points toa 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 aread a field through a referencesame spelling for a reference to a struct
two mut_ref of one value · ref plus a writeoverlapping borrows — rejected (E-EXCL)many readers or one writer
one storage in a written and a read positiononly when the callee allows it with inplace <written> <read> . and the ranges are the same — else E-EXCL-INPLACEa body that reads and writes element by element is right only on the same range
the body of an op that declares inplace o awrite o only after every read of a, or read and write element by element with one index — else E-INPLACE-UNPROVENthe processor checks the declaration against the body
using an old view after calling an op that declares invalidates <input> on its storagerejected (E-VIEW-INVALIDATED) — take the view againafter a release, a growth or a rewind the view points at someone else’s place
return ref here .return a reference to a local — rejectednever point at a value that is gone
mut ref slicedoes 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.