---
title: 'Stack and Heap'
source: 'https://academia.sh/en/courses/ownership/stack-and-heap'
course: 'Ownership and the Memory Model'
language: en
updated: '2026-09-15T04:58:41+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Defines and distinguishes the four classes (scope end, where it was moved, an explicit call, together with its container)'
    - 'Verifies the 6/3/1/4 distribution of the fourteen values by running the measurement core'
    - 'Demonstrates by running it that values ending at scope end finish in the reverse of their declaration order'
---

# Stack and Heap

A fourteen-value measurement core runs and places every value's life into one of four classes — scope end, where it was moved, an explicit call, together with its container; the result is fixed at 6/3/1/4. It measures that values ending at scope end finish in the exact reverse of their declaration order, and that a heap-allocated value drops into the same ledger.

The previous course measured what a line written in source actually produces: an
expression, a statement, an item. That three-way classification determined what a line
was at the moment it ran, but it deliberately left one question unasked — how long the
value that line produces lives. This course looks at exactly that, and one single
question runs through every lesson in it: where does this value's life end?

This question has come up before, in a different context. The Concurrency course
measured how a goroutine stops — the termination of a running unit. This course applies
the same verb to a different subject: there, a running unit stopped; here, a value ends.
A unit's stopping is triggered by an outside decision (a channel closing, a context
being canceled), while a value's ending can be read straight from the source's own
structure — with no separate decision mechanism needed.

## Four Answers

In Rust, a value's life ends in one of four forms, and these four names stay the same
throughout the course.

| Class | Definition |
|---|---|
| scope end | The value ends at the close of the block it is bound to |
| where it was moved | Ownership passed to someone else; the life ends where the new owner ends |
| an explicit call | A call written in the source ends the life early |
| together with its container | The value is inside a container; it ends when the container does |

These four classes are mutually exclusive: a value cannot end both at scope end and
where it was moved at the same time, because a moved value's old binding is already
dead — the ending happens exactly once in a value's life, through exactly one of the
four paths. The ending point is read from the source's own structure and does not
depend on a runtime decision; that is why the measurement produces not a duration or a
counter, but an ordered record — a list of which value ended when.

The Programming Fundamentals course already established the concept of scope: which
block a name appears in, and how the name becomes unreachable once that block is left.
This course does not redefine that concept; what this topic contributes is that Rust
uses scope not only as a visibility boundary but also as an **ending point**. A name
going out of scope means, in most languages, only that the name can no longer be
reached; here, the same event also means that the value the name held ends — and that
ending happens at a point known at compile time, with certainty, at runtime.

## The Measurement Core

The core used through the rest of the course is `life_core.rs`. Fourteen separate
values are created in four sections (A, B, C, D); each value's ending is written to a
shared observation ledger, and a total per class is printed at the end.

Every section takes its own measurement the same way: before the section starts, the
ledger's current length (`a0`, `b0`, `c0`, `d0`) is recorded; once the section's scope
closes, the `new_entries` function returns only the lines added after that length. This
keeps the four sections from bleeding into each other's measurement — the three values
that end in section A never show up again in section B's measurement, because B's
starting point already takes the length after A finished. The same ledger is shared
across all four, but each section reads its own share starting from its own starting
point.

The observation ledger is held by the `Ledger` type — a shared record that lets
multiple `Trace` instances write to the same list. The internal structure behind this
sharing (`Rc<RefCell<Vec<String>>>`) is a design choice of the measurement tool; how it
works will be treated as its own subject in the Reference Counting and Interior
Mutability lessons. What matters here is only this: every `Trace` value implements the
`Drop` trait, so when a value's life ends, the compiler automatically runs a closing
code path, and that code adds the value's own name to the ledger. `Drop`'s own rules —
which types can implement it, its relationship to `Clone`/`Copy` — are not this course's
subject; the only thing measured is **where** in the source this triggering happens.

**OR1.** `Ledger` and `Trace` themselves are design choices of the measurement tool, not
a requirement of the language; what decides which class a value falls into is the
structure of the code itself — a scope closing, a move, an explicit call, or being
inside a container. The `Trace` type was written to make these four situations visible;
it is not itself one of them.

