Common Gotchas and Pitfalls

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 program below was compiled with gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all (GNU Fortran 16.2) and run, and, where the feature is portable, built and run with LLVM Flang 23 (flang-23). Both ran in a Linux (aarch64) container, and the output and diagnostics shown are the real ones. Programs that deliberately fall into a trap say so, show the real diagnostic, and are followed by the corrected version. Where the two compilers disagree, the page says so, because a trap that one compiler hides is the most dangerous kind.

Fortran is a small, regular language, but it has a number of rules that behave differently from C, Python or Java, and a handful of defaults that date from the 1950s. The twelve traps below account for most of the surprises that a programmer meets in the first months. Each section follows the same pattern: the program that falls into the trap, what it prints, why (with the clause of the standard), and the flag, run-time check or coding habit that catches it. The traps are drawn from the heatflow running example (a grid of temperatures relaxed by Jacobi or FTCS sweeps), so the code resembles what is written in practice. The compiler flags and sanitizing options are described in Compilers and Compiler Options, the debugging tools in Testing, Debugging and Profiling, and the habits that avoid the traps in the first place in Best Practices and Style.

The traps at a glance

Trap What happens What catches it Section

An initialized local variable keeps its value between calls

integer :: count = 0 implies save, so the "reset" happens once, at program start

code review; -Wall does not warn

Implicit save on initialized locals

0.1 is a default real

x = 0.1 stores 0.100000001490116…​ in a real(dp) variable

gfortran -Wconversion-extra (not in -Wall), Flang -Wreal-constant-widening

Default real and literal kinds

Integer division truncates toward zero

1 / (nx - 1) is 0 for every nx > 2

gfortran -Winteger-division for constants only; -ftrapv for overflow

Integer division

.and. and .or. do not short-circuit

i <= n .and. t(i) > 0 may read t(n + 1)

gfortran -fcheck=bounds at run time

No short-circuit evaluation

intent(out) throws the old value away

an allocatable dummy is deallocated on entry, default-initialized components are reset

none; the program crashes or silently loses data

intent(out) deallocates and undefines

Array sections passed to explicit-shape dummies are copied

a hidden temporary per call; intent(out) copies back

gfortran -Warray-temporaries, -fcheck=array-temps

Array temporaries and aliasing violations

Aliased arguments break the optimizer’s assumptions

call smooth(a, a) gives a different answer at -O0 and -O2

gfortran warns for the plain case only

Array temporaries and aliasing violations

Character comparison pads with blanks

'ab' == 'ab ' is true

none; read the rule

Character padding in comparisons and assignments

Dummy arguments start at index 1

an assumed-shape dummy loses a 0:n halo bound

none; print lbound

Lower bounds of dummy arguments

Uninitialized variables hold garbage

a local that was never assigned is used

gfortran -Wuninitialized and -Wmaybe-uninitialized, -finit-real=snan -ffpe-trap=invalid

Uninitialized variables

Unit numbers are global

two parts of a program use unit 10, or a write to an unopened unit creates fort.N

newunit=; iostat= and iomsg=

Unit numbers already in use

List-directed output (print *) has no fixed layout

widths and digits differ between compilers

explicit formats

List-directed output formatting

A big automatic array overflows the stack

a segmentation fault on entry to the procedure

ulimit -s; allocatable arrays instead

Stack overflow from large automatic arrays

Implicit save on initialized locals

A variable that is explicitly initialized in its declaration implicitly has the save attribute (8.4: "Explicit initialization of a variable that is not in a common block implies the SAVE attribute"). In C the same declaration, int count = 0;, resets the variable at every call; in Fortran the initialization happens once, before the program starts, and the value survives from call to call. The compiler does not warn, because the program is correct, it just does not do what a C programmer expects.

module sweep_counter
  use, intrinsic :: iso_fortran_env, only: int32
  implicit none
  private
  public :: next_id_wrong, next_id_right
contains
  integer(int32) function next_id_wrong() result(id)
    integer(int32) :: count = 0            ! initialization implies save
    count = count + 1
    id = count
  end function next_id_wrong

  integer(int32) function next_id_right() result(id)
    integer(int32) :: count                ! a fresh local at every call
    count = 0
    count = count + 1
    id = count
  end function next_id_right
end module sweep_counter

program implicit_save
  use sweep_counter, only: next_id_wrong, next_id_right
  implicit none
  integer :: k
  do k = 1, 3
    print '(a, i0, a, i0)', 'call ', k, ': wrong = ', next_id_wrong()
  end do
  do k = 1, 3
    print '(a, i0, a, i0)', 'call ', k, ': right = ', next_id_right()
  end do
end program implicit_save
call 1: wrong = 1
call 2: wrong = 2
call 3: wrong = 3
call 1: right = 1
call 2: right = 1
call 3: right = 1

Both compilers print the same. The corrected function declares the variable and assigns it in an executable statement, which runs at every call. The consequences go beyond counters: a procedure with a saved local is not safe to call from two threads or do concurrent iterations at once, and it cannot be pure, because a local variable of a pure subprogram shall not have the save attribute and initialization implies it (C1598 and its note, 15.7). Variables of the main program, modules and submodules are saved anyway (8.5.16). Default initialization of a derived-type variable is different: the components get their default values each time the variable is created, and the standard says that default initialization, unlike explicit initialization, does not imply save (7.5.4.6). Saved storage is also behind a different surprise: GNU Fortran moves a large fixed-size local array to static storage, which makes the procedure non-reentrant, and -frecursive (implied by -fopenmp) changes that choice (see Stack overflow from large automatic arrays). See Variables, Declarations and Attributes for the full rules on initialization and save.

Default real and literal kinds

A literal constant without a kind suffix has the default kind: 0.1 is a default real (single precision on every mainstream compiler), 1.0d0 is a double precision, and 0.1_dp has the kind dp. Assigning a default real to a real(dp) variable converts the single-precision value, which has already lost the digits, so the variable holds 0.100000001490116…​ instead of 0.1.

program real_literals
  use, intrinsic :: iso_fortran_env, only: real32, real64
  implicit none
  integer, parameter :: dp = real64
  real(dp) :: a, b, c, d
  a = 0.1                  ! a default real constant, converted to real(dp)
  b = 0.1_dp               ! a real(dp) constant
  c = real(0.1, dp)        ! the same trap, spelled with a conversion
  d = 1.0d-1               ! the double precision exponent letter
  print '(a, es24.16)', 'a = 0.1       : ', a
  print '(a, es24.16)', 'b = 0.1_dp    : ', b
  print '(a, es24.16)', 'c = real(0.1) : ', c
  print '(a, es24.16)', 'd = 1.0d-1    : ', d
  print '(a, l1)', 'a == b: ', a == b
  print '(a, l1)', 'b == d: ', b == d
  print '(a, i0, a, i0)', 'kind of 1.0 is ', kind(1.0), ', kind of 1.0d0 is ', kind(1.0d0)
  print '(a, i0)', 'real32 kind is ', real32
