Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/sintef/fortran-error-handling

A library for flexible and easy to use error handling in Fortran projects
https://github.com/sintef/fortran-error-handling

error-handling fortran fortran-package-manager

Last synced: about 2 months ago
JSON representation

A library for flexible and easy to use error handling in Fortran projects

Awesome Lists containing this project

README

        

= Fortran Error Handling
:imagesdir: doc/

:repo: SINTEF/fortran-error-handling
**Efficient and easy to use error handling for modern Fortran.**

image:https://github.com/{repo}/actions/workflows/built-test.yml/badge.svg[link="https://github.com/{repo}/actions/workflows/built-test.yml"]
image:https://img.shields.io/github/v/release/{repo}?label=version&sort=semver[link="https://github.com/{repo}/releases"]

Fortran does not have any built-in mechanisms for errors as seen in most
other programming languages.
Over the years, developers often have resorted to integer or logical arguments
as error flags and manual labelling of error returns to be able to determine the
source of the error.
This process is time consuming and mistakes lead to inaccurate information and
annoyance while debugging.

The error-handling library provides a solution for error handling in Fortran code.
It is primarily targeted towards development of Fortran based applications, but
could be used in library code as well.

At its core is the abstract type `error_t` which is used to indicate if a procedure
invocation has failed.
Errors can be handled gracefully and context can be added while returning up
the call stack.
It is also possible to make code where errors can be identified and handled programmatically
by extending the `error_t` base class.

But perhaps most interesting is the ability to generate stacktraces along with any
error when combined with the https://github.com/SINTEF/fortran-stacktrace[fortran-stacktrace]
library.
This means that you can easily make even old legacy code output errors messages like this:

image::stacktrace-example.png[]

The source code snippets are of course voluntary and only available on a machine
with access to the source code itself.

