Redefining loop variables in old FORTRAN?

This is contrary to my recollection of the change to DO with Fortran 77.

F77 introduced the concept of DO trip count. (I don’t think my memory is that bad !)
Once the trip count is establiched, changes to m2 and m3 have no effect on the trip count.

I do have pre-f77 code that changed m2, which altered the “trip count” It was a loop that processed an increasing list of connected nodes in a tree, that ended when i = the known number of nodes. It was an elegant solution.

The other significant change with F77 DO was that the trip count was established at the start of the DO, while most pre F77 compilers did the test at the end of each DO itteration, effectively as
I = i + m3 ; if ( i*m3 > m2*m3 ) exit

This is how we got the problem about 1-pass DO. I can’t have got this wrong ?

I think it is significant to remember how Fortran 77 broke many old FORTRAN codes, which was a big issue at the time.
FTN5 is just another example of the many / most / all Fortran compilers of the time that did not conform to the current Fortran standard.

It would have been an elegant solution if this were allowed, but the fortran standard did not allow m2 to be changed within the loop body (or within the extended loop range).

As discussed upthread, if the compiler documented the behavior of modifying m2, then it was an extension of the fortran standard by that compiler. If it was not documented, then the observed behavior was just a quirk of the specific compiler (and the current compiler options, optimization levels, etc.). A compiler also could have detected the change of m2 and printed an error message. I expect that some compilers did that, but I don’t remember that with the compilers I used back then.

Because the do parameters could not be changed within the do body in pre-f77, the number of iterations was established at the beginning of the loop, just as in f77 and later. In f66, all three do parameters were required to be positive, and m1<=m2 was also required, so every valid do loop was executed at least once. It was the programmer’s responsibility to branch around the do loop if those requirements were not satisfied. By placing that responsibility on the programmer, the compiler could itself eliminate any tests on the loop variables at the beginning of the loop and thereby reduce the loop overhead. The test at the end of the loop after the loop index was incremented could be the equivalent of simply if(i>m2)exit. It was f77 (and later) that defined the behavior for nonpositive loop control variables and when the trip count was nonpositive. All of those things would have been nonconforming prior to f77. The “problem” with one-pass do loops was because programmers had written nonstandard code (i.e. they did not test and branch around the body of the loop as was required) and the pre-f77 compilers either did not themselves test for violations of the standard, or they allowed such violations intentionally as an extension to the standard.

As far as I can determine, the only codes that were “broken” by f77 do loops were those that were nonconforming (to f66) to begin with.

Compilers of that era did support many extensions. One purpose for those extensions, especially those unique to only one compiler vendor, was to lock in customers and make it difficult to switch hardware and/or software in future purchase cycles.

2 Likes

Ron, your claim is patently false.
If this were the case we would never have needed compiler options like:

  • ifort’s /f66 option for" DO loops are always executed at least once" or
  • FTN95’s /DO1 for “Causes DO loops to be executed at least once to allow certain Fortran 66 programs to execute correctly.”

For all pre-f77 compilers I used, the DO loop exit status was evaluated at the end of each DO itteration. (perhaps IBM was different, as it was in many other cases)

To conform with the F77+ DO rules, compiler developers have been continuing to improve identification of changes to m1, m2, m3 and importantly “i”, the DO variable. Most compilers of F77 or earlier had very poor error identification, so dod not exclude changes to these variables.

To begin, there would have been very few useful programs written that conformed to the f66 Standard, as you needed extensions to the standard to achieve a practical solution. Very few programers were aware of a f66 standard or it’s limitations.
Look at memory management (not available until f90) or file management (some in f77 then f90+) on CDC, Pr1me or Vax; all these required extended Fortran to be productive.
The limitations of the f66 AND f77 standards is hard to comprehend today. The use of f77 “wrapper” coding is an enduring example of extending the standard.

Ron, your claims about how DO loops were used and managed pre f77 are contary to my experience. Perhaps you never used any of the compilers I have referred to.

If anyone has experience of DO usage pre f77, I would be interested in your recollections.

