Skip to content

Commit 6d91d06

Browse files
authored
Merge pull request #2 from SINTEF/second-iteration
Second iteration of library
2 parents 6bb46b5 + d682be8 commit 6d91d06

21 files changed

+904
-1359
lines changed

README.adoc

Lines changed: 101 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@ source of the error.
1616
This process is time consuming and mistakes lead to inaccurate information and
1717
annoyance while debugging.
1818

19-
The fortran error-handling library makes this process much easier by providing a type,
20-
`error_t`, to indicate if a procedure invocation has failed.
19+
The error-handling library provides a solution for error handling in Fortran code.
20+
It is primarily targeted towards development of Fortran based applications, but
21+
could be used in library code as well.
22+
23+
At its core is the abstract type `error_t` which is used to indicate if a procedure
24+
invocation has failed.
2125
Errors can be handled gracefully and context can be added while returning up
2226
the call stack.
23-
It is also possible to programmatically identify and handle certain types or errors
24-
without terminating the application.
27+
It is also possible to make code where errors can be identified and handled programmatically
28+
by extending the `error_t` base class.
2529

2630
But perhaps most interesting is the ability to generate stacktraces along with any
2731
error when combined with the https://github.com/SINTEF/fortran-stacktrace[fortran-stacktrace]
@@ -38,13 +42,13 @@ with access to the source code itself.
3842
== Quick Start
3943

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

4448
[source,fortran]
4549
----
4650
module sqrt_inplace_mod
47-
use error_handling, only: error_t
51+
use error_handling, only: error_t, fail
4852
implicit none
4953
5054
private
@@ -54,10 +58,10 @@ contains
5458
5559
pure subroutine sqrt_inplace(x, error)
5660
real, intent(inout) :: x
57-
type(error_t), allocatable, intent(inout) :: error
61+
class(error_t), allocatable, intent(inout) :: error
5862
5963
if (x <= 0.0) then
60-
error = error_t('x is negative')
64+
error = fail('x is negative')
6165
return
6266
end if
6367
x = sqrt(x)
@@ -73,12 +77,12 @@ Then use your newly created routines:
7377

7478
[source,fortran,indent=0]
7579
----
76-
use error_handling, only: error_t, set_error_hook
80+
use error_handling, only: error_t
7781
use sqrt_inplace_mod, only: sqrt_inplace
7882
implicit none
7983
8084
real :: x
81-
type(error_t), allocatable :: error
85+
class(error_t), allocatable :: error
8286
8387
! Here we are using a labelled block to separate multiple fallible
8488
! procedure calls from the code that handles any error
@@ -100,7 +104,7 @@ Then use your newly created routines:
100104
return
101105
end block fallible
102106
! If we're here then an error has happened!
103-
write(*, '(a)') error%display()
107+
write(*, '(a,a)') 'Error: ', error%to_chars()
104108
----
105109

106110
=== Generating Stacktraces
@@ -122,7 +126,7 @@ https://github.com/cpm-cmake/CPM.cmake/[CMake Package Manager (CPM)]:
122126

123127
[source,cmake]
124128
----
125-
CPMAddPackage("https://github.com/SINTEF/fortran-error-handling.git@0.1.0")
129+
CPMAddPackage("https://github.com/SINTEF/fortran-error-handling.git@0.2.0")
126130
target_link_libraries(<your target> error-handling)
127131
----
128132

@@ -156,19 +160,15 @@ In your Fortran Package Manager `fpm.toml` configuration file, add this repo as
156160

157161
```toml
158162
[dependencies]
159-
error-handling = { git = "https://github.com/SINTEF/fortran-error-handling.git", tag = "v0.1.0" }
163+
error-handling = { git = "https://github.com/SINTEF/fortran-error-handling.git", tag = "v0.2.0" }
160164
```
161165

162166
== API Reference
163167

164-
All functionality in this library can be accessed from the module `error_handling`.
165-
The modules directly under the `src/` folder contains specification and documentation
166-
of the different types and their procedures:
167-
168-
* For type `error_t`, see link:src/error_handling_error.f90[]
169-
* For class `fail_reason_t`, see link:src/error_handling_fail_reason.f90[]
170-
* For types `error_hookt_t` and `error_handler_t`, see link:src/error_handling_hook.f90[]
171-
* For subroutine `error_stop`, see link:src/error_handling_error_stop.f90[]
168+
The abstract `error_t` class is declared in the link:src/error.f90[`error_mod`] module,
169+
but also available as a re-export from the `error_handling` module for convenience.
170+
The rest of the public API is available from the link:src/error_handling.f90[`error_handling`]
171+
module which also contains documentation for each type and procedure.
172172

173173

