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 |
|
level-1 expression |
a primary, optionally preceded by a defined unary operator |
|
level-2 expression |
level-1 expressions combined with the numeric operators |
|
level-3 expression |
level-2 expressions combined with the character operator |
|
level-4 expression |
level-3 expressions with at most one relational operator |
|
level-5 expression |
level-4 expressions combined with |
|
expression |
level-5 expressions combined with defined binary operators |
|
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, |
as defined by the interface |
Fortran 90 |
2 |
|
numeric; the result kind follows the operands |
FORTRAN 66 |
3 |
|
numeric |
FORTRAN 66 |
4 |
unary |
numeric; only allowed at the start of a level-2 expression |
FORTRAN 66 |
5 |
binary |
numeric |
FORTRAN 66 |
6 |
|
character, equal kinds |
FORTRAN 77 |
7 |
|
numeric, character, and (only |
the dotted forms are FORTRAN 66, the symbols Fortran 90 |
8 |
|
logical |
FORTRAN 66 |
9 |
|
logical |
FORTRAN 66 |
10 |
|
logical |
FORTRAN 66 |
11 |
|
logical |
FORTRAN 77 |
12 (loosest) |
defined binary operators, |
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, so2**3**2is2**(3**2), which is 512, and it binds tighter than unary minus, so-2**2is-(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, soa * -banda ** -bare not valid. The compiler asks for parentheses,a * (-b). Because unary minus is looser than*and/,-a*bis-(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 < cis rejected; writea < b .and. b < c. -
.not.is looser than the relational operators, so.not. a == bnegates the comparison, and a second.not.directly after the first is not allowed (.not. .not. ais rejected; write.not. (.not. a)). -
.and.binds tighter than.or., which binds tighter than.eqv., soa .eqv. b .or. cmeansa .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)/3is -2 and5/2is 2, whatever the type of the variable that receives the result.mod(a, p)has the sign ofa,modulo(a, p)the sign ofp. An integer raised to a negative integer power is the integer division1/(x**abs(n)), which is 0 for everyxwithabs(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*xdivides two integers first andnx*x/nydoes not. Convert explicitly withreal(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.1for areal(dp)variable stores a double-precision number that carries the single-precision error;0.1_dpdoes 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 + x2before 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, uselge,lgt,lleandllt. -
If an operand can be an IEEE NaN,
.not. (x < y)andx >= yare 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
Source: J3/23-007r1 clauses 10.1.2.3 (Conditional expressions) and 10.1.4 (Evaluation of operations)
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 |
|
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 |
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:
-
The expression
exprand every expression insidevariableare evaluated. -
If the variable is unallocated,
exprmust have the same rank, and the variable is allocated (below). -
If the variable is allocated and
expris 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. -
The new allocation takes the dynamic type and kind parameters of
exprif the variable is polymorphic, each deferred length parameter fromexpr, the shape ofexprwith each lower bound equal tolbound(expr), and, ifexpris a scalar and the variable an array, the same bounds as before. -
The values are copied, with the conversions of the previous section.
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 = fineallocated an unallocatedu.unew = uallocatedunewwith the shape ofu. -
u = coarsen(fine)changed the shape, souwas 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
utakes them over (lbound0 afteru = 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
Source: J3/23-007r1 clause 10.2.1.3 (an unallocated array target requires an expression of the same rank)
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 |
|
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 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 => targetfor a data pointer. The pointer takes the shape of the target, with lower bounds equal tolboundof 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_namefor 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
Source: J3/23-007r1 clauses 10.2.2.3 (Data pointer assignment) and 10.2.2.4 (Procedure pointer assignment)
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 |
|---|---|---|
|
|
use |
|
|
parenthesise the unary operand: |
|
integers used as logical operands |
use |
|
implicit truncation in an assignment |
write |
no diagnostic; |
a default-kind real constant assigned to a |
use the kind suffix, |
|
array assignment between non-conformable shapes |
fix the section or assign to the whole allocatable variable |
|
non-conformable shapes known at compile time |
fix the declaration or the section |
|
a subscript evaluated in a guard that the language does not promise to short-circuit |
nested |
|
scalar assigned to an unallocated allocatable array |
allocate first, or assign an array expression |
|
array-valued conditional expression in gfortran 16.2 |
use |
|
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
iformerge; 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. -
forallis obsolescent. Older texts presentforallas the way to write parallel assignment; since Fortran 2018 it is obsolescent anddo concurrentreplaces 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=f95is 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 |
|---|---|
|
FORTRAN 66 |
|
FORTRAN 77 |
relational symbols |
Fortran 90 |
|
Fortran 95 ( |
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 |
|
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
-
J3/23-007r1, the Fortran 2023 final working draft: clause 10.1 (Expressions, including 10.1.2.3 Conditional expressions, 10.1.3 Precedence of operators, 10.1.4 Evaluation of operations, 10.1.5 Intrinsic operations, 10.1.6 Defined operations, 10.1.7 Evaluation of operands, 10.1.8 Integrity of parentheses, 10.1.9 Type, type parameters and shape of an expression and 10.1.12 Constant expression) and clause 10.2 (Assignment, including 10.2.1 Assignment statement, 10.2.2 Pointer assignment and 10.2.3 Masked array assignment)
-
GNU Fortran: Error and Warning Options (
-Wconversion,-Wcompare-reals,-Wcharacter-truncation,-Wrealloc-lhs,-Wrealloc-lhs-all,-Winteger-division) -
GNU Fortran: Options for Code Generation Conventions (
-frealloc-lhs,-fprotect-parens,-ffrontend-optimize,-faggressive-function-elimination) -
GNU Fortran: Runtime Options (
-fcheck) -
Michael Metcalf, John Reid, Malcolm Cohen and Reinhold Bader, Modern Fortran Explained: Incorporating Fortran 2023, 3rd ed., Oxford University Press, 2023: publisher page