Fortran stdlib

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 stdlib 0.8.1 (released 2026-01-26, still the newest release on 2026-10-19) and Fortran 2023 (ISO/IEC 1539-1:2023, J3/23-007r1), and was checked on 2026-10-19. Every example was built with GNU Fortran 16.2 under gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all, and run, and then built and run again with LLVM Flang 23 against a Flang build of the same stdlib, on an aarch64 Linux container. stdlib itself was compiled from the v0.8.1 tag (see below for how, because a plain fpm git dependency on that tag does not work). The output shown is real output; where the two compilers differ the difference is shown. Examples that fail on purpose show the real diagnostic. Intel ifx, NAG and the other vendor compilers were not available, so nothing here claims how stdlib behaves with them beyond what the stdlib README itself says.

The Fortran Standard Library, stdlib, is a community project of the fortran-lang organization. It is not part of the language: the standard defines the intrinsic procedures and intrinsic modules (see Intrinsic Procedures Overview and Intrinsic Modules and iso_fortran_env), and stdlib is an ordinary library, written in Fortran, that you add to a project the way you add any other dependency. Its goal, in the words of its README, is a de facto standard library that fills the gaps the standard leaves: string handling, file-system and process access, sorting, hash maps, bitsets, a logging facility, linear algebra with a modern interface, statistics and numerical integration. The running example, heatflow, uses it for text input and output (loadtxt, savetxt), a linear solve for an implicit time step, logging, string processing of configuration records and sparse operators; how heatflow itself is built is in Fortran Package Manager (fpm) and Building with Make, CMake and Meson, and the BLAS and LAPACK side of stdlib_linalg is in BLAS, LAPACK and Numerical Libraries.

What stdlib is, and how its modules mature

stdlib has a written process (its WORKFLOW.md): an idea is discussed in an issue, the API is discussed, a specification page is written, an implementation is added, and every new public API starts in an experimental state. A specification is a document that describes the procedure well enough that someone could implement it without reading the stdlib source; the stdlib code is the reference implementation. Each spec page carries a Status heading for every procedure.

On the 0.8.1 tag the specification index (doc/specs/index.md) lists every one of its 28 entries under Experimental Features & Modules and states "(None yet)" under Released/Stable Features & Modules. The individual spec pages are slightly more nuanced, and this is checked against the tag rather than assumed: 19 procedures of stdlib_linalg are marked Stable in stdlib_linalg.md (diag, eye, trace, solve, solve_lu, lstsq, solve_lstsq, lstsq_space, det, .det., eig, eigh, eigvals, eigvalsh, svd, svdvals, .inv., invert and inv); every other documented procedure in the 37 spec pages is marked Experimental, and none is marked Deprecated (stdlib_kinds.md has no status headings). In practice:

Question Answer for 0.8.1

What does "experimental" promise?

the API may still change or be removed in a later release; the library is usable, and it is tested by its own test suite and compiled in the project’s CI, but you should pin the version

How do you pin it?

a git tag or the metapackage’s resolved release in fpm, GIT_TAG v0.8.1 in CMake; a moving branch such as stdlib-fpm is not a pin

Is there a compatibility promise across 0.x releases?

no; read CHANGELOG.md before upgrading (the 0.8.1 entry lists one behaviour change, the removal of the pure attribute from inner_product in stdlib_linalg_iterative_solvers)

Is stdlib part of Fortran 2028?

no; the standard and stdlib are independent, and nothing in the standard requires a compiler to ship stdlib

License

MIT

Two things the outline of this section sometimes assumes are not in 0.8.1: there is no date-and-time module (datetime is not in the source tree), and stdlib_strings has no split procedure (its public interface is to_string, to_c_char, strip, chomp, starts_with, ends_with, slice, find, replace_all, padl, padr, count, zfill and join). The Fortran 2023 intrinsics split and tokenize exist in the language, but as recorded in Characters and Strings, GNU Fortran 16.2 accepts tokenize and then fails to link it, so splitting a record by hand, as in the string example below, is still the portable choice.

Getting stdlib

stdlib is written with the fypp preprocessor (templates over kinds and ranks), so its repository is not directly compilable: the .fypp sources have to be expanded to .f90 first, and which route you take decides who does it. There are four practical routes, and the differences are worth knowing before the first failed build.

Route What it does Checked

fpm metapackage stdlib = "*"

fpm downloads the latest release of stdlib as a git dependency (per the fpm documentation); no Python needed

worked, reported 0.8.1

fpm git branch stdlib-fpm

the repository branch that already contains the expanded .f90 sources, built with the default macros; a moving target

worked, reported 0.8.1

fpm git tag v0.8.1

the tag contains only .fypp sources

fails, see below

fpm path dependency on a locally expanded tree

run config/fypp_deployment.py --deploy_stdlib_fpm in a checkout of the tag, point fpm at the resulting stdlib-fpm directory

worked; used for every example here

CMake FetchContent or an installed package

CMake runs fypp itself; needs the fypp Python package, a CMake generator, and BLAS/LAPACK if found

worked

fpm

Add the metapackage to the manifest. The [fortran] table is the same strict setting every heatflow manifest uses:

name = "heatflow"
version = "0.1.0"

[build]
auto-executables = true

[dependencies]
stdlib = "*"

[fortran]
implicit-typing = false
implicit-external = false

Metapackages are listed in the fpm documentation as experimental, the stdlib README says fpm 0.9.0 and later provides stdlib as a metapackage, and the version cannot be constrained (the "*" means any version; it resolves to the newest release when the dependency is fetched). The older spelling, still shown in the stdlib README, is the git branch:

[dependencies]
stdlib = { git = "https://github.com/fortran-lang/stdlib", branch = "stdlib-fpm" }

Both were built and run in the container with fpm 0.13.0 and printed 0.8.1. What does not work is the obvious-looking pin to the release tag:

[dependencies]
stdlib = { git = "https://github.com/fortran-lang/stdlib", tag = "v0.8.1" }
<ERROR> *cmd_run* Targets error: Unable to find source for module dependency: "stdlib_kinds" used by "app/main.f90"
STOP 1

fpm fetched the tag and found no Fortran source for stdlib_kinds, because on the tag it is src/core/stdlib_kinds.fypp. This is the real message from fpm 0.13.0. To get exactly 0.8.1 you expand the tree yourself, which is what the examples on this page do:

git clone --branch v0.8.1 https://github.com/fortran-lang/stdlib stdlib-src
pip install fypp joblib
cd stdlib-src
python3 config/fypp_deployment.py --deploy_stdlib_fpm   # writes ./stdlib-fpm with .f90 files

and then in the manifest of the project that uses it (the maximum array rank defaults to 4 in this route):

[dependencies]
stdlib = { path = "../stdlib-src/stdlib-fpm" }

fpm only compiles the modules that the program transitively uses, so a program that imports stdlib_kinds builds in about a second, while one that imports stdlib_linalg also compiles the embedded BLAS and LAPACK. The first build of the full set of modules used by the examples below took about 40 seconds in the container.

fpm with an external BLAS and LAPACK

stdlib ships its own BLAS and LAPACK in Fortran (stdlib_linalg_blas, stdlib_linalg_lapack), so stdlib_linalg works with nothing else installed. To use an optimized library instead, fpm 0.12.0 and later connects the two metapackages:

[dependencies]
stdlib = "*"
blas = "*"

With both enabled fpm defines STDLIB_EXTERNAL_BLAS and STDLIB_EXTERNAL_LAPACK for stdlib, which switch off the internal routines. In the container this resolved to OpenBLAS: the link line contained -lopenblas and a solve call returned the right answer.

CMake

The CMake build runs fypp for you. With the Python tools installed (pip install fypp cmake ninja), FetchContent can pin the tag:

cmake_minimum_required(VERSION 3.20)
project(heatflow_stdlib LANGUAGES Fortran)

include(FetchContent)
FetchContent_Declare(
  fortran_stdlib
  GIT_REPOSITORY https://github.com/fortran-lang/stdlib.git
  GIT_TAG v0.8.1
)
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(fortran_stdlib)

add_executable(version_check version_check.f90)
target_link_libraries(version_check PRIVATE fortran_stdlib::fortran_stdlib)

This configured, built (588 build steps) and ran in the container; it found the container’s OpenBLAS and reported "Found external BLAS" and "Found external LAPACK". The alternative is to build once and install (cmake -S . -B build, cmake --build build, cmake --install build with -DCMAKE_INSTALL_PREFIX=...) and use find_package. That route has a trap that was reproduced: stdlib records the BLAS and LAPACK it was configured with in its exported targets, so a consumer that only says find_package(fortran_stdlib) fails at configure time:

CMake Error at /work/inst/lib/cmake/fortran_stdlib/fortran_stdlib-targets.cmake:84 (set_target_properties):
  The link interface of target "fortran_stdlib::fortran_stdlib_blas"
  contains:

    BLAS::BLAS

  but the target was not found.

The fix is to find them first:

cmake_minimum_required(VERSION 3.20)
project(heatflow_stdlib LANGUAGES Fortran)

# stdlib's installed package refers to BLAS::BLAS and LAPACK::LAPACK when it was
# configured against an external BLAS/LAPACK, so the consumer has to find them first
find_package(BLAS REQUIRED)
find_package(LAPACK REQUIRED)
find_package(fortran_stdlib REQUIRED)

