Procedures, Functions and Subroutines

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Fortran 2023 (ISO/IEC 1539-1:2023, J3 final draft J3/23-007r1) and was checked on 2026-10-18 with GNU Fortran 16.2 and LLVM Flang 23. Procedures are the unit of reuse in Fortran, and most of what makes modern Fortran code safe and fast is decided in a procedure’s interface: which arguments it takes, which of them it may change (intent), which may be missing (optional), whether it is allowed to have side effects (pure, elemental, simple), and whether the compiler is allowed to check any of that at the call. The page follows the order in which those ideas build on each other: the two kinds of subprogram and where they live, explicit and implicit interfaces, argument association, intent and value, optional and keyword arguments (including the Fortran 2023 conditional argument), function results, recursion, the procedure prefixes, internal procedures and host association, character arguments, procedures as arguments, and the aliasing rules that let a compiler optimise across a call.

Modules, which are the recommended home for procedures, are covered on Modules and Submodules; generic interfaces, defined operators and procedure pointers on Interfaces, Generics and Operator Overloading; array dummy arguments (assumed shape, assumed size, assumed rank) on Array Arguments: Assumed Shape, Size and Rank; and type-bound procedures on Type-Bound Procedures, Abstract Types and Finalization.

Every example belongs to the heatflow scenario (a two-dimensional heat-diffusion solver) and uses its names (heatflow_* modules, dp for real64), but each one is a complete, self-contained file that can be extracted and run on its own. Unless a paragraph says otherwise, every example compiles without warnings under gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all and was also built and run with LLVM Flang 23, and the output shown is what it printed. Examples that are deliberately wrong are labelled as non-conforming in the text and in a comment on their first line, and the compiler messages or run-time failures they produce are shown.

Two kinds of subprogram

Fortran has exactly two kinds of subprogram. A function is invoked from inside an expression and delivers a value through its result; a subroutine is invoked by a call statement and communicates through its arguments. Both are defined by a subprogram (a function or subroutine statement, a specification part, an execution part, an optional contains part and an end statement) and both can take dummy arguments, which are the names a procedure uses for the actual arguments supplied at a call.

[prefix] function name([dummy-arguments]) [result(result-name)]
    specification part      ! declarations, use, import, implicit none ...
    execution part
[contains
    internal subprograms]
end function [name]

[prefix] subroutine name([dummy-arguments])
    specification part
    execution part
[contains
    internal subprograms]
end subroutine [name]

call name(actual-arguments)         ! invokes a subroutine
x = name(actual-arguments) + 1.0    ! invokes a function inside an expression

The points that distinguish the two, and that the rest of the page relies on:

  • A function result is a variable. The type of the result can be written in the function statement (real(dp) function f(x)) or by declaring the result name in the specification part; it cannot be given both ways. Without a result clause the result variable has the function’s own name, and inside the function that name always means the result variable, never a recursive call. With result(r), the function name refers to the function itself and r is the variable that holds the value (J3/23-007r1, 15.6.2.2). A function must define its result before it returns (unless the result is a pointer, whose association status must not be undefined).

  • A subroutine statement cannot carry a type (constraint C1574); a subroutine has no result.

  • The end name is optional but checked. end function stable_dt must repeat the function’s name if it names anything (constraints C1573 and C1576), which catches a misplaced end early.

  • return is rarely needed. Execution ends at the end statement; return is only useful for an early exit. Fortran has no return value form, so an early exit from a function assigns the result first.

  • A function with no dummy arguments is still referenced with parentheses: default_dt(). Without them the name would denote the function, not a reference to it.

  • Prefixes appear before function or subroutine and are listed in the next table (R1529 and R1530). Each may appear at most once (C1552).

Prefix Meaning Rules

pure

No side effects visible outside the procedure.

Constraints C1592 to C15110 (clause 15.7). Required to call a procedure from do concurrent or from another pure procedure.

elemental

Written for scalars, applicable to arrays of any rank. Implies pure unless impure is also given.

All dummy arguments are scalar non-pointer non-allocatable (C15118); the result is scalar (C15119). Clause 15.9.

impure

Only valid with elemental; removes the implied pure.

Cannot be combined with pure or simple (C1553). Fortran 2008.

simple

A pure procedure that also does not reference variables from outside through use or host association.

Fortran 2023, clause 15.8. A simple procedure may also be elemental.

recursive

Advisory only since Fortran 2018: every procedure is recursive unless it says otherwise.

Cannot be combined with non_recursive (C1554).

non_recursive

The procedure promises that it never invokes itself, directly or indirectly.

Fortran 2018. A processor may use static storage for its local variables.

module

Marks the definition of a separate module procedure (a procedure whose interface was declared elsewhere).

C1556 to C1560; see Modules and Submodules.

type specification

Functions only: integer function count_hot(u).

Constraint C1574 forbids it on a subroutine.

Where procedures live

A procedure’s place of definition decides how it is found, what interface callers see and which variables it can reach. The standard distinguishes the classes in the following table (J3/23-007r1, 15.2.2).

Class Defined by Interface seen by callers Reaches host variables?

Intrinsic

The processor (sin, size, merge, …​).

Always explicit.

Not applicable.

External

A subprogram that is not inside another program unit. Compiled and linked independently.

Implicit in the caller unless an interface body (or a procedure declaration with an interface) describes it. Inside its own definition it is explicit.

No. An external procedure sees only its own scope and whatever it accesses through its own use statements.

Module

A subprogram in the contains part of a module or submodule.

Explicit everywhere the module is used.

It sees the module’s entities (this is host association with the module).

Internal

A subprogram in the contains part of a main program, an external subprogram or a module subprogram.

Explicit, but visible only inside the host.

Yes: every name of the host is visible (host association) unless a local declaration hides it.

Dummy procedure

A dummy argument that is a procedure (procedure(boundary_rule) :: rule).

Explicit if declared with an interface, implicit if only given external.

Not applicable.

Procedure pointer

A pointer with the procedure attribute.

As declared.

Not applicable.

Statement function

One-line f(x) = expression inside a scoping unit. Obsolescent.

Always implicit.

Yes.

flowchart LR subgraph DEF["Where is the procedure defined?"] direction TB INTR["Intrinsic: sin, size, merge, ..."] MODP["Module subprogram: after contains in a module"] INTN["Internal subprogram: after contains in a program unit or module procedure"] EXTP["External subprogram: stands alone, linked separately"] end subgraph IFC["What interface does a caller get?"] direction TB EXPL["Explicit interface: the compiler checks arguments and allows optional, keywords, arrays, results"] IMPL["Implicit interface: only the name and the call are known"] end INTR --> EXPL MODP -- "use module" --> EXPL INTN -- "inside the host" --> EXPL EXTP -- "interface body or procedure declaration" --> EXPL EXTP -- "external statement only" --> IMPL

The diagram shows why module procedures are the default recommendation in the fortran-lang Best Practices guide: the explicit interface comes for free with use, the compiler checks every call, and nothing has to be written twice. External procedures survive in libraries (BLAS, LAPACK), in code that crosses a language boundary, and in legacy source, and they are always called through an interface that someone had to write or leave out.

A module, an internal and an external procedure side by side

The first example shows all three in one file: average4 and relax_point are module procedures, twice is an internal function of the main program, and report is an external subroutine that the program sees only through an external statement. The program uses implicit none (type, external) (Fortran 2018), which turns a missing declaration of an external procedure into a compile-time error instead of a silent implicit interface.

! Module, internal and external procedures in one file, with explicit interfaces.
module heatflow_stencil
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  private
  public :: average4, relax_point

contains

  ! Module function: the interface is explicit wherever the module is used.
  function average4(north, south, east, west) result(mean)
    real(dp), intent(in) :: north, south, east, west
    real(dp) :: mean
    mean = 0.25_dp*(north + south + east + west)
  end function average4

  ! Module subroutine: updates one grid point and reports how much it moved.
  subroutine relax_point(u, i, j, change)
    real(dp), intent(inout) :: u(:, :)
    integer, intent(in) :: i, j
    real(dp), intent(out) :: change
    real(dp) :: old
    old = u(i, j)
    u(i, j) = average4(u(i, j + 1), u(i, j - 1), u(i + 1, j), u(i - 1, j))
    change = abs(u(i, j) - old)
  end subroutine relax_point

end module heatflow_stencil

program procedure_kinds
  use, intrinsic :: iso_fortran_env, only: dp => real64
  use heatflow_stencil, only: relax_point
  implicit none (type, external)
  real(dp) :: u(3, 3), change
  external :: report   ! an external procedure: only its name is known here

  u = 0.0_dp
  u(2, 1) = 100.0_dp   ! one hot neighbour of the centre point
  call relax_point(u, 2, 2, change)
  call report(u(2, 2), change)
  print '(a,f6.2)', 'doubled by an internal function: ', twice(u(2, 2))

contains

  ! Internal function: sees the host's names (host association), has an explicit interface.
  function twice(x) result(y)
    real(dp), intent(in) :: x
    real(dp) :: y
    y = 2.0_dp*x
  end function twice

end program procedure_kinds

! External subroutine: compiled separately in real projects; callers see only an implicit interface.
subroutine report(value, change)
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none (type, external)
  real(dp), intent(in) :: value, change
  print '(a,f6.2,a,f6.2)', 'centre = ', value, ', moved by ', change
end subroutine report
centre =  25.00, moved by  25.00
doubled by an internal function:  50.00

Things to notice in the example:

  • relax_point calls average4 without any declaration: both are in the same module, so each sees the other’s explicit interface through host association. They are public/private as the module decides, which is covered on the modules page.

  • report is written below the program only so that the file runs. In a real build it lives in a separate file, and the caller then knows only that report is an external procedure. Even when both are in one file, the standard does not oblige the compiler to check the call against the later definition.

  • twice uses nothing from the program, but it could read u and change directly: that is host association.

  • The statement implicit none of the module also applies to every module procedure, because module and internal subprograms inherit the implicit-typing mapping of their host (J3/23-007r1, 8.7). External subprograms and interface bodies do not inherit it, so report repeats the statement.

Explicit and implicit interfaces

The interface of a procedure is everything a caller needs in order to invoke it: its name, its dummy argument names and characteristics (type, kind, rank, intent, optional, …​), and, for a function, the characteristics of the result. An interface is explicit when the compiler can see all of that at the call; it is implicit when it can see only what the call itself says (J3/23-007r1, 15.4.2.1). The interface is explicit for internal, module and intrinsic procedures, for a procedure defined in the same scoping unit as its reference (a subroutine, or a function with a separate result name), and for a procedure declared by an interface body or by a procedure declaration with an interface. It is implicit otherwise; the interface of a statement function is always implicit.

An explicit interface is not merely a convenience. The standard requires one whenever the call uses a feature that needs information the call itself cannot carry (J3/23-007r1, 15.4.2.2):

The call or the procedure has …​ Why an implicit interface cannot work

a reference with an argument keyword (call describe_run(64, label='fine'))

Keywords name dummy arguments; the caller must be able to see the names.

a dummy argument that is allocatable, optional, pointer, target, value, asynchronous or volatile

These change how the argument is passed (descriptors, absent arguments, copies).

an assumed-shape or assumed-rank dummy array, a coarray dummy, a polymorphic dummy, or a dummy of a parameterized derived type

The caller must pass a descriptor, not just an address.

an array, pointer or allocatable result, or a character result whose length is not constant

The caller must know the shape or length to receive the result.

the elemental prefix, the bind attribute, or use in a context that requires pure (or simple)

The caller must be able to check the property.

Without an explicit interface, an argument mismatch is not an error that any compiler is obliged to find, because the compiler cannot know what the callee expects. With one, the checks are mandatory. The next file is deliberately non-conforming: the dummy arguments are real(dp) and the actual arguments are a default integer and a default real, plus one argument missing. A module procedure has an explicit interface, so gfortran stops at compile time.

