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.
Related pages: History, Standards and Compilers (where the
deleted and obsolescent features come from),
Program Structure and Source Form (the two source forms),
Modules and Submodules (the replacement for common),
Procedures, Functions and Subroutines,
Control Flow,
Compilers and Compiler Options,
Fortran Package Manager (fpm),
Testing, Debugging and Profiling and
What’s New and Fortran 2028 Preview.
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 ( |
The compiler manual |
Source: J3/23-007r1 (Fortran 2023), 4.2 Conformance, 4.4 Deleted and obsolescent features and Annex B
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 |
|
Everything on this page is accepted without a message, except a branch into a construct, which warns. |
|
Warns about some obsolescent features (7 of the 12 in the test file: alternate return, |
Reports a "portability" message for each deleted feature (3 for the program above); none for the obsolescent ones. |
|
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 |
|
|
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 |
|
|
Lines longer than 72 columns in fixed form |
|
|
Accept old argument-type mismatches across calls in one file |
|
not needed: an error is never raised for it; with both units in one file there is a warning ( |
Make all local variables static, as in some old programs |
|
|
Promote default |
|
|
C-style trailing underscore in external names |
on by default; |
|
Cray pointers, |
|
both built without an option |
DEC extensions of VAX-era compilers |
|
not available |
Require declaration of every name |
|
|
Warn that a statement was cut at column 72 |
|
no equivalent was found |
Warn about nonstandard intrinsics |
|
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);
-
Cor*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, 4andDO10I=1,4are the same, and so is the notoriousDO 10 I = 1.4, which is an assignment to a variable calledDO10I; -
a program unit’s
ENDstatement 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 |
|
dropped with Fortran 95 (4.4.2) |
an integer count and a computed value, |
Branch to an |
|
dropped with Fortran 95 |
branch to a |
|
|
dropped with Fortran 95 |
write a prompt, then |
|
|
dropped with Fortran 95 |
|
|
|
dropped with Fortran 95 |
a character literal, |
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 |
|
deleted in Fortran 2018 (C.1) |
|
Nonblock |
|
deleted in Fortran 2018 (C.1) |
|
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 |
|
B.3.2 |
a status argument and |
Computed |
|
B.3.3 |
|
Statement function |
|
B.3.4 |
an internal function, often |
|
|
B.3.5 |
initialization in the declaration, or an assignment |
Assumed-length character function |
|
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) |
|
|
B.3.8 |
|
|
|
B.3.9 |
one module procedure per entry point and private module data |
Labelled |
|
B.3.10 |
|
|
|
B.3.11 |
module variables, |
Specific names of intrinsics |
|
B.3.12 |
generic names with a kind: |
|
|
B.3.13 |
array assignment, |
Source: J3/23-007r1 (Fortran 2023), B.3 Obsolescent features and C.1 Features that were new in Fortran 2018
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
Source: J3/23-007r1 (Fortran 2023), 4.2 Conformance, 4.4 Deleted and obsolescent features and Annex B
Flang 23 produces the same output (checked). Three of the rewrites are not mechanical renamings and need care:
-
foralltodo concurrentis not a rename. Aforallevaluates the whole right-hand side before assigning, sov(2:4) = v(1:3)(the shift) is whatforall (i = 2:4) v(i) = v(i - 1)meant. Ado concurrentbody 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, anddo concurrentonly for the independent update. See do concurrent and GPU Offload. -
entrydata becomes module data. The sharedtotalis a module variable, which has thesaveattribute, and therefore global state; give itprivateaccess (done above) and pass state explicitly where concurrency matters. -
The real
dovariable 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 |
|---|---|---|
|
byte-size type declarations of the 1970s, where the number is the storage in bytes |
|
|
standard, not obsolescent; the second real kind |
|
|
nonstandard argument-list functions that pass by value or by reference to a routine with an implicit interface |
|
Cray pointers, |
a pointer integer and the array it addresses |
|
|
one copy of the declaration textually included in many units |
a module |
|
compiler library routines |
|
Hollerith constants, |
character data held in an integer |
a |
Integer used as an address |
pointer-sized |
|
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
Source: GNU Fortran manual: Extensions
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
Source: GNU Fortran manual: Extensions
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.dedit 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, andDw.d"may emit an E exponent letter instead of D" and has no exponent-width form.Ew.dandEw.dEework 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
implicitstatement itself stays: it can still map initial letters to types, as inimplicit 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.
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
Source: J3/23-007r1 (Fortran 2023), 6.3.3 Fixed source form, 11.1.7.2 Form of the DO construct and Annex B
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
Source: J3/23-007r1 (Fortran 2023), 8.4 Initialization, 8.10.2 COMMON statement and B.3 Obsolescent features
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 |
free-form |
|
kinds from |
|
|
pointer-sized integers instead of |
|
hand-written type statements for each API function |
explicit interfaces |
|
interleaved complex API: |
|
|
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 ( |
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 |
|
footnotes in that book call these specifiers "obsolescent" and point at the appendix of deprecations. They are not obsolescent in the standard; |
Compilers and flags |
the main reference gives no compiler-support information, so it does not say what a compiler does with old code ( |
Build tools |
the recompilation advice is about |
MATLAB style |
the MATLAB Fortran API book uses fixed-form |
Standard version provenance and compiler support
| Item | Status in the standard | Compiler behaviour checked on 2026-10-19 |
|---|---|---|
Real |
deleted, not in Fortran 95 or later (4.4.2, B.1) |
GNU Fortran: warning in the default dialect, error with |
Arithmetic |
deleted in Fortran 2018 (B.2, C.1) |
same as above |
Alternate return, computed |
obsolescent (B.3.2 to B.3.9, B.3.12) |
GNU Fortran: |
Labelled |
obsolescent, made so in Fortran 2018 (C.1) |
GNU Fortran: |
|
obsolescent in the Fortran 2028 working draft J3/26-007r1 only (B.3.14, B.3.15) |
both accept; no warning |
|
nonstandard |
GNU Fortran: accepted by default, errors with |
Cray pointers |
nonstandard |
GNU Fortran needs |
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
gfortranbuild 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,nstepandindex2are integers, and a typo is a new variable. Addimplicit nonebefore editing a routine. -
commonblocks 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-mismatchhides the error that GNU Fortran reports; use it only to build code you cannot change yet. -
real*8meaning "8 bytes". Replace by a named kind, and check that mixed code compiled with-fdefault-real-8still passes arguments of the right size. -
Converting
foralltodo concurrentby 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, declaresaveexplicitly, 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=legacyearly. 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.
See also Best Practices and Style and Common Gotchas and Pitfalls.
References
-
GNU Fortran manual: Options controlling Fortran dialect, Options to request or suppress errors and warnings and Options for code generation conventions
-
GNU Fortran manual: Extensions, Cray pointers, Argument list functions and Hollerith constants support
-
Fortran Package Manager: package manifest reference (the
[fortran]table)