Derived-Type and Asynchronous I/O

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/23-007r1) and was checked on 2026-10-18. Every example was compiled with gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all (GNU Fortran 16.2) and run, and every example was also built and run with LLVM Flang 23; the output shown is real output, and where the two compilers print something different, or one of them rejects the program, the page says so and shows both. Examples that are deliberately wrong are labelled as such and show the real diagnostics. Defined input/output is specified in 12.6.4.8 (procedures, execution, resolution), 13.7.6 (the DT edit descriptor), 15.4.3.2 and 15.4.3.4.4 (the generic interface block form) and 7.5.5 (the generic type-bound form). Asynchronous input/output is specified in 12.5.6.5 (open), 12.6.2.5 and 12.6.2.9 (data transfer statements), 12.7 (waiting), 12.10.2.22 (inquire with pending=) and 8.5.4 and 8.6.3 (the asynchronous attribute and statement). Recursive input/output is 12.12, and the encoding of Unicode files is 12.5.6.9.

Two features that are rare in tutorials share this page because both change what a data transfer statement means. With defined input/output the program, not the library, decides how an object of derived type is written or read: a statement such as print *, grid calls a procedure of the type. With asynchronous input/output a data transfer statement may return before the transfer is finished, and a later wait completes it, so computation can overlap with transfers. The examples reuse the pieces of the running example, heatflow: the grid_t of heatflow_grid defines write(formatted) for itself, and snapshots of the temperature field are written asynchronously while the next sweep is computed. Ordinary input/output is covered in Input/Output: Units and Files, Format Specifications and Edit Descriptors and Internal Files, Namelist and Non-Advancing I/O; derived types and their type-bound procedures in Derived Types and Type-Bound Procedures, Abstract Types and Finalization.

Defined input/output at a glance

A defined input/output procedure is a subroutine with one of four fixed interfaces, connected to a derived type either by a generic type-bound procedure (generic :: write(formatted) => name) or by an interface block (interface write(formatted)). The program need not supply all four (12.6.4.8.2).

Generic specification Used when Dummy arguments after dtv

read(formatted)

a formatted, list-directed or namelist read reaches an item of the type

unit, iotype, v_list, iostat, iomsg

write(formatted)

a formatted, list-directed or namelist write reaches an item of the type

unit, iotype, v_list, iostat, iomsg

read(unformatted)

an unformatted read reaches an item of the type

unit, iostat, iomsg

write(unformatted)

an unformatted write reaches an item of the type

unit, iostat, iomsg

The dummy arguments always have these characteristics, with arbitrary names (the names used here are the standard’s):

Argument Declaration Meaning

dtv

class(t), intent(inout) for reading and class(t), intent(in) for writing

the object being transferred (the "derived-type variable")

unit

integer, intent(in)

the unit to use in the child data transfer statements

iotype

character(len=*), intent(in)

'LISTDIRECTED', 'NAMELIST', or 'DT' followed by the optional character literal of the edit descriptor (formatted only)

v_list

integer, intent(in) :: v_list(:)

the integers in parentheses after DT, zero-sized if there are none (formatted only)

iostat

integer, intent(out)

zero on success, positive on error, iostat_end or iostat_eor for end-of-file and end-of-record on input

iomsg

character(len=*), intent(inout)

an explanatory message when iostat is not zero, otherwise left unchanged

The procedure is selected, when an item of derived type is reached in a data transfer, if both of these are true (12.6.4.8.4): the transfer was initiated by a list-directed, namelist or unformatted statement, or an explicit format is given and the edit descriptor for that item is DT; and a suitable generic type-bound procedure or generic interface exists. For the type-bound form the binding is looked up in the declared type and the one in the dynamic type is selected, so an extension can override it. An explicit format with an ordinary edit descriptor (f8.2) for the type is an error, and a DT descriptor for a type that has no suitable procedure is also an error (13.7.6). The facility is independent of the compiler: both gfortran and Flang implement it.

Statement Which procedure runs

write (u, '(dt)') g

write(formatted), with iotype = 'DT'

write (u, '(dt"table"(7, 1))') g

write(formatted), with iotype = 'DTtable' and v_list = [7, 1]

write (u, *) g or print *, g

write(formatted), with iotype = 'LISTDIRECTED'

write (u, nml=run) with g in the group

write(formatted), with iotype = 'NAMELIST'

write (u) g

write(unformatted)

read (u, *) g, read (u, nml=run), read (u, '(dt)') g

read(formatted) with the respective iotype

read (u) g

read(unformatted)

Provenance: asynchronous input/output is listed by the LLVM Flang documentation among the Fortran 2003 features (see the status page cited in the references), and gfortran’s manual states that it implements the Fortran 2003 standard with the exception of finalization. This page does not assign an edition to anything else.

A formatted writer for grid_t

The first example is the one heatflow_grid uses. The type-bound generic makes every output statement that reaches a grid_t call grid_write_formatted. The procedure chooses its behaviour from iotype and v_list: the plain DT descriptor and list-directed output give a one-line summary (the first v value is the number of decimals), and DT"table" with two v values prints the whole field as a table, a field width and a number of decimals.

module heatflow_kinds
  use, intrinsic :: iso_fortran_env, only: real64, int32
  implicit none
  integer, parameter :: dp = real64, ip = int32
end module heatflow_kinds

module heatflow_grid
  use heatflow_kinds, only: dp, ip
  implicit none
  private
  public :: grid_t, make_grid

  type :: grid_t
    integer(ip) :: nx = 0_ip, ny = 0_ip
    real(dp), allocatable :: u(:, :)
  contains
    generic :: write(formatted) => write_formatted
    procedure, private :: write_formatted => grid_write_formatted
  end type grid_t

contains

  function make_grid(nx, ny, hot) result(g)
    integer(ip), intent(in) :: nx, ny
    real(dp), intent(in) :: hot
    type(grid_t) :: g
    g%nx = nx
    g%ny = ny
    allocate (g%u(nx, ny), source=0.0_dp)
    g%u(nx/2 + 1, ny/2 + 1) = hot
  end function make_grid

  subroutine grid_write_formatted(dtv, unit, iotype, v_list, iostat, iomsg)
    class(grid_t), intent(in) :: dtv
    integer, intent(in) :: unit
    character(len=*), intent(in) :: iotype
    integer, intent(in) :: v_list(:)
    integer, intent(out) :: iostat
    character(len=*), intent(inout) :: iomsg
    character(len=32) :: fmt
    integer :: decimals

    if (iotype == 'DTtable') then          ! DT'table'(w, d): the whole array as rows
      decimals = 2
      if (size(v_list) >= 2) decimals = v_list(2)
      write (fmt, '(a,i0,a,i0,a,i0,a)') '(', dtv%nx, '(f', v_list(1), '.', decimals, '))'
      block
        integer :: j
        do j = 1, dtv%ny
          write (unit, fmt, iostat=iostat, iomsg=iomsg) dtv%u(:, j)
          if (iostat /= 0) return
          if (j < dtv%ny) write (unit, '(a)', iostat=iostat, iomsg=iomsg) new_line('a')
          if (iostat /= 0) return
        end do
      end block
    else                                    ! 'LISTDIRECTED', 'NAMELIST' or plain 'DT'
      decimals = 3
      if (size(v_list) >= 1) decimals = v_list(1)
      write (fmt, '(a,i0,a)') '(a,i0,a,i0,a,f0.', decimals, ',a)'
      write (unit, fmt, iostat=iostat, iomsg=iomsg) &
        'grid_t(', dtv%nx, 'x', dtv%ny, ' max=', maxval(dtv%u), ')'
    end if
  end subroutine grid_write_formatted
end module heatflow_grid

program dtio_write
  use heatflow_kinds, only: ip, dp
  use heatflow_grid, only: grid_t, make_grid
  implicit none
  type(grid_t) :: g
  character(len=80) :: line
  integer :: ios
  character(len=100) :: msg

  g = make_grid(4_ip, 3_ip, 98.6_dp)

  print '(dt)', g                             ! plain DT, no v-list: default 3 decimals
  print '(dt(1))', g                          ! v_list = [1]
  print *, g                                  ! list-directed: iotype = 'LISTDIRECTED'
  write (*, '(a, dt(2), a)') 'before ', g, ' after'   ! mixed with other items
  print '(dt"table"(7, 1))', g                ! iotype = 'DTtable', v_list = [7, 1]

  write (line, '(dt)', iostat=ios, iomsg=msg) g        ! internal unit: unit argument is negative
  print '(a, i0, 2a)', 'iostat=', ios, ' line=', trim(line)