add_executable(version_check version_check.f90)
target_link_libraries(version_check PRIVATE fortran_stdlib::fortran_stdlib)

with -DCMAKE_PREFIX_PATH pointing at the install prefix. The build options that matter, from the README of the tag:

Option Effect

-DCMAKE_MAXIMUM_RANK=n

largest array rank the generated procedures support; default 4, minimum 4, 15 at most (7 for compilers without full Fortran 2003 support); a rank of 15 needs several gigabytes of memory to compile

-DBUILD_SHARED_LIBS=on

link dynamically; default off

-DBUILD_TESTING=off

skip stdlib’s own tests

-DFIND_BLAS=off

do not look for an external BLAS and LAPACK; the internal ones are used

-DWITH_ILP64=True

also build the 64-bit-integer BLAS and LAPACK interfaces

-DWITH_QP=ON/OFF, -DWITH_XDP=ON/OFF

generate quadruple and extended precision code; auto-detected

-DSTDLIB_IO=Off, -DSTDLIB_LOGGER=Off, -DSTDLIB_STATS=Off, -DSTDLIB_SYSTEM=Off, …​

leave out optional modules (STDLIB_ANSI, STDLIB_BITSETS, STDLIB_HASHMAPS, STDLIB_IO, STDLIB_LINALG_ITERATIVE, STDLIB_LOGGER, STDLIB_QUADRATURE, STDLIB_SPECIALMATRICES, STDLIB_STATS, STDLIB_STRINGLIST, STDLIB_SYSTEM)

The build configuration is visible in your program. The same example, printing the kind constants from stdlib_kinds, gave qp = -1 with the fpm route and qp = 16 with the CMake route on the same compiler and machine, because CMake auto-detected quadruple precision and the fpm route did not enable it (the fypp deployment script has --with_qp for that). Code that uses qp or xdp has to be written for the value -1, which means "not available" (the spec of stdlib_kinds says so), and the kind of the stdlib you linked is part of your build, not of the stdlib version. Make-based projects use pkg-config --cflags --libs fortran_stdlib or -lfortran_stdlib with the -I and -L paths of the install prefix, as the README describes (not run here).

A tour of the modules

The 0.8.1 source tree contains the modules below. The Status column is taken from the spec pages of the tag; the last column links the specification, and the web pages at https://stdlib.fortran-lang.org/page/specs/index.html track the development branch, which can be ahead of the release.

Module Purpose Status Spec

stdlib_kinds

kind constants sp, dp, xdp, qp (value -1 if unavailable) and re-exported int8 to int64 and logical kinds

experimental

stdlib_kinds

stdlib_optval

optval(x, default), the value of an optional argument or a fallback

experimental

stdlib_optval

stdlib_constants, stdlib_codata, stdlib_codata_type

mathematical and physical constants; all CODATA values as a derived type with value, uncertainty and unit

experimental

stdlib_constants

stdlib_ascii

NUL to DEL control characters, digits, letters, is_alpha, is_digit and friends, to_upper, to_lower, to_title, to_sentence, reverse for default characters

experimental

stdlib_ascii

stdlib_strings

strip, chomp, starts_with, ends_with, slice, find, replace_all, padl, padr, zfill, count, join, to_string, to_c_char

experimental

stdlib_strings

stdlib_string_type

string_type, a deferred-length string with the intrinsic string procedures and operators overloaded, and formatted and unformatted I/O

experimental

stdlib_string_type

stdlib_stringlist_type

stringlist_type, a one-dimensional list of strings with forward (fidx) and backward (bidx) indexing

experimental

stdlib_stringlist_type

stdlib_str2num

to_num, to_num_from_stream: text to integer or real of the kind of a mold argument

experimental

stdlib_str2num

stdlib_io and stdlib_io_npy

loadtxt, savetxt, open with mode strings, get_line, get_file, format constants such as FMT_REAL_DP, and NumPy .npy files (load_npy, save_npy)

experimental

stdlib_io

stdlib_logger

logger_type, a global logger, severity levels, time stamps, log files and units

experimental

stdlib_logger

stdlib_error

check, error_stop, and state_type, the fixed-size error state used by the other modules

experimental

stdlib_error

stdlib_system

run and monitor external processes (run, runasync, wait, kill), file-system and path procedures (is_directory, make_directory_all, delete_file, join_path, base_name, get_cwd), OS detection and sleep

experimental

stdlib_system

stdlib_ansi

ANSI escape sequences for colours and styles, combined with + and //

experimental

stdlib_ansi

stdlib_version

get_stdlib_version, stdlib_version_string, stdlib_version_compact

experimental

stdlib_version

stdlib_sorting

sort (introsort), ord_sort (stable merge-insertion hybrid), radix_sort, sort_index, sort_adjoint

experimental

stdlib_sorting

stdlib_selection

select and arg_select: the k-th smallest element without a full sort

experimental

stdlib_selection

stdlib_array

trueloc, falseloc: index lists of the true or false elements of a mask

experimental

stdlib_array

stdlib_hashmaps, stdlib_hashmap_wrappers

chaining_hashmap_type and open_hashmap_type, key_type for integer-vector and character keys, class(*) values

experimental

stdlib_hashmaps

stdlib_hash_32bit, stdlib_hash_64bit

FNV-1, FNV-1a, Fibonacci, universal multiplicative, NM, Water, Pengy and SpookyV2 hash functions

experimental

hash procedures

stdlib_bitsets

bitset_64 and bitset_large with set, clear, flip, test, count and logical operations, bit positions from 0

experimental

stdlib_bitsets

stdlib_math

linspace, logspace, arange, clip, swap, gcd, diff, meshgrid, is_close, all_close, deg2rad, rad2deg, arg, argd, argpi

experimental

stdlib_math

stdlib_linalg and stdlib_linalg_state

solve, lstsq, det, inv, eig, eigh, svd, qr, schur, cholesky, pinv, norms, expm, eye, diag, trace, products, and linalg_state_type

19 stable, the rest experimental

stdlib_linalg

stdlib_linalg_blas, stdlib_linalg_lapack

the full reference BLAS and LAPACK for all real and complex kinds (and optionally 64-bit integers), with optional external replacement

experimental

README, BLAS and LAPACK

stdlib_linalg_iterative_solvers

conjugate gradient, preconditioned CG and BiCGSTAB for dense or CSR systems

experimental

iterative solvers

stdlib_sparse

COO, CSR, CSC, ELLPACK and SELL-C matrices, conversions, spmv

experimental

stdlib_sparse

stdlib_specialmatrices

tridiagonal matrix types with spmv, dense, transposition and operators

experimental

stdlib_specialmatrices

stdlib_quadrature

trapz, simps, their weights, gauss_legendre, gauss_legendre_lobatto

experimental

stdlib_quadrature

stdlib_random

random_seed and dist_rand for the distributions' generator

experimental

stdlib_random

stdlib_stats

mean, median, var, cov, corr, moment with dim and mask

experimental

stdlib_stats

stdlib_stats_distribution_uniform, _normal, _exponential

random variates (rvs_*), densities (pdf_*) and distribution functions (cdf_*), plus shuffle

experimental

normal

stdlib_specialfunctions, stdlib_specialfunctions_gamma

Legendre polynomials, neural-network activation functions and their derivatives, gamma, log_gamma, incomplete gamma functions

experimental

gamma

stdlib_intrinsics

stdlib_sum, stdlib_sum_kahan, stdlib_dot_product, stdlib_dot_product_kahan, stdlib_matmul: more accurate or faster replacements for the intrinsics

experimental

stdlib_intrinsics

Every example below includes the version information it was built against, so each output can be reproduced. The Source line under each example links the specification page of the tag, and the example sources in the stdlib repository (example/<module>/) are worth reading as short, tested usage snippets.

Kinds, version, optval and constants

Almost every other module takes its kinds from stdlib_kinds, so it is the first import. get_stdlib_version and the stdlib_version_string constant tell a program which release it was built against; the spec recommends using the getter at run time because a dynamically linked library can differ from the constants of the headers you compiled with. optval replaces the three-line if (present(x)) idiom for optional arguments:

program kinds_version_optval
  use stdlib_kinds, only: sp, dp, qp, xdp, int32, int64
  use stdlib_version, only: get_stdlib_version, stdlib_version_string, stdlib_version_compact
  use stdlib_optval, only: optval
  use stdlib_constants, only: pi => PI_dp, stefan_boltzmann
  implicit none
  integer :: major, minor, patch
  character(len=:), allocatable :: version

  call get_stdlib_version(major, minor, patch, version)
  print '(a, i0, a, i0, a, i0, a, a)', 'stdlib ', major, '.', minor, '.', patch, ' = ', version
  print '(a, a, a, i0)', 'compile-time string ', stdlib_version_string, ', compact ', stdlib_version_compact

  print '(a, 4(i0, 1x))', 'sp dp qp xdp = ', sp, dp, qp, xdp
  print '(a, 2(i0, 1x))', 'int32 int64  = ', int32, int64

  print '(a, f8.6)', 'diffusion length, default scale  ', diffusion_length(0.01_dp, 4.0_dp)
  print '(a, f8.6)', 'diffusion length, scale = 2      ', diffusion_length(0.01_dp, 4.0_dp, 2.0_dp)
  print '(a, es12.5)', 'Stefan-Boltzmann constant        ', stefan_boltzmann
  print '(a, f0.10)', 'PI_dp                            ', pi