! INTENTIONALLY NON-CONFORMING: the actual argument types do not match the dummy arguments.
module heatflow_checks
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
contains
  function stable_dt(alpha, dx) result(dt)
    real(dp), intent(in) :: alpha, dx
    real(dp) :: dt
    dt = dx**2/(4.0_dp*alpha)
  end function stable_dt
end module heatflow_checks

program interface_mismatch
  use heatflow_checks
  implicit none
  print *, stable_dt(1, 0.1)   ! default integer and default real instead of real(dp)
  print *, stable_dt(1.0_dp)   ! missing actual argument
end program interface_mismatch
02a_error_explicit_interface_mismatch.f90:16:10:

   16 |   print *, stable_dt(1, 0.1)   ! default integer and default real instead of real(dp)
      |          1
Error: Type mismatch in argument 'alpha' at (1); passed INTEGER(4) to REAL(8)
02a_error_explicit_interface_mismatch.f90:16:10:

   16 |   print *, stable_dt(1, 0.1)   ! default integer and default real instead of real(dp)
      |          1
Error: Type mismatch in argument 'dx' at (1); passed REAL(4) to REAL(8)
02a_error_explicit_interface_mismatch.f90:17:10:

   17 |   print *, stable_dt(1.0_dp)   ! missing actual argument
      |          1
Error: Missing actual argument for argument 'dx' at (1)

Flang 23 reports the same three problems with its own wording. If a project truly needs mismatching calls to an external procedure (a generic message-passing routine, say), the standard-conforming tool is an explicit interface with an assumed-type dummy type(*), not the legacy escape -fallow-argument-mismatch, which GNU Fortran offers only to degrade this error to a warning and recommends against.

Interface blocks and import

For an external procedure that is not in a module, an interface body provides the explicit interface. It repeats the procedure’s function or subroutine statement, the declarations of its dummy arguments and result, and the matching end statement, and nothing else. An interface body is a scoping unit of its own: it does not inherit the host’s use associations, kinds or types. If it needs a kind such as dp that the host obtained with use, it has to repeat the use or bring the name in with an import statement (J3/23-007r1, 8.8). The four forms:

Statement Effect Standard

import :: dp, grid_t

Makes the listed host entities accessible in the interface body.

Fortran 2003

import, only: dp

Like the list form, and additionally nothing else is imported; if any import in a scope has only, all of them must (C8102).

Fortran 2018

import, none

Nothing from the host is accessible. This is already the default for an ordinary interface body; the statement is useful where host access is the default instead (a block construct, a module procedure interface body) and to document the intent.

Fortran 2018

import, all

Every host entity is accessible; a local declaration that would hide a host entity is an error (C8106). It guards against accidental shadowing.

Fortran 2018

A bare import (no list) makes every host entity accessible; it is the default for module procedure interface bodies, internal subprograms and derived-type definitions. import cannot appear in a main program, a module, an external subprogram or a block data program unit (C8100), because those have no host.

The example declares three external procedures with three styles of import. The use statement of a scope must come before import (the compiler message when it does not is explicit about it), and the external procedures are defined below the main program only to keep the file runnable.

! An interface body describes an external procedure; import controls what it can see.
program interface_import
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none (type, external)

  interface
    ! import, only: the body may use dp from the host and nothing else
    function legacy_norm(n, x) result(r)
      import, only: dp
      integer, intent(in) :: n
      real(dp), intent(in) :: x(n)
      real(dp) :: r
    end function legacy_norm

    ! import, none: the body brings its own use statement (use comes first), nothing else is inherited
    subroutine legacy_fill(n, x, v)
      use, intrinsic :: iso_fortran_env, only: wp => real64
      import, none
      integer, intent(in) :: n
      real(wp), intent(out) :: x(n)
      real(wp), intent(in) :: v
    end subroutine legacy_fill

    ! import, all (Fortran 2018): every entity of the host is accessible, so a local name
    ! that clashes with a host name would be an error.
    function legacy_scale(x, factor) result(y)
      import, all
      real(dp), intent(in) :: x, factor
      real(dp) :: y
    end function legacy_scale
  end interface

  real(dp) :: temperatures(4)

  call legacy_fill(size(temperatures), temperatures, 20.0_dp)
  temperatures(3) = 24.0_dp
  write (*, '(a,f8.4)') 'norm = ', legacy_norm(size(temperatures), temperatures)
  write (*, '(a,f8.4)') 'scaled = ', legacy_scale(temperatures(3), 0.5_dp)
end program interface_import

! The two external procedures described above (normally in another file or library).
function legacy_norm(n, x) result(r)
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none (type, external)
  integer, intent(in) :: n
  real(dp), intent(in) :: x(n)
  real(dp) :: r
  r = sqrt(sum(x**2))
end function legacy_norm

subroutine legacy_fill(n, x, v)
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none (type, external)
  integer, intent(in) :: n
  real(dp), intent(out) :: x(n)
  real(dp), intent(in) :: v
  x = v
end subroutine legacy_fill

function legacy_scale(x, factor) result(y)
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none (type, external)
  real(dp), intent(in) :: x, factor
  real(dp) :: y
  y = x*factor
end function legacy_scale
norm =  42.1426
scaled =  12.0000

implicit none (external) and missing declarations

Since Fortran 2018, implicit none can be written with a list: implicit none (type) is the classic form, implicit none (external) additionally demands that every external or dummy procedure used in the scope has an explicit interface or the external attribute (J3/23-007r1, 8.7, constraint C899), and implicit none (type, external) does both. Plain implicit none is the same as implicit none (type). The next file is non-conforming: it calls an external subroutine that has neither.

! INTENTIONALLY NON-CONFORMING under implicit none (external): no interface, no external statement.
program missing_external
  implicit none (type, external)
  real :: x
  call report(x)
end program missing_external
02c_error_missing_external.f90:5:16:

    5 |   call report(x)
      |                1
Error: Procedure 'report' called at (1) is not explicitly declared

What an implicit interface lets through

An implicit interface checks nothing across separately compiled files. The pair of files below is the textbook disaster: the callee is correct (it expects one real(dp) value), the caller is syntactically fine (it passes an array of two default reals to a procedure declared only with external), and every compiler accepts both files. The program is non-conforming and its behaviour is undefined; on a little-endian IEEE machine the callee reads the eight bytes of the two single-precision numbers as one double. Compile the files separately, as a build system would:

! File 1 of 2. A perfectly correct external subroutine: it expects one real(dp) value.
subroutine show_value(x)
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none (type, external)
  real(dp), intent(in) :: x
  print '(a,es22.15)', 'show_value received ', x
end subroutine show_value
! File 2 of 2. INTENTIONALLY NON-CONFORMING: with only an external statement the interface is implicit,
! so nothing checks that two default reals are passed where one real(dp) is expected.
program implicit_interface_caller
  implicit none (type, external)
  external :: show_value
  real :: pair(2)
  pair = [1.0, 2.0]
  call show_value(pair)
end program implicit_interface_caller
gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all -c 02d_error_implicit_interface_callee.f90
gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all -c 02d_error_implicit_interface_caller.f90
gfortran 02d_error_implicit_interface_callee.o 02d_error_implicit_interface_caller.o -o implicit_demo
./implicit_demo
show_value received  2.000000473111868E+00

Neither -Wall nor -Wextra complains here. GNU Fortran has two diagnostics aimed at this situation: -Wimplicit-interface warns at every call that uses an implicit interface (it does not check that interfaces agree across files), and -Wimplicit-procedure warns about a call to a procedure that has neither an explicit interface nor an external declaration. Within a single source file GNU Fortran does compare a call with a definition it has already seen and reports a mismatch as an error, but that is a courtesy and it cannot happen across separately compiled files. The cure is structural: put the callee in a module so that its interface is explicit, or write an interface body that is shared through a module.

Argument association

When a procedure is invoked, each dummy argument becomes associated with an actual argument for the duration of the call. Fortran does not say how: it says what a program may observe (J3/23-007r1, 15.5.2.4). In practice a compiler passes the address of a scalar or contiguous array, or a descriptor of an assumed-shape array, so a dummy argument without the value attribute is an alias of the actual argument, not a copy of it. Two consequences follow, and both surprise programmers who come from languages with call-by-value:

  • A change made through the dummy argument is a change to the caller’s variable at once, not when the procedure returns.

  • Note 2 to 15.5.2.4 says that argument association usually resembles call by reference combined with call by value-result, and leaves the mechanism to the processor. A compiler is free to make a temporary copy on entry and copy it back on exit (copy-in/copy-out) when that is the only way to present contiguous storage to the callee. A program that is correct under the rules of the standard cannot tell the difference, and a program that depends on one of them is non-conforming (see the aliasing section below).

flowchart TD A["Actual argument at the call"] --> V{"Dummy argument has the value attribute?"} V -- yes --> C1["Anonymous copy of the value: changes stay local, the actual argument is untouched"] V -- no --> S{"Dummy needs contiguous storage? explicit-shape or assumed-size array, or contiguous attribute"} S -- no --> R1["Associated with the actual argument itself: address or descriptor, no copy"] S -- yes --> K{"Actual argument is contiguous?"} K -- yes --> R2["Associated with the actual argument itself: no copy"] K -- no --> C2["Compiler copies in to a temporary and copies back after the call: copy-in and copy-out"]
! Argument association: by reference, by value, and copy-in/copy-out for a non-contiguous section.
module heatflow_args
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  private
  public :: scale_in_place, bump_copy, scale_explicit_shape, scale_assumed_shape

contains

  ! Without value, the dummy argument is associated with the actual argument (no copy is made).
  subroutine scale_in_place(x, factor)
    real(dp), intent(inout) :: x
    real(dp), intent(in) :: factor
    x = x*factor
  end subroutine scale_in_place

  ! With value, the dummy argument is a local copy that may be modified freely.
  subroutine bump_copy(x)
    real(dp), value :: x
    x = x + 1.0_dp
    print '(a,f5.1)', '  inside bump_copy, x = ', x
  end subroutine bump_copy

  ! Explicit-shape dummy: it requires contiguous storage, so a strided actual is copied in and out.
  subroutine scale_explicit_shape(n, x, factor)
    integer, intent(in) :: n
    real(dp), intent(inout) :: x(n)
    real(dp), intent(in) :: factor
    x = x*factor
  end subroutine scale_explicit_shape

  ! Assumed-shape dummy: the array descriptor carries the stride, no copy is needed.
  subroutine scale_assumed_shape(x, factor)
    real(dp), intent(inout) :: x(:)
    real(dp), intent(in) :: factor
    x = x*factor
  end subroutine scale_assumed_shape

end module heatflow_args

program argument_association
  use, intrinsic :: iso_fortran_env, only: dp => real64
  use heatflow_args
  implicit none
  real(dp) :: t, row(8)
  integer :: k

  t = 20.0_dp
  call scale_in_place(t, 1.5_dp)
  print '(a,f5.1)', 'after scale_in_place, t = ', t

  call bump_copy(t)
  print '(a,f5.1)', 'after bump_copy, t = ', t

  row = [(real(k, dp), k=1, 8)]
  print '(a)', 'assumed-shape dummy with a strided section (no copy):'
  call scale_assumed_shape(row(1:8:2), 10.0_dp)
  print '(8f6.1)', row

  print '(a)', 'explicit-shape dummy with a strided section (copy-in/copy-out):'
  call scale_explicit_shape(4, row(2:8:2), 10.0_dp)
  print '(8f6.1)', row
end program argument_association

The output of the program is shown first, followed by what gfortran wrote to the error unit; the -fcheck=array-temps check, which is part of -fcheck=all, prints the warning once per call site that needs a temporary. The same program under Flang prints the same numbers and no warning.

after scale_in_place, t =  30.0
  inside bump_copy, x =  31.0
after bump_copy, t =  30.0
assumed-shape dummy with a strided section (no copy):
  10.0   2.0  30.0   4.0  50.0   6.0  70.0   8.0
explicit-shape dummy with a strided section (copy-in/copy-out):
  10.0  20.0  30.0  40.0  50.0  60.0  70.0  80.0
