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 (IN → 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.