contains

  pure function diffusion_length(alpha, t, scale) result(l)
    real(dp), intent(in) :: alpha, t
    real(dp), intent(in), optional :: scale
    real(dp) :: l
    l = optval(scale, 1.0_dp) * sqrt(4.0_dp * alpha * t)
  end function diffusion_length

end program kinds_version_optval

Output:

stdlib 0.8.1 = 0.8.1
compile-time string 0.8.1, compact 801
sp dp qp xdp = 4 8 -1 -1
int32 int64  = 4 8
diffusion length, default scale  0.400000
diffusion length, scale = 2      0.800000
Stefan-Boltzmann constant         5.67037E-08
PI_dp                            3.1415926536

Flang 23 printed the same eight lines. qp and xdp are -1 here, which is stdlib’s way to say the kind was not generated in this build (see the CMake discussion above). optval is generic over the intrinsic types; its second argument must have the same type and kind as the first. stdlib_constants gives PI_sp, PI_dp, …​ and the main CODATA constants in double precision (c, h, k, N_A, sigma/stefan_boltzmann, …​), and stdlib_codata has every CODATA constant as a codata_constant_type that converts with to_real to the precision you ask for. Short constant names such as c and e can collide with your own names, which is why the spec suggests renaming at the use, as in use stdlib_constants, only: clight => c.

Text input and output: stdlib_io

loadtxt and savetxt read and write a rank-2 array of real, complex or integer as a delimited text file, one array row per line. Writing uses the default format constant of the kind, which for real(dp) is FMT_REAL_DP, (es24.16e3), as the file below shows. open takes a Python-like mode string ('r', 'w', 'x', 'a', '+', 'b', 't') and returns the unit, get_line reads a whole line into a deferred-length string, get_file reads a whole file, and save_npy/load_npy exchange arrays of any rank with NumPy (they live in stdlib_io_npy, not in stdlib_io, in 0.8.1):

program stdlib_io_demo
  use stdlib_kinds, only: dp
  use stdlib_io, only: loadtxt, savetxt, open, get_line, get_file, FMT_REAL_DP
  use stdlib_io_npy, only: load_npy, save_npy
  implicit none
  integer, parameter :: nx = 5, ny = 3
  real(dp) :: u(nx, ny)
  real(dp), allocatable :: v(:, :), w(:, :)
  character(len=:), allocatable :: line, text
  integer :: i, j, unit, stat

  ! a small temperature field: u(i, j) = 10*(i-1) + (j-1)
  do j = 1, ny
    do i = 1, nx
      u(i, j) = 10.0_dp * real(i - 1, dp) + real(j - 1, dp)
    end do
  end do

  ! text round trip: one array row per line, columns separated by ';'
  call savetxt('field.csv', u, delimiter=';')
  call loadtxt('field.csv', v, delimiter=';')
  print '(a, 2(1x, i0), a, es9.2)', 'loaded shape', shape(v), ', max |u - v| = ', maxval(abs(u - v))

  ! skiprows and max_rows select a block of rows
  call loadtxt('field.csv', w, skiprows=1, max_rows=2, delimiter=';')
  print '(a, 2(1x, i0))', 'rows 2..3 shape', shape(w)

  ! open() with a mode string returns a unit; get_line reads a whole line
  unit = open('field.csv', 'r', iostat=stat)
  print '(a, i0)', 'open iostat = ', stat
  call get_line(unit, line, iostat=stat)
  print '(a, a)', 'first line: ', line
  close(unit)

  ! get_file reads the entire file into one allocatable string
  call get_file('field.csv', text)
  print '(a, i0, a)', 'file holds ', len(text), ' characters'

  ! .npy: binary and self-describing, any rank, readable by NumPy
  call save_npy('field.npy', u)
  call load_npy('field.npy', v)
  print '(a, es9.2)', 'npy round trip, max |u - v| = ', maxval(abs(u - v))

  ! format constant that prints a real(dp) with full precision
  print FMT_REAL_DP, 1.0_dp / 3.0_dp

  ! missing file with iostat, instead of the default error stop
  unit = open('does-not-exist.csv', 'r', iostat=stat)
  print '(a, l1)', 'missing file reported through iostat: ', stat /= 0
end program stdlib_io_demo

Output:

loaded shape 5 3, max |u - v| =  0.00E+00
rows 2..3 shape 2 3
open iostat = 0
first line:  0.0000000000000000E+000; 1.0000000000000000E+000; 2.0000000000000000E+000
file holds 375 characters
npy round trip, max |u - v| =  0.00E+00
 3.3333333333333331E-001
missing file reported through iostat: T

The file field.csv that savetxt wrote is five lines of three columns, the first being

 0.0000000000000000E+000; 1.0000000000000000E+000; 2.0000000000000000E+000

Flang 23 printed the same output. Two behaviours are worth knowing. skiprows and max_rows select rows, not bytes, so skiprows=1, max_rows=2 gives rows 2 and 3, a 2 x 3 array. And a procedure that can fail takes an iostat argument; without it, a missing file stops the program, which is why the last line of the example passes iostat. The array is transposed relative to the file only in the sense that Fortran stores columns contiguously: savetxt writes u(i, :) as line i, the rows of the array, not the columns that are contiguous in memory.

Linear algebra: stdlib_linalg

stdlib_linalg wraps LAPACK behind generic, allocation-friendly interfaces: you write x = solve(a, b) instead of declaring workspace and calling dgesv. Every procedure has two forms. The plain form returns a value and stops the program with error stop if something goes wrong (a singular matrix, a size mismatch); the expert form takes a final err argument of type linalg_state_type and never stops, it reports the problem in that argument (the function is then no longer pure). The example does one implicit (backward Euler) step of one-dimensional heat flow, (I + r K) u_new = u_old, with the second-difference matrix K = tridiag(-1, 2, -1) and r = 0.4, and then uses the other decompositions on the same matrix:

program stdlib_linalg_demo
  use stdlib_kinds, only: dp
  use stdlib_linalg, only: solve, det, inv, eye, diag, trace, norm, eigvalsh, svdvals, cholesky, lstsq
  use stdlib_linalg_state, only: linalg_state_type
  implicit none
  integer, parameter :: n = 5
  real(dp), parameter :: r = 0.4_dp          ! alpha * dt / dx**2
  real(dp) :: a(n, n), u(n), l(n, n), c(n, n)
  real(dp) :: lambda(n), sigma(n), xs(6), ys(6), design(6, 2)
  real(dp), allocatable :: x(:), fit(:)
  type(linalg_state_type) :: err
  integer :: i

  ! One implicit (backward Euler) step of 1D heat flow: (I + r*K) u_new = u_old,
  ! where K = tridiag(-1, 2, -1) is the discrete second difference.
  a = (2.0_dp * r + 1.0_dp) * eye(n)
  do i = 1, n - 1
    a(i, i + 1) = -r
    a(i + 1, i) = -r
  end do
  u = [0.0_dp, 0.0_dp, 100.0_dp, 0.0_dp, 0.0_dp]

  x = solve(a, u)                               ! LAPACK *GESV underneath
  print '(a, 5f9.4)', 'u_new         =', x
  print '(a, es10.2)', '|A u_new - u|_2 = ', norm(matmul(a, x) - u, 2)
  print '(a, f9.4)', 'heat kept     =', sum(x)

  print '(a, f12.6)', 'det(A)        = ', det(a)
  print '(a, f12.6)', 'trace(A)      = ', trace(a)
  print '(a, es10.2)', '|A * inv(A) - I|_max = ', maxval(abs(matmul(a, inv(a)) - eye(n)))

  lambda = eigvalsh(a)                          ! symmetric: ascending eigenvalues
  print '(a, 5f8.4)', 'eigenvalues   =', lambda
  sigma = svdvals(a)                            ! descending singular values
  print '(a, 5f8.4)', 'cond_2(A)     =', sigma(1) / sigma(n)

  call cholesky(a, c, lower=.true.)             ! A = L L^T
  l = c
  print '(a, es10.2)', '|L L^T - A|_max = ', maxval(abs(matmul(l, transpose(l)) - a))
  print '(a, 5f8.4)', 'diag(L)       =', diag(l)

  ! least squares through the same module: fit T(x) = p + q x to noisy samples
  xs = [0.0_dp, 1.0_dp, 2.0_dp, 3.0_dp, 4.0_dp, 5.0_dp]
  ys = [1.1_dp, 2.9_dp, 5.2_dp, 7.1_dp, 8.8_dp, 11.2_dp]
  design(:, 1) = 1.0_dp
  design(:, 2) = xs
  fit = lstsq(design, ys)
  print '(a, 2f8.4)', 'fit (p, q)    =', fit

  ! expert interface: a singular matrix reports an error instead of error stop
  a = 0.0_dp
  x = solve(a, u, err=err)
  print '(a, l1)', 'singular matrix flagged: ', err%error()
  print '(a)', err%print()
end program stdlib_linalg_demo

Output:

u_new         =   3.2206  14.4928  61.9968  14.4928   3.2206
|A u_new - u|_2 =   3.09E-15
heat kept     =  97.4235
det(A)        =    15.301440
trace(A)      =     9.000000
|A * inv(A) - I|_max =   2.22E-16
eigenvalues   =  1.1072  1.4000  1.8000  2.2000  2.4928
cond_2(A)     =  2.2515
|L L^T - A|_max =   2.22E-16
diag(L)       =  1.3416  1.3081  1.3063  1.3062  1.3062
fit (p, q)    =  1.0429  2.0029
singular matrix flagged: T
[solve] returned Algebra Error: singular matrix

The same output was produced by Flang 23. Details that matter in practice:

  • eigvalsh returns the eigenvalues of a symmetric matrix in ascending order and svdvals the singular values in descending order, so the 2-norm condition number of a symmetric positive definite matrix is sigma(1) / sigma(n), here 2.2515.

  • cholesky(a, c, lower=.true.) returns the triangular factor in c and leaves a alone. With a single argument, call cholesky(a, lower=.true.), the factor overwrites a.

  • lstsq(design, ys) solves the least-squares problem through LAPACK, here fitting p + q x to six samples.

  • The second solve call uses the expert interface on a zero matrix. err%error() is true, err%print() returns [solve] returned Algebra Error: singular matrix, and the program continues. The first form, with no err, would have stopped with that message.

  • eye(n) returns a real(real64) identity by default; its optional mold argument selects another type and kind.

  • matmul(a, x) in the residual is the intrinsic; the module stdlib_intrinsics has stdlib_matmul and stdlib_dot_product variants of the intrinsics for when their summation order matters.

The Cholesky factorization as a numerical concept, and when to prefer BLAS-level calls, are in BLAS, LAPACK and Numerical Libraries.

Logging: stdlib_logger

stdlib_logger provides a logger_type and a ready-made global_logger. A logger has a threshold (all_level, debug_level, information_level, warning_level, error_level, io_error_level, text_error_level, none_level; the default is information_level), an optional time stamp, optional indentation and maximum width, and a list of units to write to. While that list is empty the messages go to output_unit. Messages below the threshold are ignored. The example sends the global logger to the terminal and a local logger to a file:

program stdlib_logger_demo
  use stdlib_logger, only: logger_type, global_logger, debug_level, warning_level
  implicit none
  type(logger_type) :: run_log
  integer :: unit, stat, step

  ! The global logger writes to output_unit while its log_units list is empty.
  call global_logger%configure(time_stamp=.false.)
  call global_logger%log_information('heatflow started', module='demo', procedure='main')

  ! A local logger with its own file; messages below the level are ignored.
  call run_log%configure(time_stamp=.false., level=warning_level)
  call run_log%add_log_file('run.log', unit, stat=stat)
  print '(a, i0, a, i0)', 'log files opened: ', run_log%log_units_assigned(), ', stat = ', stat

  do step = 1, 3
    call run_log%log_debug('step detail')                 ! dropped: below warning_level
    call run_log%log_information('step finished')         ! dropped: below warning_level
  end do
  call run_log%log_warning('residual still large', module='solver', procedure='run')
  call run_log%log_error('did not converge in 3 steps', module='solver', procedure='run', stat=1, errmsg='max_steps reached')

  ! lower the threshold: everything is logged from now on
  call run_log%configure(level=debug_level)
  call run_log%log_debug('now visible')
  call run_log%log_message('a bare message without a label')

  call run_log%remove_log_unit(unit, close_unit=.true., stat=stat)
  print '(a, i0)', 'units left: ', run_log%log_units_assigned()

  ! echo the file content to show what was recorded
  call echo('run.log')

contains

  subroutine echo(file)
    character(len=*), intent(in) :: file
    character(len=200) :: line
    integer :: u, ios
    open(newunit=u, file=file, status='old', action='read')
    do
      read(u, '(a)', iostat=ios) line
      if (ios /= 0) exit
      print '(2a)', '| ', trim(line)
    end do
    close(u)
  end subroutine echo

end program stdlib_logger_demo

Output:

demo % main: INFO: heatflow started
log files opened: 1, stat = 0
units left: 0
| solver % run: WARN: residual still large
| solver % run: ERROR: did not converge in 3 steps
| With stat = 1
| With errmsg = "max_steps reached"
| DEBUG: now visible
| a bare message without a label

The lines starting with | are the content of run.log, printed by the echo routine. The first line came from the global logger (time stamps were turned off to make the output reproducible; by default every message is preceded by yyyy-mm-dd hh:mm:ss.sss). The two log_debug/log_information calls inside the loop never reached the file because the level was warning_level; after configure(level=debug_level) the debug message appears. log_error appends stat and errmsg when they are given, log_io_error does the same with iostat and iomsg, and log_text_error marks a column in a line of text. Flang 23 printed the same output. For parallel programs the spec advises one logger file per process, and a guarding region (for example an OpenMP critical) around the calls for threads.

Strings and text

Fortran’s character(len=*) variables are fixed length, and the intrinsics (trim, adjustl, index, scan, verify, repeat) are low level; see Characters and Strings. stdlib adds three layers on top. stdlib_strings has functions over default characters (and string_type) that return allocatable results. stdlib_string_type has the derived type string_type, with the intrinsic procedures, relational operators, // and formatted I/O (through user-defined derived-type I/O, written dt) overloaded for it. stdlib_stringlist_type is a list of them.