At line 61 of file 04_argument_association.f90
Fortran runtime warning: An array temporary was created for argument 'x' of procedure 'scale_explicit_shape'

What the example shows:

  • scale_in_place changes t in the caller: the dummy argument is t under another name.

  • bump_copy has the value attribute, so it works on an anonymous copy initialised from the actual argument; the increment is invisible to the caller. value may not be combined with intent(out) or intent(inout), with allocatable, pointer or volatile (constraint C868), and it cannot be a coarray or an assumed-size array (C867). intent(in) with value is legal and, for a scalar, simply forbids the callee from assigning to its copy. value is also how Fortran receives C-style arguments passed by value (see Interoperability with C).

  • scale_assumed_shape takes a stride-2 section without any copy because an assumed-shape dummy argument is passed with a descriptor that records the stride. scale_explicit_shape declares x(n), which promises contiguous storage; the section row(2:8:2) is not contiguous, so the compiler built a temporary, called the procedure, and copied the result back. That is correct but slow in a hot loop, and the reason why the performance advice for array arguments is to prefer assumed-shape dummies (or add contiguous when the callee needs contiguity and the caller usually has it). GNU Fortran can also report temporaries at compile time with -Warray-temporaries.

The detailed rules for array dummy arguments, including sequence association and the contiguous attribute, are on Array Arguments: Assumed Shape, Size and Rank.

intent: stating how an argument is used

The intent attribute states what the procedure intends to do with a dummy argument (J3/23-007r1, 8.5.10). It documents the interface, it lets the compiler reject mistakes, and it lets the optimiser rely on the facts it states.

Attribute Meaning

intent(in)

The dummy argument may be used but must neither be defined nor become undefined during the call (C846: it cannot appear in a variable definition context). The actual argument may be any expression, including a constant.

intent(out)

The dummy argument becomes undefined on entry, except for components that have default initialization. The actual argument must be definable. An allocatable actual argument is deallocated on entry (9.7.3.2); a pointer’s association status becomes undefined.

intent(inout)

The value is used and may be changed. The actual argument must be definable.

(none)

The dummy argument is subject to the limits of whatever it is associated with: it may be defined only if the actual argument is definable, and the compiler cannot reason about it.

Points that go beyond the table:

  • intent(inout) is not the same as no intent: an actual argument for inout must always be definable, whereas with no intent it need be definable only when the callee actually defines it. Pass a literal constant to an intent(inout) dummy argument and the compiler rejects the call.

  • intent(out) means the result value is entirely determined by the procedure. If a procedure may leave an argument unchanged and the argument should then keep its value, intent(inout) is the right choice, because intent(out) would have made it undefined (Note 4 of 8.5.10).

  • For allocatable and pointer dummy arguments, intent applies to the allocation status or the association as well as the value. intent(out) on an allocatable deallocates the actual argument on entry; intent(in) on a pointer forbids changing its association, but the target remains modifiable (Note 3).

  • A nonpointer dummy argument with intent(in) is not modifiable as a whole or in part, including every subobject. A pointer component of an intent(in) derived-type dummy can still have its target changed.

  • Pure procedures need intent for every nonpointer dummy argument (C1592 for functions, C1595 for subroutines). Elemental procedures need it for every dummy argument that does not have value (C15120).

  • intent is only available for dummy data objects and dummy procedure pointers; a plain dummy procedure has none (C845).

The example below demonstrates intent(inout), intent(out) with default initialization, intent(out) with an allocatable, and intent(in). run_state_t has default initialization for both components, so every call to start_run resets the caller’s variable before the first statement of the callee executes.

! What intent(in), intent(out) and intent(inout) mean, and what intent(out) does to its actual argument.
module heatflow_intent
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  private
  public :: run_state_t, start_run, advance, reset_buffer

  type :: run_state_t
    integer :: steps = 0                 ! default initialization
    real(dp) :: residual = huge(1.0_dp)  ! default initialization
  end type run_state_t

contains

  ! intent(out) with a derived type that has default initialization:
  ! on entry the dummy argument is reset to the defaults, whatever the caller had.
  subroutine start_run(state)
    type(run_state_t), intent(out) :: state
    print '(a,i0,a)', '  start_run sees steps = ', state%steps, ' on entry'
  end subroutine start_run

  ! intent(inout): the value is used and updated.
  subroutine advance(state, new_residual)
    type(run_state_t), intent(inout) :: state
    real(dp), intent(in) :: new_residual   ! intent(in): cannot be assigned here
    state%steps = state%steps + 1
    state%residual = new_residual
  end subroutine advance

  ! intent(out) with an allocatable dummy: the actual argument is deallocated on entry.
  subroutine reset_buffer(buffer, n)
    real(dp), allocatable, intent(out) :: buffer(:)
    integer, intent(in) :: n
    print '(a,l1,a)', '  reset_buffer sees allocated(buffer) = ', allocated(buffer), ' on entry'
    allocate (buffer(n), source=0.0_dp)
  end subroutine reset_buffer

end module heatflow_intent

program intent_demo
  use, intrinsic :: iso_fortran_env, only: dp => real64
  use heatflow_intent
  implicit none
  type(run_state_t) :: state
  real(dp), allocatable :: buffer(:)

  call start_run(state)
  call advance(state, 0.5_dp)
  call advance(state, 0.25_dp)
  print '(a,i0,a,f5.2)', 'after two steps: steps = ', state%steps, ', residual = ', state%residual

  print '(a)', 'second start_run on the same variable:'
  call start_run(state)
  print '(a,i0)', 'steps is back to ', state%steps

  allocate (buffer(5), source=1.0_dp)
  print '(a)', 'reset_buffer on an allocated array:'
  call reset_buffer(buffer, 3)
  print '(a,i0,a,f4.1)', 'size = ', size(buffer), ', sum = ', sum(buffer)
end program intent_demo
  start_run sees steps = 0 on entry
after two steps: steps = 2, residual =  0.25
second start_run on the same variable:
  start_run sees steps = 0 on entry
steps is back to 0
reset_buffer on an allocated array:
  reset_buffer sees allocated(buffer) = F on entry
size = 3, sum =  0.0

An assignment to an intent(in) dummy argument is a constraint violation, and every compiler reports it. The next file is non-conforming:

! INTENTIONALLY NON-CONFORMING: assigning to an intent(in) dummy argument.
subroutine clamp_residual(residual, limit)
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none (type, external)
  real(dp), intent(inout) :: residual
  real(dp), intent(in) :: limit
  if (residual > limit) limit = residual   ! wrong: limit is intent(in)
end subroutine clamp_residual
03a_error_intent_in.f90:7:25:

    7 |   if (residual > limit) limit = residual   ! wrong: limit is intent(in)
      |                         1~~~~~
Error: Dummy argument 'limit' with INTENT(IN) in variable definition context (assignment) at (1)

Common intent mistakes

  • Forgetting that intent(out) discards the old value. A subroutine that returns early without assigning an intent(out) scalar leaves the caller’s variable undefined. If the old value must survive when nothing is computed, use intent(inout).

  • intent(out) and derived types. A derived type with default initialization is reset on every call; a type without default initialization comes back undefined, component by component.

  • intent(out) on a finalizable argument. A nonpointer, nonallocatable intent(out) dummy argument is finalized before it becomes undefined (7.5.6.3); pure procedures therefore forbid it when finalization would call an impure procedure (C1596), and forbid polymorphic intent(out) arguments altogether (C1597).

  • No intent at all. The compiler cannot warn about assignments to something that should be read-only, a reader cannot tell input from output, and the optimiser can assume less.

  • An unused dummy argument. -Wunused-dummy-argument, implied by -Wall, reports a dummy argument that is never referenced, which usually means an interface that drifted from its body.

Optional and keyword arguments

A dummy argument with the optional attribute need not have an actual argument at every call (J3/23-007r1, 8.5.12). The intrinsic function present(x) tells the procedure whether it has one. Calls can then name the arguments they supply with argument keywords (name = value), so that optional arguments can be skipped and the call documents itself.

The rules, all of which need an explicit interface at the call (15.4.2.2):

  • Arguments are matched first by position and then by keyword. After the first keyword, every later actual argument must have a keyword too (C1531). The keyword is the name of the dummy argument in the interface the caller sees, which may differ from the name in the callee’s own source if the caller sees an interface body (C1532).

  • Exactly one actual argument corresponds to each nonoptional dummy argument; at most one to each optional dummy argument (15.5.2.1).

  • An absent optional argument must not be referenced or defined. It must not be used as the target of a pointer assignment, invoked (if it is a procedure), used as the selector of associate, select type or select rank, or passed on to a nonoptional dummy argument. The only things that can be done with it are present(x), passing it to another optional dummy argument (in which case it is absent there too).

  • A dummy argument is also absent when its actual argument is an unallocated allocatable or a disassociated pointer and the dummy argument is neither allocatable nor a pointer. This turns an allocatable variable into an optional switch: allocated means "supplied" (15.5.2.13).

  • present is available for any optional dummy argument, including nested forwarding through several procedures.

The next example gives describe_run three optional numeric arguments and one optional string, and shows keywords in any order, skipped arguments, the unallocated-allocatable rule and forwarding an absent argument.

! optional dummy arguments, present, keyword calls and forwarding of absent arguments.
module heatflow_options
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  private
  public :: describe_run, stable_step

  real(dp), parameter :: default_alpha = 1.0_dp

contains

  ! Optional dummy arguments need an explicit interface at the call (here: a module procedure).
  subroutine describe_run(nx, ny, alpha, tolerance, label)
    integer, intent(in) :: nx
    integer, intent(in), optional :: ny
    real(dp), intent(in), optional :: alpha, tolerance
    character(len=*), intent(in), optional :: label
    integer :: ny_used
    real(dp) :: alpha_used

    ! Never reference an absent argument; choose a default first.
    ny_used = nx
    if (present(ny)) ny_used = ny
    alpha_used = default_alpha
    if (present(alpha)) alpha_used = alpha
    print '(a,i0,a,i0,a,f4.2)', 'grid ', nx, ' x ', ny_used, ', alpha = ', alpha_used
    if (present(label)) print '(2a)', '  label: ', label
    if (present(tolerance)) then
      if (tolerance > 0.0_dp) print '(a,es8.1)', '  tolerance: ', tolerance
    end if
  end subroutine describe_run

  ! Forwarding: an absent optional actual argument may be passed on to another optional dummy.
  function stable_step(dx, alpha, safety) result(dt)
    real(dp), intent(in) :: dx
    real(dp), intent(in), optional :: alpha, safety
    real(dp) :: dt
    dt = scaled_step(dx, alpha)
    if (present(safety)) dt = safety*dt
  end function stable_step

  function scaled_step(dx, alpha) result(dt)
    real(dp), intent(in) :: dx
    real(dp), intent(in), optional :: alpha
    real(dp) :: dt
    dt = dx**2/(4.0_dp*default_alpha)
    if (present(alpha)) dt = dx**2/(4.0_dp*alpha)
  end function scaled_step

end module heatflow_options

program optional_keyword
  use, intrinsic :: iso_fortran_env, only: dp => real64
  use heatflow_options
  implicit none
  real(dp), allocatable :: tolerance_override

  call describe_run(64)
  call describe_run(64, 32, 0.5_dp)
  call describe_run(64, tolerance=1.0e-6_dp, label='coarse run')      ! keywords may skip optional arguments
  call describe_run(label='fine run', nx=128, alpha=0.25_dp)           ! and may appear in any order
  ! An unallocated allocatable actual argument is treated as absent by an optional, non-allocatable dummy.
  call describe_run(32, tolerance=tolerance_override)
  tolerance_override = 1.0e-8_dp
  call describe_run(32, tolerance=tolerance_override)
  print '(a,es10.3)', 'step, defaults:        ', stable_step(0.1_dp)
  print '(a,es10.3)', 'step, alpha=2 safety=.5:', stable_step(0.1_dp, safety=0.5_dp, alpha=2.0_dp)