end program dtio_write

Output of gfortran:

grid_t(4x3 max=98.600)
grid_t(4x3 max=98.6)
 grid_t(4x3 max=98.600)
before grid_t(4x3 max=98.60) after
    0.0    0.0    0.0    0.0
    0.0    0.0   98.6    0.0
    0.0    0.0    0.0    0.0
iostat=0 line=grid_t(4x3 max=98.600)

LLVM Flang 23 prints the same lines with one difference: for the list-directed print *, g it does not write the leading blank that gfortran adds before a list-directed record, so the third line starts at the left margin. That blank belongs to the list-directed record, not to the defined output procedure (the standard leaves the spacing of list-directed output processor dependent, 13.10.4), so programs must not rely on it. Things to notice in the program:

  • dtv is class(grid_t) with intent(in) because the type is extensible, and write procedures must not modify the object. A read procedure has intent(inout).

  • The default print '(dt)' has an empty v_list. The procedure tests size(v_list) before using v_list(1).

  • The unit passed in is not necessarily 6 or a positive number. For the internal file in the last statement it is a negative, processor-dependent value; see The unit argument.

  • A newline is written by the procedure. The table is written with one child write per row and new_line('a') between them, because a '(a)' write inside a defined procedure does not start a new record by itself (Child data transfer statements). A slash edit descriptor inside the child format is the usual alternative.

  • write (fmt, ...) builds a format. That is an internal write whose format depends on v_list; internal files are allowed inside a defined procedure (Restrictions).

iotype and v_list

The DT edit descriptor has the form DT [ char-literal-constant ] [ ( v-list ) ] (13.3.2, R1307). The string is not a format: it is passed to the procedure so that one type can have several output styles, and the integers (R1312, v is a signed-int-literal-constant) are passed so that field widths and similar tuning can be chosen at the call. This program only reports what it receives.

module probe_mod
  implicit none
  private
  public :: probe_t

  type :: probe_t
    integer :: id = 0
  contains
    procedure, private :: show => probe_write
    generic :: write(formatted) => show
  end type probe_t

contains

  subroutine probe_write(dtv, unit, iotype, v_list, iostat, iomsg)
    class(probe_t), intent(in) :: dtv
    integer, intent(in) :: unit
    character(len=*), intent(in) :: iotype
    integer, intent(in) :: v_list(:)
    integer, intent(out) :: iostat
    character(len=*), intent(inout) :: iomsg
    write (unit, '(a, i0, 3a, i0, a, *(i0, 1x))', iostat=iostat, iomsg=iomsg) &
      'id=', dtv%id, ' iotype="', iotype, '" size(v_list)=', size(v_list), ' v_list=', v_list
  end subroutine probe_write
end module probe_mod

program trace_arguments
  use probe_mod, only: probe_t
  implicit none
  type(probe_t) :: p
  character(len=40) :: fmt
  integer :: n
  namelist /run/ p

  p%id = 7
  print '(dt)', p
  print '(dt(10, 4, 2))', p
  print '(dt"Link List"(10, 4, 2))', p
  print '(dt"")', p
  print *, p
  fmt = '(dt"x"(3, 5))'                     ! the v values reach v_list
  print fmt, p
  print '(2(dt(1)), a)', p, p, '|'          ! one descriptor per effective item
  print '(dt)', [probe_t(1), probe_t(2)]    ! an array: one call per element
  write (*, nml=run)
  n = 0
end program trace_arguments

Output of gfortran:

id=7 iotype="DT" size(v_list)=0 v_list=
id=7 iotype="DT" size(v_list)=3 v_list=10 4 2
id=7 iotype="DTLink List" size(v_list)=3 v_list=10 4 2
id=7 iotype="DT" size(v_list)=0 v_list=
 id=7 iotype="LISTDIRECTED" size(v_list)=0 v_list=
id=7 iotype="DTx" size(v_list)=2 v_list=3 5
id=7 iotype="DT" size(v_list)=1 v_list=1 id=7 iotype="DT" size(v_list)=1 v_list=1 |
id=1 iotype="DT" size(v_list)=0 v_list=
id=2 iotype="DT" size(v_list)=0 v_list=
&RUN
 P=id=7 iotype="NAMELIST" size(v_list)=0 v_list=
 /

The reported differences with Flang are only in the layout of list-directed and namelist output: Flang does not write the leading blank of the list-directed record and writes the namelist group on a single line, &RUN P=id=7 iotype="NAMELIST" size(v_list)=0 v_list=/, where gfortran puts &RUN, the item and the closing slash on three lines. Those are processor-dependent layouts. What is fixed by the standard, and identical in both compilers:

  • iotype is "DT" for a bare DT, "DT" followed by the literal for DT"literal", "LISTDIRECTED" for list-directed transfers and "NAMELIST" for namelist transfers. The note in 13.7.6 gives DT'Link List'(10, 4, 2) as passing "DTLink List" and [10, 4, 2].

  • v_list has the same number of elements as the v-list, in the same order, and is zero-sized when there is no v-list, and for list-directed and namelist transfers.

  • An array is processed one element at a time and each element calls the procedure: [probe_t(1), probe_t(2)] gives two calls (the last two lines before the namelist group), and 2(dt(1)) with p, p gives two as well.

  • The character literal of the descriptor is a character constant of default kind (C1309); the v values are default integers.

A DT edit descriptor corresponds to one effective item; it consumes one item of the output list like any data edit descriptor and, if the list is exhausted, ends format processing (dt(1) repeated with 2(dt(1)) is correct for two items). It cannot correspond to an item that is not of derived type (13.7.6), so print '(dt)', 1 is an error.

Zero and negative v values in gfortran

The standard allows any signed integer constant as a v value, including zero and negative ones (R1312), and a procedure may interpret them as it likes. gfortran 16.2 rejects them: a literal format is diagnosed at compile time, and a format built at run time fails when the statement executes. This program is deliberately written to show it:

module probe_mod
  implicit none
  type :: probe_t
    integer :: id = 0
  contains
    procedure, private :: show => probe_write
    generic :: write(formatted) => show
  end type probe_t
contains
  subroutine probe_write(dtv, unit, iotype, v_list, iostat, iomsg)
    class(probe_t), intent(in) :: dtv
    integer, intent(in) :: unit
    character(len=*), intent(in) :: iotype
    integer, intent(in) :: v_list(:)
    integer, intent(out) :: iostat
    character(len=*), intent(inout) :: iomsg
    write (unit, '(a, i0, a, *(i0, 1x))', iostat=iostat, iomsg=iomsg) &
      'id=', dtv%id, ' v_list=', v_list
  end subroutine probe_write
end module probe_mod

program negative_v
  use probe_mod, only: probe_t
  implicit none
  type(probe_t) :: p
  p%id = 7
  print '(dt(-3, 0))', p
end program negative_v

gfortran 16.2 refuses to compile it:

02b_negative_v.f90:27:14:

   27 |   print '(dt(-3, 0))', p
      |              1
Error: Positive width required in format string at (1)

LLVM Flang 23 accepts it and the procedure receives v_list = [-3, 0]:

id=7 v_list=-3 0

A format held in a variable (fmt = '(dt"x"(3, 0))') is reported by gfortran at run time as Positive integer required in format. Until that is fixed, treat the values in v_list as positive widths and counts if the code has to build with gfortran, and use iotype strings for anything else.

Reading with a defined procedure

A read procedure receives dtv with intent(inout) and unit, and reads with child data transfer statements on that unit (see Child data transfer statements). Its job is to report problems through iostat and iomsg: it assigns a positive value to iostat for an error, iostat_end for end-of-file, iostat_eor for end-of-record, and zero otherwise, and sets a message when iostat is not zero (12.6.4.8.3). The procedure below reads a heat source written as three list-directed values, ix iy temperature, validates them, and commits to dtv only if everything was read; a failure in the middle leaves the object untouched. The parent read statements show the four ways the procedure’s errors are seen by the caller.

