Expressions and Assignment

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, working draft J3/23-007r1) and was checked on 2026-10-18. Every complete program on it was compiled warning-free with GNU Fortran 16.2 under gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all and run, and the examples that LLVM Flang 23 accepts were run with it as well. The output shown is the output that was produced. Examples that are deliberately wrong, rejected or warned about are labelled as such in the text and in a comment on their first line. Clause numbers refer to J3/23-007r1: expressions are clause 10.1, assignment is clause 10.2.

An expression computes a value, an assignment stores it, and most of the surprises in Fortran numerical code live in the gap between the two: an integer division where a real one was meant, a comparison that the processor may or may not short-circuit, a left-hand side that is silently reallocated, an array section whose shape does not conform. The page follows the running heat-diffusion scenario of this section, heatflow, and uses its module and type names (heatflow_kinds, grid_t, u, unew). The types that the operators work on are described in Intrinsic Types and Kinds and the declarations that give variables their attributes in Variables, Declarations and Attributes. The statements that consume logical expressions are in Control Flow, and whole-array expressions are developed further in Arrays Fundamentals and Allocatable Arrays and Dynamic Memory. Readers coming from C will find the same family of operators in the C Reference; the differences that matter are called out below.

How an expression is built

An expression is formed from operands, operators and parentheses (10.1.2.1). An operand is a scalar or an array, and an operation is either intrinsic, with a meaning built into the language, or defined, with a meaning supplied by a function through a generic interface. The standard organises the grammar into a primary, five levels of expression and the expression itself, and these levels are what give the precedence table its shape.

Category Contains Examples

primary

a constant, a variable or other designator, an array or structure constructor, a function reference, a parenthesised expression, and since F2023 a conditional expression

1.0_dp, u(i, j), [1, 2], max(a, b), (a + b), (c ? a : b)

level-1 expression

a primary, optionally preceded by a defined unary operator

.celsius. t

level-2 expression

level-1 expressions combined with the numeric operators **, *, /, binary + and -, with an optional leading unary + or -

2.0_dp*dt/dx**2, -a + b

level-3 expression

level-2 expressions combined with the character operator //

'jacobi' // '_' // 'sweep'

level-4 expression

level-3 expressions with at most one relational operator

(a + b) /= c

level-5 expression

level-4 expressions combined with .not., .and., .or., .eqv. and .neqv.

abs(r) <= tol .and. n < nmax

expression

level-5 expressions combined with defined binary operators

x .diff. y

A scalar and an array operand can be mixed: an elemental operation applies element by element, and a scalar is treated as an array of the right shape with every element equal to the scalar (10.1.10). Two array operands of an elemental binary operation must be conformable, that is, have the same shape. The result of an operation has the shape of the array operand (10.1.9.3), and the order in which the elements are computed is unspecified: for an operation that is intrinsically pure the element operations may be performed simultaneously or in any order (10.1.4).

Operators and precedence

Table 10.1 of the standard (10.1.3) fixes the precedence of every operator. The table below lists them from the tightest binding to the loosest, together with the form each one is spelled in and the version of the standard that introduced it.

Level Operators Operand types Since

1 (tightest)

defined unary operators, .name. x

as defined by the interface

Fortran 90

2

**, right to left

numeric; the result kind follows the operands

FORTRAN 66

3

* and /, left to right

numeric

FORTRAN 66

4

unary + and - (identity and negation)

numeric; only allowed at the start of a level-2 expression

FORTRAN 66

5

binary + and -, left to right

numeric

FORTRAN 66

6

//, left to right

character, equal kinds

FORTRAN 77

7

==, /=, <, <=, >, >= and the older spellings .eq., .ne., .lt., .le., .gt., .ge.

numeric, character, and (only == and /=) complex; the older spelling is identical in meaning

the dotted forms are FORTRAN 66, the symbols Fortran 90

8

.not.

logical

FORTRAN 66

9

.and., left to right

logical

FORTRAN 66

10

.or., left to right

logical

FORTRAN 66

11

.eqv. and .neqv., left to right

logical

FORTRAN 77

12 (loosest)

defined binary operators, x .name. y

as defined by the interface

Fortran 90

A few consequences of the grammar are worth memorising, because they differ from C and from most other languages:

  • ** associates from right to left, so 2**3**2 is 2**(3**2), which is 512, and it binds tighter than unary minus, so -2**2 is -(2**2), which is -4 (10.1.3, NOTE 1 and NOTE 2).

  • A unary - or + may only appear at the start of a level-2 expression, so a * -b and a ** -b are not valid. The compiler asks for parentheses, a * (-b). Because unary minus is looser than * and /, -a*b is -(a*b).

  • There is no bitwise .and. on integers. Bit operations are intrinsic functions (iand, ior, ieor, not, shiftl), described in Numeric Model, Bit Manipulation and Conversion.

  • A relational expression has at most one relational operator, so a < b < c is rejected; write a < b .and. b < c.

  • .not. is looser than the relational operators, so .not. a == b negates the comparison, and a second .not. directly after the first is not allowed (.not. .not. a is rejected; write .not. (.not. a)).

  • .and. binds tighter than .or., which binds tighter than .eqv., so a .eqv. b .or. c means a .eqv. (b .or. c).

  • A defined unary operator binds tighter than anything, a defined binary operator looser than anything, and an extended intrinsic operator keeps its intrinsic precedence (10.1.3, last sentence).

The first example evaluates each of these rules, using the heatflow_kinds module of the scenario so that every constant has an explicit kind.

module heatflow_kinds
  use, intrinsic :: iso_fortran_env, only: int32, int64, real32, real64
  implicit none
  private
  public :: sp, dp, ip, lp
  integer, parameter :: sp = real32
  integer, parameter :: dp = real64
  integer, parameter :: ip = int32
  integer, parameter :: lp = int64
end module heatflow_kinds

program precedence_demo
  use heatflow_kinds, only: dp, ip
  implicit none
  integer(ip) :: two, three
  real(dp) :: a
  logical :: p, q, r
  character(len=:), allocatable :: label

  two = 2_ip
  three = 3_ip
  a = 3.0_dp
  p = .true.
  q = .false.
  r = .false.
  label = 'ab'

  ! Numeric: ** binds tighter than unary minus and is right-associative.
  print '(a, i0)', '-two**2        = ', -two**2
  print '(a, i0)', '(-two)**2      = ', (-two)**2
  print '(a, i0)', 'two**three**2  = ', two**three**2
  print '(a, i0)', '(two**three)**2 = ', (two**three)**2
  print '(a, f0.1)', '-a*a           = ', -a*a
  ! Numeric operators bind tighter than relational ones.
  print '(a, l1)', 'two + 1 == three = ', two + 1_ip == three
  ! Concatenation binds tighter than relational ones.
  print '(a, l1)', 'label // "c" == "abc" = ', label // 'c' == 'abc'
  ! .not. is below relational operators, so it applies to the whole comparison.
  print '(a, l1)', '.not. two == three = ', .not. two == three
  ! .and. binds tighter than .or., which binds tighter than .eqv. / .neqv.
  print '(a, l1)', 'p .or. q .and. r   = ', p .or. q .and. r
  print '(a, l1)', '(p .or. q) .and. r = ', (p .or. q) .and. r
  print '(a, l1)', 'q .eqv. r .or. p   = ', q .eqv. r .or. p
  print '(a, l1)', '(q .eqv. r) .or. p = ', (q .eqv. r) .or. p
end program precedence_demo
-two**2        = -4
(-two)**2      = 4
two**three**2  = 512
(two**three)**2 = 64
-a*a           = -9.0
two + 1 == three = T
label // "c" == "abc" = T
.not. two == three = T
p .or. q .and. r   = T
(p .or. q) .and. r = F
q .eqv. r .or. p   = F
(q .eqv. r) .or. p = T

Two statements in the previous paragraph are rejected by the compiler. The file below is intentionally invalid: the first print compares two logical values with ==, which is allowed only for numeric and character operands, and the second puts a unary minus after **.

! REJECTED on purpose: neither statement is valid Fortran.
program rejected_operator_usage
  implicit none
  logical :: a, b
  real :: x, y
  a = .true.
  b = .false.
  x = 2.0
  y = 3.0
  print *, a == b
  print *, x ** -y
end program rejected_operator_usage

GNU Fortran 16.2 rejects it under -std=f2023, and LLVM Flang 23 rejects the first statement:

16_rejected_operator_usage.f90:11:18:

   11 |   print *, x ** -y
      |                  1
Error: Extension: Unary operator following arithmetic operator (use parentheses) at (1)
16_rejected_operator_usage.f90:10:10:

   10 |   print *, a == b
      |          1
