Exercises: FORTRAN 77 to Modern Fortran

This is a translation chapter, so most of these exercises are translations: you are handed a fragment of FORTRAN 77 and asked to produce its faithful modern equivalent. Do the work the way you would on the job — identify which dictionary entry applies, translate every clause, and (where the fragment computes something) confirm the numbers are unchanged. Type and compile the modern versions; the compiler is your regression test for anything that would not build.

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 (19.5, 19.9, 19.11, 19.13, 19.19) are also provided as compilable code in code/exercise-solutions.f90. Predict every output before you compile.

Throughout, "modern" means: free-form, implicit none, lowercase keywords, real(dp) reals with use, intrinsic :: iso_fortran_env, only: dp => real64, intent on every dummy argument, and modules or internal procedures rather than COMMON and external routines. The running FORTRAN 77 program is PLATE (code/plate-legacy.f); several problems draw on it.


Part A — Name the Move ⭐

19.1 † For each FORTRAN 77 construct, name its standard modern-Fortran replacement in one phrase: (a) COMMON /STATE/ X, Y, N; (b) EQUIVALENCE (R, IR) used to inspect the bits of a real; (c) a statement function SQ(X) = X*X; (d) GO TO (10, 20, 30), MODE; (e) a dummy argument declared REAL A(*).

19.2 True or false, with one sentence of justification: "Fixed-form source has been removed from the Fortran standard, so a .f file will no longer compile."

19.3 True or false, with one sentence: "Adding implicit none to a working legacy routine is a pure style change and cannot affect whether the program is correct."

19.4 Give the symbolic modern operator for each: .LT., .GE., .NE., .EQ., .LE.. Which two logical operators from the set .AND. .OR. .NOT. .EQV. .NEQV. have no symbolic form?


Part B — Storage and Data ⭐⭐

19.5 † Modernize it. Rewrite this shared state as a module, and show a two-line driver that sets the grid and prints the cell count. (Code solution provided.)

      SUBROUTINE SETGRD
      COMMON /GRID/ NX, NY
      NX = 41
      NY = 41
      END

19.6 Modernize it. A BLOCK DATA unit initializes a COMMON block. Rewrite both as a single module with an initialized module variable, and explain in one sentence why the BLOCK DATA unit disappears.

      BLOCK DATA PHYS
      COMMON /CONST/ PI, TWOPI
      DATA PI, TWOPI /3.14159265, 6.28318531/
      END

19.7 † Find the bug. Two routines share /GRID/, but one of them reads it wrong. State the symptom, say which word each routine actually reads, and explain why re-expressing /GRID/ as a module makes the bug impossible.

      SUBROUTINE SETGRD
      COMMON /GRID/ NX, NY
      NX = 200
      NY = 100
      END

      SUBROUTINE NCELLS(N)
      COMMON /GRID/ NCELL
      N = NCELL
      END

19.8 For each use of EQUIVALENCE, name the correct modern replacement (transfer, a derived type, or separate allocatable arrays), and say why the other two would be wrong: (a) overlaying a scratch buffer on an unused array to save memory; (b) viewing a REAL as an INTEGER to extract its exponent bits; (c) naming the three components of a coordinate triple so P(1), P(2), P(3) and X, Y, Z refer to the same storage.


Part C — Control Flow ⭐⭐

19.9 † Modernize it. Translate this GOTO relaxation loop to a structured do/exit loop, preserving both clauses of the exit test. Then say what breaks if you translate it to do while (diff > tol) alone. (Code solution provided.)

      ITER = 0
   10 ITER = ITER + 1
      CALL SWEEP(DIFF)
      IF (DIFF .GT. TOL .AND. ITER .LT. MAXIT) GO TO 10

19.10 Modernize it. Rewrite this computed GOTO as a select case, and add the guard the original lacked.

      GO TO (100, 200, 300), MODE
  100 CALL EXPLICIT
      GO TO 900
  200 CALL IMPLICIT
      GO TO 900
  300 CALL CRANK
  900 CONTINUE

19.11 † Modernize it. Translate this arithmetic IF (on a real residual) to a block if. Then explain which branch is fragile and how you would change it if the "zero" case is meant to catch "negligibly small." (Code solution provided.)

      IF (RESID) 10, 20, 30
   10 SGN = -1.0
      GO TO 40
   20 SGN = 0.0
      GO TO 40
   30 SGN = 1.0
   40 CONTINUE

19.12 Find the bug. A modernizer rewrote a GOTO loop as the do while below and the program now hangs on a hard problem. What did the translation drop, and what is the one-clause fix?

iter = 0
do while (diff > tol)
   iter = iter + 1
   call sweep(diff)
end do

Part D — Procedures ⭐⭐

19.13 † Modernize it. Turn this statement function into an internal pure function with declared types and an intent. Why must it be translated to compile under -std=f2018? (Code solution provided.)

      AVG(TL, TR, TB, TA) = 0.25 * (TL + TR + TB + TA)

19.14 Modernize it. Rewrite this assumed-size print routine with an assumed-shape argument, and note exactly which argument disappears and why.

      SUBROUTINE PRTROW(ROW, NVAL)
      REAL ROW(*)
      INTEGER NVAL, K
      WRITE (*, '(100F6.1)') (ROW(K), K = 1, NVAL)
      END