end program real_literals
a = 0.1       :   1.0000000149011612E-01
b = 0.1_dp    :   1.0000000000000001E-01
c = real(0.1) :   1.0000000149011612E-01
d = 1.0d-1    :   1.0000000000000001E-01
a == b: F
b == d: T
kind of 1.0 is 4, kind of 1.0d0 is 8
real32 kind is 4

The two compilers print the same values and diagnose different things. GNU Fortran, under -Wextra, warns about the comparisons (-Wcompare-reals), which are a separate trap (a comparison of two reals with == is true only when the values are identical):

g02_real_literals.f90:14:30:

   14 |   print '(a, l1)', 'a == b: ', a == b
      |                              1
Warning: Equality comparison for REAL(8) at (1) [-Wcompare-reals]

It did not warn about a = 0.1 under -Wall -Wextra. Adding -Wconversion-extra reports it (Warning: Conversion from 'REAL(4)' to 'REAL(8)' at (1) [-Wconversion-extra]), and LLVM Flang 23 warns about it by default, naming the inexact value:

g02_real_literals.f90:6:7: warning: Default real literal in REAL(8) context might need a kind suffix, as its rounded value 1.00000001490116119384765625e-1_4 is inexact [-Wreal-constant-widening]
    a = 0.1                  ! a default real constant, converted to real(dp)
        ^^^

The habits that avoid the trap: give every real literal in double-precision code a kind (0.1_dp, 2.5e-6_dp, never 0.1 or 0.1d0 unless dp is double precision); write the kinds in one module (heatflow_kinds in the examples) so that a change of precision is a one-line edit; and use real(x, dp), not real(x) or dble(x), when converting (real(0.1, dp) in the program converts the already-rounded single value, as the output shows). The option -fdefault-real-8 makes every default real an 8-byte real, but it changes the meaning of the whole program (including calls into libraries that expect default reals), so it is a porting tool for legacy code, not a fix. Details on kinds, literals and the numeric models are in Intrinsic Types and Kinds and Floating-Point and IEEE Arithmetic.

Integer division

The quotient of two integers is an integer, truncated toward zero (10.1.5.2.2). A mixed expression such as lx * (1 / (nx - 1)) evaluates the integer division first, so a spacing computed this way is 0 for every grid larger than 2 points. The remainder functions disagree for negative operands: mod takes the sign of the first argument, modulo the sign of the second.

program integer_division
  use, intrinsic :: iso_fortran_env, only: real64, int32
  implicit none
  integer, parameter :: dp = real64
  integer(int32) :: nx, big
  real(dp) :: lx, dx_wrong, dx_right
  nx = 11
  lx = 1.0_dp
  dx_wrong = lx * (1 / (nx - 1))        ! integer division first: 1 / 10 is 0
  dx_right = lx / real(nx - 1, dp)
  print '(a, f6.3, a, f6.3)', 'dx wrong = ', dx_wrong, ', dx right = ', dx_right
  print '(a, 4(i3))', '7/2, -7/2, 7/(-2), 1/2:  ', 7/2, -7/2, 7/(-2), 1/2
  print '(a, 2(i3))', 'mod(-7,3), modulo(-7,3): ', mod(-7, 3), modulo(-7, 3)
  print '(a, 2(f6.2))', 'nint(2.5), 2.5 truncated: ', real(nint(2.5)), real(int(2.5))
  big = huge(big)
  big = big + nx                        ! overflow: wraps around, no diagnostic
  print '(a, i0)', 'huge + 11 = ', big
end program integer_division
dx wrong =  0.000, dx right =  0.100
7/2, -7/2, 7/(-2), 1/2:    3 -3 -3  0
mod(-7,3), modulo(-7,3):  -1  2
nint(2.5), 2.5 truncated:   3.00  2.00
huge + 11 = -2147483638

GNU Fortran adds two warnings at compile time, both about constants (-Winteger-division, enabled by -Wall):

g03_integer_division.f90:12:71:

   12 |   print '(a, 4(i3))', '7/2, -7/2, 7/(-2), 1/2:  ', 7/2, -7/2, 7/(-2), 1/2
      |                                                                       1
Warning: Integer division truncated to constant '0' at (1) [-Winteger-division]
g03_integer_division.f90:12:63:

   12 |   print '(a, 4(i3))', '7/2, -7/2, 7/(-2), 1/2:  ', 7/2, -7/2, 7/(-2), 1/2
      |                                                                (same warning, quotient -3)
Warning: Integer division truncated to constant '-3' at (1) [-Winteger-division]

The division with variables (1 / (nx - 1)) is not reported by either compiler, and neither is the last line of the output: huge(big) + nx overflows a 32-bit integer and wraps around to a negative number. The standard prohibits an operation whose result is not defined by the arithmetic of the processor (10.1.5.2.4) but does not require the processor to detect it, and -fcheck=all does not. GNU Fortran’s -ftrapv makes the overflow trap: the same program built with it stopped with Aborted and a backtrace at the addition. The habits: convert one operand explicitly (real(nx - 1, dp), as in dx_right), or write the constant as a real (1.0_dp / (nx - 1)), and use nint when rounding, not int, which truncates (int(2.5) is 2, nint(2.5) is 3). For index arithmetic that needs a floor, modulo and floor are the right functions for negative numbers. See Expressions and Assignment for the evaluation rules.

No short-circuit evaluation

The standard does not require a .and. b to skip b when a is false. A processor may evaluate both operands, either one first, or neither if it can find the value otherwise: once the interpretation of a logical operation is established, the processor may evaluate any other expression that is logically equivalent (10.1.5.4.2). The conditions that depend on each other, such as a bounds test and a subscript, are therefore a bug waiting for a different compiler or a different optimization level.

program short_circuit
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  integer, parameter :: dp = real64
  real(dp) :: t(3) = [20.0_dp, 35.5_dp, 12.0_dp]
  integer :: i, n
  n = size(t)
  ! Search for the first temperature above 30 -- written as in C or Python.
  i = 1
  do while (i <= n .and. t(i) <= 30.0_dp)   ! t(n + 1) may be evaluated
    i = i + 1
  end do
  print '(a, i0)', 'first hot cell: ', i

  ! The same search for a limit that is never reached.
  i = 1
  do while (i <= n .and. t(i) <= 100.0_dp)
    i = i + 1
  end do
  print '(a, i0)', 'first cell above 100: ', i
end program short_circuit

GNU Fortran with -fcheck=all evaluates the second operand for i = 4 and stops with a run-time error (the output of the first search is printed first):

first hot cell: 2
At line 17 of file g04_short_circuit.f90
Fortran runtime error: Index '4' of dimension 1 of array 't' above upper bound of 3

Error termination. Backtrace:
#0  0xffff82f4196f in ???
...

LLVM Flang 23 without run-time checks reads one element past the array (an out-of-bounds read, which is undefined behaviour) and prints a plausible answer:

first hot cell: 2
first cell above 100: 4

That second result is the worst outcome: the program "works". The fix is to structure the test so that the dependent operand is only reached when it is valid:

program short_circuit_fixed
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  integer, parameter :: dp = real64
  real(dp) :: t(3) = [20.0_dp, 35.5_dp, 12.0_dp]
  integer :: i, n
  logical :: hot
  n = size(t)
  ! Option 1: a loop with an explicit exit; the test of t(i) is only reached if i <= n.
  do i = 1, n
    if (t(i) > 100.0_dp) exit
  end do
  print '(a, i0)', 'first cell above 100 (exit loop): ', i

  ! Option 2: the intrinsic does the search.
  i = findloc(t > 100.0_dp, .true., dim=1)
  print '(a, i0)', 'first cell above 100 (findloc, 0 if none): ', i

  ! Option 3: a nested if guards a single test.
  i = n + 1
  hot = .false.
  if (i <= n) then
    hot = t(i) > 100.0_dp
  end if
  print '(a, l1)', 'nested if, i = n + 1: ', hot

  ! Option 4: the Fortran 2023 conditional expression evaluates only the chosen branch.
  hot = (i <= n ? t(i) > 100.0_dp : .false.)
  print '(a, l1)', 'conditional expression, i = n + 1: ', hot
end program short_circuit_fixed
first cell above 100 (exit loop): 4
first cell above 100 (findloc, 0 if none): 0
nested if, i = n + 1: F
conditional expression, i = n + 1: F

Both compilers print the same. The exit loop is the idiom to prefer, findloc is the idiom for a pure search (it returns 0 when nothing matches), and a nested if is the idiom for a single guarded test. The Fortran 2023 conditional expression (cond ? a : b) evaluates only the chosen branch (10.1.4, NOTE 3), which .and. and merge do not promise, and both compilers accepted it in the last test; it is described in Expressions and Assignment. The same hazard applies to present(x) .and. x > 0 with an optional argument, which must be written as two nested tests (Procedures, Functions and Subroutines).

intent(out) deallocates and undefines

intent(out) is not "write only, keep the rest": it states that the incoming value is not used, and the language enforces that. On entry to the procedure (8.5.10 and 9.7.3.2):

  • an allocated allocatable actual argument that corresponds to an intent(out) allocatable dummy is deallocated;

  • a nonpointer dummy argument becomes undefined, except for subcomponents with default initialization, which get their default value back;

  • a pointer dummy argument has an undefined association status.

module readings
  use, intrinsic :: iso_fortran_env, only: real64, int32
  implicit none
  private
  public :: probe_t, append_wrong, append_right, reset_probe, set_edges
  integer, parameter :: dp = real64
  type :: probe_t
    integer(int32) :: count = 0          ! default initialization
    real(dp) :: last = -1.0_dp
  end type probe_t
contains
  ! Intended to add a value to the end of buf -- but intent(out) deallocates buf on entry.
  subroutine append_wrong(buf, value)
    real(dp), allocatable, intent(out) :: buf(:)
    real(dp), intent(in) :: value
    buf = [buf, value]                   ! buf is no longer allocated here
  end subroutine append_wrong

  subroutine append_right(buf, value)
    real(dp), allocatable, intent(inout) :: buf(:)
    real(dp), intent(in) :: value
    buf = [buf, value]
  end subroutine append_right

  ! intent(out) re-applies the default initialization: count is reset to 0.
  subroutine reset_probe(p, value)
    type(probe_t), intent(out) :: p
    real(dp), intent(in) :: value
    p%last = value                       ! count was not mentioned -- it is 0 again
  end subroutine reset_probe

  ! An intent(out) array that is only partly assigned: the interior is undefined.
  subroutine set_edges(u)
    real(dp), intent(out) :: u(:)
    u(1) = 0.0_dp
    u(size(u)) = 1.0_dp
  end subroutine set_edges
end module readings

program intent_out
  use, intrinsic :: iso_fortran_env, only: real64
  use readings
  implicit none
  integer, parameter :: dp = real64
  real(dp), allocatable :: buf(:)
  type(probe_t) :: p
  buf = [1.0_dp, 2.0_dp]
  call append_right(buf, 3.0_dp)
  print '(a, *(f5.1))', 'append_right: ', buf
  p = probe_t(count=5, last=2.5_dp)
  call reset_probe(p, 9.0_dp)
  print '(a, i0, a, f4.1)', 'after reset_probe: count = ', p%count, ', last = ', p%last
  call append_wrong(buf, 4.0_dp)
  print '(a, *(f5.1))', 'append_wrong:', buf
end program intent_out

The two correct calls print what a programmer expects, and the last one is where the trap is:

append_right:   1.0  2.0  3.0
after reset_probe: count = 0, last =  9.0

reset_probe shows the second rule: the caller’s count = 5 is gone, because probe_t has default initialization and intent(out) re-applies it. The last call, append_wrong, expects buf to still hold [1, 2, 3], but the dummy was deallocated on entry, so [buf, value] refers to an unallocated array. The standard calls this an error, and the compilers disagree on what they do with it. Under -fcheck=all GNU Fortran 16.2 stops the program with a segmentation fault:

Program received signal SIGSEGV: Segmentation fault - invalid memory reference.

Backtrace for this error:
#0  0xffff9a77196f in ???
...
#3  0x4016a0 in __readings_MOD_append_wrong
	at /work/g05_intent_out.f90:16
#4  0x401bfb in intent_out
	at /work/g05_intent_out.f90:53
Segmentation fault

whereas LLVM Flang 23 prints append_wrong: 4.0, silently treating the unallocated array as empty. The fix is intent(inout) (as in append_right), or, for a routine that really replaces its argument, the result is built in a local and moved with move_alloc. The same applies to arrays: an intent(out) array that is only partly assigned, such as set_edges, leaves the interior undefined, and reading it is a use of an undefined value. Use intent(inout) whenever the procedure reads the incoming value, even once. See Allocatable Arrays and Dynamic Memory and Procedures, Functions and Subroutines.

Array temporaries and aliasing violations

Hidden copies

An actual argument that is not contiguous (a row of a column-major array, or a(::2)) cannot be passed by address to a dummy argument that requires contiguous data: an explicit-shape (x(n)), assumed-size (x(*)) or contiguous dummy, or a dummy with the old implicit interface. The compiler creates a temporary, copies the data in before the call and, for intent(out) or intent(inout), copies it back after. The result is correct, but the cost is a hidden allocation and copy at every call, which in a hot loop is a large slowdown. An assumed-shape dummy (x(:)) receives a descriptor with the stride and needs no copy.

module norms
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: sum_explicit, sum_assumed
  integer, parameter :: dp = real64
contains
  real(dp) function sum_explicit(n, x) result(s)   ! explicit-shape dummy: needs contiguous data
    integer, intent(in) :: n
    real(dp), intent(in) :: x(n)
    s = sum(x)
  end function sum_explicit

  real(dp) function sum_assumed(x) result(s)       ! assumed-shape dummy: any stride is fine
    real(dp), intent(in) :: x(:)
    s = sum(x)
  end function sum_assumed
end module norms