Error: Logicals at (1) must be compared with .eqv. instead of ==
error: Semantic errors in 16_rejected_operator_usage.f90
16_rejected_operator_usage.f90:10:12: error: LOGICAL operands must be compared using .EQV. or .NEQV.
    print *, a == b
             ^^^^^^

gfortran labels the unary-after-binary form an "Extension" because without -std= it accepts it. The standard does not, and neither does a build with -std=f2023.

Numeric operations

The operands of a numeric operation may differ in type or kind (10.1.5.2.1). Except for a real or complex base raised to an integer power, the operand with the lower type or kind is converted first. The result type is the higher of the two types in the order integer, real, complex, and the result kind is the one with the greater decimal exponent range (integer) or precision (real, complex), as 10.1.9.3 specifies:

Operands Result type Result kind

integer with integer

integer

the kind with the greater decimal exponent range; if equal, processor dependent

integer with real or complex

real or complex

the kind of the real or complex operand; the integer is converted

real with real

real

the kind with the greater decimal precision

real with complex

complex

the kind with the greater decimal precision

real or complex raised to an integer power

real or complex

the kind of the base; the exponent is not converted

Three rules deserve their own paragraph, because they produce wrong numbers without any diagnostic:

  • Integer division truncates toward zero (10.1.5.2.2), so (-8)/3 is -2 and 5/2 is 2, whatever the type of the variable that receives the result. mod(a, p) has the sign of a, modulo(a, p) the sign of p. An integer raised to a negative integer power is the integer division 1/(x**abs(n)), which is 0 for every x with abs(x) > 1 (10.1.5.2.1). gfortran reports integer division of constants through -Winteger-division, part of -Wall, which is why the examples divide variables.

  • Operators of the same precedence combine left to right, so nx/ny*x divides two integers first and nx*x/ny does not. Convert explicitly with real(nx, dp) before dividing.

  • A real constant without a kind suffix has the default kind, which is single precision on every mainstream processor. x = 0.1 for a real(dp) variable stores a double-precision number that carries the single-precision error; 0.1_dp does not.

module heatflow_kinds
  use, intrinsic :: iso_fortran_env, only: int32, int64, real32, real64
  implicit none
  private
  public :: sp, dp, ip, lp
  integer, parameter :: sp = real32
  integer, parameter :: dp = real64
  integer, parameter :: ip = int32
  integer, parameter :: lp = int64
end module heatflow_kinds

program numeric_operations
  use heatflow_kinds, only: sp, dp, ip
  implicit none
  integer(ip) :: nx, ny, i, j
  real(dp) :: lx, dx, x
  real(sp) :: tenth_sp
  real(dp) :: tenth_dp

  ! Integer division truncates toward zero (10.1.5.2.2); mod follows the
  ! dividend, modulo follows the divisor.
  i = -8_ip
  j = 3_ip
  print '(a, i0)', 'i / j          = ', i/j
  print '(a, i0)', 'mod(i, j)      = ', mod(i, j)
  print '(a, i0)', 'modulo(i, j)   = ', modulo(i, j)
  print '(a, i0)', 'j ** (-2)      = ', j**(-2_ip)

  ! Mixed mode: the integer operand is converted to the real kind of the
  ! other operand. Convert before dividing to avoid integer division.
  nx = 5_ip
  ny = 2_ip
  lx = 1.0_dp
  dx = lx/real(nx - 1_ip, dp)
  print '(a, i0)', 'nx / ny        = ', nx/ny
  print '(a, f6.4)', 'real(nx, dp)/ny = ', real(nx, dp)/ny
  print '(a, f6.4)', 'dx = lx/(nx-1)  = ', dx

  ! Operators of equal precedence combine left to right, so the integer
  ! division in the first form happens before the multiplication.
  x = 2.5_dp
  print '(a, f6.4)', 'nx / ny * x    = ', nx/ny*x
  print '(a, f6.4)', 'nx * x / ny    = ', nx*x/ny

  ! A default real constant is single precision; its error survives
  ! assignment to a double precision variable.
  tenth_sp = 0.1
  tenth_dp = real(tenth_sp, dp)
  print '(a, es24.16)', 'real(0.1, dp)  = ', tenth_dp
  tenth_dp = 0.1_dp
  print '(a, es24.16)', '0.1_dp         = ', tenth_dp
end program numeric_operations
i / j          = -2
mod(i, j)      = -2
modulo(i, j)   = 1
j ** (-2)      = 0
nx / ny        = 2
real(nx, dp)/ny = 2.5000
dx = lx/(nx-1)  = 0.2500
nx / ny * x    = 5.0000
nx * x / ny    = 6.2500
real(0.1, dp)  =   1.0000000149011612E-01
0.1_dp         =   1.0000000000000001E-01

Raising a negative real value to a real power is prohibited (10.1.5.2.4), and so is any numeric operation whose result the arithmetic of the processor does not define. Integer division by zero belongs to the second group. It is not detected by -fcheck=all: the next program, which is non-conforming on purpose, ran to completion on the aarch64 build used for this page and printed 0, while other processors trap. Exceptions in floating-point operations are covered in Floating-Point and IEEE Arithmetic.

! NON-CONFORMING on purpose: division by zero is not defined by the
! arithmetic of the processor (10.1.5.2.4), and -fcheck=all does not catch it.
program integer_division_by_zero
  implicit none
  integer :: i, j
  i = 1
  j = 0
  print '(a, i0)', 'i / j = ', i/j
end program integer_division_by_zero
i / j = 0

Character and relational operations

The only intrinsic character operator is concatenation, // (10.1.5.3). Both operands must have the same character kind, and the result has the sum of the two lengths. Parentheses have no effect on its value, and a processor only has to evaluate as much of a concatenation as the context needs (for example, when the target has the length of the first operand only, the second operand need not be evaluated at all). Substrings, trim, adjustl and the rest of string handling are in Characters and Strings.

A relational operation always gives a result of default logical kind (10.1.5.5.1). The symbols ==, /=, <, <=, > and >= mean exactly what .eq., .ne., .lt., .le., .gt. and .ge. mean, and neither spelling is obsolescent. The rules that follow are the ones that cause trouble:

  • Numeric operands of different type or kind are converted to the type and kind of the expression x1 + x2 before comparing.

  • A complex operand can only be compared with == and /=; ordering complex numbers is a compile-time error.

  • Two logical operands cannot be compared with ==. Use .eqv. and .neqv..

  • Character operands are compared one character at a time. The shorter operand is treated as if it were extended on the right with blanks, so 'abc' == 'abc ' is true. The result of an ordering test depends on the processor’s collating sequence; == and /= do not. For comparisons that are guaranteed to use the ASCII sequence, use lge, lgt, lle and llt.

  • If an operand can be an IEEE NaN, .not. (x < y) and x >= y are not equivalent (10.1.5.5.2, NOTE), because every ordered comparison with a NaN is false.

  • Comparing real values for equality is rarely what a numerical program wants. gfortran’s -Wcompare-reals, part of -Wextra, flags == and /= on real and complex operands. Compare a difference with a tolerance instead.

module heatflow_kinds
  use, intrinsic :: iso_fortran_env, only: int32, int64, real32, real64
  implicit none
  private
  public :: sp, dp, ip, lp
  integer, parameter :: sp = real32
  integer, parameter :: dp = real64
  integer, parameter :: ip = int32
  integer, parameter :: lp = int64
end module heatflow_kinds

program relational_and_logical
  use, intrinsic :: ieee_arithmetic, only: ieee_value, ieee_quiet_nan
  use heatflow_kinds, only: dp
  implicit none
  real(dp) :: x, y, nan, residual, tolerance
  logical :: converged, hot, stable
  character(len=8) :: solver

  ! Character comparison pads the shorter operand with blanks (10.1.5.5.1).
  solver = 'jacobi'
  print '(a, l1)', "solver == 'jacobi'          = ", solver == 'jacobi'
  print '(a, l1)', "'abc' == 'abc   '           = ", 'abc' == 'abc   '
  print '(a, l1)', "'Jacobi' < 'jacobi'         = ", 'Jacobi' < 'jacobi'
  print '(a, l1)', "lgt('b', 'a')               = ", lgt('b', 'a')

  ! Logical values are compared with .eqv. and .neqv., never with ==.
  hot = .true.
  stable = .false.
  print '(a, l1)', 'hot .eqv. stable            = ', hot .eqv. stable
  print '(a, l1)', 'hot .neqv. stable           = ', hot .neqv. stable

  ! Real values: compare a difference with a tolerance instead of testing
  ! equality (gfortran -Wextra warns about == on reals).
  x = 0.1_dp + 0.2_dp
  y = 0.3_dp
  print '(a, es10.3)', 'abs(x - y)                  = ', abs(x - y)
  tolerance = 1.0e-12_dp
  residual = abs(x - y)
  converged = residual <= tolerance
  print '(a, l1)', 'residual <= tolerance       = ', converged

  ! With a NaN operand, .not. (a < b) and a >= b are different expressions.
  nan = ieee_value(nan, ieee_quiet_nan)
  x = 1.0_dp
  print '(a, l1)', '.not. (nan < x)             = ', .not. (nan < x)
  print '(a, l1)', 'nan >= x                    = ', nan >= x
