Rust Move Lab

Who owns this value? Three rejections from a real compiler, each beside the Go program that runs happily and shares the memory instead.

The finding. Rust answers the ownership question by refusing to let a second name for the value exist. Go answers it by handing you a second name that reaches the same memory. Neither is being careless: one enforces a single owner at compile time, the other documents that a slice is a header over an array somebody else may also hold. The difference shows up in what you have to run to see it.

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 suggested fix 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 stack frame, a heap box or an arrow from a name to a value — 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.

The harness is pigfox/rust-move-lab, and the fixtures below live at commit fdcb74e.

What the Go compiler said about all of this

Nothing. Not a warning, not a note, not a vet finding — ownership is not a thing Go's compiler has an opinion about, so there is no second set of diagnostics to put beside the first. That is why this page shows what the Go programs do rather than what a compiler said about them.

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, so a page carrying this section cannot have been built from a capture where the compiler spoke.

One rule, three syntaxes

Every error below carries the same code, from the same pass. Binding to a second name, passing to a function, and going round a loop a second time are the same ownership transfer written three ways — which is why the label beside each code names the syntax rather than the compiler stage, and why each diagnostic is found by which program it came from rather than by its code.

Step 1 Bind it to a second name

E0382 Assignment src/main.rs

One value, two names. Rust does not copy the String and does not alias it: it moves the single owner from the first name to the second, and the first stops being usable from that line onward. Read the message closely — the compiler calls it a BORROW of a moved value, because the print macro takes its argument by reference, so the failure is not "you used it" but "you tried to look at it".

error[E0382]: borrow of moved value: `a`

22 |     println!("{a} {b}");
  |                ^ value borrowed here after move

The error points at 2 further places, and it needs all of them: what is wrong is a relationship between two lines, which neither line shows on its own.

20 |     let a = String::from("hello");
  |         - move occurs because `a` has type `String`, which does not implement the `Copy` trait
21 |     let b = a;
  |             - value moved here

What rustc offers here

Each fix carries the compiler's own grade for it. MachineApplicable means cargo fix would apply it unattended; MaybeIncorrect means it compiles and may not be what you meant. They are shown differently because printing them the same way would flatten the compiler's hedging into a recommendation it did not make.

consider cloning the value if the performance cost is acceptable

21 -     let b = a;
21 +     let b = a.clone();

Applicability: MachineApplicable

The same shape in Go, running

The same two lines in Go leave both names alive, and pointing at the same array. A slice value is a three-word header — pointer, length, capacity — so assigning it copies the header and not the memory behind it. Writing through the second name changes what the first one sees, and the program checks that by comparing the addresses of the two slices' first elements rather than asserting it.

What the program printed in scene assign:

assign/declared: a=[1 2 3] b=[1 2 3] shared=true

assign/after b[0]=99: a=[99 2 3] b=[99 2 3]

The lines marked as measured were not narrated by this page or by the program's author. The program compared the addresses of the two slices' first elements and printed the answer, which is the only way a claim about sharing can be evidence rather than an assertion.

Step 2 Pass it to a function

E0382 Function call src/bin/into_call.rs

The same rule with no assignment operator anywhere in it. Handing a value to a function that takes it by value is a move, exactly as binding it to a new name was — which is the point of the pair. Rust does not have a rule about `=`; it has a rule about ownership, and every syntax that hands a value somewhere else is that one rule.

error[E0382]: borrow of moved value: `a`

21 |     println!("{n} {a}");
  |                    ^ value borrowed here after move

The error points at 2 further places, and it needs all of them: what is wrong is a relationship between two lines, which neither line shows on its own.

19 |     let a = String::from("hello");
  |         - move occurs because `a` has type `String`, which does not implement the `Copy` trait
20 |     let n = consume(a);
  |                     - value moved here

What rustc offers here

Each fix carries the compiler's own grade for it. MachineApplicable means cargo fix would apply it unattended; MaybeIncorrect means it compiles and may not be what you meant. They are shown differently because printing them the same way would flatten the compiler's hedging into a recommendation it did not make.

consider cloning the value if the performance cost is acceptable

20 -     let n = consume(a);
20 +     let n = consume(a.clone());

Applicability: MachineApplicable

The same shape in Go, running

Go passes the slice header by value too, and the caller's array still comes back changed, because what was copied was a pointer to it. "Passed by value" and "the callee cannot affect me" are different statements, and Go is one of the languages where only the first is true. The caller's name is also still usable afterwards, which is precisely what the Rust version is refused for.

What the program printed in scene call:

call/before: a=[1 2 3]

call/after bump(a): a=[1001 2 3] stillUsable=true

Step 3 Do it twice

E0382 Loop src/bin/in_loop.rs

Every line here would be accepted on its own, and a program that made this move once and stopped compiles. What the compiler rejects is the SECOND time round, and it says so in a note worth reading twice: the line is in conflict with ITSELF, one iteration ago. Nothing in the source text is wrong. The loop is what makes it wrong, and no amount of staring at the line reveals that.

error[E0382]: use of moved value: `a`

27 |         total += consume(a);
  |                          ^ value moved here, in previous iteration of loop

The error points at 2 further places, and it needs all of them: what is wrong is a relationship between two lines, which neither line shows on its own.

24 |     let a = String::from("hello");
  |         - move occurs because `a` has type `String`, which does not implement the `Copy` trait
26 |     for _ in 0..2 {
  |     ------------- inside of this loop

What rustc offers here

Each fix carries the compiler's own grade for it. MachineApplicable means cargo fix would apply it unattended; MaybeIncorrect means it compiles and may not be what you meant. They are shown differently because printing them the same way would flatten the compiler's hedging into a recommendation it did not make.

consider moving the expression out of the loop so it is only moved once

26 -     for _ in 0..2 {
26 +     let mut value = consume(a);
    for _ in 0..2 {

Applicability: MaybeIncorrect

consider moving the expression out of the loop so it is only moved once

27 -         total += consume(a);
27 +         total += value;

Applicability: MaybeIncorrect

consider cloning the value if the performance cost is acceptable

27 -         total += consume(a);
27 +         total += consume(a.clone());

Applicability: MachineApplicable

The same shape in Go, running

This is where the comparison earns its keep. Go runs both iterations, and the second silently overwrites what the first wrote — into a slot neither the slice nor the returned value ever shows you, because it is past the length and inside the capacity. Watch the backing array between the two iterations. Whether that write is visible at all depends on the spare capacity, which is a number nobody in this program chose deliberately.

What the program printed in scene loop:

loop/before: a=[1 2] len=2 cap=4 backing=[1 2 0 0]

loop/iteration 0: a=[1 2] backing=[1 2 90 0] returned=[1 2 90] shared=true

loop/iteration 1: a=[1 2] backing=[1 2 91 0] returned=[1 2 91] shared=true

loop/after: a=[1 2] backing=[1 2 91 0]

The lines marked as measured were not narrated by this page or by the program's author. The program compared the addresses of the two slices' first elements and printed the answer, which is the only way a claim about sharing can be evidence rather than an assertion.

Everything the program printed

The scenes above are a partition of the 8 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 here and is not incidental: in the last scene the second iteration overwrote what the first one wrote, which is a statement about sequence.

assign/declared: a=[1 2 3] b=[1 2 3] shared=true
assign/after b[0]=99: a=[99 2 3] b=[99 2 3]
call/before: a=[1 2 3]
call/after bump(a): a=[1001 2 3] stillUsable=true
loop/before: a=[1 2] len=2 cap=4 backing=[1 2 0 0]
loop/iteration 0: a=[1 2] backing=[1 2 90 0] returned=[1 2 90] shared=true
loop/iteration 1: a=[1 2] backing=[1 2 91 0] returned=[1 2 91] shared=true
loop/after: a=[1 2] backing=[1 2 91 0]

Where all of this came from

rustc
rustc 1.93.0 (254b59607 2026-01-19)
cargo
cargo 1.93.0 (083ac5135 2025-12-15)
Toolchain in force
1.93.0-x86_64-unknown-linux-gnu (overridden by '/home/peter/Documents/pigfox2-repos/rust-move-lab/rust-toolchain.toml')
Rust command
cargo build --message-format=json --keep-going — exit 101
Go
go version go1.26.5 linux/amd64
Go build
go build -o <tmp> . — exit 0
Go run
<tmp> — exit 0
Captured
2026-08-16T18:50:47Z
Captured from
39cabd77f26ad40e8d74a6b20d650563b34c181c
Committed at
fdcb74e

The Rust build was supposed to fail, and it did. cargo exited 101 and reported 3 errors across three separate binaries. Three, and not one file with three functions in it: each program is the smallest thing that shows its own move, so every caret points into a file containing nothing else.

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 35261 bytes in this repository and 78656 in the harness. Two fields were removed: the text of rustc --explain for the error code, repeated once per diagnostic, which this page does not render and you can run yourself; and the verbatim cargo output, which duplicates the parsed diagnostics beside it. The program's own 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.