What is new in Fortran 2018 and 2023
last modified November 14, 2026
Fortran is standardized, and the standard is revised every few years. Each
revision keeps the previous language almost unchanged and adds a small number of
features, so a program written in 1990 still compiles today, and a program
written today can use constructs that a 1990 compiler would reject. The
compiler learns the revision from the -std= option, and it reports
the constructs that the chosen revision does not contain.
This article walks through the changes of the last two revisions, Fortran 2018 and Fortran 2023, with a program for each feature that the installed compiler accepts, and with the exact error messages for the ones it does not. It starts with the flag itself, because the flag is what decides which revision the compiler enforces.
The flag and the newest standard
-std=f95, -std=f2003, -std=f2008 and
-std=f2018 select a revision of the standard, and
-std=legacy selects everything the compiler has ever accepted,
including features that no standard contained. Without the flag gfortran accepts
its own dialect, which is a superset of the newest revision it knows. The program
below uses one Fortran 2018 addition, the list on implicit none.
program modern
implicit none (type, external)
integer, parameter :: n = 5
integer :: i, squares(n)
do i = 1, n
squares(i) = i * i
end do
print '(a, 5i4)', 'the squares: ', squares
print '(a, i0)', 'their sum: ', sum(squares)
end program modern
The list after implicit none names the two things that used to be
assumed: type says that every variable must be declared, and
external says that every procedure called from outside must be
declared too. Either word may be used alone. The compile under
-std=f2018 succeeds and the program prints the squares; the 2008
revision does not have the list, and the compiler says so; and there is no
f2023 value at all in this release, which the last command reports
before it compiles anything.
$ gfortran -std=f2018 -Wall -Wextra -fcheck=all modern.f90 -o modern
$ ./modern
the squares: 1 4 9 16 25
their sum: 55
$ gfortran -std=f2008 -Wall modern.f90 -o modern_f2008
modern.f90:2:20:
2 | implicit none (type, external)
| 1
Error: Fortran 2018: IMPLICIT NONE with spec list at (1)
$ gfortran -std=f2023 modern.f90 -o modern_f2023
gfortran: error: unrecognized command-line option ‘-std=f2023’; did you mean ‘-std=f2003’?
implicit none with an external list
external is the useful half in an old code base. A procedure that
is called without an explicit interface, such as a subroutine from a library
with no module, is invisible to the compiler: it cannot check the number or the
types of the arguments. The 2018 list turns that silence into an error. The
program below calls a subroutine that is defined in the same file but never
declared.
program external_bad
implicit none (type, external)
integer :: i
i = 3
call report(i)
end program external_bad
subroutine report(value)
integer, intent(in) :: value
print '(a, i0)', 'report got ', value
end subroutine report
The compiler names the call and says that the procedure is not explicitly declared, which is the mistake the author made: the subroutine exists, but nothing told the compiler about it. Adding the declaration fixes the program, and then the same source compiles without a word.
program external_good
implicit none (type, external)
integer :: i
external :: report
i = 3
call report(i)
end program external_good
subroutine report(value)
integer, intent(in) :: value
print '(a, i0)', 'report got ', value
end subroutine report
The external statement gives the name the status of a procedure and
lets the call through, while the rest of the program still benefits from the
declaration requirement. The two programs differ in one line, and the output of
the second one is the only output of the pair.
$ gfortran -std=f2018 -Wall -Wextra external_bad.f90 -o external_bad
external_bad.f90:6:18:
6 | call report(i)
| 1
Error: Procedure ‘report’ called at (1) is not explicitly declared
$ gfortran -std=f2018 -Wall -Wextra -fcheck=all external_good.f90 -o external_good
$ ./external_good
report got 3
error stop with a value
Fortran 2018 allows error stop to take an expression rather than
only a literal. An integer expression becomes the exit status of the program,
which is how a script learns why a run failed, and a character expression
becomes the message that the runtime prints. The program below stops with the
code three.
program stop_int
implicit none
integer :: code
code = 3
print '(a, i0)', 'stopping with the code ', code
error stop code
end program stop_int
The message ERROR STOP 3 goes to standard error, and the shell
reports the status 3, which is the value of the variable rather than the fixed
status 1 that an error stop without a value produces. A character
value works the same way, except that the status stays 1.
program stop_code
use iso_fortran_env, only: int32
implicit none
integer :: code
character(len=64) :: message
code = 3
message = 'the file cannot be opened'
if (code /= 0) then
print '(a)', 'about to stop with a message and a code'
error stop trim(message)
end if
end program stop_code
The variant with a message is the one to use for a human reader, and the variant with an integer for a build system or a script that has to distinguish several failures. Both stop the program at the statement, after flushing the output that was already written.
$ gfortran -std=f2018 -Wall -Wextra -fcheck=all -fno-backtrace stop_int.f90 -o stop_int $ ./stop_int; echo "exit: $?" stopping with the code 3 ERROR STOP 3 exit: 3 $ gfortran -std=f2018 -Wall -Wextra -fcheck=all -fno-backtrace stop_code.f90 -o stop_code $ ./stop_code; echo "exit: $?" about to stop with a message and a code ERROR STOP the file cannot be opened exit: 1
Assumed-rank arrays and select rank
A dummy argument declared with dimension(..), usually written
x(..), accepts an actual argument of any rank: a scalar, a vector, a
matrix. Inside the procedure the rank is unknown until a select rank
construct asks for it, and each branch handles one possibility. This is the
Fortran 2018 way to write a routine that prints or forwards an array without
writing one version per rank.
program rank
implicit none
integer :: vector(4), matrix(2, 3)
vector = [1, 4, 9, 16]
matrix = reshape([1, 2, 3, 4, 5, 6], [2, 3])
call describe(7)
call describe(vector)
call describe(matrix)
contains
subroutine describe(x)
integer, intent(in) :: x(..)
select rank (x)
rank (0)
print '(a, i0)', 'a scalar with the value ', x
rank (1)
print '(a, i0, a, i0)', 'a vector of ', size(x), &
' elements, the first is ', x(1)
rank (2)
print '(a, i0, a, i0, a, i0)', 'a matrix of ', size(x, 1), &
' by ', size(x, 2), ', the last element is ', x(size(x, 1), size(x, 2))
rank default
print '(a)', 'an array of another rank'
end select
end subroutine describe
end program rank
Three calls of the same subroutine select three branches, and the array is
indexed normally inside the branch that knows its rank. The
rank default branch catches everything else, which here never
happens. In a library the same construct wraps a C function, because a C
function that receives an array also has to learn its rank at run time, and
x(..) is the only Fortran declaration that can meet it.
$ gfortran -std=f2018 -Wall -Wextra -fcheck=all rank.f90 -o rank $ ./rank a scalar with the value 7 a vector of 4 elements, the first is 1 a matrix of 2 by 3, the last element is 6
A repeatable random stream
random_init seeds the random number generator with two logical
arguments. The first, repeatable, asks for a seed that depends only
on the program, so that two runs produce the same numbers; the second,
image_distinct, asks for a different stream on every image of a
coarray program. Before 2018 the only portable route was to call
random_seed with a seed built by hand, which is easy to get wrong.
program random
use iso_fortran_env, only: real64
implicit none
real(real64) :: u(3)
call random_init(.true., .true.)
call random_number(u)
print '(a, 3f8.5)', 'a repeatable stream: ', u
call random_init(.true., .true.)
call random_number(u)
print '(a, 3f8.5)', 'after the same call: ', u
call random_init(.false., .false.)
call random_number(u)
print '(a, 3f8.5)', 'a fresh seed: ', u
end program random
The first two lines are identical because the same repeatable seed was set
twice, and the third line changes with every run because it was seeded from the
clock. The value of the third line in the transcript is therefore one sample,
not a promise. A test suite uses .true. for the first argument and
gets a run it can reproduce; a simulation that needs independent noise uses
.false..
$ gfortran -std=f2018 -Wall -Wextra -fcheck=all random.f90 -o random $ ./random a repeatable stream: 0.82526 0.19133 0.15550 after the same call: 0.82526 0.19133 0.15550 a fresh seed: 0.85209 0.22939 0.97533
findloc with dim, mask and back
findloc arrived with Fortran 2008 and returns the position of a
value in an array. Fortran 2018 gave it three optional arguments:
dim to search along one dimension and return a position for every
other one, mask to consider only some of the elements, and
back to search from the end. A position of zero means that the
value was not found.
program findloc_demo
implicit none
integer :: a(2, 3), row
a = reshape([1, 2, 3, 2, 5, 6], [2, 3])
do row = 1, 2
print '(a, i0, a, 3i4)', 'row ', row, ': ', a(row, :)
end do
print '(a, 2i3)', 'the first 2: ', findloc(a, 2)
print '(a, 2i3)', 'the last 2: ', findloc(a, 2, back=.true.)
print '(a, 3i3)', 'the row of the 3 in every column: ', findloc(a, 3, dim=1)
print '(a, 2i3)', 'a 6 among the values above 3: ', findloc(a, 6, mask=a > 3)
print '(a, 2i3)', 'a 9 among the values above 3: ', findloc(a, 9, mask=a > 3)
end program findloc_demo
The matrix holds the value 2 twice, and the two searches return the first and
the last of the two positions. The dim=1 call returns one answer per
column, zero where the column holds no 3. With mask the search sees
only the elements where the mask is true, so 6 is found and 9 is not. The
result keeps the shape of the array without the searched dimension, which is why
the first two calls print two numbers and the third prints three.
$ gfortran -std=f2018 -Wall -Wextra -fcheck=all findloc.f90 -o findloc $ ./findloc row 1: 1 3 5 row 2: 2 2 6 the first 2: 2 1 the last 2: 2 2 the row of the 3 in every column: 0 1 0 a 6 among the values above 3: 2 3 a 9 among the values above 3: 0 0
Coarray additions
The parallel part of Fortran 2018 came from a technical specification on
additional parallel features, and three of its pieces are visible in a small
program. co_broadcast copies a value from one image to all the
others, the atomic subroutines accept a stat= argument that reports
whether the operation succeeded, and an event is a variable of type
event_type that one image can post and another can wait for.
program coarray
use iso_fortran_env, only: atomic_int_kind, event_type
implicit none
integer :: value[*]
integer(atomic_int_kind) :: counter[*]
type(event_type) :: ready[*]
integer :: seen, stat
value = this_image() * 10
sync all
call co_broadcast(value, 1)
print '(a, i0, a, i0)', 'images: ', num_images(), ', the broadcast value: ', value
counter = 0
sync all
call atomic_define(counter[1], 42, stat)
call atomic_ref(seen, counter, stat)
print '(a, i0, a, i0)', 'the atomic counter: ', seen, ', the status: ', stat
if (num_images() >= 2) then
if (this_image() == 2) event post(ready[1])
if (this_image() == 1) then
event wait(ready)
print '(a)', 'image 1 received the event from image 2'
end if
else
print '(a)', 'an event needs a second image, so none was posted'
end if
end program coarray
The program is compiled with -fcoarray=single, so it runs as one
image: the broadcast returns the value of that image, the atomic operations
succeed with status zero, and the event branch reports that a second image is
missing. With several images the same source would copy image one's value
everywhere, and image two would post the event that image one waits for. The
status of an atomic operation is not a value to ignore: it reports a failure
instead of stopping the program, and the coarrays article explains the runtime
that a multi-image build needs.
$ gfortran -std=f2018 -Wall -Wextra -fcheck=all -fcoarray=single coarray.f90 -o coarray $ ./coarray images: 1, the broadcast value: 10 the atomic counter: 42, the status: 0 an event needs a second image, so none was posted
Two intrinsics that are missing
Not every Fortran 2018 feature is in this compiler. reduce folds an
array with a pure function of two arguments, which is the array counterpart of
the co_reduce collective, and out_of_range tells whether
a value lies outside the range of another type. Both are standard, and gfortran
13.2 has neither: it treats the names as ordinary functions and then complains
that they were never declared.
program reduce_bad
use iso_fortran_env, only: real64
implicit none
real(real64) :: x(3)
x = [2.0_real64, 8.0_real64, 4.0_real64]
print '(a, f0.1)', 'the product: ', reduce(x, times)
contains
pure real(real64) function times(a, b) result(c)
real(real64), intent(in) :: a, b
c = a * b
end function times
end program reduce_bad
The message has no IMPLICIT type is the compiler's way of saying
that it does not know the name, and the same message appears for the range
test. A newer gfortran implements reduce, so the program is correct
Fortran and only the compiler is behind the standard.
program range_bad
use iso_fortran_env, only: int8
implicit none
integer :: i
do i = 1, 4
print '(a, i0, a, l1)', 'out_of_range(', i * 100, '): ', &
out_of_range(i * 100, 0_int8)
end do
end program range_bad
The four calls would print F, F, T and
T, because the largest value that an eight-bit integer holds is
127, so 100 and 200 fit and 300 and 400 do not. Until a compiler implements the
intrinsic, the same test is written with a comparison against
huge(0_int8), which the operators article covers.
$ gfortran -std=f2018 -Wall -Wextra reduce_bad.f90 -o reduce_bad
reduce_bad.f90:7:39:
7 | print '(a, f0.1)', 'the product: ', reduce(x, times)
| 1
Error: Function ‘reduce’ at (1) has no IMPLICIT type
$ gfortran -std=f2018 -Wall -Wextra range_bad.f90 -o range_bad
range_bad.f90:7:64:
7 | print '(a, i0, a, l1)', 'out_of_range(', i * 100, '): ', &
| 1
Error: Function ‘out_of_range’ at (1) has no IMPLICIT type
Fortran 2023
The 2023 revision, published in November 2023, is deliberately small: it corrects what the previous one got wrong and adds a handful of conveniences. gfortran 13.2 predates it and has none of them, so the rest of this article is a tour of constructs that the compiler rejects, with the message that says so. The value of the tour is that the programs are the ones a newer compiler will accept unchanged.
Conditional expressions
A conditional expression chooses one of several values inside an expression,
with the familiar question mark syntax. It saves the four lines of an
if block when the only thing the block does is choose a value, and
it can appear anywhere an expression may appear.
program conditional
implicit none
real :: a, value
a = 2.5
value = (a > 0.0 ? a : 0.0)
print '(a, f0.1)', 'the value: ', value
end program conditional
The parenthesised form is a primary, so it can be assigned, printed or passed as an argument. The compiler reads the question mark as the start of something it does not know and reports a syntax error at the assignment, which is the expected result for a 2018 parser.
$ gfortran -std=f2018 -Wall -Wextra conditional.f90 -o conditional
conditional.f90:6:11:
6 | value = (a > 0.0 ? a : 0.0)
| 1
Error: Syntax error in VALUE statement at (1)
Splitting a string
Two new intrinsic subroutines work on separated text. split finds
the next or the previous separator, and tokenize returns all the
tokens at once, as an allocatable array whose length and bounds it chooses. Both
are what a parser of a small configuration file or a line of numbers needs.
program tokens
implicit none
character(len=*), parameter :: text = 'one two three'
character(len=:), allocatable :: words(:)
integer :: i
call tokenize(text, ' ', words)
do i = 1, size(words)
print '(a, a)', 'a token: ', trim(words(i))
end do
end program tokens
The program compiles, because a call to an unknown subroutine is legal Fortran,
and then fails at the link step with an undefined reference to
tokenize_, the name the compiler would give the external procedure.
The warnings that the allocatable array is used uninitialized are suppressed
with -w so that the missing symbol is the only line to read.
$ gfortran -std=f2018 -w tokens.f90 -o tokens /usr/bin/ld: /tmp/ccMEKV5P.o: in function `MAIN__': tokens.f90:(.text+0x58): undefined reference to `tokenize_' collect2: error: ld returned 1 exit status
Reduction in do concurrent
The 2023 revision repairs the most visible gap in do concurrent: a
loop may now name a variable that it reduces, as in
reduce(+:total), and the iterations may add to it without creating
a race. The compiler splits the loop, gives every thread a private copy of the
variable and combines the copies at the end, which is exactly what an OpenMP
reduction does by hand.
program reduce_do
implicit none
integer, parameter :: n = 1000
integer :: i, total
total = 0
do concurrent (i = 1:n) reduce(+:total)
total = total + i
end do
print '(a, i0)', 'the sum: ', total
end program reduce_do
gfortran 13 does not know the clause, and its parser stops at the
reduce keyword before it ever looks at the loop. The same program
written with reduction(+:total) and !$omp parallel do
compiles and runs, as the OpenMP article shows, and that is the form to use
until the compiler catches up.
$ gfortran -std=f2018 -Wall -Wextra reduce_do.f90 -o reduce_do
reduce_do.f90:7:28:
7 | do concurrent (i = 1:n) reduce(+:total)
| 1
Error: Syntax error in DO statement at (1)
reduce_do.f90:9:7:
9 | end do
| 1
Error: Expecting END PROGRAM statement at (1)
Smaller additions
Three more additions are easy to describe and quick to test. A logical kind can
be chosen by the number of bits it must hold, with
selected_logical_kind, in the same way that
selected_real_kind chooses a real kind. The trigonometric functions
gain degree versions such as sind and cosd, and
half-revolution versions such as sinpi and cospi, which
remove a multiplication by pi from every call. And the limit on the length of a
free-form line rises from 132 characters to ten thousand.
program logical_kind
implicit none
print '(a, i0)', 'a logical kind holding 8 bits: ', selected_logical_kind(8)
end program logical_kind
The logical kind function is unknown to this compiler, and so is the whole
2023 family of trigonometric names. The degree functions are the interesting
case, because gfortran has provided sind as a GNU extension for
years: under -std=f2018 it warns that the intrinsic is not in the
selected standard, and -fall-intrinsics makes it available, but
sinpi does not exist at all.
program degrees
implicit none
print '(a, f8.4)', 'sind(30): ', sind(30.0)
print '(a, f8.4)', 'sinpi(0.5): ', sinpi(0.5)
end program degrees
The last program is compiled with -fall-intrinsics, which is the
flag that turns on the extensions, and it still stops at sinpi.
The line-length change is the easiest to check: a free-form line of 154
characters is an error under -std=f2018, as the style guide shows,
and would be legal under a 2023 compiler.
$ gfortran -std=f2018 -Wall -Wextra logical_kind.f90 -o logical_kind
logical_kind.f90:3:55:
3 | print '(a, i0)', 'a logical kind holding 8 bits: ', selected_logical_kind(8)
| 1
Error: Function ‘selected_logical_kind’ at (1) has no IMPLICIT type
$ gfortran -std=f2018 -Wall -Wextra -fall-intrinsics degrees.f90 -o degrees
degrees.f90:4:38:
4 | print '(a, f8.4)', 'sinpi(0.5): ', sinpi(0.5)
| 1
Error: Function ‘sinpi’ at (1) has no IMPLICIT type
Which feature came with which revision
The table collects what the two articles in this pair rely on, with the revision that introduced each feature and whether this compiler accepts it. The middle column is the answer to the question a reader usually has: may I use this construct, or will an older compiler in the lab reject it?
| Feature | Standard | gfortran 13.2 |
|---|---|---|
implicit none (type, external) | 2018 | yes |
error stop with an expression | 2018 | yes |
assumed rank and select rank | 2018 | yes |
random_init | 2018 | yes |
findloc with dim, mask, back | 2018 | yes |
co_broadcast, co_reduce | 2018 | yes |
events, event post and event wait | 2018 | yes |
atomic subroutines with stat= | 2018 | yes |
reduce and out_of_range | 2018 | no |
teams, form team | 2018 | partial |
| conditional expressions | 2023 | no |
split and tokenize | 2023 | no |
selected_logical_kind | 2023 | no |
reduce clause in do concurrent | 2023 | no |
simple procedures | 2023 | no |
| degree and half-revolution trigonometry | 2023 | extension |
| 10,000-character lines | 2023 | no, 132 |
The two rows that need a word are the last ones. The degree functions are
available as GNU extensions, which is why a program that calls sind
may build here and fail with a strict compiler, and the line limit is the one
rule of the 2023 revision that a reader is most likely to meet while editing a
long expression by hand.
Next steps
The Fortran through the standards article
follows the same small program from FORTRAN 66 to Fortran 2018 and shows what
each revision changed, the
parallel loops and
coarrays articles use the 2018 parallel
features in full, and the compiling
and debugging article explains the statuses that error stop
returns. The style guide explains why
implicit none matters, and the
FORTRAN 77 article shows the opposite end
of the same history.
Source
Fortran 2018 standard (J3/18-007r1)
John Reid: The new features of Fortran 2018
John Reid: The new features of Fortran 2023
GNU Fortran manual: Fortran 2018 status
In this article we have chosen a revision with -std= and seen
where the newest one is missing, added the type and
external lists to implicit none and watched the second
one catch an undeclared procedure, stopped a program with a message and with an
exit code, described a scalar, a vector and a matrix with one assumed-rank
subroutine, seeded a random stream so that two runs agree, searched a matrix
with findloc along a dimension and through a mask, broadcast a
value and exchanged an event between images, and recorded the two 2018
intrinsics and the 2023 constructs that this compiler does not have yet.
Author
List all Fortran tutorials.