end program relational_and_logical
solver == 'jacobi'          = T
'abc' == 'abc   '           = T
'Jacobi' < 'jacobi'         = T
lgt('b', 'a')               = T
hot .eqv. stable            = F
hot .neqv. stable           = T
abs(x - y)                  =  5.551E-17
residual <= tolerance       = T
.not. (nan < x)             = T
nan >= x                    = F

The logical operators .not., .and., .or., .eqv. and .neqv. take operands of type logical (10.1.5.4). If the operands have different kinds, the kind of the result is processor dependent but is that of one of the operands. Fortran has no .xor. operator in the standard; .neqv. is the exclusive or. Using integer operands with .and. or .or. as a bitwise operation, an extension that some other compilers offer, is not standard: GNU Fortran 16.2 and LLVM Flang 23 both reject i .and. j for integers. The comparison traps above, and the others that bite numerical code, are collected in Common Gotchas and Pitfalls.

Defined operations

A program can extend an intrinsic operator to new types, and can create new operators spelled .name., by writing a function and naming it in a generic interface operator(...) block (10.1.6). A defined unary operation needs a function with one dummy argument and a defined binary operation one with two, and the dummy arguments must be non-optional with intent(in) or value, as for every function that implements an operator. The same function can be bound to a type with a generic type-bound operator(...). Defined assignment, assignment(=), is implemented by a subroutine whose first argument is the left-hand side (intent(out) or intent(inout)) and whose second is the right-hand side (intent(in)).

The precedence of a defined operation is that of its operator: an extended + keeps the precedence of +, a .name. unary operator binds tighter than everything, and a .name. binary operator is the loosest. The following module keeps a temperature as a derived type and defines one extended operator, one unary operator, one binary operator and the assignment from a Celsius number. The full treatment of generic interfaces, including type-bound generics and elemental operators, is in Interfaces, Generics and Operator Overloading.

module heatflow_kinds
  use, intrinsic :: iso_fortran_env, only: int32, int64, real32, real64
  implicit none
  private
  public :: sp, dp, ip, lp
  integer, parameter :: sp = real32
  integer, parameter :: dp = real64
  integer, parameter :: ip = int32
  integer, parameter :: lp = int64
end module heatflow_kinds

module temperature_demo
  use heatflow_kinds, only: dp
  implicit none
  private
  public :: temperature_t, operator(+), operator(.celsius.), operator(.diff.), assignment(=)

  ! A temperature stored in kelvin; the type keeps scales from being mixed.
  type :: temperature_t
    real(dp) :: kelvin = 0.0_dp
  end type temperature_t

  interface operator(+)
    module procedure add_delta
  end interface

  interface operator(.celsius.)
    module procedure to_celsius
  end interface

  interface operator(.diff.)
    module procedure difference
  end interface

  interface assignment(=)
    module procedure set_from_celsius
  end interface

contains

  pure function add_delta(t, delta) result(r)
    type(temperature_t), intent(in) :: t
    real(dp), intent(in) :: delta
    type(temperature_t) :: r
    r%kelvin = t%kelvin + delta
  end function add_delta

  pure function to_celsius(t) result(c)
    type(temperature_t), intent(in) :: t
    real(dp) :: c
    c = t%kelvin - 273.15_dp
  end function to_celsius

  pure function difference(a, b) result(d)
    type(temperature_t), intent(in) :: a, b
    real(dp) :: d
    d = a%kelvin - b%kelvin
  end function difference

  ! t = 21.5_dp stores a Celsius value as kelvin.
  pure subroutine set_from_celsius(t, celsius)
    type(temperature_t), intent(out) :: t
    real(dp), intent(in) :: celsius
    t%kelvin = celsius + 273.15_dp
  end subroutine set_from_celsius

end module temperature_demo

program defined_operators_and_assignment
  use heatflow_kinds, only: dp
  use temperature_demo, only: temperature_t, operator(+), operator(.celsius.), &
    operator(.diff.), assignment(=)
  implicit none
  type(temperature_t) :: ambient, hot, warmer

  ambient = 20.0_dp
  hot = 80.0_dp
  warmer = ambient + 5.0_dp
  print '(a, f7.2)', 'ambient in Celsius:      ', .celsius. ambient
  print '(a, f7.2)', 'warmer in Celsius:       ', .celsius. warmer
  ! A defined binary operator has the lowest precedence, so both sums are
  ! formed first: (hot + 0) .diff. (ambient + 0).
  print '(a, f7.2)', 'hot .diff. ambient:      ', hot + 0.0_dp .diff. ambient + 0.0_dp
  ! A defined unary operator has the highest precedence: it applies to
  ! "ambient", not to the sum.
  print '(a, f7.2)', '.celsius. ambient + 1.0: ', .celsius. ambient + 1.0_dp
end program defined_operators_and_assignment
ambient in Celsius:        20.00
warmer in Celsius:         25.00
hot .diff. ambient:        60.00
.celsius. ambient + 1.0:   21.00

The line hot + 0.0_dp .diff. ambient + 0.0_dp shows the rule in action: both additions are carried out before the loosest operator, .diff., is applied.

Evaluation rules: what the processor may do

Fortran specifies the value an expression has, not the steps by which a processor reaches it. The freedom this gives an optimizing compiler is large and is the reason Fortran code vectorizes well. It is also the source of three rules that programmers from C and Java must unlearn.

No guaranteed short-circuit evaluation

"It is not necessary for a processor to evaluate all of the operands of an expression, or to evaluate entirely each operand, if the value of the expression can be determined otherwise" (10.1.7). The sentence cuts both ways: the processor may skip the right operand of .and. when the left one is false, and it may evaluate it anyway. Neither .and. nor .or. promises an order, and a test written to protect a subscript or a division with a guard does not protect it.

! NON-CONFORMING on purpose: the guard does not protect the subscript.
! The standard lets the processor evaluate both operands of .and. (10.1.7).
module guard_demo
  implicit none
contains
  function first_positive(a, i) result(ok)
    real, intent(in) :: a(:)
    integer, intent(in) :: i
    logical :: ok
    ok = i <= size(a) .and. a(i) > 0.0
  end function first_positive
end module guard_demo

program no_short_circuit
  use guard_demo, only: first_positive
  implicit none
  real :: a(3)
  a = [1.0, -2.0, 3.0]
  print '(a, l1)', 'i = 2: ', first_positive(a, 2)
  print '(a, l1)', 'i = 4: ', first_positive(a, 4)
end program no_short_circuit

This program is non-conforming on purpose. Built with the checks of the strict build, GNU Fortran stops at the out-of-range subscript, which proves that the right operand of .and. was evaluated even though the left one was already false. LLVM Flang 23 runs the program to completion and prints F for the second call, and so does gfortran without -fcheck=all (also at -O2). All of these behaviours are allowed, because the program is not standard-conforming:

At line 10 of file 03_no_short_circuit_nonconforming.f90
Fortran runtime error: Index '4' of dimension 1 of array 'a' above upper bound of 3

Error termination. Backtrace:
i = 2: F
#0  0xffffa37f196f in ???
#1  0xffffa37f26b3 in ???
#2  0xffffa37f2c13 in ???
#3  0x400ae3 in __guard_demo_MOD_first_positive
	at /work/03_no_short_circuit_nonconforming.f90:10
#4  0x400d43 in no_short_circuit
	at /work/03_no_short_circuit_nonconforming.f90:20
#5  0x400da7 in main
	at /work/03_no_short_circuit_nonconforming.f90:15
i = 2: F
i = 4: F

The portable fixes are a nested if (see Control Flow) or, since Fortran 2023, a conditional expression, described in the next section. The intrinsic merge is not a fix: it is a function, and the standard does not promise that the argument it does not select is left unevaluated, so compilers are free to evaluate both.

A related statement of the same clause: if a function reference sits in a part of an expression that need not be evaluated, every entity that the function would have defined becomes undefined, whether or not the processor really skipped the call (10.1.7, NOTE 2). A function that has side effects therefore makes the whole statement fragile.