program array_temps
  use, intrinsic :: iso_fortran_env, only: real64
  use norms
  implicit none
  integer, parameter :: dp = real64
  real(dp) :: u(4, 3)
  integer :: i, j
  do j = 1, 3
    do i = 1, 4
      u(i, j) = 10.0_dp * j + i
    end do
  end do
  print '(a, f6.1)', 'column u(:, 2), contiguous, explicit-shape: ', sum_explicit(4, u(:, 2))
  print '(a, f6.1)', 'row u(2, :), strided, assumed-shape:        ', sum_assumed(u(2, :))
  print '(a, f6.1)', 'row u(2, :), strided, explicit-shape:       ', sum_explicit(3, u(2, :))
end program array_temps
column u(:, 2), contiguous, explicit-shape:   90.0
row u(2, :), strided, assumed-shape:          66.0
row u(2, :), strided, explicit-shape:         66.0

Both compilers print the same result. The third call needs a temporary, and GNU Fortran can tell you, at compile time with -Warray-temporaries and at run time with -fcheck=array-temps (part of -fcheck=all, and the reason the strict build prints a run-time warning on standard error):

g07_array_temps.f90:34:85:

   34 |   print '(a, f6.1)', 'row u(2, :), strided, explicit-shape:       ', sum_explicit(3, u(2, :))
      |                                                                                     1
Warning: Creating array temporary at (1) for argument 'x' [-Warray-temporaries]
...
At line 34 of file g07_array_temps.f90
Fortran runtime warning: An array temporary was created for argument 'x' of procedure 'sum_explicit'

LLVM Flang 23 printed no diagnostic. The cure is an assumed-shape dummy wherever the array may be a section, and contiguous only when the copy is acceptable. The rules for each kind of dummy array are in Array Arguments: Assumed Shape, Size and Rank, and the performance effect in Performance and Memory Layout.

Aliasing

If a procedure defines a dummy argument, that dummy argument may not be associated with any other dummy argument or accessible variable that is referenced in the procedure (15.5.2.14). The compiler relies on it: it keeps values in registers and reorders loads and stores. If the caller breaks the promise, the answer depends on the optimization level.

module smoothing
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: smooth
  integer, parameter :: dp = real64
contains
  ! y(i) = average of x(i-1), x(i), x(i+1). x and y must not overlap: y is defined
  ! while x is still being read, and the compiler may assume they are different.
  subroutine smooth(x, y)
    real(dp), intent(in) :: x(:)
    real(dp), intent(out) :: y(:)
    integer :: i
    y(1) = x(1)
    y(size(x)) = x(size(x))
    do i = 2, size(x) - 1
      y(i) = (x(i - 1) + x(i) + x(i + 1)) / 3.0_dp
    end do
  end subroutine smooth
end module smoothing

program aliasing
  use, intrinsic :: iso_fortran_env, only: real64
  use smoothing, only: smooth
  implicit none
  integer, parameter :: dp = real64
  real(dp) :: a(6), b(6)
  a = [0.0_dp, 0.0_dp, 9.0_dp, 0.0_dp, 0.0_dp, 0.0_dp]
  call smooth(a, b)                       ! conforming: separate arrays
  print '(a, *(f6.2))', 'separate arrays: ', b
  call smooth(a, a)                       ! not conforming: x and y are the same array
  print '(a, *(f6.2))', 'aliased call:    ', a

  ! Array assignment is safe even when both sides overlap: the right side is evaluated first.
  a = [1.0_dp, 2.0_dp, 3.0_dp, 4.0_dp, 5.0_dp, 6.0_dp]
  a(2:6) = a(1:5)
  print '(a, *(f6.2))', 'a(2:6) = a(1:5):  ', a
end program aliasing
separate arrays:   0.00  3.00  3.00  3.00  0.00  0.00
aliased call:      0.00  3.00  4.00  1.33  0.44  0.00
a(2:6) = a(1:5):    1.00  1.00  2.00  3.00  4.00  5.00

With the strict flags (-O0) the aliased call reads values that were just overwritten, which is what a sequential reading of the loop would give. GNU Fortran detects the literal case at compile time:

g06_aliasing.f90:31:14:

   31 |   call smooth(a, a)                       ! not conforming: x and y are the same array
      |              1
Warning: Same actual argument associated with INTENT(IN) argument 'x' and INTENT(OUT) argument 'y' at (1)

and when the same file is compiled at different optimization levels the line "aliased call" changes:

-O0
aliased call:      0.00  3.00  4.00  1.33  0.44  0.00
-O2
aliased call:      0.00  3.00  3.00  3.00  0.00  0.00
-O3
aliased call:      0.00  3.00  3.00  3.00  0.00  0.00

At -O2 and -O3 the optimizer assumed that x and y do not overlap, loaded the three inputs before storing, and produced the same numbers as the conforming call. Neither answer is "right"; the program is not conforming, so both are acceptable. LLVM Flang 23 printed the -O0-style result. The aliasing is usually not a literal call f(a, a) but two overlapping sections (a(1:5) and a(2:6)) or a pointer and its target, which no compiler warns about. The last assignment shows what does work: a whole-array assignment such as a(2:6) = a(1:5) is defined as if the right side were evaluated completely before the assignment (10.2.1.3), so overlapping operands are safe there, and the compiler inserts a temporary or reverses the loop if needed. A do loop that does the same in place would have to be written backwards by the programmer.

Character padding in comparisons and assignments

In a comparison, the shorter of two character operands is treated as if extended on the right with blanks to the length of the longer (10.1.5.5.1). Trailing blanks are therefore never significant in ==, /=, < and the other relational operators. An assignment to a longer variable pads with blanks, and to a shorter variable it silently truncates (10.2.1.3). A character(len=8) variable always contains 8 characters, and len_trim is the length without the blanks.

program character_padding
  implicit none
  character(len=8) :: name
  character(len=3) :: short
  character(len=:), allocatable :: dyn
  name = 'jacobi'                          ! padded with blanks to 8 characters
  print '(a, l1)', "name == 'jacobi'         : ", name == 'jacobi'
  print '(a, l1)', "name == 'jacobi   '      : ", name == 'jacobi   '
  print '(a, l1)', "'ab' == 'ab '            : ", 'ab' == 'ab '
  print '(a, l1)', "'ab' < 'ab' // char(1)   : ", 'ab' < 'ab' // char(1)
  print '(a, l1)', "'ab ' == 'ab' // char(0) : ", 'ab ' == 'ab' // char(0)
  print '(a, i0, a, i0)', 'len(name) = ', len(name), ', len_trim(name) = ', len_trim(name)
  print '(a, i0)', "index(name, ' ') = ", index(name, ' ')
  short = 'ftcs'                           ! silently truncated to 3 characters
  print '(3a)', "short = '", short, "'"
  dyn = name                               ! allocatable length takes the padded length
  print '(a, i0)', 'len(dyn) = ', len(dyn)
  dyn = trim(name)
  print '(a, i0)', 'len(dyn) after trim = ', len(dyn)
  print '(3a)', "[", name, "]"
  print '(3a)', "[", trim(name), "]"
end program character_padding
name == 'jacobi'         : T
name == 'jacobi   '      : T
'ab' == 'ab '            : T
'ab' < 'ab' // char(1)   : F
'ab ' == 'ab' // char(0) : F
len(name) = 8, len_trim(name) = 6
index(name, ' ') = 7
short = 'ftc'
len(dyn) = 8
len(dyn) after trim = 6
[jacobi  ]
[jacobi]