module hotspot_mod
  use, intrinsic :: iso_fortran_env, only: real64, iostat_end
  implicit none
  private
  public :: hotspot_t
  integer, parameter :: dp = real64

  ! A heat source given as "ix iy temperature" (three values on the current record).
  type :: hotspot_t
    integer :: ix = 0, iy = 0
    real(dp) :: temperature = 0.0_dp
  contains
    generic :: read(formatted) => read_formatted
    generic :: write(formatted) => write_formatted
    procedure, private :: read_formatted => hotspot_read
    procedure, private :: write_formatted => hotspot_write
  end type hotspot_t

contains

  subroutine hotspot_read(dtv, unit, iotype, v_list, iostat, iomsg)
    class(hotspot_t), intent(inout) :: dtv
    integer, intent(in) :: unit
    character(len=*), intent(in) :: iotype
    integer, intent(in) :: v_list(:)
    integer, intent(out) :: iostat
    character(len=*), intent(inout) :: iomsg
    integer :: ix, iy
    real(dp) :: t

    if (iotype /= 'LISTDIRECTED' .or. size(v_list) > 0) then
      iostat = 1
      iomsg = 'hotspot_t can only be read with list-directed input'
      return
    end if
    read (unit, *, iostat=iostat, iomsg=iomsg) ix, iy, t   ! child read: continues at the parent position
    if (iostat /= 0) return                                  ! pass iostat_end and errors up unchanged
    if (ix < 1 .or. iy < 1) then
      iostat = 2
      iomsg = 'hotspot indices must be positive'
      return
    end if
    dtv%ix = ix                                              ! dtv is class(...): assign components, not the whole object
    dtv%iy = iy
    dtv%temperature = t
  end subroutine hotspot_read

  subroutine hotspot_write(dtv, unit, iotype, v_list, iostat, iomsg)
    class(hotspot_t), intent(in) :: dtv
    integer, intent(in) :: unit
    character(len=*), intent(in) :: iotype
    integer, intent(in) :: v_list(:)
    integer, intent(out) :: iostat
    character(len=*), intent(inout) :: iomsg
    character(len=24) :: fmt
    integer :: decimals
    decimals = 2
    if (size(v_list) > 0) decimals = v_list(1)             ! DT(n): n decimals
    write (fmt, '(a, i0, a)') '(2(i0, 1x), f0.', decimals, ')'
    if (iotype == 'LISTDIRECTED') fmt = '(2(i0, 1x), f0.2)'
    write (unit, fmt, iostat=iostat, iomsg=iomsg) dtv%ix, dtv%iy, dtv%temperature
  end subroutine hotspot_write
end module hotspot_mod

program dtio_read
  use hotspot_mod, only: hotspot_t
  implicit none
  type(hotspot_t) :: a, b
  character(len=64) :: msg
  character(len=24) :: text(4)
  integer :: ios, k


  text = [character(len=24) :: '3 4 98.6', '5 6 20.0, 7 8 30.5', '0 1 5.0', '2 x 1.0']

  read (text(1), *) a
  print '(a, dt(4))', 'one:   ', a

  read (text(2), *) a, b                         ! two items in one record, list-directed separators
  print '(a, dt, a, dt)', 'two:   ', a, ' and ', b

  do k = 3, 4
    read (text(k), *, iostat=ios, iomsg=msg) a
    print '(a, i0, a, i0, 2a)', 'record ', k, ': iostat=', ios, ' iomsg=', trim(msg)
  end do

  read (text(1), '(dt)', iostat=ios, iomsg=msg) a   ! formatted read: our procedure refuses
  print '(a, i0, 2a)', 'DT read: iostat=', ios, ' iomsg=', trim(msg)

end program dtio_read

Output of gfortran:

one:   3 4 98.6000
two:   5 6 20.00 and 7 8 30.50
record 3: iostat=2 iomsg=hotspot indices must be positive
record 4: iostat=5010 iomsg=Bad integer for item 2 in list input
DT read: iostat=1 iomsg=hotspot_t can only be read with list-directed input

Flang prints the same lines except for record 4, where the child read’s message comes from the Flang runtime: record 4: iostat=1001 iomsg=Bad character 'x' in INTEGER input field. The value of iostat and the text of iomsg for a failed conversion are processor dependent; the ones produced by the procedure itself (iostat=2, iostat=1) are the program’s. Observations:

  • The child read passes iostat through. A conversion error, the end of the file or the end of a record in the child read set iostat to the processor’s value, and the procedure returns it unchanged (if (iostat /= 0) return). Dropping that test and continuing is the classic bug: the later statements then run on undefined data.

  • dtv is polymorphic. Whole-object assignment to a class(hotspot_t) dummy is not allowed (both compilers reject dtv = hotspot_t(ix, iy, t): "Nonallocatable variable must not be polymorphic in intrinsic assignment" in gfortran), so the components are assigned one by one. A type with many components can use a non-polymorphic helper with a type(...) dummy.

  • One record can hold several objects. read (text(2), *) a, b consumes 5 6 20.0, 7 8 30.5 with two calls, because the child read stops after the third value and the next call starts where it left off.

  • The iotype check is optional but worth having. The explicit-format read in the last statement is refused with the procedure’s own message, which iomsg= returns.

An iostat from a defined procedure is delivered to the parent statement. If the parent has no iostat=, err= or end=, the program terminates and the processor makes the iomsg available in some way (12.6.4.8.3); both compilers print it:

module fail_mod
  implicit none
  type :: fussy_t
    integer :: id = 1
  contains
    procedure, private :: wf
    generic :: write(formatted) => wf
  end type fussy_t
contains
  subroutine wf(dtv, unit, iotype, v_list, iostat, iomsg)
    class(fussy_t), intent(in) :: dtv
    integer, intent(in) :: unit
    character(len=*), intent(in) :: iotype
    integer, intent(in) :: v_list(:)
    integer, intent(out) :: iostat
    character(len=*), intent(inout) :: iomsg
    if (size(v_list) /= 1) then
      iostat = 17
      iomsg = 'fussy_t needs exactly one v value: DT(width)'
      return
    end if
    write (unit, '(i0)', iostat=iostat, iomsg=iomsg) dtv%id
    if (.false.) print *, iotype
  end subroutine wf
end module fail_mod

program unhandled
  use fail_mod
  implicit none
  type(fussy_t) :: f
  integer :: ios
  character(len=80) :: msg
  write (*, '(dt(1))') f
  write (*, '(dt)', iostat=ios, iomsg=msg) f
  print '(a, i0, 2a)', 'handled: iostat=', ios, ' iomsg=', trim(msg)
  write (*, '(dt)') f                    ! no iostat=: the program terminates
  print '(a)', 'not reached'
end program unhandled

gfortran prints the following, with the message in the runtime error (the first statement prints 1; the second fails inside the procedure, which wrote nothing, so its record is empty):

1

handled: iostat=17 iomsg=fussy_t needs exactly one v value: DT(width)
At line 36 of file 17_unhandled_iostat.f90 (unit = 6, file = 'stdout')
Fortran runtime error: fussy_t needs exactly one v value: DT(width)

Error termination. Backtrace:

Flang terminates with its own wording, fatal Fortran runtime error(/work/17_unhandled_iostat.f90:36): fussy_t needs exactly one v value: DT(width) and Aborted, which shows that the message is carried through in both compilers.

The unit argument

The unit argument is the unit on which the child statements must write or read. The standard says what it holds in which situation (12.6.4.8.3): the unit number if the parent uses a unit number, output_unit for print and an asterisk write, input_unit for an asterisk read, and otherwise, for an internal file, a processor-dependent negative value. Note 3 adds that it is also negative when the parent used an external unit connected with newunit=.

module unit_probe
  use, intrinsic :: iso_fortran_env, only: output_unit, error_unit
  implicit none
  type :: probe_t
    integer :: id = 1
  contains
    procedure, private :: wf
    generic :: write(formatted) => wf
  end type probe_t