Function side effects and why they are restricted

A function reference in an expression may not change anything that the same statement also uses (10.1.4). The standard puts it as two rules: the evaluation of a function reference shall neither affect nor be affected by the evaluation of any other entity in the statement, and if the reference defines or undefines an actual argument, that argument shall not appear elsewhere in the statement. So a(i) = f(i) is prohibited if f modifies i, and so is y = g(x) + x if g modifies x. The only exceptions are the logical expression of an if statement and the mask of a where statement, whose subsidiary statement may be affected.

The reason is the freedom described above. The processor may reorder operands, evaluate a function once instead of twice, or not at all. GNU Fortran does exactly this on request: -ffrontend-optimize, enabled by default at every -O level except -O0 and -Og, eliminates identical function calls within an expression for pure functions, and -faggressive-function-elimination does it even for functions not marked pure, so a = f(b, c) + f(b, c) calls f once. A function with side effects can then be called fewer times than the source suggests.

The language gives the programmer a way to say that a function is safe: pure functions have no side effects (they may not modify their dummy arguments, module variables or perform I/O), elemental functions are pure unless declared impure, and Fortran 2023 adds simple procedures, pure procedures that are further restricted from referencing variables accessed by host or use association. Procedure attributes are described in Procedures, Functions and Subroutines. The consequence for expressions is practical: use subroutines for work with side effects, and use pure functions for values.

Mathematically equivalent expressions and the integrity of parentheses

Once the interpretation of a numeric expression is fixed by the precedence rules, "the processor may evaluate any mathematically equivalent expression, provided that the integrity of parentheses is not violated" (10.1.5.2.4). The standard lists allowed rewrites, for example x + y + z as x + (y + z), a / b / c as a / (b * c) and a / 5.0 as 0.2 * a, and forbidden ones, for example x * i / j as x * (i / j), i / 2 as 0.5 * i and the regrouping of (x + y) + z. Two numeric expressions are mathematically equivalent when they are equal for all values of their operands, but "mathematically equivalent expressions of numeric type can produce different computational results". The same freedom exists for logical expressions (l1 .and. l2 .and. l3 may become l1 .and. (l2 .and. l3)) and relational ones (10.1.5.4.2 and 10.1.5.5.2).

A parenthesised expression "shall be treated as a data entity" (10.1.8): a + (b - c) evaluates b - c before the addition and may not become (a + b) - c. Parentheses are the programmer’s tool for controlling intermediate magnitude and rounding.

module heatflow_kinds
  use, intrinsic :: iso_fortran_env, only: int32, int64, real32, real64
  implicit none
  private
  public :: sp, dp, ip, lp
  integer, parameter :: sp = real32
  integer, parameter :: dp = real64
  integer, parameter :: ip = int32
  integer, parameter :: lp = int64
end module heatflow_kinds

program parentheses_and_rounding
  use heatflow_kinds, only: dp
  implicit none
  real(dp) :: a, b, c, plain, left_first, right_first

  a = 1.0e16_dp
  b = -1.0e16_dp
  c = 1.0_dp

  ! The three sums are mathematically equal and computationally different.
  ! Without parentheses the processor may choose either grouping (10.1.5.2.4);
  ! parentheses written in the source must be honoured (10.1.8).
  plain = a + b + c
  left_first = (a + b) + c
  right_first = a + (b + c)
  print '(a, f4.1)', 'a + b + c   = ', plain
  print '(a, f4.1)', '(a + b) + c = ', left_first
  print '(a, f4.1)', 'a + (b + c) = ', right_first
end program parentheses_and_rounding
a + b + c   =  1.0
(a + b) + c =  1.0
a + (b + c) =  0.0

The three results differ although the sums are mathematically equal. The last two are fixed by the parentheses; the first is whichever grouping the compiler chose.

The processor’s freedom interacts with compiler options. GNU Fortran honours parentheses at every optimization level unless you ask otherwise: -fno-protect-parens lets it reassociate real and complex expressions (and -Ofast turns the protection off). Reassociation is only worth the trouble in performance-critical kernels, and it should be verified against a tolerance. Optimization flags are discussed in Compilers and Compiler Options and Performance and Memory Layout.

Conditional expressions (Fortran 2023)

Fortran 2023 adds a conditional expression, a primary written ( condition ? value : alternative ) (R1002 in 10.1.2.3):

( scalar-logical-expr ? expr [ : scalar-logical-expr ? expr ]... : expr )

The evaluation rule (10.1.4) is what makes it useful: the logical expressions are evaluated in order until one is true, the expr after it is chosen, and otherwise the last expr is chosen. Only the chosen expr is evaluated. "Only one expr of a conditional expression is evaluated, and any of its scalar-logical-exprs subsequent to one that evaluates to true are not evaluated" (10.1.4, NOTE 3). That is the guaranteed short-circuit that .and. and merge do not give.

The rules for the branches (C1004) are short: every expr must have the same declared type, kind type parameters and rank; the lengths of character branches may differ, and the length (and, for a polymorphic branch, the dynamic type) is that of the chosen branch. A conditional expression is a primary, so it can appear wherever an expression can. It is not a constant expression (10.1.12 does not list it), so it cannot initialise a named constant. GNU Fortran 16.2 stops with an internal compiler error when a conditional expression is used directly as a print item; assigning it to a variable first works, see What’s New and Fortran 2028 Preview.

The next module uses the three typical idioms: a guard that protects a subscript, an optional argument with a default value (an absent optional argument may only be referenced in a few permitted ways, and a conditional expression with present is one of the cleanest), and a character result whose branches differ in length.

module heatflow_kinds
  use, intrinsic :: iso_fortran_env, only: int32, int64, real32, real64
  implicit none
  private
  public :: sp, dp, ip, lp
  integer, parameter :: sp = real32
  integer, parameter :: dp = real64
  integer, parameter :: ip = int32
  integer, parameter :: lp = int64
end module heatflow_kinds

module guard_demo
  use heatflow_kinds, only: dp, ip
  implicit none
  private
  public :: positive_at, sample, describe

contains

  ! Safe guard: the subscript is only evaluated when the condition is true.
  pure function positive_at(a, i) result(ok)
    real(dp), intent(in) :: a(:)
    integer(ip), intent(in) :: i
    logical :: ok
    ok = (i >= 1_ip .and. i <= size(a, kind=ip) ? a(i) > 0.0_dp : .false.)
  end function positive_at

  ! Optional argument with a default, without a second variable.
  pure function sample(a, i, fill) result(v)
    real(dp), intent(in) :: a(:)
    integer(ip), intent(in) :: i
    real(dp), intent(in), optional :: fill
    real(dp) :: v
    v = (i >= 1_ip .and. i <= size(a, kind=ip) ? a(i) : present(fill) ? fill : 0.0_dp)
  end function sample

  ! The branches may have different character lengths; the result takes the
  ! length of the chosen one.
  function describe(residual, tolerance) result(text)
    real(dp), intent(in) :: residual, tolerance
    character(len=:), allocatable :: text
    text = (abs(residual) <= tolerance ? 'ok' : 'did not converge')
  end function describe

end module guard_demo

program conditional_expressions
  use heatflow_kinds, only: dp, ip
  use guard_demo, only: positive_at, sample, describe
  implicit none
  real(dp) :: a(3)
  integer(ip) :: i

  a = [1.0_dp, -2.0_dp, 3.0_dp]
  do i = 0_ip, 4_ip
    print '(a, i0, a, l1, a, f6.2, a, f6.2)', 'i = ', i, '  positive: ', positive_at(a, i), &
      '  sample: ', sample(a, i), '  sample(fill=-1): ', sample(a, i, -1.0_dp)
  end do

  print '(a)', describe(2.0e-5_dp, 1.0e-4_dp)
  print '(a)', describe(3.0e-2_dp, 1.0e-4_dp)

  ! Chain of conditions, evaluated in order.
  print '(a)', (a(1) > 5.0_dp ? 'big' : a(1) > 0.5_dp ? 'medium' : 'small')
end program conditional_expressions
i = 0  positive: F  sample:   0.00  sample(fill=-1):  -1.00
i = 1  positive: T  sample:   1.00  sample(fill=-1):   1.00
i = 2  positive: F  sample:  -2.00  sample(fill=-1):  -2.00
i = 3  positive: T  sample:   3.00  sample(fill=-1):   3.00
i = 4  positive: F  sample:   0.00  sample(fill=-1):  -1.00
ok
did not converge
medium

The out-of-range indices 0 and 4 never touch a, even though the program ran with -fcheck=all.