[#quickstart]
== Quick Start

All functionality is located in the link:src/error_handling.f90[`error_handling`] module.
When writing a subroutine that might fail, add an `class(error_t), allocatable` argument.
Use the `fail` function to create a general error with a given message, for example:

[source,fortran]
----
module sqrt_inplace_mod
use error_handling, only: error_t, fail
implicit none

private
public sqrt_inplace

contains

pure subroutine sqrt_inplace(x, error)
real, intent(inout) :: x
class(error_t), allocatable, intent(inout) :: error

if (x <= 0.0) then
error = fail('x is negative')
return
end if
x = sqrt(x)
end subroutine

end module
----

NOTE: If the subroutine is pure or elemental the intent must be `intent(inout)` in order
to be standard compliant, otherwise `intent(out)` may be used.

Then use your newly created routines:

[source,fortran,indent=0]
----
use error_handling, only: error_t
use sqrt_inplace_mod, only: sqrt_inplace
implicit none

real :: x
class(error_t), allocatable :: error

! Here we use a labelled block to separate multiple fallible
! procedure calls from the code that handles any errors
fallible: block
write(*,*) 'computing square root...'
x = 20.0
call sqrt_inplace(x, error)
! If an error occurred, go to error handling code
if (allocated(error)) exit fallible
! Success -> write result
write(*,*) ' - sqrt = ', x
write(*,*) 'computing square root...'
x = - 20.0
call sqrt_inplace(x, error)
if (allocated(error)) exit fallible
write(*,*) ' - sqrt = ', x
! Return from subroutine on success, code below is only for
! error handling so no allocated(error) check is needed there.
return
end block fallible
! If we're here then an error has happened!
write(*, '(a,a)') 'Error: ', error%to_chars()
----

=== Generating Stacktraces
For enabling stacktraces from errors, see instructions https://github.com/SINTEF/fortran-stacktrace#fortran-stacktrace[here].

== Building

A fairly recent Fortran and compiler is required to build this library.
The following compilers are known to work:

- gfortran version 9 or later
- Intel Fortran 2021 or later

=== CMake

The recommended way of getting the source code for this library when using CMake
is to add it as a dependency using
https://github.com/cpm-cmake/CPM.cmake/[CMake Package Manager (CPM)]:

[source,cmake]
----
CPMAddPackage("https://github.com/SINTEF/[email protected]")
target_link_libraries( error-handling)
----

//TODO:

// === CMake With Declarative CMake Template

// //TODO: link
// If you're using http://todo[Declarative CMake Template] as a template for your CMake
// projects, simply add `error-handling` to your dependencies list:
// //TODO: Update link
// [source,json]
// ----
// "dependencies": {
// "error-handling": {"git": "https://github.com/SINTEF/fortran-error-handling.git", "version": "0.1.0"},
// },
// ----

=== CMake Without CPM

If you don't want to use CPM you can either use
https://cmake.org/cmake/help/latest/module/FetchContent.html[FetchContent]
manually or add this repo as a git submodule to your project. Then in your
`CMakeLists.txt` add it as a subdirectory and use `target_link_libraries` to
link against `error-handling`.

=== Fortran Package Manager (FPM)

In your Fortran Package Manager `fpm.toml` configuration file, add this repo as a dependency:

```toml
[dependencies]
error-handling = { git = "https://github.com/SINTEF/fortran-error-handling.git", tag = "v0.2.0" }
```

== API Reference

The abstract `error_t` class is declared in the link:src/error.f90[`error_mod`] module,
but also available as a re-export from the `error_handling` module for convenience.
The rest of the public API is available from the link:src/error_handling.f90[`error_handling`]
module which also contains documentation for each type and procedure.

== Usage

After trying out the <>, see the sections below for some more advanced
features in this library.

=== Error Contextual Information

For the developer a stacktrace is an invaluable resource for determining the reason
of an error.
For users however, the stacktrace is hardly of any use at all.
This is why it is important to gracefully unwind the application and provide some
information about what caused the error so that users may take action themselves.

The example below shows how the subroutine `wrap_error` can be used to provide
contextual information in the event of an error.
In fact this information will be very useful for a developer as well since the stacktrace
from a successful invocation of `accumulate_and_check` looks exactly the same as
the one that fails.

[source,fortran]
----
module processing_mod
use error_handling, only: error_t, wrap_error, fail
implicit none

contains

pure subroutine process_array(arr, res, error)
integer, intent(inout) :: arr(:)
integer, intent(out) :: res
class(error_t), allocatable, intent(inout) :: error

integer :: i
character(len=20) :: i_value

! Here we use a labelled block to separate multiple fallible procedure calls
! from the code that handles any errors
res = 0
fallible: block
do i = 1, size(arr)
call check_and_accumulate(arr(i), res, error)
if (allocated(error)) exit fallible
end do
! Return for subroutine on success, code below is only for
! error handling so no allocated(error) check is needed there.
return
end block fallible
! Provide some context with error
write(i_value, *) i
call wrap_error(error, 'Processing of array failed at element ' &
// trim(adjustl(i_value)))
end subroutine

pure subroutine check_and_accumulate(i, res, error)
integer, intent(in) :: i
integer, intent(inout) :: res
class(error_t), allocatable, intent(inout) :: error

if (res > 50) then
error = fail('Magic limit reached')
return
end if
res = res + i
end subroutine

end module

program basic_example
use error_handling, only: error_t, wrap_error
use processing_mod, only: process_array
implicit none

integer :: res
integer, allocatable :: arr(:)
class(error_t), allocatable :: error

arr = [1, 2, 3, 5, 8, 12, 11, 20, 5, 2, 4, 6]
call process_array(arr, res, error)
if (allocated(error)) then
call wrap_error(error, 'Example failed (but that was the intent...)')
write(*,'(a,a)') 'Error: ', error%to_chars()
else
write(*,*) 'Got back: ', res
end if
end program
----

This will produce an error message that is quite readable even for those not familiar
with the source code:

```
Error: Example failed (but that was the intent...)

Caused by:
- Processing of array failed at element 9
- Magic limit reached
```

=== Pure Functions

Pure and elemental subroutines can have multiple arguments with `intent(inout)`
or `intent(out)`.
This makes it possible to modify one or more arguments and have an additional
`error_t` argument for communicating if any error has ocurred.

Pure and elemental functions on the other hand are only allowed to modify their
return value which means that one cannot add an `error_t` argument with
`intent(inout)` to indicate failures.

One way of dealing with this is to return a type which can either hold the result
ing data or an `error_t`, for example:

[source,fortran]
----
type :: result_int_t
integer, allocatable :: value
type(error_t), allocatable :: error
end type
----

WARNING: Technically, this type can also hold a value AND an error.
The programmer must make sure that this does not happen.

This idea is very similar to the
https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html[Result]
enum in the Rust programming language.
Since Fortran neither have https://github.com/j3-fortran/generics/issues/9[generics]
nor any support for https://en.wikipedia.org/wiki/Tagged_union[sum data types]
(enums) this is quite a bit more cumbersome to set up in Fortran.
The module link:src/experimental/result.f90[`error_handling_experimental_result`]
provide such result types for some primitive data types. Example:

[source,fortran]
----
use iso_fortran_env, only: dp => real64
use error_handling_experimental_result, only: result_real_dp_rank1_t
use error_handling, only: fail

! (...)

type(result_real_dp_rank1_t) pure function func(x) result(y)
real(dp), intent(in) :: x

if (x >= 0) then
y = x * [1.0, 2.0, 3.0]
else
y = fail('x must be positive')
end if
end function
----

To use the function:

[source,fortran]
----
type(result_real_dp_rank1_t) :: y

y = func(-12.0_dp)
if (y%is_error()) then
! Handle error here
else
! y%value is safe to use here
end if
----

WARNING: This is currently an experimental feature. Expect breaking changes in the
future.

=== Programmatically Handling Specific Errors

In some situations it might be desirable to detect and handle specific error conditions,
for example in order to continue execution.
If you're developing a library for others to use it is good practice to do so
as you don't know how users may wish to use your library.

In these situations, make your own type(s) that extend `error_t`. Checking for
this specific error can the be done using a `select type` statement:

[source,fortran]
----
class(error_t), allocatable :: error
! (...)
select type (error)
type is (my_error_t)
! Add code here to gracefully handle an error of type my_error_t
end select
----

Note that due to limitations in the Fortran standard
(see link:https://github.com/j3-fortran/fortran_proposals/issues/242[#242])
you should still have subroutines take a `class(error_t)` argument and not
a `type(my_error_t)` argument.
If you use a `type(my_error_t)` and any caller just want to pass errors
back up the call stack then they need to add much boilerplate code to convert the
`type(my_error_t)` variable into a `class(error_t)`.
Instead, use an argument `class(error_t)` and clearly state the possible error types
that might be returned in the documentation.

It is also worth noting that any custom error handler (e.g. for stacktrace generation)
will not be attached to the custom error type.
This will first happen when the error is stored in the general error report type by
either the `fail` function or the `wrap_error` subroutine.

For a complete example, see link:example/custom-error-type.f90[`custom-error-type.f90`].

== Design

The design of this library is heavily inspired by error handling mechanisms in
the https://doc.rust-lang.org/book/ch09-00-error-handling.html[Rust programming language]
and specifically the Rust library https://docs.rs/eyre/latest/eyre/[eyre].
Rust don't use exceptions like many other popular programming languages.
Interestingly this means that error handling in Fortran - one of the oldest
programming languages still actively used - share certain patterns with one of the
more "modern" programming languages around.

The vast majority of all source code includes error scenarios of some sorts.
Fundamentally, a good method for handling errors in Fortran should satisfy the
following requirements:

* Usable both in pure and impure subroutines and functions.
* Low overhead, especially for successful calls.
* Errors should be difficult to overlook. It should be obvious for the developer that they need to check if something went wrong.
* It should be possible to provide accurate information about what failed and when it occurred.
* Some errors might need to be recoverable, i.e. the _caller_ of a procedure should be able
to programmatically detect and act if a certain error occurred.

There are many ways of designing a error handling system for Fortran.
This library satisfies the above requirements and should be reasonably easy to use.
Some design decisions in might however not be obvious at first glance,
but are done so for good reasons:

Why is a second library required for stacktrace generation?::

The stacktrace generation code requires some additional dependencies, namely a
C++ compiler, some Win32 API calls on Windows and libbfd on Linux.
For complex project this might not be a big deal, but smaller projects it could be
advantageous to have a simple pure Fortran library instead.
Also, the error context generation using `wrap_error` is very useful by itself, even
without code to generate a stacktrace along with it.

== Contributions

Feel free to submit Feedback, suggestions or any problems in the issue tracker.

== License and Copyright

Copyright 2022 SINTEF Ocean AS. All Rights Reserved. MIT License.