```rust
// life_core.rs — where does a value's life end: at scope end, where it was moved,
// as an explicit call, or together with its container.

use std::cell::RefCell;
use std::rc::Rc;

type Ledger = Rc<RefCell<Vec<String>>>;

struct Trace {
    name: String,
    ledger: Ledger,
}

impl Trace {
    fn new(name: &str, ledger: &Ledger) -> Trace {
        Trace {
            name: name.to_string(),
            ledger: Rc::clone(ledger),
        }
    }
}

impl Drop for Trace {
    fn drop(&mut self) {
        self.ledger.borrow_mut().push(self.name.clone());
    }
}

struct Container {
    content: Trace,
}

fn consume(_received: Trace) {
    // The value was moved in here; its life ends here when the body finishes.
}

fn read_log(ledger: &Ledger) -> Vec<String> {
    ledger.borrow().clone()
}

fn new_entries(ledger: &Ledger, previous: usize) -> String {
    let all = read_log(ledger);
    all[previous..].join(", ")
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(Vec::new()));
    let mut tally: Vec<(&str, &str)> = Vec::new();

    // --- A: scope end ---
    let a0 = read_log(&ledger).len();
    {
        let _first = Trace::new("scope-1", &ledger);
        let _second = Trace::new("scope-2", &ledger);
        let _third = Trace::new("scope-3", &ledger);
    }
    println!("A scope end                    -> {}", new_entries(&ledger, a0));
    for name in ["scope-1", "scope-2", "scope-3"] {
        tally.push((name, "scope end"));
    }

    // A shadowed value also lives until scope end: a new binding does not end the old one.
    let g0 = read_log(&ledger).len();
    {
        let _shadow = Trace::new("shadowed", &ledger);
        let _shadow = Trace::new("shadower", &ledger);
        println!("   at the moment of shadowing -> {}", new_entries(&ledger, g0));
    }
    println!("A shadowing                    -> {}", new_entries(&ledger, g0));
    tally.push(("shadowed", "scope end"));
    tally.push(("shadower", "scope end"));

    // --- B: where it was moved ---
    let b0 = read_log(&ledger).len();
    {
        let moved_in = Trace::new("into-function", &ledger);
        consume(moved_in);
        println!("   right after the function returns -> {}", new_entries(&ledger, b0));

        let inner = Trace::new("inner-scope", &ledger);
        {
            let _new_owner = inner;
        }
        println!("   as soon as the inner scope ends -> {}", new_entries(&ledger, b0));

        let field_value = Trace::new("into-struct", &ledger);
        {
            let container = Container { content: field_value };
            let _name_length = container.content.name.len();
        }
    }
    println!("B where it was moved            -> {}", new_entries(&ledger, b0));
    for name in ["into-function", "inner-scope", "into-struct"] {
        tally.push((name, "where it was moved"));
    }

    // --- C: an explicit call ---
    let c0 = read_log(&ledger).len();
    {
        let early = Trace::new("early-dropped", &ledger);
        let _late = Trace::new("stays-in-scope", &ledger);
        drop(early);
        println!("   right after the explicit call -> {}", new_entries(&ledger, c0));
    }
    println!("C an explicit call              -> {}", new_entries(&ledger, c0));
    tally.push(("early-dropped", "an explicit call"));
    tally.push(("stays-in-scope", "scope end"));

    // --- D: together with its container ---
    let d0 = read_log(&ledger).len();
    {
        let mut list = Vec::new();
        list.push(Trace::new("container-1", &ledger));
        list.push(Trace::new("container-2", &ledger));
        list.push(Trace::new("container-3", &ledger));
        let _boxed = Box::new(Trace::new("on-heap", &ledger));
        println!("   while the container is full -> [{}]", new_entries(&ledger, d0));
    }
    println!("D together with its container   -> {}", new_entries(&ledger, d0));
    for name in ["container-1", "container-2", "container-3", "on-heap"] {
        tally.push((name, "together with its container"));
    }

    // --- report ---
    let finished = read_log(&ledger);
    println!();
    println!("total finished values: {}", finished.len());

    let classes = ["scope end", "where it was moved", "an explicit call", "together with its container"];
    for class in classes {
        let count = tally.iter().filter(|(_, c)| *c == class).count();
        println!("{:<29}{}", class, count);
    }
}
```

```
A scope end                    -> scope-3, scope-2, scope-1
   at the moment of shadowing -> 
A shadowing                    -> shadower, shadowed
   right after the function returns -> into-function
   as soon as the inner scope ends -> into-function, inner-scope
B where it was moved            -> into-function, inner-scope, into-struct
   right after the explicit call -> early-dropped
C an explicit call              -> early-dropped, stays-in-scope
   while the container is full -> []
D together with its container   -> on-heap, container-1, container-2, container-3

total finished values: 14
scope end                    6
where it was moved           3
an explicit call             1
together with its container  4
```

## Reading the Four Sections

Section A creates three values (`scope-1`, `scope-2`, `scope-3`), and all three drop
into the ledger the moment the inner scope closes — in the output, in the order
`scope-3, scope-2, scope-1`. This ordering is returned to in the Boundary Measurement
section. The second part of section A measures shadowing: binding the same name twice
(`shadowed`, then `shadower`) does not produce a single ending — the two live
separately, and both end at scope end. The measurement taken at the moment of shadowing
comes out empty, because nothing has ended yet; shadowing only sets up a new binding, it
does not shorten the old one's life. The Programming Fundamentals course established
shadowing as a concept, and the Variables and Immutability lesson deepened it with the
ability to change type; what this topic contributes here is narrow: shadowing is not an
ending.

Section B shows three separate forms of moving that fall into the same class: a value
being passed to a function (`into-function`), a value being rebound in an inner scope
(`inner-scope`), and a value being placed in a struct field (`into-struct`). All three
end before the outer scope closes, because ownership moves to a narrower scope each
time, and that narrower scope closes first. The `consume` function and the `Container`
struct were added to the core to make these three forms concrete: `consume` takes a
value as a parameter and drops it at the end of its scope without returning anything,
while `Container` places a value into its own field and shows that the owner of that
field is now the `Container` instance. Neither is part of the language — both are small
helpers written to place the three forms of moving side by side. The detail of these
three forms will be taken up in the Ownership and Moves lesson; here, only that all
three fall into the same class (where it was moved) is shown.

