Legacy Features and Modernization

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Fortran 2023 (ISO/IEC 1539-1:2023, J3/23-007r1) and was checked on 2026-10-19. Every example was compiled and run in a container with GNU Fortran 16.2 and LLVM Flang 23 (flang-23); the modern examples were compiled warning-free with gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all and the output shown is real output. The legacy examples are labelled as such, are built with the options named next to them (fixed form, -std=legacy or the default dialect), and show the real diagnostics each compiler gives. This is the only page of the section in which legacy code is shown as code: the other pages show the modern form and point here for the old one.

Fortran is unusual in how little it removes. A program written for FORTRAN 77 in 1980 still compiles with a 2026 compiler, usually with no more than a flag, and so a large part of the numerical software in use today (climate and weather models, structural mechanics, quantum chemistry, libraries such as the reference BLAS and LAPACK) is still partly written in the old dialect. Reading it is a skill, and converting it safely is another. This page covers four things:

  • what the standard has deleted and what it has marked obsolescent (Annex B), with the compiler behaviour for each, and the two further obsolescent features that the Fortran 2028 working draft adds;

  • the nonstandard idioms that were never part of any standard but fill old code (real*8, %val, Cray pointers, Hollerith constants, vendor intrinsics);

  • the compiler options that make old code build, and the traps of fixed form;

  • a step-by-step, tested conversion of a small FORTRAN 77 program into free-form module code, using the heatflow heat-diffusion problem of the rest of this section.

Three kinds of old: deleted, obsolescent and nonstandard

Three different things are called "legacy Fortran", and mixing them up is the most common source of confusion.

Category Meaning Where defined

Deleted

A feature that was in an earlier standard and is no longer in the language. A conforming program cannot use it, and a processor must be able to report its use (S 4.2). Most compilers keep accepting it as an extension.

Annex B.1 (from Fortran 90) and B.2 (from Fortran 2008) of J3/23-007r1; the policy is in 4.4.2

Obsolescent

A feature that is still in the language, was already redundant in Fortran 90 or 95, and has a better replacement. It is printed in a smaller font in the standard, a processor must be able to report it, and a future revision might delete it (4.4.3).

Annex B.3; the policy is in 4.4.3

Nonstandard

An extension of a particular compiler or vendor (real*8, %val, Cray pointers, etime). The standard says nothing about it, except that a processor must be able to report a nonstandard intrinsic (4.2).

The compiler manual

A fourth list is often mistaken for the standard’s: the books on modern Fortran contain lists of "deprecated" features that are the authors' recommendations, not the standard’s. One of the two reference books consulted for this section has such a list in its first appendix, naming include, double precision, do while, go to, the .eq.-style relational operators, implicit interfaces, sequence and storage association, non-default implicit statements, volatile and sync memory; its second appendix reproduces the standard’s actual lists. None of the first group is deleted or obsolescent in Fortran 2023. Only the second list is the standard’s, and the Fortran 2028 working draft adds two entries to it (see The Fortran 2028 additions). This page keeps them apart: sections "Deleted features" and "Obsolescent features" follow Annex B, and "Nonstandard idioms" follows the compiler manuals. The authors' advice is treated on Best Practices and Style.

Building old code: compiler modes

Which of the three categories a compiler accepts depends on its mode. The table records what was measured with the two compilers in the container, on the examples of this page.

Mode GNU Fortran 16.2 LLVM Flang 23

default

-std=gnu. Extensions and deleted features are accepted. Deleted features produce a warning (3 for the program above), obsolescent features produce none (0 for the obsolescent test file below).

Everything on this page is accepted without a message, except a branch into a construct, which warns.

-pedantic

Warns about some obsolescent features (7 of the 12 in the test file: alternate return, character*n, statement function, computed go to, data among executables, character(*) function) and about nonstandard forms. It does not report entry, common, equivalence, the labelled do or forall; those need -std=f2018 or later.

Reports a "portability" message for each deleted feature (3 for the program above); none for the obsolescent ones.

-std=f2018, -std=f2023

Rejects nonstandard extensions and deleted features with an error, and warns about every obsolescent feature the compiler can detect (12 in the test file). The messages read Fortran 2018 obsolescent feature or Obsolescent feature.

-std= accepts only f2018, which prints the same portability messages as -pedantic and does not stop the build. None of the 12 obsolescent features in the test file was reported.

-std=legacy

No restriction; extensions are accepted, and an argument-type mismatch within a file is a warning instead of an error.

Not an option; the default is already permissive.

Options that matter for old code, all of which were accepted by the compilers in the container:

Purpose GNU Fortran LLVM Flang

Fixed or free form regardless of the extension

-ffixed-form, -ffree-form

-ffixed-form, -ffree-form

Lines longer than 72 columns in fixed form

-ffixed-line-length-none (or -ffixed-line-length-N)

-ffixed-line-length=none (or =N)

Accept old argument-type mismatches across calls in one file

-fallow-argument-mismatch (a warning instead of an error)

not needed: an error is never raised for it; with both units in one file there is a warning (-Wknown-bad-implicit-interface)

Make all local variables static, as in some old programs

-fno-automatic

-fno-automatic

Promote default real and integer to 8 bytes

-fdefault-real-8, -fdefault-integer-8

-fdefault-real-8, -fdefault-integer-8

C-style trailing underscore in external names

on by default; -fno-underscoring to drop it

-funderscoring is accepted

Cray pointers, %val as a nonstandard extension

-fcray-pointer (the pointer syntax), the default dialect for %val

both built without an option

DEC extensions of VAX-era compilers

-fdec

not available

Require declaration of every name

-fimplicit-none

-fimplicit-none

Warn that a statement was cut at column 72

-Wline-truncation (also in -Wall)

no equivalent was found

Warn about nonstandard intrinsics

-Wintrinsics-std

none found

-fdefault-real-8 and its relatives deserve caution: they change the meaning of every real in the compilation and of the intrinsics that return it, but not of code that calls a library compiled without the same flag, and they do not affect explicit kinds such as real(real32). Prefer to introduce a kind parameter (see Step 2: implicit none and declarations).

A FORTRAN 77 program

The running example of this page is a FORTRAN 77 program of the kind found in research groups: an explicit scheme for the 2D heat equation on a 21 by 21 grid, which is a small version of the heatflow kernel. It is deliberately full of the idioms of the period: fixed form, a implicit double precision statement, common blocks for parameters, a block data unit to initialize them, a statement function, specific intrinsic names (dmax1, dabs), labelled do loops, an arithmetic if, and a nonblock do that shares a label. It is legacy code, shown only so that you can recognize it and convert it.

C     LEGACY: 2D heat diffusion (explicit scheme) in FORTRAN 77 style.
      PROGRAM HEAT
      IMPLICIT DOUBLE PRECISION (A-H,O-Z)
      PARAMETER (NMAX = 21)
      DIMENSION U(NMAX,NMAX), UNEW(NMAX,NMAX)
      COMMON /PARAMS/ ALPHA, DT, DX
      COMMON /SIZES/ N
      N = NMAX
      DX = 1.0D0/(N-1)
      DT = 0.2D0*DX*DX/ALPHA
      CALL INIT(U, NMAX)
      K = 0
   10 CONTINUE
      K = K + 1
      CALL STEP(U, UNEW, NMAX, RES)
      IF (K - 200) 10, 20, 20
   20 CONTINUE
      WRITE (*,100) K, RES, U(11,11)
  100 FORMAT (' steps=', I4, ' residual=', 1PE10.3, ' center=', 0PF8.3)
      END

      BLOCK DATA PINIT
      DOUBLE PRECISION ALPHA, DT, DX
      COMMON /PARAMS/ ALPHA, DT, DX
      DATA ALPHA /1.0D0/
      END

      SUBROUTINE INIT(U, LD)
      IMPLICIT DOUBLE PRECISION (A-H,O-Z)
      DIMENSION U(LD,*)
      COMMON /SIZES/ N
      DO 20 J = 1, N
         DO 10 I = 1, N
            U(I,J) = 0.0D0
   10    CONTINUE
   20 CONTINUE
      U(11,11) = 100.0D0
      RETURN
      END

      SUBROUTINE STEP(U, UNEW, LD, RES)
      IMPLICIT DOUBLE PRECISION (A-H,O-Z)
      DIMENSION U(LD,*), UNEW(LD,*)
      COMMON /PARAMS/ ALPHA, DT, DX
      COMMON /SIZES/ N
      LAP(I,J) = (U(I-1,J) + U(I+1,J) + U(I,J-1) + U(I,J+1)
     &            - 4.0D0*U(I,J))/(DX*DX)
      RES = 0.0D0
      DO 20 J = 2, N-1
         DO 10 I = 2, N-1
            UNEW(I,J) = U(I,J) + ALPHA*DT*LAP(I,J)
            RES = DMAX1(RES, DABS(UNEW(I,J) - U(I,J)))
   10    CONTINUE
   20 CONTINUE
      DO 30 J = 2, N-1
      DO 30 I = 2, N-1
   30 U(I,J) = UNEW(I,J)
      RETURN
      END

Built as fixed form with a permissive dialect it runs on both compilers:

$ gfortran -std=legacy -ffixed-form 01_heat_legacy.f && ./a.out
 steps= 200 residual= 1.500E-03 center=   0.163
$ flang-23 -ffixed-form 01_heat_legacy.f && ./a.out
 steps= 200 residual= 1.500E-03 center=   0.163

Under Fortran 2023 rules it does not build, and the compiler says why. With gfortran -std=f2023 -Wall -Wextra -pedantic -c (the 12 common warnings are left out):