end program optional_keyword
grid 64 x 64, alpha = 1.00
grid 64 x 32, alpha = 0.50
grid 64 x 64, alpha = 1.00
  label: coarse run
  tolerance:  1.0E-06
grid 128 x 128, alpha = 0.25
  label: fine run
grid 32 x 32, alpha = 1.00
grid 32 x 32, alpha = 1.00
  tolerance:  1.0E-08
step, defaults:         2.500E-03
step, alpha=2 safety=.5: 6.250E-04

The absent-optional trap

Because an absent argument must not even be referenced, an expression that always evaluates both operands is unsafe. merge(alpha, 1.0_dp, present(alpha)) looks like a neat way to supply a default, but merge is an ordinary function: its arguments are evaluated before it is called, so alpha is referenced even when absent. The same applies to present(x) .and. x > 0: Fortran does not guarantee short-circuit evaluation, and a processor may evaluate x > 0 first (J3/23-007r1, 10.1.7). The safe forms are a nested if, or assigning a default to a local variable first, as describe_run does. The next program is non-conforming and fails at run time:

! INTENTIONALLY NON-CONFORMING: merge evaluates both value arguments, so an absent optional is referenced.
program absent_optional
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none (type, external)
  call show()
contains
  subroutine show(alpha)
    real(dp), intent(in), optional :: alpha
    real(dp) :: used
    used = merge(alpha, 1.0_dp, present(alpha))   ! alpha is referenced even when it is absent
    print '(f5.2)', used
  end subroutine show
end program absent_optional
Program received signal SIGSEGV: Segmentation fault - invalid memory reference.

Backtrace for this error:
...
Segmentation fault

The backtrace lines are omitted. Neither -fcheck=all nor Flang diagnosed the mistake at compile time or run time; both programs ended with a segmentation fault, which is only one of the possible outcomes of undefined behaviour.

Conditional arguments (Fortran 2023)

Fortran 2023 adds conditional expressions (cond ? a : b) and conditional arguments that may end in .nil., a consequent that means "pass no argument at all" (J3/23-007r1, 10.1.2.3 and 15.5.1; 15.5.2.3 gives the semantics). A conditional argument chooses among its consequents by evaluating the logical conditions in order; if the chosen consequent is .nil. the corresponding optional dummy argument is absent. It replaces the pattern in which the caller wraps a call in an if construct once per combination of optional arguments, or defines a wrapper procedure that forwards the possibly missing argument. The rules in short:

  • The whole conditional argument is parenthesised; only the consequents that are evaluated are evaluated (unlike merge), and at least one consequent must be an expression or variable, never only .nil. (C1540).

  • .nil. is allowed only for an optional dummy argument.

  • All consequent arguments have the same declared type, kind and rank (C1538, C1539). If the dummy argument is intent(out) or intent(inout), every consequent must be a variable (C1541).

  • Nesting of conditional arguments is not allowed; a consequent may be a conditional expression.

The next file uses the new syntax. It is valid Fortran 2023 according to J3/23-007r1 but it is not accepted by either compiler used for this page, so it has not been run; the two diagnostics are shown below it. It is the only example on the page that has not been compiled.

! FORTRAN 2023 SYNTAX (conditional expression and conditional argument, J3/23-007r1 10.1.4 and 15.5.2.3).
! NOT COMPILED SUCCESSFULLY: gfortran 16.2 and LLVM Flang 23 both reject it (see the diagnostics on the page).
program conditional_arguments
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none (type, external)
  real(dp) :: alpha
  logical :: alpha_known

  alpha = 0.5_dp
  alpha_known = .true.
  ! The second actual argument is absent when alpha_known is false (.nil.).
  call show(64, (alpha_known ? alpha : .nil.))
contains
  subroutine show(nx, alpha)
    integer, intent(in) :: nx
    real(dp), intent(in), optional :: alpha
    if (present(alpha)) then
      print '(i0,a,f4.2)', nx, ' with alpha ', alpha
    else
      print '(i0,a)', nx, ' with the default alpha'
    end if
  end subroutine show
end program conditional_arguments
05a_conditional_arguments_f2023.f90:12:45:

   12 |   call show(64, (alpha_known ? alpha : .nil.))
      |                                             1
Error: Syntax error in expression at (1)
error: Semantic errors in 05a_conditional_arguments_f2023.f90
05a_conditional_arguments_f2023.f90:12:3: error: not yet implemented: Fortran 2023 conditional arguments are not yet supported
    call show(64, (alpha_known ? alpha : .nil.))
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The first message is from GNU Fortran 16.2, the second from LLVM Flang 23, whose status table also lists conditional expressions and arguments as not supported. Until your compiler accepts the syntax, write the same behaviour with an if construct, which works with every Fortran 2008 compiler (this program was compiled and run with both):

! The same behaviour as a conditional argument, written with an if construct (any Fortran 2008 or later compiler).
program conditional_portable
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none (type, external)
  real(dp) :: alpha
  integer :: pass

  alpha = 0.5_dp
  do pass = 1, 2
    if (pass == 1) then
      call show(64, alpha)    ! alpha present
    else
      call show(64)           ! alpha absent
    end if
  end do
contains
  subroutine show(nx, alpha)
    integer, intent(in) :: nx
    real(dp), intent(in), optional :: alpha
    if (present(alpha)) then
      print '(i0,a,f4.2)', nx, ' with alpha ', alpha
    else
      print '(i0,a)', nx, ' with the default alpha'
    end if
  end subroutine show
end program conditional_portable
64 with alpha 0.50
64 with the default alpha

Support differs between vendors; the compiler-support table on History, Standards and Compilers lists which compilers accept conditional expressions and arguments.

Function results

A function returns whatever type and shape its result variable has: a scalar of an intrinsic type, a derived type, an array, an allocatable or a pointer. The result is declared like a local variable, either through the type in the function statement or by name in the specification part, and if it is an array, allocatable or a pointer this must be said in the specification part (J3/23-007r1, 15.6.2.2).

  • Derived-type results return several values at once without intent(out) arguments.

  • Array results may be explicit-shape, with extents given by specification expressions of the dummy arguments, such as an array x(n) of type real(dp). An array result with a shape that depends on the contents of an argument cannot be explicit-shape; it must be allocatable (or a pointer).

  • Allocatable results (Fortran 2003 and later) are allocated by the function and released automatically once the expression that used them has been evaluated. In the body the first assignment p = u(row, :) allocates p with the right extent (automatic reallocation on assignment), so no allocate statement is needed.

  • Character results are either a constant or specification-expression length (character(len=len(text))) or a deferred-length allocatable (character(len=:), allocatable). An assumed-length result character(*) function is obsolescent and is covered with the legacy features below.

  • Pointer results are allowed; the pointer’s association status must not be undefined on return. They are rarely the right choice, because the target’s lifetime is then the caller’s problem.

  • A function that does not define its (nonpointer) result on every path is non-conforming. GNU Fortran finds the cases that are visible to its data-flow analysis only when optimising: with -Wall -O1 it reports that the result "may be used uninitialized" (-Wmaybe-uninitialized) or "is used uninitialized" (-Wuninitialized).

All of these require an explicit interface at the call (J3/23-007r1, 15.4.2.2). return ends the function early; the result variable keeps whatever it held at that point, so assign it first (hot_cells below does).

! Function results: scalar, array, allocatable, character, derived type, and a function without arguments.
module heatflow_results
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  private
  public :: stats_t, grid_stats, grid_spacing, profile, run_label, hot_cells, default_dt

  type :: stats_t
    real(dp) :: lowest, highest, mean
  end type stats_t

contains

  ! Derived-type result: several values from one call without intent(out) arguments.
  function grid_stats(u) result(s)
    real(dp), intent(in) :: u(:, :)
    type(stats_t) :: s
    s = stats_t(minval(u), maxval(u), sum(u)/real(size(u), dp))
  end function grid_stats

  ! Explicit-shape array result: its extent is a specification expression of a dummy argument.
  function grid_spacing(n, length) result(x)
    integer, intent(in) :: n
    real(dp), intent(in) :: length
    real(dp) :: x(n)
    integer :: i
    x = [(length*real(i - 1, dp)/real(n - 1, dp), i=1, n)]
  end function grid_spacing

  ! Assumed-shape arguments cannot size a result, so the result is allocatable (Fortran 2003 and later).
  function profile(u, row) result(p)
    real(dp), intent(in) :: u(:, :)
    integer, intent(in) :: row
    real(dp), allocatable :: p(:)
    p = u(row, :)          ! allocated on assignment to the right extent
  end function profile

  ! Deferred-length allocatable character result.
  function run_label(nx, ny) result(text)
    integer, intent(in) :: nx, ny
    character(len=:), allocatable :: text
    character(len=32) :: buffer
    write (buffer, '(i0,a,i0)') nx, 'x', ny
    text = 'heatflow-' // trim(buffer)
  end function run_label

  ! A function without dummy arguments is still referenced with an empty argument list: default_dt().
  function default_dt() result(dt)
    real(dp) :: dt
    dt = 1.0e-4_dp
  end function default_dt

  ! The result variable can be used like any local variable before the function returns.
  function hot_cells(u, limit) result(count)
    real(dp), intent(in) :: u(:, :)
    real(dp), intent(in) :: limit
    integer :: count
    count = 0
    if (size(u) == 0) return        ! return leaves the function with the current result value
    count = count_if_above(u, limit)
  end function hot_cells

  pure function count_if_above(u, limit) result(n)
    real(dp), intent(in) :: u(:, :)
    real(dp), intent(in) :: limit
    integer :: n
    n = count(u > limit)
  end function count_if_above

end module heatflow_results

program function_results
  use, intrinsic :: iso_fortran_env, only: dp => real64
  use heatflow_results
  implicit none
  real(dp) :: u(3, 4)
  type(stats_t) :: s
  integer :: i, j

  u = reshape([((real(10*i + j, dp), i=1, 3), j=1, 4)], shape(u))
  s = grid_stats(u)
  print '(a,3f7.2)', 'low/high/mean: ', s%lowest, s%highest, s%mean
  print '(a,5f6.2)', 'x: ', grid_spacing(5, 1.0_dp)
  print '(a,4f6.1)', 'row 2: ', profile(u, 2)
  print '(2a)', 'label: ', run_label(64, 48)
  print '(a,es9.2)', 'dt: ', default_dt()
  print '(a,i0)', 'cells above 25: ', hot_cells(u, 25.0_dp)
  print '(a,i0)', 'length of the label: ', len(run_label(8, 8))
end program function_results
low/high/mean:   11.00  34.00  22.50
x:   0.00  0.25  0.50  0.75  1.00
row 2:   21.0  22.0  23.0  24.0
label: heatflow-64x48
dt:  1.00E-04
cells above 25: 4
length of the label: 12

Recursion

A procedure is recursive if it can invoke itself, directly or through another procedure. Each invocation gets its own set of dummy arguments and unsaved local variables (an "instance", 15.6.2.4), while variables with the save attribute (including anything initialised in its declaration, which implies save) are shared by every instance.

Since Fortran 2018 every procedure is recursive by default: the recursive prefix no longer changes the meaning of a program, and non_recursive is the new way to promise that a procedure never invokes itself (15.6.2.1). Before Fortran 2018 a procedure that called itself had to be marked recursive, and a directly recursive function also needed a result clause so that its own name could be told apart from its result variable. Without result, the function name inside the body is always the result variable, so fact(n - 1) would be a reference to a scalar with a subscript, not a call.

Support is not uniform. GNU Fortran 16.2 still requires recursive for a procedure that calls itself and rejects non_recursive; LLVM Flang 23 accepts both and checks non_recursive. The safe, portable style is to write recursive on any procedure that recurses, along with result:

! Recursion: recursive prefix, result clause, mutual recursion, and the cost of saved locals.
module heatflow_recursion
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  private
  public :: coarsen_levels, refine_until, is_even, is_odd