The type rule is enforced by the compiler. The next file is rejected on purpose: it tries to choose between an integer and a real:

! REJECTED on purpose: the branches of a conditional expression must have
! the same declared type and kind (C1004).
program conditional_mixed_types
  implicit none
  integer :: i = 1
  print *, (i > 0 ? 1 : 2.0)
end program conditional_mixed_types
17_conditional_expression_rejected.f90:6:20-23:

    6 |   print *, (i > 0 ? 1 : 2.0)
      |                    1  2
Error: expr at (1) and expr at (2) in conditional expression must have the same declared type
error: Semantic errors in 17_conditional_expression_rejected.f90
17_conditional_expression_rejected.f90:6:12: error: All values in conditional expression must have the same type and kind; have INTEGER(4) and REAL(4)
    print *, (i > 0 ? 1 : 2.0)
             ^^^^^^^^^^^^^^^^^

Compiler support

Conditional expressions are one of the newest features of the language, and implementations differ in how much of it they accept. On 2026-10-18 GNU Fortran 16.2 accepts scalar conditional expressions but rejects array-valued ones, although the standard allows any rank as long as all branches match. This file is valid Fortran 2023 that gfortran 16.2 rejects, and LLVM Flang 23 accepts it:

! Valid F2023 (rank may be > 0), but REJECTED by gfortran 16.2 as not yet
! implemented; LLVM Flang 23 accepts it.
program conditional_array
  implicit none
  real :: a(3) = [1.0, -2.0, 3.0]
  print '(3f6.1)', (a(1) > 0.0 ? a : -a)
end program conditional_array
18_conditional_array_gfortran_rejected.f90:6:18:

    6 |   print '(3f6.1)', (a(1) > 0.0 ? a : -a)
      |                  1
Error: Sorry, array is currently unsupported for conditional expressions at (1)
   1.0  -2.0   3.0

LLVM’s own support table, Fortran standards support in LLVM Flang, lists "Conditional expressions and arguments" as not yet supported, while the LLVM 23 build used for this page nevertheless compiled and ran the scalar and array examples above; the table was the reference at the time of writing and implementations move quickly, so check your own compiler. GNU Fortran has no Fortran 2023 status page at all (the manual’s page of that name returns 404), so its behaviour has to be tested. The related Fortran 2023 feature, conditional arguments using .nil. to pass an absent optional argument, is not exercised here; the per-compiler support table is on History, Standards and Compilers. Code that has to build with older compilers should use the nested-if form.

Intrinsic assignment

An assignment statement has the form variable = expr (R1033, 10.2.1.1). It is either an intrinsic assignment, whose meaning is built into the language, or a defined assignment through assignment(=) (shown above). Execution of an intrinsic assignment "shall have the same effect as if the evaluation of expr and the evaluation of all expressions in variable occurred before any portion of the variable is defined" (10.2.1.3). The consequence is that the right-hand side can reference the variable that is being assigned, and that overlapping array sections behave as if a temporary had been used.

The constraints in 10.2.1.2 say which combinations are allowed. In summary:

Variable Allowed expr (Table 10.8) What happens

integer, real, complex

integer, real or complex

expr is converted to the type and kind of the variable (Table 10.9)

character

character

padded with blanks or truncated on the right to the length of the variable; a different kind is converted character by character

logical

logical

a different kind is converted

derived type

the same derived type

component-wise assignment, see below

enumeration type

the same enumeration type

a Fortran 2023 type; see Enumerations

enum type (named enum, bind(c) definition, Fortran 2023)

the same enum type, or an integer

an integer must be an enumerator of the enum type

integer or real

a BOZ literal constant

converted to the variable’s type and kind (extended in Fortran 2023)

Other combinations are not allowed: a logical cannot be assigned from an integer, nor a character from a number. If expr is an array, the variable must also be an array. A polymorphic variable must be allocatable and not a coarray, so that its dynamic type can follow the right-hand side (see Object Oriented Programming).

Numeric conversion

When expr has another numeric type or kind it is converted by int(expr, kind=kind(variable)), real(...) or cmplx(...) (Table 10.9). Conversion from real to integer truncates toward zero; from a wider to a narrower real kind it rounds; from real to complex it supplies an imaginary part of zero. These conversions are silent unless the compiler is asked: gfortran’s -Wconversion (part of -Wall) reports the ones that can change the value. The following program compiles, with warnings on purpose, and shows the conversions that Table 10.9 specifies, next to the explicit nint that most numerical code actually wants.

! Compiles, but gfortran -Wall warns on purpose (-Wconversion): each
! assignment converts implicitly by the rules of Table 10.9.
module heatflow_kinds
  use, intrinsic :: iso_fortran_env, only: int32, int64, real32, real64
  implicit none
  private
  public :: sp, dp, ip, lp
  integer, parameter :: sp = real32
  integer, parameter :: dp = real64
  integer, parameter :: ip = int32
  integer, parameter :: lp = int64
end module heatflow_kinds

program implicit_conversion
  use heatflow_kinds, only: sp, dp, ip
  implicit none
  real(dp) :: t
  integer(ip) :: cells, rounded
  real(sp) :: t_single
  complex(dp) :: z

  t = 3.99_dp
  cells = t              ! INT(t, kind(cells)): truncation toward zero
  rounded = nint(t, ip)  ! explicit rounding
  t_single = t           ! REAL(t, kind(t_single)): precision is lost
  z = t                  ! CMPLX(t, kind=kind(z)): imaginary part 0
  print '(a, i0)', 'cells   = ', cells
  print '(a, i0)', 'rounded = ', rounded
  print '(a, f10.7)', 't_single = ', t_single
  print '(a, 2f6.2)', 'z = ', z
end program implicit_conversion

GNU Fortran 16.2 prints the two warnings below at compile time (they are trimmed to the message lines) and then the program output:

15_implicit_conversion_warns.f90:23:10: Warning: Possible change of value in conversion from REAL(8) to INTEGER(4) at (1) [-Wconversion]
15_implicit_conversion_warns.f90:25:13: Warning: Possible change of value in conversion from REAL(8) to REAL(4) at (1) [-Wconversion]
cells   = 3
rounded = 4
t_single =  3.9900000
z =   3.99  0.00

Character assignment

A character variable of fixed length always keeps its length (10.2.1.3): a shorter value is extended on the right with blanks and a longer value is truncated from the right. There is no error, and gfortran’s -Wcharacter-truncation can warn when the truncation is visible at compile time. If the kinds differ, each character is converted by achar(iachar(c), kind(variable)); a character that is not representable in the target kind is replaced by a processor-dependent one.

The assignment may reference the variable on the right: s(2:5) = s(1:4) on 'ABCDEF' gives 'AABCDF', because the right-hand side is evaluated completely first. And a character variable with a deferred length (character(len=:), allocatable) takes the length of the value, as described in the next section, unless the left-hand side is written with the substring name(:), which assigns to the existing allocation:

program character_assignment
  implicit none
  character(len=8) :: fixed
  character(len=:), allocatable :: label, first, surname
  character(len=6) :: work

  ! Fixed-length target: the value is padded with blanks or truncated on the right.
  first = 'Joseph'
  surname = 'Fourier'
  fixed = first
  print '(3a)', '[', fixed, ']'
  fixed = first // ' ' // surname
  print '(3a)', '[', fixed, ']'

  ! Deferred-length allocatable target: reallocated to the length of the value.
  label = first
  print '(a, i0, 3a)', 'len=', len(label), ' [', label, ']'
  label = 'Dr. ' // first // ' ' // surname
  print '(a, i0, 3a)', 'len=', len(label), ' [', label, ']'

  ! With (:) the target keeps its length: pad or truncate again.
  label(:) = first
  print '(a, i0, 3a)', 'len=', len(label), ' [', label, ']'

  ! The right-hand side is evaluated completely before the target changes.
  work = 'ABCDEF'
  work(2:5) = work(1:4)
  print '(3a)', '[', work, ']'
end program character_assignment
[Joseph  ]
[Joseph F]
len=6 [Joseph]
len=18 [Dr. Joseph Fourier]
len=18 [Joseph            ]
[AABCDF]

The output shows the three behaviours. fixed is a character(len=8) variable, so 'Joseph Fourier' is cut to 'Joseph F'. The deferred-length label is 6 characters long after label = first, and 18 after the concatenation, with no declaration of the new length. After label(:) = first its length stays 18 and the value is padded. More on strings is in Characters and Strings.

Automatic (re)allocation of allocatable left-hand sides