01_heat_legacy.f:16:72 Error: Fortran 2018 deleted feature: Arithmetic IF statement at (1)
01_heat_legacy.f:22:16 Warning: Fortran 2018 obsolescent feature: BLOCK DATA construct at (1)
01_heat_legacy.f:32:72 Warning: Fortran 2018 obsolescent feature: Labeled DO statement at (1)
01_heat_legacy.f:33:72 Warning: Fortran 2018 obsolescent feature: Labeled DO statement at (1)
01_heat_legacy.f:47:72 Warning: Obsolescent feature: Statement function at (1)
01_heat_legacy.f:49:72 Warning: Fortran 2018 obsolescent feature: Labeled DO statement at (1)
01_heat_legacy.f:50:72 Warning: Fortran 2018 obsolescent feature: Labeled DO statement at (1)
01_heat_legacy.f:55:72 Warning: Fortran 2018 obsolescent feature: Labeled DO statement at (1)
01_heat_legacy.f:56:72 Error: Fortran 2018 deleted feature: Shared DO termination label 30 at (1)
01_heat_legacy.f:57:72 Error: Fortran 2018 deleted feature: DO termination statement which is not END DO or CONTINUE with label 30 at (1)
01_heat_legacy.f:46:17 Warning: Possible change of value in conversion from REAL(8) to INTEGER(4) at (1) [-Wconversion]

Three of these are errors (the arithmetic if and the nonblock do, both deleted in Fortran 2018), the rest are warnings about obsolescent features. The last line is not about a deleted or obsolescent feature at all, and it is worth reading carefully: it is the first sign of a latent bug that the conversion below will uncover.

Fixed source form

Fixed form (6.3.3) was designed for punched cards, and it is obsolescent (B.3.7). The standard’s rules, all of which old files obey:

  • a source line has exactly 72 character positions (6.3.3.1); the standard defines nothing beyond column 72 (historically the card sequence number), and compilers differ in what they do with text there;

  • a label occupies positions 1 to 5; a character other than blank or zero in position 6 marks a continuation line; the statement itself starts in position 7 and ends in position 72 (6.3.3.3, 6.3.3.5);

  • C or * in position 1, or ! in any position other than 6, starts a comment (6.3.3.2);

  • blanks are not significant outside character literals (6.3.3.1), so DO 10 I = 1, 4 and DO10I=1,4 are the same, and so is the notorious DO 10 I = 1.4, which is an assignment to a variable called DO10I;

  • a program unit’s END statement may not be continued.

Two traps are not diagnosed by default, and both can silently change a result. The first is the 72nd-column cut. This program computes a sum with a term that starts after column 72:

      PROGRAM TRAPS
      REAL TOTAL
      TOTAL = 1.0
      TOTAL = TOTAL + 2.0 + 3.0 + 4.0 + 5.0 + 6.0 + 7.0 + 8.0 + 9.0      + 100.0
      TOTL = TOTAL + 100.0
      PRINT *, TOTAL, TOTL
      END

The statement TOTAL = TOTAL + 2.0 + ... + 9.0 is complete at column 61, then there are blanks, and + 100.0 begins at column 73. No compiler reports an error, because what is left is a valid statement. The default build prints the wrong total, and so does Flang:

$ gfortran 04_fixed_traps.f && ./a.out
   45.0000000       145.000000
$ flang-23 04_fixed_traps.f && ./a.out
 45. 145.

The correct totals are 145 and 245. GNU Fortran reports the cut only when asked, with -Wline-truncation (included in -Wall):

04_fixed_traps.f:4:72 Warning: Line truncated at (1) [-Wline-truncation]

Lifting the limit, gfortran -ffixed-line-length-none or flang-23 -ffixed-line-length=none, gives the intended answer, which proves that the cut was the problem:

   145.000000       245.000000
 145. 245.

The second trap is the typo that implicit typing turns into a new variable. In the same file TOTL = TOTAL + 100.0 creates a new, single precision variable named TOTL instead of failing. With -fimplicit-none (or an implicit none statement) the compiler stops:

$ gfortran -std=f2023 -fimplicit-none -c 04_fixed_traps.f
04_fixed_traps.f:5:7 Error: Symbol 'totl' at (1) has no IMPLICIT type; did you mean 'total'?

Free form (whose limit is 10,000 characters per line) removes the first trap and implicit none removes the second, which is why the first two steps of the conversion below are the ones that matter most. Fixed form in a modern code base is mostly encountered in two situations: a vendor library or API that still ships .f sources or fixed-form headers (see Legacy idioms in vendor APIs), and the build setup for a legacy package (see Building and tooling for legacy packages).

Deleted features

Annex B.1 lists six features of Fortran 90 that the later standards dropped, and B.2 two that were still in Fortran 2008. This section shows each, the diagnostic each compiler gives, and the replacement. In every case GNU Fortran accepts the feature in its default mode (with a warning) and rejects it under -std=f2023; Flang accepts it silently, and with -std=f2018 prints a "portability" message that does not stop the build.

Deleted feature Old code Gone since Replacement

Real and double precision do variables (B.1 (1))

do x = 0.0, 1.0, 0.25

dropped with Fortran 95 (4.4.2)

an integer count and a computed value, x = 0.25*real(i)

Branch to an end if from outside its block (B.1 (2))

if (i > 0) goto 10 …​ 10 end if

dropped with Fortran 95

branch to a continue that follows the end if, or better, restructure with exit, cycle or block

pause (B.1 (3))

pause 'press return'

dropped with Fortran 95

write a prompt, then read a line

assign and assigned go to, assigned formats (B.1 (4))

assign 10 to lab, go to lab

dropped with Fortran 95

select case, a procedure pointer, or a character variable holding a format

H edit descriptor (B.1 (5))

format (5HHello)

dropped with Fortran 95

a character literal, format ('Hello')

Vertical format control (B.1 (6))

first character of a printed record taken as a carriage-control code

dropped with Fortran 95

post-process the file; the first character is ordinary data

Arithmetic if (B.2 (1))

if (i - 5) 10, 20, 30

deleted in Fortran 2018 (C.1)

if / else if / else

Nonblock do (B.2 (2))

do 10 i = 1, 3 …​ 10 s = s + i

deleted in Fortran 2018 (C.1)

do …​ end do, one per loop

The remainder of the section shows the real diagnostics. Each snippet is a complete program in its own file and was built three ways. The first program has a real do variable:

program d1
  real :: x
  do x = 0.0, 1.0, 0.25
    print *, x
  end do
end program d1
$ gfortran d1_real_do.f90          (default dialect: warnings, then a running program)
d1_real_do.f90:3:6 Warning: Deleted feature: Loop variable at (1) must be integer
d1_real_do.f90:3:8 Warning: Deleted feature: Start expression in DO loop at (1) must be integer
d1_real_do.f90:3:13 Warning: Deleted feature: End expression in DO loop at (1) must be integer
d1_real_do.f90:3:18 Warning: Deleted feature: Step expression in DO loop at (1) must be integer
$ gfortran -std=f2023 d1_real_do.f90
d1_real_do.f90:3:6 Error: Deleted feature: Loop variable at (1) must be integer
$ flang-23 -std=f2018 d1_real_do.f90
d1_real_do.f90:3:6: portability: DO controls should be INTEGER [-Wreal-do-controls]
d1_real_do.f90:3:10: portability: DO controls should be INTEGER [-Wreal-do-controls]
d1_real_do.f90:3:15: portability: DO controls should be INTEGER [-Wreal-do-controls]
d1_real_do.f90:3:20: portability: DO controls should be INTEGER [-Wreal-do-controls]

A real loop variable invites rounding questions: 0.25 is exact in binary, so this loop runs five times, but with a step such as 0.1 the iteration count is computed from real arithmetic and may depend on the precision and the compiler. An integer count and a computed value (x = 0.25*real(i)) removes the question (see Control Flow). The branch to an end if is the second:

program d2
  integer :: i
  i = 1
  if (i > 0) goto 10
  if (i > 5) then
    print *, 'inside'
10 end if
  print *, 'done'
end program d2
$ gfortran d2_end_if.f90
d2_end_if.f90:7:2 Warning: Legacy Extension: Label at (1) is not in the same block as the GOTO statement at (2)
$ gfortran -std=f2023 d2_end_if.f90
d2_end_if.f90:7:2 Error: Legacy Extension: Label at (1) is not in the same block as the GOTO statement at (2)
$ flang-23 -std=f2018 d2_end_if.f90
d2_end_if.f90:4:3: warning: Label '10' is in a construct that should not be used as a branch target here [-Wbranch-into-construct]

The arithmetic if and the nonblock do, the two features deleted in Fortran 2018 and therefore the ones most likely to be met in real code, in two further files:

program d6
  integer :: i
  i = 3
  if (i - 5) 10, 20, 30
10 print *, 'negative'
  stop
20 print *, 'zero'
  stop
30 print *, 'positive'
end program d6
$ gfortran d6_arith_if.f90
d6_arith_if.f90:4:23 Warning: Fortran 2018 deleted feature: Arithmetic IF statement at (1)
$ gfortran -std=f2023 d6_arith_if.f90
d6_arith_if.f90:4:23 Error: Fortran 2018 deleted feature: Arithmetic IF statement at (1)
$ flang-23 -std=f2018 d6_arith_if.f90
d6_arith_if.f90:4:3: portability: deprecated usage
program d7
  integer :: i, j, s
  s = 0
  do 10 i = 1, 3
  do 10 j = 1, 3
10 s = s + i*j
  print *, s
end program d7
$ gfortran d7_nonblock.f90
d7_nonblock.f90:5:16 Warning: Fortran 2018 deleted feature: Shared DO termination label 10 at (1)
d7_nonblock.f90:6:14 Warning: Fortran 2018 deleted feature: DO termination statement which is not END DO or CONTINUE with label 10 at (1)
$ gfortran -std=f2023 d7_nonblock.f90
d7_nonblock.f90:4:16 Warning: Fortran 2018 obsolescent feature: Labeled DO statement at (1)
d7_nonblock.f90:5:16 Error: Fortran 2018 deleted feature: Shared DO termination label 10 at (1)
d7_nonblock.f90:6:14 Error: Fortran 2018 deleted feature: DO termination statement which is not END DO or CONTINUE with label 10 at (1)
$ flang-23 -std=f2018 d7_nonblock.f90
d7_nonblock.f90:4:3: portability: A DO loop should terminate with an END DO or CONTINUE [-Wold-label-do-end-statements]
d7_nonblock.f90:5:3: portability: A DO loop should terminate with an END DO or CONTINUE [-Wold-label-do-end-statements]