contains
  subroutine wf(dtv, unit, iotype, v_list, iostat, iomsg)
    class(probe_t), intent(in) :: dtv
    integer, intent(in) :: unit
    character(len=*), intent(in) :: iotype
    integer, intent(in) :: v_list(:)
    integer, intent(out) :: iostat
    character(len=*), intent(inout) :: iomsg
    character(len=12) :: kind_of_unit
    if (unit == output_unit) then
      kind_of_unit = 'output_unit'
    else if (unit < 0) then
      kind_of_unit = 'negative'
    else
      kind_of_unit = 'other'
    end if
    write (unit, '(a, i0, 2a)', iostat=iostat, iomsg=iomsg) 'probe ', dtv%id, ' on unit: ', trim(kind_of_unit)
    if (iotype == 'DT' .and. size(v_list) < 0) iostat = 1
  end subroutine wf
end module unit_probe

program unit_argument
  use unit_probe
  implicit none
  type(probe_t) :: p
  character(len=40) :: s
  integer :: u

  print '(dt)', p                                  ! PRINT: output_unit
  write (*, '(dt)') p                              ! asterisk unit: output_unit
  write (s, '(dt)') p                              ! internal file: negative
  print '(a)', trim(s)
  open (newunit=u, status='scratch')               ! newunit values are negative too
  write (u, '(dt)') p
  rewind (u)
  read (u, '(a)') s
  print '(a)', trim(s)
  close (u)
  open (7, status='scratch')                       ! a positive unit number
  write (7, '(dt)') p
  rewind (7)
  read (7, '(a)') s
  print '(a)', trim(s)
end program unit_argument
probe 1 on unit: output_unit
probe 1 on unit: output_unit
probe 1 on unit: negative
probe 1 on unit: negative
probe 1 on unit: other

Flang prints the same five lines. The practical rules follow from this: always write to the unit that was passed, never to a hard-coded unit, and never test unit > 0 to detect "a file": inquire (unit=unit, ...) on a negative internal unit gives an error with iostat_inquire_internal_unit, and newunit files are also negative.

Interface blocks, unformatted procedures and TYPE versus CLASS

The generic type-bound form needs access to the type definition. When the type cannot be modified, for instance because it comes from a library, the same procedures can be attached with an interface block, which is the form 15.4.3.4.4 describes: interface write(unformatted) followed by module procedure or an interface body. The unformatted procedures have no iotype or v_list, and they decide the layout in the file. The program below writes only the step and a single-precision residual, to produce a compact record, and reads them back.

module sample_mod
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  integer, parameter :: dp = real64

  ! A type we pretend we cannot modify: no bindings, so DTIO comes from an interface block.
  type :: sample_t
    integer :: step = 0
    real(dp) :: residual = 0.0_dp
  end type sample_t

  interface write(unformatted)
    module procedure sample_write_u
  end interface
  interface read(unformatted)
    module procedure sample_read_u
  end interface
  interface write(formatted)
    module procedure sample_write_f
  end interface

contains

  subroutine sample_write_u(dtv, unit, iostat, iomsg)
    class(sample_t), intent(in) :: dtv
    integer, intent(in) :: unit
    integer, intent(out) :: iostat
    character(len=*), intent(inout) :: iomsg
    write (unit, iostat=iostat, iomsg=iomsg) dtv%step     ! only the step and a single-precision residual
    if (iostat /= 0) return
    write (unit, iostat=iostat, iomsg=iomsg) real(dtv%residual)
  end subroutine sample_write_u

  subroutine sample_read_u(dtv, unit, iostat, iomsg)
    class(sample_t), intent(inout) :: dtv
    integer, intent(in) :: unit
    integer, intent(out) :: iostat
    character(len=*), intent(inout) :: iomsg
    real :: r
    read (unit, iostat=iostat, iomsg=iomsg) dtv%step
    if (iostat /= 0) return
    read (unit, iostat=iostat, iomsg=iomsg) r
    if (iostat /= 0) return
    dtv%residual = real(r, dp)
  end subroutine sample_read_u

  subroutine sample_write_f(dtv, unit, iotype, v_list, iostat, iomsg)
    class(sample_t), intent(in) :: dtv
    integer, intent(in) :: unit
    character(len=*), intent(in) :: iotype
    integer, intent(in) :: v_list(:)
    integer, intent(out) :: iostat
    character(len=*), intent(inout) :: iomsg
    character(len=32) :: fmt
    integer :: digits
    digits = 2
    if (size(v_list) > 0) digits = v_list(1)
    if (iotype == 'LISTDIRECTED') digits = 4
    write (fmt, '(a, i0, a, i0, a)') '(a, i0, a, es', digits + 8, '.', digits, ', a)'
    write (unit, fmt, iostat=iostat, iomsg=iomsg) '[step ', dtv%step, ' residual ', dtv%residual, ']'
  end subroutine sample_write_f
end module sample_mod

program unformatted_dtio
  use sample_mod
  implicit none
  type(sample_t) :: a, b
  integer :: u, ios

  a = sample_t(2000, 1.38e-4_dp)

  open (newunit=u, status='scratch', access='stream', form='unformatted')
  write (u) a                          ! unformatted DTIO: 4 + 4 bytes, not 4 + 8
  write (u) a%step + 1, a              ! DTIO can sit among ordinary items
  inquire (unit=u, size=ios)
  print '(a, i0)', 'file size in bytes: ', ios
  rewind (u)
  read (u) b
  print '(a, i0, a, es10.3)', 'step=', b%step, ' residual=', b%residual
  close (u)

  print '(a, dt)', 'sample: ', a            ! formatted: the interface block provides the DT procedure
  print '(a, dt(3))', 'wide:   ', a
  print *, a                                ! list-directed output uses the same procedure
end program unformatted_dtio

Output of gfortran:

file size in bytes: 20
step=2000 residual= 1.380E-04
sample: [step 2000 residual   1.38E-04]
wide:   [step 2000 residual   1.380E-04]
 [step 2000 residual   1.3800E-04]

Flang prints the same output, except for the leading blank in the last line. The file has 20 bytes: the defined procedure wrote 4 + 4 bytes for each a and the explicit a%step + 1 wrote 4 bytes. An unformatted defined procedure does not position the file after the transfer (the child statements are processed differently; see Child data transfer statements), which is why several of them can be placed in one record. Two details:

  • The residual was stored as a default real and so it is not what was written: 1.38E-4_dp comes back as 1.380E-04 only because of the display precision. Defined input/output procedures are an easy place to lose precision silently; use the same kind in both directions unless the file format requires otherwise.

  • The interface block is found by name resolution, not by type. A suitable generic interface must be accessible in the scoping unit that contains the data transfer statement (12.6.4.8.4), so a module that defines such an interface has to be used (use sample_mod above). A type-bound generic is always found with the type.

dtv: class for extensible types, type for the others

dtv is class(t) if t is extensible, which every derived type is unless it is a sequence or bind(c) type (C1236, R1221), and type(t) otherwise. Writing type(sample_t) for an ordinary type is the first error most people meet. It is rejected by both compilers. This program is deliberately wrong:

module sample_mod
  implicit none
  type :: sample_t
    integer :: step = 0
  end type sample_t

  interface write(unformatted)
    module procedure sample_write_u
  end interface

contains

  subroutine sample_write_u(dtv, unit, iostat, iomsg)
    type(sample_t), intent(in) :: dtv        ! wrong: sample_t is extensible, so this must be class(sample_t)
    integer, intent(in) :: unit
    integer, intent(out) :: iostat
    character(len=*), intent(inout) :: iomsg
    write (unit, iostat=iostat, iomsg=iomsg) dtv%step
  end subroutine sample_write_u
end module sample_mod

gfortran 16.2:

05_type_vs_class_error.f90:13:29:

   13 |   subroutine sample_write_u(dtv, unit, iostat, iomsg)
      |                             1~~
Error: DTIO dummy argument at (1) must be of type CLASS

LLVM Flang 23:

error: Semantic errors in 05_type_vs_class_error.f90
05_type_vs_class_error.f90:14:35: error: Dummy argument 'dtv' of a defined input/output procedure must be CLASS() when the derived type is extensible
      type(sample_t), intent(in) :: dtv        ! wrong: sample_t is extensible, so this must be class(sample_t)
                                    ^^^

