Before the file system layer (see #201, #220) in stdlib can be implemented, it would be important to reach some agreement on how to signalize errors within stdlib. I am not talking about logging, as this is an add on we can discuss later. I want to concentrate here on how errors can be passed between routines in stdlib.
Fortran's current approach to report status in I/O commands is to have some optional integer argument, returning the status of the operation. If the argument has not been specified, the code stops in case of errors. I'd propose to generalize this principle, but using derived types to achieve higher robustness and flexibility:
type(status)), which methods allowing to set and query it.error stop.class(error) item. It can carry arbitrary types derived from a base class, so that arbitrary complex error information can be passed around. This way, the error signaling mechanism is extensible, we could even think to add an error-hierarchy as we have in Python.I have made a toy project to demonstrate the principle. Please, have a look at it and let me know what you think.
A few more notes:
change_dir()).This issue is related to several other error discussons, e.g. #219 , #193, #95. My suggestion is, again, to first reach agreement on the low-level of the error reporting, and we can add extended functionality, such as logging, on top later.
Would the idea still be that if the status argument is not provided, that it will error out right away? I think this is the key principle.
I think your approach can work.
The only possible downside that I can see is that you can notice that built-in Fortran functions never return a derived type, but only simple arguments such as integers or arrays. The reason for that is that it does not impose any particular derived type on the user. In here, we would be imposing the type type(status). So that's the main downside compared to returning just an integer.
Regarding speed, the derived type can possibly be a bit slower than an integer. So maybe not a good idea for small functions in hot loops. But for IO functions, the possible overhead is probably small.
Just like integer, type(status) is thread safe, does not have any global state.
And the Pros are: extensible, can convey more rich error information such as a string.
And it is still relatively simple.
If we chose to go with returning just an integer, then we can document the various different errors as different integers, and then provide a function that prints a nice error message based on the integer number.
So I think the key difference to an integer is that type(status) allows to convey information that is not just the error type, but something that changes all the time --- for example which directory failed, or what error some low level command (shell) returned. As is obvious from fpm, we need to have some functionality for running commands, and if they fail, the stderr output can be put into type(status) and propagated to the user. That would be hard to do with just integers.
So it seems to me if just a simple error with no extra information is needed, the integer might be better. But if more information must be attached to the error that is only known at runtime, then I think your type(status) is the way to go.
Would the idea still be that if the
statusargument is not provided, that it will error out right away? I think this is the key principle.
Actually, originally I wanted to propose to make them mandatory. But to be in accordance with Fortrans usual way of doing things, we could make it optional, and hope, that people would always pass (and handle it) in order to obtain a robust program.
I agree, that Fortran ususally operates with scalar types and arrays of them preventing to impose derived types on the programmer. But in my opinion, this is exaclty what makes many things quite painful, needing a lot of extra-coding for basic stuff.
As for type(status), the extensibility would be very important:
type(status) with traceback capabilities, so that it can collect the call stack while being propagated upwards.we could make it optional, and hope, that people would always pass (and handle it) in order to obtain a robust program.
Why would you consider a program not robust if it does not handle the error? If the status is not provided, then the function stops the program with a useful error message. I consider this very robust and the desired behavior. If I don't want to handle the error, I want to get a nice error message and a stacktrace. If the user wants to handle the error, then the status is provided. So it seems it is robust either way.
Regarding the stacktrace --- I think it's the compiler's job to create a nice stacktrace when the error occurs. GFortran does it, although I would like it to be formatted better.
What we can do is to try using type(status) in the IO routines and see how it goes.
I overall like it, although I don't think it's feasible to use for everything. For procedures that return a single result we use functions, and if we use an intent(out) argument in functions we can't make them pure. If we trust the long-term vision of Fortran being parallel and running on emerging architectures, then writing pure functions is what we should aim for.
So, without being convinced otherwise, my suggestion would be to limit such status/error propagation to subroutines that modify variables in-place. I/O and file system stuff is I think the perfect candidate for such things, stats functions not so much.
@milancurcic I agree. The stat functions will be used in hot loops, so we do not want to have the overhead of more general error handling there. The IO functions will have the overhead of the IO, so we can afford more general error handling there.
Of course, this error handling mechanism is not meant for functions at all. Especially numerical functions have better ways of signalizing errors by returning a specific value (e.g. nan).
I'd propose to use this error-handling mechanism in those cases, where otherwise an error stop would have been called. IMO, a library, which calls an error stop in a deeply nested internal routine instead of propagating the error back to the consumer is a true nightmare and can not really be considered being robust.
There are 2 schools of thought I think on having an error stop in a library procedure. On the one hand, the error is unlikely to be helpful to a user of the program, as they're unlikely to be able to deduce why their input caused an error in that procedure without the rest of the context, which isn't available to that procedure to print out.
However, if such an error occurs, it is more likely a bug in the program, the procedure was called with invalid inputs, and that's on the caller. You need to fix your program.
So you could take the stance that, here's the requirements of calling this procedure, if you violate them that's on you, or, you can strive for "total functions", that can gracefully handle all possible inputs, but force the caller to deal with potential errors.
I generally like using libraries that strive towards the latter, as it makes explicit in the interface that things might go wrong, but there are of course performance penalties, and there's nothing wrong with a procedure taking the stance of "if you call me with bad inputs, I crash".
I consider a procedure that only calls ERROR STOP when it encounters a problem to be a nightmare, but I also consider one that passes a status flag up to a calling procedure, that represents a problem a calling procedure can't be expected to handle, to also be a nightmare. Instead, a procedure, if it encounters a problem it thinks is unhandleable, should print a detailed error message describing the type of problem, and the location where it was found, and then call ERROR STOP. On rare occasions when the errors are genuinely handleable (e.g. a fast but non-robust method, that discovers it is having problems may pass a flag indicating that a more robust method is needed), it should pass an optional STATUS flag, that, if not present, stops the application with the detailed error message. FWIW I also consider most user errors to be unhadleable. See Herb Sutterâs paper for SC22/WG21, "Zero-overhead deterministic exceptions: Throwing valuesâ, for a discussion of the problems with trying to handle user or allocation errors.
Yes, I think we all agree to print detailed error information before calling error stop.
On Thu, Jul 23, 2020, at 10:00 PM, William B. Clodius wrote:
I consider a procedure that only calls ERROR STOP when it encounters a
problem to be a nightmare, but I also consider one that passes a status
flag up to a calling procedure, that represents a problem a calling
procedure can't be expected to handle, to also be a nightmare. Instead,
a procedure, if it encounters a problem it thinks is unhandleable,
should print a detailed error message describing the type of problem,
and the location where it was found, and then call ERROR STOP. On rare
occasions when the errors are genuinely handleable (e.g. a fast but
non-robust method, that discovers it is having problems may pass a flag
indicating that a more robust method is needed), it should pass an
optional STATUS flag, that, if not present, stops the application with
the detailed error message. FWIW I also consider most user errors to be
unhadleable. See Herb Sutterâs paper for SC22/WG21, "Zero-overhead
deterministic exceptions: Throwing valuesâ,
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0709r0.pdf
for a discussion of the problems with trying to handle user or
allocation errors.â
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/fortran-lang/stdlib/issues/224#issuecomment-663336741, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAFAWHYE3IFPQESMTZGE33R5EBOBANCNFSM4PFPIXJQ.
Yes, I think we all agree to print detailed error information before calling error stop.
Indeed, I think we agree all on that. However, this means that functions are not allowed to be pure, as currently in stdlib_experimental_stats (see example here.
Any ideas how to resolve that for functions, since this proposition doesn't seem to cover functions?
Of course, error stop with message is better. But often, the error is not caused by a bug, but by wrong user input (e.g. paths, some numerical values). And I find it much nicer, if the simulation package writes out a qualified error message (Error while trying to read the specified parameterization file (input.txt:23)) instead of (Could not read 'something.dat. Or The mixer scheme failed, try to use a different mixer settings versus Unable to solve linearly dependent system of equations.
As for functions: I think, we should only allow for functions which either never fail or can report their failure via a special value of their return type (e.g. NaN).
As for functions: I think, we should only allow for functions which either never fail or can report their failure via a special value of their return type (e.g.
NaN).
I am not sure I fully agree with that. For example, for the function mean, the dimension dim can be provided (with 0 < dim < maximum_rank_array). Currently if the user provides a wrong dim, a message is printed and error stop is called.
This is similar to what happen with the intrinsic sum (with gfortran):
Fortran runtime error: Dim argument incorrect in SUM intrinsic: is 3, should be between 1 and 2
I don't think that returning NaN in this case would be correct, but this strategy would allow pure functions.
However, this means that functions are not allowed to be
pure
It seems error stop can be in pure functions, at least with GFortran. We should check the Fortran Standard on this one.
This function indeed does not compile:
pure integer function f(x)
integer, intent(in) :: x
f = x+1
stop "ok"
end function
with:
a.f90:4:9:
stop "ok"
1
Error: STOP statement not allowed in PURE procedure at (1)
But this one compiles:
pure integer function f(x)
integer, intent(in) :: x
f = x+1
error stop "ok"
end function
According to MRC: "Execution of an error stop statement causes execution to cease as soon as possible on all images, so there is no need to disallow it in a pure procedure. It is now allowed ..." (It is apparently a Fortran 2018 thing).
It seems
error stopcan be in pure functions, at least with GFortran. We should check the Fortran Standard on this one.
Since Fortran 2018, indeed.
There you go. So this is an option. One still cannot have any print statements in pure functions, but we can communicate the message via error stop, as in this function:
pure integer function f(x)
integer, intent(in) :: x
character(len=100) :: msg
msg = "some text"
f = x+1
error stop msg
end function
There you go. So this is an option. One still cannot have any
error stop, as in this function:
This is indeed an option. However it might limit the number of compilers that support pure procedures with F18 error stop. But for the long term, it is indeed a solution.
@aradi the main performance impact I see in your proposal is that I believe the finalizer will be called every time a procedure is called with a STAT argument with INTENT(OUT) whether or not the STAT argument is present. If not present it obviously will be called when the procedure ends. If present I believe it will be called on entry since the corresponding variable will cease to exist. The call of the finalizer will have the cost of the creation of the call stack, the jump to the procedure, the if branch in the procedure, and unwinding the call stack.
If the class(error) can be used to pass arbitrary information the structure of a STAT object can be arbitrarily complex which can make constructing the stack and accessing the object from the stack more costly. I don't know what effect that will have on the finalizer. There is a cost for an arbitrary class hierarchy.
FWIW I have minor objections to the name STATUS for the type, as that is the natural name for the STAT variable, and users will try out that name forgetting that types and variables belong to the same namespace. FWIW I use a type ERRORS for my enumerations of error conditions for that reason.
Don't forget about those of us developing GUI applications. For us, an error_stop is never acceptable, and printing a message to the console is useless since the user may not even see that. All errors have to be handled.
I like the type(status) extensible type. But of course, what we really need is proper exception handling in the language. Anything else is just a stop gap solution. (like the current integer and string error messages of some of the intrinsic routines). It's just not enough in 2020.
I agree, the finalization may give an extra overhead, especially due to the intent(out) arguments. Thanks @wclodius2 to point it out! So, I have an alternative suggestion, which should decrease the overhead significantly and is almost as convenient and flexible as the previous suggestion:
allocatable, intent(out) dummy argument of a given derived type.%deactivate() method.
subroutine emits_os_error_only(error, ...)
type(os_error), allocatable, intent(out) :: error
type(os_error), allocatable :: error
call emits_os_es_error_only(error, ...)
if (allocated(error)) then
! Do something with the info found in an os_error type
...
end if
subroutine emits_multiple_error_types(error, ...)
class(critical_error), allocatable, intent(out) :: error
select type if it wants to differentiate between the different error types:
class(critical_error), allocatable :: error
call emits_multiple_error_types(error, ...)
if (allocated(error)) then
select type (error)
type is (os_error)
! handle os-error
...
end select
end if
@wclodius2 As for the naming convention, I've opened issue #225 on derived type naming conventions.
I am not sure I fully agree with that. For example, for the function
mean, the dimensiondimcan be provided (with 0 <dim< maximum_rank_array). Currently if the user provides a wrongdim, a message is printed anderror stopis called.
This is similar to what happen with the intrinsicsum(withgfortran):
[...]
I don't think that returningNaNin this case would be correct, but this strategy would allow pure functions.
@jvdp1 Yes, returning NaN here were not correct, for sure. However, in my opinion, this case is a clear programming error, as the caller is responsible to set a dimension which is compatible with the data it passes to the function (the same way as the programmer is responsible not to address elements beyond the array sizes). In those cases I'd find stop error with a proper error message acceptable and probably the best/only option we have at the moment, especially if the function is supposed to be pure.
The error-reporting mechanism here is more for cases, where the error is expected by design as the success of the call can not be warranted by the programmer/caller, like
@aradi your test program, test_fxerror, doesn't appear to have an example of a call of a procedure with an optional STAT argument. When I try to imagine how the code would be set up for an optional argument I don't see how having the finalizer simplifies things. If it is used for the optional STAT argument, it will have to be guarded by IF blocks in such a way that if it exists it never goes out of scope, while if it is used for local variables it will always go out of scope.
As near as I can tell the most user friendly way to propagate errors
in Fortran is through an optional argument, typically called STAT or
STATUS. The programmer ensures that if STAT is present the
resulting argument has information about the nature of any error
encountered in executing the subprogram, and if STAT is absent the
program stops if an error is found and reports details about the
nature of the error to either ERROR_UNIT or an appropriate log
file. The main questions to be answered are:
STAT convey;STAT have; andI will put off discussing the form of error reporting and focus on
the information STAT could convey, and options for the type of
STAT.
There are two categories of information that a STAT argument might
convey: the severity of the error, and the cause of the error.
FLIBS and
Futility
seem to focus on the severity of the error providing values
representing:
The severity flags seem to be more useful for logging routines
and not for use as a STAT argument. My cursory review indicates that
the FLIBS and Futility library codes focus on error reporting and not
on error handling. As to the cause of the error, in many cases a
library code is well focussed and can have only one cause of
errors. For such codes a simple success/failure flag is sufficient to
identify the cause. But for some codes there can be multiple causes of
failure, e.g., a code that reads a value out of a file may fail
because the file does not exist, the file exists, but does not have
read privileges, an end of file was found before the desired value,
the value did not have the desired form, or the value was outside its
expected range. Each cause has a different possible fix, and each
cause should be distinguished by the STAT argument value. They can
be distinguished by either an ad hoc module specific set of error
codes, or by a library defined set of error codes.
The most obvious type for STAT is a default INTEGER. This is what
the Fortran standard has historically used, and, as a result, what
every Fortran programmer is familiar with. The use of an integer has a
bad rap as the standard did nothing for decades about standardizing
the values, so that all a user could rely on was that a non-zero value
indicated that an error had occurred. Even when it standardized the
meaning of some values in ISO_FORTRAN_ENV it limited the number of
defined values to a handful. However integers could be used for
either an application specific enumeration of error codes, or for a
library wide enumeration of error code values. This allows it to
potentially convey a significant amount of information about the
nature of an error. It has the advantage that no wrapper procedures
are needed for comparisons or setting values, so it can be very
efficient. A problem with an application specific enumeration is that
different applications may use the same numeric value for different
errors, or different numeric values for essentially the same error. A
problem with a library wide enumeration is that it will initially be
incomplete, requiring updates. Another problem is that it will be
large, I suspect with more than a hundred values, so finding the
appropriate code will require a detailed search. FWIW I have created
my own enumeration of error codes. It numbers over 90 entries, though
some are, I believe, relatively useless so trimming might lower the
number of codes to about 80.
The other option is a derived type. Such a derived type can
distinguish between different errors in several ways:
By a component that is an integer that takes on different values
for different categories of errors; or
By a component that is another intrinsic type that takes on
different values for different errors; or
By having the derived type be a class and using inheritance to
create different classes that represent different errors.
An integer component has many of the advantages and disadvantages of
using an integer directly. Unless a library wide enumeration of values
is provided, different applications may use the same value to
represent different sources of errors or different values to
represent the same error. If the component is not made private, then
comparisons and setting values can be very efficient, at the syntactic
cost of consistently referring to the component. If the component is
made private then comparisons and setting values will require the
(implicit) use of simple procedures, that will require inter-module
optimization to reduce the cost.
Using other intrinsic types as component to distinguish different
errors is problematic. I cannot see enumerating errors using a REAL
or COMPLEX, though it can be done. A LOGICAL value can be used to
note whether an error is active, but not to meaningfully distinguish
errors. A CHARACTER string can be used to distinguish errors, but
the comparisons would have a significant performance cost. Still a
CHARACTER string component can be used to supplement an INTEGER
component, by providing an error specific message. The usefulness
of this message depends on whether information about the error should
be specific to where it is first detected, or to where it is
reported. In the first case the string is useful, in the second case
it is much less useful.
Using the class hierarchy to distinguish different errors has one
major advantage: the processor will ensure that different names of
errors will correspond to different values. However the same name can
be used by different applications, and if defined in different modules
the same name can correspond to different values. This problem again
might be addressed by a library wide enumeration of error
codes. However from @aradi's code it seems that ALLOCATE needs to be
used to activate an error and MOVE_ALLOC needs to be used to
associate that specific error with the nominal STAT argument, though
maybe `ALLOCATE( TYPE(error-type-name):: STAT ) might work instead. In
any case the syntax would be unintuitive to a novice.
The above analysis prompts the following questions:
Should the library support STAT arguments? (yes/no)
If it supports STAT arguments should they represent severity or
cause?
If it supports STAT arguments should they be:
A. Integers?
B. A derived type wrapper around integers with the component public?
C. A derived type wrapper around integers with the component private?
D. A derived type that uses the class hierarchy to distinguish errors?
If the library uses a derived type to support STAT arguments, should
it have a CHARACTER string component, e.g., MSG?
If the library uses a derived type to support STAT arguments, should
it follow the integer path of using a special value to indicate that
no error was found or should there be an internal logical(?) flag to
indicate whether an error occurred?
If the library supports STAT arguments, should it provide a
predefined enumeration of error codes, or should it leave it up to the
module's programmer to come up with an ad hoc enumeration of module
specific codes?
FWIW my answers to the above questions are that the library should
support STAT arguments, that they should represent the cause of the
error, that they should be either integers or a derived type wrapper
around integers with the component public, with a mild preference for
simple integers, that the derived type, if it is used, should have a
string component, that it should use a special value to indicate that
no error occurred, and that it should provide a pre-defined
enumeration of error codes.
I pretty much agree with everything that William wrote and have the same preferences (slight preference for an integer, but not opposed to a derived type).
On Wed, Jul 29, 2020, at 5:15 PM, William B. Clodius wrote:
As near as I can tell the most user friendly way to propagate errors
in Fortran is through an optional argument, typically calledSTATor
STATUS. The programmer ensures that ifSTATis present the
resulting argument has information about the nature of any error
encountered in executing the subprogram, and ifSTATis absent the
program stops if an error is found and reports details about the
nature of the error to eitherERROR_UNITor an appropriate log
file. The main questions to be answered are:
- What information should
STATconvey;- What type should
STAThave; and- What form should the error reporting take?
I will put off discussing the form of error reporting and focus on
the informationSTATcould convey, and options for the type of
STAT.There are two categories of information that a
STATargument might
convey: the severity of the error, and the cause of the error.
FLIBS http://flibs.sourceforge.net/m_exception.html and
Futility https://github.com/CASL/Futility/blob/master/src/ExceptionHandler.f90
seem to focus on the severity of the error providing values
representing:
- Information - provides information to the user without stopping;
- Warning - provides a warning to the user without stopping;
- Error - that will stop processing only if stop on error is set;
- Fatal Error - that will always stop processing; and
- Failure - not well defined
The severity flags seem to be more useful for logging routines
and not for use as aSTATargument. My cursory review indicates that
the FLIBS and Futility library codes focus on error reporting and not
on error handling. As to the cause of the error, in many cases a
library code is well focussed and can have only one cause of
errors. For such codes a simple success/failure flag is sufficient to
identify the cause. But for some codes there can be multiple causes of
failure, e.g., a code that reads a value out of a file may fail
because the file does not exist, the file exists, but does not have
read privileges, an end of file was found before the desired value,
the value did not have the desired form, or the value was outside its
expected range. Each cause has a different possible fix, and each
cause should be distinguished by theSTATargument value. They can
be distinguished by either an ad hoc module specific set of error
codes, or by a library defined set of error codes.The most obvious type for
STATis a defaultINTEGER. This is what
the Fortran standard has historically used, and, as a result, what
every Fortran programmer is familiar with. The use of an integer has a
bad rap as the standard did nothing for decades about standardizing
the values, so that all a user could rely on was that a non-zero value
indicated that an error had occurred. Even when it standardized the
meaning of some values inISO_FORTRAN_ENVit limited the number of
defined values to a handful. However integers could be used for
either an application specific enumeration of error codes, or for a
library wide enumeration of error code values. This allows it to
potentially convey a significant amount of information about the
nature of an error. It has the advantage that no wrapper procedures
are needed for comparisons or setting values, so it can be very
efficient. A problem with an application specific enumeration is that
different applications may use the same numeric value for different
errors, or different numeric values for essentially the same error. A
problem with a library wide enumeration is that it will initially be
incomplete, requiring updates. Another problem is that it will be
large, I suspect with more than a hundred values, so finding the
appropriate code will require a detailed search. FWIW I have created
my own enumeration of error codes. It numbers over 90 entries, though
some are, I believe, relatively useless so trimming might lower the
number of codes to about 80.The other option is a derived type. Such a derived type can
distinguish between different errors in several ways:
By a component that is an integer that takes on different values
for different categories of errors; orBy a component that is another intrinsic type that takes on
different values for different errors; orBy having the derived type be a class and using inheritance to
create different classes that represent different errors.An integer component has many of the advantages and disadvantages of
using an integer directly. Unless a library wide enumeration of values
is provided, different applications may use the same value to
represent different sources of errors or different values to
represent the same error. If the component is not made private, then
comparisons and setting values can be very efficient, at the syntactic
cost of consistently referring to the component. If the component is
made private then comparisons and setting values will require the
(implicit) use of simple procedures, that will require inter-module
optimization to reduce the cost.Using other intrinsic types as component to distinguish different
errors is problematic. I cannot see enumerating errors using aREAL
orCOMPLEX, though it can be done. ALOGICALvalue can be used to
note whether an error is active, but not to meaningfully distinguish
errors. ACHARACTERstring can be used to distinguish errors, but
the comparisons would have a significant performance cost. Still a
CHARACTERstring component can be used to supplement anINTEGER
component, by providing an error specific message. The usefulness
of this message depends on whether information about the error should
be specific to where it is first detected, or to where it is
reported. In the first case the string is useful, in the second case
it is much less useful.Using the class hierarchy to distinguish different errors has one
major advantage: the processor will ensure that different names of
errors will correspond to different values. However the same name can
be used by different applications, and if defined in different modules
the same name can correspond to different values. This problem again
might be addressed by a library wide enumeration of error
codes. However from @aradi https://github.com/aradi's code it seems
thatALLOCATEneeds to be
used to activate an error andMOVE_ALLOCneeds to be used to
associate that specific error with the nominalSTATargument, though
maybe `ALLOCATE( TYPE(error-type-name):: STAT ) might work instead. In
any case the syntax would be unintuitive to a novice.The above analysis prompts the following questions:
Should the library support
STATarguments? (yes/no)If it supports
STATarguments should they represent severity or
cause?If it supports
STATarguments should they be:A. Integers?
B. A derived type wrapper around integers with the component public?
C. A derived type wrapper around integers with the component private?
D. A derived type that uses the class hierarchy to distinguish errors?
If the library uses a derived type to support
STATarguments, should
it have aCHARACTERstring component, e.g.,MSG?If the library uses a derived type to support
STATarguments, should
it follow the integer path of using a special value to indicate that
no error was found or should there be an internal logical(?) flag to
indicate whether an error occurred?If the library supports
STATarguments, should it provide a
predefined enumeration of error codes, or should it leave it up to the
module's programmer to come up with an ad hoc enumeration of module
specific codes?FWIW my answers to the above questions are that the library should
supportSTATarguments, that they should represent the cause of the
error, that they should be either integers or a derived type wrapper
around integers with the component public, with a mild preference for
simple integers, that the derived type, if it is used, should have a
string component, that it should use a special value to indicate that
no error occurred, and that it should provide a pre-defined
enumeration of error codes.â
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/fortran-lang/stdlib/issues/224#issuecomment-665977853, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAFAWHYBAVPYKCH4OJNH23R6CUR7ANCNFSM4PFPIXJQ.
@wclodius2 The finalizer in my approach made sure, that one can not let the error being unhandled: If the subroutine returns an error, it must be handled (and explicitely deactivated) before it goes out of scope. Especially, anti-patterns like
! We pass the optional error argument, but do not hande it afterwards before passing it to the next routine.
call some_routine(error=error, ...)
call other_routine(error=error,...)
...
would give then a run-time error. If that is too much of constraints, we could leave away the finalizer. Then, we were basically back to my original proposal using a derived type as status container. No allocation, no move_alloc necessary then.
I would still argue for using derived types / classes for error classification, as this is the only way can stay flexible with the payload of the error. Some (maybe most) errors only return a simple error message, but sometimes also additiional data could be useful to understand the details of the error (numerical value of the determinant of the matrix, which could not be inverted...). We can make the error msg + integer combination the default case, but it should be possible to go beyond that, if needed.
The following minimal working example demonstrates what I vaguealy have in mind:
module error_handling
implicit none
private
public :: general_error, status, raise_error
type :: general_error
character(:), allocatable :: msg
integer :: error_code
contains
procedure :: as_char => general_error_as_char
end type general_error
type :: status
class(general_error), allocatable :: error
contains
procedure :: is_ok => status_is_ok
procedure :: has_failed => status_has_failed
end type status
interface general_error
module procedure construct_general_error
end interface general_error
contains
pure function status_is_ok(this) result(isok)
class(status), intent(in) :: this
logical :: isok
isok = .not. allocated(this%error)
end function status_is_ok
pure function status_has_failed(this) result(hasfailed)
class(status), intent(in) :: this
logical :: hasfailed
hasfailed = allocated(this%error)
end function status_has_failed
function construct_general_error(msg, errorcode) result(this)
character(:), allocatable :: msg
integer, intent(in) :: errorcode
type(general_error) :: this
this%msg = msg
this%error_code = errorcode
end function construct_general_error
function general_error_as_char(this) result(errorchar)
class(general_error), intent(in) :: this
character(:), allocatable :: errorchar
character(100) :: buffer
write(buffer, "(' (Error code: ',I0,')')") this%error_code
errorchar = this%msg // trim(buffer)
end function general_error_as_char
subroutine raise_error(stat, error)
type(status), optional, intent(out) :: stat
class(general_error), intent(in) :: error
character(:), allocatable :: errormsg
if (present(stat)) then
stat%error = error
else
errormsg = error%as_char()
error stop errormsg
end if
end subroutine raise_error
end module error_handling
program test_error_handling
use error_handling
type(status) :: stat
call error_raiser(stat, .true.)
if (stat%has_failed()) then
print *, "Error occured: ", stat%error%as_char()
end if
contains
subroutine error_raiser(stat, raise)
type(status), optional, intent(out) :: stat
logical, intent(in) :: raise
if (raise) then
! It either sets an error in stat, or stops if stat was not present
call raise_error(stat, general_error("Some error occured", -1))
return
end if
end subroutine error_raiser
end program test_error_handling
Errors with more complicated payload could be derived from type(general_error). If the handler wished to access the additional details, it would have to use select type to access the content of the derived error type.
@aradi it is not clear to me that being flexible with the payload of an error is that useful. If you need detailed context for handling the error then you are better off handling it where it is first detected and you have all the context available. What errors do you see needing large payloads?
FWIW passing unbounded objects on the stack is one of Herb Sutters' complaints about the C++ exception handling system: "Zero-overhead deterministic exceptions: Throwing valuesâ, http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0709r0.pdf http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0709r0.pdf . According to his claims it has a significant run-time cost, and little benefit.
On Jul 30, 2020, at 8:28 AM, BĂĄlint Aradi notifications@github.com wrote:
@wclodius2 https://github.com/wclodius2 The finalizer in my approach made sure, that one can not let the error being unhandled: If the subroutine returns an error, it must be handled (and explicitely deactivated) before it goes out of scope. Especially, anti-patterns like
! We pass the optional error argument, but do not hande it afterwards before passing it to the next routine.
call some_routine(error=error, ...)
call other_routine(error=error,...)
...
would give then a run-time error. If that is too much of constraints, we could leave away the finalizer. Then, we were basically back to my original proposal using a derived type as status container. No allocation, no move_alloc necessary then.I would still argue for using derived types / classes for error classification, as this is the only way can stay flexible with the payload of the error. Some (maybe most) errors only return a simple error message, but sometimes also additiional data could be useful to understand the details of the error (numerical value of the determinant of the matrix, which could not be inverted...). We can make the error msg + integer combination the default case, but it should be possible to go beyond that, if needed.
The following minimal working example demonstrates what I vaguealy have in mind:module error_handling
implicit none
privatepublic :: general_error, status, raise_error
type :: general_error
character(:), allocatable :: msg
integer :: error_code
contains
procedure :: as_char => general_error_as_char
end type general_errortype :: status
class(general_error), allocatable :: error
contains
procedure :: is_ok => status_is_ok
procedure :: has_failed => status_has_failed
end type statusinterface general_error
module procedure construct_general_error
end interface general_errorcontains
pure function status_is_ok(this) result(isok)
class(status), intent(in) :: this
logical :: isokisok = .not. allocated(this%error)end function status_is_ok
pure function status_has_failed(this) result(hasfailed)
class(status), intent(in) :: this
logical :: hasfailedhasfailed = allocated(this%error)end function status_has_failed
function construct_general_error(msg, errorcode) result(this)
character(:), allocatable :: msg
integer, intent(in) :: errorcode
type(general_error) :: thisthis%msg = msg this%error_code = errorcodeend function construct_general_error
function general_error_as_char(this) result(errorchar)
class(general_error), intent(in) :: this
character(:), allocatable :: errorcharcharacter(100) :: buffer write(buffer, "(' (Error code: ',I0,')')") this%error_code errorchar = this%msg // trim(buffer)end function general_error_as_char
subroutine raise_error(stat, error)
type(status), optional, intent(out) :: stat
class(general_error), intent(in) :: errorcharacter(:), allocatable :: errormsg if (present(stat)) then stat%error = error else errormsg = error%as_char() error stop errormsg end ifend subroutine raise_error
end module error_handling
program test_error_handling
use error_handlingtype(status) :: stat
call error_raiser(stat, .true.)
if (stat%has_failed()) then
print *, "Error occured: ", stat%error%as_char()
end ifcontains
subroutine error_raiser(stat, raise)
type(status), optional, intent(out) :: stat
logical, intent(in) :: raiseif (raise) then ! It either sets an error in stat, or stops if stat was not present call raise_error(stat, general_error("Some error occured", -1)) return end ifend subroutine error_raiser
end program test_error_handling
Errors with more complicated payload could be derived from type(general_error). If the handler wished to access the additional details, it would have to use select type to access the content of the derived error type.â
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub https://github.com/fortran-lang/stdlib/issues/224#issuecomment-666399956, or unsubscribe https://github.com/notifications/unsubscribe-auth/APTQDOTNCURVZUJFVSOTMD3R6F7SJANCNFSM4PFPIXJQ.
I like the
type(status)extensible type. But of course, what we really need is proper exception handling in the language. Anything else is just a stop gap solution. (like the current integer and string error messages of some of the intrinsic routines). It's just not enough in 2020.
I agree that the error handling of Fortran is quite cumbersome. error stop with a fixed error message (i.e no runtime information can be communicated to the user) seems to be the best that the language offers. I have used error stop in the os and os_path implementations and figured out that it gives at least a reasonable stack trace.
For subroutines, the error handling by means of return values is quite reasonable, but for functions with one return value it becomes annoying. If I remember correctly, intent(out) is not even allowed in pure functions.
I would prefer if basic building blocks of the stdlib would use error stop without more elaborated error handling. In addition, stdlib should provide reasonable error handling/logging utilities that allow the user to deal with unexpected situations. Consider the following case:
call chdir('this_dir_does_not_exist') ! will terminate with error stop
if(.not. isdir('this_dir_does_not_exist')) then
call stdlib_error_handler('this_dir_does_not_exist'// 'does not exist')
else
call chdir('this_dir_does_not_exist')
endif
This means extra work for the user, but all intrinsic statements (i.e. open) work like that.
If we would enforce error propagation by means of an error type, we would essentially forbid the use of pure functions!
Fortran 2018 increased the functionality of ERROR STOP by:
Allowing it in pure procedures
Allowing it to have runtime stop codes.
However the num her of processors "in the wildâ that donât implement this part of F18 is still very large, and it is probably best that the standard library limit itself to F08 functionality. In F08 runtime stop codes can sometimes be kluged, by using SELECT CASE to choose at runtime among a fixed set of static stop codes.
Constraint C1583 in my copy of the draft of the standard for PURE functions requires that all non-pointer arguments of the function have the INTENT(IN) or VALUE attribute.
Even without the constraint Fortran imposes only a partial order of evaluation for many expression, so functions with side effects, such as INTENT(INOUT) or INTENT(OUT) are generally a bad idea unless you can guarantee they will only be used with great discipline.
Pure functions can indicate a problem was found either by returning a special value, e.g. NaN for REAL function results, or -1 for some INTEGER function results, or by invoking ERROR STOP in F18.
FWIW I have taken to using the idiom for error handling roughly equivalent to
if (.not. isdir(the_dir) ) then
if ( present(stat) ) then
stat = missing_dir_failure
return
else
call error_handler( the_dir // â is missingâ, &
module = this_module, &
procedure = this_procedure, &
stat = missing_dir_failure ) ! invoking error stop
end if
else
opent(newlun=lun, file=the_dir // filename, ⊠)
end if
Note that following FLIBS and FUTIL I am considering adding a LEVEL keyword to ERROR_HANDLER which can take as arguments predefined values: INFORMATION, WARNING, ERROR, or FATAL_ERROR.
If the conditions that lead to an error stop are "identifiable" from the
outside, like the above example, then we can always allow for a wrapper
that checks the error conditions first. Such a wrapper can then be used to
pass on the information that something went wrong and allow for error
recovery. Whether a programmer wants to use the underlying routine or the
wrapper is then up to them.
This would be more difficult, if you have, say, a matrix solver, that
halfway through, concludes that the matrix is near-singular and then stops.
This would not be something easily checked in advance.
Regards,
Arjen
Op vr 31 jul. 2020 om 19:08 schreef Martin Diehl notifications@github.com:
I like the type(status) extensible type. But of course, what we really
need is proper exception handling in the language. Anything else is just a
stop gap solution. (like the current integer and string error messages of
some of the intrinsic routines). It's just not enough in 2020.I agree that the error handling of Fortran is quite cumbersome. error stop
with a fixed error message (i.e no runtime information can be communicated
to the user) seems to be the best that the language offers. I have used error
stop in the os and os_path implementations and figured out that it gives
at least a reasonable stack trace.
For subroutines, the error handling by means of return values is quite
reasonable, but for functions with one return value it becomes annoying. If
I remember correctly, intent(out) is not even allowed in pure functions.I would prefer if basic building blocks of the stdlib would use error stop
without more elaborated error handling. In addition, stdlib should provide
reasonable error handling/logging utilities that allow the user to deal
with unexpected situations. Consider the following case:call chdir('this_dir_does_not_exist') ! will terminate with error stop
if(.not. isdir('this_dir_does_not_exist')) then
call stdlib_error_handler('this_dir_does_not_exist'// 'does not exist')
else
call chdir('this_dir_does_not_exist')
endifThis means extra work for the user, but all intrinsic statements (i.e.
open) work like that.If we would enforce error propagation by means of an error type, we would
essentially forbid the use of pure functions!â
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/fortran-lang/stdlib/issues/224#issuecomment-667233550,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AAN6YR342UMCE64AYSM3ROTR6L3B7ANCNFSM4PFPIXJQ
.
@MarDiehl I'd personally would have very-very strong objections against letting low level I/O-routines die on error with error stop without the possibility of preventing it via error reporting / handling. Think about a Fortran library linked to a big graphical UI. It is a no-go, if an entire UI just crashes / stops without saving user data, just because a badly written Fortran routine at the very depth is not able to handle / propagate errors.
In my opinion, there must be always an alternative to error stop whenever
How, the error is indicated/propagated/handled is of course a matter of taste (and topic of this issue :wink: ) and can even differ in each case, but error stop without an alternative can not be a serious choice for a standard library IMO. We can offer the error stop solution if the caller wants it (e.g. by making the error/status argument optional and stopping on error if not present.) But we also provide an alternative way, so that one can write robust components of big packages in Fortran.
Your proposed error handling for change_directory is not robust IMO. It would be quite elaborate to find out in advance whether you can change into a directory as you would also have to check for the proper access rights for example (which is usually not covered by is_dir()). And even then, it may happen, that a concurrent process deletes the directory between is_dir() and chdir. The simplest and most robust way to know, whether you can change into it is to try it (make the appropriate libc-call), report the error on failure and let the caller decide.
As for pure functions: I think, we only should allow pure functions, where:
-1 or NaN).Note that I am not referring here to programming errors as mentioned by @jvdp1 . If you pass the wrong dimension to a routine, it is OK if it calls stop error as this a clear programming error.
You have convinced me that error stop is a bad idea and will change os and os_pathaccordingly.
My suggestion would be to follow the Fortran way and use integer return values and/or return special values. This is for example also done in the open statement. The user can then handle errors from stdlib and the core language in a similar way.
In also think that it makes sense to offer a stdlib solution for error handling (a special type/printing messages etc). However, I would like to avoid a strong coupling of individual parts from stdlib. Therefore, I'm opposing to use this error handling routine in basic routines like os and os_path. That would force the user to also use the error handling routine (which is not part of the language, but 'just' a part of the standard library). If it turns out that there is a commonly used pattern of combining file system routines and error handling, this could become a second level module (similar to os.path and Pathlib in python: A more direct, low level library and a more advanced, OO-based approach).
In the codes I am attempting to write, I find error handling to be a pervasive problem and think that strong coupling to library defined error handling to be unavoidable. I do not want each module to provide its own logging system. I therefore want the library to provide strong quality error handling routines. For other applications strong coupling to the file handling and os services may be unavoidable.
Take our linalg as an example.
The very low level API is Lapack itself, which does not use derived types (thus there could be 15 arguments, but everything is essentially pure, no side effects) nor error stop, errors are propagated up, the user has to check them and handle them. This design ensures maximum reusability, it truly works for everybody.
The rest of the work is providing optional layers on top to simplify the API for some users, but some other users will always use Lapack directly.
The mid level API that I am hoping will go to stdlib is to provide simple Matlab/Python like functions such as y=solve(A, x). This has the following features: still no derived types, but it will fail with error stop is the matrix is singular, to ensure users code will never continue with bogus answers. We can optionally add a status variable, which if provided will propagate the error up. This API is for a subset of users who want to quickly prototype their code and just solve a matrix, not having to lose time figuring out the 15 arguments for lapack, and they want it to fail with error stop.
Then there can be an OO style API, like solver = Factorize(A); y=solver.solve(x).
There can be various ways to do error handling, different for each API. But it all starts having the procedural low level API that can be used as a building blocks for everything else.
On Sat, Aug 1, 2020, at 7:51 AM, William B. Clodius wrote:
In the codes I am attempting to write, I find error handling to be a
pervasive problem and think that strong coupling to library defined
error handling to be unavoidable. I do not want each module to provide
its own logging system. I therefore want the library to provide strong
quality error handling routines. For other applications strong coupling
to the file handling and os services may be unavoidable.â
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/fortran-lang/stdlib/issues/224#issuecomment-667535468, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAFAWCTYID2FTFMCZ3TXCLR6QMWNANCNFSM4PFPIXJQ.
Do we want users to have the option of sending the error messages, if the optional STAT is not present, to their log files as well as the ERROR_UNIT of ERROR STOP? If we do then we want the library to consistently use our logging routines for error reporting.
What I had in mind for the y=solve(A,x) kind of middle level API is that it would just call error stop with some nice explanation, and that's it. If users want something more sophisticated, then they have to provide the optional stat argument, or use the lower level or more OO oriented API. That way functions like solve can stay pure.
Do we want users to have the option of sending the error messages, if the optional STAT is not present, to their log files as well as the ERROR_UNIT of ERROR STOP? If we do then we want the library to consistently use our logging routines for error reporting.
I would suggest to make it consistent with the language features: exit with error stop if stat is not present and an error occurs, otherwise do not exit and inform via stat. This is at least the case for read and open
What I had in mind for the
y=solve(A,x)kind of middle level API is that it would just call error stop with some nice explanation, and that's it. If users want something more sophisticated, then they have to provide the optional stat argument, or use the lower level or more OO oriented API. That way functions likesolvecan stay pure.
y=solve(A,x) with optional stat argument cannot be pure, it would need to be call solve(A,x,y) or call solve(A,x,y,err). Pure functions can have only intent(in) arguments, unless for pointers. Could make sense to define the a pointer solution for the error handling routines.
But apart of this, I like the approach to have a error handling type for more involved/abstract functions and use plain integer for simple functions.
I would suggest to make it consistent with the language features: exit with
error stopifstatis not present and an error occurs, otherwise do not exit and inform viastat. This is at least the case forreadandopen
and write, inquire, close, rewind, backspace, allocate, deallocate, the "atomic" subroutines, and basically anything else that can go wrong that the processor can detect. There are two limitations with error stop by itself for this application: first, in Fortran 08 it cannot accept variable strings as arguments; and, second, it is awkward by itself to send detailed messages. Both of these issues could be dealt with by having a logging system in the standard library.
But apart of this, I like the approach to have a error handling type for more involved/abstract functions and use plain integer for simple functions.
There is no simple dividing line between involved/abstract functions and simple functions. I like using the same approach for all procedures.
To facilitate discussion of this issue I thought it would be useful to
code up examples for different approaches to error handling where
additional information might be useful. As my example I chose an
an I/O failure on an OPEN statement, where the standard provides
addional data in the form of an IOSTAT and IOMSG. In practice, I
thought an I/O failure would be more amenable to handling, than an
allocation or co-array related failure, where the standard provides
additional information in the form of a STAT (or STATUS) and
ERRMSG. For my examples I choose a simple flattend derived type,
scalar integer codes, and a variant of @aradi's derived type that has a
derived type as an attribute. In all examples I assume the existence
of a module ERROR_CODES with predefined values for the most obvious
error codes. FWIW I have such a module with about 100 error codes
defined.
For my first example of a handling method I chose a flattened derived
type, ERROR_T, with the attributes, CODE, MSG, STAT, and
ERRMSG as one that could handle both IOSTAT with IOMSG and
STAT with ERRMSG. As testing for the value of the CODE value is
both a cheap and relatively intuitive means of determining whether an
error has occured I don't define IS_OK or HAS_FAILED
functions. Additional attributes MODULE and PROCEDURE to indicate
the source of the error, might also be usefully added to ERROR_T.
module error_handling
use error_codes, only : success
implicit none
private
public :: error_t, error
type :: error_t
integer :: code = success
character(:), allocatable :: msg
integer :: stat = 0
character(:), allocatable :: errmsg
contains
procedure :: as_char => error_as_char
end type error_t
interface error
module procedure construct_error
end interface error
contains
function construct_error(msg, code, errmsg, stat) &
result(this)
character(:), intent(in) :: msg
integer, intent(in) :: code
character(:), intent(in), optional :: errmsg
integer, intent(in), optional :: stat
type(error_t) :: this
this % msg = msg
this % code = code
if ( present(errmsg) ) then
this % errmsg = errmsg
end if
if ( present(stat) ) then
this % stat = stat
end if
end function construct_error
function error_as_char(this) result(errorchar)
type(error_t), intent(in) :: this
character(:), allocatable :: errorchar
character(100) :: buffer
write(buffer, "(' (Error code: ',I0,')')") this % code
errorchar = trim(this % msg) // trim(buffer)
end function error_as_char
end module error_handling
program test_error_t_handling
use error_handling
use error_codes, only: success, missing_file_failure, &
open_failure
type(error_t) :: stat
call error_raiser(stat)
if (stat % code /= success) then
print *, "Error occured: ", stat % as_char()
write(output_unit, '("IOMSG =", a)' ) stat % errmsg
write(output_unit, '("IOSTAT = ", i0)' ) stat % stat
end if
contains
subroutine error_raiser(stat)
type(error_t), optional, intent(out) :: stat
logical :: exist
integer :: iostat, lun
character(256) :: iomsg
open( newunit=lun, file='dummy.txt', status='old', &
form='formatted', action='read', err=999, &
iostat=iostat, iomsg=iomsg )
return
999 inquire( file='dummy.txt' exist=exist )
if ( exist ) then
! May never happen
if ( present(stat) ) then
stat = error( msg='Unable to open existing' // &
' "OLD" file "dummy.txt".', &
code = open_failure, errmsg=trim(iomsg), &
stat=iostat )
return
else
write(output_unit, '("Procedure: ERROR_RAISER")' )
write(output_unit, '("IOMSG =", a)' ) iomsg
write(output_unit, '("IOSTAT = ", i0)' ) iostat
error stop 'Unable to open existing "OLD" file ' // &
'"dummy.txt".'
end if
else
! May be due to a lack of READ priviledges for the file
if ( present(stat) ) then
stat = error( msg='Missing "OLD" file "dummy.txt".', &
code = missing_file_failure, errmsg=trim(iomsg), &
stat=iostat )
return
else
write(output_unit, '("Procedure: ERROR_RAISER")' )
write(output_unit, '("IOMSG =", a)' ) iomsg
write(output_unit, '("IOSTAT = ", i0)' ) iostat
error stop 'Missing "OLD" file "dummy.txt".'
end if
end if
end subroutine error_raiser
end program test_error_t_handling
As an alternative, I show the handling with a simple integer error
code. This, of course, lacks the derived type's MSG, STAT, and
ERRMSG attributes. FWIW for the I/O errors I suspect the fixes would
involve either moving a file or changing its priviledges, and for
those actions I suspect the error code is sufficient and the other
attributes are unnecessary.
program test_integer_handling
use error_codes, only: success, missing_file_failure, &
open_failure
integer :: stat
call error_raiser(stat)
if (stat /= success) then
select case ( stat )
case( open_failure )
print *, "A failure occurred on an OPEN statement."
case( missing_file_failure )
print *, "A missing file failure occured."
end select
end if
contains
subroutine error_raiser(stat)
integer, optional, intent(out) :: stat
logical :: exist
integer :: iostat, lun
character(256) :: iomsg
open( newunit=lun, file='dummy.txt', status='old', &
form='formatted', action='read', err=999, &
iostat=iostat, iomsg=iomsg )
return
999 inquire( file='dummy.txt' exist=exist )
if ( exist ) then
! May never happen
if ( present(stat) ) then
stat = open_failure
return
else
write(output_unit, '("Procedure: ERROR_RAISER")' )
write(output_unit, '("IOMSG =", a)' ) iomsg
write(output_unit, '("IOSTAT = ", i0)' ) iostat
error stop 'Unable to open existing "OLD"' // &
' file "dummy.txt".'
end if
else
! May be due to a lack of READ priviledges for the file
if ( present(stat) ) then
stat = missing_file_failure
return
else
write(output_unit, '("Procedure: ERROR_RAISER")' )
write(output_unit, '("IOMSG =", a)' ) iomsg
write(output_unit, '("IOSTAT = ", i0)' ) iostat
error stop 'Missing "OLD" file "dummy.txt".'
end if
end if
end subroutine error_raiser
end program test_integer_handling
Finally I show a modified version of @aradi's code in which I extend
his GENERAL_ERROR with IO_ERROR with the additional attributes
IOSTAT and IOMSG. This also requires defining an additonal
constructor for IO_ERROR. Because defining a new type and associated
constructor for each type of error strikes me as awkward and a pain, I
assume that ERRORCODE is used to distinguish different sub-types of
I/O errors.
module error_handling
implicit none
private
public :: general_error, io_error, status, raise_error
type :: general_error
character(:), allocatable :: msg
integer :: error_code
contains
procedure :: as_char => general_error_as_char
end type general_error
type, extends(general_error) :: io_error
character(:), allocatable :: iomsg
integer :: iostat
end type io_error
type :: status
class(general_error), allocatable :: error
contains
procedure :: is_ok => status_is_ok
procedure :: has_failed => status_has_failed
end type status
interface general_error
module procedure construct_general_error
end interface general_error
interface io_error
module procedure construct_io_error
end interface io_error
contains
pure function status_is_ok(this) result(isok)
class(status), intent(in) :: this
logical :: isok
isok = .not. allocated(this%error)
end function status_is_ok
pure function status_has_failed(this) result(hasfailed)
class(status), intent(in) :: this
logical :: hasfailed
hasfailed = allocated(this%error)
end function status_has_failed
function construct_general_error(msg, errorcode) &
result(this)
character(*), intent(in) :: msg
integer, intent(in) :: errorcode
class(general_error) :: this
this%msg = msg
this%error_code = errorcode
end function construct_general_error
function construct_io_error(msg, errorcode, iomsg, iostat) &
result(this)
character(*), intent(in) :: msg
integer, intent(in) :: errorcode
character(*), intent(in) :: iomsg
integer, intent(in) :: iostat
class(io_error) :: this
this % msg = msg
this % error_code = errorcode
this % iomsg = iomsg
this % iostat = iostat
end function construct_io_error
function general_error_as_char(this) result(errorchar)
class(general_error), intent(in) :: this
character(:), allocatable :: errorchar
character(100) :: buffer
write(buffer, "(' (Error code: ',I0,')')") this%error_code
errorchar = this%msg // trim(buffer)
end function general_error_as_char
subroutine raise_error(stat, error)
type(status), optional, intent(out) :: stat
class(general_error), intent(in) :: error
character(:), allocatable :: errormsg
if (present(stat)) then
stat%error = error
return
else
errormsg = error%as_char()
error stop errormsg ! Legal in F18 but not in F08
end if
end subroutine raise_error
end module error_handling
program test_error_handling
use error_handling
use error_codes, only: success, missing_file_failure, &
open_failure
type(status) :: stat
call error_raiser(stat)
if (stat%has_failed()) then
print *, "Error occured: ", stat%error%as_char()
end if
contains
subroutine error_raiser(stat)
type(status), optional, intent(out) :: stat
logical :: exist
integer :: iostat, lun
character(256) :: iomsg
open( newunit=lun, file='dummy.txt', status='old', &
form='formatted', action='read', err=999, &
iostat=iostat, iomsg=iomsg )
return
999 inquire( file='dummy.txt' exist=exist )
if ( exist ) then
! May never happen
if ( present(stat) ) then
call raise_error( stat, io_error( msg='Unable' // &
' to open existing "OLD" file "dummy.txt".', &
errorcode = open_failure, errmsg=trim(iomsg), &
stat=iostat ) )
return
else
write(output_unit, '("Procedure: ERROR_RAISER")' )
write(output_unit, '("IOMSG =", a)' ) iomsg
write(output_unit, '("IOSTAT = ", i0)' ) iostat
error stop 'Unable to open existing "OLD" file ' // &
'"dummy.txt".'
end if
else
! May be due to a lack of READ priviledges for the file
if ( present(stat) ) then
call raise_error( stat, io_error( msg='Missing' // &
' "OLD" file "dummy.txt".', &
errorcode = missing_file_failure, &
errmsg=trim(iomsg), &
stat=iostat )
return
else
write(output_unit, '("Procedure: ERROR_RAISER")' )
write(output_unit, '("IOMSG =", a)' ) iomsg
write(output_unit, '("IOSTAT = ", i0)' ) iostat
error stop 'Missing "OLD" file "dummy.txt".'
end if
end if
end subroutine error_raiser
end program test_error_handling
As for the third version, considering our earlier discussions, I'd suggest to use the error type directly, and its allocation status as status flag (not allocated: OK, allocated: error occured). This is more direct and probably also somewhat more efficient. So, it would be something like:
module error_handling
implicit none
private
public :: general_error, io_error
type :: general_error
character(:), allocatable :: msg
integer :: error_code
contains
procedure :: as_char => general_error_as_char
end type general_error
type, extends(general_error) :: io_error
character(:), allocatable :: iomsg
integer :: iostat
end type io_error
interface general_error
module procedure construct_general_error
end interface general_error
interface io_error
module procedure construct_io_error
end interface io_error
contains
function construct_general_error(msg, errorcode) result(this)
character(*), intent(in) :: msg
integer, intent(in) :: errorcode
type(general_error) :: this
this%msg = msg
this%error_code = errorcode
end function construct_general_error
function construct_io_error(msg, errorcode, iomsg, iostat) result(this)
character(*), intent(in) :: msg
integer, intent(in) :: errorcode
character(*), intent(in) :: iomsg
integer, intent(in) :: iostat
type(io_error) :: this
this%msg = msg
this%error_code = errorcode
this%iomsg = iomsg
this%iostat = iostat
end function construct_io_error
function general_error_as_char(this) result(errorchar)
class(general_error), intent(in) :: this
character(:), allocatable :: errorchar
character(100) :: buffer
write(buffer, "(' (Error code: ',I0,')')") this%error_code
errorchar = this%msg // trim(buffer)
end function general_error_as_char
end module error_handling
program test_error_handling
use error_handling
use error_codes, only: success, missing_file_failure, open_failure
type(io_error), allocatable :: error
call ioerror_raiser(error)
if (allocated(error)) then
print *, "Error occured: ", error%as_char()
end if
contains
subroutine ioerror_raiser(error)
type(io_error), allocatable, intent(out), optional :: error
logical :: exist
integer :: iostat, lun
character(256) :: iomsg
open(newunit=lun, file='dummy.txt', status='old', form='formatted',&
& action='read', err=999, iostat=iostat, iomsg=iomsg)
return
999 inquire(file='dummy.txt', exist=exist)
if (exist) then
! May never happen
if (present(error)) then
error = io_error(msg='Unable to open existing "OLD" file "dummy.txt"',&
& errorcode=open_failure, iomsg=trim(iomsg), iostat=iostat)
return
else
write(output_unit, '("Procedure: ERROR_RAISER")' )
write(output_unit, '("IOMSG =", a)' ) iomsg
write(output_unit, '("IOSTAT = ", i0)' ) iostat
error stop 'Unable to open existing "OLD" file ' // &
'"dummy.txt".'
end if
else
! May be due to a lack of READ priviledges for the file
if (present(error)) then
error = io_error(msg='Missing "OLD" file "dummy.txt".', &
& errorcode = missing_file_failure, iomsg=trim(iomsg),&
& iostat=iostat)
return
else
write(output_unit, '("Procedure: ERROR_RAISER")' )
write(output_unit, '("IOMSG =", a)' ) iomsg
write(output_unit, '("IOSTAT = ", i0)' ) iostat
error stop 'Missing "OLD" file "dummy.txt".'
end if
end if
end subroutine ioerror_raiser
end program test_error_handling
Most helpful comment
According to MRC: "Execution of an
error stopstatement causes execution to cease as soon as possible on all images, so there is no need to disallow it in apureprocedure. It is now allowed ..." (It is apparently a Fortran 2018 thing).