contains

  ! Number of times a grid of n points per side can be halved while staying at least 3 points wide.
  ! A directly recursive function needs a result clause so that its name can be referenced as a function.
  recursive function coarsen_levels(n) result(levels)
    integer, intent(in) :: n
    integer :: levels
    if (n < 5) then
      levels = 0
    else
      levels = 1 + coarsen_levels((n + 1)/2)
    end if
  end function coarsen_levels

  ! A recursive subroutine halving a step until it meets a stability limit.
  recursive subroutine refine_until(dt, limit, halvings)
    real(dp), intent(inout) :: dt
    real(dp), intent(in) :: limit
    integer, intent(inout) :: halvings
    if (dt <= limit) return
    dt = dt/2.0_dp
    halvings = halvings + 1
    call refine_until(dt, limit, halvings)
  end subroutine refine_until

  ! Indirect (mutual) recursion between two module procedures.
  recursive function is_even(n) result(r)
    integer, intent(in) :: n
    logical :: r
    if (n == 0) then
      r = .true.
    else
      r = is_odd(n - 1)
    end if
  end function is_even

  recursive function is_odd(n) result(r)
    integer, intent(in) :: n
    logical :: r
    if (n == 0) then
      r = .false.
    else
      r = is_even(n - 1)
    end if
  end function is_odd

end module heatflow_recursion

program recursion_demo
  use, intrinsic :: iso_fortran_env, only: dp => real64
  use heatflow_recursion
  implicit none
  real(dp) :: dt
  integer :: halvings

  print '(a,i0)', 'levels for 129 points per side: ', coarsen_levels(129)
  dt = 0.01_dp
  halvings = 0
  call refine_until(dt, 1.0e-4_dp, halvings)
  print '(a,i0,a,es9.3)', 'halvings: ', halvings, ', final dt = ', dt
  print '(a,l1,a,l1)', 'is_even(10) = ', is_even(10), ', is_odd(10) = ', is_odd(10)
end program recursion_demo
levels for 129 points per side: 6
halvings: 7, final dt = 7.813E-05
is_even(10) = T, is_odd(10) = F

Practical points:

  • Direct recursion without recursive on gfortran 16.2 is a compile-time error ("Function 'fact' at (1) cannot be called recursively, as it is not RECURSIVE"); Flang 23 accepts it and prints the result. Declaring a procedure non_recursive and then recursing is an error in Flang ("NON_RECURSIVE procedure 'fact' cannot call itself"), and a syntax error in gfortran 16.2.

  • Indirect recursion is where compilers disagree at run time. A procedure that is called back through a callback or a procedure pointer may be re-entered without any visible self-call, as the next file shows. gfortran 16.2 only knows a procedure is recursive if it carries the recursive prefix (or -frecursive is given); -fcheck=recursion, part of -fcheck=all, makes a re-entry into a non-recursive procedure a run-time error. The file below is conforming Fortran 2018 and later but fails under gfortran 16.2 because of this convention; Flang runs it correctly.

  • Stack and static storage. Local arrays of a non-recursive procedure may be placed in static storage by some compilers, especially above a size limit (-fmax-stack-var-size); recursion needs automatic (stack) storage. -frecursive makes GNU Fortran allocate every local array on the stack and cannot be combined with -fmax-stack-var-size= or -fno-automatic. Deep recursion on large locals can exhaust the stack: the symptom is a segmentation fault, not a Fortran error message.

! Indirect recursion through a callback. Conforming since Fortran 2018 (procedures are recursive by
! default), but gfortran 16.2 still treats a procedure without the recursive prefix as non-recursive.
module walker
  implicit none
  abstract interface
    subroutine visitor_i(depth)
      integer, intent(in) :: depth
    end subroutine visitor_i
  end interface
contains
  subroutine walk(depth, visit)
    integer, intent(in) :: depth
    procedure(visitor_i) :: visit
    call visit(depth)
  end subroutine walk
end module walker

program indirect_recursion
  use walker
  implicit none (type, external)
  call walk(0, descend)
contains
  subroutine descend(depth)
    integer, intent(in) :: depth
    print '(a,i0)', 'depth ', depth
    if (depth < 2) call walk(depth + 1, descend)
  end subroutine descend
end program indirect_recursion

The gfortran 16.2 run (the warning is printed at compile time, then the check fires at run time; the backtrace is shortened) and the Flang 23 run:

07b_indirect_recursion_check.f90:26:40:

   26 |     if (depth < 2) call walk(depth + 1, descend)
      |                                        1
Warning: Non-RECURSIVE procedure 'descend' at (1) is possibly calling itself recursively.  Declare it RECURSIVE or use '-frecursive'
depth 0
At line 11 of file 07b_indirect_recursion_check.f90
Fortran runtime error: Recursive call to nonrecursive procedure 'walk'

Error termination. Backtrace:
...
depth 0
depth 1
depth 2

The warning message in the first output shows that GNU Fortran treats the program as suspect; adding recursive to walk and descend makes it run under gfortran, and also under every older standard.

Purity: pure, elemental, impure elemental and simple

The prefixes pure, elemental, impure and simple are promises about side effects. They exist so that a compiler (and a human) can call a procedure in contexts where the order of evaluation is unspecified or parallel: inside do concurrent, inside the expressions of an array statement, and inside other pure procedures. The summary:

Kind What it promises Typical use

pure

No variable outside the procedure is defined (no assignment to host- or use-associated variables, no intent(in) dummy argument changed, no pointer association changed through them); no external I/O; no stop or other image control statement (error stop is allowed); every procedure it calls is pure.

Numerical kernels, helper functions called from do concurrent or from other pure procedures.

elemental

pure, plus: written with scalar dummy arguments and a scalar result, and applicable to any conformable arrays. The result has the shape of the array arguments.

Scalar formulas that should work for scalars, vectors and matrices alike (fourier_number, celsius_to_kelvin).

impure elemental

Elemental without the purity promise: it may read or write files, change module variables, or call impure procedures. Elements are processed in array element order.

Logging, counters, random numbers, applying an action to every element of an array.

simple

pure, plus: it references no variable from use or host association (named constants are fine), no common, has no entry, and calls only simple procedures. Fortran 2023.

Library kernels that must not depend on global state, and procedures that a compiler can reorder, vectorise or offload freely. A simple procedure may also be elemental.

The constraints on pure procedures

The summary in Note 4 of 15.7 says it best: a pure subprogram cannot contain any operation that could conceivably assign to a variable the caller can see, perform external file I/O, or execute an image control statement (including stop). Note the word conceivably: a pure subprogram is not allowed to contain an assignment to a global variable even in a branch that never executes. The main constraints (C1592 to C15110):

  • Every nonpointer dummy argument of a pure function has intent(in) or value; every nonpointer dummy argument of a pure subroutine has an explicit intent. Dummy procedures must be pure.

  • No local variable has save or volatile. Initialization in a declaration implies save, so integer :: n = 0 is not allowed in a pure procedure.

  • No variable that is accessed by use or host association, is an intent(in) dummy argument, is in common or is a coindexed object may appear in a variable definition context, as the target of a pointer assignment, or as an actual argument for a pointer dummy argument.

  • No print, open, close, backspace, endfile, rewind, flush, wait, inquire, and no read or write with an external unit or *. Internal I/O (reading and writing a character variable) is allowed.

  • Every procedure referenced, including through defined operators, defined assignment, defined I/O and finalization, is pure. All internal procedures of a pure procedure are pure.

  • The interface must be explicit and say pure at every place where purity is required, such as the body of a do concurrent construct.

Because an elemental procedure is pure unless it says impure, the same constraints apply to it, plus the elemental rules: scalar, nonpointer, nonallocatable, noncoarray dummy arguments and result (C15118, C15119), and an intent for every dummy argument (C15120). A nonintrinsic elemental procedure cannot be used as an actual argument (C1533); wrap it in a nonelemental procedure first. A nonelemental pure procedure can be passed as a dummy-procedure argument (see "Procedures as arguments" below), provided the dummy procedure is declared pure in its interface.

An elemental procedure applied to arrays

In a reference to an elemental procedure with at least one array argument, all array arguments must be conformable, scalar arguments are broadcast, and the result has the shape of the arrays (15.9.2). Every actual argument corresponding to an intent(out) or intent(inout) dummy argument must then be an array. The array actual arguments are considered to be associated with the scalar dummy arguments throughout the entire elemental reference, so the aliasing restrictions of 15.5.2.14 apply to the arrays as a whole.

! pure, elemental and impure elemental procedures, and their use inside do concurrent.
module heatflow_physics
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  private
  public :: stable_dt, fourier_number, celsius_to_kelvin, clamp_temperature, log_reading, readings_logged

  integer :: readings_logged = 0

contains

  ! pure: no side effects, so it may be called from do concurrent and from other pure procedures.
  pure function stable_dt(alpha, dx, dy) result(dt)
    real(dp), intent(in) :: alpha, dx, dy
    real(dp) :: dt
    dt = 0.5_dp/(alpha*(1.0_dp/dx**2 + 1.0_dp/dy**2))
  end function stable_dt

  ! elemental: written for scalars, applied element by element to arrays of any rank.
  elemental function fourier_number(alpha, dt, dx) result(fo)
    real(dp), intent(in) :: alpha, dt, dx
    real(dp) :: fo
    fo = alpha*dt/dx**2
  end function fourier_number

  elemental function celsius_to_kelvin(t) result(k)
    real(dp), intent(in) :: t
    real(dp) :: k
    k = t + 273.15_dp
  end function celsius_to_kelvin

  ! elemental subroutine with an intent(inout) argument.
  elemental subroutine clamp_temperature(t, lo, hi)
    real(dp), intent(inout) :: t
    real(dp), intent(in) :: lo, hi
    t = min(max(t, lo), hi)
  end subroutine clamp_temperature

  ! impure elemental: allowed to do I/O and to change module state; elements are processed in array element order.
  impure elemental subroutine log_reading(t)
    real(dp), intent(in) :: t
    readings_logged = readings_logged + 1
    print '(a,i0,a,f6.1)', '  reading ', readings_logged, ': ', t
  end subroutine log_reading

end module heatflow_physics

program pure_elemental
  use, intrinsic :: iso_fortran_env, only: dp => real64
  use heatflow_physics
  implicit none
  real(dp) :: dt_limit(4), u(2, 3)
  integer :: i

  ! The elemental function works on a scalar, a vector and a matrix with the same source code.
  print '(a,f6.4)', 'Fourier number (scalar): ', fourier_number(1.0_dp, 0.002_dp, 0.1_dp)
  print '(a,4f7.4)', 'Fourier numbers (dt varies): ', fourier_number(1.0_dp, [0.001_dp, 0.002_dp, 0.003_dp, 0.004_dp], 0.1_dp)

  u = reshape([-5.0_dp, 0.0_dp, 20.0_dp, 40.0_dp, 80.0_dp, 120.0_dp], shape(u))
  call clamp_temperature(u, 0.0_dp, 100.0_dp)
  print '(a,6f6.1)', 'clamped: ', u
  print '(a,6f7.2)', 'kelvin:  ', celsius_to_kelvin(u)

  ! A pure function is allowed in do concurrent.
  do concurrent (i = 1:4)
    dt_limit(i) = stable_dt(real(i, dp), 0.1_dp, 0.1_dp)
  end do
  print '(a,4es10.2)', 'stable dt: ', dt_limit

  print '(a)', 'impure elemental subroutine applied to an array:'
  call log_reading([21.5_dp, 22.0_dp, 22.5_dp])
  print '(a,i0)', 'readings logged: ', readings_logged
end program pure_elemental
Fourier number (scalar): 0.2000
Fourier numbers (dt varies):  0.1000 0.2000 0.3000 0.4000
clamped:    0.0   0.0  20.0  40.0  80.0 100.0
kelvin:   273.15 273.15 293.15 313.15 353.15 373.15
stable dt:   2.50E-03  1.25E-03  8.33E-04  6.25E-04
impure elemental subroutine applied to an array:
  reading 1:   21.5
  reading 2:   22.0
  reading 3:   22.5