For a type-bound generic, C782 requires the type of dtv to be the type that declares the binding. A sequence type is not extensible, so there the declaration is type(...) instead:

module pair_mod
  implicit none
  type :: pair_t
    sequence                         ! a sequence type is not extensible, so dtv is declared with type(...)
    integer :: ix, iy
  end type pair_t

  interface write(formatted)
    module procedure pair_write
  end interface
contains
  subroutine pair_write(dtv, unit, iotype, v_list, iostat, iomsg)
    type(pair_t), intent(in) :: dtv
    integer, intent(in) :: unit
    character(len=*), intent(in) :: iotype
    integer, intent(in) :: v_list(:)
    integer, intent(out) :: iostat
    character(len=*), intent(inout) :: iomsg
    write (unit, '(a, i0, a, i0, a)', iostat=iostat, iomsg=iomsg) '(', dtv%ix, ',', dtv%iy, ')'
    if (.false.) print *, iotype, v_list
  end subroutine pair_write
end module pair_mod

program sequence_type
  use pair_mod
  implicit none
  print '(a, dt)', 'pair: ', pair_t(3, 4)
  print *, pair_t(5, 6)
end program sequence_type
pair: (3,4)
 (5,6)

Flang prints (5,6) without the leading blank on the last line. All length type parameters of the type in dtv must be assumed (C1237), so a type with a length parameter is declared type(t(*)) or class(t(*)) (see Parameterized Derived Types).

Child data transfer statements

A statement executed inside a defined input/output procedure on the unit that was passed in is a child data transfer statement and the statement that invoked the procedure is its parent (12.6.4.8.3). Child statements are processed differently from ordinary ones:

Rule Consequence

A child statement does not position the file before the transfer

it continues from where the parent statement left the file (not necessarily at the start of a record); t and tl edit descriptors shall not move before the position at which the procedure was invoked

An unformatted child statement does not position after the transfer

several unformatted child statements build one record

advance= in a child statement is ignored

child output never starts a new record by itself; write a slash or a line feed explicitly

The connection modes are those of the parent at the time of the call

blank=, pad=, round=, decimal=, delim= and so on are inherited; inquire on the unit returns them

A child statement shall not have id=, pos= or rec=

those cannot be used inside the procedure (Note 7)

Neither parent nor child shall be asynchronous

asynchronous='yes' cannot be combined with defined input/output (12.6.4.8.3)

A list-directed or namelist child statement may contain an item of derived type

it calls the matching procedure again

A child statement can contain a DT edit descriptor

this is how a component that is itself of a type with defined I/O is handled

A parent can also be a child, so a procedure can call itself indirectly. If that happens the procedure cannot be declared non_recursive (Note 2 of 12.6.4.8.3). A procedure is recursive unless it has the non_recursive prefix, and recursive is advisory only (15.6.2.1); the example spells it out for the reader. The list history_t in the next program writes itself through a child dt statement that follows the older pointer.

module history_mod
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: sample_t, history_t, push, dp
  integer, parameter :: dp = real64

  type :: sample_t
    integer :: step = 0
    real(dp) :: residual = 0.0_dp
  contains
    procedure, private :: sample_write
    generic :: write(formatted) => sample_write
  end type sample_t

  ! A singly linked list of samples; its DTIO procedure calls itself through a child statement.
  type :: history_t
    type(sample_t) :: sample
    type(history_t), pointer :: older => null()
  contains
    procedure, private :: history_write
    generic :: write(formatted) => history_write
  end type history_t

contains

  subroutine sample_write(dtv, unit, iotype, v_list, iostat, iomsg)
    class(sample_t), intent(in) :: dtv
    integer, intent(in) :: unit
    character(len=*), intent(in) :: iotype
    integer, intent(in) :: v_list(:)
    integer, intent(out) :: iostat
    character(len=*), intent(inout) :: iomsg
    if (iotype == 'NAMELIST' .and. size(v_list) > 0) then
      iostat = 1
      iomsg = 'no v-list expected'
      return
    end if
    write (unit, '(a, i0, a, es9.2)', iostat=iostat, iomsg=iomsg) 'step ', dtv%step, ' residual ', dtv%residual
  end subroutine sample_write

  recursive subroutine history_write(dtv, unit, iotype, v_list, iostat, iomsg)
    class(history_t), intent(in) :: dtv
    integer, intent(in) :: unit
    character(len=*), intent(in) :: iotype
    integer, intent(in) :: v_list(:)
    integer, intent(out) :: iostat
    character(len=*), intent(inout) :: iomsg
    write (unit, '(dt)', iostat=iostat, iomsg=iomsg) dtv%sample      ! child statement, component with DTIO
    if (iostat /= 0) return
    if (associated(dtv%older)) then
      write (unit, '(a)', iostat=iostat, iomsg=iomsg) new_line('a')    ! records are the procedure's business
      if (iostat /= 0) return
      write (unit, '(dt)', iostat=iostat, iomsg=iomsg) dtv%older       ! recursion through a child statement
    end if
    if (.false.) print *, iotype, v_list
  end subroutine history_write

  function push(old, step, residual) result(new)
    type(history_t), pointer, intent(in) :: old
    integer, intent(in) :: step
    real(dp), intent(in) :: residual
    type(history_t), pointer :: new
    allocate (new)
    new%sample = sample_t(step, residual)
    new%older => old
  end function push
end module history_mod

program nested_and_recursive
  use history_mod, only: history_t, push, dp
  implicit none
  type(history_t), pointer :: top => null(), p
  integer :: k

  do k = 1, 3
    top => push(top, 500*k, 1.0e-2_dp/k)
  end do

  print '(dt)', top               ! three child writes, one per node, written with one parent statement
  print '(a)', '--- after the list'
  do while (associated(top))      ! free the list
    p => top%older
    deallocate (top)
    top => p
  end do
end program nested_and_recursive

Output (gfortran and Flang print the same lines):

step 1500 residual  3.33E-03
step 1000 residual  5.00E-03
step 500 residual  1.00E-02
--- after the list

The sample component is handled with dt in the child statement write (unit, '(dt)') dtv%sample. The procedure writes new_line('a') between the records because a child statement does not end the record by itself. The standard’s own Note 8 and Note 9 show the same ideas (a two-component type with widths from v_list and a linked list written one per line); the example above is an original one.

Components with their own defined I/O in gfortran

When an item is of a type without defined input/output that has a component of a type with defined input/output, the item is treated as the list of its components in component order, and the component with the procedure is processed by it (12.6.3). In gfortran 16.2 that works only when the component with the defined procedure is the first one in storage order. This is the program that reproduces it, in which inner_t has a write(formatted) binding and outer_t has none:

module m
  implicit none
  type :: inner_t
    integer :: step = 0
  contains
    procedure, private :: wf
    generic :: write(formatted) => wf
  end type inner_t

  type :: outer_t
    character(len=4) :: label = ''
    type(inner_t) :: last
    integer :: count = 0
  end type outer_t
contains
  subroutine wf(dtv, unit, iotype, v_list, iostat, iomsg)
    class(inner_t), intent(in) :: dtv
    integer, intent(in) :: unit
    character(len=*), intent(in) :: iotype
    integer, intent(in) :: v_list(:)
    integer, intent(out) :: iostat
    character(len=*), intent(inout) :: iomsg
    write (unit, '(a, i0, a)', iostat=iostat, iomsg=iomsg) '<', dtv%step, '>'
    if (.false.) print *, iotype, v_list
  end subroutine wf
end module m

program p
  use m
  implicit none
  type(outer_t) :: h
  h = outer_t('run', inner_t(2000), 7)
  print *, h
  write (*, '(a4, dt, i3)') h%label, h%last, h%count
  write (*, '(a4, dt, i3)') h
end program p

gfortran 16.2 (the number in the first and third line is the characters run interpreted as an integer, that is, the procedure received the address of the whole object instead of the component):

 run  <544109938>           7
run <2000>  7
run <544109938>  7

