Making FORTRAN 77 easier to read

FORTRAN is an old language. The first version of FORTRAN appeared in the mid 1950s, when programs were entered into the computer using punched cards. These punched cards are a relic today, but they were a common way to input data and programs into computers. Punched cards could contain 80 columns of data, consisting of the uppercase letters, numbers, and a small set of symbols like * and - and /.

But you couldn’t use all 80 columns; computers reserved columns 73 to 80 for card ordering. Cards would come pre-punched with sequential numbers in those last 7 columns. The idea was that if you dropped a stack of program cards (and yes, that happened) then you could feed the cards into a special card sorting machine and get your cards back in some kind of order.

FORTRAN also reserved the first column to indicate if the card was a comment line. To indicate a comment, put a * or C in this first column. Later versions of FORTRAN also allowed lowercase letters, so c might also start a comment line. If the card wasn’t a comment line, then columns 1 to 5 were reserved for labels (which look like line numbers, but they are just labels so you can refer to a line by a number).

That left columns 6 to 72, which isn’t a lot of room to write a program if you assume that a program instruction is just one per card. So FORTRAN also reserved column 6 for a continuation marker. If you put a marker (usually + or $ or a number, but most compilers could assume any non-space character) in column 6, then that card just continued from the card before it. Programming instructions went in columns 7 to 72.

A simple pretty printer

All of these special columns can make it confusing to read old FORTRAN code. To make this a bit easier to read, many programmers processed their code through a “pretty printer” to help statements and lines stand out. A simple pretty printer can highlight comment lines and indicate continuation lines, and de-emphasize or hide the extra stuff in columns 72 to 80. A more complete pretty printer would also provide extra formatting for key words in the FORTRAN language, like PROGRAM or IF or END IF, and other formatting for numbers, strings, and inline comments.

Let’s see how a pretty printer works by writing a simple implementation to generate pretty printed FORTRAN code as HTML. To do this, we can write a trivial parser in C that reads a FORTRAN 77 program one character at a time, and keeps track of the column. In this version, we’ll just examine the special columns to determine what kind of line this might be:

  • * or C or c in column 1 means it is a comment line
  • any non-space character in column 6 (for a normal programming line) indicates a continuation
  • extra stuff starting at column 73 will get crossed out

We can start with this basic function definition, which we’ll add to:

void pretty77(FILE *in, FILE *out)
{
    enum { Normal, Comment, Continue, Extra } linetype;

    char c;
    int col = 1;

    while ((c = fgetc(in)) != EOF) {
...
    }

}

Detecting special columns

The first thing this function needs to do is make determinations about the line based on the column. As a starting point, this function should look for a comment marker in column 1:

    while ((c = fgetc(in)) != EOF) {
        /* special columns */

        if (col == 1) {
            if ((c == '*') || (c == 'C') || (c == 'c')) {
                fputs(COMM_START, out);
                linetype = Comment;
            }
            else {
                linetype = Normal;
            }
        }
...

    }

This looks only for * or C or c in the first column. If so, then the function assumes the line is a comment line, and prints a string to start a comment. I’ve used a placeholder value here called COMM_START which allows me to define what HTML tag I want to use for comments.

If the line doesn’t start with a comment marker, then it’s assumed to be just a normal program line.

We can extend this if block to also detect continuation markers in column 6. While FORTRAN 77 allows a limited set of characters in column 6 as a continuation marker, let’s make a simple assumption that any non-blank character indicates a continuation line:

    while ((c = fgetc(in)) != EOF) {
        /* special columns */

        if (col == 1) {
            if ((c == '*') || (c == 'C') || (c == 'c')) {
                fputs(COMM_START, out);
                linetype = Comment;
            }
            else {
                linetype = Normal;
            }
        }
        else if (col == 6) {
            if ((linetype == Normal) && (c != ' ')) {
                fputs(CONT_START, out);
                linetype = Continue;
            }
        }
...

    }

This extra code evaluates if the line is a normal line, and only then looks for a non-blank character in column 6. We need to limit this to normal lines only, because comment lines could contain anything, and the entire line in a comment should be ignored anyway.

I’ve used another placeholder value to start the continuation code, called CONT_START, so I can later go back to define what HTML tag I want to use for continuations.