Since Fortran 2003 the left-hand side of an intrinsic assignment may be an allocatable variable that is unallocated or has a different shape, and the assignment then allocates or reallocates it (10.2.1.3). The rules in the standard, applied in this order, are:

  1. The expression expr and every expression inside variable are evaluated.

  2. If the variable is unallocated, expr must have the same rank, and the variable is allocated (below).

  3. If the variable is allocated and expr is an array of a different shape, or the length type parameters differ, or the variable is polymorphic and the dynamic type or kind parameters differ, the variable is deallocated and allocated again.

  4. The new allocation takes the dynamic type and kind parameters of expr if the variable is polymorphic, each deferred length parameter from expr, the shape of expr with each lower bound equal to lbound(expr), and, if expr is a scalar and the variable an array, the same bounds as before.

  5. The values are copied, with the conversions of the previous section.

flowchart TD start(["v = expr (v allocatable)"]) --> ev["evaluate expr and all subscripts of v"] ev --> alloc{"v allocated?"} alloc -- no --> rank{"same rank as expr?"} rank -- no --> bad(["not conforming: expr scalar or other rank"]) rank -- yes --> make["allocate v: shape, lower bounds, deferred length, dynamic type from expr"] alloc -- yes --> diff{"shape, length parameters or dynamic type differ?"} diff -- yes --> redo["deallocate v, then allocate as above"] diff -- no --> keep["keep the existing allocation and its bounds"] make --> copy["copy expr into v, converting if needed"] redo --> copy keep --> copy copy --> done(["v is allocated and defined"])

The flowchart summarises the decision. The program below exercises it with u and unew, the two fields of the heat solver, plus a halo array declared with lower bounds of 0 to show where the bounds of the new allocation come from. The scalar u = 0.0_dp keeps the existing shape, and u(:, :) = ..., with a section on the left, never reallocates.

module heatflow_kinds
  use, intrinsic :: iso_fortran_env, only: int32, int64, real32, real64
  implicit none
  private
  public :: sp, dp, ip, lp
  integer, parameter :: sp = real32
  integer, parameter :: dp = real64
  integer, parameter :: ip = int32
  integer, parameter :: lp = int64
end module heatflow_kinds

module field_tools
  use heatflow_kinds, only: dp, ip
  implicit none
  private
  public :: coarsen, report

contains

  ! Keep every second point in each direction.
  pure function coarsen(u) result(c)
    real(dp), intent(in) :: u(:, :)
    real(dp), allocatable :: c(:, :)
    c = u(1:size(u, 1):2, 1:size(u, 2):2)
  end function coarsen

  subroutine report(name, u)
    character(len=*), intent(in) :: name
    real(dp), allocatable, intent(in) :: u(:, :)
    if (allocated(u)) then
      print '(a, a, i0, a, i0, a, i0, a, i0)', name, ': shape ', size(u, 1), ' x ', size(u, 2), &
        ', lbound ', lbound(u, 1), ', ', lbound(u, 2)
    else
      print '(a, a)', name, ': not allocated'
    end if
  end subroutine report

end module field_tools

program allocatable_assignment
  use heatflow_kinds, only: dp, ip
  use field_tools, only: coarsen, report
  implicit none
  real(dp), allocatable :: u(:, :), unew(:, :)
  real(dp) :: fine(9, 9)
  real(dp) :: halo(0:4, 0:3)
  integer(ip) :: i

  fine = reshape([(real(i, dp), i=1_ip, 81_ip)], shape(fine))

  call report('u', u)
  ! Unallocated target: allocated with the shape of the right-hand side.
  u = fine
  call report('u = fine', u)
  ! Same shape: no reallocation, just the copy.
  unew = u
  call report('unew = u', unew)
  ! Different shape: u is deallocated and allocated again.
  u = coarsen(fine)
  call report('u = coarsen(fine)', u)
  ! A scalar is broadcast to an allocated array; the shape is kept.
  u = 0.0_dp
  call report('u = 0', u)
  ! Section on the left: no reallocation, the shapes must conform.
  u(:, :) = coarsen(fine)
  call report('u(:,:) = ...', u)
  ! A section or a function result has lower bounds 1 ...
  u = fine(3:5, 4:6)
  call report('u = fine(3:5, 4:6)', u)
  ! ... but a whole array keeps its declared lower bounds, and the
  ! reallocated target takes them over.
  halo = 1.0_dp
  u = halo
  call report('u = halo', u)
  deallocate (u)
  call report('deallocate(u)', u)
  print '(a, f6.1)', 'unew(9, 9) = ', unew(9, 9)
end program allocatable_assignment
u: not allocated
u = fine: shape 9 x 9, lbound 1, 1
unew = u: shape 9 x 9, lbound 1, 1
u = coarsen(fine): shape 5 x 5, lbound 1, 1
u = 0: shape 5 x 5, lbound 1, 1
u(:,:) = ...: shape 5 x 5, lbound 1, 1
u = fine(3:5, 4:6): shape 3 x 3, lbound 1, 1
u = halo: shape 5 x 4, lbound 0, 0
deallocate(u): not allocated
unew(9, 9) =   81.0

Notice what the example shows:

  • u = fine allocated an unallocated u. unew = u allocated unew with the shape of u.

  • u = coarsen(fine) changed the shape, so u was reallocated. If the shape had been equal, the existing allocation would have been reused, with its old bounds.

  • A section and a function result have lower bounds of 1, but a whole array keeps its declared lower bounds, and the reallocated u takes them over (lbound 0 after u = halo).

  • A scalar on the right does not reallocate an allocated array; it is broadcast.

The two ways to get this wrong are not the same error, and compilers diagnose them differently. The first program is non-conforming on purpose: a scalar cannot supply a shape, so assigning it to an unallocated array is not allowed.

! NON-CONFORMING on purpose: a scalar cannot define the shape of an
! unallocated array target (10.2.1.3).
program scalar_into_unallocated
  implicit none
  real, allocatable :: u(:, :)
  u = 0.0
  print '(a, l1)', 'allocated(u) = ', allocated(u)
end program scalar_into_unallocated

GNU Fortran 16.2 warns about uninitialized bounds (trimmed below) and then leaves u unallocated without a run-time error, even with -fcheck=all; LLVM Flang 23 stops at run time:

08a_scalar_into_unallocated_nonconforming.f90:6:9:
Warning: 'u.dim[0].ubound' may be used uninitialized [-Wmaybe-uninitialized]
(three more warnings of the same kind)
allocated(u) = F
fatal Fortran runtime error(08a_scalar_into_unallocated_nonconforming.f90:6): Assign: mismatched ranks (2 != 0) in assignment to unallocated allocatable
Aborted

The second is non-conforming on purpose too. When the left-hand side is u(:, :), the section designator turns off the reallocation, and the shapes of u (4 by 4) and v (3 by 3) must conform:

! NON-CONFORMING on purpose: u(:, :) is a section, so no reallocation
! happens and the shapes must conform (10.2.1.2 item 3).
program section_shape_mismatch
  implicit none
  real, allocatable :: u(:, :), v(:, :)
  allocate (u(4, 4), v(3, 3))
  u = 0.0
  v = 1.0
  u(:, :) = v
  print '(a, f4.1)', 'u(1, 1) = ', u(1, 1)
end program section_shape_mismatch
At line 9 of file 08b_section_shape_mismatch_nonconforming.f90
Fortran runtime error: Array bound mismatch for dimension 1 of array 'u' (4/3)
fatal Fortran runtime error(08b_section_shape_mismatch_nonconforming.f90:9): Assign: mismatching element counts in array assignment (to 16, from 9)
Aborted

Reallocation has a cost, and it can hide mistakes. A hot loop that assigns a new array to the same variable on every iteration pays an allocation check each time, and a typo that gives u = unew(2:nx-1, :) a smaller shape changes the grid without a diagnostic. GNU Fortran’s -Wrealloc-lhs warns where the compiler might insert allocation or reallocation code for an array of intrinsic type and -Wrealloc-lhs-all also covers scalars and derived types. The manual recommends writing the left-hand side as a whole-array section such as u(:, :) where the array is known to be allocated with the right shape, which removes the check. -frealloc-lhs is on by default except with -std=f95, and -fno-realloc-lhs returns to the Fortran 95 behaviour where the left-hand side must already match. Other compilers have equivalent switches; see their manuals, and Allocatable Arrays and Dynamic Memory for allocate, move_alloc and the lifetime rules.

Array assignment and conformance

If the variable is an array, an intrinsic assignment is performed element by element; a scalar expr is treated as an array of the same shape with every element equal to the scalar (10.2.1.3). The shape rule is strict: unless the variable is an allocatable array of the same rank, the variable and expr must be conformable, that is, have the same shape. An array is not conformable with another array of a different shape even if the number of elements matches. The element assignments may be performed in any order, and the whole of expr is evaluated before anything is stored, which is why x(1:10) = x(10:1:-1) reverses x (10.2.1.3, NOTE 4).