Flang 23 prints <2000> in all three places. If inner_t is moved to be the first component of outer_t gfortran also prints it correctly. Until the defect is fixed, give an enclosing type its own defined procedure that writes each component with a child dt statement, as history_write does above, or write the components explicitly. This is a compiler bug and not a rule of the language; the behaviour of Flang is the one the standard describes.

Restrictions

A defined input/output procedure is more restricted than the code that calls it (12.6.4.8.3):

  • While a parent read is active, no I/O statement may read from an external unit other than unit, and none may write to any external unit. While a parent write or print is active, no statement may write to an external unit other than unit, and none may read from any external unit.

  • A data transfer statement that specifies an internal file is permitted while a parent statement is active. That is how write (fmt, ...) ... is used to build a format in the examples.

  • open, close, backspace, endfile and rewind shall not be executed while a parent statement is active.

  • The procedure and the procedures it invokes shall not define, or make undefined, any storage unit referenced by an item of the active statement, the format or any specifier, except through dtv.

  • The v_list and iotype cannot be used to read or write a different object: they are inputs.

The processors do not diagnose most of these. This program writes to error_unit inside a defined output procedure that was called by a print to output_unit, which breaks the second rule:

module log_mod
  use, intrinsic :: iso_fortran_env, only: error_unit
  implicit none
  type :: noisy_t
    integer :: id = 1
  contains
    procedure, private :: wf
    generic :: write(formatted) => wf
  end type noisy_t
contains
  subroutine wf(dtv, unit, iotype, v_list, iostat, iomsg)
    class(noisy_t), intent(in) :: dtv
    integer, intent(in) :: unit
    character(len=*), intent(in) :: iotype
    integer, intent(in) :: v_list(:)
    integer, intent(out) :: iostat
    character(len=*), intent(inout) :: iomsg
    write (error_unit, '(a)') 'logging to another external unit'   ! not allowed in a defined output procedure
    write (unit, '(a, i0)', iostat=iostat, iomsg=iomsg) 'noisy ', dtv%id
    if (.false.) print *, iotype, v_list
  end subroutine wf
end module log_mod

program other_unit
  use log_mod
  implicit none
  type(noisy_t) :: n
  print '(dt)', n
end program other_unit

Both compilers print logging to another external unit and noisy 1 without any diagnostic, even with every check enabled:

logging to another external unit
noisy 1

It works on these processors, and is not standard conforming. A program that has to be portable sends diagnostics through the unit it was given, or collects them in iomsg.

A missing procedure is a run time error

If the format has a DT descriptor for a type that has no suitable defined procedure, the program compiles and fails when the statement executes (this is deliberately wrong):

program no_procedure
  implicit none
  type :: point_t
    integer :: ix = 1, iy = 2
  end type point_t
  type(point_t) :: p
  print '(dt)', p                  ! no defined output procedure exists for point_t
end program no_procedure

gfortran 16.2:

At line 7 of file 16_no_procedure_error.f90 (unit = 6, file = 'stdout')
Fortran runtime error: Missing DTIO procedure or intrinsic type passed for item 1 in formatted transfer
(dt)
  ^

Error termination. Backtrace:

LLVM Flang 23 terminates as well, with a message that comes from its treatment of dt as an ordinary data edit descriptor:

fatal Fortran runtime error(/work/16_no_procedure_error.f90:7): Data edit descriptor 'd' may not be used with an INTEGER data item
Aborted

Recursive input/output

A defined input/output procedure is the one case where an I/O statement is meant to execute while another one is active. In all others the rule of 12.12 applies: an I/O statement executed while another is being executed is a recursive I/O statement, and it shall not identify an external unit that is identified by the statement being executed. The exception is a child statement on its parent’s unit. A recursive statement shall not modify any internal unit, except that a recursive write may modify the internal unit it names itself. In other words, a function referenced in an output list cannot print, but it can write into its own internal file. This is the failing case:

program recursive_io
  implicit none
  ! The function writes to unit 6 while a PRINT to unit 6 is executing: recursive I/O on one unit.
  print *, 'value: ', f(3)
contains
  function f(n) result(r)
    integer, intent(in) :: n
    integer :: r
    print *, 'inside f'
    r = n*2
  end function f
end program recursive_io

gfortran 16.2 stops at run time (the backtrace is omitted):

At line 9 of file 08_recursive_io.f90 (unit = 6, file = 'stdout')
Fortran runtime error: Recursive I/O not allowed

Error termination. Backtrace:

Flang 23 stops too: fatal Fortran runtime error(/work/08_recursive_io.f90:9): Recursive I/O attempted on unit 6, followed by Aborted. The remedy is to compute the value first and print it after, or to make the function write to a different external unit. A function that only converts with an internal file is fine:

program recursive_ok
  implicit none
  ! label writes only to an internal file, which is allowed while unit 6 is being written.
  write (*, '(2a)') 'text:  ', label(7)
contains
  function label(n) result(s)
    integer, intent(in) :: n
    character(len=8) :: s
    write (s, '(a, i0)') 'n=', n
  end function label
end program recursive_ok
text:  n=7

Another corner of 12.12: an I/O statement shall not cause the value of an established format specification to be modified, so a statement whose format is held in fmt must not also write into fmt.

Asynchronous input/output

An asynchronous data transfer statement initiates a transfer and may return before it has finished. The processor is allowed to perform it asynchronously, but is not required to (12.6.2.5), and the order of the records and file storage units is the same as if the statements had been synchronous. The mechanism has four parts:

Part Rule

asynchronous='yes' in open

the unit allows asynchronous transfers; the default is 'no' (12.5.6.5). Preconnected units, internal files and units opened without it allow only synchronous I/O

asynchronous='yes' in read or write

this statement is asynchronous. It must name an external unit (C1226) and the specifier value is a constant expression (12.6.2.5). id= requires it (C1227)

id= in the data transfer statement

a scalar default integer that receives a processor-determined value: zero if the transfer has already completed, otherwise an identifier different from the identifier of any other pending transfer on the unit (12.6.2.9)

wait, inquire or another statement

completes the transfer. Only after that are the variables defined (for input) or free to be modified (for output)

While a transfer is pending, the pending input/output storage sequence (the items of the list, the group objects of a namelist, and the size= variable) is owned by the I/O system. A variable that is part of it is a pending input/output storage sequence affector. For a pending output the program must not redefine an affector, make it undefined or change its pointer association status; for a pending input it must not even reference it, define it, or change its association status (12.6.4.1). Both hold until the wait. To let the compiler know where that can happen the variable has the asynchronous attribute (8.5.4).

The asynchronous attribute

The attribute can be given in a type declaration (real(dp), asynchronous :: a(:,:)) or with the asynchronous statement (8.6.3). A variable that appears in an asynchronous data transfer statement implicitly has the attribute in that scoping unit, and the attribute can be confirmed explicitly (12.6.2.5). The rule in 8.5.4 is that the base object of a variable has to have the attribute in a scoping unit if the variable is a dummy argument or appears in an executable statement or specification expression there, and any statement of the scoping unit is executed while the variable is a pending affector. An object with the attribute may be associated with one without it, including by use or host association, and subobjects inherit it. A block construct can give a variable the attribute inside the construct only. The attribute exists to tell the compiler which variables may be touched by a pending transfer, so that it does not move references to them across the statements that start and complete it (Note to 8.5.4). This example shows a snapshot writer: it declares the attribute because the transfer started in write_snapshot may still be pending when it returns.

module snapshot_mod
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: dp, jacobi_sweep, write_snapshot
  integer, parameter :: dp = real64

contains

  ! One Jacobi sweep of the interior; returns the largest change.
  subroutine jacobi_sweep(u, unew, residual)
    real(dp), intent(in) :: u(:, :)
    real(dp), intent(inout) :: unew(:, :)
    real(dp), intent(out) :: residual
    integer :: nx, ny
    nx = size(u, 1)
    ny = size(u, 2)
    unew = u
    unew(2:nx-1, 2:ny-1) = 0.25_dp*(u(1:nx-2, 2:ny-1) + u(3:nx, 2:ny-1) + u(2:nx-1, 1:ny-2) + u(2:nx-1, 3:ny))
    residual = maxval(abs(unew - u))
  end subroutine jacobi_sweep

  ! Start writing a snapshot. The dummy argument carries the
  ! asynchronous attribute because a data transfer that uses it may still be pending on return.
  subroutine write_snapshot(unit, u)
    integer, intent(in) :: unit
    real(dp), intent(in), asynchronous :: u(:, :)
    write (unit, asynchronous='yes') u
  end subroutine write_snapshot