Both compilers print the same. What this shows:

  • name == 'jacobi' is true, although the strings have lengths 8 and 6, and 'ab' == 'ab ' is true for the same reason.

  • The padding blank is a real character in the collating sequence, so 'ab' < 'ab' // char(1) is false: comparing 'ab' with a longer string pads the shorter one with a blank (code 32), which is greater than char(1). The ordering of strings with different lengths is therefore the ordering of the padded strings, not a lexicographic order with "shorter is smaller".

  • index(name, ' ') finds the first padding blank, not the end of the name, and len(name) is the declared length. Use len_trim and trim.

  • Assigning 'ftcs' to a character(len=3) variable keeps 'ftc' without a run-time error; GNU Fortran warns at compile time when it can see it (-Wcharacter-truncation, enabled by -Wall):

g08_character_padding.f90:14:79:

   14 |   short = 'ftcs'                           ! silently truncated to 3 characters
      |                                                                               1
Warning: CHARACTER expression will be truncated in assignment (3/4) at (1) [-Wcharacter-truncation]
  • A character(len=:), allocatable variable takes the length of the assigned expression, so dyn = name keeps the padding (length 8) while dyn = trim(name) has length 6. The output [jacobi ] versus [jacobi] is the visible effect.

The habit is to use trim when building strings and file names, adjustl when aligning text, and the character(len=:), allocatable variables for text of unknown length. The details are in Characters and Strings.

Lower bounds of dummy arguments

A Fortran array can have any lower bound, and a common use is a halo: u(0:nx+1) with the boundary values at 0 and nx+1. Whether a dummy argument sees the bounds depends on how it is declared (8.5.8):

module bounds_demo
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: show
  integer, parameter :: dp = real64
contains
  subroutine show(label, shape_info)
    character(*), intent(in) :: label
    integer, intent(in) :: shape_info(2)
    print '(a, a, i0, a, i0)', label, ': lbound = ', shape_info(1), ', ubound = ', shape_info(2)
  end subroutine show
end module bounds_demo

module bounds_probes
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: assumed_shape, assumed_shape_lb, explicit_shape, allocatable_dummy, pointer_dummy
  integer, parameter :: dp = real64
contains
  subroutine assumed_shape(a, lb, ub)
    real(dp), intent(in) :: a(:)
    integer, intent(out) :: lb, ub
    lb = lbound(a, 1); ub = ubound(a, 1)
  end subroutine assumed_shape

  subroutine assumed_shape_lb(a, lb, ub)
    real(dp), intent(in) :: a(0:)
    integer, intent(out) :: lb, ub
    lb = lbound(a, 1); ub = ubound(a, 1)
  end subroutine assumed_shape_lb

  subroutine explicit_shape(a, n, lb, ub)
    integer, intent(in) :: n
    real(dp), intent(in) :: a(0:n - 1)
    integer, intent(out) :: lb, ub
    lb = lbound(a, 1); ub = ubound(a, 1)
  end subroutine explicit_shape

  subroutine allocatable_dummy(a, lb, ub)
    real(dp), allocatable, intent(in) :: a(:)
    integer, intent(out) :: lb, ub
    lb = lbound(a, 1); ub = ubound(a, 1)
  end subroutine allocatable_dummy

  subroutine pointer_dummy(a, lb, ub)
    real(dp), pointer, intent(in) :: a(:)
    integer, intent(out) :: lb, ub
    lb = lbound(a, 1); ub = ubound(a, 1)
  end subroutine pointer_dummy
end module bounds_probes

program lower_bounds
  use, intrinsic :: iso_fortran_env, only: real64
  use bounds_demo, only: show
  use bounds_probes
  implicit none
  integer, parameter :: dp = real64
  real(dp), allocatable, target :: u(:)
  real(dp), pointer :: p(:)
  integer :: lb, ub
  allocate (u(0:5))                        ! halo-style bounds 0:5
  u = 1.0_dp
  p => u
  print '(a, i0, a, i0)', 'actual argument: lbound = ', lbound(u, 1), ', ubound = ', ubound(u, 1)
  call assumed_shape(u, lb, ub);      call show('assumed shape  a(:)  ', [lb, ub])
  call assumed_shape_lb(u, lb, ub);   call show('assumed shape  a(0:) ', [lb, ub])
  call explicit_shape(u, 6, lb, ub);  call show('explicit shape a(0:n-1)', [lb, ub])
  call allocatable_dummy(u, lb, ub);  call show('allocatable    a(:)  ', [lb, ub])
  call pointer_dummy(p, lb, ub);      call show('pointer        a(:)  ', [lb, ub])
  call assumed_shape(u(2:4), lb, ub); call show('section u(2:4) to a(:)', [lb, ub])
end program lower_bounds
actual argument: lbound = 0, ubound = 5
assumed shape  a(:)  : lbound = 1, ubound = 6
assumed shape  a(0:) : lbound = 0, ubound = 5
explicit shape a(0:n-1): lbound = 0, ubound = 5
allocatable    a(:)  : lbound = 0, ubound = 5
pointer        a(:)  : lbound = 0, ubound = 5
section u(2:4) to a(:): lbound = 1, ubound = 3

Both compilers print the same. The rule is:

Dummy argument Bounds seen in the procedure

assumed shape, a(:)

lower bound 1, upper bound the extent (8.5.8.3), whatever the bounds of the actual argument

assumed shape with a lower bound, a(0:)

the stated lower bound, the extent of the actual argument

explicit shape, a(0:n - 1)

the declared bounds

allocatable or pointer, a(:)

the bounds of the actual argument (the dummy is the argument itself)

an array section, u(2:4)

a new array: lower bound 1

The trap is the first row: a routine written for u(0:nx+1) that receives it through an assumed-shape dummy sees u(1:nx+2), and its u(0) is out of bounds. The result is an off-by-one error that shows only at the edges. The fix is to declare the lower bound in the dummy (real(dp), intent(inout) :: u(0:)) or to receive the array as allocatable or pointer. The result of an array expression or of an intrinsic such as pack or reshape also starts at 1. A loop that must accept any array should run from lbound(u, 1) to ubound(u, 1), not from the constant 1. See Array Arguments: Assumed Shape, Size and Rank and Arrays Fundamentals.

Uninitialized variables

A local variable without an initializer has an undefined value until it is assigned (19.6.4). Reading it gives whatever was in memory, and the program may print a plausible number, as the one below does.

program uninitialized
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  integer, parameter :: dp = real64
  real(dp) :: residual, peak
  real(dp) :: u(5)
  integer :: i, steps
  u(1:4) = [1.0_dp, 4.0_dp, 2.0_dp, 3.0_dp]     ! u(5) is never set
  do i = 1, 4
    if (u(i) > peak) peak = u(i)                  ! peak is never initialized
  end do
  steps = steps + 1                               ! nor is steps
  residual = maxval(abs(u))
  print '(a, es12.4)', 'peak     = ', peak
  print '(a, i0)', 'steps    = ', steps
  print '(a, es12.4)', 'residual = ', residual