Array syntax is not just shorthand for a loop; it is a statement that tells the compiler there is no dependence between element computations. The solver kernels of heatflow express a time step of the finite-difference scheme as one array assignment on sections. In the program below the section arithmetic updates the interior of a 5 by 5 field after a single hot cell, then reverses a vector, and finally clips values with a masked assignment, where.

module heatflow_kinds
  use, intrinsic :: iso_fortran_env, only: int32, int64, real32, real64
  implicit none
  private
  public :: sp, dp, ip, lp
  integer, parameter :: sp = real32
  integer, parameter :: dp = real64
  integer, parameter :: ip = int32
  integer, parameter :: lp = int64
end module heatflow_kinds

program array_assignment_and_where
  use heatflow_kinds, only: dp, ip
  implicit none
  integer(ip), parameter :: nx = 5_ip, ny = 5_ip
  real(dp), parameter :: dx = 0.25_dp, dy = 0.25_dp, alpha = 1.0_dp, dt = 0.01_dp
  real(dp) :: u(nx, ny), unew(nx, ny)
  real(dp) :: line(6)
  integer(ip) :: j

  ! A scalar is broadcast to every element.
  u = 0.0_dp
  u(3, 3) = 1.0_dp

  ! Whole-array arithmetic on conformable sections: one FTCS step of the heat
  ! equation. Every right-hand side is evaluated before unew is changed.
  unew = u
  unew(2:nx - 1, 2:ny - 1) = u(2:nx - 1, 2:ny - 1) + alpha*dt*( &
    (u(3:nx, 2:ny - 1) - 2.0_dp*u(2:nx - 1, 2:ny - 1) + u(1:nx - 2, 2:ny - 1))/dx**2 + &
    (u(2:nx - 1, 3:ny) - 2.0_dp*u(2:nx - 1, 2:ny - 1) + u(2:nx - 1, 1:ny - 2))/dy**2)
  do j = 1_ip, ny
    print '(5f8.4)', unew(:, j)
  end do

  ! Overlapping sections: the whole right-hand side is evaluated first.
  line = [1.0_dp, 2.0_dp, 3.0_dp, 4.0_dp, 5.0_dp, 6.0_dp]
  line(1:6) = line(6:1:-1)
  print '(6f5.1)', line

  ! Masked assignment: clip temperatures, element by element.
  where (unew > 0.3_dp)
    unew = 0.3_dp
  elsewhere (unew < 0.01_dp)
    unew = 0.0_dp
  end where
  print '(a, f6.3, a, f6.3)', 'max = ', maxval(unew), '  min = ', minval(unew)
  print '(a, i0)', 'cells above zero = ', count(unew > 0.0_dp)
end program array_assignment_and_where
  0.0000  0.0000  0.0000  0.0000  0.0000
  0.0000  0.0000  0.1600  0.0000  0.0000
  0.0000  0.1600  0.3600  0.1600  0.0000
  0.0000  0.0000  0.1600  0.0000  0.0000
  0.0000  0.0000  0.0000  0.0000  0.0000
  6.0  5.0  4.0  3.0  2.0  1.0
max =  0.300  min =  0.000
cells above zero = 5

The stencil update reads the old u on the right and writes unew on the left, so there is no aliasing to reason about. An update in place, u(2:nx-1, :) = u(1:nx-2, :) + u(3:nx, :), would also be well defined (the right-hand side is evaluated first), but the compiler may need a temporary for it, which costs memory and time.

where selects elements by a logical mask array of the same shape. In the construct form the first mask is evaluated once, the statements of the where body run for the true elements, and each elsewhere branch runs for those that are still false, optionally with its own mask (10.2.3.2). Only the assignments in the body are masked. Function references in the body are evaluated for the whole array unless they are elemental. forall, the Fortran 95 relative of where, is obsolescent since Fortran 2018; use do concurrent, described in do concurrent and GPU Offload. The array-oriented intrinsics (maxval, count, pack and the rest) are in Array Intrinsics and Reductions.

Two arrays of different shapes are rejected at compile time when the shapes are known, and at run time with -fcheck=bounds otherwise:

! REJECTED on purpose: the shapes of a and b are not conformable.
program nc
implicit none
real :: a(4), b(3)
a = 0.0
b = 1.0
a = b
print *, a
end program
10_nonconformable_arrays_rejected.f90:7:1:
    7 | a = b
      | 1~
Error: Different shape for array assignment at (1) on dimension 1 (4 and 3)

Derived-type assignment

For a variable of derived type, an intrinsic assignment is performed "as if each component of the variable were assigned from the corresponding component of expr" (10.2.1.3). What "assigned" means depends on the kind of component:

Component Assignment

ordinary (nonpointer, nonallocatable)

intrinsic assignment, or defined assignment if its type has a type-bound assignment(=) that fits

allocatable

a deep copy: the target component is deallocated, then allocated with the shape and type parameters of the source if the source is allocated, then assigned; an unallocated source leaves the target unallocated

pointer

pointer assignment: the target component becomes associated with the same target as the source, and no data is copied

The distinction between "owns its data" and "refers to data" is the reason to prefer allocatable components over pointer components in types like grid_t in heatflow: copying a grid copies its field, and final procedures and ownership are simple. The next program has one of each kind of component:

module heatflow_kinds
  use, intrinsic :: iso_fortran_env, only: int32, int64, real32, real64
  implicit none
  private
  public :: sp, dp, ip, lp
  integer, parameter :: sp = real32
  integer, parameter :: dp = real64
  integer, parameter :: ip = int32
  integer, parameter :: lp = int64
end module heatflow_kinds

program derived_type_assignment
  use heatflow_kinds, only: dp, ip
  implicit none

  type :: probe_t
    integer(ip) :: id = 0_ip
    real(dp), allocatable :: readings(:)    ! owned by the object
    real(dp), pointer :: shared(:) => null() ! only associated, never owned
  end type probe_t

  real(dp), target :: log_buffer(3)
  type(probe_t) :: a, b

  log_buffer = [10.0_dp, 20.0_dp, 30.0_dp]
  a%id = 7_ip
  a%readings = [0.1_dp, 0.2_dp]
  a%shared => log_buffer

  ! Intrinsic assignment: ordinary components are copied, allocatable
  ! components are deep-copied (and reallocated if needed), pointer
  ! components are pointer-assigned (10.2.1.3).
  b = a

  a%readings(1) = -1.0_dp
  a%shared(1) = -10.0_dp

  print '(a, i0)', 'b%id            = ', b%id
  print '(a, 2f6.1)', 'b%readings      =', b%readings
  print '(a, 3f6.1)', 'b%shared        =', b%shared
  print '(a, l1)', 'same pointer target: ', associated(b%shared, a%shared)
  b%readings(2) = 5.0_dp
  print '(a, 2f6.1)', 'a%readings      =', a%readings
end program derived_type_assignment
b%id            = 7
b%readings      =   0.1   0.2
b%shared        = -10.0  20.0  30.0
same pointer target: T
a%readings      =  -1.0   0.2

b = a copies id and readings and points b%shared at the buffer that a%shared already points to. Afterwards a change through the pointer is visible in both objects (-10.0 in b%shared) while a change to b%readings leaves a%readings alone. Derived types, including type-bound defined assignment, are the subject of Derived Types.

Pointer assignment

Pointer assignment, written =>, associates a pointer with a target; it does not copy anything (10.2.2). The forms, from 10.2.2.2, are:

  • p => target for a data pointer. The pointer takes the shape of the target, with lower bounds equal to lbound of the target. For a section the lower bounds are 1.

  • p(lb1:, lb2:) => target, a list of lower bounds that replaces the default ones (Fortran 2003).

  • p(lb1:ub1, lb2:ub2) => target, bounds remapping, which gives the pointer a different rank from its rank-one target (Fortran 2003) or from any simply contiguous target (Fortran 2008). The target must have at least as many elements as the new shape needs.

  • q => procedure_name for a procedure pointer, whose interface must be compatible with the procedure.

A data pointer can only be associated with an object that has the target attribute (or is itself a pointer), a restriction that lets the compiler assume that only target objects are aliased. A pointer assignment never changes the target, while an ordinary assignment through a pointer, p = 99.0_dp, assigns to the target. associated(p) and associated(p, target) test the association, nullify(p) or p => null() disassociates it, and a polymorphic pointer takes the dynamic type of the target (10.2.2.3).

