How many readers, how many writers? Two rejections captured from a real compiler, the program it accepts because the borrow had already ended, and a Go program that runs all three shapes without comment.
The finding. The rule is any number of readers or one writer, never both at once. What is easy to miss is that the checker is not simply strict about it: the third program below is the first one with two statements swapped, and it compiles — because a borrow lasts until its last use, not until the end of its block. Go has no notion of a borrow to be strict or precise about, so its three programs all run, and what they do is the only place to look.
Nothing on this page touches a chain.
Captured output, not a compiler or a program running here
No compiler runs when you open this page, nothing is executed and nothing is fetched. A committed harness ran the Rust build, ran the Go build, ran the Go binary it had just built, and wrote all of it to JSON — which is compiled into this binary. Every caret, every label, every line of source and every line of program output below is read out of that.
There is no memory diagram on this page, and that is deliberate. You will not find a box, an arrow or a lifetime drawn as a bar — not because such a picture would be hard to draw, but because the capture does not contain one. Drawing it would mean this page teaching you something it did not measure.
That split is the whole argument of this page, so the two numbers in the heading above are read out of the capture rather than counted from the walkthrough below. cargo emits a diagnostic for a target it refuses and an artifact record for one it builds; the harness indexes both, per target, because an acceptance is otherwise silent — a program that compiled says nothing, which is exactly what a program that was never built also says.
Targets rejected
2
Targets accepted
1
Rust command
cargo build --message-format=json --keep-going
--keep-going is load-bearing here rather than a habit. Without it cargo stops scheduling work at the first refusal, and the target that compiles — the one this page is really about — would never be built at all.
What the Go compiler said about all of this
Nothing. Not a warning, not a note, not a vet finding. Go has no notion of a borrow, so there is no second set of diagnostics to put beside the first — which is why the Go half of every step below shows what the program does rather than what a compiler said about it.
Build command
go build -o <tmp> . — exit 0
Bytes the compiler printed
0
Recorded as
compilerSaidNothing = true
That last line is a control, not a claim. The harness computes it from both of the build's output streams and refuses the whole capture if it is ever false.
Refused, refused, accepted
The order is the lesson, and the last step is the answer to the ones above it rather than a footnote to them. Read the first, then read the last: the statements are the same and only their order differs.
Step 1 One reader, then one writer
E0502Reader and writersrc/main.rs
A shared borrow of the vector, a push that needs an exclusive one, and the shared borrow read afterwards. The error names three places and needs all of them: where the shared borrow started, where the exclusive one was taken, and where the shared one was used LATER. That last span is what makes the error true — remove it and there is no overlap, which is exactly what the third step does.
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
25 | v.push(4);
| ^^^^^^^^^ mutable borrow occurs here
The error points at 2 further places, and it needs all of them: what is wrong is a relationship between lines, which no single line shows on its own.
24 | let first = &v[0];
| - immutable borrow occurs here
26 | println!("{first}");
| ----- immutable borrow later used here
rustc offers no fix for this one, and there is nothing evasive about that: the edit that would work is not a change to any single line but a change to where the borrow stops being used, which is what the last step shows.
The same shape in Go, running
Go runs the same three steps, and the push does reallocate — the slice was made at capacity, so growing it has nowhere to go. The pointer taken beforehand is left addressing an array nothing else refers to any more. Watch the last line: writing through that pointer SUCCEEDS. It is a legal write to memory the program still owns, it does not panic, and the value never appears in the slice again.
The lines marked as measured were not narrated by this page or by the program's author. The program compared addresses — whether two slices start at the same place, or whether a pointer still addresses a slice's first element — and printed the answer, which is the only way a claim about sharing can be evidence rather than an assertion.
Step 2 Two writers at once
E0499Two writerssrc/bin/two_muts.rs
The other half of the rule, and the spans tell a different story: both borrows are exclusive here, so the compiler points at the first, the second, and the place the first is used afterwards. Note what it is NOT reasoning about — the two pushes could not possibly collide. The rule is about aliasing, because deciding the other question is not something a compiler can do in general.
error[E0499]: cannot borrow `v` as mutable more than once at a time
21 | let b = &mut v;
| ^^^^^^ second mutable borrow occurs here
The error points at 2 further places, and it needs all of them: what is wrong is a relationship between lines, which no single line shows on its own.
20 | let a = &mut v;
| ------ first mutable borrow occurs here
22 | a.push(4);
| - first borrow later used here
rustc offers no fix for this one, and there is nothing evasive about that: the edit that would work is not a change to any single line but a change to where the borrow stops being used, which is what the last step shows.
The same shape in Go, running
Two slice headers over one array is the plainest thing Go hands you, and both write. The second write lands on top of the first, the first writer has no way to find out, and neither the compiler nor the runtime mentions it. The program checks that the two headers share an array by comparing addresses rather than asserting it.
What the program printed in scene writers:
writers/declared: a=[1 2 3] b=[1 2 3] shared=true
writers/a[1]=20 then b[1]=30: a=[1 30 3] b=[1 30 3]
The lines marked as measured were not narrated by this page or by the program's author. The program compared addresses — whether two slices start at the same place, or whether a pointer still addresses a slice's first element — and printed the answer, which is the only way a claim about sharing can be evidence rather than an assertion.
Step 3 The same program, accepted
no errorBorrow endedsrc/bin/nll_ok.rs
Every line below also appears in the first step, which was refused. The only difference is that the shared borrow's last USE now happens before the exclusive borrow is taken. A borrow does not last until the end of its block; it lasts until the last point it is used, and the compiler knows it. So the fix for a borrow error is usually not to restructure the program but to stop using the borrow sooner — and this is what that looks like.
The compiler accepted this one. There is no diagnostic to show, because a program that compiles produces none — so what is below is the source itself, read off disk by the harness and recorded beside the two refusals.
24 | fn main() {
25 | let mut v = vec![1, 2, 3];
26 | let first = &v[0];
27 | println!("{first}");
28 | v.push(4);
29 | println!("{v:?}");
30 | }
That is the function, not the whole file — the recorded source starts at src/bin/nll_ok.rs line 1 with a comment header the harness keeps and this page does not print. Follow the link above to read it entire.
The same shape in Go, running
Go's range also ends something early and says nothing about it. The range expression is evaluated ONCE, at loop entry, so appending inside the body cannot extend the loop and the elements it reads come from the array the header pointed at when it started. The slice doubles in length while the loop runs three times and never sees a single element it appended. Nothing here is wrong and nothing is reported.
The scenes above are a partition of the 10 lines below, not a selection from them. Here is the whole run, verbatim and in order, so that can be checked rather than taken on trust. Order matters and is not incidental: in the first scene a pointer is inside the slice before an append and outside it after, which reads as its own opposite reversed.
The Rust build exited 101, and that is the intended outcome rather than a failure of the capture. A non-zero exit is what cargo reports when any target fails, so the exit code alone cannot tell you which of the three did what — it is the same number whether one was refused or all of them were. The target index above is what can.
Two Go versions appear in that capture, and only one built anything. The capture machine's default toolchain is go version go1.26.3 linux/amd64; the harness module asks for a newer one, and with GOTOOLCHAIN=auto Go switches to it silently for any command run inside that module. Both are pinned so the switch is visible rather than hidden.
The two commit identifiers above differ, and they always will. A capture reads the working tree, writes the fixture, and only then is the fixture committed — so the sources that produced these bytes are one commit earlier than the commit carrying them. Nothing is out of step.
What was dropped on the way here. The Rust fixture is 18014 bytes in this repository and 30765 in the harness. Two fields were removed: the text of rustc --explain for each code, which this page does not render and you can run yourself; and the verbatim cargo output, which duplicates the parsed diagnostics beside it. The target index and the accepted target's source were kept — they are the only record that one of the three programs compiled. The Go capture is committed whole, which is why its output is printed above in full.
One of these fixtures is byte-reproducible and the other is not, and the harness treats them differently for that reason. cargo compiles the three binaries in parallel, so the order they finish in varies between runs of the same commit and the capture timestamp moves every time; the harness compares two Rust captures through a projection that sorts those streams. The program's output has no such excuse — same binary, same bytes, every time — so it is compared exactly, and its order is never sorted away.