readings logged: 3

Things to notice: fourier_number is written once and called with a scalar, then with a vector where dt varies and alpha and dx are broadcast; clamp_temperature is an elemental subroutine whose intent(inout) argument is an array, so lo and hi are broadcast; log_reading is impure elemental, which is the only way to put print or a counter into an elemental procedure; and stable_dt is pure, which makes the reference inside do concurrent legal (see do concurrent and GPU Offload).

The standard states that the elements of an elemental reference are processed as if in array element order for subroutines and function elements (15.9.2, 15.9.3), which is what makes impure elemental output predictable here. For pure elemental calls the order is deliberately unobservable, since a pure procedure has no side effects; that is the freedom a compiler uses to vectorise or parallelise the loop.

Violating purity

A pure subprogram with a side effect is a constraint violation that every compiler reports. The next module is non-conforming; it modifies a module variable and does external I/O from a pure function.

! INTENTIONALLY NON-CONFORMING: a pure function with side effects.
module bad_pure
  implicit none
  integer :: calls = 0
contains
  pure function counted_square(x) result(y)
    real, intent(in) :: x
    real :: y
    calls = calls + 1             ! modifies a module variable
    print *, 'squaring', x        ! external I/O
    y = x*x
  end function counted_square
end module bad_pure
08a_error_impure_in_pure.f90:10:48:

   10 |     print *, 'squaring', x        ! external I/O
      |                                                1
Error: PRINT statement at (1) not allowed within PURE procedure
08a_error_impure_in_pure.f90:9:5:

    9 |     calls = calls + 1             ! modifies a module variable
      |     1~~~~~
Error: Variable 'calls' cannot appear in a variable definition context (assignment) at (1) in PURE procedure

The messages are the gfortran 16.2 output. Flang 23 reports the same two problems ("Left-hand side of assignment is not definable …​ 'calls' may not be defined in pure subprogram" and "External I/O is not allowed in a pure subprogram").

simple procedures (Fortran 2023)

simple is new in Fortran 2023. A simple procedure is pure and in addition cannot reference variables from outside through use or host association (a named constant is not a variable, so unit_alpha below is fine), cannot use common, cannot have an entry, and may call only simple procedures. All intrinsic functions, all functions of the intrinsic modules, and the subroutines mvbits, split and tokenize are simple (WG5 N2212, clause 7.1). It is a stronger promise than pure: a pure procedure may read a module variable, a simple one depends only on its arguments, so a compiler can evaluate it in any order, on any image or on any device.

GNU Fortran 16.2 does not accept the simple prefix. LLVM Flang 23 does accept it, so the example below was built and run with Flang only. The Flang status table still lists simple procedures as unsupported, and in practice the prefix is parsed but its extra constraints are not checked: Flang compiled the non-conforming module that follows the example without a diagnostic. A conforming program must not rely on that.

! Fortran 2023 simple procedures (J3/23-007r1 15.8). Accepted by LLVM Flang 23, rejected by gfortran 16.2.
module heatflow_simple
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  private
  public :: fourier_number, scaled

  real(dp), parameter :: unit_alpha = 1.0_dp

contains

  ! simple: pure, and in addition it cannot reference host- or use-associated variables
  ! (named constants are fine), so it depends only on its arguments.
  simple elemental function fourier_number(dt, dx) result(fo)
    real(dp), intent(in) :: dt, dx
    real(dp) :: fo
    fo = unit_alpha*dt/dx**2
  end function fourier_number

  simple function scaled(x) result(y)
    real(dp), intent(in) :: x
    real(dp) :: y
    y = 2.0_dp*fourier_number(x, 0.1_dp)   ! a simple procedure may call only simple procedures
  end function scaled

end module heatflow_simple

program simple_demo
  use, intrinsic :: iso_fortran_env, only: dp => real64
  use heatflow_simple
  implicit none (type, external)
  print '(a,3f6.2)', 'Fourier numbers: ', fourier_number([0.001_dp, 0.002_dp, 0.003_dp], 0.1_dp)
  print '(a,f6.2)', 'scaled: ', scaled(0.001_dp)
end program simple_demo
Fourier numbers:   0.10  0.20  0.30
scaled:   0.20

The gfortran 16.2 diagnostic for the first simple line is "Error: Unclassifiable statement at (1)" followed by cascading errors. The next module breaks constraint C15115 by reading a variable (alpha) through host association from a simple function; a conforming compiler must reject it. Flang 23 accepted it silently (the file was compiled with flang -c):

! INTENTIONALLY NON-CONFORMING Fortran 2023: a simple function that references a module variable.
module heatflow_simple_bad
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  real(dp) :: alpha = 1.0_dp     ! a variable, not a named constant
contains
  simple function fourier_number(dt, dx) result(fo)
    real(dp), intent(in) :: dt, dx
    real(dp) :: fo
    fo = alpha*dt/dx**2          ! not allowed: alpha is accessed by host association
  end function fourier_number
end module heatflow_simple_bad

Until simple is widely available, use pure and keep to the discipline yourself: a pure procedure that touches no module variable is simple in everything but name, and can be re-labelled when your toolchain allows it.

Internal procedures and host association

An internal procedure is defined after the contains statement of a main program, an external subprogram or a module subprogram, and is called an internal subprogram of its host. It has an explicit interface, and it is visible only inside the host. Everything declared in the host, including its dummy arguments, local variables, types and the host’s own use associations, is accessible in the internal procedure by host association (J3/23-007r1, 19.5.1.4) unless an internal declaration of the same name hides it. Because the internal procedure shares the host’s variables rather than receiving them as arguments, it works like a lightweight closure over its host’s state: it sees the current value of a host variable every time it runs.

The rules:

  • An internal subprogram cannot itself contain an internal subprogram (constraints C1572 and C1575). Nesting stops at one level; a module procedure may contain internal procedures, an internal procedure may not.

  • An internal procedure’s name is not accessible outside the host, so it cannot be called from other program units.

  • The host’s implicit none is inherited, so internal procedures need not repeat it (8.7).

  • import in an internal procedure restricts host association: import, none blocks it, import, only: ... limits it (8.8). It is a good way to keep a "pure computation" internal procedure from touching state by accident.

  • An internal procedure may be an actual argument (Fortran 2008) and may be the target of a procedure pointer. In both cases it remains associated with the instance of the host that was active when the association was made (15.6.2.4). Once the host returns, using the pointer is undefined; a procedure pointer to an internal procedure must not outlive its host.

  • Internal procedures are the natural place for helpers that need the host’s state and are not worth a module-level interface. When they grow or are reused, move them to the module and pass the data as arguments.

! Internal procedures: host association, shadowing, and the rule that they are visible only to their host.
module heatflow_report
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  private
  public :: summarize

contains

  subroutine summarize(u, unit_label)
    real(dp), intent(in) :: u(:, :)
    character(len=*), intent(in) :: unit_label
    real(dp) :: lo, hi

    lo = minval(u)
    hi = maxval(u)
    call show('minimum', lo)
    call show('maximum', hi)
    call show('spread', spread_of())
    call show('half range', half_range())

  contains

    ! show uses unit_label from the host through host association.
    subroutine show(name, value)
      character(len=*), intent(in) :: name
      real(dp), intent(in) :: value
      print '(a,a,f7.2,1x,a)', name, ': ', value, unit_label
    end subroutine show

    ! spread_of uses lo and hi from the host: it always sees their current values.
    function spread_of() result(d)
      real(dp) :: d
      d = hi - lo
    end function spread_of

    ! A local variable of the same name hides the host's variable inside this procedure.
    function half_range() result(h)
      real(dp) :: h
      real(dp) :: lo          ! hides the host's lo
      lo = (hi - minval(u))/2.0_dp
      h = lo
    end function half_range

  end subroutine summarize

end module heatflow_report

program internal_procedures
  use, intrinsic :: iso_fortran_env, only: dp => real64
  use heatflow_report, only: summarize
  implicit none (type, external)
  real(dp) :: u(2, 3)

  u = reshape([12.5_dp, 15.0_dp, 17.5_dp, 20.0_dp, 25.0_dp, 30.0_dp], shape(u))
  call summarize(u, 'degC')
  ! call show('x', 1.0_dp)   would be an error here: show is visible only inside summarize
end program internal_procedures
minimum:   12.50 degC
maximum:   30.00 degC
spread:   17.50 degC
half range:    8.75 degC

The example shows host association of unit_label, lo and hi; a local variable lo in half_range that hides the host’s lo; and an internal procedure called only from its host (a call to show from the main program would be rejected because the name is not accessible there). A common source of bugs is the accidental shadowing the standard’s Note 2 of 8.8 describes: assigning to an undeclared name inside an internal procedure under implicit typing creates a local; with implicit none it is an error instead, and with import, all no host name can ever be hidden by accident.

Nesting an internal procedure inside another one is non-conforming:

! INTENTIONALLY NON-CONFORMING: an internal procedure cannot contain another internal procedure.
program nested_internal
  implicit none (type, external)
  call outer()
contains
  subroutine outer()
    call inner()
  contains
    subroutine inner()
      print *, 'inner'
    end subroutine inner
  end subroutine outer
end program nested_internal
09a_error_nested_internal.f90:8:10:

    8 |   contains
      |          1
Error: CONTAINS statement at (1) is already in a contained program unit
09a_error_nested_internal.f90:11:24:

   11 |     end subroutine inner
      |                        1
Error: Expected label 'outer' for END SUBROUTINE statement at (1)
09a_error_nested_internal.f90:7:16:

    7 |     call inner()
      |                1
Error: Procedure 'inner' called at (1) is not explicitly declared

The first error is gfortran’s; Flang says "An internal subprogram may not contain an internal subprogram".

An internal procedure as a callback

Since an internal procedure can see its host’s variables, it can be passed to a general-purpose routine as a callback that "captures" them. integrate is a module function that takes any function matching an abstract interface, and decay is an internal function of the main program that uses alpha, t and calls from the host. Changing alpha between the two integrations changes the second result because decay reads the current value.

! Internal procedures: host association, an internal procedure used as a callback, and what it cannot do.
module heatflow_quadrature
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  private
  public :: integrate, scalar_function

  abstract interface
    function scalar_function(x) result(y)
      import, only: dp
      real(dp), intent(in) :: x
      real(dp) :: y
    end function scalar_function
  end interface

contains

  ! Composite trapezoid rule for any function that matches the abstract interface.
  function integrate(f, a, b, n) result(area)
    procedure(scalar_function) :: f
    real(dp), intent(in) :: a, b
    integer, intent(in) :: n
    real(dp) :: area
    real(dp) :: h
    integer :: i
    h = (b - a)/real(n, dp)
    area = 0.5_dp*(f(a) + f(b))
    do i = 1, n - 1
      area = area + f(a + real(i, dp)*h)
    end do
    area = area*h
  end function integrate

end module heatflow_quadrature

program internal_procedures
  use, intrinsic :: iso_fortran_env, only: dp => real64
  use heatflow_quadrature
  implicit none (type, external)
  real(dp), parameter :: pi = 3.14159265358979323846_dp
  real(dp) :: alpha, t, energy
  integer :: calls

  calls = 0
  alpha = 0.5_dp
  t = 0.05_dp

  ! decay uses alpha and t from the host through host association and counts its own calls.
  energy = integrate(decay, 0.0_dp, pi, 200)
  print '(a,f7.4,a,i0)', 'integral of sin(x) exp(-alpha t): ', energy, ', calls: ', calls

  alpha = 2.0_dp            ! the internal procedure sees the new value: it shares the host's variables
  energy = integrate(decay, 0.0_dp, pi, 200)
  print '(a,f7.4,a,i0)', 'after changing alpha:               ', energy, ', calls: ', calls

