Appendix E: FORTRAN 77 to Modern Fortran — Translation Reference
This is the expanded, one-stop version of the translation dictionary developed in
Chapter 19 — the tables you keep
open on a second monitor while you carry old code forward. It maps every common FORTRAN 77 (and fixed-form)
construct to its faithful Modern Fortran equivalent, grouped the way you meet them: source form and typing,
storage and data, control flow, and procedures. Where a table cell cannot show the whole move, a small
labeled snippet stands beside it — the legacy form (as a C-commented fixed-form fragment) on top, the
modern form (!-commented, free-form) below.
Two rules govern every entry, and they are worth reading before the tables:
- Diagnose before you translate. Several constructs —
EQUIVALENCE,GO TO— do different jobs in different places, and the correct replacement depends on which. Read the surrounding code the way Chapter 17 teaches, decide the intent, then pick the matching row. - Translation preserves the numerics. Every move here changes the engineering, not the science. Prove it after each step with the regression test of Chapter 18: bit-for-bit when you did not touch the arithmetic, within a stated tolerance when you deliberately did.
Reading the Status column. Each table notes how the current standard regards the old form: legal (still standard — translating is a style or safety choice), obsolescent (retained but flagged by the standard as discouraged), or deleted (removed from a modern standard — you must translate it to compile under a strict mode; the standard's own term is "deleted feature," which Chapter 19 also calls "removed"). Treat the status as guidance, not gospel: the obsolescent/deleted boundary shifts between revisions, so verify anything load-bearing against ISO/IEC 1539-1 (the exact edition and clause), and do not quote a clause number you have not checked.
E.1 Source Form and Typing
| FORTRAN 77 (fixed-form) | Modern Fortran (free-form) | Status | Note |
|---|---|---|---|
| columns 1–72 layout; label in 1–5; statement from 7 | free-form; statement anywhere | legal | .f/.for → .f90; style, not correctness |
C or * comment in column 1 |
! comment anywhere on the line |
legal | end-of-line comments now allowed |
| any character in column 6 continues the line | trailing & on the previous line |
legal | lines to 132 chars, continued with & |
statement label in columns 1–5 (GO TO/FORMAT) |
labels rarely needed; keep only where referenced | legal | structured constructs replace most labels |
implicit typing (I–N → integer, else real) |
implicit none + explicit declarations |
legal | the one correctness fix here — catches typos |
IMPLICIT DOUBLE PRECISION (A-H,O-Z) |
explicit real(dp) with a kind parameter |
legal | choose precision on purpose, not by first letter |
.LT. .LE. .EQ. .NE. .GE. .GT. |
< <= == /= >= > |
legal | old forms remain valid; conversion optional |
The column reflow and the C-to-! change are mechanical (tools such as findent or fprettify do the
first pass). Two entries are more than cosmetic. implicit none is a correctness change: it turns a
mistyped name into a compile error instead of a silently created garbage variable. And a blanket
IMPLICIT DOUBLE PRECISION should become a deliberate kind, not a first-letter accident.
C FORTRAN 77: implicit typing, double via IMPLICIT, column-6 continuation
IMPLICIT DOUBLE PRECISION (A-H,O-Z)
TOTAL = A + B + C +
& D + E + F
! Modern: implicit none, an explicit real(dp) kind, and a trailing-& continuation
use kinds, only: dp ! dp = selected_real_kind(15, 307)
implicit none
real(dp) :: total, a, b, c, d, e, f
total = a + b + c + &
d + e + f
The logical operators .and., .or., .not., .eqv., and .neqv. have no symbolic form and are
unchanged. Fixed-form itself remains legal Fortran — a compiler reads a .f file forever — so converting
form is optional; converting implicit typing is the change that actually removes bugs.
E.2 Storage and Data
FORTRAN 77 had no dynamic memory, no modules, and no derived types; every storage idiom below is a workaround for one of those three absences, and each modern replacement is the feature that filled the gap.
| FORTRAN 77 | Modern Fortran | Status | Note |
|---|---|---|---|
COMMON /blk/ a, b |
a module with use (or a derived-type argument) |
obsolescent | one authoritative, typed, checked declaration |
BLOCK DATA |
module-variable initializers | obsolescent | the initializer is the initialization |
EQUIVALENCE (reinterpret bits) |
intrinsic transfer(source, mold) |
obsolescent | explicit, typed, no lasting alias |
EQUIVALENCE (save memory) |
separate allocatable arrays |
obsolescent | memory is no longer the constraint |
EQUIVALENCE (fields of one slab) |
a derived type | obsolescent | named, typed components that cannot overlap |
DATA x /1.0/ |
real(dp) :: x = 1.0_dp (initializer) |
legal | beware the implicit SAVE in a procedure |
PARAMETER (P = 3.14) |
real(dp), parameter :: p = 3.14_dp |
legal | add a kind suffix |
over-dimensioned x(NMAX) + real size N |
allocatable + allocate(x(n)) |
legal | size to the true problem; no NMAX ceiling |
Hollerith 13HTEMPERATURE= |
a quoted character literal 'TEMPERATURE=' |
deleted | count the characters; the comma often hid inside |
COMMON → module is the most important move, because it is both the most common and the most dangerous.
A COMMON block is shared memory associated by position: nothing checks that every unit lists the same
members, in the same order, with the same types. A module declares the state once, by name, and the
compiler enforces it everywhere.
C FORTRAN 77: shared state as a named COMMON block, repeated in every unit
PARAMETER (NMAX = 21)
COMMON /GRID/ T(NMAX,NMAX), N
! Modern: shared state as a module -- one declaration the compiler enforces
module grid_data
use kinds, only: dp
implicit none
real(dp), allocatable :: t(:,:) ! also drops the NMAX ceiling
integer :: n = 0
end module grid_data
Go one step further where you can: make the state private and reach it through procedures, or bundle the
grid into a derived-type argument so a routine never touches global state at all. The dictionary entry is
"COMMON → module"; the craft is choosing how far past the literal translation to go.
EQUIVALENCE is the one entry where the right answer depends most on why, not what. Diagnose which
of three jobs it does, then pick the matching row above:
C FORTRAN 77: reinterpret a REAL's bits as an INTEGER
REAL X
INTEGER IX
EQUIVALENCE (X, IX)
! Modern: bit reinterpretation, explicit and portable -- no permanent alias
use, intrinsic :: iso_fortran_env, only: int32, real32
integer(int32) :: bits
bits = transfer(1.0_real32, 1_int32) ! IEEE-754 encoding of 1.0 -> 1065353216 (0x3F800000)
DATA → initializer, but mind the trap. A DATA statement, and a modern declaration-initializer, both
give a local variable in a procedure the SAVE attribute implicitly: it is initialized once, not on
each call, and keeps its value between calls. real(dp) :: total = 0.0_dp inside a subroutine does not
reset total each time. For a per-call reset, use an executable statement (total = 0.0_dp) on its own
line.
C FORTRAN 77: initial values via DATA
DATA THOT, TCOLD / 100.0D0, 0.0D0 /
! Modern: fold the value into the declaration; a parameter where it never changes
real(dp), parameter :: t_hot = 100.0_dp, t_cold = 0.0_dp
E.3 Control Flow
FORTRAN 77 had IF, a counted DO, and — for everything else — GO TO. Modern Fortran has a dedicated
construct for each job the GO TO was doing; translation is mostly recognizing which job a given jump
performs and reaching for the construct built for it.
| FORTRAN 77 | Modern Fortran | Status | Note |
|---|---|---|---|
backward GO TO (forms a loop) |
do ... end do + exit |
legal | name the loop if nested |
forward GO TO (leave a loop early) |
exit / exit name |
legal | one construct per escape |
GO TO end-of-body (start next iteration) |
cycle |
legal | negate the guard as you convert |
GO TO out of a procedure (error) |
return / error stop |
legal | fail loudly, not silently |
GO TO around a block (skip it) |
if (...) then ... end if |
legal | wrap the block |
computed GO TO (l1,l2,l3), k |
select case (k) |
obsolescent | add a case default guard |
arithmetic IF (e) n1, n2, n3 |
if / else if / else on the sign |
obsolescent | exact == 0 on reals is fragile |
assigned GO TO / ASSIGN |
select case or a procedure call |
deleted (F95) | gone from the standard |
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 editing hazard |
PAUSE |
remove, or a read prompt |
deleted (F95) | interactive halts are gone |
The canonical case — a GO TO convergence loop — becomes an unconditional do with an explicit exit:
C FORTRAN 77: relaxation loop built from a label and a backward GO TO
ITER = 0
60 CONTINUE
ITER = ITER + 1
C ... one sweep, compute DMAX ...
IF (DMAX .GT. TOL .AND. ITER .LT. MAXIT) GO TO 60
! Modern: the same loop, structured -- the exit is the NEGATION of the continue test
iter = 0
do
iter = iter + 1
! ... one sweep, compute dmax ...
if (dmax <= tol .or. iter >= max_iter) exit
end do
When you negate a compound GO TO condition, translate every clause — dropping the iter < max_iter
guard turns a non-converging run into an infinite loop. A forward GO TO out of nested loops becomes a
named exit; a GO TO to the loop's end becomes cycle:
C FORTRAN 77: forward GO TO leaves both loops; GO TO 10 skips to next I
DO 20 J = 1, NY
DO 10 I = 1, NX
IF (MASK(I,J) .EQ. 0) GO TO 10
IF (T(I,J) .GT. TMELT) GO TO 30
10 CONTINUE
20 CONTINUE
30 CONTINUE
! Modern: cycle for "next iteration", a named exit for "leave both loops"
search: do j = 1, ny
do i = 1, nx
if (mask(i,j) == 0) cycle ! was: GO TO 10 (loop end)
if (t(i,j) > t_melt) exit search ! was: forward GO TO 30 (out of both)
end do
end do search
The multi-way jumps map to select case and a block if. Both gain something the old form lacked — a
case default for the value the computed GO TO silently mishandled, and readable named cases for the
arithmetic IF:
C FORTRAN 77: computed GO TO, and an arithmetic IF on the sign of RESID
GO TO (100, 200, 300), K
IF (RESID) 10, 20, 30
! Modern: select case (total, with a guard) and if/else if/else (readable)
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'
end select
if (resid < 0.0_dp) then
call handle_negative()
else if (resid == 0.0_dp) then ! exact 0.0 rarely occurs for a real result
call handle_zero()
else
call handle_positive()
end if
The middle branch of an arithmetic IF fires only on an exact zero, which almost never happens for a
floating-point result. When you translate one on a real value, treat it as a chance to fix a latent bug:
replace resid == 0.0_dp with a tolerance test, abs(resid) < eps, if the "zero" case was meant to catch
"small enough."
E.4 Procedures
The three procedure translations all add checkable information the old code left implicit: what a dummy argument's shape is, whether the procedure may modify it, and where a small helper lives.
| FORTRAN 77 | Modern Fortran | Status | Note |
|---|---|---|---|
statement function f(x) = ... |
internal/module pure (or elemental) function |
deleted (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 sharing module state | obsolescent | one procedure per entry |
alternate return CALL S(*10) |
a status argument + if at the call site |
obsolescent | return a code, branch on it |
| external procedure (implicit interface) | a module procedure (explicit interface) | legal | enables checking + inlining |
A statement function is deleted from the current standard, so it will not compile under a strict
modern mode — it must be translated. The faithful replacement is an internal or module function with
declared types and an intent; add pure for a side-effect-free helper, or elemental if the formula
should also apply element-wise to whole arrays:
C FORTRAN 77: two statement functions among the declarations
AVG(TL,TR,TB,TA) = 0.25D0 * (TL + TR + TB + TA)
SQ(X) = X * X
! Modern: pure for the scalar helper; elemental so SQ also maps over arrays
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
elemental function sq(x) result(y)
real(dp), intent(in) :: x
real(dp) :: y
y = x * x ! sq(v) now works for a scalar OR a whole array v(:)
end function sq
Assumed-size → assumed-shape lets the array carry its own extent, so the separately-passed size argument
disappears and size works inside the procedure. Adding intent documents and enforces the data flow
the old code left silent; deciding whether an argument is in, out, or inout forces you to determine
what the routine actually does with it — and when the intent you write contradicts the code, you have
found a real question, not a nuisance:
C FORTRAN 77: assumed-size dummy, size passed by hand, no intent
SUBROUTINE PRTROW(ROW, N)
REAL ROW(*)
INTEGER N
END
! Modern: assumed-shape carries its own size; intent states read-only
subroutine print_row(row)
real(dp), intent(in) :: row(:) ! size(row) is known; N is redundant
write(*, '(*(f6.1))') row
end subroutine print_row
Assumed-shape and checked calls both require an explicit interface, which you get for free by putting
the procedure in a module or after contains — the same move that replaced COMMON in §E.2. That is why
the procedure translations compound: module procedures with assumed-shape arguments and declared intents
have interfaces the compiler can check, inline, and (for the pure/elemental ones) reorder.
E.5 Standard Status: Obsolescent vs Deleted
A quick status board for the features most likely to force a decision. Deleted features must be translated to compile under a strict modern standard; obsolescent ones still compile but are flagged by the standard as discouraged and should be modernized when you touch the code.
| Legacy feature | Status of the old form | Must you translate? |
|---|---|---|
| statement function | deleted (removed, F2018) | Yes — will not compile strict |
COMMON block |
obsolescent | Recommended |
EQUIVALENCE |
obsolescent | Recommended (diagnose the intent first) |
BLOCK DATA |
obsolescent | Recommended |
arithmetic IF (IF (e) l1,l2,l3) |
obsolescent | Recommended |
computed GO TO (GO TO (...), k) |
obsolescent | Recommended |
assigned GO TO / ASSIGN |
deleted (F95) | Yes |
PAUSE |
deleted (F95) | Yes |
Hollerith constant (nHtext) |
deleted | Yes |
alternate return (CALL S(*10)) |
obsolescent | Recommended |
ENTRY (multiple entry points) |
obsolescent | Recommended |
CHARACTER*n name (the *len form) |
obsolescent | Recommended → character(len=n) |
Verify before you rely on this. The categories above match Chapter 19 and are correct to the best of the book's knowledge, but the standard moves features between "obsolescent" and "deleted" from one revision to the next. Confirm the status — and the edition it applies to — against ISO/IEC 1539-1 whenever a compilation decision or a portability promise depends on it, and do not cite a clause number you have not checked. A compiler's own
-std=mode is the most reliable arbiter of what a given standard accepts.
E.6 The Eight-Step Modernization Checklist
When you sit down with an actual .f file, work the tables above through this ordered recipe from
Chapter 18. It runs, roughly,
from the safest and most mechanical changes to the most structural, and each step is a refactoring —
verified against a regression reference before you move on.
0. Capture a golden reference first: build the legacy code once, save its output.
1. Add implicit none -- declare every variable; a typo becomes an error (E.1)
2. Convert fixed-form to free-form -- reflow columns; C -> !, column-6 -> trailing & (E.1)
3. Replace COMMON with modules -- one typed, checked declaration by name (E.2)
4. Add intent to every dummy arg -- document and enforce the data flow (E.4)
5. Assumed-size -> assumed-shape -- let the array carry its own shape (E.4)
6. Replace GO TO with structure -- do/exit/cycle, if/else, select case (E.3)
7. Retire EQUIVALENCE -- transfer, allocatable, or a derived type (E.2)
8. Add error handling -- validate inputs; error stop on a bad precondition
Two practices make the recipe safe. Work incrementally — one conceptual change, then recompile and re-run the regression test — so any break is localized to the single edit that caused it. And judge each step by numerical equivalence: demand bit-for-bit agreement for the arithmetic-preserving steps (1–6), and fall back to a stated tolerance only when you deliberately change the math (promoting precision, reassociating a sum, enabling fast-math). Compare the parsed numbers, never the raw text — a formatting difference is not a regression.
Read the whole reference top to bottom and one pattern emerges: nearly every obsolescent or deleted form is
one that let the compiler not check something — the block two routines might describe differently, the
label a GO TO might miss, the shape a 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 whole dictionary, and of Part IV: modernization trades unchecked cleverness for
checked clarity, and the science is preserved on the way. Keep this appendix beside
Chapter 19; you will reach for
both every time a .f file lands on your desk.