The rest of the if block evaluates column 73 and beyond. Any data starting at column 73 should be ignored in FORTRAN 77, so should be highlighted as such. To make this evaluation, I think it’s easier to “end” the evaluation of a normal program statement or continuation line before highlighting the extra stuff. In this case, we haven’t given special formatting for normal program lines, so we can just evaluate at column 73 if this is a continuation line, end it, then add an HTML tag to indicate the extra stuff to be ignored:

    while ((c = fgetc(in)) != EOF) {
        /* special columns */

        if (col == 1) {
            if ((c == '*') || (c == 'C') || (c == 'c')) {
                fputs(COMM_START, out);
                linetype = Comment;
            }
            else {
                linetype = Normal;
            }
        }
        else if (col == 6) {
            if ((linetype == Normal) && (c != ' ')) {
                fputs(CONT_START, out);
                linetype = Continue;
            }
        }
        else if (col == 73) {
            if (linetype == Continue) {
                fputs(CONT_END, out);
                linetype = Normal;
            }

            if (linetype == Normal) {
                fputs(XTRA_START, out);
                linetype = Extra;
            }
        }
...

    }

Reaching the end of a line

In HTML, if you open a tag, you need to close it when you’re done. This pretty printer evaluates FORTRAN code line-by-line, the same way a FORTRAN 77 compiler would. That means we need to close any open HTML tags whenever we reach the end of a line.

The program only adds formatting for comment lines, continuation lines, and “extra” stuff, so we can add code that detects if the input character is a newline, then closes out the HTML tags. We could do this with a switch block, but an if block is just as easy to do, and easier to read:

    while ((c = fgetc(in)) != EOF) {
        /* special columns */

        if (col == 1) {
            if ((c == '*') || (c == 'C') || (c == 'c')) {
                fputs(COMM_START, out);
                linetype = Comment;
            }
            else {
                linetype = Normal;
            }
        }
        else if (col == 6) {
            if ((linetype == Normal) && (c != ' ')) {
                fputs(CONT_START, out);
                linetype = Continue;
            }
        }
        else if (col == 73) {
            if (linetype == Continue) {
                fputs(CONT_END, out);
                linetype = Normal;
            }

            if (linetype == Normal) {
                fputs(XTRA_START, out);
                linetype = Extra;
            }
        }

        /* end the line */

        if (c == '\n') {
            if (linetype == Comment) {
                fputs(COMM_END, out);
            }
            else if (linetype == Continue) {
                fputs(CONT_END, out);
            }
            else if (linetype == Extra) {
                fputs(XTRA_END, out);
            }

            col = 1;
        }
        else if (col < 80) {
            col++;
        }
...

    }

At the end of each line, and after closing the HTML tags the program resets the column counter to 1, ready to start again on the next line. Again, I’ve used placeholders with COMM_END and CONT_END and XTRA_END so I can define those later.

As part of this if block, the program also increments the column counter. I don’t really care about columns beyond column 73, but I’ve used column 80 as the cutoff for this if test because old-style punched cards shouldn’t be longer than 80 columns anyway.

Printing the output

Now that the loop has detected special conditions like columns and the end of the line, the loop can finish by printing the character to the output. This is the core part of the pretty77 function, which we can now write in its final form:

#define CODE_START "<pre><code>"
#define CODE_END   "</code></pre>"

#define COMM_START "<i>"
#define COMM_END   "</i>"

#define CONT_START "<ins>"
#define CONT_END   "</ins>"

#define XTRA_START "<del>"
#define XTRA_END   "</del>"

void pretty77(FILE *in, FILE *out)
{
    enum { Normal, Comment, Continue, Extra } linetype;

    char c;
    int col = 1;

    fputs(CODE_START, out);

    while ((c = fgetc(in)) != EOF) {
        /* special columns */

        if (col == 1) {
            if ((c == '*') || (c == 'C') || (c == 'c')) {
                fputs(COMM_START, out);
                linetype = Comment;
            }
            else {
                linetype = Normal;
            }
        }
        else if (col == 6) {
            if ((linetype == Normal) && (c != ' ')) {
                fputs(CONT_START, out);
                linetype = Continue;
            }
        }
        else if (col == 73) {
            if (linetype == Continue) {
                fputs(CONT_END, out);
                linetype = Normal;
            }

            if (linetype == Normal) {
                fputs(XTRA_START, out);
                linetype = Extra;
            }
        }

        /* end the line */

        if (c == '\n') {
            if (linetype == Comment) {
                fputs(COMM_END, out);
            }
            else if (linetype == Continue) {
                fputs(CONT_END, out);
            }
            else if (linetype == Extra) {
                fputs(XTRA_END, out);
            }

            col = 1;
        }
        else if (col < 80) {
            col++;
        }

        /* print the character */

        fputc(c, out);
    }

    fputs(CODE_END, out);
    fputc('\n', out);
}