19.15 † Add intent. Give the correct intent for each dummy argument of the legacy routine below, having inferred the data flow from the body, and rewrite its header in modern form (assumed-shape, real(dp)).

      SUBROUTINE SETEDGE(T, NX, NY, THOT, TCOLD)
      REAL T(NX, NY), THOT, TCOLD
      INTEGER NX, NY, I, J
      DO 10 I = 1, NX
         T(I, NY) = THOT
   10 CONTINUE
      DO 20 J = 1, NY
         T(1, J)  = TCOLD
         T(NX, J) = TCOLD
   20 CONTINUE
      END

Part E — Form and Typing ⭐⭐

19.16 † Convert the form. Rewrite this fixed-form fragment in free-form: turn the C comment into !, the column-6 continuation into a trailing &, and drop the label if it is unreferenced.

C     ACCUMULATE THE WEIGHTED SUM
      S = W1*X1 + W2*X2 +
     &    W3*X3 + W4*X4

19.17 Add implicit none. Declare every variable in this implicitly-typed fragment with the type implicit typing would have given it, and add implicit none. Which variables were integer, and which real?

      SUM = 0.0
      DO 10 K = 1, N
         SUM = SUM + A(K)
   10 CONTINUE
      XBAR = SUM / N

19.18 † Find the bug. This fragment compiles under FORTRAN 77 and prints a wrong answer; under implicit none it will not compile at all. Identify the defect and explain how implicit none exposes it.

      TOTAL = 0.0
      DO 10 I = 1, N
         TOTAL = TOATL + X(I)
   10 CONTINUE
      WRITE (*,*) TOTAL

Part F — Modernize PLATE / Design ⭐⭐/⭐⭐⭐

19.19 † Design it (PLATE). Finish modernizing the PLATE edge-setter: write a modern set_edges subroutine (assumed-shape, intent, real(dp)) that sets the top edge hot and the other three cold, using array-section assignments rather than loops. Confirm by hand that a cold corner reads 0.00. (Code solution provided.)

19.20 Design it (PLATE). Bundle PLATE's COMMON /GRID/ (the field T and the size N) into a single field_t derived type (n and an allocatable t(:,:)), and rewrite the relax signature to take one type(field_t) argument. What did the derived type buy you over separate module variables? (See Chapter 9.)

19.21 † Modernize it (PLATE). Rewrite PLATE's reporting section — the two WRITE/FORMAT pairs and the CALL PRTROW(T(1,JC), NX) — in modern Fortran: an inline format on the print/write, an assumed-shape row, and no assumed-size sequence-association trick. Keep the printed values identical.

19.22 Back of the envelope. The legacy PLATE declares T and TNEW as DOUBLE PRECISION (NMAX,NMAX) with NMAX = 21, but runs an N = 4 grid. (a) How many double precision cells are allocated per array, and how many does the 4×4 run use? (b) At 8 bytes per double precision, how many bytes per array are wasted? (c) What does switching to allocatable (sized 4×4) save across the two arrays, and what limit does it also remove?


Part G — Interleaved and Deeper ⭐⭐⭐

19.23 † Interleaved (Chapter 8). Explain why converting COMMON to a module is a prerequisite for safely parallelizing a kernel across OpenMP threads or coarray images — not just a tidiness improvement. What specifically goes wrong if global state stays in COMMON?

19.24 Interleaved (Chapter 4). Rewrite this nested DO with a shared CONTINUE terminator as modern nested loops with one end do each, then collapse the whole thing to a single whole-array statement.

      DO 10 J = 1, NY
         DO 10 I = 1, NX
            T(I,J) = 0.0
   10 CONTINUE

19.25 † Interleaved (Ch. 17–18). A legacy routine contains both a statement function and an assumed-size array argument. Which one must be translated for the routine to compile under -std=f2018, and which is only advisable? Justify each from the construct's standard status.

19.26 Port it. Translate this Python loop (a while with a break) to a Fortran do/exit loop. Note which Fortran keyword plays the role of Python's break.

n = 0
while True:
    n += 1
    r = residual()
    if r < tol or n >= max_iter:
        break

19.27 † Find the bug. A modernization changed every REAL to real(dp) but left one real literal untouched, as shown. What subtle change did this introduce, and what is the fix? (Preview of Chapter 20.)

real(dp) :: alpha
! ...
alpha = 0.1                 ! a default-real literal, assigned to a real(dp) variable

19.28 Back of the envelope. Skim the quick-reference table (§19.5) and the PLATE program. Of the roughly fifteen dictionary rows, estimate how many apply to a typical 200-line FORTRAN 77 numerical kernel, and argue which single translation removes the most risk per line changed. Defend your choice.


Solutions to the daggered and odd-numbered problems are in appendices/answers-to-selected.md; the computational ones (19.5, 19.9, 19.11, 19.13, 19.19) are compilable in code/exercise-solutions.f90. The "find the bug" problems (19.7, 19.12, 19.18, 19.27) are worth extra attention: each is a real mistake made during a real modernization, and learning to see them is most of the skill this chapter teaches.