From the FORTRAN 66 standard:

image

1 Like

@RonShepard is correct. I used both FTN and FTN5 extensively back in the day.
Both conformed to their respective standards, in the sense that a Standard-conforming program would work.

Your example which modified m2 during loop execution was not legal Fortran 66. Doesn’t matter if the compiler detected it at compile time or not. Still not legal. Fortran 77 decided the matter. But in a different way than your example was written to assume.

I agree with this too. Many/most programmers of the day, often self-taught, just picked up a textbook like McCracken, and went from there. If something dubious seemed to “work”, they just moved on.

It was a real revelation to me the first time I used FTN’s ANSI flagger on the code I was working in the late 1970s. (~50,000 lines of Finite Element Analysis) My lowly grasshopper task was to eliminate as many of the non-Standardisms as practical. I mostly accomplished it. But there were a few cases where the lead developer refused - because it made his code less readable. (His code was very cleanly written. I consider him my first real mentor in my career.) In particular with the F66 array subscript limitations, which were relaxed in F77.

Yes, I did use many pre-f77 compilers from a variety of vendors in the 1970s and early 1980s. But I somehow knew about the standard requirement on the do loop parameters, and I always tried to write standard conforming code in this respect. By that, I mean that I wrote loops that always has positive m1, m2, and m3 values and with m1.le.m2. If those conditions were not met, then I manually branched around the do loop.

Of course, I inherited code from others that did not do this, so I was also aware of the pitfalls.

As to how I knew about the f66 requirements, it was not because I had a copy of the f66 standard at that time. Rather it was probably due to the convention that many vendors adopted in their printed user manuals that extensions were highlighted somehow. For example, IBM fortran manuals were pretty good about shading the paragraphs that described extensions. I think DEC manuals also used shading. I think UNIVAC manuals used vertical lines in the margins next to the extensions.

I think the only difference in our experience was that you embraced the extensions allowing m2 to be changed or allowing m2<m1, while I avoided them.

When I started using f77 compilers, my most common change regarding do loops was to remove the now-redundant tests surrounding the do loops in order to speed up my programs.

I agree with your comments about almost no fortran programs fully conforming to the f66, and later the f77, standards. There was just too much missing from those standards to write useful programs with just the documented features. Any time you needed a command line argument, or the date and time, or a file characteristic, or used “complex*16” or “double precision complex”, or did some bit-level manipulations, you had to go outside of the standard language and its standard intrinsics. So all of my comments in this thread about the do loop variables in standard f66 have been meant to apply to just that feature.

The F77 standard cleaned up many inconsistencies between compilers, at a time when portability was a significant problem. When porting old FORTRAN, especially pre-f77, you need to be aware of the diversity of compiler fortrans.
Another change with F77 DO was that the DO variable “i” had a defined value on exit from the loop, so that “i” could be tested to help identify the exit status. Previously the value of “i” on exit was “undefined” and did vary between compilers.

I did a lot of conversions of structural engineering codes, mainly between CDC, Pr1me and PC, but was reluctant to attempt codes developed on DEC/Vax (for their extensive list of extensions) , IBM (for their lack of extensions often replaced by JCL that was not supplied) or ICL compilers (with many different atypical extensions and word lengths ).
We should also mention that pre-F77 character variables were Holerith strings and typically stored in long reals, which was always a mess to port.

It was always an interesting experience to learn how other groups extended their use of a language standard that did not keep up with the user requirements.
FORTRAN had been in use for over 35 years before ALLOCATE (and automatic arrays) was part of the standard and provided a cleaner way of allocating memory. The standard still doesn’t provide a way of identifying the available memory resource, which can be so important in choosing a solution approach for a real problem.

I make some differences between the extensions. Some of them were really game changers. To take the examples from @RonShepard, if you needed some bit manipulations, command-line arguments, etc, you had no choice other than using extensions, or interfacing with C, which was not standardized either (the interfacing). Also things like complex*16 or dynamic allocation (Cray pointers or other) could considerably simplify and/or extend the possiblities of the codes. In contrast, not honoring the m2.le.m3 constraint in F66 was just saving a test around the loop.

