rust ownership

one owner, many readers, no dangling — the whole model in four moves

You've already met the borrow checker. It's the thing that stopped your build with value used after move and made you wonder why a language would forbid something every other language lets you do without comment. This page is the answer to that wondering. The model is small — three rules, honestly — and once it clicks, the errors stop reading as obstruction and start reading as a second pair of eyes on your memory.

1one owner

Every value in Rust has exactly one owner: the variable responsible for cleaning it up. When the owner goes out of scope, the value is freed. No garbage collector deciding later, no manual free() you can forget — the scope is the lifetime.

let s = String::from("hello"); // s owns the heap buffer
// ... use s ...
// scope ends → s dropped → buffer freed. automatically. exactly once.

Step through what that looks like in memory. The stack frame is on the left — named slots your function holds directly. The heap is on the right — the actual bytes of "hello", which live somewhere the stack only points at.

Keep that picture. Everything else on this page is a variation on it: who holds the solid arrow (the owner), who holds a dashed one (a borrower), and what the checker does when those two overlap wrong.

2moving

Assignment doesn't copy the heap data. let t = s moves ownership: the pointer transfers to t, and s is dead — not zeroed, not dangling, just statically forbidden. You saw this in step 2 of the animation above: the arrow jumps, the old slot grays out.

let s = String::from("hello");
let t = s;              // ownership moves to t
println!("{s}");        // error: value used after move

The same thing happens when you pass a value into a function — the parameter becomes the new owner, and your local is gone after the call. This is the error you've already hit in your own code. It looks like the compiler being pedantic; it's actually the compiler refusing to let two variables both believe they're responsible for freeing one buffer. In C++ that mistake is a double-free you find at runtime, if you're lucky. Here it's a compile error with a line number.

Small types like i32, bool, and f64 are Copy — assignment duplicates them because there's nothing on the heap to fight over. Moves only bite on types that own resources, like String and Vec.

3borrowing

Most of the time you don't want to give a value away — you want to let a function look at it and get it back. That's a borrow: a reference that points at the value without taking ownership. The owner stays put; the borrow is temporary and the checker tracks exactly how long it lives.

let t = String::from("hello");
let r = &t;             // shared borrow — read-only, many allowed
let m = &mut other;     // exclusive borrow — read/write, one at a time

Two flavors, and the whole discipline lives in the difference:

&T is a shared borrow. Read-only. You can hand out as many as you like simultaneously, because readers can't step on each other. &mut T is an exclusive borrow. Read and write — but while it exists, nothing else may touch the value, not even another reader. Many readers or one writer, never both. That single rule is what makes “someone changed the collection while I was iterating it” a compile error instead of a haunting.

And a borrow can never outlive its owner — step 4 of the animation is exactly that rejection. The checker won't let the owner drop while a reference to it is still alive, so a Rust reference always points at something real.

rule 1 — one owner
w owner
w2
One value, one variable responsible for freeing it.
rule 2 — many readers or one writer
v owner
Add shared borrows freely; then try a mutable one while any exist.
rule 3 — no dangling references
The reference dies before its owner: the checker accepts this.

This is the shape your snacko-rs tick loop wants: tick_day(&mut world) lends the world out for exactly one call, and every NPC-memory read deeper in the stack takes &World. Nothing consumes the world; when the tick returns, you still own it, and the checker proved no one kept a stale pointer into the affinity tables.

4the payoff

So what did all that friction buy you?

No garbage collector. Lifetimes are known at compile time, so frees are compiled in at exact points. No pauses, no runtime bookkeeping — which matters when you're ticking a simulation every frame.

No use-after-free, no double-free, no dangling pointers. The move rules make double-frees unrepresentable; the borrow rules make dangling references unrepresentable. A whole category of crash simply has no spelling in safe Rust.

No data races. The many-readers-or-one-writer rule extends across threads: two threads can't hold mutable access to the same data at once, so races are caught before the program runs even once.

To be precise about the claim: the checker guarantees memory safety and freedom from data races in safe code. It doesn't catch logic bugs — you can still compute an NPC's affinity wrong, deadlock two locks, or leak by holding on too long. What it removes is the class of bug where the program's own memory betrays it. Every value used after move you've hit is that safety net doing its job at compile time, where the fix costs a minute instead of a debugging weekend.