end module snapshot_mod

program async_write
  use snapshot_mod, only: dp, jacobi_sweep, write_snapshot
  implicit none
  integer, parameter :: n = 64, steps = 4
  real(dp), allocatable, asynchronous :: a(:, :), b(:, :)
  real(dp) :: residual
  integer :: unit, ios, step
  logical :: pending
  character(len=100) :: msg

  allocate (a(n, n), b(n, n))
  a = 0.0_dp
  a(1, :) = 100.0_dp                      ! hot edge

  open (newunit=unit, file='snapshots.bin', status='replace', access='stream', form='unformatted', &
        asynchronous='yes', iostat=ios, iomsg=msg)
  if (ios /= 0) error stop trim(msg)

  do step = 1, steps
    call write_snapshot(unit, a)      ! start writing the current grid ...
    call jacobi_sweep(a, b, residual)     ! ... while the sweep only reads a and writes b
    wait (unit, iostat=ios, iomsg=msg)          ! complete the transfer before a is modified
    if (ios /= 0) error stop trim(msg)
    a = b
    print '(a, i0, a, es9.2)', 'step ', step, ' residual ', residual
  end do

  inquire (unit, pending=pending, iostat=ios)
  print '(a, l1)', 'anything still pending: ', pending
  inquire (unit, size=ios)
  print '(a, i0, a)', 'file size: ', ios, ' bytes'
  close (unit, status='delete')
end program async_write

Output of gfortran:

step 1 residual  2.50E+01
step 2 residual  1.25E+01
step 3 residual  7.81E+00
step 4 residual  5.47E+00
anything still pending: F
file size: 131072 bytes

Flang prints the same numbers. The structure of the loop is the standard double-buffering pattern: the sweep only reads a while the snapshot of a is being written (a pending output variable may be referenced but not redefined, 12.6.4.1), and a = b is executed only after the wait. In this program the wait has no id=, which is the portable form: it completes every pending transfer on the unit. iostat= and iomsg= on the wait carry the errors of the transfer: the actions of the statement that performs the wait apply (12.7.1).

Which statements perform a wait? Besides wait, 12.7.1 lists backspace, close, endfile, flush, inquire, print, read, rewind and write; so a later read or write on the unit, or close, also completes the pending transfers. A file positioning statement waits for all pending transfers on its unit (12.8.1). If an error or end-of-file condition occurs during a wait for a unit, the processor waits for all pending transfers of that unit (12.7.1).

Reading asynchronously

A read is dangerous in the other direction: the input variable has no defined value until the wait. Both compilers give the same data here; the difference is where an end-of-file is reported, which the standard allows, since errors can be raised by the statement that starts the transfer or by the wait (note to 12.7.1). A correct program therefore checks iostat on both the read and the wait.

program async_read
  use, intrinsic :: iso_fortran_env, only: real64, iostat_end
  implicit none
  integer, parameter :: dp = real64, n = 5
  real(dp), asynchronous :: buf(n), next(n)
  integer :: unit, ios, k
  character(len=100) :: msg

  ! Create a stream file holding the numbers 1 to 10.
  open (newunit=unit, file='data.bin', status='replace', access='stream', form='unformatted')
  write (unit) [(real(k, dp), k = 1, 10)]
  close (unit)

  open (newunit=unit, file='data.bin', status='old', access='stream', form='unformatted', &
        action='read', asynchronous='yes')
  read (unit, asynchronous='yes', pos=1) buf             ! two reads queued on one unit
  read (unit, asynchronous='yes', pos=1 + 8*n) next
  wait (unit)                                            ! all pending transfers on the unit complete here
  print '(a, 5f5.1)', 'buf  = ', buf
  print '(a, 5f5.1)', 'next = ', next

  ! Reading past the end of the file: where the end-of-file condition is reported is up to the processor.
  read (unit, asynchronous='yes', pos=1 + 8*8, iostat=ios, iomsg=msg) buf
  print '(a, l1)', 'end of file reported by the read statement: ', ios == iostat_end
  wait (unit, iostat=ios, iomsg=msg)
  print '(a, l1)', 'end of file reported by the wait statement: ', ios == iostat_end
  close (unit, status='delete')
end program async_read

Output of gfortran (the data are right, and the end-of-file is reported by the wait):

buf  =   1.0  2.0  3.0  4.0  5.0
next =   6.0  7.0  8.0  9.0 10.0
end of file reported by the read statement: F
end of file reported by the wait statement: T

LLVM Flang 23 prints the first two lines, then reports the end-of-file already in the read statement:

buf  =   1.0  2.0  3.0  4.0  5.0
next =   6.0  7.0  8.0  9.0 10.0
end of file reported by the read statement: T
end of file reported by the wait statement: F

id= and pending=

id= gives each request a handle that wait (unit, id=...) and inquire (unit, id=..., pending=...) can use to refer to one request. When id= appears in inquire, pending= shall appear too (C1250). The pending= logical is false when the identified transfer (or, without id=, all previously pending transfers on the unit) is complete, and then the inquire performs the wait operation; otherwise it is true and nothing is waited for (12.10.2.22). A processor may always return true until the wait, or always false after waiting; the standard lists three acceptable behaviours. A zero value in id= means the transfer is already complete (12.6.2.9), and wait with id=0 is allowed.

program async_ids
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  integer, parameter :: dp = real64, n = 5
  real(dp), asynchronous :: first(n), second(n)
  integer :: unit, id1, id2, k

  open (newunit=unit, file='ids.bin', status='replace', access='stream', form='unformatted')
  write (unit) [(real(k, dp), k = 1, 2*n)]
  close (unit)

  open (newunit=unit, file='ids.bin', status='old', access='stream', form='unformatted', &
        action='read', asynchronous='yes')
  read (unit, asynchronous='yes', id=id1, pos=1) first
  read (unit, asynchronous='yes', id=id2, pos=1 + 8*n) second
  print '(a, i0, a, i0)', 'id1 = ', id1, ', id2 = ', id2     ! processor-determined; 0 means already complete

  wait (unit, id=id1)                                         ! complete one particular request
  print '(a, 5f5.1)', 'first  = ', first
  wait (unit, id=id2)
  print '(a, 5f5.1)', 'second = ', second
  close (unit, status='delete')
end program async_ids

gfortran 16.2 prints the following. The first request is reported as already complete (id1 = 0) and the data of first are nevertheless not defined: the values differ from run to run (zeros, uninitialized garbage, or asterisks where the garbage does not fit the f5.1 field, as in the run shown). This violates 12.6.2.9 (a zero identifier means the transfer is complete, and 12.7.1 says a completed input defines its variables):

id1 = 0, id2 = 1
first  =   0.0  0.0*****  0.0  0.0
second =   6.0  7.0  8.0  9.0 10.0

LLVM Flang 23 returns the identifiers 1 and 2, and both variables are defined after their waits:

id1 = 1, id2 = 2
first  =   1.0  2.0  3.0  4.0  5.0
second =   6.0  7.0  8.0  9.0 10.0

The workaround on gfortran 16.2 is to complete requests with wait (unit) without id=, as the previous two programs do. A related observation: in a variant of the snapshot program that completed each write with wait (unit, id=id), gfortran 16.2 reported anything still pending: T after the loop, where Flang reported F. Waiting without id= gave F on both.

Asynchronous input/output without id= has a cost in diagnosability. Errors and end-of-file for such a statement may be reported during any later data transfer or wait on the unit, and then the actions of that later statement are taken (12.6.4.1), so the code cannot tell which read failed. The note to that clause draws the conclusion: reliably attributing an end-of-file to a read needs id= on every asynchronous read of the unit.

The attribute is not diagnosed when it is missing