Any use of non-standard features should be questioned by balancing the advantages (that can vary) and the drawbacks (portability issues, compatibility with future revisions of the standard).

In the FEA code I mentioned a few posts up, we were dogmatic about always using integers to hold characters. Using reals could lead to accidental “normalization” of the characters… We also only used 4 characters per integer max - unpacking and repacking as necessary. Pfort argued for only a single character per integer. But this was deemed wasteful on the small memory machines we were developing on (primarily Data General Eclipse.)

But back on loop variables. A naive compiler that only analysed individual statements, one at a time, would not necessarily notice the illegal modification of the variables. Detecting such a problem would require carrying information about the loop around while compiling the remainder of it - including possible extended ranges. An optimizing compiler might be able to do a fairly complete job. But even then it would be possible to evade checking via their presence in common blocks or as procedure call arguments.

Fortran 77 mitigated the problem quite a bit by only using m1/m2/m3 for computing the trip count up front. Also by eliminating extended ranges. That just left the control variable itself subject to possible misuse, and is where we are today. Compilers, including gfortran, diagnose this when they can.

One final clarification on this topic of old (pre-f77) FORTRAN.
It is important to understand how these pre-f77 DO loops were used when giving life back to these old codes.
My impression was the changes to DO loops with F77 were very welcomed as it imposed clear rules that were better understood and so improved their usage.

The big disappointment with F77 was that some important changes to FORTRAN had to wait to F88/F90/95, which I suspect was mainly due to hardware manufacurer push back.

I have complained that the Fortran standard does not support hardware features, but perhaps considering how F77 evolved, this may be a difficult route.

Certainly, the change from a 32-bit to a 64-bit environment provides very different needs for hardware interface routines. The implementation of SIZE (but not LOC) has demonstrated some standardising problems, especially without an appropriate integer type.
What available memory means has changed considerably over the years for virtual vs physical vs cache memory; and now with multiple variable processors with p-cores vs e-cores; what will the future hardware information needs be ?

In the codes that I remember, extended range do loops were used basically as internal procedures. There was the loop body itself, which might have been a few statements that expressed the basic iterative or stepwise algorithm, then there was the goto that branched outside the loop, then there were the statements that expressed some function evaluation, then there was either a goto or an assigned goto that returned to the loop body. When written in this way, the two blocks of code were separated, which provided some clarity to the two separate parts of the overall algorithm, without requiring an expensive external subroutine call or a push/pop of the stack frame. Also, all local variables were available to both the loop body and to the “internal procedure.” Using the assigned goto from the extended range allowed the same block of code (i.e. the same “internal procedure”) to be executed from several places within the code, even from several separate do loops.

This functionality was not replaced with f77, it would be f90, with contained procedures, where this functionality was really replaced with a modern construct.

The whole 64-bit address space with default integers set to 32 bits has been a problem for a long time. Supporting longer integer kinds in the intrinsics (and in ISO_FORTRAN_ENV) helps a lot. But the language hasn’t helped in all ways it could - in particular, silently allowing “narrowing” of integer data.

It has always miffed me that Fortran 66 and 77 seemed to learn little from the ALGOL derived languages. Think about how much memory was wasted by statically allocating local variables in procedures. Having some block constructs with variables local to them, and automatic sized arrays, would have saved a lot of memory. Also would have saved some of us a lot of headaches with overlaying large codes into small memories. (On the PDP-11/70, even with heavy overlaying, I couldn’t get our FEA codes to fit. Finally invoked the option to split code/data into separate address spaces and it worked.)

Yet there were those who steadfastly insisted that Fortran eschew any feature which required a stack or other form of dynamic memory allocation. We didn’t get automatic arrays until Fortran 90, nor a generalized BLOCK construct until F2008. The latter nearly 50 years after the publication of ALGOL-60!

What do you mean?

I may be wrong, but I think it has never been mandated by the standard. It was more a choice made by the compiler writers.