program stdlib_strings_demo
  use stdlib_kinds, only: dp
  use stdlib_strings, only: strip, chomp, starts_with, ends_with, slice, find, replace_all, &
                            count, join, to_string
  use stdlib_string_type, only: string_type, assignment(=), operator(//), operator(==), &
                                len, to_upper, write(formatted)
  implicit none
  character(len=*), parameter :: line = '   output = "heat_0042.csv"   '
  character(len=:), allocatable :: name
  type(string_type) :: s, t
  integer :: step

  ! stdlib_strings: functions over default character and string_type
  name = strip(line)
  print '(3a)', '[', name, ']'
  print '(3a)', '[', chomp('field.csv', '.csv'), ']'
  print '(l1, 1x, l1)', starts_with(name, 'output'), ends_with(name, '.csv"')
  print '(a)', slice('abcdefghij', 2, 6, 2)
  print '(i0)', find('heat_0042_0042.csv', '0042', 2)
  print '(i0)', count('heat_0042_0042.csv', '_')
  print '(a)', replace_all('heat_0042.csv', 'heat', 'flux')
  print '(a)', join(['nx', 'ny', 'lx'], ', ')

  step = 42
  print '(a)', 'file heat_' // to_string(step, '(i0.6)') // '.csv'
  print '(a)', to_string(3.14159_dp, '(f8.3)')
  print '(a)', to_string(.true.)

  ! string_type: a deferred-length string with the intrinsic string procedures overloaded
  s = 'Heat'
  t = s // ' flow'
  print '(dt)', t
  print '(i0)', len(t)
  print '(l1)', t == 'Heat flow'
  t = to_upper(t)
  write (*, '(dt)') t
end program stdlib_strings_demo

Output:

[output = "heat_0042.csv"]
[field]
T T
bdf
11
2
flux_0042.csv
nx, ny, lx
file heat_000042.csv
   3.142
T
Heat flow
9
T
HEAT FLOW

strip removes leading and trailing blanks, chomp removes trailing characters of a set or a trailing substring (trailing whitespace when none is given), slice(s, first, last, stride) extracts with a stride (so slice('abcdefghij', 2, 6, 2) is bdf), find(s, pattern, occurrence) returns the position of the n-th occurrence (the second 0042 starts at position 11), count counts overlapping occurrences unless consider_overlapping=.false., join concatenates with a separator, and to_string(value, format) formats an integer, real, complex or logical without the character(len=32) and write dance of an internal file; a format that does not fit the type produces [*]. To pad a number with zeros the format '(i0.6)' does the job. Flang 23 printed identical output.

padl, padr and zfill are the three procedures that pad a string to a width, with a fill character (padl('7', 4, '0')), blanks, or zeros. They work, but under GNU Fortran 16.2 and -Wall the use statement that imports any of them prints a warning about stdlib’s own module, five times for this three-name import:

program stdlib_padding_demo
  use stdlib_strings, only: padl, padr, zfill
  implicit none
  print '(3a)', '[', padl('7', 4, '0'), ']'
  print '(3a)', '[', padr('ny', 6), ']'
  print '(3a)', '[', zfill('42', 6), ']'
end program stdlib_padding_demo
05b_padding.f90:2:6:

    2 |   use stdlib_strings, only: padl, padr, zfill
      |      1
Warning: Type specified for intrinsic function 'len' at (1) is ignored [-Wsurprising]

followed by the correct output

[0007]
[ny    ]
[000042]

The warning comes from the interface of those procedures in the module file (they have a dummy named len), the program is correct, and Flang 23 prints no warning. With GNU Fortran you can silence it with -Wno-surprising for those compilation units; it does not appear for the other stdlib_strings procedures.

stdlib_ascii classifies and converts single characters and strings, stdlib_str2num converts text to numbers whose type and kind are those of a mold argument, and stringlist_type keeps fields. The next example splits a tab-separated configuration record by hand, since stdlib_strings has no split in this release, and parses the values:

program stdlib_text_demo
  use stdlib_kinds, only: sp, dp, int32
  use stdlib_ascii, only: is_alpha, is_digit, is_white, is_punctuation, to_upper, to_lower, to_title, &
                          reverse, TAB, digits
  use stdlib_str2num, only: to_num
  use stdlib_stringlist_type, only: stringlist_type, fidx, bidx
  use stdlib_string_type, only: string_type, write(formatted)
  implicit none
  character(len=*), parameter :: record = 'nx=64'//TAB//'ny=32'//TAB//'alpha=1.0e-2'
  type(stringlist_type) :: keys
  type(string_type) :: first
  integer :: i, nd, start
  integer :: n(2)
  real(dp) :: alpha

  ! character classification, one character at a time
  nd = 0
  do i = 1, len(record)
    if (is_digit(record(i:i))) nd = nd + 1
  end do
  print '(a, i0, a, l1, l1, l1)', 'digits in record: ', nd, '; classes of "a", TAB, ",": ', &
    is_alpha('a'), is_white(TAB), is_punctuation(',')
  print '(2a)', 'digit set: ', digits

  ! case conversion on default characters
  print '(a)', to_upper('heatflow')
  print '(a)', to_lower('HEATFLOW')
  print '(a)', to_title('heat flow solver')
  print '(a)', reverse('abc')

  ! to_num converts text to the type and kind of the mold, without an internal read
  n(1) = to_num('64', 1_int32)
  n(2) = to_num('32', 1_int32)
  alpha = to_num('1.0e-2', 1.0_dp)
  print '(a, 2i4, a, es10.3)', 'parsed n =', n, ', alpha =', alpha
  print '(a, f8.3)', 'single precision:', to_num('0.125', 1.0_sp)

  ! split a tab separated record by hand and keep the fields in a stringlist
  start = 1
  do i = 1, len(record) + 1
    if (i > len(record)) then
      call keys%insert_at(bidx(1), record(start:i - 1))
    else if (record(i:i) == TAB) then
      call keys%insert_at(bidx(1), record(start:i - 1))
      start = i + 1
    end if
  end do
  print '(a, i0)', 'fields: ', keys%len()
  first = keys%get(fidx(1))
  print '(dt)', first
  first = keys%get(bidx(1))
  print '(dt)', first
end program stdlib_text_demo

Output:

digits in record: 7; classes of "a", TAB, ",": TTT
digit set: 0123456789
HEATFLOW
heatflow
Heat Flow Solver
cba
parsed n =  64  32, alpha = 1.000E-02
single precision:   0.125
fields: 3
nx=64
alpha=1.0e-2

to_num is a hand-written converter, not an internal read; the spec notes the accuracy it reaches for each kind (an exact match for sp, up to epsilon(0.0_dp) for dp), so use a formatted read where the last bit matters. stringlist_type is indexed with fidx(i) (counted from the front, starting at 1) and bidx(i) (from the back, starting at 1); insert_at(bidx(1), x) appends. get with an index outside the list returns an empty string instead of failing, as the spec describes. Flang 23 printed identical output.

Sorting and selection

stdlib_sorting has five generic subroutines over rank-1 arrays. sort, ord_sort, sort_index and sort_adjoint accept the integer kinds, real, character(*), string_type and the bitset types (see each spec for the exact list); radix_sort takes integers and reals only. sort is an introsort and is not stable. ord_sort is a stable hybrid of merge and insertion sort that is fast on data that is partly ordered already. radix_sort is a least-significant-digit radix sort. sort_index and sort_adjoint sort and also tell you the permutation or carry a second array along. The index kind is int_index, which is int64. Every one takes reverse for descending order. stdlib_selection finds the k-th smallest value without sorting everything:

program stdlib_sorting_demo
  use stdlib_kinds, only: dp, int32
  use stdlib_sorting, only: sort, ord_sort, sort_index, radix_sort, sort_adjoint, int_index
  use stdlib_selection, only: select, arg_select
  implicit none
  real(dp) :: t(8), w(8), median
  integer(int_index) :: order(8)
  integer(int32) :: id(8)
  integer :: k, kth
  integer :: idx(8)

  t = [310.5_dp, 295.0_dp, 301.2_dp, 295.0_dp, 330.9_dp, 288.4_dp, 301.2_dp, 305.0_dp]
  id = [1_int32, 2_int32, 3_int32, 4_int32, 5_int32, 6_int32, 7_int32, 8_int32]

  w = t
  call sort(w)                          ! introsort, not stable
  print '(a, 8f7.1)', 'sort            ', w
  w = t
  call sort(w, reverse=.true.)
  print '(a, 8f7.1)', 'sort, reverse   ', w

  w = t
  call ord_sort(w)                      ! stable merge sort, fast on partly ordered data
  print '(a, 8f7.1)', 'ord_sort        ', w

  w = t
  call sort_index(w, order)             ! sorted values plus the permutation
  print '(a, 8f7.1)', 'sort_index      ', w
  print '(a, 8i7)', 'permutation     ', order

  w = t
  call sort_adjoint(w, id)              ! carry a second array along
  print '(a, 8i7)', 'ids sorted by T ', id

  call radix_sort(w)                    ! integers and reals only
  print '(a, l1)', 'radix_sort keeps sorted data sorted: ', all(w(1:7) <= w(2:8))

  ! selection: the 4th smallest value without sorting everything
  w = t
  call select(w, 4, median)             ! select reorders its argument
  print '(a, f6.1)', '4th smallest    ', median
  idx = [(k, k = 1, 8)]
  call arg_select(t, idx, 4, kth)       ! arg_select leaves the data alone
  print '(a, i0, a, f6.1)', 'its index       ', kth, ', value ', t(kth)
end program stdlib_sorting_demo

Output:

sort              288.4  295.0  295.0  301.2  301.2  305.0  310.5  330.9
sort, reverse     330.9  310.5  305.0  301.2  301.2  295.0  295.0  288.4
ord_sort          288.4  295.0  295.0  301.2  301.2  305.0  310.5  330.9
sort_index        288.4  295.0  295.0  301.2  301.2  305.0  310.5  330.9
permutation           6      2      4      3      7      8      1      5
ids sorted by T       6      2      4      3      7      8      1      5
radix_sort keeps sorted data sorted: T
4th smallest     301.2
its index       7, value  301.2

Flang 23 printed identical output. The permutation 6 2 4 3 7 8 1 5 shows the stable sort at work for the ties: 295.0 occurs at positions 2 and 4 and 301.2 at positions 3 and 7, and their original order is kept. Be careful with select: it reorders its argument (the example works on the copy w), while arg_select leaves the data alone and partially sorts the index array idx, which must contain a permutation of 1:size(array) of the same integer kind as k. With equal values, arg_select may return either index (here 7 rather than 3), because its partial sort is not stable.

Hash maps and bitsets

Fortran has no built-in dictionary. stdlib_hashmaps provides two implementations of one abstract hashmap_type: chaining_hashmap_type (separate chaining) and open_hashmap_type (open addressing). Keys are a key_type, built from an integer vector, a character string or raw bytes (set(key, ...)), and the helper map_entry and get_other_data accept those arguments directly. The associated value is class(*), so you test its dynamic type with select type. stdlib_bitsets provides bitset_64 and bitset_large, compact sets of flags whose positions start at 0:

program stdlib_containers_demo
  use stdlib_kinds, only: dp
  use stdlib_hashmaps, only: chaining_hashmap_type, open_hashmap_type
  use stdlib_hashmap_wrappers, only: key_type, set, get
  use stdlib_bitsets, only: bitset_large
  implicit none
  type(chaining_hashmap_type) :: registry
  type(open_hashmap_type) :: seen
  type(key_type), allocatable :: keys(:)
  class(*), allocatable :: value
  character(len=:), allocatable :: name
  logical :: conflict, exists
  type(bitset_large) :: fixed
  integer :: i, nx

  ! a map from solver names to relaxation factors; values are class(*)
  call registry%init(slots_bits=6)
  call registry%map_entry('jacobi', 1.0_dp, conflict)
  call registry%map_entry('ftcs', 0.25_dp, conflict)
  call registry%map_entry('jacobi', 2.0_dp, conflict)        ! duplicate key
  print '(a, l1, a, i0)', 'duplicate rejected: ', conflict, ', entries = ', registry%entries()

  call registry%get_other_data('ftcs', value, exists)
  select type (value)
  type is (real(dp))
    print '(a, f5.2, a, l1)', 'ftcs -> ', value, ' found: ', exists
  end select

  call registry%set_other_data('ftcs', 0.20_dp)
  call registry%get_all_keys(keys)
  print '(a, i0)', 'keys stored: ', size(keys)
  do i = 1, size(keys)
    call get(keys(i), name)
    print '(2a)', '  key ', name
  end do
  call registry%remove('jacobi', exists)
  print '(a, l1, a, i0)', 'removed: ', exists, ', entries now ', registry%entries()

  ! the open-addressing variant has the same API
  call seen%init()
  call seen%map_entry([3, 4], conflict=conflict)
  call seen%key_test([3, 4], exists)
  print '(a, l1)', 'integer key present: ', exists

  ! a bitset marking the fixed (boundary) cells of a 12-cell row (bit positions start at 0)
  nx = 12
  call fixed%init(nx)
  call fixed%set(0)
  call fixed%set(nx - 1)
  print '(a, i0, a, i0, a)', 'fixed cells: ', fixed%bit_count(), ' of ', fixed%bits(), ' bits'
  print '(a, l1, l1)', 'bit 0 set, bit 4 set: ', fixed%test(0), fixed%test(4)
  call fixed%not()
  print '(a, i0)', 'interior cells after not: ', fixed%bit_count()
end program stdlib_containers_demo

Output:

duplicate rejected: T, entries = 2
ftcs ->  0.25 found: T
keys stored: 2
  key jacobi
  key ftcs
removed: T, entries now 1
integer key present: T
fixed cells: 2 of 12 bits
bit 0 set, bit 4 set: TF
interior cells after not: 10

Flang 23 printed identical output. map_entry with an existing key does not overwrite: it sets conflict to true and keeps the old value (the 1.0 for jacobi was not replaced by 2.0); use set_other_data to change a value, remove to delete, and get_all_keys for the key list, whose order the spec does not define. slots_bits makes the initial table 2**slots_bits slots, and loading(), entries() and num_slots() report its state. For a bitset, bit_count() is the number of set bits, bits() the size, not() flips every bit, and the spec says positions outside 0 to bits()-1 are ignored by the procedures that take a position, with an index_invalid_error status for the ones that report one.

Mathematics, statistics, random numbers and quadrature

program stdlib_numerics_demo
  use stdlib_kinds, only: dp, int32
  use stdlib_math, only: linspace, arange, clip, is_close, all_close, diff, deg2rad
  use stdlib_stats, only: mean, var, median, corr
  use stdlib_random, only: random_seed
  use stdlib_stats_distribution_normal, only: rvs_normal, pdf_normal, cdf_normal
  use stdlib_quadrature, only: trapz, simps, gauss_legendre
  use stdlib_specialfunctions_gamma, only: gamma, log_gamma
  implicit none
  integer, parameter :: n = 9
  real(dp) :: x(n), y(n), noisy(1000), nodes(5), weights(5)
  real(dp) :: pi_, pair(n, 2), c(2, 2)
  integer :: previous

  ! grids and sequences
  x = linspace(0.0_dp, 1.0_dp, n)
  print '(a, 9f6.3)', 'linspace        ', x
  print '(a, 5i3)', 'arange(1, 10, 2)', arange(1, 10, 2)
  print '(a, 3f6.2)', 'clip            ', clip(-0.5_dp, 0.0_dp, 1.0_dp), clip(0.4_dp, 0.0_dp, 1.0_dp), clip(1.7_dp, 0.0_dp, 1.0_dp)
  print '(a, l1, l1)', 'is_close, <=0   ', is_close(0.1_dp + 0.2_dp, 0.3_dp), abs(0.1_dp + 0.2_dp - 0.3_dp) <= 0.0_dp
  print '(a, 4f6.3)', 'diff            ', diff(x(1:5))

  ! a temperature profile and its statistics
  y = 100.0_dp * sin(deg2rad(180.0_dp) * x)
  print '(a, f8.3, a, f8.3, a, f8.3)', 'mean ', mean(y), ' var ', var(y), ' median ', median(y)
  pair(:, 1) = x
  pair(:, 2) = 100.0_dp * x**2
  c = corr(pair, 1)                     ! 2 x 2 correlation matrix of the columns
  print '(a, f8.4)', 'corr(x, 100 x^2)', c(1, 2)

  ! integrals of the profile: trapezoid, Simpson, Gauss-Legendre
  print '(a, f10.5)', 'trapz           ', trapz(y, x)
  print '(a, f10.5)', 'simps           ', simps(y, x)
  print '(a, f10.5)', 'exact           ', 200.0_dp / acos(-1.0_dp)
  call gauss_legendre(nodes, weights, [0.0_dp, 1.0_dp])
  pi_ = acos(-1.0_dp)
  print '(a, f10.5)', 'gauss_legendre  ', sum(100.0_dp * sin(pi_ * nodes) * weights)

  ! reproducible noise: fix the seed, then draw a normal sample
  call random_seed(12345_int32, previous)
  noisy = rvs_normal(10.0_dp, 2.0_dp, 1000)
  print '(a, f8.3, a, f8.3)', 'sample mean ', mean(noisy), ', sample sd ', sqrt(var(noisy))
  print '(a, f8.5, a, f8.5)', 'pdf at mean ', pdf_normal(10.0_dp, 10.0_dp, 2.0_dp), ', cdf at mean ', cdf_normal(10.0_dp, 10.0_dp, 2.0_dp)

  ! special functions
  print '(a, f10.5, f10.5)', 'gamma(5), gamma(0.5)', gamma(5.0_dp), gamma(0.5_dp)
  print '(a, f10.5)', 'log_gamma(10)   ', log_gamma(10.0_dp)
  print '(a, l1)', 'all_close       ', all_close(x, linspace(0.0_dp, 1.0_dp, n))
end program stdlib_numerics_demo

Output:

linspace         0.000 0.125 0.250 0.375 0.500 0.625 0.750 0.875 1.000
arange(1, 10, 2)  1  3  5  7  9
clip              0.00  0.40  1.00
is_close, <=0   TF
diff             0.125 0.125 0.125 0.125
mean   55.859 var 1489.702 median   70.711
corr(x, 100 x^2)  0.9622
trapz             62.84174
simps             63.67055
exact             63.66198
gauss_legendre    63.66198
sample mean    9.950, sample sd    2.054
pdf at mean  0.19947, cdf at mean  0.50000
gamma(5), gamma(0.5)  24.00000   1.77245
log_gamma(10)     12.80183
all_close       T

Flang 23 printed identical output, including the random sample: the generator is stdlib’s own (xoshiro256** in the source), seeded by random_seed(put, get) (put seeds the state, get returns a value of the seeded state), so a seed makes the sequence reproducible across compilers. A few points:

  • is_close(a, b) compares with a relative tolerance (default sqrt(epsilon) of the kind), so 0.1 + 0.2 and 0.3 are close even though 0.1_dp + 0.2_dp - 0.3_dp is not zero; all_close is the array form. Comparing reals with == is what -Wcompare-reals warns about.

  • mean, var, median, corr, cov and moment take dim and mask. var divides by n - 1 unless corrected=.false.. corr takes a rank-1 or rank-2 array and returns the correlation of its columns along dim, a 2 x 2 matrix in the example; there is no two-argument corr(x, y).

  • trapz(y, x) and simps(y, x) integrate sampled data (62.84 and 63.67 against the exact 63.66 for the half-sine); gauss_legendre(nodes, weights, interval) returns nodes and weights for any number of points, double precision only, so five points reach the exact value to the five decimals shown.

  • rvs_normal(loc, scale, n) returns n variates, pdf_normal and cdf_normal the density and distribution function. The generator for the normal and exponential variates is limited to double precision, according to the spec.

  • gamma and log_gamma of stdlib_specialfunctions_gamma extend the intrinsic functions, which take real arguments, to integer and complex arguments (the incomplete gamma family is in the same module).

Sparse operators and iterative solvers

Large diffusion problems give matrices that are almost all zeros. stdlib_sparse stores them in COO, CSR, CSC, ELLPACK or SELL-C format (CSR_dp_type, COO_dp_type, …​), builds them from coordinate triplets with from_ijv or from a dense matrix with dense2coo and coo2csr, and multiplies with spmv. stdlib_linalg_iterative_solvers solves a system without factorizing it, and stdlib_specialmatrices stores a tridiagonal matrix in three vectors. The example solves the steady problem -u'' = 1 with u = 0 at both ends on eight interior points:

program stdlib_sparse_demo
  use stdlib_kinds, only: dp
  use stdlib_array, only: trueloc
  use stdlib_sparse, only: CSR_dp_type, from_ijv, spmv
  use stdlib_specialmatrices, only: tridiagonal_dp_type, tridiagonal, dense, spmv
  use stdlib_linalg_iterative_solvers, only: stdlib_solve_cg
  implicit none
  integer, parameter :: n = 8
  real(dp), parameter :: h = 1.0_dp / real(n + 1, dp)
  integer :: row(3 * n - 2), col(3 * n - 2), k, i
  real(dp) :: val(3 * n - 2), b(n), x(n), y(n), res
  type(CSR_dp_type) :: a
  type(tridiagonal_dp_type) :: t
  real(dp), allocatable :: hot(:)

  ! steady 1D heat flow with uniform source: -u'' = 1, u(0) = u(1) = 0
  ! finite differences give the tridiagonal matrix tridiag(-1, 2, -1) / h**2
  k = 0
  do i = 1, n
    if (i > 1) then
      k = k + 1; row(k) = i; col(k) = i - 1; val(k) = -1.0_dp / h**2
    end if
    k = k + 1; row(k) = i; col(k) = i; val(k) = 2.0_dp / h**2
    if (i < n) then
      k = k + 1; row(k) = i; col(k) = i + 1; val(k) = -1.0_dp / h**2
    end if
  end do
  call from_ijv(a, row, col, val)          ! coordinate triplets to compressed sparse row
  print '(a, i0, a, i0, a, i0)', 'CSR: ', a%nrows, ' rows, ', a%nnz, ' stored entries instead of ', n * n

  b = 1.0_dp
  x = 0.0_dp
  call stdlib_solve_cg(a, b, x, rtol=1.0e-12_dp, restart=.false.)   ! conjugate gradients
  print '(a, 8f7.4)', 'u (CG)       =', x

  y = 0.0_dp
  call spmv(a, x, y)                       ! y = A x
  res = maxval(abs(y - b))
  print '(a, es9.2)', 'max |A u - b| =', res

  ! the exact solution u(x) = x (1 - x) / 2 at the grid points
  print '(a, 8f7.4)', 'exact        =', [(0.5_dp * real(i, dp) * h * (1.0_dp - real(i, dp) * h), i = 1, n)]

  ! the same operator as a tridiagonal special matrix: three vectors, no sparse structure needed
  t = tridiagonal(spread(-1.0_dp / h**2, 1, n - 1), spread(2.0_dp / h**2, 1, n), spread(-1.0_dp / h**2, 1, n - 1))
  y = 0.0_dp
  call spmv(t, x, y)
  print '(a, es9.2)', 'special matrix agrees: ', maxval(abs(y - b))
  print '(a, i0, a, i0)', 'dense() shape ', size(dense(t), 1), ' x ', size(dense(t), 2)

  ! trueloc turns a logical mask into the index list of its .true. elements
  hot = x(trueloc(x > 0.1_dp))
  print '(a, i0, a, 4f7.4)', 'cells above 0.1: ', size(hot), ' ', hot
end program stdlib_sparse_demo

Output:

CSR: 8 rows, 22 stored entries instead of 64
u (CG)       = 0.0494 0.0864 0.1111 0.1235 0.1235 0.1111 0.0864 0.0494
max |A u - b| = 0.00E+00
exact        = 0.0494 0.0864 0.1111 0.1235 0.1235 0.1111 0.0864 0.0494
special matrix agrees:  0.00E+00
dense() shape 8 x 8
cells above 0.1: 4  0.1111 0.1235 0.1235 0.1111

Flang 23 printed the same eight lines of output. Under GNU Fortran the link step also prints a linker warning, which is not part of the program output:

/usr/bin/ld: warning: .._stdlib-src_stdlib-fpm_src_stdlib_linalg_iterative_solvers_cg.f90.o: requires executable stack (because the .note.GNU-stack section is executable)

It comes from stdlib’s conjugate-gradient object file. The source assigns an internal procedure to a procedure pointer (op%matvec => matvec, with matvec defined after a contains inside the solver), which GNU Fortran implements with a code trampoline on the stack, hence the note. The program ran, but a hardened system that refuses executable stacks may not. The use statements import spmv from both stdlib_sparse and stdlib_specialmatrices: two modules can export a generic name and the generic interfaces are combined (J3/23-007r1, 15.4.3.4.1), which is why one spmv call can take a CSR matrix and another a tridiagonal_dp_type. The stdlib_solve_cg argument restart=.false. is the setting used in the spec’s own example; rtol and atol define the stopping test. trueloc(mask) turns a logical mask into the list of its true indices so that a vector subscript can select the elements.

Accurate sums and hashes

stdlib_intrinsics provides stdlib_sum, stdlib_sum_kahan, stdlib_dot_product, stdlib_dot_product_kahan and stdlib_matmul, which sum in chunks (the plain stdlib_sum) or with Kahan compensation. The hash modules provide non-cryptographic hash functions (FNV-1, FNV-1a, Fibonacci, universal multiplicative, NM, Water, Pengy, SpookyV2) for integer vectors and byte arrays, usable on their own or as the hasher of a hash map:

program stdlib_intrinsics_hash
  use stdlib_kinds, only: sp, dp
  use stdlib_intrinsics, only: stdlib_sum, stdlib_sum_kahan
  use stdlib_hash_32bit, only: fnv_1a_hash
  use stdlib_hash_64bit, only: fnv_1a_hash_64 => fnv_1a_hash
  implicit none
  integer, parameter :: n = 2000000
  real(sp), allocatable :: x(:)
  real(dp) :: reference
  integer :: i

  ! many small single-precision values: the naive running sum loses the small terms
  allocate (x(n))
  x = 1.0e-3_sp
  x(1) = 1.0e4_sp
  reference = 1.0e4_dp + real(n - 1, dp) * real(1.0e-3_sp, dp)
  print '(a, f14.4)', 'reference (dp)   ', reference
  print '(a, f14.4)', 'sum              ', sum(x)
  print '(a, f14.4)', 'stdlib_sum       ', stdlib_sum(x)
  print '(a, f14.4)', 'stdlib_sum_kahan ', stdlib_sum_kahan(x)

  ! non-cryptographic hashes of integer vectors and bytes, e.g. for cache keys
  print '(a, i0)', 'fnv_1a 32-bit    ', fnv_1a_hash([(i, i = 1, 8)])
  print '(a, i0)', 'fnv_1a 64-bit    ', fnv_1a_hash_64([(i, i = 1, 8)])
end program stdlib_intrinsics_hash

Output with GNU Fortran 16.2:

reference (dp)       11999.9991
sum                  11953.1240
stdlib_sum           11998.8975
stdlib_sum_kahan     11999.9990
fnv_1a 32-bit    -1706177971
fnv_1a 64-bit    7244529863066876845

Output with LLVM Flang 23:

reference (dp)       11999.9991
sum                  11999.9990
stdlib_sum           11998.8975
stdlib_sum_kahan     11999.9990
fnv_1a 32-bit    -1706177971
fnv_1a 64-bit    7244529863066876845

Two million single-precision values are added; the exact answer is 11999.9991. The plain intrinsic sum loses the small terms under GNU Fortran (11953.1240) and does not under Flang (11999.9990): the standard lets the processor evaluate a reduction in any mathematically equivalent way, so the accuracy of sum depends on the compiler and the optimization flags. stdlib_sum_kahan returns 11999.9990 on both, stdlib_sum is a chunked pairwise-style sum that is better than the naive sum but not exact, and the hashes are the same on both compilers.

Files, processes, terminals and error states

stdlib_system is the part of stdlib that talks to the operating system: run starts a process and waits (runasync does not), want_stdout=.true. captures its output into process%stdout, and the file-system procedures (make_directory_all, remove_directory, delete_file, is_directory, exists, get_cwd, set_cwd) and path procedures (join_path, the / operator, base_name, dir_name, split_path) work on both POSIX and Windows paths. The module is implemented partly in C (stdlib_system.c, stdlib_system_subprocess.c in the fpm tree), so a project that uses it needs a C compiler too. stdlib_ansi produces terminal colour codes, and stdlib_error supplies the state_type that all of these use to report errors without stopping:

program stdlib_system_demo
  use stdlib_error, only: check, state_type
  use stdlib_system, only: make_directory_all, remove_directory, delete_file, exists, is_directory, &
                           join_path, base_name, dir_name, run, process_type, is_windows, operator(/)
  use stdlib_ansi, only: fg_color_green, fg_color_red, style_bold, style_reset, ansi_code, &
                         operator(//), operator(+)
  implicit none
  type(state_type) :: err
  type(process_type) :: p
  character(len=:), allocatable :: out_dir, file
  type(ansi_code) :: ok_style
  integer :: u

  out_dir = 'results' / 'run01'
  call make_directory_all(out_dir, err)
  print '(a, l1, a, l1)', 'created: ', err%ok(), ', is_directory: ', is_directory(out_dir)

  file = join_path(out_dir, 'heat.csv')
  open(newunit=u, file=file, status='replace', action='write')
  write (u, '(a)') '0.0,1.0'
  close(u)
  print '(2a)', 'file    : ', file
  print '(2a)', 'base    : ', base_name(file)
  print '(2a)', 'dir     : ', dir_name(file)
  print '(a, l1)', 'exists  : ', exists(file)

  ! run an external program and capture its standard output
  if (.not. is_windows()) then
    p = run('wc -l ' // file, want_stdout=.true.)
    print '(a, i0, 2a)', 'wc exit code ', p%exit_code, ': ', trim(adjustl(p%stdout))
  end if

  ! state-based error handling instead of error stop
  call delete_file(file, err)
  print '(a, l1)', 'first delete ok: ', err%ok()
  call delete_file(file, err)                    ! the file is gone: reported as ok, not as an error
  print '(a, l1, 2a)', 'second delete ok: ', err%ok(), ' -> ', err%print()
  call delete_file(out_dir, err)                 ! a directory cannot be deleted as a file
  print '(a, l1, 2a)', 'delete directory failed: ', err%error(), ' -> ', err%print()
  call remove_directory(out_dir, err)
  call remove_directory('results', err)
  print '(a, l1)', 'cleaned up: ', err%ok()

  ! check(): a failed condition with warn=.true. prints and continues
  call check(1 + 1 == 3, msg='arithmetic is broken', warn=.true.)

  ! ANSI escape sequences for coloured terminal output
  ok_style = fg_color_green + style_bold
  print '(a)', ok_style // 'PASS' // style_reset
  print '(a)', fg_color_red // 'FAIL' // style_reset
end program stdlib_system_demo

Output with GNU Fortran 16.2 (ESC marks the escape character, byte 27, which is not printable; the line arithmetic is broken is written to standard error by check and appears here in the order the terminal received the two streams):

created: T, is_directory: T
file    : results/run01/heat.csv
base    : heat.csv
dir     : results/run01
exists  : T
wc exit code 0: 1 results/run01/heat.csv
first delete ok: T
second delete ok: T -> Success!
delete directory failed: T -> Error encountered: Filesystem Error: Cannot delete results/run01 - is a directory
 arithmetic is broken
cleaned up: T
ESC[0;1;32mPASSESC[0m
ESC[0;31mFAILESC[0m

Flang 23 printed the same lines (the standard-error line first, because of buffering) once stdlib’s C files were compiled with -fPIE: with the default C flags fpm used, linking with Flang 23 failed with relocation R_AARCH64_ADR_PREL_PG_HI21 against symbol 'stderr@@GLIBC_2.17' ... recompile with -fPIC, and fpm build --c-compiler gcc --c-flag -fPIE fixed it.

The error-state design is the important part. A state_type (and its subtype linalg_state_type) holds an integer flag (STDLIB_SUCCESS, STDLIB_VALUE_ERROR, STDLIB_LINALG_ERROR, STDLIB_INTERNAL_ERROR, STDLIB_IO_ERROR, STDLIB_FS_ERROR), a fixed-length message and a location, with the type-bound procedures ok(), error(), print() and print_message() and the comparison operators against the integer flags. The storage is fixed-size on purpose: no allocation on the error path. Procedures that can fail take an optional err; when it is absent they call error stop with the message; when present they set it and return. Note the behaviour of delete_file in 0.8.1 shown above: its spec says deleting a file that does not exist is an error, but the source returns a success state with the message "not deleted: file does not exist", and the example output confirms the source (second delete ok: T), so test for existence yourself if you need to tell the cases apart. Deleting a directory is an error (Cannot delete results/run01 - is a directory).

check, error_stop and exit codes

check(condition, msg, code, warn) stops the program with error stop when the condition is false, unless warn is true, in which case it only prints to standard error. The default of warn is false in the source of 0.8.1, so a bare check stops (the prose of the spec says the default is true, but its behaviour list and the code agree on stopping). error_stop(msg, code) writes the message to standard error and stops. The standard says an integer stop code on error stop is recommended to become the process exit status (J3/23-007r1, 11.4), and the stdlib code argument is meant to do that, but the implementation file the fpm route compiles (f08estop.f90) honours only the values 1, 2 and 77; any other code prints ERROR: code n was specified. and exits with status 1. (The CMake build selects f18estop.f90, which forwards any code, only when its configure-time test of error stop with a variable succeeds; on this compiler the CMake build used the fallback as well.) A singular inv without an err argument ends the program like this:

program stdlib_check_stop
  use stdlib_error, only: check, error_stop
  use stdlib_linalg, only: inv
  implicit none
  real(kind(1.0d0)) :: a(2, 2), b(2, 2)

  call check(2 > 1, 'never printed')                    ! true: nothing happens
  a = 0.0d0
  b = inv(a)                                            ! no err argument: a singular matrix stops the program
  print *, 'not reached'
  call error_stop('also not reached', 7)
end program stdlib_check_stop
ERROR STOP [inverse] returned Algebra Error: singular matrix

Error termination. Backtrace:

(more lines follow: the gfortran backtrace with addresses, then exit status 1; the exact text of the first line depends on the compiler, Flang 23 printed Fortran ERROR STOP: [inverse] returned Algebra Error: singular matrix.) And a check with a code that the fallback does not know (warn is not given, so it stops):

program stdlib_check_code
  use stdlib_error, only: check
  implicit none
  integer, parameter :: nx = 2
  call check(nx >= 3, msg='grid needs at least 3 points', code=3)
  print *, 'not reached'
end program stdlib_check_code
 grid needs at least 3 points
 ERROR: code            3  was specified.
ERROR STOP

The exit status was 1 with both compilers (Flang 23 printed Fortran ERROR STOP: code 1); changing code=3 to code=77 made the exit status 77 with both. Use 1, 2 or 77 if you rely on the status in 0.8.1, or call error stop yourself with the code, as described in Error Handling and Program Termination.

Library conventions

Several design choices are common to the modules and explain why they look the way they do:

  • Kinds come from stdlib_kinds, and most procedures are generic over sp, dp and the integer kinds (and over xdp and qp when the build has them). The number of generic specifics is the product of kinds and ranks, which is why MAXRANK exists and why a first build of stdlib_linalg compiles thousands of lines.

  • Optional err arguments select between fatal and recoverable error handling, as above.

  • Pure when possible. Many functions are pure in their plain form and lose pure when you pass err; a few, such as inner_product in the iterative solvers, lost it in 0.8.1.

  • Allocatable results. strip, join, solve, lstsq and friends return allocatable results; assign to an allocatable variable.

  • Zero-based and one-based indexing are both used. Bitset positions start at 0; stringlist indices start at 1 (fidx(1)); trueloc returns indices of the lower bound you give. Read the spec of each module.

  • Specs are the contract. The behaviour of delete_file above is an example of why the source and the spec need to be read together; the spec index of the tag, not the web site’s current pages, is the document that matches the release.

Pitfalls

  • A git dependency on the release tag does not compile with fpm. Use the metapackage or the stdlib-fpm branch, or expand the tree with fypp_deployment.py (see above).

  • stdlib = "*" and the branch are not pins. They follow the newest release or branch head; record the version your tests passed with, and read CHANGELOG.md before upgrading.

  • qp = -1 and xdp = -1 when the build did not generate those kinds. A real(qp) declaration then fails to compile; guard it or use only sp and dp.

  • The CMake package needs find_package(BLAS) and find_package(LAPACK) first when stdlib was configured with an external BLAS.

  • sum and the other intrinsic reductions are not exact summations. Use stdlib_sum_kahan where the accuracy of long sums matters.

  • select reorders its input, arg_select does not.

  • map_entry with an existing key keeps the old value and reports conflict.

  • Bitset positions start at 0, and bit_count counts set bits.

  • error_stop(msg, code) and check(..., code=n) honour only 1, 2 and 77 in the build the fpm route produces.

  • corr and cov need a rank-1 or rank-2 array and a dim; a pair of vectors must be stored as two columns.

  • GNU Fortran prints -Wsurprising warnings for padl, padr and zfill, and the linker warns about an executable stack for the conjugate-gradient solver; neither is a program error.

  • Everything is experimental. Even the 19 stable stdlib_linalg procedures sit in a module that is listed as experimental on the index; pin the version.

  • The web site tracks development. stdlib.fortran-lang.org documents the head of the repository; for a release, read doc/specs at the tag (the Source links on this page do that).

Where the books are outdated

The two Fortran books consulted for this section were written before stdlib existed or while it was starting, so they teach the language with the intrinsics only. Where current practice differs:

Area Outdated in the books, current approach

Library support

the books present the language and its intrinsics as the whole toolbox, and the numerical examples build their own string, sorting, statistics and file helpers; today stdlib offers tested versions of those (an experimental API, so check the version)

Strings

variable-length text is presented through character(len=:), allocatable and fixed-length padding; string_type, stdlib_strings and to_num are community conveniences for it

Linear algebra

a hand-written LU or Cholesky routine, or direct calls to dgesv with workspace arguments, are the books' route; stdlib_linalg gives x = solve(a, b) over the same LAPACK algorithms, and the full reference BLAS and LAPACK ship inside stdlib

Package management

building a program from library sources is left to the reader; fpm and CMake fetch stdlib as a dependency

Error handling

stat= and iostat= arguments and error stop; stdlib adds a state_type convention for libraries

Standard version provenance and compiler support

Item Introduced Checked on 2026-10-19

stdlib as a project

fortran-lang organization, not part of any ISO standard

release 0.8.1 of 2026-01-26 built from the tag

Generic interfaces from several modules merging under one name

rules in 15.4.3.4.1 of Fortran 2023; edition not confirmed here

spmv from stdlib_sparse and stdlib_specialmatrices in one program, both compilers

error stop with an integer stop code and the exit status recommendation

defined in 11.4 of Fortran 2023; the edition that introduced it is not confirmed from the standard text, stdlib’s own comment calls its fallback a "Fortran 2008 error stop"

ERROR STOP text differs between GNU Fortran and Flang; exit statuses equal

User-defined derived-type I/O (dt) used by string_type

defined in 12.6.4.8 of Fortran 2023; edition not confirmed here

GNU Fortran 16.2 and LLVM Flang 23 print string_type values with dt

Kind constants dp, qp from stdlib_kinds

library constants, not a language feature

qp is -1 in the fpm build, 16 in the CMake build

stdlib build compiler support (README)

 — 

GCC 10 to 15 and Intel oneAPI (classic 2021.10, LLVM 2024.1) are in the project’s CI; this page additionally built and ran stdlib 0.8.1 with GNU Fortran 16.2 and LLVM Flang 23 (aarch64 Linux)

NAG, NVIDIA HPC and older Intel releases are listed as not working in the README of the tag (issues 107 and 108, and failures with Intel Parallel Studio XE 16 to 18); none of them was available here.