The compiler cannot see across procedures that a transfer is pending, so omitting the attribute is not an error it can report. The program below is not conforming: start_write has no asynchronous attribute on its dummy argument, so the compiler may keep u in a register or move the assignment a = 2.0_dp before the transfer has finished, and the file may receive the new values.

module snapshot_mod
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  integer, parameter :: dp = real64
contains
  subroutine start_write(unit, u)
    integer, intent(in) :: unit
    real(dp), intent(in) :: u(:)          ! no asynchronous attribute here
    write (unit, asynchronous='yes') u
  end subroutine start_write
end module snapshot_mod

program forgotten
  use snapshot_mod
  implicit none
  real(dp) :: a(4)
  integer :: unit
  a = 1.0_dp
  open (newunit=unit, status='scratch', access='stream', form='unformatted', asynchronous='yes')
  call start_write(unit, a)
  a = 2.0_dp                              ! a is a pending storage sequence affector, and may not be defined
  wait (unit)
end program forgotten

Both compilers accept it and run it without a message (there is no output). It is a good example of a bug that only appears at a higher optimization level, on a processor that really performs the transfer asynchronously, or with another compiler. Declare the attribute on every dummy argument, module variable and local variable that can be an affector, and complete the transfer before touching the data.

Compiler support

Asynchronous transfers are allowed to be synchronous. The LLVM Flang documentation lists asynchronous input/output as only partially supported ("IO will happen synchronously"), and the identifiers Flang printed above (1 and 2) are consistent with that. gfortran’s behaviour was observed above only through its results, not its implementation: with it an id= of zero was returned for the first request and inquire (..., pending=...) could be true after a wait. A program that has to be portable should treat asynchronous='yes' as an optimization hint, use wait (unit) to complete every transfer, test iostat on both the transfer and the wait, and not assume overlap. Measure before relying on it: file systems and the I/O library decide whether any overlap occurs, and a second thread with !$omp or a coarray image doing the writing is often a more predictable way of overlapping I/O with computation (see OpenMP and OpenACC and Coarrays, Images and Synchronization).

The asynchronous attribute has a second use. The same attribute marks variables that take part in asynchronous communication, for instance the buffers of non-blocking MPI calls (mpi_isend and the like, 18.10.4 Asynchronous communication), and the rule in 8.5.4 applies to them in the same way; see MPI with mpi_f08.

UTF-8 files

Default character kind is whatever the processor chooses (usually a single byte per character). The standard way to hold and store Unicode is the ISO 10646 character kind, obtained with selected_char_kind('ISO_10646') (16.9.180), which returns a kind value or -1 if the processor does not support it. Characters of that kind are stored in memory as UCS-4 code points. To write them to a file as UTF-8, open the unit with encoding='UTF-8' (12.5.6.9, only for formatted I/O); the default is processor dependent. inquire (unit, encoding=...) returns 'UTF-8' for such a connection and 'UNDEFINED' for an unformatted one (12.10.2.10).

program utf8_files
  implicit none
  integer, parameter :: ucs4 = selected_char_kind('ISO_10646')
  character(kind=ucs4, len=:), allocatable :: title, back
  character(len=100) :: enc, msg
  character(kind=ucs4, len=40) :: line
  integer :: unit, ios, n

  print '(a, i0)', 'selected_char_kind("ISO_10646") = ', ucs4
  if (ucs4 < 0) stop 'ISO 10646 characters are not supported'

  title = ucs4_'Temperatura ' // char(int(z'00b0'), ucs4) // ucs4_'C: ' // char(int(z'03b1'), ucs4) // ucs4_' = 0.01'

  open (newunit=unit, file='heat.txt', status='replace', encoding='UTF-8', iostat=ios, iomsg=msg)
  if (ios /= 0) error stop trim(msg)
  write (unit, '(a)') title
  write (unit, '(a)') ucs4_'caf' // char(int(z'00e9'), ucs4)
  inquire (unit, encoding=enc)
  print '(2a)', 'encoding reported by inquire: ', trim(enc)
  close (unit)

  inquire (file='heat.txt', size=n)
  print '(a, i0, a)', 'file size: ', n, ' bytes'     ! UTF-8 on disk: 2 bytes for each non-ASCII character

  open (newunit=unit, file='heat.txt', status='old', encoding='UTF-8')
  read (unit, '(a)') line
  n = len_trim(line)
  back = line(1:n)
  print '(a, i0, a, i0)', 'characters read: ', n, ', code of the 13th: ', iachar(back(13:13), ucs4)
  close (unit, status='delete')
end program utf8_files

Output (gfortran and Flang print the same lines):

selected_char_kind("ISO_10646") = 4
encoding reported by inquire: UTF-8
file size: 33 bytes
characters read: 24, code of the 13th: 176

The file has 33 bytes: 24 characters and a record end on the first line, 4 characters and a record end on the second, and one extra byte for each of the three non-ASCII characters (°, α, é), which take two bytes in UTF-8. The kind-4 literals need the ucs4_ prefix, and kinds cannot be mixed in a concatenation, which both compilers report at compile time as an operand kind mismatch. Writing a character constant with code points above 127 in the source requires the compiler to read the source as UTF-8, which is not part of the standard; building the characters with char(code, ucs4) as above is portable. For an internal file the kind of the list items is restricted by the kind of the file (12.6.3). More character handling is in Characters and Strings.

Where the books are outdated

  • Defined input/output and iotype. Older texts treat derived-type I/O as a vendor extension, or stop at namelist. The procedures are standard and both gfortran and Flang implement them, but the behaviour of the details is uneven (zero and negative v values, defined I/O on components), as shown above.

  • class for dtv. Examples that declare type(t) for dtv predate the rule for extensible types (C1236) and are rejected today.

  • recursive is no longer needed to call a procedure from itself; non_recursive is the opt-out (15.6.2.1).

  • Asynchronous I/O is often presented as a guarantee of overlap. It is a permission: processors may do the transfer synchronously, as Flang does.

  • Unicode. Texts that predate selected_char_kind('ISO_10646') and encoding='UTF-8' recommend storing bytes in default characters and decoding them by hand.

  • Free-form escape sequences in character constants are not standard Fortran; use char(code, kind) or the achar/iachar functions.

Pitfalls

Pitfall What to do

type(t) for an extensible dtv

use class(t); type(t) only for sequence and bind(c) types

Using v_list(1) without a test

v_list is zero-sized for a plain dt, for list-directed and for namelist transfers

Zero or negative v values with gfortran

rejected (compile time with a literal format, run time otherwise); use positive values

Hard-coding unit 6

write to the unit argument; it is negative for internal files and newunit units

Ignoring iostat of the child statements

return immediately if it is not zero, and set iomsg when the procedure itself detects the error

Whole-object assignment to a polymorphic dtv in a read procedure

assign the components (or use a non-polymorphic helper)

A type with allocatable or pointer components in a formatted statement without defined I/O

not allowed (12.6.3); define write(formatted) or write the components by hand, as grid_t does for its allocatable array

A child write that is expected to end the record

write new_line('a') or use a slash edit descriptor; child statements do not start records

Writing to another external unit inside the procedure

not conforming and not diagnosed (12.6.4.8.3); report through iomsg

A function in an output list that does I/O on the same unit

recursive I/O error (12.12); compute the value first

Components with defined I/O under gfortran 16.2

give the enclosing type its own procedure that uses a child dt statement

Reading or modifying an asynchronous variable before the wait

the variable is a pending affector (12.6.4.1); wait first

Forgetting the asynchronous attribute on a dummy argument

not diagnosed; add it in every scoping unit that sees the variable while a transfer can be pending

Assuming asynchronous means concurrent

it may be synchronous; Flang currently performs the transfers synchronously

wait (unit, id=...) on gfortran 16.2

observed to leave input undefined for an id of zero; use wait (unit)

Checking iostat only on the initiating statement

errors can show up at the wait or at a later statement on the unit

Combining asynchronous='yes' with defined input/output

neither parent nor child may be asynchronous (12.6.4.8.3)

encoding='UTF-8' with unformatted or stream-unformatted files

the specifier applies only to formatted connections (12.5.6.9)

See also Common Gotchas and Pitfalls, Best Practices and Style and Object Oriented Programming for the type-bound procedures that carry the write(formatted) generic.