A common mantra in these times was “Once a program has started, I want to be sure that it won’t abort 10h later because it ran out of memory”. Having everything static was a way to achieve that. Even when pointers appeared as extensions (e.g. Cray Pointers), it was still a very common practice to allocate everything needed right at the beginning of the programs.

program li
  use iso_fortran_env
  implicit none

  integer(kind=int64) :: i64
  integer :: i

  i64 = 123456789012_int64
  i = i64  ! Silently narrows data
  print *, i

end program

When I compile the above, I get no errors, and the answer is returned as -1097262572. Though to gfortran’s credit, if I compile with -Wall, I do get a warning message.

Stack allocation was “allowed”, but array sizes ultimately still had to be fixed length. There were a couple of Fortran 77 compilers which did implement stack allocation. (Two come to mind: Burroughs was one. A third party PDP-11 compiler was another.) Didn’t really take off until multiprocessing - e.g., when Cray introduced the X-MP system and multi-tasking. CFT as of around V1.10 or V1.11 offered a stack allocation option. The CFT77 guys wanted to do stack allocation by default, but there was some push-back. So it didn’t happen until several years later.

Are you expecting an error because the RHS has a larger range than the LHS, or because that particular value cannot be represented on the LHS?

If it is the former, then what would you expect the equivalent explicit expression

i = int(i64,kind=kind(i))

to do? The same thing, or something different?

I’m unsure why, but it is tradition (in fortran an in many other languages) that integer overflows usually do not generate any kind of warning or error. Many algorithms (e.g. linear congruence generators) more or less depend on such overflows to be silent.

This is usually described by saying that f77 could be implemented entirely with static allocation. Even with overlay linkers, that was still the model on which the language was based. Some compilers did use stack allocation for some variables, particularly if they supported recursion as an extension. Recursion was allowed in f90 (by specifying the attribute), and it became the default in (I think) f2008.

It really depends on the context of how the integers are used. The context I (and I think @JohnCampbell ) referred to is with lower/upper bounds and sizes of arrays. Also character string lengths. Undiagnosed overflows are definitely not very friendly.

Some languages, such as java, require some form of casting to do narrowing. This is similar to what you wrote above, and tells the reader that “yes, I know…”. C has historically had a lot of problems with this too. Hence the need for size_t and the like in more modern dialects.

MPI, in v4.0, finally fixed their problem by introducing MPI_COUNT_KIND, MPI_ADDRESS_KIND, and MPI_OFFSET_KIND integer kinds with the Fortran 2008 bindings.

All pre-f77 compilers I used had only static allocation, but f77 compilers introduced dynamic allocation, which I thought was an improvemnent
With the “old FORTRAN” compilers I used, we had either blank common or “common /junk/” as a pseudo stack to mitigate memory wastage with local static variables or (unfortunately) could be to save on dummy arguments. (another coding style to be identified in legacy codes, with many hidden gotcha tricks)

And @wspector mentions overlays ! Those were dark days trying to squeeze code using plink(?). It took as much time to finesse as it did Lahey to provide extended/enhanced memory. Not everything was the “Good ol’ days”

Fortran has had a few misses with 64-bit OS, especially without a default integer kind for memory address, especially for (what should be) SIZE and LOC intrinsics. I don’t understand why these could not have been provided in a workable form. ( also failed in 32-bit with 3GB )

In practice, C or Fortran compilers have had 64 bits integers for decades. If you needed them you had them.

64-bit integers were available only if the programmer used a nonstandard syntax to declare a nonstandard type, and 32-bit unsigned integers are still not available, even now. One can argue whether that was really a solution to the problem. At least in the early 1980s, I think most people tolerated this situation because they thought there would be a new standard revision in 1983 or 1984 that would address that issue. But that revision got pushed back to 1988, and then 8x, and then it finally came in the early 1990s with just the syntax part for 64-bit integers, not the requirement for the data type itself (which would eventually come in f2008) which guarantees the portability within the language. I don’t think the kind= argument to the size() intrinsic was added until f2003 (right?). So this has been a long saga to get to where we are now.