contains

  function decay(x) result(y)
    real(dp), intent(in) :: x
    real(dp) :: y
    calls = calls + 1
    y = sin(x)*exp(-alpha*t)
  end function decay

end program internal_procedures
integral of sin(x) exp(-alpha t):  1.9506, calls: 201
after changing alpha:                1.8096, calls: 402

On Linux, both GNU Fortran and Flang implement this by creating a small piece of code on the stack at run time (a trampoline) that binds the host’s frame to the procedure’s address, so the object file is marked as needing an executable stack and the linker prints a warning ("requires executable stack"). The program still runs and prints the output above. To avoid the executable stack on GNU Fortran, compile with -ftrampoline-impl=heap (implemented in libgcc for x86_64 Darwin and for x86_64 and aarch64 Linux, and useful on platforms that do not permit an executable stack), or avoid the trampoline altogether by passing the state as an argument instead:

  • put the data in a derived type and pass it through a class(*) or fixed-type "context" argument to a module procedure;

  • store the data in a module variable (shared state, with all that implies for threads and recursion);

  • use a type-bound procedure with a type(...) object that carries the state (see Object Oriented Programming).

Character dummy arguments and results

A character dummy argument is almost always declared character(len=*) (or character(*)), meaning that the length is assumed from the actual argument at each call (J3/23-007r1, 8.5 and 15.5.2.5). Inside the procedure len(text) returns that length, len_trim the length without trailing blanks, and the dummy argument cannot be larger than what was passed. The rules:

  • A scalar character(len=n) dummy argument of fixed length n may be associated with a longer actual argument and then sees the leftmost n characters, but it must not be longer than the actual argument (15.5.2.5). Assumed length avoids the question altogether.

  • The result of a function can have its length from a dummy argument by a specification expression (character(len=len(text))) or be deferred (character(len=:), allocatable) so that the assignment sets the length.

  • A character dummy argument with intent(out) keeps the caller’s length. Assignment pads a shorter value with blanks and truncates a longer one; a formatted internal write that does not fit is a run-time error ("End of record"), not truncation.

  • An character(*) function result (assumed-length result) is obsolescent (B.3.6) and cannot be recursive; use a deferred-length allocatable result instead.

! Assumed-length character dummy arguments, character results and what len() sees.
module heatflow_text
  implicit none
  private
  public :: banner, shout, fill_label

contains

  ! character(len=*): the length comes from the actual argument at every call.
  function banner(text) result(line)
    character(len=*), intent(in) :: text
    character(len=:), allocatable :: line
    line = repeat('=', len(text) + 4) // new_line('a') // '| ' // text // ' |' // new_line('a') // &
           repeat('=', len(text) + 4)
  end function banner

  ! The result length is a specification expression of a dummy argument.
  function shout(text) result(upper)
    character(len=*), intent(in) :: text
    character(len=len(text)) :: upper
    integer :: i
    do i = 1, len(text)
      if (text(i:i) >= 'a' .and. text(i:i) <= 'z') then
        upper(i:i) = achar(iachar(text(i:i)) - 32)
      else
        upper(i:i) = text(i:i)
      end if
    end do
  end function shout

  ! An assumed-length intent(out) dummy keeps the caller's length. An internal write pads a short text
  ! with blanks and fails with a runtime error when the text does not fit.
  subroutine fill_label(label, nx, ny)
    character(len=*), intent(out) :: label
    integer, intent(in) :: nx, ny
    write (label, '(i0,a,i0)') nx, 'x', ny
  end subroutine fill_label

end module heatflow_text

program character_dummies
  use heatflow_text
  implicit none (type, external)
  character(len=12) :: fixed
  character(len=:), allocatable :: flexible

  print '(a)', banner('heatflow')
  print '(3a)', '[', shout('stable dt'), ']'

  call fill_label(fixed, 64, 48)
  print '(a,i0,3a)', 'fixed: len = ', len(fixed), ', value = "', fixed, '"'
  print '(a,i0)', 'len_trim(fixed) = ', len_trim(fixed)

  flexible = 'abc  '
  print '(a,i0,a,i0)', 'flexible: len = ', len(flexible), ', len_trim = ', len_trim(flexible)
  flexible = 'abcdefgh'
  print '(a,i0)', 'after reassignment, len = ', len(flexible)
end program character_dummies
============
| heatflow |
============
[STABLE DT]
fixed: len = 12, value = "64x48       "
len_trim(fixed) = 5
flexible: len = 5, len_trim = 3
after reassignment, len = 8

Passing a label that is too short to fill_label is non-conforming use (the formatted internal write does not fit) and stops the program at run time:

! INTENTIONALLY NON-CONFORMING USE: the caller's label is too short for the text written into it.
module heatflow_label
  implicit none
  private
  public :: fill_label
contains
  subroutine fill_label(label, nx, ny)
    character(len=*), intent(out) :: label
    integer, intent(in) :: nx, ny
    write (label, '(i0,a,i0)') nx, 'x', ny
  end subroutine fill_label
end module heatflow_label

program label_too_short
  use heatflow_label, only: fill_label
  implicit none (type, external)
  character(len=3) :: tiny
  call fill_label(tiny, 64, 48)
  print '(a)', tiny
end program label_too_short
At line 10 of file 10a_error_label_too_short.f90
Fortran runtime error: End of record

Error termination. Backtrace:
...

The message and the shortened backtrace are gfortran’s; Flang 23 stops with "Excessive output to fixed-size record".

Strings, len_trim, index, trim, deferred-length characters and the Fortran 2023 split and tokenize intrinsics are covered on Characters and Strings.

Procedures as arguments

A dummy argument may itself be a procedure. The caller passes a procedure name; the callee invokes it through the dummy name. Three declaration styles exist, in order of preference:

Style Example Notes

procedure(interface_name) with an abstract interface

procedure(boundary_rule) :: rule

Preferred. The interface is declared once in abstract interface and reused for dummies, pointers and components. Fortran 2003.

procedure(existing_procedure)

procedure(fixed_value) :: rule

Takes the interface from a procedure that is already accessible. Fortran 2003.

Interface body inside the specification part

interface; function rule(x) ... end function; end interface

The Fortran 90 spelling. Equivalent, but longer and without reuse.

external only

external :: rule

Implicit interface. No argument checking; avoid. Required to be given the external attribute by implicit none (external).

Rules for the actual argument that is associated with a dummy procedure (15.5.2.10): it must be an external, internal, module or dummy procedure, a specific intrinsic function listed in the standard’s table of specific names (and declared intrinsic), or a procedure pointer. A nonintrinsic elemental procedure cannot be an actual argument (C1533). The characteristics of the actual procedure must agree with the dummy’s interface, including pure if the dummy is declared pure.

A procedure pointer (declared with procedure(boundary_rule) and the pointer attribute) is the run-time version: it can be associated, nullified, returned from a function, stored in a derived-type component and passed to another procedure. Dummy procedures and procedure pointers are covered in full on Interfaces, Generics and Operator Overloading; the next example is the part that belongs here.

! Dummy procedures: an abstract interface, procedure dummy arguments and a procedure pointer.
module heatflow_boundary_rules
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  private
  public :: boundary_rule, fixed_value, insulated, apply_left_edge, choose_rule

  abstract interface
    ! One rule decides the value of a ghost point from its interior neighbour.
    pure function boundary_rule(interior, setting) result(edge)
      import, only: dp
      real(dp), intent(in) :: interior, setting
      real(dp) :: edge
    end function boundary_rule
  end interface

contains

  pure function fixed_value(interior, setting) result(edge)
    real(dp), intent(in) :: interior, setting
    real(dp) :: edge
    edge = 2.0_dp*setting - interior   ! ghost value: the face between the two points sits at the setting
  end function fixed_value

  pure function insulated(interior, setting) result(edge)
    real(dp), intent(in) :: interior, setting
    real(dp) :: edge
    edge = interior - setting          ! prescribed gradient (flux); setting = 0 copies the neighbour
  end function insulated

  ! A dummy procedure: any procedure with a matching interface can be passed.
  subroutine apply_left_edge(u, rule, setting)
    real(dp), intent(inout) :: u(:, :)
    procedure(boundary_rule) :: rule
    real(dp), intent(in) :: setting
    integer :: j
    do j = 1, size(u, 2)
      u(1, j) = rule(u(2, j), setting)
    end do
  end subroutine apply_left_edge

  ! A procedure pointer chosen at run time.
  function choose_rule(name) result(rule)
    character(len=*), intent(in) :: name
    procedure(boundary_rule), pointer :: rule
    select case (name)
    case ('dirichlet')
      rule => fixed_value
    case default
      rule => insulated
    end select
  end function choose_rule

end module heatflow_boundary_rules

program procedure_arguments
  use, intrinsic :: iso_fortran_env, only: dp => real64
  use heatflow_boundary_rules
  implicit none (type, external)
  real(dp) :: u(3, 2)
  procedure(boundary_rule), pointer :: rule

  u = reshape([0.0_dp, 5.0_dp, 9.0_dp, 0.0_dp, 6.0_dp, 9.0_dp], shape(u))

  call apply_left_edge(u, fixed_value, 100.0_dp)          ! pass a module procedure by name
  print '(a,2f7.1)', 'dirichlet 100: ', u(1, :)

  rule => choose_rule('neumann')
  call apply_left_edge(u, rule, 0.0_dp)                    ! pass the procedure a pointer designates
  print '(a,2f7.1)', 'insulated:     ', u(1, :)

  print '(a,l1)', 'pointer associated: ', associated(rule)
  print '(a,l1)', 'rule is insulated:  ', associated(rule, insulated)
end program procedure_arguments
dirichlet 100:   195.0  194.0
insulated:         5.0    6.0
pointer associated: T
rule is insulated:  T

The example declares one abstract interface, boundary_rule, with two module procedures that satisfy it. apply_left_edge takes whatever rule is passed, and the main program passes fixed_value by name, then the rule that choose_rule selected through a procedure pointer. associated(rule, insulated) tests which target the pointer has.

A callback needs a matching interface

The compiler checks that the actual procedure matches the dummy’s interface only if both are explicit. A callback declared with an implicit interface (the old external style) accepts any procedure, so a mismatch in argument types or counts shows up as a crash or as wrong numbers, a frequent problem in legacy numerical libraries. In new code give every callback an abstract interface, declare it pure if the callee is pure or is called from do concurrent, and pass extra state in a derived type or a module variable rather than in common.

Aliasing and the rules of argument association

Because a dummy argument is normally an alias of the actual argument, two dummy arguments can be aliases of the same memory. The standard forbids the cases that would prevent optimisation (15.5.2.14), in exchange for letting the compiler assume the opposite. While a dummy argument is associated with an entity:

  1. Any action that changes the entity’s value, or its allocation status, must be taken through that dummy argument. The exceptions are a dummy argument with the pointer attribute, and a scalar, assumed-shape or assumed-rank dummy argument that has target but neither intent(in) nor contiguous and whose actual argument is a target (coarray cases aside).

  2. If the value is changed through the dummy argument, the entity must not be referenced by any other path during the call (same exceptions).

  3. If the actual arguments of two dummy arguments overlap and neither dummy is a pointer or a target, the overlapping part cannot be defined or redefined through either of them.

Examples from the standard’s notes: after call sub(a(1:5), a(3:9)) the section a(3:5) cannot be defined through either dummy argument, while a(1:2) and a(6:9) remain definable through their own dummy argument; and if a subroutine init(v) is called as call init(x) and defines v, then x cannot be referenced by its own name during the call, only through v, even when x is a module variable that init also accesses by use association. These restrictions let the compiler keep values in registers across a call and vectorise a loop that writes through one dummy argument and reads through another (J3/23-007r1, 15.5.2.14, Note 4, which names such optimisations as the purpose of the restrictions).

The compiler need not diagnose a violation; many cannot be detected at all. GNU Fortran does catch the simplest case, an identical actual argument for an intent(in) and an intent(out) dummy argument. The next program is non-conforming: it calls an in-place smoothing routine written for two arrays with the same array twice.

