ZetCode

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.

old1.f
      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.

old2.f
      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.

old3.f
      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.

old4.f
      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.

old5.f
      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.

modern.f90
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.

legacy_sub.f
      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.

modern_caller.f90
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

ANSI X3.9-1978: FORTRAN 77

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Fortran tutorials.