You may notice that this version also defines the placeholder tags: Before any code is evaluated, the function starts the block with <pre> to define a preformatted section in HTML, and <code> to indicate source code. Comment lines use <i> to format them with italic text, continuation lines use <ins> to suggest inserted text, and extra content at column 73 and beyond use <del> to indicate deleted text which will be ignored by the compiler anyway. At the end, the function closes the preformatted code block with </code> and </pre>.

Putting it all together

The pretty77 function knows how to read its input and where to print its output because we provide it with in and out file pointers. This makes the function more flexible, because we can open any FORTRAN 77 file then use the pretty77 function to read it. At the same time, we can direct the pretty77 function to save its output to a new file, such as an HTML document or print it to the screen.

With this function, we can write a fairly simple main function that processes a list of FORTRAN 77 source files as arguments, and uses pretty77 to format them.’ In the simplest case, we can just print the output to the screen by giving stdout as the output pointer; if the user wants to save the pretty printed code to a new file, they can redirect it to a file on their own.

#include <stdio.h>

#define CODE_START "<pre><code>"
#define CODE_END   "</code></pre>"

#define COMM_START "<i>"
#define COMM_END   "</i>"

#define CONT_START "<ins>"
#define CONT_END   "</ins>"

#define XTRA_START "<del>"
#define XTRA_END   "</del>"

void pretty77(FILE *in, FILE *out)
{
    enum { Normal, Comment, Continue, Extra } linetype;

    char c;
    int col = 1;

    fputs(CODE_START, out);

    while ((c = fgetc(in)) != EOF) {
        /* special columns */

        if (col == 1) {
            if ((c == '*') || (c == 'C') || (c == 'c')) {
                fputs(COMM_START, out);
                linetype = Comment;
            }
            else {
                linetype = Normal;
            }
        }
        else if (col == 6) {
            if ((linetype == Normal) && (c != ' ')) {
                fputs(CONT_START, out);
                linetype = Continue;
            }
        }
        else if (col == 73) {
            if (linetype == Continue) {
                fputs(CONT_END, out);
                linetype = Normal;
            }

            if (linetype == Normal) {
                fputs(XTRA_START, out);
                linetype = Extra;
            }
        }

        /* end the line */

        if (c == '\n') {
            if (linetype == Comment) {
                fputs(COMM_END, out);
            }
            else if (linetype == Continue) {
                fputs(CONT_END, out);
            }
            else if (linetype == Extra) {
                fputs(XTRA_END, out);
            }

            col = 1;
        }
        else if (col < 80) {
            col++;
        }

        /* print the character */

        fputc(c, out);
    }

    fputs(CODE_END, out);
    fputc('\n', out);
}

int main(int argc, char **argv)
{
    for (int i = 1; i < argc; i++) {
        FILE *in = fopen(argv[i], "r");

        if (in == NULL) {
            fputs("cannot open file: ", stderr);
            fputs(argv[i], stderr);
            fputc('\n', stderr);
        }
        else {
            pretty77(in, stdout);
            fclose(in);
        }
    }

    if (argc == 1) {
        pretty77(stdin, stdout);
    }

    return 0;
}

Save this as pretty.c and compile it with your favorite C compiler, such as the GNU C Compiler. This doesn’t use any libraries, so you don’t need to specify any special flags for the compiler—but I like to use the -Wall option to print any warnings. (Fortunately, I’ve written good code here, so I shouldn’t see any warnings.)

$ gcc -Wall -o pretty77 pretty77.c 

In most cases, you would list one or more FORTRAN 77 files on the command line, and redirect the output to a file:

$ pretty77 file.f > file.html

The program also manages the special case of no input files. If the user doesn’t specify a FORTRAN 77 source file on the command line, the program will instead read from standard input and print to standard output. This allows the program to be used as a “filter” if needed, such as if you need to use the expand command to convert tabs to spaces in the FORTRAN 77 source file:

$ expand file.f | pretty77 > file.html

Let’s look at a few examples to see the new program in action. The most trivial program prints “Hello” to the terminal. This is just a few lines long:

$ ./pretty77 hello.f  
<pre><code>      PROGRAM HELLO
      PRINT *, 'Hello world'
      END
</code></pre>

This generates a very plain FORTRAN 77 source file, without any noticeable formatting:

      PROGRAM HELLO
      PRINT *, 'Hello world'
      END

A not-too-complicated program is the classic “guess the number” program, where the user must try to guess a secret number between 1 and 100. At each guess, the program gives hints like “too low” or “too high” so the user can make a better guess:

$ ./pretty77 numgame.f  
<pre><code>      PROGRAM NUMGAME
      INTEGER NUMBER, GUESS, SEED