The nonblock loop sums i*j for i and j from 1 to 3, which is 36, and the rewrite with two end do statements gives the same value (it is in the replacement program below). The remaining three, pause, assign and the H descriptor, behave the same way, and are shown with the single diagnostic that each earns:

d3_pause.f90 (pause 'press return')
  gfortran             d3_pause.f90:3:22 Warning: Deleted feature: PAUSE statement at (1)
  gfortran -std=f2023  d3_pause.f90:3:22 Error: Deleted feature: PAUSE statement at (1)
  flang-23 -std=f2018  d3_pause.f90:3:3: portability: deprecated usage

d4_assign.f90 (assign 10 to lab, then go to lab)
  gfortran             d4_assign.f90:3:18 Warning: Deleted feature: ASSIGN statement at (1)
d4_assign.f90:4:10 Warning: Deleted feature: Assigned GOTO statement at (1)
  gfortran -std=f2023  d4_assign.f90:3:18 Error: Deleted feature: ASSIGN statement at (1)
d4_assign.f90:4:10 Error: Deleted feature: Assigned GOTO statement at (1)
  flang-23 -std=f2018  d4_assign.f90:3:3: portability: deprecated usage
d4_assign.f90:4:3: portability: deprecated usage

d5_hedit.f90 (100 format (5HHello))
  gfortran             d5_hedit.f90:3:17 Warning: The H format specifier at (1) is a Fortran 95 deleted feature
  gfortran -std=f2023  d5_hedit.f90:3:14 Error: The H format specifier at (1) is a Fortran 95 deleted feature
d5_hedit.f90:2:11 Error: FORMAT label 100 at (1) not defined
  flang-23 -std=f2018  d5_hedit.f90:3:13: portability: deprecated usage
d5_hedit.f90:3:13: warning: Legacy 'H' edit descriptor

Note that pause in the default GNU dialect prints a message and waits for input on standard input (To resume execution, type go), so a batch job that still contains one hangs or stops there. In d5_hedit.f90 the second -std=f2023 error is a consequence of the first: the format statement was rejected, so the label 100 is not defined.

The -std=f2023 option is the cheapest audit available for the deleted features: compile once with it, read every error, and you have the list. The obsolescent features need a different option, because they are only warnings; they follow in the next section.

Obsolescent features

Annex B.3 of Fortran 2023 lists twelve obsolescent features. The test file below uses every one except fixed form (which is a property of a file, not of a statement; it was shown above). It is deliberately unreadable code: it exists to collect the compiler’s diagnostics.

! Old code you may meet: every obsolescent feature of Fortran 2023 except fixed form.
subroutine pick(n, *, *)
  implicit none
  integer, intent(in) :: n
  if (n == 1) return 1
  if (n == 2) return 2
end subroutine pick

function pad(n)
  implicit none
  integer, intent(in) :: n
  character(*) pad
  pad = repeat('*', n)
end function pad

subroutine two_entries(x)
  implicit none
  real :: x, total
  save total
  data total /0.0/
  total = total + x
  return
  entry reset_total()
  total = 0.0
end subroutine two_entries

program obsolescent
  implicit none
  integer :: i, k, sel
  real :: v(3), w(3), f, g, z
  character(8), external :: pad
  character*4 label
  common /box/ k
  equivalence (v, w)
  f(g) = g*g
  k = 2
  label = 'ok'
  sel = k
  go to (10, 20) sel
10 print *, 'one'
20 print *, 'two'
  do 30 i = 1, 3
    v(i) = real(i)
30 continue
  data z /5.0/
  print *, f(3.0), sqrt(w(2)), dsqrt(4.0d0), float(k), z, label
  forall (i = 1:3) v(i) = v(i) + 1.0
  call pick(1, *40, *50)
40 print *, 'return 1'
50 print *, pad(3)
end program obsolescent

Built with gfortran -std=f2023 -Wall -pedantic -c, the compiler lists the features it recognizes:

02_obsolescent.f90:2:24 Warning: Obsolescent feature: Alternate-return argument at (1)
02_obsolescent.f90:23:20 Warning: Fortran 2008 obsolescent feature: ENTRY statement at (1)
02_obsolescent.f90:32:14 Warning: Obsolescent feature: Old-style character length at (1)
02_obsolescent.f90:34:20 Warning: Fortran 2018 obsolescent feature: EQUIVALENCE statement at (1)
02_obsolescent.f90:35:12 Warning: Obsolescent feature: Statement function at (1)
02_obsolescent.f90:39:20 Warning: Obsolescent feature: Computed GOTO at (1)
02_obsolescent.f90:42:16 Warning: Fortran 2018 obsolescent feature: Labeled DO statement at (1)
02_obsolescent.f90:45:14 Warning: Obsolescent feature: DATA statement at (1) after the first executable statement
02_obsolescent.f90:48:24 Warning: Obsolescent feature: Alternate-return argument at (1)
02_obsolescent.f90:9:0 Warning: Obsolescent feature: CHARACTER(*) function 'pad' at (1)
02_obsolescent.f90:33:14 Warning: Fortran 2018 obsolescent feature: COMMON block at (1)
02_obsolescent.f90:47:36 Warning: Fortran 2018 obsolescent feature: FORALL construct at (1)

Three notes on that list. First, common, equivalence, forall, the labelled do and (in the first program of this page) block data are labelled Fortran 2018 obsolescent feature: Annex C.1 confirms that Fortran 2018 made them obsolescent, together with the specific names of intrinsic functions. The other items are labelled only Obsolescent feature or, for entry, Fortran 2008 obsolescent feature, which is the compiler’s own labelling: Annex B of the 2023 standard does not say in which edition entry became obsolescent, so this page does not either. Second, the data statement in the executable part earns a warning only because it follows the first executable statement. Third, the specific names of intrinsic functions (dsqrt, float) are in Annex B.3.12 but earned no warning from either compiler, although the program contains them on purpose.

With -std=legacy the program builds, and it runs identically on the two compilers, apart from the list-directed formatting of reals, which is processor dependent:

$ gfortran -std=legacy -w 02_obsolescent.f90 && ./a.out
 two
   9.00000000       1.41421354       2.0000000000000000        2.00000000       5.00000000     ok
 return 1
 ***
$ flang-23 02_obsolescent.f90 && ./a.out
 two
 9. 1.4142135 2. 2. 5. ok
 return 1
 ***

Each obsolescent feature and what replaces it (the replacements are in the next program):

Obsolescent feature What it looks like Annex B Replacement

Alternate return

subroutine pick(n, *, *), return 1, call pick(1, *40, *50)

B.3.2

a status argument and select case in the caller; the standard’s own example

Computed go to

go to (10, 20) sel

B.3.3

select case

Statement function

f(g) = g*g

B.3.4

an internal function, often pure or elemental

data among executable statements

data z /5.0/ after executable code

B.3.5

initialization in the declaration, or an assignment

Assumed-length character function

character(*) function pad(n)

B.3.6

an automatic-length or deferred-length allocatable result

Fixed form

columns 1 to 6 and 72

B.3.7

free form; conversion is mechanical (see Building and tooling for legacy packages)

character*4

character*4 label

B.3.8

character(len=4)

entry

entry reset_total() in a subroutine

B.3.9

one module procedure per entry point and private module data

Labelled do

do 30 i = 1, 3 …​ 30 continue

B.3.10

do …​ end do, with cycle for the "skip" branch

common, equivalence, block data

common /box/ k

B.3.11

module variables, transfer, pointer remapping, initialization in declarations

Specific names of intrinsics

dsqrt, float, dmax1, alog

B.3.12

generic names with a kind: sqrt, real(k, dp), max, log

forall

forall (i = 1:3) v(i) = v(i) + 1.0

B.3.13

array assignment, where, or do concurrent

The replacements, in a single program that compiles warning-free under the strict options:

! The modern replacement for each deleted or obsolescent feature met above.
module legacy_replacements
  use, intrinsic :: iso_fortran_env, only: int32, real64
  implicit none (type, external)
  private
  public :: pick, stars, add_total, reset_total

  integer, parameter :: ok = 0, first = 1, second = 2
  real(real64) :: total = 0.0_real64     ! replaces SAVE + DATA + ENTRY

contains

  ! Alternate return -> status argument examined with select case by the caller.
  subroutine pick(n, status)
    integer, intent(in) :: n
    integer, intent(out) :: status
    select case (n)
    case (1)
      status = first
    case (2)
      status = second
    case default
      status = ok
    end select
  end subroutine pick

  ! character(*) function -> allocatable deferred-length result.
  function stars(n) result(s)
    integer, intent(in) :: n
    character(len=:), allocatable :: s
    s = repeat('*', n)
  end function stars

  ! ENTRY -> one module procedure per entry point, shared data private to the module.
  subroutine add_total(x)
    real(real64), intent(in) :: x
    total = total + x
  end subroutine add_total

  subroutine reset_total(value)
    real(real64), intent(out) :: value
    value = total
    total = 0.0_real64
  end subroutine reset_total
end module legacy_replacements

