> "Programs must be written for people to read, and only incidentally for machines to execute."
Prerequisites
- 4
- 8
- 17
- 18
Learning Objectives
- Look up any FORTRAN 77 construct and produce its faithful modern-Fortran equivalent, using a categorized dictionary.
- Translate storage and data constructs — COMMON, EQUIVALENCE, BLOCK DATA, DATA — into modules, transfer, derived types, and initializers.
- Convert unstructured control flow — GOTO, computed GOTO, arithmetic IF, labeled DO — into do/exit/cycle/if/select case.
- Replace statement functions with internal procedures and assumed-size arrays with assumed-shape, adding intent as you go.
- Move from fixed-form implicit-typed source to free-form source with implicit none and explicit declarations, without changing the numerics.
In This Chapter
Chapter 19: FORTRAN 77 to Modern Fortran — A Translation Dictionary
"Programs must be written for people to read, and only incidentally for machines to execute." — Harold Abelson and Gerald Jay Sussman, Structure and Interpretation of Computer Programs
Overview
The last two chapters gave you the two halves of working with old code. Chapter 17
taught you to read FORTRAN 77 — to look at fixed columns, COMMON blocks, GOTOs, and implicit typing
without flinching. Chapter 18 taught you the process
of modernization — the eight-step recipe, applied incrementally, with regression tests proving you changed
the engineering and not the science. This chapter is the third thing you need: the dictionary you keep
open on the desk while you do the work.
A dictionary is not a narrative; it is a lookup. When you hit a line of FORTRAN 77 you want to rewrite, you should not have to re-derive the modern equivalent from first principles — you should be able to find the old construct in a table, read across to its modern replacement, and note the one or two traps that make the translation faithful rather than merely plausible. That is what the following pages are: every legacy pattern you will actually meet, laid beside the modern Fortran that replaces it, organized so you can find it fast.
We keep one thread running through all of it. Across Chapters 17–19 the anchor is a single real program: a
FORTRAN 77 solver named PLATE that computes the steady-state temperature of a square metal plate by Jacobi
relaxation. It is fixed-form, implicitly typed, wired together with a COMMON block, seeded with DATA
statements, and driven by a GOTO convergence loop with a statement function at its core — a compact museum of
every FORTRAN 77 habit in one place. Chapter 17 read it; Chapter 18 began modernizing it; here we finish the
job, and every entry in the dictionary is illustrated with a real snippet from PLATE.
In this chapter, you will learn to:
- Use a categorized translation dictionary to convert any FORTRAN 77 construct to modern Fortran on sight.
- Translate the storage and data layer:
COMMON→ module,EQUIVALENCE→transferor a derived type,BLOCK DATAandDATA→ declared initializers. - Restructure control flow:
GOTO, computedGOTO, arithmeticIF, and labeledDOintodo,exit,cycle,if, andselect case. - Modernize procedures: statement functions into internal procedures, assumed-size arrays into
assumed-shape, and bare dummy arguments into
intent-annotated ones. - Convert form and typing: fixed-form to free-form, implicit typing to
implicit nonewith explicit declarations — and know which of these are style choices and which fix real bugs.
Learning Paths
How to read this chapter by track. - 🔧 Legacy ("I inherited old code") — this is your reference chapter. Read it straight through once to see the shape of the whole map, then keep it (and its twin, Appendix E) open while you work. The dictionary is the deliverable. - 📖 Standard — read §§19.1–19.4 for the reasoning behind each mapping; the standard's own list of obsolescent features is essentially this dictionary read backwards. - 🔬 Scientist ("my Python is too slow") — you came for the numerics in Part V, but you will still meet old code. Skim the tables; read the
COMMON→ module (§19.1) and statement-function (§19.3) entries closely, since those are the two you will hit first in a real solver. - ⚡ HPC ("I need parallel code") — legacy kernels are exactly what gets handed to you to parallelize. TheCOMMON→ module move (§19.1) and the assumed-size → assumed-shape move (§19.3) are prerequisites for the parallel back-ends of Part VIII; aCOMMONblock cannot be safely shared across images or threads.
This chapter defines almost nothing new — the terms all belong to Chapter 17 (the old constructs) and to Parts I–II (the modern ones). Its job is to connect them. Wherever a term first appeared, we link back rather than re-explain.
19.1 Storage and Data
FORTRAN 77 had no dynamic memory, no modules, and no user-defined types. Every technique for organizing data in that language is a workaround for one of those three absences, and every modern replacement is the feature that filled the gap. Understanding what the old construct was working around is the fastest way to pick the right modern equivalent.
📜 From History: In 1977 a program's memory was laid out, in full, at compile time. There was no
allocate. If a subroutine needed a scratch array, you either sized it for the largest case you would ever run (and wasted the rest) or you overlaid it on other storage withEQUIVALENCE. If two routines needed to share data, there were no modules to hold it, so you declared aCOMMONblock — a named slab of memory — in each of them and trusted every declaration to agree.COMMON,EQUIVALENCE, and the over-dimensioned array are not bad ideas badly executed; they are careful solutions to a problem modern Fortran simply does not have.
COMMON → module
The COMMON block (Chapter 17) is the single most important
translation you will do, because it is both the most common and the most dangerous. PLATE shares its
temperature array and its working grid size through one named COMMON block, declared identically in each of
its four program units:
C --- FORTRAN 77: shared state as a named COMMON block, repeated in every unit ---
PARAMETER (NMAX = 21)
COMMON /GRID/ T(NMAX,NMAX), N
PROGRAM PLATE, SETBC, RELAX, and OUTPT each repeat that line verbatim. Nothing checks that the
repetitions agree; a unit that lists the members in a different order, gives T a different bound, or omits
N, is accepted, and the program silently misreads the shared memory. The modern replacement is a module
(Chapter 8), where the data is declared
once, with names and types the compiler enforces at every use:
! --- Modern Fortran: shared state as a module ---
module grid_data
use kinds, only: dp
implicit none
real(dp), allocatable :: t(:,:)
integer :: n = 0
end module grid_data
Two improvements arrive together. First, the over-dimensioned static array T(NMAX,NMAX) becomes
allocatable (§19.1, below), so the grid is sized at run time to exactly what the problem needs. Second, and
more importantly, there is now a single authoritative declaration; the entire class of "two routines disagree
about what the block means" bug is gone, because there is only one declaration to agree with. A routine that
wants the grid writes use grid_data, and it sees t and n with their true types.
🚪 Threshold Concept. Every FORTRAN 77 construct in this chapter has a faithful, compiler-checked modern equivalent — and that is not a coincidence. Modern Fortran was designed, revision by revision, to subsume the old language: the committee looked at what
COMMON,GOTO, and statement functions were for, and added features (modules, structured loops, internal procedures) that do the same jobs while letting the compiler check whatCOMMONandGOTOleft unchecked. Translation is possible precisely because the new language was built to absorb the old one. Once you see the dictionary as deliberate, you stop asking "can this be modernized?" and start asking "which modern feature was designed to replace this?"
The direct translation above exposes t and n as public module variables — the closest mirror of
COMMON's global visibility. It is a correct and enormous improvement, but you can go one step further and
make the state private, reachable only through procedures, which is the design the
Chapter 8 case study builds. Better still,
bundle the grid into a derived type (Chapter 9)
so a routine receives a field_t argument instead of reaching into global state at all — the move the running
project makes. The dictionary entry is "COMMON → module"; the craft is choosing how far past the literal
translation to go.
🐍 Python Comparison: A
COMMONblock is FORTRAN 77's version of a pile of module-level globals shared byfrom mymodule import *— convenient, and a notorious source of "who changed this?" bugs. A Fortran module withprivatedata and public procedures is the same discipline Python programmers reach for when they wrap globals in a class or hide them behind functions: one owner, a checked interface, no spooky action at a distance.
EQUIVALENCE → transfer, derived types, or nothing
EQUIVALENCE (Chapter 17) forces two names to share the same
storage. In legacy code it was used for three quite different jobs, and each has a different modern
replacement — so the first step is always to diagnose why the EQUIVALENCE is there.
Job 1: saving memory by overlaying scratch arrays. When memory was scarce, a routine that needed a temporary buffer would overlay it on an array no longer in use:
C FORTRAN 77: reuse WORK's storage for SCRATCH to save memory
REAL WORK(10000), SCRATCH(10000)
EQUIVALENCE (WORK, SCRATCH)
The modern replacement is: don't. Allocate what you need, when you need it, and let it go
(Chapter 5). Memory is no longer the constraint that
justified the aliasing, and the aliasing itself defeats the compiler's optimizer, which now has to assume
work and scratch might be the same storage.
! Modern: allocate scratch on demand; no aliasing, better optimization
real(dp), allocatable :: scratch(:)
allocate(scratch(n)); ! ... use it ...; deallocate(scratch) ! or let it auto-deallocate
Job 2: reinterpreting the bits of a value — viewing a REAL as an INTEGER to inspect or hash its bit
pattern. This is the one genuinely valid use, and it has a genuine modern replacement: the intrinsic function
transfer, which reinterprets the bits of one object as another type, explicitly and portably:
! Modern: bit reinterpretation without aliasing
use, intrinsic :: iso_fortran_env, only: int32, real32
integer(int32) :: bits
bits = transfer(1.0_real32, 1_int32) ! the IEEE-754 encoding of 1.0 -> 1065353216 (0x3F800000)
transfer(source, mold) says exactly what EQUIVALENCE only implied: "give me the bits of source,
interpreted as the type of mold." It is a function, so it has no lasting alias, and it works on the value
you hand it rather than on a permanent overlap of two variables. (The IEEE-754 bit patterns behind that
0x3F800000 are the subject of Chapter 20.)
Job 3: treating one slab of storage as several named fields — the poor man's struct. That is exactly
what a derived type is for (Chapter 9):
declare a type with named, typed components and let the compiler lay it out. You get the "several names, one
object" behavior with none of the danger, because the components cannot overlap by accident.
⚠️ Common Pitfall: Do not translate an
EQUIVALENCEmechanically without first asking which of the three jobs it is doing. Replacing a memory-saving overlay withtransferis nonsense; replacing a bit-reinterpretation with a derived type changes the meaning. Read the surrounding code, decide the intent, then pick the matching replacement.EQUIVALENCEis the one entry in this dictionary where the right answer depends most on why, not what.
BLOCK DATA and DATA → declared initializers
DATA gave a variable its initial value; BLOCK DATA was the special program unit whose only job was to
initialize COMMON blocks (you cannot initialize COMMON with DATA inside a normal routine). PLATE
seeds its edge temperatures and its convergence controls with DATA:
C FORTRAN 77: initial values via DATA (types come from IMPLICIT DOUBLE PRECISION)
DATA THOT, TCOLD / 100.0D0, 0.0D0 /
DATA TOL, MAXIT / 1.0D-6, 1000 /
Modern Fortran folds the initial value into the declaration itself, and — since these values never change —
into a parameter where appropriate:
! Modern: initialize at the point of declaration
real(dp), parameter :: t_hot = 100.0_dp, t_cold = 0.0_dp
real(dp), parameter :: tol = 1.0e-6_dp
integer, parameter :: max_iter = 1000
BLOCK DATA disappears entirely: because a module variable can carry an initializer (integer :: n = 4),
there is no separate unit whose job is to initialize shared storage. The initializer is the initialization.
(PLATE's convergence controls TOL and MAXIT live in exactly such a BLOCK DATA unit in some versions of
the code — the perfect thing to fold into module initializers.)
⚠️ Common Pitfall — the implicit
SAVEtrap. ADATAstatement, and a modern declaration-initializer, both give a local variable in a procedure theSAVEattribute implicitly: it is initialized once, not on every call, and it keeps its value between calls.real(dp) :: total = 0.0_dpinside a subroutine does not resettotalto zero each time the subroutine runs — a bug that has bitten every Fortran programmer at least once. If you want a fresh zero each call, initialize with an executable statement (total = 0.0_dpon its own line) instead. This trap is old (DATAhad it) and new (initializers inherit it); Chapter 13 treats it in full.
The static over-dimensioned array → allocatable
One more storage idiom deserves its own entry because you will see it in every FORTRAN 77 numerical code. With no dynamic memory, the pattern for "an array whose size is known only at run time" was to declare it at a compile-time maximum and carry the actual size in a separate integer:
C FORTRAN 77: over-dimension to a max, carry the real size in N
PARAMETER (NMAX = 21)
DOUBLE PRECISION T(NMAX, NMAX)
...
N = 4 ! the real size; rows/cols N+1..NMAX are wasted
The modern replacement is an allocatable array
(Chapter 5), sized to exactly what the run needs:
! Modern: size to the real problem at run time
real(dp), allocatable :: t(:,:)
allocate(t(n, n)) ! exactly n*n cells, no waste, no NMAX ceiling
This removes the arbitrary NMAX ceiling (the old code silently failed — or overran — for grids larger than
21), reclaims the wasted memory, and lets size(t, 1) report the true extent. Prefer allocatable to
pointer for this; the reasons — automatic deallocation and no aliasing — are exactly those of
Chapter 11.
🔄 Check Your Understanding. 1. A FORTRAN 77 subroutine declares
COMMON /GRID/ NX, NYand another declaresCOMMON /GRID/ NY, NX. What goes wrong, and why does a module make the mistake impossible? 2. You findEQUIVALENCE (A, IA)whereAisREALandIAisINTEGER, used to print the bits ofA. Which modern feature replaces it, and why is it safer? 3.real(dp) :: count = 0.0_dpappears inside a subroutine that is called in a loop. Why mightcountnot behave the way a newcomer expects?Answers
1. The two routines disagree about which word of the block isNXand which isNY, so one of them reads the dimensions swapped; the program computes on the wrong grid with no error. A module declaresnx, nyonce with fixed names and types, so there is nothing to disagree with. 2.transfer(a, mold)reinterprets the bits explicitly as a value, with no permanent alias and no risk of the optimizer being confused about which storage is which. 3. The initializer givescountan implicitSAVE: it is set to zero once, not on each call, so it accumulates across calls instead of resetting. Use an executablecount = 0.0_dpfor a per-call reset.
19.2 Control Flow
FORTRAN 77 had IF, a counted DO, and — for everything else — GOTO. Loops, early exits, multi-way
branches, and error escapes were all built by jumping to numbered labels. Modern Fortran has a dedicated,
named construct for each of those jobs (Chapter 4),
and the translation is mostly a matter of recognizing which job a given GOTO is doing and reaching for the
construct built for it. The reward is code whose control structure you can see, and which the compiler can
check for you.
The GOTO convergence loop → do / exit
The heart of PLATE is its relaxation loop: sweep the interior, measure the largest change, and jump back to
the top unless the change is small enough (or you have run out of iterations). In FORTRAN 77 that is a
backward GOTO:
C --- FORTRAN 77: PLATE's relaxation loop, built from a GOTO and labels ---
ITER = 0
60 CONTINUE
ITER = ITER + 1
DMAX = 0.0D0
DO 80 J = 2, N-1
DO 70 I = 2, N-1
TNEW(I,J) = AVG(T(I-1,J), T(I+1,J), T(I,J-1), T(I,J+1))
DIFF = ABS(TNEW(I,J) - T(I,J))
IF (DIFF .GT. DMAX) DMAX = DIFF
70 CONTINUE
80 CONTINUE
DO 100 J = 2, N-1
DO 90 I = 2, N-1
T(I,J) = TNEW(I,J)
90 CONTINUE
100 CONTINUE
IF (DMAX .GT. TOL .AND. ITER .LT. MAXIT) GO TO 60
The modern version names the loop and its exit condition, so the "repeat until converged, but cap the iterations" intent is visible at a glance:
! --- Modern Fortran: the same loop, structured ---
iter = 0
tnew = t ! copy once; boundary values stay fixed
do
iter = iter + 1
dmax = 0.0_dp
do j = 2, n-1
do i = 2, n-1
tnew(i,j) = avg4(t(i-1,j), t(i+1,j), t(i,j-1), t(i,j+1))
dmax = max(dmax, abs(tnew(i,j) - t(i,j)))
end do
end do
t = tnew ! whole-array copy-back (Ch. 5)
if (dmax <= tol .or. iter >= max_iter) exit
end do
Read the two side by side and the labeled DO 80 ... 80 CONTINUE pairs become plain do ... end do; the
backward GO TO 60 becomes an unconditional do with an explicit exit; and the copy-back loop becomes a
single whole-array assignment t = tnew. The logic is identical — this is a faithful translation, the kind
Chapter 18 insists you verify with a regression test —
but the modern form cannot "fall through" to the wrong label, and the compiler now matches every do with its
end do.
🐛 Find the Bug. A well-meaning modernizer rewrites the convergence test as a
do whileand drops what looks like redundant bookkeeping:
fortran iter = 0 do while (dmax > tol) ! BUG: the iteration cap is gone iter = iter + 1 ! ... sweep, compute dmax ... end doWhat did they break? The original condition was
DMAX .GT. TOL .AND. ITER .LT. MAXIT— two clauses. Dropping theiter < max_iterclause means that if the problem never converges (a bad configuration, a sign error in the physics, a tolerance below the achievable precision), the loop runs forever instead of stopping and reporting non-convergence. The cap is not bookkeeping; it is the safety valve. Keep both clauses:do while (dmax > tol .and. iter < max_iter), or thedo ... if (...) exitform above. When you translate a compoundGOTOcondition, translate every clause.
GOTO's other jobs → exit, cycle, and restructuring
Not every GOTO is a loop. Diagnose the job before you translate:
What the GOTO does |
Modern replacement |
|---|---|
| Jumps back to repeat a block | do ... end do with exit |
| Jumps forward, out of a loop (early finish) | exit (optionally a named-loop exit outer) |
| Jumps to the end of the loop body to start the next iteration | cycle |
| Jumps out of a procedure on an error | return, or error stop (Ch. 13) |
| Jumps around a block to skip it conditionally | wrap the block in if (...) then ... end if |
Named constructs make the two loop-escape cases precise. When you must break out of nested loops — a common
reason for a forward GOTO in old code — name the outer loop and exit it by name, so there is no ambiguity
about which loop you are leaving:
search: do j = 1, ny
do i = 1, nx
if (t(i,j) > t_melt) then
hot_i = i; hot_j = j
exit search ! leave BOTH loops -- was a forward GO TO
end if
end do
end do search
🐍 Python Comparison: Python has no
gotoat all — a deliberate choice — and itsbreak,continue, andfor/elsecover exactly the cases above. Fortran'sexitis Python'sbreak; Fortran'scycleis Python'scontinue; Fortran's named loops (exit search) do the job Python needs an extra flag variable or an exception to accomplish. Translating FORTRAN 77GOTOs toexit/cycleis, in effect, rewriting the code in the structured style Python never let you leave.
Computed GOTO → select case
The computed GOTO branches to one of several labels chosen by an integer
(Chapter 17):
C FORTRAN 77: branch to label #K
GO TO (100, 200, 300), K
This is precisely select case (Chapter 4),
which says the same thing with none of the label-chasing — and, crucially, gives you a case default for the
out-of-range value the computed GOTO silently mishandled:
! Modern: the multi-way branch, made explicit and total
select case (k)
case (1)
call do_first()
case (2)
call do_second()
case (3)
call do_third()
case default
error stop 'k out of range' ! the computed GOTO had no such guard
end select
Arithmetic IF → if / else if / else
The three-way arithmetic IF branches on the sign of an expression — negative, zero, positive — to three
labels:
C FORTRAN 77: branch on sign(RESID) -> negative, zero, positive
IF (RESID) 10, 20, 30
Modern Fortran spells the three cases out as an ordinary block if:
! Modern: the three-way sign test, readable
if (resid < 0.0_dp) then
call handle_negative()
else if (resid == 0.0_dp) then
call handle_zero()
else
call handle_positive()
end if
⚠️ Common Pitfall: The middle branch of an arithmetic
IFfires only on an exact zero. For a real expression that is almost always the wrong test — floating-point results rarely land on exactly0.0(Chapter 20). When you translate an arithmeticIFon a real value, treat it as an opportunity to fix a latent bug: replace the exactresid == 0.0_dpwith a tolerance test,abs(resid) < eps, if the "zero" case is meant to catch "small enough." Preserve exact-zero only when the value is a genuine integer or a deliberately exact sentinel.
Labeled DO and CONTINUE → do / end do
The counted loop itself changes only cosmetically, but the cosmetics matter for readability. A FORTRAN 77
DO names a terminating label; nested loops often shared one CONTINUE:
C FORTRAN 77: shared terminator for two nested loops
DO 40 J = 1, NY
DO 40 I = 1, NX
T(I,J) = 0.0
40 CONTINUE
Modern Fortran gives each loop its own end do, which removes a real hazard (adding a statement "after" the
inner loop but "before" the shared label is a classic slip):
! Modern: one end do per loop -- unambiguous nesting
do j = 1, ny
do i = 1, nx
t(i,j) = 0.0_dp
end do
end do
Better yet, when the whole nest just initializes an array, collapse it to a whole-array assignment,
t = 0.0_dp, and let the compiler write the loops.
⚡ Performance Note: Structured control flow is not a tax you pay for readability — it is frequently faster. A visible
doloop with a cleanexitis easier for the optimizer to analyze, unroll, and vectorize than a tangle ofGOTOs whose target it must treat conservatively. And the whole-array forms (t = tnew,t = 0.0_dp) hand the compiler an entire operation to optimize at once. Modernizing control flow tends to make code both clearer and quicker; you rarely trade one for the other.🔄 Check Your Understanding. 1. A backward
GO TOthat repeats a block becomes which modern construct? What about a forwardGO TOthat leaves a loop early? 2. Why isselect casea better translation of a computedGOTOthan a chain ofif/else if? 3. You translateIF (X) 10, 20, 30whereXis areal(dp)residual. Which branch is the dangerous one, and what should you consider changing?Answers
1. A backwardGO TO→ ado ... end doloop withexit; a forwardGO TOout of a loop → anexit(named, if it must leave more than one loop). 2.select casestates the multi-way branch as a single total construct with acase defaultfor out-of-range values, which the computedGOTOhandled by silently falling through — a latent bug. 3. The middle branch (case 20, the exact-zero case): a real residual almost never equals0.0exactly, so the branch effectively never fires. Consider a tolerance testabs(x) < epsif it was meant to catch "negligibly small."
19.3 Procedures
FORTRAN 77 procedures worked, but they hid information the modern language makes explicit: what a dummy argument's shape is, whether the procedure may modify it, and where a small helper computation lives. The three translations in this section all add checkable information the old code left implicit.
Statement functions → internal procedures
A statement function (Chapter 17) is a one-line function
defined among the declarations. PLATE uses one for the four-neighbor average at the core of the Jacobi
sweep:
C FORTRAN 77: PLATE's statement function (left, right, below, above neighbours)
AVG(TL, TR, TB, TA) = 0.25D0 * (TL + TR + TB + TA)
Statement functions were declared obsolescent in Fortran 90 and are removed from the current standard, so
you cannot rely on them compiling under a strict modern mode — they must be translated. The faithful
replacement is an internal procedure (Chapter 6):
a real function, after contains, with declared argument types, an intent, and — for a pure computation like
this — the pure attribute that tells the compiler it has no side effects:
! Modern: an internal pure function -- typed, checked, and optimizer-friendly
pure function avg4(tl, tr, tb, ta) result(m)
real(dp), intent(in) :: tl, tr, tb, ta
real(dp) :: m
m = 0.25_dp * (tl + tr + tb + ta)
end function avg4
You gain declared types (the statement function inherited whatever implicit typing gave TL, TR, TB, TA),
an explicit interface, and the pure promise that lets the compiler inline and reorder the call freely — the
same promise the running project relies on to parallelize its update later
(Chapter 6 makes the case; the payoff is in
Part VIII). The one-line brevity is gone, but a three-line internal function is a small price for a computation
the compiler now understands and checks.
Assumed-size → assumed-shape
FORTRAN 77's array arguments could not carry their own shape. The assumed-size form a(*) leaves the last
extent unknown to the procedure, so the caller must pass the size separately, and size, whole-array
operations, and bounds checking all fail on the dummy
(Chapter 6). A typical legacy print helper — the
kind bolted onto a grid code like PLATE — shows the pattern:
C FORTRAN 77: assumed-size dummy, size passed by hand
SUBROUTINE PRTROW(ROW, N)
REAL ROW(*)
INTEGER N, K
WRITE (*, '(100F6.1)') (ROW(K), K = 1, N)
END
The modern assumed-shape form row(:) carries the extent with the array, so the n argument disappears
and size(row) works inside the procedure:
! Modern: assumed-shape carries its own size; N is redundant
subroutine print_row(row)
real(dp), intent(in) :: row(:)
write(*, '(*(f6.1))') row ! size(row) known; unlimited-format list
end subroutine print_row
Assumed-shape requires an explicit interface — which you get for free by putting the procedure in a module or
contains (Chapter 8), the very move
§19.1 already made. For a two-dimensional argument the win is larger: T(NX, *) (which needs NX passed and
correct) becomes t(:,:) (which needs nothing passed and cannot be given the wrong leading dimension).
Bare dummy arguments → add intent
FORTRAN 77 had no intent. Every dummy argument was, in effect, intent(inout): the procedure could read it,
write it, or both, and nothing recorded which. Modern Fortran asks you to declare the data flow, and the
compiler enforces it (Chapter 6):
C FORTRAN 77: no intent -- data flow is undocumented and unchecked
SUBROUTINE SETEDGE(T, NX, NY, THOT, TCOLD)
REAL T(NX, NY), THOT, TCOLD
...
END
! Modern: intent on every argument -- documented and compiler-checked
subroutine set_edges(t, t_hot, t_cold)
real(dp), intent(inout) :: t(:,:) ! reads shape, overwrites edges
real(dp), intent(in) :: t_hot, t_cold
...
end subroutine set_edges
⚠️ Common Pitfall: Adding
intentis not always mechanical, and that is a feature, not a nuisance. Deciding whether an argument isin,out, orinoutforces you to determine what the procedure actually does with it — and legacy routines sometimes modify an argument the caller never expected them to. When theintentyou write contradicts how the code behaves, the compiler flags it, and you have found a real question about the code's data flow. This is exactly the kind of thing Chapter 18's regression tests exist to keep honest: add theintent, recompile, and if the build breaks, understand why before you "fix" it.🔗 Connection: The three procedure translations compound. Once
set_edgesandprint_roware internal or module procedures with assumed-shape arguments and declared intents, they have explicit interfaces — which is what lets the compiler check every call, inline the small ones, and (for thepureones) reorder them freely. The modernizedPLATEyou assemble in this chapter's Project Checkpoint is faster and safer than the original for reasons that all trace back to information these translations made explicit.
19.4 Form and Typing
The last two translations are the most visible and, in one case, the most consequential. One is largely cosmetic; the other fixes a whole category of silent bugs.
Fixed-form → free-form
Fixed-form source (Chapter 17) assigns meaning to columns: a
label in 1–5, a continuation mark in column 6, statements in 7–72, a C or * in column 1 for a comment.
Free-form source frees you from the columns entirely. The mechanical conversion:
| Fixed-form (FORTRAN 77) | Free-form (modern) |
|---|---|
C or * in column 1 begins a comment |
! begins a comment, anywhere on the line |
| statement starts in column 7 | statement starts anywhere |
| any character in column 6 continues the previous line | a trailing & continues to the next line |
| statement label in columns 1–5 | labels rarely needed; keep only where referenced |
| line limited to column 72 | lines to 132 characters, continued with & |
C FIXED-FORM: continuation marked in column 6, statement from column 7
TOTAL = A + B + C +
& D + E + F
! Free-form: trailing & continues the line
total = a + b + c + &
d + e + f
Fixed-form is still legal Fortran — a compiler will accept a .f file forever — so this is a style
translation, not a correctness one. But it is worth doing: free-form is what every modern tool, example, and
colleague expects, and the column rules are a needless source of "why won't this compile?" (a statement that
strays into column 73 is silently truncated). Most compilers choose form by file extension: .f and .for
are fixed, .f90 and later are free. Renaming and reflowing is the bulk of the work; tools like findent or
fprettify can do the first pass.
While you are reflowing, you may also modernize the relational operators, though you are not required to — the old forms remain valid:
| FORTRAN 77 | Modern | FORTRAN 77 | Modern | |
|---|---|---|---|---|
.LT. |
< |
.GE. |
>= |
|
.LE. |
<= |
.GT. |
> |
|
.EQ. |
== |
.NE. |
/= |
The logical operators .and., .or., .not., .eqv., and .neqv. have no symbolic form and are unchanged.
Implicit typing → implicit none + declarations
This is the translation that fixes bugs. FORTRAN 77 typed any undeclared variable by its first letter: I
through N were INTEGER, everything else REAL (Chapter 17).
Modern style forbids this with implicit none (Chapter 2),
which requires every variable to be declared — and thereby catches the typo that implicit typing turns into a
silent second variable.
The procedure is reliable and worth memorizing:
- Add
implicit noneat the top of the program unit (afteruse, before declarations). - Compile. The compiler now reports every undeclared name — one error per variable.
- Declare each name with the type implicit typing would have given it, unless you can see it was meant to be
something else. Preserve the numerics: a variable that was implicitly
INTEGER(say, a loop countern) must stayinteger, and one that was implicitlyREALmust stay real (choosing your precision deliberately — see below). - Recompile until clean, then run the regression test (Chapter 18) to confirm you changed no values.
C FORTRAN 77: implicit typing -- DIFF and TEMP are REAL, I and N are INTEGER
DIFF = 0.0
DO 10 I = 1, N
TEMP = X(I) - XBAR
DIFF = DIFF + TEMP*TEMP
10 CONTINUE
! Modern: implicit none + explicit declarations
implicit none
real(dp) :: diff, temp, xbar
real(dp) :: x(:)
integer :: i, n
diff = 0.0_dp
do i = 1, n
temp = x(i) - xbar
diff = diff + temp*temp
end do
⚠️ Common Pitfall: The reason
implicit noneearns its place is the typo. In the FORTRAN 77 fragment above, misspellingDIFFasDFIFon one line creates a brand-new implicitly-typed real variable, initialized to garbage, and the sum is silently wrong — no error, ever. Underimplicit none,dfifis an undeclared name and the compile fails on the spot. This single habit, made non-negotiable in Chapter 2, eliminates one of the most expensive bug classes in all of legacy Fortran.
A close cousin is the blanket IMPLICIT DOUBLE PRECISION (A-H, O-Z) that many numerical codes used to make
everything double precision without declaring it. Translate it the same way — add implicit none, declare
each real as real(dp) — and take the moment to choose precision deliberately rather than by a first-letter
rule (Chapter 20 is where precision
becomes a decision, not an accident).
📜 From History: Implicit typing was a 1957 convenience — it saved punching declaration cards, and the
I–N-means-integer rule matches the mathematician's habit of using $i, j, k, m, n$ for indices and counts. The habit outlived its usefulness the moment programs grew large enough for a single typo to hide for years.implicit noneis Fortran 90's admission that the convenience was never worth the cost — and it is the one FORTRAN 77 default that modern Fortran most decisively reverses.🔄 Check Your Understanding. 1. Is converting fixed-form to free-form a correctness fix or a style fix? Is converting implicit typing to
implicit nonea correctness fix or a style fix? 2. What is the reliable four-step procedure for addingimplicit noneto a legacy routine? 3. Why does.LT.not need to be changed to<?Answers
1. Fixed → free is a style fix (fixed-form remains legal). Implicit typing →implicit noneis a correctness fix: it catches typos that implicit typing turns into silent new variables. 2. Addimplicit none; compile; declare each name the compiler flags with the type implicit typing would have given it (preserving the numerics); recompile clean and run the regression test. 3. The.LT.family remains valid modern Fortran;<was added in Fortran 90 as an alternative, not a replacement. Changing it is optional polish.
19.5 The Quick-Reference Table
Here is the whole dictionary on one page — the table to keep beside you while you work, reproduced (with more examples) as Appendix E. The Status column notes how the current standard regards the old construct: legal (still standard, a style choice), obsolescent (retained but flagged by the standard as discouraged), or removed (deleted from a modern standard — it must be translated). Treat the status as guidance and verify the exact clause against ISO/IEC 1539-1 for anything load-bearing.
Storage and data (§19.1)
| FORTRAN 77 | Modern Fortran | Status of old form | Note |
|---|---|---|---|
COMMON /blk/ a, b |
a module with use (or a derived-type argument) |
obsolescent | one authoritative typed declaration; ends the sharing bug |
BLOCK DATA |
module variable initializers | obsolescent | initializer is the initialization |
EQUIVALENCE (bit reinterpret) |
transfer(x, mold) |
obsolescent | explicit, typed, no lasting alias |
EQUIVALENCE (save memory) |
separate allocatable arrays |
obsolescent | memory is no longer the constraint |
EQUIVALENCE (fields of storage) |
a derived type | obsolescent | named, typed components, no overlap |
DATA x /1.0/ |
real(dp) :: x = 1.0_dp initializer |
legal | beware implicit SAVE in procedures |
PARAMETER (P = 3.14) |
real(dp), parameter :: p = 3.14_dp |
legal | add a kind suffix |
over-dimensioned x(MAX) + real size |
allocatable + allocate |
legal | size to the true problem |
Control flow (§19.2)
| FORTRAN 77 | Modern Fortran | Status of old form | Note |
|---|---|---|---|
backward GO TO (loop) |
do ... end do + exit |
legal | name the loop if nested |
forward GO TO (leave loop) |
exit / exit name |
legal | one construct per escape |
GO TO end-of-body (next iter) |
cycle |
legal | |
GO TO out of a procedure (error) |
return / error stop |
legal | Ch. 13 |
computed GO TO (a,b,c), k |
select case (k) |
obsolescent | add case default |
arithmetic IF (e) n1,n2,n3 |
if / else if / else |
obsolescent | exact == 0 on reals is fragile |
assigned GO TO / ASSIGN |
select case or a procedure |
removed (F95) | |
DO 10 I=1,N ... 10 CONTINUE |
do i=1,n ... end do |
legal | one end do per loop |
shared-terminator nested DO |
separate end dos |
legal | removes a real hazard |
PAUSE |
remove, or a read prompt |
removed (F95) |
Procedures (§19.3)
| FORTRAN 77 | Modern Fortran | Status of old form | Note |
|---|---|---|---|
statement function f(x)=... |
internal/module pure function |
removed (F2018) | typed, checked, inlinable |
assumed-size a(*) / a(n,*) |
assumed-shape a(:) / a(:,:) |
legal | carries its own extent |
| dummy args with no intent | add intent(in/out/inout) |
legal | compiler-checked data flow |
ENTRY (multiple entry points) |
separate module procedures | obsolescent | share via module state |
alternate return CALL S(*10) |
a status argument + if |
obsolescent | |
| external proc (implicit interface) | module procedure (explicit) | legal | enables checking + inlining |
Form and typing (§19.4)
| FORTRAN 77 | Modern Fortran | Status of old form | Note |
|---|---|---|---|
| fixed-form (columns 1–72) | free-form | legal | .f → .f90; style, not correctness |
C/* comment in column 1 |
! comment anywhere |
legal | |
| continuation in column 6 | trailing & |
legal | |
implicit typing (I–N = integer) |
implicit none + declarations |
legal | correctness fix — catches typos |
IMPLICIT DOUBLE PRECISION (...) |
explicit real(dp) |
legal | choose precision deliberately (Ch. 20) |
.LT. .LE. .EQ. .NE. .GE. .GT. |
< <= == /= >= > |
legal | old forms still valid |
Hollerith 6HFORTRA |
a character literal 'FORTRA' |
removed | |
CHARACTER*8 NAME |
character(len=8) :: name |
obsolescent (the *len form) |
💡 Intuition: Read the whole table top to bottom and a pattern emerges. Nearly every "obsolescent" or "removed" old form is one that let the compiler not check something — the block two routines might describe differently, the label a
GOTOmight miss, the argument shape the caller might get wrong, the variable a typo might invent. The modern replacements are, almost without exception, the same computation with the checking turned back on. That is the through-line of the entire dictionary, and of Part IV: modernization trades unchecked cleverness for checked clarity, and the science is preserved on the way.
Project Checkpoint
Across Chapter 17 you met the FORTRAN 77 PLATE kernel and read
it; in Chapter 18 you applied the first two modernization
steps — implicit none and free-form. Now you finish the job, using this chapter's dictionary as your
checklist. The remaining steps, and the entry each one comes from:
| Step | Dictionary entry | Applied to PLATE |
|---|---|---|
COMMON → module |
§19.1 | /GRID/ T(NMAX,NMAX), N becomes module grid_data (later, a field_t) |
static arrays → allocatable |
§19.1 | T(NMAX,NMAX) → allocate(t(n,n)) |
DATA → initializers |
§19.1 | edge temps and tolerance become parameters / keyword defaults |
| statement function → internal proc | §19.3 | AVG(TL,TR,TB,TA) → pure function avg4(...) |
| assumed-size → assumed-shape | §19.3 | grid arguments become t(:,:) |
add intent |
§19.3 | every dummy argument gets in/out/inout |
GO TO loop → do/exit |
§19.2 | the relaxation loop becomes structured |
IMPLICIT DOUBLE PRECISION → implicit none |
§19.4 | every variable declared real(dp) / integer |
The result is project-checkpoint.f90: a clean, modular, modern solver whose numerics are identical to the
original. Its heart is the structured relaxation loop and the pure averaging function, on a real(dp)
field. Driven on the same 4×4 test plate Chapter 18
established as the reference, it reproduces that reference exactly:
! The modern PLATE on a 4x4 plate: top edge hot (100), three edges cold (0).
call set_boundary(t, thot = 100.0_dp, tcold = 0.0_dp)
call relax(t, tol = 1.0e-6_dp, maxit = 1000, iters = iters)
print '(a, i0, a)', 'converged in ', iters, ' iterations'
call print_interior(t)
$ gfortran -std=f2018 -Wall -O2 project-checkpoint.f90 -o plate && ./plate
converged in 25 iterations
37.5000 37.5000
12.5000 12.5000
That output is the fixed point Chapter 18 proved by hand,
and reproducing it exactly is how you know the translation preserved the physics. On the 4×4 plate, the two
interior cells next to the hot edge equal $a$ and the two next to the cold edge equal $b$; steady state
requires each to equal the average of its four neighbours, giving $3a - b = 100$ and $a = 3b$, hence
$8b = 100 \Rightarrow b = 12.5$, $a = 37.5$. Because promoting the statement function AVG to the pure
function avg4 preserves the operand order, the modern program runs the identical double-precision
arithmetic and converges in the identical 25 sweeps — a bit-for-bit faithful translation, not merely a
close one.
This side-quest is complete: the FORTRAN 77 PLATE is now modern Fortran, translated line by line through the
dictionary, and it still computes what it always did. The main project — your own heat solver — meets the
real finite-difference stencil in Chapter 24,
where the same five-point average you just modernized becomes the genuine PDE core.
Summary
This chapter is a reference; its summary is a table of the mappings, and the two facts that make them safe.
| Category | The move | The reason |
|---|---|---|
| Storage | COMMON → module; EQUIVALENCE → transfer/derived type; static → allocatable; DATA → initializer |
one authoritative typed declaration; end global aliasing; size to the real problem |
| Control | GOTO/computed GOTO/arithmetic IF/labeled DO → do/exit/cycle/if/select case |
the control structure becomes visible and compiler-checked |
| Procedures | statement function → internal pure function; assumed-size → assumed-shape; add intent |
the shape and the data flow the old code hid become explicit |
| Form/typing | fixed → free-form; implicit typing → implicit none + declarations |
style, and the one correctness fix that catches typos |
The two things to remember. First, diagnose before you translate — an EQUIVALENCE and a GOTO each
do several different jobs, and the faithful replacement depends on which. Second, translation preserves the
numerics — every entry in this dictionary changes the engineering, not the science, and a regression test
(Chapter 18) is how you prove it. When both hold, you
have done the one thing legacy scientific code most needs: modernized the code without touching the results.
Spaced Review
Retrieval practice on the two chapters that set up this one — Chapter 17 (reading FORTRAN 77) and Chapter 18 (the modernization process).
-
(Ch. 17) In fixed-form source, what is the significance of column 6, and what character in column 1 marks a comment line?
Answer
A non-blank, non-zero character in **column 6** marks the line as a *continuation* of the previous statement. A `C` (or `*`) in **column 1** marks the whole line as a comment. Statements themselves live in columns 7–72. -
(Ch. 17) Why can two subroutines that both declare
COMMON /GRID/disagree about the block's contents without the compiler noticing?
Answer
A `COMMON` block is just a slab of memory each routine reinterprets through its *own* declarations, compiled in isolation. Nothing forces the declarations to match, so a reordered, retyped, or differently-lengthed declaration is accepted and silently misreads the shared memory. -
(Ch. 18) The modernization recipe is applied incrementally, one step at a time, rather than as a single rewrite. What is the practical reason?
Answer
So that after each small, reversible change you can recompile and run the **regression test** to confirm the results are unchanged. If a step breaks the numerics, you know exactly which change did it. A big-bang rewrite gives you no such foothold and risks silently breaking validated science. -
(Ch. 18) What does it mean to test a modernization for numerical equivalence, and when might "bit-for-bit identical" be too strict a standard?
Answer
It means confirming the modernized code produces the *same results* as the original. Bit-for-bit identity is achievable when you truly changed only form; but if you also changed something that affects rounding — reordering a sum, or moving from single to `real(dp)` — the results may differ in the last digits, and the right test is agreement to a stated **tolerance**, not exact equality. -
(Ch. 17 & 18) A legacy routine uses a statement function and an assumed-size array argument. Which one must you translate to compile under a strict modern standard, and which is merely advisable?
Answer
The **statement function** must go — it is removed from the current standard. The **assumed-size** array (`a(*)`) is still legal, so translating it to assumed-shape (`a(:)`) is advisable (for `size`, bounds checking, and clarity) but not required to compile.
What's Next
Part IV closes here. You can now read old Fortran (Chapter 17),
modernize it safely (Chapter 18), and translate any
construct on sight (this chapter). Keep the quick-reference table — and its expanded twin,
Appendix E — within reach; you will use them
every time a .f file lands on your desk.
Now the book turns from the code's form to its numbers. Part V
is the numerical computing Fortran was built for, and it opens with the ground truth beneath every calculation
you have written so far: floating-point arithmetic. Chapter 20
explains why 0.1 + 0.2 is not 0.3, what machine epsilon is, and how to choose precision deliberately —
the decision you deferred every time you wrote real(dp) on faith. The PLATE solver you just modernized
will be the first code whose round-off you learn to reason about.