<i>C FOR RANDOM NUMBERS (GNU GFORTRAN ON LINUX) YOU CAN USE RAND(SEED)</i>
<i>C THE FIRST TIME, AND THAT WILL SEED THE GENERATOR, AND RAND(0)</i>
<i>C AFTER THAT.</i>
<i>C OR, YOU CAN 'CALL SRAND(SEED)' AND THEN 'RAND(0)'</i>

<i>C ON OPENWATCOM (DOS) I THINK IT'S THE SAME BUT WITH URAND(SEED)</i>
<i>C AND URAND(0).</i>

      PRINT *, 'ENTER A RANDOM NUMBER SEED:'
      READ *, SEED

      NUMBER = INT(RAND(SEED) * 100) + 1

      PRINT *, 'GUESS MY SECRET NUMBER (FROM 1 TO 100)'

 10   READ *, GUESS

      IF (GUESS.LT.NUMBER) PRINT *, 'TOO LOW'
      IF (GUESS.GT.NUMBER) PRINT *, 'TOO HIGH'
      IF (GUESS.NE.NUMBER) GOTO 10

      PRINT *, 'THAT''S RIGHT!'
      END
</code></pre>

This is a fairly straightforward program that doesn’t use continuation lines, so the output only formats the comment lines as italic text:

      PROGRAM NUMGAME
      INTEGER NUMBER, GUESS, SEED

C FOR RANDOM NUMBERS (GNU GFORTRAN ON LINUX) YOU CAN USE RAND(SEED)
C THE FIRST TIME, AND THAT WILL SEED THE GENERATOR, AND RAND(0)
C AFTER THAT.
C OR, YOU CAN 'CALL SRAND(SEED)' AND THEN 'RAND(0)'

C ON OPENWATCOM (DOS) I THINK IT'S THE SAME BUT WITH URAND(SEED)
C AND URAND(0).

      PRINT *, 'ENTER A RANDOM NUMBER SEED:'
      READ *, SEED

      NUMBER = INT(RAND(SEED) * 100) + 1

      PRINT *, 'GUESS MY SECRET NUMBER (FROM 1 TO 100)'

 10   READ *, GUESS

      IF (GUESS.LT.NUMBER) PRINT *, 'TOO LOW'
      IF (GUESS.GT.NUMBER) PRINT *, 'TOO HIGH'
      IF (GUESS.NE.NUMBER) GOTO 10

      PRINT *, 'THAT''S RIGHT!'
      END

I also wrote a special FORTRAN 77 program designed to test various “corner” cases of the pretty printer. This program just prints the numbers from 1 to 10, but it also includes a continuation line to provide the loop increment (which is 1). This also includes comment lines that start with * and C and c. One comment line includes some extra characters at the end, so it goes beyond column 80. Several lines also include card ordering values in columns 73 to 80:

$ ./pretty77 count.f  
<pre><code><i>* START A LINE WITH C OR * TO MAKE A COMMENT</i>
<i>C THIS IS A COMMENT</i>
<i>c this is also a comment in lowercase ------------------------------------------</i>

      PROGRAM COUNT                                                     <del>8675309</del>
      INTEGER I                                                         <del>8675310</del>
<i>* COUNT FROM 1 TO 10</i>
<i>c this is actually one statement split across two lines:</i>
      DO 10 I = 1,10                                                    <del>8675311</del>
     <ins>$,1</ins>
10    PRINT *, I                                                        <del>8675312</del>
      END
</code></pre>

This shows all of the formatting that the pretty printer can do, including comments, continuation lines, and extra text starting at column 73:

* START A LINE WITH C OR * TO MAKE A COMMENT
C THIS IS A COMMENT
c this is also a comment in lowercase ------------------------------------------

      PROGRAM COUNT                                                     8675309
      INTEGER I                                                         8675310
* COUNT FROM 1 TO 10
c this is actually one statement split across two lines:
      DO 10 I = 1,10                                                    8675311
     $,1
10    PRINT *, I                                                        8675312
      END

Making FORTRAN 77 easier to read

This pretty printer generates HTML code, but it’s not a fully-formed HTML document. You can include the HTML output in another HTML document, which might also link to a stylesheet to provide special styling. For example, you could define a style that puts comment lines in a different color, such as gray. By default, most browsers should format <del> as “strike through” text, and <ins> with an underline, but you might also update these styles to use other colors or highlighting.

The special columns in FORTRAN 77 can make it hard to see what’s going on in the code. If you’re getting started with FORTRAN, add this pretty printer to your programming toolkit to make your old-style FORTRAN easier to read.

Leave a Reply