Section C shows a rare class with a single example: of two values in the same scope,
one (`early-dropped`) is dropped by a call before its scope ends, while the other
(`stays-in-scope`) waits for the normal end of scope. Only one of the fourteen values
ends this way — an explicit call moves an ending forward without waiting for the
scope's own flow.

Section D collects four values in a single container: a growable array holds three
elements (`container-1`, `container-2`, `container-3`), and a single heap-allocated
value (`on-heap`) sits in the same scope. While the container is full, the ledger comes
out empty — no element ends before the container closes. When the container closes, all
four values drop into the ledger at the same time, but they keep their own order among
themselves: `on-heap` ends before the list (the reverse of declaration), while the
list's own elements end in the order they were added.

**OR2.** The drop order of the elements inside a growable array — in insertion order,
from the lowest index to the highest — is the language's own guarantee; this behavior is
not an example the core chose, it is a rule the standard library defines.

## Stack, Heap, and the Single Owner

Memory layout itself is not this course's subject — the How Computers Work course and
the Escape Analysis lesson had already established the stack/heap distinction as a
concept, and this third encounter is not a repeat of that theory. What this topic
contributes here is narrow, and it can be summed up in two sentences: the distinction is
**visible in the type**, and every value has **a single owner**.

The `on-heap` variable makes this concrete. An ordinary `Trace` value (`scope-1`, for
example) sits directly where it is bound; `Box::new(Trace::new("on-heap", &ledger))`, in
contrast, places the value on the heap, and this placement is visible in the code — the
type itself is `Box<Trace>`, not `Trace`. Heap allocation itself as a subject — how it
makes recursive types possible, how ownership behaves there — will be taken up later in
the Heap-Allocated Single Ownership lesson. The only thing that needs to be seen here is
this: `on-heap` drops into the same ledger through the same `Drop` mechanism, just like
the three array elements on the stack. Where a value lives (on the stack or on the heap)
and when it ends are separate questions — one is read from the type, the other from the
scope.

The phrase "single owner" is not a slogan here; it is an observation read straight from
the ledger itself. None of the fourteen values drops into the ledger twice — every name
appears exactly once across the whole output. Even when whether a value sits on the
stack or the heap changes, this singularity does not: `on-heap`, `container-1`, and
`scope-1` each end exactly once. This observation is not incidental; if a value could be
owned in two separate places at the same time, that value would be dropped twice, and
the same name would appear twice in the ledger. The next lesson measures directly how
this singularity is preserved during a move — why the old owner can no longer reach the
value once ownership changes hands.

## Boundary Measurement: Drop Order Is the Reverse of Declaration

Section A's three values are the clearest example of this. `scope-1`, `scope-2`,
`scope-3` are declared in that order, but when the inner scope closes, the ending order
is exactly reversed: `scope-3, scope-2, scope-1`. This is not a choice made by the
measurement tool — it is the language's own word.

**OR3.** Values in a local scope always end in the reverse of their declaration order —
this is part of Rust's language specification and does not vary from implementation to
implementation. The same rule shows up in section D as well: `on-heap` ends before
`list`, which is declared after it.

This order is not incidental: the last binding opened in a scope ending first
guarantees that nested resources — a connection a file opened, for example — always
close in the reverse of the order they were opened. Someone reading the source can
state a scope's ending order without running anything, just by reversing the
declaration order.

This guarantee is not just an ordering detail — it also establishes a dependency order.
If a value bound second in a scope shares a resource belonging to a value bound first —
for example, holding the same ledger in its own field — the second value has to end
before the first one; otherwise, by the time it ended, the resource it held would
already be closed. Reversing the declaration order does exactly this: whatever opened
last ends while its dependencies are still alive. In the `life_core.rs` core, this
dependency is the `ledger` itself — every `Trace` value holds its own `ledger` field,
and that field ends not after the `Trace` value itself, but exactly together with it.

## Summary

- A value's life ends in one of four forms: scope end, where it was moved, an explicit
  call, together with its container; of the fourteen values, 6 ended at scope end, 3
  where they were moved, 1 at an explicit call, and 4 together with their container.
- Shadowing is not an ending; rebinding the same name does not shorten the old
  binding's life.
- Whether a value lives on the stack or the heap is read from the type (the difference
  between `Trace` and `Box<Trace>`); where it lives is a separate question from when it
  ends.
- Values that end at scope end always end in the reverse of their declaration order;
  this is the language's word, not a choice made by the measurement tool.

## Next Step

This lesson split where a value's life ends into four classes and measured scope end's
own order. Of the remaining three classes, the most common — where it was moved — has
so far only been seen through three examples. The next lesson deepens that class: when
ownership passes from one place to another, exactly what happens to the ending point,
and why three separate forms of moving (into a function, into an inner scope, into a
struct field) always arrive at the same result.