end program uninitialized
peak     =   4.0000E+00
steps    = 1
residual =   4.0000E+00

Flang printed the same three lines. They are plausible, and they come out the same each time on this machine, which is why this trap survives testing: steps happened to start as zero and peak as a value below 4, so the program "works". It printed steps = -1624492493 when compiled with -O1. The tools that catch it are:

  • the compile-time analysis of GNU Fortran. In the strict build above it reported, once the optimizer’s data flow was available:

    g10_uninitialized.f90:12:64:
    
       12 |   steps = steps + 1                               ! nor is steps
          |                                                                ^
    Warning: 'steps' may be used uninitialized [-Wmaybe-uninitialized]

    At -O1 the warning is the stronger 'steps' is used uninitialized [-Wuninitialized]. The analysis is conservative and did not report peak, which is only uninitialized on a path through the loop.

  • -finit-real=snan -finit-integer=n with -ffpe-trap=invalid (GNU Fortran): every uninitialized local real starts as a signaling NaN, so the first arithmetic use stops the program at the line:

    Floating point exception
    
    Program received signal SIGFPE: Floating-point exception - erroneous arithmetic operation.
    
    Backtrace for this error:
    ...
    #3  0x4009c4 in uninitialized
    	at /work/g10_uninitialized.f90:10

    Line 10 is the comparison with peak. The -finit-* options exist for finding bugs, not for fixing them; the program must assign every variable before use. They are not available in Flang 23 (see Compilers and Compiler Options).

  • Valgrind’s Memcheck on a plain -g build (no optimization), which reported two uses of undefined values: the comparison with peak at line 10 and the maxval(abs(u)) that reads the never-set u(5) at line 13:

    ==49== Conditional jump or move depends on uninitialised value(s)
    ==49==    at 0x400934: MAIN__ (g10_uninitialized.f90:10)
    ==49==    by 0x400B87: main (g10_uninitialized.f90:17)
    ==49== Conditional jump or move depends on uninitialised value(s)
    ==49==    at 0x4009E0: MAIN__ (g10_uninitialized.f90:13)
    ==49==    by 0x400B87: main (g10_uninitialized.f90:17)

The fixes: initialize in the declaration only when save semantics are acceptable (see Implicit save on initialized locals), otherwise assign on the first line of the procedure; initialize allocatable arrays at the allocate with source= or mold= and a value.

Unit numbers already in use

A unit number is a global resource of the program. The units 5, 6 and 0 are preconnected on common compilers to standard input, output and error, and iso_fortran_env provides them under the names input_unit, output_unit and error_unit (error_unit is not required to be 0). Beyond that, the number is the programmer’s choice, and libraries that choose a fixed number (10, 11, 20, …​) collide with the program. In a read or write, unit=* means the first two of these (12.5.5).

program units
  use, intrinsic :: iso_fortran_env, only: input_unit, output_unit, error_unit
  implicit none
  integer :: u1, u2, ios
  character(len=256) :: msg, name
  logical :: is_open, exists
  print '(a, 3(1x, i0))', 'preconnected units (input, output, error):', input_unit, output_unit, error_unit

  ! A hard-coded unit number is easy to reuse by accident. OPEN of a connected unit
  ! on a different file is not an error: the first file is closed without a word.
  open (unit=10, file='first.txt', status='replace', action='write')
  write (10, '(a)') 'first'
  open (unit=10, file='second.txt', status='replace', action='write')
  write (10, '(a)') 'second'
  inquire (unit=10, name=name)
  print '(a, a)', 'unit 10 is now connected to ', trim(name)
  close (10)

  ! Writing to a unit that was never opened creates a file with a made-up name.
  write (77, '(a)') 'surprise'
  close (77)
  inquire (file='fort.77', exist=exists)
  print '(a, l1)', 'file fort.77 exists: ', exists

  ! status='new' fails when the file exists; iostat and iomsg report it.
  msg = ''
  open (unit=11, file='first.txt', status='new', iostat=ios, iomsg=msg)
  print '(a, l1, a, l1)', 'open status=new failed: ', ios /= 0, ', message given: ', len_trim(msg) > 0

  ! newunit= picks a free negative number, so it cannot collide.
  open (newunit=u1, file='first.txt', status='old', action='read')
  open (newunit=u2, file='second.txt', status='old', action='read')
  print '(a, l1)', 'newunit values are negative: ', u1 < 0 .and. u2 < 0
  inquire (unit=10, opened=is_open)
  print '(a, l1)', 'unit 10 still open: ', is_open
  close (u1, status='delete')
  close (u2, status='delete')
  open (newunit=u1, file='fort.77', status='old')
  close (u1, status='delete')
end program units
preconnected units (input, output, error): 5 6 0
unit 10 is now connected to second.txt
file fort.77 exists: T
open status=new failed: T, message given: T
newunit values are negative: T
unit 10 still open: F

Both compilers print the same. What happened:

  • The second open(unit=10, file='second.txt') was accepted. The standard says that connecting a connected unit to a different file first closes the first file as if by close (12.5.4), so no error is raised and the first file is quietly closed. If the first file was still in use through that unit, the write goes into the wrong file. The page on input and output shows the related case of two units on the same file, which the two compilers handle differently (Input/Output: Units and Files).

  • write (77, ...) on a unit that was never opened created a file named fort.77. The standard says that an I/O statement (other than open, close, inquire and wait) shall not refer to a unit that does not exist (12.5.3), and that the set of units that exist is processor dependent; both compilers treat every number as existing and connect it to fort.N, so a typo in the unit number produces a stray file, not an error. The program removes the file again at the end.

  • open(..., status='new') on an existing file failed, and iostat= and iomsg= captured it; without iostat the program would have stopped.

  • open(newunit=u) (Fortran 2008) picks a unit that is not in use and negative, so it cannot collide with a user-chosen number. This is what library code should always use.

inquire(unit=n, opened=flag) tests a number, and numbers 0 to 9 are best left alone, because several compilers preconnect some of them. iostat= and iomsg= on every open, read and write test an I/O operation without stopping the program, as explained in Error Handling and Program Termination.

List-directed output formatting

print *, x and write (*, *) x use list-directed formatting (13.10.4): the processor chooses the layout. The standard fixes only a few things (a leading blank for carriage control, values separated by a blank or a comma, a character value without quotes unless it is a namelist). The width, the number of digits and whether a value like 0.1 is written as 0.1 or 0.10000000000000001 are the compiler’s choice.

program list_directed
  use, intrinsic :: iso_fortran_env, only: real32, real64, int32
  implicit none
  real(real64) :: t = 0.1_real64, big = 1.0e9_real64, small = 2.5e-6_real64
  real(real32) :: s = 0.1_real32
  integer(int32) :: n = 42
  logical :: ok = .true.
  character(len=5) :: word = 'ab'
  print *, 'steps =', n, 'ok =', ok
  print *, t, s
  print *, big, small
  print *, 'word=[', word, ']'
  print *, cmplx(1.0_real64, -2.0_real64, real64)
  print *, [1.0_real64, 2.5_real64, 3.0_real64]
  print '(a, i0, a, f8.3)', 'n=', n, ' t=', t
  print '(es12.4, 1x, es12.4)', big, small
