Case Study 1: The Leaking Stack
"A pointer bug does not announce itself. It waits."
Executive Summary
You have inherited a small but load-bearing routine from a departed colleague: a pointer-based stack
used inside a particle code to hold "candidate collisions" while a sweep runs. It works on small inputs and
then, on a long production run, it slowly consumes all of memory and eventually crashes — and once, alarmingly,
it printed a wrong answer without crashing at all. This is the archetypal pointer situation: code that is
mostly right, failing silently in exactly the ways §11.2 warned about. We will read it, name each of its
three defects precisely — an undefined pointer, a leak, and a dangling alias — repair them into
a version that runs clean, and then make the professional call: port the structure to a plain allocatable
array, because in Fortran that is what it should have been all along.
Skills applied: reading pointer code and identifying the data structure (§11.1, §11.3); the three
association statuses and the two hazards (§11.2); safe teardown of a linked structure (§11.3); the
allocatable-over-pointer rule and why it removes whole bug classes (§11.4).
Background
The structure is a singly linked stack: push a node onto the head, pop from the head, last-in-first-out. Here is the inherited code, faithfully reproduced. Read it before you read our diagnosis — see how many of the three faults you can find yourself.
! --- INHERITED CODE (contains three bugs; do not model code on this) ---
type :: node_t
integer :: id
type(node_t), pointer :: next ! (bug 1 lives here)
end type node_t
type(node_t), pointer :: top ! (and here)
subroutine push(id)
integer, intent(in) :: id
type(node_t), pointer :: fresh
allocate(fresh)
fresh%id = id
fresh%next => top
top => fresh
end subroutine
subroutine pop(id)
integer, intent(out) :: id
id = top%id
top => top%next ! (bug 2 lives here)
end subroutine
Elsewhere, the caller keeps a handle to the "current best" candidate for use after the sweep:
best => top ! remember the current top node
call pop(discard) ! ... but then pop it off
print *, best%id ! (bug 3 lives here)
Four hard questions press on any such inheritance: what is the structure? where does it corrupt state? where does it leak? and should it exist at all? We take them in turn.
Phase 1 — Read the Code and Name the Structure
Start by recognizing the shape, because the bugs follow from it. node_t holds an id and a pointer to the
same type — the signature of a linked structure (§11.3). top is the head; push prepends; pop reads the
head's id and advances top to the next node. So it is a stack, and its operations are $O(1)$, which is
why the original author chose it. Nothing here is exotic. Everything here is a place a pointer can betray you.
A useful first move with any inherited pointer code is to annotate, for every pointer, when it gets a defined status and who is responsible for freeing what it points at. Do that here and two of the three bugs fall out immediately.
Phase 2 — Bug One: The Undefined Pointer
Look at the declarations: type(node_t), pointer :: next in the type, and type(node_t), pointer :: top at
module level. Neither is initialized. That means top begins with undefined association status (§11.2) —
not disassociated, undefined. The very first push does fresh%next => top, reading top before it has
ever been given a defined value, and any code that guards with if (associated(top)) before the first push
is asking a question the standard says must not be asked.
On most runs this happens to work, because uninitialized memory happens to look null — which is exactly what makes it dangerous: it passes your tests and fails on the cluster. The fix is the one-time habit from §11.2: give every pointer a defined status at birth.
type(node_t), pointer :: next => null() ! in the type
type(node_t), pointer :: top => null() ! at module level
Now top is genuinely disassociated before the first push, associated(top) is a legal question with a
.false. answer, and the end-of-stack test (next => null() on the last node) is well defined.
Phase 3 — Bug Two: The Leak
Trace the memory in pop. It reads top%id, then does top => top%next, moving the head pointer past the
old node. But nothing ever deallocates that old node. Its memory was obtained by allocate in push; once
top no longer points at it and no other pointer does either, it is unreachable and unfreed — a classic
leak. Push a million candidates over a long run, pop them all, and you have leaked a million nodes: the slow
memory growth the production run exhibited.
The repair is the save-then-free discipline of §11.3 — capture the node, advance the head, then free:
subroutine pop(id, status)
integer, intent(out) :: id, status
type(node_t), pointer :: old
if (.not. associated(top)) then ! defensive: don't pop an empty stack
id = 0; status = -1; return
end if
id = top%id
old => top ! remember the node to free
top => top%next ! unlink it from the head
deallocate(old) ! free it — no leak
status = 0
end subroutine
Note we also hardened pop against an empty stack: reading top%id when top is disassociated is itself a
null-dereference, and returning a status lets the caller notice rather than crash.
Phase 4 — Bug Three: The Dangling Alias
The subtlest fault is in the caller, not the structure. It saves best => top — a second alias for the head
node — then pops that very node, which (with our fix) now frees it. best is untouched by the
deallocation; it still holds the old address; it now dangles (§11.2). The subsequent print *, best%id
reads freed memory: on a good day a crash, on a bad day a plausible-looking wrong number — which is precisely
the "wrong answer without crashing" the run once produced.
There is no way to make best safe after the fact, because associated(best) may still report .true..
The only cure is discipline about ownership: if you intend to keep using a node's data, either do not free it
while an alias survives, or copy the value out before freeing:
best_id = top%id ! copy the VALUE out (an integer, owned by nobody)
call pop(discard, st)
print *, best_id ! safe: it is a plain integer, not an alias
This is the deepest lesson of the case study: a deallocation is the death of every alias to that memory, and the language tracks only the one pointer you named. The rest are your responsibility.
Phase 5 — The Fix, Verified — and the Port
Assemble the repairs into a self-contained stack and run it. This version initializes every pointer, frees on pop, and copies values rather than aliasing nodes it will destroy:
program fixed_stack
implicit none
type :: node_t
integer :: id
type(node_t), pointer :: next => null()
end type node_t
type(node_t), pointer :: top => null()
integer :: id, st
call push(10)
call push(20)
call push(30) ! stack (top -> bottom): 30, 20, 10
do while (associated(top))
call pop(id, st)
if (st /= 0) exit
print '(a, i0)', 'popped ', id
end do
print '(a, l1)', 'empty now? ', .not. associated(top)
contains
subroutine push(id_in)
integer, intent(in) :: id_in
type(node_t), pointer :: fresh
allocate(fresh)
fresh%id = id_in
fresh%next => top
top => fresh
end subroutine push
subroutine pop(id_out, status)
integer, intent(out) :: id_out, status
type(node_t), pointer :: old
if (.not. associated(top)) then
id_out = 0; status = -1; return
end if
id_out = top%id
old => top
top => top%next
deallocate(old)
status = 0
end subroutine pop
end program fixed_stack
$ gfortran -std=f2018 -Wall fixed_stack.f90 -o stack && ./stack
popped 30
popped 20
popped 10
empty now? T
The output confirms the LIFO order (30, 20, 10) and an empty, disassociated stack at the end — and, though you cannot see it in the output, no leak: every node freed on its way out.
Now the professional judgment. We have a correct pointer stack. Should we keep it? In Fortran, almost
certainly not. A stack is a textbook case where an array wins (§11.3, §11.4): a stack is just an
allocatable array plus an integer "top" index. Push is n = n + 1; data(n) = x (growing the array when
full, as the second case study builds); pop is x = data(n); n = n - 1. There are no nodes to allocate,
no next pointers to maintain, no teardown loop, and — the point of this whole study — none of the three
bugs is even expressible: an integer index cannot be undefined-as-a-pointer, cannot leak, and cannot
dangle. The array version is faster (contiguous, cache-friendly), shorter, and safe by construction. The
pointer stack was not wrong to fix; it was wrong to exist, and the modernizing move is to replace it.
Discussion Questions
- Of the three bugs, which would you expect to survive the longest in production undetected, and why? Rank them by "days until someone notices."
- The undefined-pointer bug "happens to work" when uninitialized memory reads as null. Why is code that works by accident more dangerous than code that fails immediately? Connect your answer to the book's honesty discipline.
- The port to an array eliminates all three bug classes "by construction." Name another data structure where switching from pointers to an index-based array would remove entire categories of bug, and one where it genuinely would not.
Your Turn: Extensions
- Option A. Take the corrected pointer stack and add a
peek(id, status)operation that returns the topidwithout removing the node. Show thatpeekis safe even thoughpopfrees nodes — what makes the difference? - Option B. Rewrite the stack as an
allocatablearray with atopindex (fixed capacity is fine for a first pass). Compare the line counts and list, for each of the three original bugs, why it can no longer occur. - Option C. Add
-fcheck=allandvalgrindto your toolchain (previewing Chapter 13) and run both the leaking original and your fix. What does each tool report, and which bug does each catch?
Key Takeaways
- Pointer code fails in three characteristic ways — undefined status, leaks, dangling aliases — and all three are silent; you learn to see them on the page, not from a crash.
- A deallocation kills every alias to that memory. The language tracks the one pointer you named; the rest are your responsibility. Copy values out before you free what an alias still watches.
- Teardown is save-then-free: never read a node after deallocating it.
- The deepest fix is often not to fix the pointer code but to replace it. In Fortran a stack, a queue, or a
growable list is an
allocatablearray, and that choice makes the whole family of pointer bugs inexpressible.