Case Study 1: The COMMON-Block Bug
"The bug had been in production for six years. It was not in the physics. It was in the plumbing."
Executive Summary
A scientific code inherited from the 1980s produces a subtly wrong number — not a crash, not a NaN, just
a result that is quietly off — and the error has survived for years because nothing in the language ever
checked the assumption that broke. This case study is an exercise in reading such code: we take a
COMMON-block sharing bug of the kind Fortran programmers really do inherit, trace exactly how it goes
undetected, and then show how re-expressing the same shared state as a module turns the silent run-time
failure into a loud compile-time error. You will come away able to spot the single most common class of
legacy-Fortran defect on sight, and to explain — precisely, not vaguely — why modules make it impossible.
Skills applied: reading shared-state declarations and finding a mismatch (§8.4); the explicit-interface
and access-control guarantees a module provides (§8.2); translating COMMON to a module (§8.4); the
"private state behind a checked interface" pattern (§8.2). This foreshadows the legacy-reading skills of
Chapter 17.
Background
You have inherited PLATEQ, a small FORTRAN 77 code that computes properties of a rectangular
finite-difference grid. Two of its routines share the grid's dimensions through a COMMON block named
GRID. Here is what you find when you open the two files — and this is a faithful sketch of a real class
of bug, so read the declarations carefully before continuing:
SUBROUTINE SETGRID
C Fills the shared grid dimensions.
COMMON /GRID/ NX, NY
INTEGER NX, NY
NX = 200
NY = 100
END
SUBROUTINE GMEM(NBYTES)
C Reports the memory a double-precision field on the grid would need.
INTEGER NBYTES
COMMON /GRID/ NCELL
INTEGER NCELL
NBYTES = NCELL * 8
END
SETGRID sets the grid to $200 \times 100$. GMEM wants the total memory a double precision field would
occupy, at 8 bytes per cell. It should report $200 \times 100 \times 8 = 160{,}000$ bytes. It does not.
Phase 1 — The Symptom
Run the code (in your head — we never execute anything here) and GMEM returns a number that is not
$160{,}000$, and not even consistently wrong in an obvious way. On this machine it returns $1{,}600$: a
factor of one hundred too small. On a different compiler, or with the routines linked in a different order,
it might return something else entirely. The physics is fine. The grid is genuinely $200 \times 100$. And
yet the memory estimate is nonsense, and has been for years, causing PLATEQ to under-allocate a buffer
and occasionally corrupt a downstream calculation that no one has connected to this routine.
The tell: a result that is wrong but not random, changes with compiler or link order, and lives in code that shares state through
COMMON. That combination should make an experienced reader look straight at theCOMMONdeclarations, not at the arithmetic.
Phase 2 — Read the Declarations Side by Side
Lay the two COMMON /GRID/ declarations against each other. This is the whole skill:
SETGRID sees |
GMEM sees |
|
|---|---|---|
First word of /GRID/ |
NX (integer) — set to 200 |
NCELL (integer) — read as the cell count |
Second word of /GRID/ |
NY (integer) — set to 100 |
(not declared) |
| Block length | 2 integers | 1 integer |
Now the failure is visible. A COMMON block is nothing but a slab of memory that each routine reinterprets
through its own declarations. SETGRID writes 200 into the first word and 100 into the second. GMEM
reads the first word — 200 — and calls it NCELL, the total number of cells. It never sees the second
word at all. So GMEM computes NBYTES = 200 * 8 = 1600, not 20000 * 8 = 160000. The routine confused
"the number in the first slot" with "the product of the two dimensions," because the COMMON block gave it
no way to know the difference.
The bug is not a typo in the arithmetic. It is a disagreement between two routines about what the shared memory means, and FORTRAN 77 provided no mechanism to detect the disagreement.
Phase 3 — Why Nothing Caught It
Enumerate the safety nets that should have caught this, and watch each one fail to apply:
- Type checking? Both slots are integers, so even a type-aware check sees nothing wrong. (Had one
routine declared the block
REAL, the bits would have been silently reinterpreted — a worse version of the same disease.) - Length checking? The blocks are different lengths (two words vs one), which the standard permits; a shorter declaration simply views a prefix of the memory. No error.
- An interface? There is none.
COMMONshares data, invisibly, with no procedure boundary to check. - The compiler? It compiled each routine in isolation, and each routine is internally consistent. The contradiction only exists between them, in a place no single compilation unit can see.
This is the heart of the FORTRAN 77 organization problem from §8.4: the program's shared state is one global slab of untyped memory, and no compiler can check that any two routines agree about what is in it. The bug is not a mistake anyone made cleverly; it is the default failure mode of the mechanism.
Phase 4 — The Module Makes It Impossible
Now re-express the shared state as a module, exactly as §8.4 prescribes, and watch the bug become unrepresentable. There is one declaration of the grid, in one place, with names and types the compiler enforces everywhere:
module grid
implicit none
private
integer :: nx = 0, ny = 0
public :: set_grid, cell_count, mem_bytes
contains
subroutine set_grid(nx_in, ny_in)
integer, intent(in) :: nx_in, ny_in
nx = nx_in; ny = ny_in
end subroutine set_grid
pure integer function cell_count()
cell_count = nx * ny ! the ONE definition of "cells"
end function cell_count
pure integer function mem_bytes()
mem_bytes = cell_count() * 8 ! 8 bytes per double-precision cell
end function mem_bytes
end module grid
program plateq
use grid, only: set_grid, cell_count, mem_bytes
implicit none
call set_grid(200, 100)
print '(a, i0)', 'cells = ', cell_count()
print '(a, i0)', 'bytes = ', mem_bytes()
end program plateq
$ gfortran -std=f2018 -Wall grid.f90 plateq.f90 -o plateq && ./plateq
cells = 20000
bytes = 160000
Verify by hand: set_grid(200, 100) stores nx = 200, ny = 100; cell_count() = 200 * 100 = 20000;
mem_bytes() = 20000 * 8 = 160000. Correct, and — this is the point — impossible to get wrong the old
way. There is no "first word of the block" for a second routine to misread. nx and ny are private;
the only way to compute a cell count is to call cell_count(), which multiplies them. A second routine
cannot declare the grid differently, because it does not declare the grid at all — it uses the one
authoritative definition. The class of bug from Phase 2 has been legislated out of existence.
And had a maintainer tried to reintroduce it — say, by reaching for the raw dimensions under a wrong name — the compiler stops them cold:
! Somewhere in a careless refactor:
use grid, only: ncell ! there is no public `ncell`
Error: Symbol 'ncell' referenced at (1) not found in module 'grid'
The mistake that silently cost six years in the COMMON version is now a one-line compile error the first
time anyone types it.
Phase 5 — Sanity Check and the General Lesson
Confirm the fix on the numbers and on the principle. Numerically: $200 \times 100 = 20{,}000$ cells,
$\times 8$ bytes $= 160{,}000$ bytes — matching the hand calculation and, crucially, stable across
compilers and link orders because there is no memory-layout dependence left to vary. Structurally: the
grid's representation (nx, ny) is private, and every consumer goes through a checked interface, so the
two routines can no longer disagree about what the shared state means.
The transferable skill is the reading move of Phase 2: when a COMMON-based code returns a wrong but
non-random number, put the block's declarations side by side across routines before you touch the
arithmetic. Nine times in ten the defect is a disagreement about the block's contents — a reordered
variable, a type mismatch, a length difference — and every one of those disagreements is exactly what a
module would have refused to compile.
Discussion Questions
- In Phase 2,
GMEMread the first word of/GRID/. Suppose insteadGMEMhad declaredCOMMON /GRID/ NX, NCELL— reading the second word as the cell count. What would it have computed, and why is "it happens to readNY = 100" an even more insidious bug than readingNX? - The module version keeps
nxandnyprivate. Some codebases would instead make thempublicso other routines can read them directly. Argue for and against, using the guarantee that private state gives you. - This bug changed behavior with link order. Explain why the
COMMONversion is link-order-sensitive and the module version is not.
Your Turn: Extensions
- Option A. Reproduce the bug's shape safely: write two modern subroutines that each take the grid
dimensions as arguments, but call one of them with the arguments swapped (
step(ny, nx)). Show how, even withoutCOMMON, a positional-argument mistake can still bite — and how naming the arguments (keyword arguments, §8.2) defends against it. What does this tell you about where the remaining risk lives onceCOMMONis gone? - Option B. Take a
COMMONblock from real inherited code (or invent a three-variable one) and write the module that replaces it, keeping the state private behindset/getprocedures. Then write the one-line compile error you would get if a consumer tried to misuse it. - Option C. Research
BLOCK DATA(the FORTRAN 77 way to initialize aCOMMONblock) and explain, in a paragraph, what modern feature replaces it and why the replacement is safer. (You will meetBLOCK DATAfor real in Chapter 17.)
Key Takeaways
- The classic
COMMON-block bug is a disagreement between routines about what the shared memory means — a reordered, retyped, or re-lengthened declaration — and it fails silently because no compilation unit can see the contradiction. - The reading skill is diagnostic: a wrong-but-not-random result in
COMMON-based code points at the block declarations, not the arithmetic. Lay them side by side. - A module makes the bug unrepresentable: one authoritative, typed declaration; private representation; access only through a checked interface. The silent run-time failure becomes a loud compile-time error.
- This is why "convert
COMMONto modules" is the highest-value early move when modernizing legacy Fortran — it does not just tidy the code, it removes an entire class of defect.