end program list_directed
 steps =          42 ok = T
  0.10000000000000001       0.100000001
   1000000000.0000000        2.5000000000000002E-006
 word=[ab   ]
              (1.0000000000000000,-2.0000000000000000)
   1.0000000000000000        2.5000000000000000        3.0000000000000000
n=42 t=   0.100
  1.0000E+09   2.5000E-06

The above is the output of GNU Fortran 16.2. LLVM Flang 23 prints the same two lines at the end, and the rest in its own style:

 steps = 42 ok = T
 .1 .1
 1000000000. 2.5E-06
 word=[ab   ]
 (1.,-2.)
 1. 2.5 3.
n=42 t=   0.100
  1.0000E+09   2.5000E-06

The two outputs differ in the field widths (steps = 42 against steps = 42), in the number of digits (Flang writes .1 for both 0.1_real64 and 0.1_real32, GNU Fortran writes 17 significant digits for a real64 and 9 for a real32), in the exponent style (2.5000000000000002E-006 against 2.5E-06), and in the padding of the complex number and the array. Both are valid. A test that compares the output of print * with a stored text breaks when the compiler changes. For output that others read or programs parse, use an explicit format: i0 for integers of any width, f8.3 or es12.4 for reals, a for text, l1 for logicals, and an unlimited repeat count for lists ((*(f8.3))). For round-tripping reals, es24.16e3 is a usual choice. All the edit descriptors are in Format Specifications and Edit Descriptors, and list-directed and namelist input in Internal Files, Namelist and Non-Advancing I/O.

Stack overflow from large automatic arrays

An automatic array is a local array whose bounds depend on a dummy argument or other nonconstant expression (real(dp) :: work(n)). The standard says nothing about where it lives, and the compilers differ. A fixed-size local array can overflow the stack too.

module work_arrays
  use, intrinsic :: iso_fortran_env, only: real64, int64
  implicit none
  private
  public :: mean_automatic, mean_allocatable
  integer, parameter :: dp = real64
contains
  ! An automatic array: its size is a dummy argument, and it usually lives on the stack.
  real(dp) function mean_automatic(n) result(m)
    integer(int64), intent(in) :: n
    real(dp) :: work(n)
    work = 1.0_dp
    m = sum(work) / real(n, dp)
  end function mean_automatic

  ! An allocatable local: the data live on the heap, and allocate reports a failure.
  real(dp) function mean_allocatable(n) result(m)
    integer(int64), intent(in) :: n
    real(dp), allocatable :: work(:)
    allocate (work(n))
    work = 1.0_dp
    m = sum(work) / real(n, dp)
  end function mean_allocatable
end module work_arrays

program stack_usage
  use, intrinsic :: iso_fortran_env, only: int64, real64
  use work_arrays
  implicit none
  integer(int64), parameter :: n = 50000000_int64        ! 400 MB of real64
  print '(a, f4.1)', 'allocatable: ', mean_allocatable(n)
  print '(a, f4.1)', 'automatic:   ', mean_automatic(n)
end program stack_usage

The program works with the automatic array on an 8 MiB stack (ulimit -s is 8192 in the container) when built with GNU Fortran, because GNU Fortran puts automatic arrays on the heap by default:

allocatable:  1.0
automatic:    1.0

and crashes in these builds, which end with Segmentation fault (the Flang run showed no output line at all, presumably because the first line was still in a buffer when the process died):

Build Result

gfortran -std=f2023 -fstack-arrays

allocatable: 1.0 then Segmentation fault

gfortran -std=f2023 -O2 -fstack-arrays

allocatable: 1.0 then Segmentation fault

flang-23

Segmentation fault, no output line

The same program on Flang with ulimit -s 1000000 (about 1 GB) ran and printed both lines, which shows that the cause is the stack limit, but raising the limit is a workaround that depends on the environment (and on OMP_STACKSIZE for the threads of an OpenMP program). -fstack-arrays is enabled by -Ofast, so a program that worked at -O2 can start to crash at -Ofast. A second variant, a fixed-size array in a procedure, behaves differently again:

program fixed_local
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  call work()
contains
  subroutine work()
    real(real64) :: big(50000000)           ! fixed size, 400 MB
    big = 1.0_real64
    print '(a, f4.1)', 'mean = ', sum(big) / size(big)
  end subroutine work
end program fixed_local

With GNU Fortran 16.2 the program printed mean = 1.0, because a fixed-size local array larger than the limit of -fmax-stack-var-size is placed in static memory, which makes the procedure non-reentrant. With -frecursive or -fopenmp (which forbid that choice), with -fmax-stack-var-size=1000000000, and with Flang 23 it crashed with Segmentation fault. Neither behaviour is wrong, and neither is portable.

The cure is an allocatable array for anything whose size is large or comes from the user: allocate(work(n), stat=ierr) reports a failure instead of crashing, and the memory is released at the end of the scope. The two ways of getting this wrong in the opposite direction (an array allocated in a hot loop) are covered in Performance and Memory Layout; the rules for allocatable arrays and the same experiment in a different setting are in Allocatable Arrays and Dynamic Memory. The diagnostic to remember: a segmentation fault on entering a procedure, before any of its statements run, is a stack overflow until proven otherwise. GNU Fortran has -Wstack-usage=<byte-size> (not tested here) for finding procedures that may use too much stack.

More small traps

Four more traps are small enough for a single program.

program loops_and_reals
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  integer, parameter :: dp = real64
  real(dp) :: a, b, tol
  integer :: i, n
  ! Loop variable after the loop: one past the last iteration, not the last value.
  do i = 1, 5
  end do
  print '(a, i0)', 'i after do i = 1, 5 : ', i
  n = 0
  do i = 5, 1                            ! zero iterations; i is still set to the initial value
    n = n + 1
  end do
  print '(a, i0, a, i0)', 'trips of do i = 5, 1: ', n, ', i = ', i
  ! Changing the bounds inside the loop has no effect: they are evaluated once.
  n = 3
  do i = 1, n
    n = 10
  end do
  print '(a, i0, a, i0)', 'n after loop = ', n, ', i after loop = ', i
  ! Comparing reals.
  a = 0.1_dp + 0.2_dp
  b = 0.3_dp
  tol = 4 * epsilon(b)
  print '(a, l1)', '0.1 + 0.2 == 0.3: ', a == b
  print '(a, es10.2)', 'difference:       ', a - b
  print '(a, l1)', 'abs(a - b) <= tol * abs(b): ', abs(a - b) <= tol * abs(b)
  ! maxval of an empty array.
  print '(a, es12.4)', 'maxval of an empty array: ', maxval([real(dp) ::])
  print '(a, i0)', 'maxval of an empty integer array: ', maxval([integer ::])
end program loops_and_reals
i after do i = 1, 5 : 6
trips of do i = 5, 1: 0, i = 5
n after loop = 10, i after loop = 4
0.1 + 0.2 == 0.3: F
difference:         5.55E-17
abs(a - b) <= tol * abs(b): T
maxval of an empty array:  -1.7977+308
maxval of an empty integer array: -2147483648

