ZetCode

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.

modern.f90
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.

external_bad.f90
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.

external_good.f90
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.

stop_int.f90
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.

stop_code.f90
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.

rank.f90
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.

random.f90
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.

findloc.f90
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.

coarray.f90
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.

reduce_bad.f90
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.

range_bad.f90
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.

conditional.f90
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.

tokens.f90
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.

reduce_do.f90
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.

logical_kind.f90
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.

degrees.f90
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?

FeatureStandardgfortran 13.2
implicit none (type, external)2018yes
error stop with an expression2018yes
assumed rank and select rank2018yes
random_init2018yes
findloc with dim, mask, back2018yes
co_broadcast, co_reduce2018yes
events, event post and event wait2018yes
atomic subroutines with stat=2018yes
reduce and out_of_range2018no
teams, form team2018partial
conditional expressions2023no
split and tokenize2023no
selected_logical_kind2023no
reduce clause in do concurrent2023no
simple procedures2023no
degree and half-revolution trigonometry2023extension
10,000-character lines2023no, 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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Fortran tutorials.