module heatflow_kinds
  use, intrinsic :: iso_fortran_env, only: int32, int64, real32, real64
  implicit none
  private
  public :: sp, dp, ip, lp
  integer, parameter :: sp = real32
  integer, parameter :: dp = real64
  integer, parameter :: ip = int32
  integer, parameter :: lp = int64
end module heatflow_kinds

module kernels_demo
  use heatflow_kinds, only: dp
  implicit none
  private
  public :: scale_by_two, cool

contains

  pure function scale_by_two(t) result(r)
    real(dp), intent(in) :: t
    real(dp) :: r
    r = 2.0_dp*t
  end function scale_by_two

  pure function cool(t) result(r)
    real(dp), intent(in) :: t
    real(dp) :: r
    r = 0.5_dp*t
  end function cool

end module kernels_demo

program pointer_assignment
  use heatflow_kinds, only: dp, ip
  use kernels_demo, only: scale_by_two, cool
  implicit none
  real(dp), target :: u(4, 3)
  real(dp), target :: flat(12)
  real(dp), pointer :: row(:), interior(:, :), matrix(:, :), p
  procedure(scale_by_two), pointer :: update
  integer(ip) :: i

  u = reshape([(real(i, dp), i=1_ip, 12_ip)], shape(u))

  ! Pointer assignment (=>) associates; it does not copy.
  row => u(2, :)
  row(1) = -1.0_dp
  print '(a, f5.1)', 'u(2, 1) after row(1) = -1: ', u(2, 1)

  ! Ordinary assignment through a pointer assigns to its target.
  p => u(4, 3)
  p = 99.0_dp
  print '(a, f5.1)', 'u(4, 3) after p = 99:      ', u(4, 3)

  ! A pointer to a section has lower bounds 1 unless you give them.
  interior => u(2:3, 2:3)
  print '(a, 2i3, a, 2i3)', 'lbound(interior) =', lbound(interior), '   shape =', shape(interior)
  interior(0:, 0:) => u(2:3, 2:3)
  print '(a, 2i3)', 'lbound with (0:, 0:) =', lbound(interior)

  ! Bounds remapping: a rank-2 view of rank-1 storage.
  flat = [(real(i, dp), i=1_ip, 12_ip)]
  matrix(1:4, 1:3) => flat
  print '(a, f5.1)', 'matrix(2, 3) = ', matrix(2, 3)

  ! Pointer status.
  print '(a, l1)', 'associated(row, u(2, :)) = ', associated(row, u(2, :))
  nullify (row)
  print '(a, l1)', 'associated(row) after nullify = ', associated(row)

  ! Procedure pointer assignment.
  update => scale_by_two
  print '(a, f5.1)', 'update(3) = ', update(3.0_dp)
  update => cool
  print '(a, f5.1)', 'update(3) = ', update(3.0_dp)
end program pointer_assignment
u(2, 1) after row(1) = -1:  -1.0
u(4, 3) after p = 99:       99.0
lbound(interior) =  1  1   shape =  2  2
lbound with (0:, 0:) =  0  0
matrix(2, 3) =  10.0
associated(row, u(2, :)) = T
associated(row) after nullify = F
update(3) =   6.0
update(3) =   1.5

The last two calls change which function update refers to, without changing its interface. Pointers, targets and the lifetime rules are covered in Pointers and Targets; the mixed use of pointer and allocatable components is discussed there too. Module procedures with an explicit interface, which pointer targets such as scale_by_two must have, are in Modules and Submodules.

Common diagnostics

The messages below were produced by GNU Fortran 16.2 and LLVM Flang 23 while preparing this page, in the programs above and in their variants.

Symptom Cause Remedy

Logicals at (1) must be compared with .eqv. instead of == (gfortran), LOGICAL operands must be compared using .EQV. or .NEQV. (Flang)

== on two logical operands

use .eqv. and .neqv.

Extension: Unary operator following arithmetic operator (use parentheses)

x ** -y or x * -y

parenthesise the unary operand: x ** (-y)

Operands of logical operator '.and.' at (1) are INTEGER(4)/INTEGER(4)

integers used as logical operands

use iand, ior, ieor for bit operations, or compare to zero

Possible change of value in conversion from REAL(8) to INTEGER(4) (-Wconversion)

implicit truncation in an assignment

write int(x, ip), nint(x, ip), floor or ceiling

no diagnostic; x holds 0.10000000149011612 instead of 0.1

a default-kind real constant assigned to a real(dp) variable

use the kind suffix, 0.1_dp

Array bound mismatch for dimension 1 of array 'u' (4/3) (run time, -fcheck=bounds)

array assignment between non-conformable shapes

fix the section or assign to the whole allocatable variable

Different shape for array assignment at (1) on dimension 1 (4 and 3) (compile time)

non-conformable shapes known at compile time

fix the declaration or the section

Index '4' of dimension 1 of array 'a' above upper bound of 3

a subscript evaluated in a guard that the language does not promise to short-circuit

nested if, or a conditional expression

Warning: ... may be used uninitialized; array stays unallocated

scalar assigned to an unallocated allocatable array

allocate first, or assign an array expression

Sorry, array is currently unsupported for conditional expressions

array-valued conditional expression in gfortran 16.2

use if/else or merge for arrays

expr at (1) and expr at (2) in conditional expression must have the same declared type

branches of different type or kind

convert one branch explicitly

Where the books are outdated

This page draws on the chapter on expressions and assignment of Metcalf, Reid, Cohen and Bader’s Modern Fortran Explained: Incorporating Fortran 2023 (referred to as MRCB, Chapter 3) and on its section on the Fortran 2023 expression features (section 23.2.3). Only the concepts are used; nothing is reproduced. Where an older source, including older printings and the MATLAB book’s example style, differs from the current practice, the current practice is as follows.

  • Conditional expressions are new. A book written before Fortran 2023 cannot show them, and MRCB covers them only among the Fortran 2023 additions. Pre-2023 code uses a nested if or merge; the section on conditional expressions above states where each is appropriate. The book has no compiler-support information. On 2026-10-18 GNU Fortran 16.2 supports scalar conditional expressions but not array-valued ones, and LLVM Flang 23 accepts both although its support table still lists the feature as not supported.

  • The authors' recommendations are not the standard’s deprecations. MRCB’s appendix on features it advises against includes the dotted relational spellings .eq., .ne., .lt., .le., .gt. and .ge.. They are not obsolescent or deleted in Fortran 2023, and the compiler does not warn about them. The symbols ==, /=, <, <=, > and >= are a style choice.

  • forall is obsolescent. Older texts present forall as the way to write parallel assignment; since Fortran 2018 it is obsolescent and do concurrent replaces it.

  • Allocation on assignment is the default. Material written for Fortran 95 compilers warns against assigning to unallocated arrays. Since Fortran 2003 it is standard, and gfortran enables it unless -std=f95 is given. The newer hazards are the silent reallocation and the cost, covered above.

  • Source-form limits changed. Long right-hand sides are easier to write in Fortran 2023, which allows lines of 10,000 characters and no limit on the number of continuation lines (1,000,000 characters per statement), where the F2018 limits were 132 and 255. The statements in this page are nevertheless wrapped at conventional widths.

  • IEEE references are older than the standard. The book’s references to IEEE 754-2008 and ISO/IEC/IEEE 60559:2011 are superseded by IEEE 754-2019 and ISO/IEC 60559:2020, which Fortran 2023 adopts. The comparison rules for NaN used on this page are unchanged by that revision.

Version provenance

Feature Introduced or changed

**, *, /, +, -, the relational .eq. forms, .not., .and., .or.

FORTRAN 66

//, .eqv., .neqv., character assignment with padding and truncation

FORTRAN 77

relational symbols ==, /=, <, <=, >, >=, defined operators and assignment, array expressions and array assignment, pointer assignment =>, where

Fortran 90

where construct with masked elsewhere, forall

Fortran 95 (forall obsolescent in Fortran 2018)

automatic (re)allocation on assignment, deferred-length character reallocation, pointer lower bounds and bounds remapping, procedure pointer assignment

Fortran 2003

polymorphic allocatable assignment, bounds remapping of simply contiguous targets

Fortran 2008

simple procedures, enumeration and enum types in Table 10.8, extended BOZ in assignment, conditional expressions

Fortran 2023

Fortran 2028 is still a draft. The latest public working draft, J3/26-007r1, is outside the scope of this page, which only records that new expression features are expected there; see What’s New and Fortran 2028 Preview. The compiler matrix for Fortran 2023 is in History, Standards and Compilers, and the old forms that are still found in legacy code, such as .eq., statement functions and computed assignments, are in Legacy Features and Modernization.

References