Fortran Control Flow

Control flow decides what happens next. Fortran's constructs are block-structured like every modern language, but with two quirks: blocks are delimited by explicit END words (end if, end do), and the branch conditions use the dotted operators (>, .and., .eqv.). This lesson builds the complete decision and loop toolkit.

IF constructs

The if family selects one block of statements. A bare logical expression between if ( ... ) then and end if; an else if ladder for several mutually exclusive branches; else for the fallback. One-line form if (x > 0) y = 1 exists but blocks are the readable default. Note the comparison operators: > < >= <= == /=, with /= meaning "not equal" — the != you know from C-family languages is an error here.

program if_demo
  implicit none
  real :: temperature
  print '(a)', 'Temperature (C)?'
  read *, temperature
  if (temperature < 0.0) then
     print '(a)', 'ice'
  else if (temperature < 100.0) then
     print '(a)', 'liquid water'
  else
     print '(a)', 'steam'
  end if
end program if_demo

Nested if blocks are allowed anywhere, but deep nesting is the first thing reviewers flag — reach for the select case construct below when the test is an equality against a small set of values.

SELECT CASE

select case evaluates one expression once and dispatches on equality with a list of values — the Fortran cousin of C's switch. The selector may be integer, character, or logical; case default catches everything else. Unlike C, no explicit break is needed: exactly one branch executes, then control jumps past end select.

SELECT CASE dispatch diagram

Fig. 1 — One selector, one branch, one merge point.

program case_demo
  implicit none
  integer :: slot
  print '(a)', 'Slot 1..3?'
  read *, slot
  select case (slot)
  case (1)
     print '(a)', 'first'
  case (2)
     print '(a)', 'second'
  case default
     print '(a)', 'unknown slot'
  end select
end program case_demo

DO loops

The do loop iterates a control variable over a range with an optional step: do i = first, last, step. All three bounds may be expressions, evaluated once at entry. The loop body runs for i = 1, 3 then 4, 5 (zero step is a compile-time error; a reversed range runs zero times — which is standard and useful for "skip if empty" logic).

program do_demo
  implicit none
  integer :: i, total
  total = 0
  do i = 1, 5, 2          ! odd steps: i = 1, 3, 5
     total = total + i
     print '(a,i2,a,i3)', 'i =', i, ' running total =', total
  end do
  print '(a,i3)', 'final: ', total     ! 1 + 3 + 5 = 9
end program do_demo

CYCLE and EXIT

cycle skips the rest of the current iteration and jumps to the next value; exit leaves the loop entirely. They replace the C-family continue/break. The idiom below scans a list for the first negative value — exit stops the scan the moment it is found, which is both faster and clearer than looping forever and testing inside.

program scan
  implicit none
  integer :: i, data(10)
  integer :: first_negative = 0
  data = [3, 7, 2, -5, 9, 1, -2, 8, 4, 6]
  do i = 1, size(data)
     if (data(i) < 0) then
        first_negative = data(i)
        exit                  ! stop searching
     end if
  end do
  print '(a,i0,a,i0)', 'first negative ', first_negative, ' at index ', i
end program scan

Named constructs

Any block construct can carry a name — outer: do i = 1, n — and the name makes nested exits unambiguous: exit outer ends the outer loop from inside an inner one. Naming every loop in a three-deep nest is the standard answer to the "which end does this end?" question, and gfortran's -Wall verifies that end labels match.

DO WHILE and infinite loops

do while (condition) repeats until the condition is false, testing at the top. The classic convergence loop do while (err > tol) fits it perfectly. For an unbounded loop use the even simpler do with an exit inside — the "loop forever until told otherwise" shape every simulation uses.

program convergence
  implicit none
  real :: x, err
  x = 2.0
  err = huge(1.0)
  do while (err > 1.0e-5)        ! iterate a sequence until it settles
     x = 0.5 * (x + 2.0 / x)     ! Newton step for sqrt(2)
     err = abs(x - sqrt(2.0))
     print '(f12.8)', x
  end do
end program convergence

DO CONCURRENT: the parallel-ready loop

do concurrent looks like a do loop but declares that iterations are independent — no iteration reads or writes a value another iteration writes. That independence is a contract, not a wish: the compiler may parallelize, vectorize, or redistribute the iterations, and it must return the same answer. Body restrictions enforce the contract (no exit, no writes to a variable shared across iterations except reductions). All the parallel lessons build on it:

program concurrent_sum
  use iso_fortran_env, only: int64
  implicit none
  integer, parameter :: n = 100000
  real :: v(n), result
  call random_number(v)
  result = 0.0
  ! Every iteration touches only v(i): the compiler may use any
  ! thread count or SIMD width to add the elements. The reduce
  ! locality specifier (Fortran 2018) makes the summation legal.
  do concurrent (i = 1:n) reduce(+:result)
     result = result + v(i)      ! reduction with explicit locality
  end do
  print '(a,es15.7)', 'sum = ', result
end program concurrent_sum

Without the reduce clause the summation would be a race — two iterations mutating the same scalar — and the standard forbids it. The same pattern generalizes to reduce(min:...), reduce(*:...) and user-defined reductions. The message is the same: write the loop as if it will run on a hundred cores, because the parallel lessons will make that true.