FORTRAN 77 and legacy code
last modified November 14, 2026
Fortran is the oldest high-level language still in daily use, and a large amount of it was written before 1990. That code is in fixed form: the first six columns of a line are reserved for statement labels, column seven marks a continuation, and only columns eight to seventy-two hold the statement. The language of that era also has implicit typing, jumps to labels, shared storage blocks and a few constructs that later standards removed. Reading it is a practical skill, because numerical libraries and old projects are full of it.
This article writes FORTRAN 77 in its own layout: a table with a labelled loop
and a FORMAT statement, implicit typing and its integer division,
an arithmetic IF and a computed GOTO, a
COMMON block with EQUIVALENCE and DATA,
and a Hollerith constant. It then shows which of these the standards still
accept, rewrites the first program in modern Fortran, and calls a fixed-form
subroutine from a free-form program, which is how old code is modernised in
practice.
Fixed form and its columns
A fixed-form source file has one statement per line, and the position of the
text matters. Columns one to five may hold a statement label, column six is
blank or holds any other character when the line continues the previous one,
and the statement itself begins in column seven. Columns seventy-three to eighty
are ignored, which is why old code rarely reaches the right margin. The file
name ends in .f so that the compiler reads it in this form, and
gfortran has done so by default for decades.
PROGRAM TABLE
INTEGER I
DO 10 I = 1, 5
WRITE (*, 20) I,
& I*I, I*I*I
10 CONTINUE
20 FORMAT (I5, 2I8)
STOP
END
The DO 10 I = 1, 5 runs to the statement labelled 10, which is the
CONTINUE at the end of the body, and the loop shares the label
with the WRITE format below it. The WRITE statement is
broken after the comma, and the continuation line carries an ampersand in column
six to say so. FORMAT describes the layout of the record:
I5 for the first integer and 2I8 for the other two.
$ gfortran -std=legacy -Wall -Wextra old1.f -o old1
$ ./old1
1 1 1
2 4 8
3 9 27
4 16 64
5 25 125
Implicit typing
Without declarations, a variable's type follows its first letter: names
beginning with I through N are integers and every
other name is a real. The program below never declares a thing, and still
computes, because ICOUNT is an integer and TOTAL and
AVERAGE are reals. The rule also explains why so much old code
calls its loop counters I, J and K, and
why a name like KMAX is an integer while MAXK is a
real.
PROGRAM IMPLICIT
ICOUNT = 3
TOTAL = 10.0
AVERAGE = TOTAL / ICOUNT
WRITE (*, 10) ICOUNT, TOTAL, AVERAGE
WRITE (*, 20) ICOUNT / 2, TOTAL / 2.0
10 FORMAT ('COUNT ', I3, ' TOTAL ', F6.1, ' AVERAGE ', F6.2)
20 FORMAT ('HALVES ', I3, 1X, F5.1)
END
The average is 3.33, the real division of 10.0 by 3, while the second line
shows the trap of the same program: ICOUNT / 2 divides integers and
gives 1, not 1.5, because the fractional part is discarded before the value is
converted for printing. A typo in a name silently creates a new variable of the
type its first letter implies, which is the reason every modern program starts
with implicit none.
$ gfortran -std=legacy -Wall -Wextra old2.f -o old2 $ ./old2 COUNT 3 TOTAL 10.0 AVERAGE 3.33 HALVES 1 5.0
Arithmetic IF and computed GOTO
Before the block IF, Fortran branched with a three-way statement:
IF (expression) label1, label2, label3 goes to the first label when
the value is negative, the second when it is zero and the third when it is
positive. The computed GOTO selects a label from a list by index.
Both take the reader to a label somewhere else in the program, and both are the
reason old code has so many numbered statements.
PROGRAM CHOOSE
INTEGER N
READ (*, *) N
IF (N) 10, 20, 30
10 WRITE (*, *) 'NEGATIVE'
GO TO 40
20 WRITE (*, *) 'ZERO'
GO TO 40
30 WRITE (*, *) 'POSITIVE'
40 CONTINUE
GO TO (50, 60, 70), N
50 WRITE (*, *) 'ONE'
GO TO 80
60 WRITE (*, *) 'TWO'
GO TO 80
70 WRITE (*, *) 'THREE'
80 CONTINUE
END
The program reads a number and prints its sign, then jumps again on the same
number to print its name for the values one to three. The second run with 2
prints POSITIVE and TWO, and the third with 0 prints
ZERO and then ONE, because an index outside one to
three makes the computed GOTO continue with the next statement,
which here is the first branch. In modern Fortran the same logic is a select case
with a case default, which the conditionals article covers.
$ gfortran -std=legacy -Wall -Wextra old3.f -o old3 $ echo 5 | ./old3 POSITIVE ONE $ echo 2 | ./old3 POSITIVE TWO $ echo 0 | ./old3 ZERO ONE
COMMON, EQUIVALENCE and DATA
A COMMON block is a named area of memory that every program unit
declaring the same name shares. EQUIVALENCE makes two variables
occupy the same storage, and DATA gives initial values at compile
time. The three together were the only way to pass data between routines without
arguments, and they are still found in old libraries, often as a block of
coefficients declared in a BLOCK DATA subprogram.
PROGRAM COMMONBLOCK
COMMON /BLOCK/ A, B, C
REAL A, B, C, D
DATA A /1.0/, B /2.0/, C /3.0/
EQUIVALENCE (D, B)
D = D + 10.0
WRITE (*, 10) A, B, C
10 FORMAT ('MAIN: ', 3F6.1)
CALL SHOW
END
SUBROUTINE SHOW
COMMON /BLOCK/ X, Y, Z
REAL X, Y, Z
WRITE (*, 20) X, Y, Z
20 FORMAT ('SUB: ', 3F6.1)
END
The main program and the subroutine each declare /BLOCK/ with the
same three reals, so they see the same memory: the value 12.0 that the main
program wrote through D appears as B in both places,
because EQUIVALENCE (D, B) made the two names aliases. The
DATA statement supplied 1.0, 2.0 and 3.0 before the program ran.
The modern replacements are a module for the shared data, a derived type for the
group, and an ordinary assignment for the initial values.
$ gfortran -std=legacy -Wall -Wextra old4.f -o old4 $ ./old4 MAIN: 1.0 12.0 3.0 SUB: 1.0 12.0 3.0
What the standards still accept
gfortran's -std=legacy accepts the whole historical language,
including features that no standard ever contained. -std=f95
accepts only what the 1995 standard allowed and warns about the parts it had
already marked obsolescent, and -std=f2018 rejects what later
revisions deleted. A Hollerith constant, which packs characters into the storage
of a number, is a good test: it was a legacy extension in 1977 and gone from the
standard long before 2018.
PROGRAM HOLLERITH
CHARACTER*4 NAME
DATA NAME /4HFORT/
WRITE (*, 10) NAME
10 FORMAT ('NAME = ', A4)
END
The 4HFORT notation stores four characters in the four bytes of the
variable, a trick that predates the CHARACTER type. The transcript
compiles it with the legacy flag and runs it, then compiles the arithmetic
IF program under the 1995 rule, which accepts it with a warning, and
under the 2018 rule, which rejects the arithmetic IF as a deleted
feature. The computed GOTO is still only obsolescent in 2018, so
gfortran warns about it rather than refusing it.
$ gfortran -std=legacy -Wall -Wextra old5.f -o old5
$ ./old5
NAME = FORT
$ gfortran -std=f95 -Wall old3.f -o old3f95
old3.f:4:72:
4 | IF (N) 10, 20, 30
| 1
Warning: Obsolescent feature: Arithmetic IF statement at (1)
old3.f:11:72:
11 | GO TO (50, 60, 70), N
| 1
Warning: Obsolescent feature: Computed GOTO at (1)
$ gfortran -std=f2018 old3.f -o old3f2018
old3.f:4:72:
4 | IF (N) 10, 20, 30
| 1
Error: Fortran 2018 deleted feature: Arithmetic IF statement at (1)
old3.f:11:72:
11 | GO TO (50, 60, 70), N
| 1
Warning: Obsolescent feature: Computed GOTO at (1)
The same program in modern Fortran
The table from the first program needs no labels, no FORMAT and no
declarations at the top of a routine: a counted do ends at
end do, the format is a string beside the print, and
every variable is declared. The output is identical, which is the point of the
rewrite: modern Fortran is the same language with a safer surface.
program modern
implicit none
integer :: i
do i = 1, 5
print '(i5, 2i8)', i, i * i, i * i * i
end do
end program modern
The four lines of the fixed-form program that carried the loop, the label and
the format have become three lines, and the label 10 has disappeared from the
program entirely. The implicit none makes a misspelled name a
compile error instead of a new real variable, and i5 and
2i8 in the format string are the same descriptors as before, in
lower case.
$ gfortran -std=f2018 -Wall -Wextra -fcheck=all modern.f90 -o modern
$ ./modern
1 1 1
2 4 8
3 9 27
4 16 64
5 25 125
Mixing old and new
Rewriting a library in one go is rarely possible, so the practical route is to compile the old routines as they are and call them from new code. The fixed-form subroutine below scales an array in place, and the free-form program that follows declares an interface for it, which gives the compiler the argument types that the old source cannot express.
SUBROUTINE SCALE(X, N, FACTOR)
REAL X(N)
INTEGER N
REAL FACTOR
INTEGER I
DO 10 I = 1, N
X(I) = X(I) * FACTOR
10 CONTINUE
END
The subroutine uses an adjustable array, REAL X(N), which was the
FORTRAN 77 way of passing an array whose size is an argument, and a labelled
loop. The interface in the caller says the same thing with an assumed-size
declaration and intent, so a mistake in the call is caught at
compile time.
program modern_caller
implicit none
interface
subroutine scale(x, n, factor)
real, intent(inout) :: x(*)
integer, intent(in) :: n
real, intent(in) :: factor
end subroutine scale
end interface
real :: values(4)
values = [1.0, 2.0, 3.0, 4.0]
call scale(values, 4, 2.5)
print '(a, 4f6.1)', 'after the legacy call: ', values
end program modern_caller
The two objects are compiled separately, each with the flag its source needs, and linked into one program. The array arrives as 1, 2, 3, 4, the old routine multiplies every element by 2.5, and the caller prints 2.5, 5.0, 7.5 and 10.0. This is the pattern to follow when a large old code base has to be kept working: wrap it, declare it, and replace one routine at a time.
$ gfortran -std=legacy -Wall -Wextra -c legacy_sub.f -o legacy_sub.o $ gfortran -std=f2018 -Wall -Wextra -fcheck=all modern_caller.f90 legacy_sub.o -o caller $ ./caller after the legacy call: 2.5 5.0 7.5 10.0
Next steps
The program structure article
explains the free-form layout that replaced the columns, the
variables article covers declarations and
implicit none, and the
conditionals article shows the
select case that replaces the computed GOTO. The
modules article describes the replacement for
COMMON, the derived types
article the replacement for EQUIVALENCE, and the
compiling and debugging article
explains the separate compilation used in the last transcript.
Source
Fortran 2018 standard (J3/18-007r1)
GNU Fortran manual: Fortran Dialect Options
In this article we have read and written fixed-form source with its labels and
continuation column, relied on implicit typing and seen integer division catch a
program out, branched with an arithmetic IF and a computed
GOTO, shared memory through COMMON with
EQUIVALENCE and DATA, packed characters into a
Hollerith constant, compiled the same code under the legacy, 1995 and 2018
rules, rewritten the table in modern Fortran, and called a fixed-form
subroutine from a free-form program. The previous article,
Fortran C interoperability, deals with
another kind of foreign code, and the same wrapping technique applies to both.
Author
List all Fortran tutorials.