! INTENTIONALLY NON-CONFORMING: one variable is the actual argument for an intent(in) and an intent(out) dummy.
module heatflow_alias
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
contains
  subroutine smooth(src, dst)
    real(dp), intent(in) :: src(:)
    real(dp), intent(out) :: dst(:)
    integer :: i
    dst(1) = src(1)
    do i = 2, size(src) - 1
      dst(i) = 0.5_dp*src(i) + 0.25_dp*(src(i - 1) + src(i + 1))
    end do
    dst(size(src)) = src(size(src))
  end subroutine smooth
end module heatflow_alias

program same_actual
  use, intrinsic :: iso_fortran_env, only: dp => real64
  use heatflow_alias
  implicit none (type, external)
  real(dp) :: u(5)
  u = [0.0_dp, 0.0_dp, 8.0_dp, 0.0_dp, 0.0_dp]
  call smooth(u, u)       ! in-place smoothing through two dummy arguments: not allowed
  print '(5f6.2)', u
end program same_actual
12a_error_same_actual.f90:24:14:

   24 |   call smooth(u, u)       ! in-place smoothing through two dummy arguments: not allowed
      |              1
Warning: Same actual argument associated with INTENT(IN) argument 'src' and INTENT(OUT) argument 'dst' at (1)
  0.00  2.00  4.50  1.12  0.00

The warning is the only diagnostic, and the result is wrong: with two separate arrays the smoothing of [0, 0, 8, 0, 0] is the symmetric [0, 2, 4, 2, 0], but with one array the loop reads values it has already overwritten. Flang 23 printed the same wrong numbers with no diagnostic at all. At higher optimisation levels the result could differ again, because the compiler is entitled to assume that src and dst do not overlap. The conforming ways to update in place are an extra array (smooth) or a local copy inside a procedure with one dummy argument (smooth_in_place):

! Aliasing rules: an in-place stencil needs a separate output array or a local copy.
module heatflow_alias
  use, intrinsic :: iso_fortran_env, only: dp => real64
  implicit none
  private
  public :: smooth, smooth_in_place

contains

  ! Conforming: src and dst are different objects, so the compiler may keep src values in registers.
  subroutine smooth(src, dst)
    real(dp), intent(in) :: src(:)
    real(dp), intent(out) :: dst(:)
    integer :: i
    dst(1) = src(1)
    do i = 2, size(src) - 1
      dst(i) = 0.5_dp*src(i) + 0.25_dp*(src(i - 1) + src(i + 1))
    end do
    dst(size(src)) = src(size(src))
  end subroutine smooth

  ! The in-place version uses one dummy argument and a local copy.
  subroutine smooth_in_place(u)
    real(dp), intent(inout) :: u(:)
    real(dp), allocatable :: old(:)
    old = u
    call smooth(old, u)
  end subroutine smooth_in_place

end module heatflow_alias

program aliasing
  use, intrinsic :: iso_fortran_env, only: dp => real64
  use heatflow_alias
  implicit none (type, external)
  real(dp) :: u(5), v(5)

  u = [0.0_dp, 0.0_dp, 8.0_dp, 0.0_dp, 0.0_dp]
  call smooth(u, v)
  print '(a,5f6.2)', 'smooth (two arrays):  ', v
  call smooth_in_place(u)
  print '(a,5f6.2)', 'smooth_in_place:      ', u
end program aliasing
smooth (two arrays):    0.00  2.00  4.00  2.00  0.00
smooth_in_place:        0.00  2.00  4.00  2.00  0.00

The target and pointer attributes are the sanctioned exceptions: they tell the compiler that aliasing is possible, at the cost of some optimisation. See Pointers and Targets.

Function references, side effects and order of evaluation

Rules that matter when functions are used in expressions (J3/23-007r1, 10.1.7 and Annex C.6.1):

  • A processor need not evaluate all operands of an expression if the value can be determined otherwise, which is why a logical expression such as x > y .or. l(z) may skip the call to l. If the skipped function would have defined an argument, that argument becomes undefined after the expression.

  • If a statement contains more than one function reference, they may run in any order, and their values must not depend on that order. A function with side effects therefore must not be used twice in one expression.

  • A function reference that is not pure may not be used where purity is required: do concurrent, a specification expression, or another pure procedure.

  • Prefer pure functions for computation and subroutines for effects. Use intent(out) for results of a subroutine rather than side effects on module variables. Avoid hidden state: a function that increments a counter in a module works until someone writes f(x) + f(y).

Compiler support and diagnostics

The following was verified on 2026-10-18 with the examples above; the "not verified" cells mean that no compiler of that family was available for this page.

Feature GNU Fortran 16.2 LLVM Flang 23 Recursion by default (F2018)

No: recursive is required for self-calls; -fcheck=recursion flags re-entry into a procedure without it.

Yes.

non_recursive (F2018)

Rejected (syntax error).

Accepted and checked.

import, only, import, none, import, all (F2018)

Accepted.

Accepted.

implicit none (type, external) (F2018)

Accepted; an undeclared external procedure is an error.

Accepted; an undeclared external procedure is an error.

impure elemental (F2008)

Accepted.

Accepted.

simple (F2023)

Rejected (syntax error).

Accepted, but its constraints are not checked.

Conditional expressions and arguments, .nil. (F2023)

Rejected (syntax error).

Rejected ("not yet implemented").

The other compilers (Intel ifx, NAG, HPE Cray, NVIDIA HPC SDK) are not covered by this table because none was available for this page; the compiler-support table on History, Standards and Compilers collects their published status.

GNU Fortran options that matter for procedures (see the manual for the full text):

Option Effect

-fimplicit-none

Same as implicit none (type) in every scoping unit: rejects implicitly typed names. It does not check external procedures; implicit none (external) in the source does.

-Wall (includes -Wunused-dummy-argument and -Wexternal-argument-mismatch)

Reports unused dummy arguments and mismatching dummy procedures.

-Wimplicit-interface

Warns at every call that uses an implicit interface. Not part of -Wall.

-Wimplicit-procedure

Warns about a call to a procedure with neither an explicit interface nor an external declaration.

-Warray-temporaries

Compile-time report of every array temporary created to pass an argument.

-fcheck=array-temps

Run-time warning, once per call site, when a temporary was created. Part of -fcheck=all.

-fcheck=recursion

Run-time error when a procedure not marked recursive is re-entered.

-fcheck=bounds

Checks array subscripts, including those of dummy arguments and of conforming-array operations.

-frecursive

Allows indirect recursion by putting all local arrays on the stack; incompatible with -fmax-stack-var-size= and -fno-automatic.

-fmax-stack-var-size=n

Largest local array kept on the stack in a non-recursive procedure; larger ones are static.

-fno-automatic

Treats every local variable of a procedure not marked recursive as having the save attribute.

-fallow-argument-mismatch

Downgrades argument type/rank mismatches in external procedure calls from errors to warnings. Strongly discouraged by the manual.

-ftrampoline-impl=heap (GCC option)

Allocates trampolines for internal procedures passed as arguments on the heap instead of the stack.

-std=f2023

Accepts the Fortran 2023 language and rejects GNU extensions.

Standard-version provenance

Feature Standard Notes

External procedures, function and subroutine subprograms, return, entry

FORTRAN 66 and 77

entry is now obsolescent; return remains.

Module procedures, internal procedures, contains, interface, intent, optional, keyword arguments, recursive and result, array-valued results

Fortran 90

The basis of modern procedure design.

pure, elemental

Fortran 95

Allocatable and pointer dummy arguments, allocatable function results, value, import, procedure pointers, abstract interface, procedure statements

Fortran 2003

impure elemental, internal procedures as actual arguments and procedure targets, contiguous

Fortran 2008

non_recursive, recursion by default, import, only/none/all, implicit none (external), assumed-rank dummy arguments, error stop in pure procedures

Fortran 2018

simple, conditional expressions and arguments with .nil.

Fortran 2023

Not widely implemented in 2026.

Templates and generic procedures

Fortran 2028 (planned)

Described on What’s New and Fortran 2028 Preview.

Statement functions, entry, alternate return, assumed-length character functions, specific names for intrinsics

Obsolescent

Annex B.3 of J3/23-007r1.

Legacy procedure features you may meet

Old code still contains forms that new code should not copy. The table lists what each looks like, its status in the standard, and the replacement. They are described in more detail on Legacy Features and Modernization.

Legacy form Status Modern replacement

entry statements

Obsolescent (B.3.9)

Separate procedures that share a private helper in a module.

Statement functions, f(x) = x*x

Obsolescent (B.3.4)

An internal function.

Alternate return, call sub(*10) and return 1

Obsolescent (B.3.2)

A status argument (integer, intent(out) :: stat) or an optional error flag.

Assumed-length character function results, character(*) function f(x)

Obsolescent (B.3.6)

character(len=:), allocatable result.

Specific intrinsic names such as dsqrt, float

Obsolescent (B.3.12)

Generic names with kind parameters.

external with an implicit interface

Not obsolescent, but error-prone

Module procedures, or an interface body.

common blocks as a way to pass arguments

Obsolescent (B.3.11)

Module variables or arguments.

Where the books are outdated

Modern Fortran Explained (Metcalf, Reid, Cohen and Bader, 3rd edition, Fortran 2023) covers procedures in its chapter on program units and procedures, with the Fortran 2023 additions in its closing chapter. It remains an accurate reference to the language, and the following points are where a reader has to look beyond it:

  • Compiler status. The book describes non_recursive, recursion by default, simple procedures and conditional arguments as part of the language but gives no information on which compilers implement them. As the compiler table above shows, GNU Fortran 16.2 still lacks all three groups and Flang 23 implements only recursion by default and non_recursive. Programs that must build on several compilers still write recursive and use if constructs instead of .nil..

  • Appendix A. The book’s list of deprecated features includes implicit interfaces. That is the authors' recommendation, not the standard’s: a procedure with an implicit interface is still conforming. The enforceable modern tool is implicit none (type, external) together with modules, as used throughout this page.

  • Build practice. The book says nothing about compiler diagnostics (-Wimplicit-interface, -fcheck=array-temps, -fcheck=recursion), about trampolines for internal procedures passed as arguments, or about module build order. They are covered here and on Compilers and Compiler Options.

The MathWorks MATLAB C/C++, Fortran, Java and Python API Reference (R2023a) shows Fortran MEX gateways written in fixed form with integer*4 and real*8 declarations and hand-written type statements instead of interfaces, so every call into the MATLAB libraries has an implicit interface. Modern code should write free-form source with iso_c_binding or iso_fortran_env kinds and explicit interfaces, as described on MATLAB: MEX, Engine and MAT-File APIs.

Common mistakes at a glance

Mistake Symptom Fix

Calling an external procedure with an implicit interface and the wrong argument types

No diagnostic; wrong values or crashes at run time.

Put the callee in a module or write an interface body; use implicit none (type, external) and -Wimplicit-interface.

No intent

Accidental writes are not reported; weaker optimisation.

Give every dummy argument an intent.

Referencing an absent optional argument, directly or inside merge or .and.

Segmentation fault or garbage.

Use present in a nested if and copy to a local variable first.

Passing a strided section to an explicit-shape dummy

Run-time warning of an array temporary, and a slower program.

Use assumed-shape dummy arguments or add contiguous.

Recursion without recursive on gfortran

Compile-time error or run-time "Recursive call to nonrecursive procedure".

Write recursive and result.

Side effects in a pure or elemental procedure

Constraint violation error.

Use impure elemental, or move the effect to a subroutine.

Passing the same array to two dummy arguments

Wrong results that depend on the optimisation level.

Use a separate output array or a local copy.

Forgetting that intent(out) resets the dummy argument

Lost values or deallocated arrays.

Use intent(inout) when the old value matters.

Using an internal procedure after its host has returned

Undefined behaviour.

Pass state in an argument instead of through a procedure pointer to an internal procedure.