Skip to content

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.

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.

Pseudocode for reasoning
{
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 automatically

As 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 directly

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

After t = s, both names point at the same block on the heap.
Two names point at one blockOn the stack, s and t each occupy a cell holding a pointer. Two arrows run from those cells to the same hello block on the heap.stacksptrtptrheaphello

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:

  1. t goes out of scope and automatically frees the heap block it points to.
  2. s is still in scope, and its ptr now points at freed memory—a dangling pointer.
  3. Immediately afterward, s also 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.

After assignment, t points at the block and s is no longer valid.
After assignment, the original name is invalidTwo rows on the stack. The row for s is marked invalid and has no arrow. The row for t holds a copy of the pointer, and an arrow runs from it to the single block on the heap.stacksinvalidptrtptrheaphello

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.

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.

Pseudocode for reasoning
b = a // a is an integer
t = s // s points to a heap resource

At 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, a and b both 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 variable s must be invalidated on the spot, leaving only t valid.

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:

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