1.3 The minimal model: why assignment invalidates the old variable
The previous section left us with a key clue: a heap resource has no built-in exit moment of its own, but the stack variable pointing to it does—when a function returns or a block closes (}), the exact point where that stack variable goes out of scope is fixed at compile time.
Before touching any Rust syntax, let’s follow that clue and design the simplest possible model for automatic memory reclamation—and see whether it can solve the problem on its own.
Building the minimal model
Section titled “Building the minimal model”Turn that clue from the previous section into the model’s first rule:
When a stack variable goes out of scope (}), the heap resource it points to is freed automatically.
Let’s trace how this minimal model behaves. Suppose a block allocates a chunk of heap memory and holds it through a stack variable s. We’ll use pseudocode so we can focus purely on the mechanics.
{ s = alloc_heap("hello") // ... read or use s inside this block ...} // ← s goes out of scope here; the heap resource it points to is freed automaticallyAs long as s is the only variable involved, this single rule gets the job done:
- You never write
free, so you cannot leak memory by forgetting it. - The release point is fixed right at the closing brace
}, known at compile time, with zero runtime scanning or reference-counting overhead.
Now let’s test this model against a single assignment statement, =.
Why a single assignment causes a double free
Section titled “Why a single assignment causes a double free”First, try an ordinary integer on the stack:
Pseudocode for reasoning{ a = 1 b = a} // ← b and a go out of scope; the stack frame is reclaimed directlyUnder the hood, b = a does one thing: it copies the bytes of a on the stack bitwise into b. Because the integer 1 lives entirely on the stack and carries no heap allocation, a and b each end up with an independent 1. When the block closes at }, both stack variables vanish with the stack frame, no heap cleanup is triggered, and nothing goes wrong.
Next, try a dynamic string backed by a heap resource:
Pseudocode for reasoning{ s = alloc_heap("hello") t = s} // ← what happens when this scope ends?As we saw in the previous section, a value like this has a three-word control header on the stack (pointer ptr, length len, capacity cap), while the actual character buffer "hello" sits on the heap.
When t = s runs in our model, it copies those three machine words from s into t. Because the heap address inside ptr is copied along with them, both stack variables now point at the exact same heap resource rather than holding separate character buffers.
Now follow execution to the closing brace }. Variables exit in reverse order of declaration—t leaves first, then s. Our minimal model’s rule fires once for each variable going out of scope:
tgoes out of scope and automatically frees the heap block it points to.sis still in scope, and itsptrnow points at freed memory—a dangling pointer.- Immediately afterward,
salso goes out of scope and frees the exact same heap block a second time—a double free, which typically crashes the program on the spot.
Notice that the rule itself only said one thing: when a stack variable goes out of scope, free the heap block it points to. The assignment t = s didn’t break that rule—it merely copied three machine words on the stack. What broke the program was the state left behind after the copy: two stack variables pointing at the same heap block meant “free on scope exit” ran twice.
From that failure, we can extract a fundamental requirement our model must uphold:
At any given moment, each heap resource can have only one stack variable responsible for freeing it.
An ordinary assignment t = s puts two variables on the same heap resource and shatters that requirement. For the minimal model to survive, it has to resolve the conflict created by assignment.
Patching the model: invalidate the old variable
Section titled “Patching the model: invalidate the old variable”Our patch should stay just as direct and uncompromising as the minimal model itself. Since two variables cannot both remain responsible for freeing the same heap block, only one can stay valid after an assignment:
When a value backed by a heap resource is assigned, its stack control header is copied to the new variable, the old variable becomes invalid immediately, and the responsibility for freeing the heap transfers to the new variable.
The "hello" buffer on the heap is never duplicated—only the three machine words on the stack are copied. When the scope finally ends, only the valid variable t triggers deallocation; the invalidated s does nothing. The trade-off is equally direct: after the assignment, the old variable can no longer be used.
One =, two semantics: Copy and Move
Section titled “One =, two semantics: Copy and Move”In surface syntax, assignment is still written with the same equals sign =—the simplest notation possible. At the physical level, it still performs the exact same action: copying bytes on the stack.
b = a // a is an integert = s // s points to a heap resourceAt the logical level, however, that single = splits into two distinct behaviors:
- For
b = a: the stack bytes carry no heap resource that needs freeing. After the copy,aandbboth remain valid and independent. We call this Copy semantics. - For
t = s: the stack bytes carry a heap resource that requires exclusive cleanup. Keeping both variables valid after copying the header would cause a double free, so the old variablesmust be invalidated on the spot, leaving onlytvalid.
In the second case, what actually protects the model is invalidating the old variable. Yet calling it “a copy that invalidates the original” sounds awkward: physically you copied the bytes, but logically only one variable remains usable. Look at the net effect instead—s is gone, and t now holds the heap resource along with the duty to free it. Logically, it looks as though the value was moved out of s and into t.
That is why we give the model two names for the same =: the underlying mechanism is invalidating the old variable, and we express that mechanism as a Move. From here on, the same assignment operator = means Copy for types without heap resources, and Move for types carrying a heap resource.
What the minimal model has achieved so far
Section titled “What the minimal model has achieved so far”Put the two pieces from this section together, and our minimal model already meets the goal set in the first section:
- When a variable goes out of scope, the heap resource it points to is freed automatically—no manual
free, and no runtime scanning or reference counting. - Assigning a heap-backed value performs a Move, invalidating the old variable—only one variable is responsible for freeing a heap block at any moment, preventing double frees.
Automatic reclamation, no GC, and no double frees—on paper, everything seems to be in place.
Still, this model has only passed its first test. Pause for a moment and ask yourself: if this model is already complete, what happens when you write the most routine collaboration in everyday code—passing a value into a function just to let the function use it? Can you still write that naturally, or does it get stuck somewhere?
Hold on to that question as we step into the next section.