174174
== Usage
@@ -184,69 +184,35 @@ For users however, the stacktrace is hardly of any use at all.
184184
This is why it is important to gracefully unwind the application and provide some
185185
information about what caused the error so that users may take action themselves.
186186

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

192193

193194
[source,fortran]
194195
----
195-
module bounded_mod
196-
use error_handling, only: error_t
196+
module processing_mod
197+
use error_handling, only: error_t, wrap_error, fail
197198
implicit none
198199
199200
contains
200201
201-
pure subroutine add_bounded(i, j, error)
202-
integer, intent(inout) :: i
203-
integer, intent(in) :: j
204-
type(error_t), allocatable, intent(inout) :: error
205-
206-
if (i > 25) then
207-
error = error_t('i is too large')
208-
return
209-
end if
210-
i = i + j
211-
end subroutine
212-
213-
214-
pure subroutine multiply_bounded(i, j, error)
215-
integer, intent(inout) :: i
216-
integer, intent(in) :: j
217-
type(error_t), allocatable, intent(inout) :: error
218-
219-
if (i > 25) then
220-
error = error_t('i is too large')
221-
return
222-
end if
223-
i = i * j
224-
end subroutine
225-
226-
end module
227-
228-
229-
module some_mod
230-
use bounded_mod, only: add_bounded, multiply_bounded
231-
use error_handling, only: error_t
232-
implicit none
233-
234-
contains
202+
pure subroutine process_array(arr, res, error)
203+
integer, intent(inout) :: arr(:)
204+
integer, intent(out) :: res
205+
class(error_t), allocatable, intent(inout) :: error
235206
236-
pure subroutine do_something(i, error)
237-
integer, intent(inout) :: i
238-
type(error_t), allocatable, intent(inout) :: error
239-
240-
integer :: j
241-
character(len=20) :: i_value, j_value
207+
integer :: i
208+
character(len=20) :: i_value
242209
243210
! Here we are using a block to separate multiple fallible procedure calls
244211
! from the code that handles any error
212+
res = 0
245213
fallible: block
246-
do j = 1, 5
247-
call add_bounded(i, j + 2, error)
248-
if (allocated(error)) exit fallible
249-
call multiply_bounded(i, j, error)
214+
do i = 1, size(arr)
215+
call accumulate_and_check(arr(i), res, error)
250216
if (allocated(error)) exit fallible
251217
end do
252218
! Return for subroutine on success, code below is only for
@@ -255,32 +221,56 @@ contains
255221
end block fallible
256222
! Provide some context with error
257223
write(i_value, *) i
258-
write(j_value, *) j
259-
call error%with_cause('Could not do some thing with i = ' &
260-
// trim(adjustl(i_value)) // ' and j = ' // trim(adjustl(j_value)))
224+
call wrap_error(error, 'Processing of array failed at element ' &
225+
// trim(adjustl(i_value)))
261226
end subroutine
227+
228+
229+
pure subroutine accumulate_and_check(i, res, error)
230+
integer, intent(in) :: i
231+
integer, intent(inout) :: res
232+
class(error_t), allocatable, intent(inout) :: error
233+
234+
if (res > 50) then
235+
error = fail('Magic limit reached')
236+
return
237+
end if
238+
res = res + i
239+
end subroutine
240+
262241
end module
263242
264243
265244
program basic_example
266-
use error_handling, only: error_t
267-
use some_mod, only: do_something
245+
use error_handling, only: error_t, wrap_error
246+
use processing_mod, only: process_array
268247
implicit none
269-
integer :: i
270-
type(error_t), allocatable :: error
271248
272-
i = 10
273-
call do_something(i, error)
249+
integer :: res
250+
integer, allocatable :: arr(:)
251+
class(error_t), allocatable :: error
252+
253+
arr = [1, 2, 3, 5, 8, 12, 11, 20, 5, 2, 4, 6]
254+
call process_array(arr, res, error)
274255
if (allocated(error)) then
275-
call error%with_cause('Example failed (but that was the intent...)')
276-
write(*,'(a)') error%display()
256+
call wrap_error(error, 'Example failed (but that was the intent...)')
257+
write(*,'(a,a)') 'Error: ', error%to_chars()
277258
else
278-
write(*,*) 'Got back: ', i
259+
write(*,*) 'Got back: ', res
279260
end if
280261
end program
281262
----
282263

283-
This will produce the output shown in the screenshot on the top of this page.
264+
This will produce an error message that is quite readable even for those not familiar
265+
with the source code:
266+
267+
```
268+
Error: Example failed (but that was the intent...)
269+
270+
Caused by:
271+
- Processing of array failed at element 9
272+
- Magic limit reached
273+
```
284274

285275
=== Pure Functions
286276

@@ -320,7 +310,7 @@ provide such result types for some primitive data types. Example:
320310
----
321311
use iso_fortran_env, only: dp => real64
322312
use error_handling_experimental_result, only: result_real_dp_rank1_t
323-
use error_handling, only: error_t
313+
use error_handling, only: fail
324314
325315
! (...)
326316
@@ -330,7 +320,7 @@ type(result_real_dp_rank1_t) pure function func(x) result(y)
330320
if (x >= 0) then
331321
y = x * [1.0, 2.0, 3.0]
332322
else
333-
y = error_t('x must be positive')
323+
y = fail('x must be positive')
334324
end if
335325
end function
336326
----
@@ -349,11 +339,6 @@ else
349339
end if
350340
----
351341

352-
WARNING: There seems to be a bug in gfortran with finalization when a types
353-
assignment operator is overloaded like we do here.
354-
If you use or plan to support gfortran you currently need to assign
355-
errors like this: `y%error = error_t('...')` or your program will crash!
356-
357342
WARNING: This is currently an experimental feature. Expect breaking changes in the
358343
future.
359344

@@ -364,20 +349,35 @@ for example in order to continue execution.
364349
If you're developing a library for others to use it is good practice to do so
365350
as you don't know how users may wish to use your library.
366351

367-
The `error_t` type can be constructed with a custom type extending
368-
`fail_reason_t`. This can later be detected with a `select type` block:
352+
In these situations, make your own type(s) that extend `error_t`. Checking for
353+
this specific error can the be done using a `select type` statement:
369354

370355
[source,fortran]
371356
----
372-
type(error_t), allocatable :: error
357+
class(error_t), allocatable :: error
373358
! (...)
374-
select type (reason => error%root_cause)
375-
type is (special_fail_reason_t)
376-
! Add code here to gracefully handle an failure reason of type special_fail_reason_t
359+
select type (error)
360+
type is (my_error_t)
361+
! Add code here to gracefully handle an error of type my_error_t
377362
end select
378363
----
379364

380-
For a complete example, see link:example/fail-reason.f90[`fail-reason.f90`].
365+
Note that due to limitations in the Fortran standard
366+
(see link:https://github.com/j3-fortran/fortran_proposals/issues/242[#242])
367+
you should still have subroutines take a `class(error_t)` argument and not
368+
a `type(my_error_t)` argument.
369+
If you use a `type(my_error_t)` and any caller just want to pass errors
370+
back up the call stack then they need to add much boilerplate code to convert the
371+
`type(my_error_t)` variable into a `class(error_t)`.
372+
Instead, use an argument `class(error_t)` and clearly state the possible error types
373+
that might be returned in the documentation.
374+
375+
It is also worth noting that any custom error handler (e.g. for stacktrace generation)
376+
will not be attached to the custom error type.
377+
This will first happen when the error is stored in the general error report type by
378+
either the `fail` function or the `wrap_error` subroutine.
379+
380+
For a complete example, see link:example/custom-error-type.f90[`custom-error-type.f90`].
381381

382382
== Design
383383

@@ -409,24 +409,10 @@ Why is a second library required for stacktrace generation?::
409409

410410
The stacktrace generation code requires some additional dependencies, namely a
411411
C++ compiler, some Win32 API calls on Windows and libbfd on Linux.
412-
By keeping this library pure Fortran with no additional dependencies it is very easy
413-
to use it for error handling in other libraries.
414-
This means that you (as the library developer) don't impose additional dependencies
415-
to your users that they might not want to use.
416-
Stacktrace generation may be desirable in a standalone application, but if the Fortran
417-
code is to be embedded in for example a Python library this might not be desirable.
418-
The separation means that this library and libraries depending on it will be relevant
419-
in both scenarios.
420-
421-
Why isn't `error_t` itself abstract, instead of `fail_reason_t`?::
422-
423-
One could imagine that subroutines could take a `class(my_error_t), allocatable`
424-
where `my_error_t` extends `error_t` to enable checking for specific errors.
425-
While testing this approach I encountered way too may compiler bugs
426-
to bother carrying on with it.
427-
Also, the Fortran standard unfortunately makes using such a design very clumsy.
428-
See https://github.com/j3-fortran/fortran_proposals/issues/242[this proposal] for
429-
further details.
412+
For complex project this might not be a big deal, but smaller projects it could be
413+
advantageous to have a simple pure Fortran library instead.
414+
Also, the error context generation using `wrap_error` is very useful by itself, even
415+
without code to generate a stacktrace along with it.
430416

431417

432418
== License and Copyright

cmake-project.jsonc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "error-handling",
3-
"version": "0.1.0",
3+
"version": "0.2.0",
44
"languages": ["Fortran"],
55
"linker_language": "Fortran",
66
"release_dbg_info": true,

0 commit comments

Comments
 (0)