Both compilers print the same, and GNU Fortran adds the warnings Warning: DO loop at (1) will be executed zero times [-Wzerotrip] and the -Wcompare-reals warning already shown. What the program demonstrates:

  • The do variable after the loop. After do i = 1, 5 the variable is 6, not 5: the loop increments it before it tests the condition. After a do with zero trips the variable is the initial value, 5 here (11.1.7.4.1 and 11.1.7.4.3). Code that uses i after the loop, for instance as the result of a search, must exit out of the loop or save the index.

  • Loop bounds are evaluated once. The statement n = 10 in the body does not change the three iterations, because the iteration count is established before the loop starts (11.1.7.4.1). To get a loop whose condition is re-evaluated, use do while or do with exit.

  • Real numbers are compared with a tolerance. 0.1_dp + 0.2_dp == 0.3_dp is false (the difference is 5.55E-17, one unit in the last place), and the right test is a relative tolerance, abs(a - b) <= tol * abs(b), with tol a small multiple of epsilon(b). See Floating-Point and IEEE Arithmetic.

  • Reductions over empty arrays return identities. maxval of a zero-size real array is -huge(x) (the output -1.7977+308 is -huge(1.0_dp) written with es12.4 because the exponent needs three digits), and maxval of an empty integer array is -2147483648: the standard says the negative number of the largest magnitude supported by the processor (16.9.138). sum gives 0 and product gives 1. A program that takes the maxval of the residual of an empty subdomain gets a huge negative number, not an error.

Diagnostic flags that catch these traps

The flags used above, in one place. The first column names the compiler, because the two compilers support different sets: Flang 23 takes individual warnings with -W<warning> and has -fstack-arrays, but -Wextra is not supported and -Wall is ignored (flang-23 -Wall printed argument unused during compilation), and it has no equivalent of -fcheck=, -finit-real or -ffpe-trap (see Compilers and Compiler Options).

Compiler Flag What it reports

gfortran

-Wall -Wextra

the baseline used by every example: -Wall includes -Waliasing, -Wconversion, -Winteger-division, -Wzerotrip and -Wtarget-lifetime (and reported the character truncation above), -Wextra adds -Wcompare-reals and -Wdo-subscript; the uninitialized-variable warnings -Wuninitialized and -Wmaybe-uninitialized come from the optimizer’s data flow

gfortran

-Wconversion-extra

also conversions that cannot change a value, such as real(4) to real(8), which reports a = 0.1

gfortran

-Warray-temporaries

compile-time notice for each array temporary created for an argument

gfortran

-Wrealloc-lhs, -Wrealloc-lhs-all

reallocation of a left side by assignment

gfortran

-Waliasing

the literal aliasing of an intent(out) or intent(inout) dummy with another dummy

gfortran

-Wunused-dummy-argument, -Wimplicit-interface, -Wimplicit-procedure

unused dummy arguments; calls without an explicit interface

gfortran

-fcheck=all

run-time checks, among them bounds, array-temps, do, pointer, recursion and mem

gfortran

-finit-real=snan -finit-integer=n -finit-logical=... -finit-character=n

initialize uninitialized locals to a recognizable value

gfortran

-ffpe-trap=invalid,zero,overflow

stop at the first floating-point exception, with a backtrace under -g -fbacktrace

gfortran

-fstack-arrays, -fmax-stack-var-size=n, -frecursive, -Wstack-usage=n, -ftrapv

where arrays live; stack usage warnings; trapping integer overflow

Flang 23

-Wreal-constant-widening (on by default)

a default real literal in a wider context

Flang 23

-fstack-arrays

array temporaries on the stack

All the flags above were taken from gfortran --help=warnings, --help=fortran and --help=common, and from flang-23 --help and --help-hidden, in the container on 2026-10-19. A plain fpm build passes none of the warning flags, so a project that relies on it sees none of the warnings above until the flags are added to the profile; see Fortran Package Manager (fpm).

Where the books are outdated

The two Fortran books consulted for this section mostly list these traps correctly, because the rules have not changed since Fortran 90. What has moved:

Area Outdated in the books, current approach

Allocatable and intent(out)

the books teach intent(out) mainly for fixed-size arguments; since Fortran 2003 an allocatable dummy with intent(out) is deallocated on entry, which is a common source of bugs when intent(inout) was meant (8.5.10)

newunit=

unit numbers are chosen by the programmer in the older text; open(newunit=u) (Fortran 2008) removes the collision and iso_fortran_env names the standard units

Real literals

1.0d0 and double precision are shown as the way to get 64 bits; kinds from selected_real_kind or iso_fortran_env with _dp suffixes replace them

Run-time checking

no mention of the compiler-specific checks; -fcheck=all, -ffpe-trap and -finit-real=snan are the current practice, and they are options of GNU Fortran, not of the standard

Diagnostics

the code is compiled with the compiler’s defaults; with the flags of the previous section most of the traps above are reported, and -std=f2023 rejects non-standard extensions

Stack and heap

automatic arrays are described as ordinary local variables; on modern systems a large one is a stack overflow whose threshold depends on the compiler, optimization level and operating system limit

Compiler context

no compiler-specific behaviour; on this page GNU Fortran 16.2 and LLVM Flang 23 differ on the aliasing result, the unallocated intent(out) array, list-directed output and the stack

Standard version provenance and compiler support

Feature Introduced Compiler support checked on 2026-10-19

implicit save of explicitly initialized variables, intent, list-directed output, character comparison with blank padding

long-standing rules of the language (not dated here); current text in J3/23-007r1 8.4, 8.5.10, 13.10.4 and 10.1.5.5.1

all compilers

allocatable dummies deallocated by intent(out)

Fortran 2003 (allocatable dummy arguments, listed under TR 15581 in the GNU Fortran Fortran 2003 status page)

both; the unallocated case behaves differently, see above

newunit= in open

Fortran 2008

both

iso_fortran_env unit constants input_unit, output_unit, error_unit

Fortran 2003

both

Fortran 2023 additions that touch these traps

none: the rules of this page are unchanged in Fortran 2023

not applicable

See History, Standards and Compilers for the per-compiler support table, and Legacy Features and Modernization for the traps that come from fixed form, common and equivalence, which this page leaves out.

Pitfalls

  • Trusting a program because it printed a plausible answer. Out-of-bounds reads, uninitialized variables and aliased arguments all produced a plausible result in at least one compiler above. Build with -fcheck=all and -std=f2023 in development.

  • Testing with one compiler. Flang accepted the unallocated intent(out) array and the out-of-bounds read that gfortran stopped on; gfortran accepted the 400 MB automatic array that Flang crashed on.

  • Fixing a trap by changing a default. -fdefault-real-8, -finit-local-zero and ulimit -s unlimited hide the cause and change the program for other compilers.

  • Using list-directed output for anything that is compared or parsed.

  • Hard-coding unit numbers in library code.

  • Warnings ignored in a build that scrolls past. Treat the warnings of the flags above as errors with -Werror in continuous integration, after cleaning them up once.