program rewrites
  use, intrinsic :: iso_fortran_env, only: int64, real64
  use legacy_replacements, only: pick, stars, add_total, reset_total
  implicit none (type, external)
  integer :: i, j, k, status, s
  integer, parameter :: sel = 2
  real(real64) :: v(4), w(4), t, x
  character(len=4) :: label

  ! alternate return
  call pick(1, status)
  select case (status)
  case (1);  print '(a)', 'return 1'
  case (2);  print '(a)', 'return 2'
  case default;  print '(a)', 'normal return'
  end select

  ! character(*) function, character*4, computed GO TO
  label = 'ok'
  select case (sel)
  case (1);  print '(a)', 'one'
  case (2);  print '(a)', 'two'
  end select
  print '(a,a,a)', stars(3), ' ', label

  ! ENTRY, SAVE and DATA
  call add_total(1.5_real64)
  call add_total(2.5_real64)
  call reset_total(t)
  print '(a,f4.1)', 'total=', t

  ! statement function -> internal function (see also elemental)
  print '(f4.1)', sq(3.0_real64)

  ! EQUIVALENCE -> transfer for a bit view, no storage overlay
  x = 1.0_real64
  print '(z16.16)', transfer(x, 0_int64)

  ! specific names -> generic names
  print '(f6.3,1x,f6.3)', sqrt(2.0_real64), real(7, real64)/2

  ! FORALL -> array assignment, which also evaluates the right side first
  v = [1.0_real64, 2.0_real64, 3.0_real64, 4.0_real64]
  v(2:4) = v(1:3)
  print '(4f4.1)', v
  w = [(real(i, real64), i = 1, 4)]
  do concurrent (i = 1:4)
    w(i) = w(i)*w(i)
  end do
  print '(4f5.1)', w

  ! real DO variable -> integer count and a computed value
  do i = 0, 4
    x = 0.25_real64*real(i, real64)
    write (*, '(f5.2)', advance='no') x
  end do
  print *

  ! labelled DO and nonblock DO -> nested do / end do
  s = 0
  do i = 1, 3
    do j = 1, 3
      s = s + i*j
    end do
  end do
  print '(a,i0)', 'sum=', s

  ! arithmetic IF -> if / else if / else
  k = 3
  if (k - 5 < 0) then
    print '(a)', 'negative'
  else if (k - 5 == 0) then
    print '(a)', 'zero'
  else
    print '(a)', 'positive'
  end if

  ! H edit descriptor: a character literal (PAUSE: write a prompt and read a line)
  print '(a)', 'Hello'

contains

  pure function sq(g)
    real(real64), intent(in) :: g
    real(real64) :: sq
    sq = g*g
  end function sq
end program rewrites
return 1
two
*** ok
total= 4.0
 9.0
3FF0000000000000
 1.414  3.500
 1.0 1.0 2.0 3.0
  1.0  4.0  9.0 16.0
 0.00 0.25 0.50 0.75 1.00
sum=36
negative
Hello

Flang 23 produces the same output (checked). Three of the rewrites are not mechanical renamings and need care:

  • forall to do concurrent is not a rename. A forall evaluates the whole right-hand side before assigning, so v(2:4) = v(1:3) (the shift) is what forall (i = 2:4) v(i) = v(i - 1) meant. A do concurrent body with the same assignment is not valid because its iterations may not depend on each other. The program above uses the array assignment for the shift, and do concurrent only for the independent update. See do concurrent and GPU Offload.

  • entry data becomes module data. The shared total is a module variable, which has the save attribute, and therefore global state; give it private access (done above) and pass state explicitly where concurrency matters.

  • The real do variable needs an integer count: the loop above multiplies, it does not accumulate, so rounding errors do not build up.

The standard also asks, in 4.2, that the processor report deleted and obsolescent features, "insofar as such use can be detected by reference to the numbered syntax rules and constraints". In practice the useful check is a build with -std=f2023 -Wall: GNU Fortran reported every feature in the test file that it could, and Flang 23 reported none of the obsolescent ones, so a clean Flang build says nothing about obsolescence.

Shared storage and implicit interfaces: the hazards that stay hidden

Deleted features are noisy: the compiler says so. The costly legacy hazards are the quiet ones, which compile cleanly and give a wrong answer. They are the reason modernization is more than cosmetic.

common, block data and equivalence

A common block (8.10.2) shares storage by position, not by name or type. Two program units that declare the same block with different layouts compile, link and run, and each sees the same bytes through its own declaration:

      PROGRAM LAYOUT
      COMMON /BLK/ I, J
      I = 1
      J = 2
      CALL SHOW
      END
      SUBROUTINE SHOW
      DOUBLE PRECISION D
      COMMON /BLK/ D
      PRINT *, D
      END

The main program stores the integers 1 and 2; SHOW reads the same eight bytes as one double precision number. Nothing is diagnosed, in either compiler:

$ gfortran -Wall 05_common_layout.f && ./a.out
   4.2439915824246103E-314
$ flang-23 05_common_layout.f && ./a.out
 4.2439915824E-314

The two printed values are the same subnormal number; they differ in length only because the compilers print list-directed reals with different numbers of digits. Not diagnosing this is correct: the standard permits objects of default integer and double precision real type to be associated through a common block (8.10.2.4), so the program is conforming, and its output is whatever the bit pattern of the two integers means as a double. What the standard does require is that a named common block has the same size in every scoping unit (8.10.2.5), and a compiler can check that only when it sees all the units. A module variable has the name and the type in one place, and a mismatch is a compile-time error. The block data unit exists only to give initial values to common blocks (B.3.11); a module variable takes an initializer instead, as heat_params does in Step 3: modules and explicit interfaces. For why module variables are not a free lunch either, see Modules and Submodules.

equivalence (8.10.1) overlays two objects in the same storage. Old code used it for two jobs, both of which have a replacement: to view an array as a differently shaped one, and to see the bits of a value as another type.

! Legacy: EQUIVALENCE as an overlay (two views of the same storage).
program overlay_legacy
  implicit none
  real :: grid(2, 3), flat(6)
  integer :: bits, i
  real :: x
  equivalence (grid, flat)
  equivalence (x, bits)
  do i = 1, 6
    flat(i) = real(i)
  end do
  x = 1.0
  print '(2f4.1)', grid(:, 1), grid(:, 3)
  print '(z8.8)', bits
end program overlay_legacy
06_equivalence.f90:7:26 Warning: Fortran 2018 obsolescent feature: EQUIVALENCE statement at (1)
06_equivalence.f90:8:23 Warning: Fortran 2018 obsolescent feature: EQUIVALENCE statement at (1)
$ ./a.out
 1.0 2.0
 5.0 6.0
3F800000

The modern version uses a pointer with rank remapping for the first job and transfer for the second. The pointer form requires target, so that aliasing is declared, and not discovered by the optimizer:

! Modern: a pointer with rank remapping, and transfer, instead of EQUIVALENCE.
program overlay_modern
  use, intrinsic :: iso_fortran_env, only: int32
  implicit none (type, external)
  real, target :: flat(6)
  real, pointer :: grid(:, :)
  integer :: i
  real :: x

  flat = [(real(i), i = 1, 6)]
  grid(1:2, 1:3) => flat           ! a 2 x 3 view of the same storage
  x = 1.0
  print '(2f4.1)', grid(:, 1), grid(:, 3)
  print '(z8.8)', transfer(x, 0_int32)
end program overlay_modern
 1.0 2.0
 5.0 6.0
3F800000

The output is identical, as it is with Flang. For the semantics of the pointer view see Pointers and Targets, and for transfer Numeric Model, Bit Manipulation and Conversion.

Implicit interfaces

An external procedure called without an interface has an implicit interface: the compiler knows only the name and trusts the arguments. When caller and callee are in different files, a mismatch is invisible to every compiler. Here a real is passed to a routine that expects double precision:

program caller
  implicit none
  external half
  real :: r
  r = 3.0
  call half(r)
  print *, r
end program caller
subroutine half(x)
  double precision, intent(inout) :: x
  x = x/2
end subroutine half
$ gfortran -std=f2023 -Wall -Wextra -pedantic caller.f90 callee.f90 && ./a.out
   3.00000000
$ flang-23 caller.f90 callee.f90 && ./a.out
 3.

No diagnostic, and a wrong result: half wrote eight bytes into a four-byte variable and the printed value is 3.0, not 1.5. The same mismatch is found as soon as the compiler sees both units, which is the point of putting the callee in a module. Concatenating the two files into one:

$ gfortran -std=f2023 -Wall both.f90
/tmp/both.f90:6:14 Error: Type mismatch in argument 'x' at (1); passed REAL(4) to REAL(8)
$ gfortran -fallow-argument-mismatch both.f90      (the escape hatch for old code: a warning)
/tmp/both.f90:6:14 Warning: Type mismatch in argument 'x' at (1); passed REAL(4) to REAL(8)

Flang 23 treats the one-file case more gently: it builds the program and prints only a warning ([-Wknown-bad-implicit-interface], "If the procedure’s interface were explicit, this reference would be in error"), and the program prints the same wrong 3.. With the callee in a module the error is reported from separately compiled files, by both compilers, which is the whole benefit of explicit interfaces:

mod_caller.f90:6:14 Error: Type mismatch in argument 'x' at (1); passed REAL(4) to REAL(8)
error: Semantic errors in mod_caller.f90
mod_caller.f90:6:13: error: Actual argument type 'REAL(4)' is not compatible with dummy argument type 'REAL(8)'

implicit none (type, external) (Fortran 2018) goes one step further: it also requires every external procedure to be declared, so a call to a routine without an interface is an error in the caller. See Procedures, Functions and Subroutines and Interfaces, Generics and Operator Overloading.

Variables that remember: the accidental save

Many Fortran 77 compilers allocated local variables statically, so that a local kept its value between calls, and programs came to depend on it, although the standard does not promise that. The standard does promise one case: a variable with an explicit initialization has the save attribute (8.4), whether or not the word appears. Old programs that gave a local an initial value "for safety" therefore keep state between calls:

! A declaration with an initializer gives the variable the SAVE attribute (8.5.16).
program accidental_save
  implicit none (type, external)
  integer :: k
  do k = 1, 3
    print '(a,i0,a,f4.1,1x,f4.1)', 'call ', k, ': ', mean_of(real(k)), mean_of_fixed(real(k))
  end do

contains

  function mean_of(x) result(m)          ! legacy habit: initialize in the declaration
    real, intent(in) :: x
    real :: m
    real :: total = 0.0                   ! initialized once, saved between calls
    integer :: count = 0
    total = total + x
    count = count + 1
    m = total/count
  end function mean_of

  function mean_of_fixed(x) result(m)    ! intended: no state between calls
    real, intent(in) :: x
    real :: m
    real :: total
    total = 0.0
    m = (total + x)/1
  end function mean_of_fixed
