Exercises: Modernizing Legacy Fortran
These exercises are heavy on the one skill this chapter exists to build: taking FORTRAN 77 to modern
Fortran without changing the answer. You will modernize fragments, diagnose migrations that went wrong,
reason about when bit-for-bit agreement is achievable, and extend the PLATE kernel. Do the modernizing
ones with a compiler open — the whole point is that the modern version builds clean and reproduces the
reference.
Difficulty: ⭐ warm-up · ⭐⭐ standard · ⭐⭐⭐ deeper. Solutions: worked solutions to the daggered (†)
and odd-numbered problems are in appendices/answers-to-selected.md; the computational ones are in
code/exercise-solutions.f90. Try every problem before you look. Never run a program to get its answer
before you have predicted it.
Part A — Warm-ups ⭐
18.1 † List the eight steps of the modernization recipe, in the recommended order, in your own words.
18.2 Explain why implicit none is step 1 and error handling is step 8. What makes one a natural
first move and the other a natural last one?
18.3 † Distinguish a rewrite from a refactoring. Why does the distinction matter more for a validated scientific code than for, say, a web front-end?
18.4 Define incremental modernization in one sentence, and name the two supports (from §18.2) that keep it safe.
Part B — Modernize It ⭐⭐
Rewrite each FORTRAN 77 fragment in modern style. State which recipe step(s) you are applying.
18.5 † Modernize this shared-state fragment (assume it appears in two subroutines):
PARAMETER (MX = 100)
COMMON /STATE/ U(MX), V(MX), NP
18.6 Modernize this convergence loop to structured control flow, preserving its exact behavior:
IT = 0
10 IT = IT + 1
CALL SWEEP(RES)
IF (RES .GT. EPS .AND. IT .LT. ITMAX) GO TO 10
18.7 † The legacy RELAX defines a statement function AVG(TL,TR,TB,TA) = 0.25D0*(TL+TR+TB+TA).
Give two correct modern replacements, and say when you would prefer each.
18.8 Modernize this subroutine header, adding intent and converting to assumed-shape:
SUBROUTINE NORM(A, N, S)
DIMENSION A(N)
18.9 † This fragment uses EQUIVALENCE to find the largest element of a 2-D grid. Replace it with a
single modern statement:
DIMENSION G(NX,NY), GV(NX*NY)
EQUIVALENCE (G(1,1), GV(1))
GMAX = GV(1)
DO 10 K = 2, NX*NY
IF (GV(K) .GT. GMAX) GMAX = GV(K)
10 CONTINUE
Part C — Find the Bug ⭐⭐
Each "modernization" below changed the answer, or fails to build. Diagnose it.
18.10 † A migration renders the loop of 18.6 as:
do
it = it + 1
call sweep(res)
if (res > eps .and. it < itmax) exit
end do
The solver now stops after one iteration. What went wrong, and what is the correct exit condition?
18.11 A COMMON /GRID/ T(NMAX,NMAX), N was modernized into a module, but one routine's results are now
wrong. The module declares real(dp) :: t(nmax,nmax) and integer :: n, and the buggy routine begins
use grid_data, only: t. What did the only: clause break, and how would implicit none have helped you
find it?
18.12 † A "modernization" of PLATE replaced the two-array Jacobi sweep with a single in-place update
(t(i,j) = 0.25_dp*(t(i-1,j)+t(i+1,j)+t(i,j-1)+t(i,j+1)), writing back into t immediately). It still
converges to 37.5 / 12.5, but in fewer iterations. Is this a valid refactoring? Explain what changed.
18.13 A migration promoted the plate's real field to real(dp). A reviewer runs
diff reference.txt modern.txt, sees differences in the low-order digits, and reports the migration as
broken. Are they right? What should they have compared instead?
Part D — Regression and Numerical Equivalence ⭐⭐
18.14 † When can you legitimately demand bit-for-bit agreement between the legacy and modern versions? Name two recipe steps that preserve the arithmetic and two changes that do not.
18.15 Explain, with a concrete example, why a numerical regression test must parse the numbers and
compare within tolerance rather than run diff on the text.
18.16 † "Bit-for-bit reproducible" is a property of more than the source code. Name two things that can change the bits even when the source is identical.
18.17 Write a one-paragraph regression-test plan for PLATE: what is the reference, what is the
tolerance, and what counts as a failure?
Part E — Design It ⭐⭐
Extend the PLATE kernel. Predict the output before you compile.
18.18 † (code) Modify PLATE for a 3×3 grid (one hot edge at 100°, three cold at 0°), which has a
single interior cell. What steady value does that cell take, and in how many sweeps does the convergence
loop stop? Verify with code. (Solution in code/exercise-solutions.f90.)
18.19 Add an error stop to relax that fires if the loop reaches maxit without converging (rather
than silently returning an unconverged field). Where exactly does the test go, and what message would you
print?
18.20 † (code) Compute the average interior temperature of the converged 4×4 field. Predict it
from the four interior values, then confirm with code. (Solution in code/exercise-solutions.f90.)
18.21 Extend set_boundary and reason about the result: if you raise the three cold edges from 0° to a
uniform $c$°, what happens to the interior solution? (Hint: Laplace's equation is linear; try $c = 10$.)
Part F — Back of the Envelope ⭐⭐⭐
Order-of-magnitude estimates. Show your reasoning.
18.22 † (code) PLATE's maximum change roughly halves each sweep. If it is 1.0 after some sweep, how
many more sweeps until it drops below $10^{-9}$? Compute by hand, then confirm with code. (Solution in
code/exercise-solutions.f90.)
18.23 You inherit a 15,000-line FORTRAN 77 code of roughly 40 modules. Adding implicit none and
converting each COMMON block takes about 30 minutes of editing per module. Estimate the editing time in
person-days. Then argue why the testing time — not the editing — usually dominates the schedule.
18.24 † Gauss–Seidel relaxation typically converges in roughly half the sweeps of Jacobi. If Jacobi needs 25 sweeps on the 4×4 plate, estimate Gauss–Seidel's count. What did you give up to get that speed, and why does it matter for the parallel solver of Part VIII?
Part G — Interleaved (earlier chapters) ⭐⭐
18.25 † (Ch. 8) Name two things the compiler can check about a module's shared variables that it
cannot check about a COMMON block's.
18.26 (Ch. 17) Under IMPLICIT DOUBLE PRECISION (A-H,O-Z), classify each name as integer or double:
ITER, TEMP, NSTEP, DELTA, KMAX, RHO. State the rule you used.
18.27 † (Ch. 6) Beyond documentation, why does intent(in) help the optimizer? Connect your answer
to the no-aliasing idea from Chapter 1.
18.28 (Ch. 13) Modernize this legacy failure into a proper diagnosis:
IF (N .GT. NMAX) THEN
WRITE (6,*) 'BAD N'
STOP
END IF
Solutions to the daggered and odd-numbered problems are in appendices/answers-to-selected.md; the
computational solutions (18.18, 18.20, 18.22) are in code/exercise-solutions.f90, each with a
hand-computed expected output.