end program accidental_save
call 1:  1.0  1.0
call 2:  1.5  2.0
call 3:  2.0  3.0

mean_of accumulates across calls (1.0, 1.5, 2.0) because total and count are initialized in their declarations; mean_of_fixed assigns total and so has no memory. Flang 23 gives the same output. If a legacy program relies on locals being static without ever initializing them, that behaviour is undefined, and the portable repair is an explicit save for the variables that must persist. gfortran -fno-automatic and flang-23 -fno-automatic force static allocation of all locals and can be used to test the hypothesis, but they are a diagnostic tool, not a fix: making every local static is unsafe with recursion and with threads.

Nonstandard idioms

Old programs also use features that no standard ever contained. The standard requires a processor to be able to report them (4.2), and GNU Fortran does so under -std=f2023. They are listed here because they fill old code and vendor interfaces.

Idiom What it means Replacement

real*8, integer*4, complex*16

byte-size type declarations of the 1970s, where the number is the storage in bytes

real(real64), integer(int32) and complex(real64) with the kinds of iso_fortran_env, or a kind from a kinds module

double precision

standard, not obsolescent; the second real kind

real(dp) with a named kind, which survives a change of the working precision

%val(x), %ref(x)

nonstandard argument-list functions that pass by value or by reference to a routine with an implicit interface

bind(c) interface with the value attribute

Cray pointers, pointer (p, arr)

a pointer integer and the array it addresses

c_ptr with c_f_pointer, or a Fortran pointer

include 'common.inc' with common blocks

one copy of the declaration textually included in many units

a module

getenv, iargc, etime, lnblnk, flush(6) as vendor routines

compiler library routines

get_environment_variable, command_argument_count, cpu_time or system_clock, len_trim, the flush statement

Hollerith constants, 4HABCD

character data held in an integer

a character variable

Integer used as an address

pointer-sized integer*8 to pass an address

type(c_ptr) or c_intptr_t

The kinds, in a single file (the legacy declarations are rejected by -std=f2023, as the table says):

program n1
  implicit none
  real*8 :: x
  integer*4 :: n
  complex*16 :: z
  double precision :: d
  x = 1.0d0/3.0d0
  n = huge(n)
  z = (1.0d0, 2.0d0)
  d = 2.0d0
  print *, kind(x), kind(n), kind(z), kind(d)
end program n1
$ gfortran n1_kinds.f90 && ./a.out
           8           4           8           8
$ gfortran -std=f2023 -c n1_kinds.f90
n1_kinds.f90:3:9 Error: GNU Extension: Nonstandard type declaration REAL*8 at (1)
n1_kinds.f90:4:12 Error: GNU Extension: Nonstandard type declaration INTEGER*4 at (1)
n1_kinds.f90:5:13 Error: GNU Extension: Nonstandard type declaration COMPLEX*16 at (1)
n1_kinds.f90:8:3 Error: Symbol 'n' at (1) has no IMPLICIT type
n1_kinds.f90:7:3 Error: Symbol 'x' at (1) has no IMPLICIT type
n1_kinds.f90:9:3 Error: Symbol 'z' at (1) has no IMPLICIT type
$ flang-23 n1_kinds.f90 && ./a.out
 8 4 8 8

The last three errors of the -std=f2023 list are consequences of the first three: a declaration that is rejected leaves the name undeclared. On the common x86_64 and aarch64 systems the byte count and the kind value are the same, which is why this idiom survived; but a kind value is only a processor-dependent number (a compiler may number its real kinds 1, 2 and 3), whereas the extension’s number is a byte count. That is why the kind form is the portable one. See Intrinsic Types and Kinds.

%val is used to call C functions without an explicit interface. With a C function int twice_(int k) (the trailing underscore is the GNU Fortran convention for the external name):

program n3
  implicit none
  integer :: n
  integer, external :: twice
  n = 21
  print *, twice(%val(n))
end program n3
$ gcc -c n3_twice.c && gfortran n3_val.f90 n3_twice.o && ./a.out
          42
$ gfortran -std=f2023 -c n3_val.f90
n3_val.f90:6:23 Error: GNU Extension: argument list function at (1)
$ flang-23 n3_val.f90 n3_twice.o && ./a.out
 42

It works on these two compilers, but it depends on the compiler’s external-name convention and on the argument-passing convention, neither of which the standard defines. The standard way is a bind(c) interface with the value attribute, described on Interoperability with C and Calling Fortran from C, C++ and Python.

Cray pointers are a GNU Fortran extension that needs a flag:

program n2
  implicit none
  real :: buf(4)
  real :: arr(4)
  pointer (p, arr)
  buf = [1.0, 2.0, 3.0, 4.0]
  p = loc(buf)
  print *, arr(3)
end program n2
$ gfortran n2_cray.f90
n2_cray.f90:5:11 Error: Cray pointer declaration at (1) requires '-fcray-pointer' flag
n2_cray.f90:7:3 Error: Symbol 'p' at (1) has no IMPLICIT type
$ gfortran -fcray-pointer n2_cray.f90 && ./a.out
   3.00000000
$ gfortran -fcray-pointer -std=f2023 -c n2_cray.f90
n2_cray.f90:7:6 Error: Function 'loc' at (1) has no IMPLICIT type
$ flang-23 n2_cray.f90 && ./a.out
 3.

With the flag the pointer declaration is accepted; the remaining error under -std=f2023 is only the nonstandard function loc. Flang 23 accepts the program without a flag. The portable replacement is c_loc and c_f_pointer from iso_c_binding, or a Fortran pointer with a target.

Vendor library routines are the third group:

program n4
  implicit none
  integer :: n, i
  real :: t(2), tot
  character(32) :: home
  n = iargc()
  call getenv('HOME', home)
  tot = etime(t)
  i = lnblnk('abc  ')
  print *, n, i, len_trim(home) > 0
  call flush(6)
end program n4
$ gfortran n4_vendor.f90 && ./a.out
           0           3 T
$ gfortran -std=f2023 -c n4_vendor.f90
n4_vendor.f90:6:6 Error: Function 'iargc' at (1) has no IMPLICIT type
n4_vendor.f90:8:8 Error: Function 'etime' at (1) has no IMPLICIT type
n4_vendor.f90:9:6 Error: Function 'lnblnk' at (1) has no IMPLICIT type
$ flang-23 n4_vendor.f90 && ./a.out
 0 3 T

Flang accepts all five routines. They are nonstandard intrinsic procedures, not part of the standard, and the compiler vendors are free to drop or change them; use command_argument_count, get_environment_variable, cpu_time and len_trim, which are standard. See Intrinsic Procedures Overview.

Hollerith constants complete the picture:

program n5
  implicit none
  integer :: h
  character(4) :: c
  data h /4HABCD/
  c = transfer(h, c)
  print '(a)', c
  print '(3Habc)'
end program n5
$ gfortran n5_hollerith.f90 && ./a.out
n5_hollerith.f90:5:13 Warning: Legacy Extension: Hollerith constant at (1)
n5_hollerith.f90:8:10 Warning: The H format specifier at (1) is a Fortran 95 deleted feature
n5_hollerith.f90:5:12 Warning: Extension: Conversion from HOLLERITH to INTEGER(4) at (1)
ABCD
abc
$ gfortran -std=f2023 -c n5_hollerith.f90
n5_hollerith.f90:5:13 Error: Legacy Extension: Hollerith constant at (1)
n5_hollerith.f90:8:10 Error: The H format specifier at (1) is a Fortran 95 deleted feature

Flang 23 builds it silently and prints the same two lines. The H edit descriptor in the format of the last print is the deleted feature of B.1 (5); the Hollerith constant in the data statement is a nonstandard extension. Use character variables and literals, and see Characters and Strings.

The Fortran 2028 additions

The Fortran 2028 working draft J3/26-007r1 (3 March 2026) adds two entries to its list of obsolescent features, bringing the list from twelve to fourteen. Fortran 2028 is at committee-draft stage and unpublished, so both are described as upcoming and should not be read as a promise; see What’s New and Fortran 2028 Preview.

  • Dw.d edit descriptor (B.3.14 of the working draft). It was the double precision descriptor of the time when there were only two real kinds; since Fortran 90 a processor may support more than two real kinds, and Dw.d "may emit an E exponent letter instead of D" and has no exponent-width form. Ew.d and Ew.dEe work with all real kinds.

  • Default implicit typing (B.3.15). It "is a significant source of programming errors and has been superseded by explicit typing". The implicit statement itself stays: it can still map initial letters to types, as in implicit double precision (a-h, o-z).

The behaviour of the descriptor in the two compilers, today:

program dw_d
  use, intrinsic :: iso_fortran_env, only: real32, real64, real128
  implicit none (type, external)
  real(real32) :: a = 1.5_real32
  real(real64) :: b = 1.5_real64
  real(real128) :: c = 1.5_real128
  print '(d12.4)', a
  print '(d12.4)', b
  print '(d12.4)', c
  print '(e12.4)', b
  print '(e12.4e3)', c
  print '(e12.4)', 1.0d100
end program dw_d
  0.1500D+01
  0.1500D+01
  0.1500D+01
  0.1500E+01
 0.1500E+001
  0.1000+101

Both compilers print the D descriptor for all three kinds with a D exponent letter (checked with GNU Fortran and Flang). The last two lines show the exponent-width form, which Dw.d lacks: e12.4e3 prints a three-digit exponent with its letter, whereas a plain e12.4 drops the letter when the exponent needs three digits (0.1000+101). The conversion recipe is the same one the first steps of the conversion already apply: write implicit none everywhere, and replace Dw.d by Ew.d or by ES/G0 as the output requires (see Format Specifications and Edit Descriptors). Code that follows the implicit none rule is unaffected when default implicit typing becomes obsolescent.

A step-by-step modernization

The approach that works on real code is the one that works on any refactoring: change one thing at a time, and never change behaviour and structure in the same step. For old Fortran this means an order, which the diagram shows, and a test that is run after every step.

flowchart TD S["Legacy routine
fixed form, implicit typing, COMMON"] --> T0["0. Freeze behaviour
golden output from the legacy build"] T0 --> A["1. Free form
remove deleted features"] A --> B["2. implicit none
declare every name"] B --> C["3. Modules and explicit interfaces
replace COMMON, BLOCK DATA, statement functions"] C --> D["4. Allocatables and assumed-shape arrays
intent, kinds, no global state"] D --> E["5. Derived types
data and its operations together"] E --> F["6. Automated tests in the build
fpm test or CTest"] A -. "after every step" .-> V{"Same output?
strict build clean?"} B -.-> V C -.-> V D -.-> V E -.-> V V -- "no" --> W["Find out why:
bug in the edit, or a bug in the old code"] V -- "yes" --> N["Commit and continue"]

The seven stages are shown in order below, on the program of the section "A FORTRAN 77 program". Each file was built with GNU Fortran and Flang and gives the output quoted. The files are small, but the process is the same on a real package: the steps scale by applying each one to a subset of files at a time.

Step 0: freeze the behaviour

Before touching a line, record what the program does. A characterization test does not say what the program should do; it records what it does. For a numerical program that prints a result, the first test is a stored output:

$ gfortran -std=legacy -ffixed-form 01_heat_legacy.f -o heat_legacy
$ ./heat_legacy > golden.txt
$ cat golden.txt
 steps= 200 residual= 1.500E-03 center=   0.163

For anything numerical, add the tolerance question to the plan early: exact text equality is a good first check, and it stops being meaningful as soon as an optimization level, a compiler or a kind changes the last digits. Decide, before you start, which quantities are compared and how close they have to be.

Step 1: free form, and the deleted features

Convert the source form first, because everything else is easier to read afterwards. The mechanical part of the conversion (columns, continuation, comments) can be done by a tool (see Building and tooling for legacy packages); the deleted features have to be rewritten by hand, because a compiler in -std=f2023 mode will not build them. Here the arithmetic if becomes exit in a do loop, and the nonblock do becomes two nested loops. Everything else stays the same on purpose: the implicit statement, the common blocks, the labelled loops, the statement function.

! Step 1: free form. Same logic; deleted features rewritten.
program heat
  implicit double precision (a-h, o-z)
  parameter (nmax = 21)
  dimension u(nmax, nmax), unew(nmax, nmax)
  common /params/ alpha, dt, dx
  common /sizes/ n
  n = nmax
  dx = 1.0d0/(n - 1)
  dt = 0.2d0*dx*dx/alpha
  call init(u, nmax)
  k = 0
  do
    k = k + 1
    call step(u, unew, nmax, res)
    if (k >= 200) exit        ! was the arithmetic IF
  end do
  write (*, 100) k, res, u(11, 11)
100 format (' steps=', i4, ' residual=', 1pe10.3, ' center=', 0pf8.3)
end program heat

block data pinit
  double precision alpha, dt, dx
  common /params/ alpha, dt, dx
  data alpha /1.0d0/
end block data pinit

subroutine init(u, ld)
  implicit double precision (a-h, o-z)
  dimension u(ld, *)
  common /sizes/ n
  do 20 j = 1, n
    do 10 i = 1, n
      u(i, j) = 0.0d0
10  continue
20 continue
  u(11, 11) = 100.0d0
  return
end subroutine init

subroutine step(u, unew, ld, res)
  implicit double precision (a-h, o-z)
  dimension u(ld, *), unew(ld, *)
  common /params/ alpha, dt, dx
  common /sizes/ n
  lap(i, j) = (u(i - 1, j) + u(i + 1, j) + u(i, j - 1) + u(i, j + 1) &
               - 4.0d0*u(i, j))/(dx*dx)
  res = 0.0d0
  do 20 j = 2, n - 1
    do 10 i = 2, n - 1
      unew(i, j) = u(i, j) + alpha*dt*lap(i, j)
      res = dmax1(res, dabs(unew(i, j) - u(i, j)))
10  continue
20 continue
  do j = 2, n - 1           ! was the shared-label nonblock DO
    do i = 2, n - 1
      u(i, j) = unew(i, j)
    end do
  end do
  return
end subroutine step

The result builds in Fortran 2023 mode, with only warnings about the obsolescent features that are still there:

      1 Warning: Fortran 2018 obsolescent feature: BLOCK DATA construct at (1)
     12 Warning: Fortran 2018 obsolescent feature: COMMON block at (1)
      4 Warning: Fortran 2018 obsolescent feature: Labeled DO statement at (1)
      1 Warning: Obsolescent feature: Statement function at (1)
      1 Warning: Possible change of value in conversion from REAL(8) to INTEGER(4) at (1) [-Wconversion]
$ ./a.out
 steps= 200 residual= 1.500E-03 center=   0.163

Flang 23 prints the same line. The output is identical to the golden output, so the step is safe. Look at the last warning: Possible change of value in conversion from REAL(8) to INTEGER(4) is -Wconversion, which is part of -Wall. It points at the statement function lap, and the next step explains why.

Step 2: implicit none and declarations

Add implicit none and declare every name. The compiler lists the undeclared ones and, in one case, does more than that:

! Step 2: implicit none. Every name is declared; the undeclared LAP shows up.
program heat
  implicit none
  integer, parameter :: nmax = 21
  double precision :: u(nmax, nmax), unew(nmax, nmax)
  double precision :: alpha, dt, dx, res
  integer :: n, k
  common /params/ alpha, dt, dx
  common /sizes/ n
  n = nmax
  dx = 1.0d0/(n - 1)
  dt = 0.2d0*dx*dx/alpha
  call init(u, nmax)
  k = 0
  do
    k = k + 1
    call step(u, unew, nmax, res)
    if (k >= 200) exit
  end do
  write (*, 100) k, res, u(11, 11)
100 format (' steps=', i4, ' residual=', 1pe10.3, ' center=', 0pf8.3)
end program heat

block data pinit
  implicit none
  double precision :: alpha, dt, dx
  common /params/ alpha, dt, dx
  data alpha /1.0d0/
end block data pinit

subroutine init(u, ld)
  implicit none
  integer, intent(in) :: ld
  double precision, intent(out) :: u(ld, *)
  integer :: n, i, j
  common /sizes/ n
  do j = 1, n
    do i = 1, n
      u(i, j) = 0.0d0
    end do
  end do
  u(11, 11) = 100.0d0
end subroutine init

subroutine step(u, unew, ld, res)
  implicit none
  integer, intent(in) :: ld
  double precision, intent(inout) :: u(ld, *)
  double precision, intent(inout) :: unew(ld, *)
  double precision, intent(out) :: res
  double precision :: alpha, dt, dx, lap
  integer :: n, i, j
  common /params/ alpha, dt, dx
  common /sizes/ n
  lap(i, j) = (u(i - 1, j) + u(i + 1, j) + u(i, j - 1) + u(i, j + 1) &
               - 4.0d0*u(i, j))/(dx*dx)
  res = 0.0d0
  do j = 2, n - 1
    do i = 2, n - 1
      unew(i, j) = u(i, j) + alpha*dt*lap(i, j)
      res = max(res, abs(unew(i, j) - u(i, j)))
    end do
  end do
  do j = 2, n - 1
    do i = 2, n - 1
      u(i, j) = unew(i, j)
    end do
  end do
end subroutine step

The statement function lap begins with the letter l, which is not in the range a-h, o-z of the legacy implicit double precision statement. By the default rules, a name that begins with i to n has type integer. So in the legacy program lap was an integer function: each Laplacian value was truncated to a whole number before it was used. When implicit none forces the declaration, nobody can write the wrong type any more. Without the declaration of lap, the compilers say:

/tmp/nolap.f90:55:3 Error: Variable type is UNKNOWN in assignment at (1)
/tmp/nolap.f90:60:38 Error: Operands of binary numeric operator '*' at (1) are REAL(8)/UNKNOWN
error: Semantic errors in /tmp/nolap.f90
/tmp/nolap.f90:55:3: error: No explicit type declared for 'lap'

With double precision :: lap the program builds, and the output changes:

$ ./a.out
 steps= 200 residual= 1.379E-03 center=   0.138

The new output differs from the golden output: the legacy program computed the wrong answer, and the test did its job by failing. That is not an argument against the test; it is the reason to have one. Do not fix the legacy program and the conversion at the same time. Establish the cause first: add the single declaration DOUBLE PRECISION LAP to the original fixed-form STEP routine, rebuild it with -std=legacy, and check that the legacy program now prints steps= 200 residual= 1.379E-03 center= 0.138, the same value as step 2. The cause is confirmed (one line of the legacy source), the new reference is justified, and the golden file is replaced with the corrected one. Flang gives the same result on both versions.

This is not an exotic case. Implicit typing combined with a one-letter naming habit is the commonest way a legacy numerical program is silently wrong, and the compiler can detect it only if it is asked: -Wconversion (in -Wall) reported it in the previous step, and -fimplicit-none or implicit none turns it into an error. The options of Compilers and Compiler Options belong in the build from the first day of a conversion.

Step 3: modules and explicit interfaces

Now the shared storage goes. The common blocks become a module, the block data becomes initializers in the declarations, the external routines become module procedures with explicit interfaces, the statement function becomes an internal function, and double precision becomes real(dp) with a kind from iso_fortran_env:

! Step 3: modules replace COMMON, BLOCK DATA, external routines and the statement function.
module heat_kinds
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none (type, external)
  private
  public :: dp
  integer, parameter :: dp = real64
end module heat_kinds

module heat_params
  use heat_kinds, only: dp
  implicit none (type, external)
  private
  public :: alpha, dt, dx, n
  real(dp) :: alpha = 1.0_dp, dt = 0.0_dp, dx = 0.0_dp   ! initialization replaces BLOCK DATA
  integer :: n = 0
end module heat_params

module heat_solver
  use heat_kinds, only: dp
  use heat_params, only: alpha, dt, dx, n
  implicit none (type, external)
  private
  public :: init, step

contains

  subroutine init(u, ld)
    integer, intent(in) :: ld
    real(dp), intent(out) :: u(ld, *)
    u(1:n, 1:n) = 0.0_dp
    u(11, 11) = 100.0_dp
  end subroutine init

  subroutine step(u, unew, ld, res)
    integer, intent(in) :: ld
    real(dp), intent(inout) :: u(ld, *)
    real(dp), intent(inout) :: unew(ld, *)
    real(dp), intent(out) :: res
    integer :: i, j
    res = 0.0_dp
    do j = 2, n - 1
      do i = 2, n - 1
        unew(i, j) = u(i, j) + alpha*dt*lap(i, j)
        res = max(res, abs(unew(i, j) - u(i, j)))
      end do
    end do
    u(2:n - 1, 2:n - 1) = unew(2:n - 1, 2:n - 1)
  contains
    pure function lap(i, j)      ! was a statement function
      integer, intent(in) :: i, j
      real(dp) :: lap
      lap = (u(i - 1, j) + u(i + 1, j) + u(i, j - 1) + u(i, j + 1) &
             - 4.0_dp*u(i, j))/(dx*dx)
    end function lap
  end subroutine step
end module heat_solver

program heat
  use heat_kinds, only: dp
  use heat_params, only: dt, dx, alpha, n
  use heat_solver, only: init, step
  implicit none (type, external)
  integer, parameter :: nmax = 21
  real(dp) :: u(nmax, nmax), unew(nmax, nmax), res
  integer :: k
  n = nmax
  dx = 1.0_dp/(n - 1)
  dt = 0.2_dp*dx*dx/alpha
  call init(u, nmax)
  k = 0
  do
    k = k + 1
    call step(u, unew, nmax, res)
    if (k >= 200) exit
  end do
  write (*, '(a,i4,a,es10.3,a,f8.3)') ' steps=', k, ' residual=', res, ' center=', u(11, 11)
end program heat
 steps= 200 residual= 1.379E-03 center=   0.138

The program now builds under all strict options (-std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all) with no warning at all, and Flang gives the same output. What the compiler checks now that it could not before: every call is checked against the interface, intent violations are errors, an undefined module entity is an error, and array bounds are checked at run time where the compiler knows them (-fcheck=all). The leading dimension argument ld is still there because the arrays are assumed-size; the next step removes it.

Step 4: allocatable and assumed-shape arrays

The fixed nmax and the leading dimension disappear: the arrays carry their own shape, the grid size comes from size(u, 1), and the global parameters become arguments of a pure procedure. The program no longer needs a module for the state:

! Step 4: assumed-shape and allocatable arrays, intent, no global state.
module heat_kinds
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none (type, external)
  private
  public :: dp
  integer, parameter :: dp = real64
end module heat_kinds

module heat_solver
  use heat_kinds, only: dp
  implicit none (type, external)
  private
  public :: init, step

contains

  pure subroutine init(u)
    real(dp), intent(out) :: u(:, :)
    u = 0.0_dp
    u(11, 11) = 100.0_dp
  end subroutine init

  pure subroutine step(u, unew, alpha, dt, dx, res)
    real(dp), intent(inout) :: u(:, :)
    real(dp), intent(inout) :: unew(:, :)
    real(dp), intent(in) :: alpha, dt, dx
    real(dp), intent(out) :: res
    integer :: i, j, n
    n = size(u, 1)
    res = 0.0_dp
    do j = 2, n - 1
      do i = 2, n - 1
        unew(i, j) = u(i, j) + alpha*dt*(u(i - 1, j) + u(i + 1, j) &
                     + u(i, j - 1) + u(i, j + 1) - 4.0_dp*u(i, j))/(dx*dx)
        res = max(res, abs(unew(i, j) - u(i, j)))
      end do
    end do
    u(2:n - 1, 2:n - 1) = unew(2:n - 1, 2:n - 1)
  end subroutine step
end module heat_solver

program heat
  use heat_kinds, only: dp
  use heat_solver, only: init, step
  implicit none (type, external)
  integer, parameter :: n = 21
  real(dp), allocatable :: u(:, :), unew(:, :)
  real(dp) :: alpha, dt, dx, res
  integer :: k
  allocate (u(n, n), unew(n, n))
  alpha = 1.0_dp
  dx = 1.0_dp/(n - 1)
  dt = 0.2_dp*dx*dx/alpha
  call init(u)
  unew = 0.0_dp
  do k = 1, 200
    call step(u, unew, alpha, dt, dx, res)
  end do
  write (*, '(a,i4,a,es10.3,a,f8.3)') ' steps=', k - 1, ' residual=', res, ' center=', u(11, 11)
end program heat
 steps= 200 residual= 1.379E-03 center=   0.138

Flang gives the same output. Passing alpha, dt and dx as arguments instead of through a module is not just style: the procedure is now pure, which is the precondition for calling it from do concurrent and from OpenMP parallel regions (see Array Arguments: Assumed Shape, Size and Rank and Allocatable Arrays and Dynamic Memory).

Step 5: derived types

When several routines always receive the same bundle of arguments, the bundle becomes a type, and the routines become its type-bound procedures. This is the shape of the grid_t of the heatflow project (see Derived Types and Type-Bound Procedures, Abstract Types and Finalization):

! Step 5: the data and the operations that belong together live in one derived type.
module heat_grid
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none (type, external)
  private
  public :: dp, grid_t

  integer, parameter :: dp = real64

  type :: grid_t
    integer :: n = 0
    real(dp) :: dx = 0.0_dp
    real(dp), allocatable :: u(:, :)
  contains
    procedure :: set_hot_spot
    procedure :: step
  end type grid_t

  interface grid_t
    module procedure make_grid
  end interface grid_t

contains

  function make_grid(n, length) result(g)
    integer, intent(in) :: n
    real(dp), intent(in) :: length
    type(grid_t) :: g
    g%n = n
    g%dx = length/(n - 1)
    allocate (g%u(n, n), source=0.0_dp)
  end function make_grid

  subroutine set_hot_spot(self, i, j, temperature)
    class(grid_t), intent(inout) :: self
    integer, intent(in) :: i, j
    real(dp), intent(in) :: temperature
    self%u(i, j) = temperature
  end subroutine set_hot_spot

  subroutine step(self, alpha, dt, residual)
    class(grid_t), intent(inout) :: self
    real(dp), intent(in) :: alpha, dt
    real(dp), intent(out) :: residual
    real(dp) :: unew(self%n, self%n)
    integer :: i, j, n
    n = self%n
    unew = self%u
    residual = 0.0_dp
    do j = 2, n - 1
      do i = 2, n - 1
        unew(i, j) = self%u(i, j) + alpha*dt*(self%u(i - 1, j) + self%u(i + 1, j) &
                     + self%u(i, j - 1) + self%u(i, j + 1) - 4.0_dp*self%u(i, j))/self%dx**2
        residual = max(residual, abs(unew(i, j) - self%u(i, j)))
      end do
    end do
    self%u = unew
  end subroutine step
end module heat_grid
program heat
  use heat_grid, only: dp, grid_t
  implicit none (type, external)
  type(grid_t) :: g
  real(dp) :: alpha, dt, res
  integer :: k
  g = grid_t(21, 1.0_dp)
  call g%set_hot_spot(11, 11, 100.0_dp)
  alpha = 1.0_dp
  dt = 0.2_dp*g%dx**2/alpha
  do k = 1, 200
    call g%step(alpha, dt, res)
  end do
  write (*, '(a,i4,a,es10.3,a,f8.3)') ' steps=', k - 1, ' residual=', res, ' center=', g%u(11, 11)
end program heat
 steps= 200 residual= 1.379E-03 center=   0.138

The main program no longer knows how the grid is stored. The constructor grid_t(21, 1.0_dp) allocates and zeroes the array, the hot spot and the step are methods, and grid_t has no global state, so two grids can coexist, which was impossible with common. The output matches the previous step on both compilers.

Step 6: tests that live in the build

The stored output of step 0 is replaced by a test program that compares numbers with a tolerance and reports failure through its exit status, so that a build tool or a CI job can use it:

! Step 6: the characterization test. The reference numbers come from the legacy program
! (after declaring LAP), so the test fails if a later edit changes the numerics.
program test_heat
  use heat_grid, only: dp, grid_t
  implicit none (type, external)
  real(dp), parameter :: ref_residual = 1.379e-3_dp, ref_center = 0.138_dp
  type(grid_t) :: g
  real(dp) :: alpha, dt, res
  integer :: k, failures

  g = grid_t(21, 1.0_dp)
  call g%set_hot_spot(11, 11, 100.0_dp)
  alpha = 1.0_dp
  dt = 0.2_dp*g%dx**2/alpha
  do k = 1, 200
    call g%step(alpha, dt, res)
  end do

  failures = 0
  call check('residual', res, ref_residual, 5.0e-7_dp)
  call check('center', g%u(11, 11), ref_center, 5.0e-4_dp)
  if (failures > 0) error stop 'regression test failed'
  print '(a)', 'all checks passed'

contains

  subroutine check(name, actual, expected, tol)
    character(len=*), intent(in) :: name
    real(dp), intent(in) :: actual, expected, tol
    if (abs(actual - expected) > tol) then
      print '(a,a,2es12.4)', 'FAIL ', name, actual, expected
      failures = failures + 1
    else
      print '(a,a,es12.4)', 'ok   ', name, actual
    end if
  end subroutine check
end program test_heat
ok   residual  1.3788E-03
ok   center  1.3822E-01
all checks passed
exit status 0

Flang gives the same two ok lines. To show that the test protects the code, the reference values were temporarily changed back to the legacy program’s wrong ones (1.500E-03 and 0.163); the test fails, as it should:

FAIL residual  1.3788E-03  1.5000E-03
FAIL center  1.3822E-01  1.6300E-01
ERROR STOP regression test failed
exit status 1

In a package, the file goes in test/ and runs with fpm test; the test-drive framework, CTest and Meson’s test() are alternatives (see Fortran Package Manager (fpm), Testing, Debugging and Profiling and Building with Make, CMake and Meson). The seven stages show the order; a real project applies them to one routine at a time, with the old and the new code linked side by side while the callers are migrated.

What the conversion did not do is also worth stating. It did not change the numerical scheme, did not introduce parallelism and did not touch the output format. Those are separate changes, each with its own test, and they belong after the structure is sound, on pages such as Performance and Memory Layout and OpenMP and OpenACC.

Building and tooling for legacy packages

Converting fixed form to free form

The conversion of form is mechanical, and a tool can do it. findent (a command-line program by Willem Vermin, packaged as findent in Debian; it is a community tool, not part of any compiler) reads fixed or free form and writes the other. Run on the legacy program of this page (findent -ifixed -ofree -Rr):

$ findent -ifixed -ofree -Rr < 01_heat_legacy.f > heat_free.f90
findent version 4.3.5

The first lines of the result: the labels move, the continuation columns disappear, end becomes end program HEAT, and the indentation is regular. It changes the form and not the style: the converted file still has the arithmetic if, the nonblock do, common, the statement function and implicit typing, and the compiler reports exactly the same diagnostics (3 errors and 20 warnings) as for the fixed-form original (compared with gfortran -std=f2023 -Wall -Wextra -pedantic -c). The conversion is therefore step 1 without the deleted-feature fixes. More options of the tool, and of the formatter fprettify and the linter Fortitude, are on Documentation and Editor Tooling.

Flang has a -fc1 -fdebug-unparse option that prints the program tree as Fortran source. It was tried on a seven-line routine and is not a conversion tool: it prints the compiler’s normalized form (for instance y(__builtin_int(i,kind=8)) for y(I)), which is neither valid nor readable source.

Two practical rules apply to any converter. Review the diff, because comments and continued format lists are where tools differ; and convert and compile in the same commit, so that a failed build points at the conversion. Convert file by file only if the package builds with both forms, which every build system on this page does (a file’s form is chosen by its extension or by an option).

Describing a legacy package to a build tool

A package of fixed-form files is described to the build system by the form of its files and by the leniency of its checks. fpm’s defaults suit modern code: it compiles with -fimplicit-none and free form. For the legacy program of this page, with app/main.f, two things were needed, and both were checked with fpm 0.13.0 in the container:

name = "heat_legacy"
version = "0.1.0"

[[executable]]
name = "heat"
source-dir = "app"
main = "main.f"

[fortran]
implicit-typing = true
implicit-external = true
source-form = "fixed"

With this manifest fpm run builds and prints steps= 200 residual= 1.500E-03 center= 0.163. Without the [fortran] table the verbose build shows that fpm compiles with -fimplicit-none -Werror=implicit-interface -ffree-form, and the same file fails with the dozens of errors shown above (Symbol 'i' at (1) has no IMPLICIT type). Without the explicit table, fpm did not discover app/main.f and reported Neither library nor executable found; the automatic discovery found only free-form .f90 sources in the test, so name the main file explicitly. See Fortran Package Manager (fpm).

For CMake and Meson, the options are the ones in the table of the section "Building old code": set the form and the line length per file (or for the target), and prefer -std=legacy only on the legacy sources, so that new files are held to the strict rules. Both build systems decide the form of a file by its extension (.f fixed, .f90 free), and the build file can override it. See Building with Make, CMake and Meson.

Legacy idioms in vendor APIs

Old idioms do not live only in old programs. A vendor API whose interface was defined decades ago keeps them in its Fortran bindings, because its users' code depends on them. The MATLAB Fortran API is the example used here: the book on the MATLAB Fortran API consulted for this section is written in the style of the 1990s, and the points where today’s code differs are these (the full treatment is on MATLAB: MEX, Engine and MAT-File APIs):

Legacy style in the book Current practice

Fixed-form .F gateway sources

free-form .F90; the capital F is still needed for the preprocessor that includes fintrf.h

integer*4, real*8, complex*16

kinds from iso_fortran_env or iso_c_binding

%val and mxCopy* routines to move data

%val and mxCopy* are compatibility mechanisms; pointers and c_f_pointer avoid the copy

pointer-sized integers instead of type(c_ptr)

type(c_ptr) or the API’s mwPointer macro

hand-written type statements for each API function

explicit interfaces

mxGetPr and mxGetPi (separate real and imaginary parts)

interleaved complex API: mxGetDoubles, mxGetComplexDoubles and mex -R2018a

mexSetTrapFlag

mexCallMATLABWithTrap

COBOL is the other language whose modernization is a field of its own, with its own conversion problems; it is outside this section and is tracked in this repository as issue #243. The Fortran case is easier in one respect: the old dialect is still a legal subset of the language, so conversion can proceed routine by routine without a flag day.

Where the books are outdated

The two reference books consulted for this section are modern in outlook and are still sound on the language; the old dialect is not their subject. Where they differ from current practice, or where a reader could misread them:

Area Outdated or easily misread, and the current position

"Deprecated" lists

the first appendix of the main reference lists the authors' deprecations (include, double precision, do while, go to, .eq.-style operators, implicit interfaces, sequence and storage association, non-default implicit, volatile, sync memory). They are recommendations. In Fortran 2023 none of them is deleted or obsolescent; the standard’s list is Annex B, covered by this page

The standard’s lists

the second appendix of that book reproduces the obsolescent and deleted features of its edition. The current Fortran 2023 lists are Annex B.1 to B.3 of J3/23-007r1 (twelve obsolescent features), and the Fortran 2028 working draft J3/26-007r1 adds Dw.d and default implicit typing, which no book can contain yet

err= and end=

footnotes in that book call these specifiers "obsolescent" and point at the appendix of deprecations. They are not obsolescent in the standard; iostat= and iomsg= are the preferred style (see Input/Output: Units and Files)

Compilers and flags

the main reference gives no compiler-support information, so it does not say what a compiler does with old code (-std=legacy, -ffixed-form, -fallow-argument-mismatch) or which compilers accept deleted features silently (see the table in the section on compiler modes)

Build tools

the recompilation advice is about make; for a legacy package fpm, CMake and Meson need the form and implicit-typing options of the section above

MATLAB style

the MATLAB Fortran API book uses fixed-form .F files, integer*4, real*8, %val, pointer-sized integers and separate-complex accessors (mxGetPr, mxGetPi); its default build mode is the separate-complex API. The current API is interleaved complex (mex -R2018a, mxGetDoubles) and mexSetTrapFlag is replaced by mexCallMATLABWithTrap; the table in the section on vendor APIs lists them. The R2026b default of the complex mode was not re-verified for this page

Standard version provenance and compiler support

Item Status in the standard Compiler behaviour checked on 2026-10-19

Real do variable, branch to end if, pause, assign, H editing, vertical format control

deleted, not in Fortran 95 or later (4.4.2, B.1)

GNU Fortran: warning in the default dialect, error with -std=f2023. Flang: accepted; message with -std=f2018

Arithmetic if, nonblock do

deleted in Fortran 2018 (B.2, C.1)

same as above

Alternate return, computed go to, statement function, data among executables, assumed-length character function, fixed form, character*n, entry, specific names

obsolescent (B.3.2 to B.3.9, B.3.12)

GNU Fortran: Obsolescent feature warning with -pedantic for alternate return, character*n, statement function, computed go to, data among executables and character(*) functions; entry needs -std=f2018 or later; no warning for specific names. Flang: no message

Labelled do, common, equivalence, block data, forall

obsolescent, made so in Fortran 2018 (C.1)

GNU Fortran: Fortran 2018 obsolescent feature with -std=f2018 or -std=f2023 (not with -pedantic alone). Flang: no message

Dw.d edit descriptor, default implicit typing

obsolescent in the Fortran 2028 working draft J3/26-007r1 only (B.3.14, B.3.15)

both accept; no warning

real*8, %val, Hollerith, vendor intrinsics

nonstandard

GNU Fortran: accepted by default, errors with -std=f2023. Flang: accepted without options

Cray pointers

nonstandard

GNU Fortran needs -fcray-pointer. Flang: accepted

Not checked here, because no compiler for them is installed: Intel ifx, NAG, HPE Cray and NVIDIA HPC compilers. Their handling of old code differs again (several compilers have long-standing options for FORTRAN 66/77 and VAX extensions). See History, Standards and Compilers for the per-compiler support table.

Pitfalls

  • Trusting a clean build of legacy code. A default gfortran build accepts the deleted features and gives a warning at most. A Flang build gives nothing. The check that means something is a build with -std=f2023 -Wall -Wextra -pedantic -fcheck=all, in the GNU compiler, run in addition to the production build.

  • Column 72. A statement longer than 72 columns is cut silently in fixed form (-Wline-truncation, in -Wall, reports it); the result is a valid statement with a different meaning. Convert to free form before anything else.

  • The first letter decides the type. Under implicit typing, lap, nstep and index2 are integers, and a typo is a new variable. Add implicit none before editing a routine.

  • common blocks with different layouts in different files. No compiler checks across files; the output is garbage and the compilers agree on it. Replace by a module.

  • Implicit interfaces. A call to an external routine with a mismatched argument is a run-time bug that the compiler can find only if it sees both units. -fallow-argument-mismatch hides the error that GNU Fortran reports; use it only to build code you cannot change yet.

  • real*8 meaning "8 bytes". Replace by a named kind, and check that mixed code compiled with -fdefault-real-8 still passes arguments of the right size.

  • Converting forall to do concurrent by search and replace. The semantics differ (right side evaluated first versus independent iterations).

  • Accidental save. A local with an initializer in its declaration is saved. If a legacy routine depends on a local keeping its value without one, declare save explicitly, or make it an argument.

  • Changing behaviour and structure in one commit. Freeze the output first; when the output changes, find out whether the edit or the old code was wrong (step 2 above did exactly that).

  • Tool conversions without a test. Run the test after the conversion, in the same change.

  • Removing -std=legacy early. Keep it on the legacy files only, and keep new files on the strict options, so that the legacy part shrinks but the new part never regresses.