Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exceptions and Exception Handling #172

Open
vansnyder opened this issue May 9, 2020 · 22 comments
Open

Exceptions and Exception Handling #172

vansnyder opened this issue May 9, 2020 · 22 comments
Labels
Clause 11 Standard Clause 11: Execution control

Comments

@vansnyder
Copy link

Block-structured exception handling has been provided in other languages for decades. According to "Programming Languages: Principles and Practice," 2nd edition, by Kenneth C. Louden (a notable textbook on programming languages), ``Exception handling was pioneered by the language PL/I in the 1960s and significantly advanced in CLU in the 1970s. However, it was only in the 1980s and early 1990s that design questions were largely resolved.'' Ada 83, for which development began in 1976, is probably the first language that provided block-structured exception handling in the modern form. It is generally agreed that exception handling is commonplace in all modern languages.

It has been observed that the CHANGE TEAM construct described in the 6 November 2014 draft of TS 18508 (ISO/IEC JTC1/SC22/WG5 paper N2033) is, in effect, an exception block, but with an intrinsic (and invisible) exception handler that only manages necessary synchronization and
deallocations.

It's time for Fortran to have a complete block-structured exception handling mechanism. Block-structured exception handling has been proposed for Fortran, but has never been implemented.

Objections have been raised to block-structured exception handling, usually citing performance degradation. While some implementations of block-structured exception handling impose expense even if an exception does not occur, block-structured exception handling does not inevitably
impose a significant execution-time penalty if an exception does not occur. For example, the Janus and Verdix Ada compilers' block-structured exception handling mechanisms impose very low cost if an exception does not occur, and the exception handling mechanism provided by the GNU Ada Translator (GNAT) imposes zero cost if an exception does not occur.

The cost of exception handling should not be confused with the cost of exception detection. An exception cannot be handled unless it is detected. Most processors have methods to specify whether certain exceptions, such as subscripts out of bounds, are detected. The cost of an exception handler is the additional cost to provide for handling an exception, if one is detected. Providing a mechanism to handle an exception does not require or imply that the processor is instructed to detect it. If the processor does detect it, the additional overhead to handle it is very
small, or nonexistent, until the exception is detected. If the processor does not detect it, the additional overhead to handle an exception that cannot occur is very small, or nonexistent.
Exceptions_3.pdf

@FortranFan
Copy link
Member

It's exceptionally important for Fortran to include support for exception handling as a modern language must. Coders can then decide whether to make use of it and where and how in their codes just as it is with IEEE exception handling introduced in Fortran starting with the 2003 standard.

One of my biggest disappointments is with WG5 and how WG5 has reduced the work-list for Fortran 202X to drop items such as exception handling and generics. Interested readers can see this link at comp,lang.fortran. As seen in this link, many a Fortranner - with no real voice or representation due to their physical/virtual locale which is well outside the 2 to 5 country-based and rather institutionally controlled org structure - have been requesting support for exception handling but to no avail.

The way things function with an ISO-driven process and how arcane, archaic, and inefficient the process is when it comes to language advancement, it could very well be decades from now before the ducks get in a row for the standard process to finally work attentively toward exception handling in Fortran. And even then there will always be the argument Fortran has survived that long without explicit exception handling support for coders, so why bother!?

In the meantime, there is another issue that too gets overlooked with this topic: in the industry where I work and the computing domains I am familiar with, code base after code base has moved away Fortran, a lack of support for exception handling and the demands that such a scenario places on the most important resource - developer time and productivity - is among the reasons for the flight away from Fortran.

For a simple example, there is a particular formula based on the pioneering scientific advancements by Gibbs and van der Waals which literally gets computed billions of times by engineers and scientists where I work. This formula, y = f(x1, x2, .. , xN), can be written in a few lines of code and computed trivially. Except when the values of the N independent variables x1 thru' xN are outside the physically acceptable range for practicable design and manufacturing. The occurrence of this is truly an exceptional circumstance but which needs to be handled very carefully, for otherwise there are considerable consequences including that of safety.

Owing to the lack of exception handling support in language, the Fortran codes were rendered exceptionally complex and verbose with developers having paid incalculable amount of attention to the handling of exceptional situations where the input values are outside the physically acceptable range. This was so much so that a new engineer/scientist tasked to work on the code (and mostly with background in MATLAB, Python, C++, Java, .NET) would entirely fail to notice the important formula in the subprogram, it was a mind-blowing departure from the concept of Formula Translation and black-board abstraction.

As I mentioned earlier, the computations involving this formula are now performed in any number of compute environments where Fortran plays little to no role. The code written in other languages is rather compact, readable and maintainable, albeit full of curly braces!! For the purse-holders and budget managers though, the new code is overall better performant in spite of the use of structured exception handling. So they can't be happier with their push away from Fortran.

This is an aspect where implementations such as LFortran, LLVM, etc. can be of great help with prototyping.

@certik
Copy link
Member

certik commented May 12, 2020

I think it is clear that many (if not the most) people in the wide Fortran community want some form of better exception handling.

I also agree that the Committee in general is not handling these requests efficiently. As you know I am running for the WG5 Convenor position and in my platform I propose that we work on these proposals continuously and when they are ready, we will consider them for inclusion. However, no matter who gets selected in the end, let us work on these things, and when they are ready, I will bring them to the Committee to consider.

So let's get to work.

There are several approaches to exception handling. The above proposal is one. Here are other issues with other proposals: #6, #66. There are more.

Ultimately, this must be prototyped in a compiler to show that there is no performance overhead, and also to iron out corner cases, and to see how difficult it is to implement it. We will be able to do such prototypes very soon (if things go well, hopefully in a year or so). Until then, let's at least narrow down the options of what ideas should be prototyped.

Also, let's collect the most common objections and have solid answers to them. Here are some off top of my head:

  • What are the performance implications?
  • Why are many C++ applications turning off exceptions for performance (in gaming industry for example)?
  • The C++ committee does not seem to be super happy with their traditional exceptions, and are proposing a different mechanism: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0709r0.pdf; Wouldn't it be a mistake to do what C++ found is not such a great idea?

And there is a lot more, see the thread at the J3 mailinglist starting with this email:

https://mailman.j3-fortran.org/pipermail/j3/2019-February/011256.html

We have to collect all the objections there and have a good answer, before we bring it back to the committee. Otherwise the same objections will be given and we will not move the discussion forward.

This is the real work that we have to do. I currently don't have time to drive this particular effort, but if somebody is interested in leading this, I will help as much as I can.

@klausler
Copy link

Here is what I think Fortran can do.

  1. Add a statement (say FAIL) that signifies failure with an error code. As you will see, it can appear only within a dynamic context that can handle the error. It causes transfer of control to the most deeply nested handler for its error code. Memory allocations are automatically cleaned up between the FAIL and its handler.
  2. Add a new specifier (say FAILING) to ALLOCATE statements, I/O statements, coarray references, &c. to give them permission to fail on error, END, and EOR conditions, thus allowing these conditions to be handled by the same handlers as if a FAIL statement had been executed.
  3. Add a construct (say TRY <block> [ CATCH (error) <block> ]... [ CATCH DEFAULT <block> ] END TRY) that establishes an error handling context. If a FAIL statement is executed, the appropriate handler block is executed. A TRY construct with no CATCH DEFAULT, or with fallible code in its CATCH DEFAULT block, may appear only in a dynamic context that can handle the uncaught errors.
  4. Add a attribute (I'll spell it FALLIBLE here) to procedures. It signifies that the procedure may fail without catching all errors. A reference to a FALLIBLE procedure may appear only where a FAIL statement could appear -- i.e., a hermetic TRY construct or in another FALLIBLE procedure. The use of FALLIBLE implies an explicit interface.

The FAIL statement, and references to FALLIBLE procedures, may appear only within a hermetic TRY construct if they occur in a DO CONCURRENT loop or main program.

Why I favor this approach:

  • It has no effect on existing code.
  • Static checking can guarantee that all errors will be caught.
  • Runtime overhead is cheap; no more expensive than alternate returns.
  • Implementation would be straightforward; no magic in the linker or runtime library.

TBD:

  • interaction with finalization
  • whether ALLOCATE statements, I/O statements, coarray references, &c. should automatically be fallible if they appear in a TRY construct.
  • how distinct user error codes would be defined

@certik
Copy link
Member

certik commented May 13, 2020

@klausler thanks for this proposal. What I like about it is that it is essentially equivalent to manual alternate returns, and thus does not require any magic at runtime with stack frames or runtime library.

At the February 2019 meeting, we fleshed out two similar / alternative proposals:

https://mailman.j3-fortran.org/pipermail/j3/2019-February/011257.html

See the links at the bottom of the email:

it is also equivalent to manual alternate returns, no magic. I'll try to do this example using your proposal also, so that we can see how it would look like in a code.

@vansnyder
Copy link
Author

vansnyder commented May 13, 2020 via email

@klausler
Copy link

Looks a lot to me like the current C++ exception feature that many significant codebases specifically go out of their way to disable and that C++ is working hard to replace.

@certik
Copy link
Member

certik commented May 14, 2020

@klausler I implemented an example using your proposal, please let me know if I got it right:

@certik
Copy link
Member

certik commented May 14, 2020

@vansnyder I have implemented an example using your proposal, please let me know if I got it right:

@vansnyder
Copy link
Author

vansnyder commented May 14, 2020 via email

@klausler
Copy link

@klausler I implemented an example using your proposal, please let me know if I got it right:

The fallible subroutine can't be called at line 15 from the main program outside an error handling context.

@certik
Copy link
Member

certik commented May 14, 2020

The fallible subroutine can't be called at line 15 from the main program outside an error handling context.

I updated the gist (still the same link), you can see the changes in the "Revisions" tab.

I was wondering about this --- I can see both ways here. Your proposal disallows calling the fallible subroutine outside of an error handling context, which is very explicit and will cause all errors to be explicitly handled, which I think is a nice feature.

Two questions:

  1. How would this be implemented in the compiler? Would there be an implicit additional intent(out) argument __failed for every subroutine that is fallible? And then an implicit if statement right after calling any fallible subroutine that will check this __failed argument and if it is .true., it would immediately return and propagate __failed to the parent subroutine or main program?

  2. Assume you are in the main program or in some non-fallible subroutine. And you need to call a subroutine that is fallible, but you don't want to be handling errors, you just want the program to fail at runtime with a stacktrace, just like currently allocate will fail if you don't set the stat argument. In your proposal currently you have to do:

try
    call mysum(5, s)
catch default
    error stop
end try

Which is quite verbose given that you don't want to handle the error. Would it make sense to create a shortcut for the above code as follows:

try mysum(5, s)

Which would be exactly equivalent to the above code. So in practice, if you have an existing code that has call mysum(5, s) and then later if the mysum subroutine becomes fallible, but you don't care about handling the error, all you have to do is to change call into try at each call site. Which is a simple change and the compiler would tell you which places need changing, and you can decide at each place if you want the default handling (error stop + stacktrace at runtime) or if you want to handle the error yourself. And also even later on, you will always see which places in your code can fail using the default error handling (error stop) and change them to handle the possible failure yourself. So that seems like a robust design.

@klausler
Copy link

klausler commented May 14, 2020

The implementation would be very much like that of alternate returns, except that it would also work for functions (not just subroutines). The value returned has to be an error code, not just a simple flag.

If a call fails, the caller either handles the failure, or fails itself. In the latter case, the caller's return is almost immediate -- it still deallocates its local state.

@FortranFan
Copy link
Member

FortranFan commented May 14, 2020

The implementation would be very much like that of alternate returns, except that it would also work for functions (not just subroutines). The value returned has to be an error code, not just a simple flag.

If a call fails, the caller either handles the failure, or fails itself. In the latter case, the caller's return is almost immediate -- it still deallocates its local state.

I like the sound of this. Plus @klausler point about "The implementation would be very much like that of alternate returns," is really good to read. For I've always had a simpleton's view the alternate return facility (given it's still part of the standard even if labeled obsolescent) in a given implementation for Fortran might be able to assist with illustration and prototyping.

Re: @klausler other point, "The value returned has to be an error code," it will be really beneficial and user friendly from a coder perspective if the return value is of an enumeration type as proposed by @vansnyder above.

@FortranFan
Copy link
Member

FortranFan commented May 14, 2020

@certik wrote on 12 May 2020:

I think it is clear that many (if not the most) people in the wide Fortran community want some form of better exception handling.

I also agree that the Committee in general is not handling these requests efficiently. As you know I am running for the WG5 Convenor position and in my platform I propose that we work on these proposals continuously and when they are ready, we will consider them for inclusion. However, no matter who gets selected in the end, let us work on these things, and when they are ready, I will bring them to the Committee to consider.

So let's get to work.
..

Thank you @certik , your feedback and suggestions are very inclusive of the broader needs of the community and as such, they are very helpful to achieve progress in the Fortran language with this important feature.

As I mentioned above, my experience in industry is along the same lines as the IEEE Spectrum ranking in terms of the popular languages employed for computing and where each and every one of the top languages offers support for exception handling. In addition to C++, the languages used and all of whom support exception handling include C# and Java and Python and R and also some new languages such as Swift which has gained a lot of influence due to the influence of iOS ecosystem: https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html. But not to be overlooked are other recent languages such as Julia which do compete with C++ and Fortran in the high-performance computing space also e.g., https://docs.julialang.org/en/v1/manual/control-flow/#Exception-Handling-1. And as mentioned by @vansnyder, Ada with its focus on embedded devices and controls and task-based concurrent programming with highly demanding environments such as aircraft systems has also provided robust support for exception handling.

Thus it appears many paths are available beyond that taken by early C++and I hope Fortran can also quickly learn from other approaches such as Julia, Swift, C#, etc. and offer up some performant facility.

@certik
Copy link
Member

certik commented May 14, 2020

@FortranFan great links. The Swift link is I think very close if not exactly what we want. Very similar to Peter's proposal, although with different syntax.

@FortranFan
Copy link
Member

@certik, the example you provided above can also be analyzed using alternate return facility in Fortran as follows:

module exception_m

   type :: exception_enum_t
      integer :: e = 0
   end type

   type(exception_enum_t), parameter :: OBJECT_ALREADY_ALLOCATED = exception_enum_t( e=1 )
   type(exception_enum_t), parameter :: INSUFFICIENT_MEMORY = exception_enum_t( e=2 )

end module

module mysum_m

   use exception_m

   interface mysum
      module procedure mysum_with_noexception
      module procedure mysum_with_exception
   end interface

   type(exception_enum_t) :: mysum_exception = exception_enum_t()

   private :: mysum_with_noexception
   private :: mysum_with_exception

contains

   subroutine mysum_with_noexception(n, s)
      integer, intent(in) :: n
      real, intent(out)   :: s
      real, allocatable :: a(:)
      if ( n == 42 ) call foo( a )
      allocate( a(n) )
      call random_number( a )
      s = sum(a)
      return
   end subroutine

   subroutine mysum_with_exception(n, s, *)
      integer, intent(in) :: n
      real, intent(out)   :: s
      real, allocatable :: a(:)
      integer :: istat
      if ( n == 42 ) call foo( a )
      allocate( a(n), stat=istat )
      if ( istat /= 0 ) then
         select case ( istat )
            case ( 151, 5014 )
               call throw_exception( OBJECT_ALREADY_ALLOCATED )
            case default
               call throw_exception( INSUFFICIENT_MEMORY )
         end select
         return 1
      end if
      call random_number( a )
      s = sum(a)
      return
   end subroutine

   subroutine foo( a )
      real, allocatable, intent(out) :: a(:)
      a = [ real :: ]
   end subroutine foo

   subroutine throw_exception( ex )
      type(exception_enum_t), intent(in) :: ex
      mysum_exception = ex
   end subroutine

end module

program p

   use mysum_m

   real :: s
   integer :: i

   do i = 41, 42

      print *, "i = ", i

      try: block

         use exception_m

         call mysum(i, s, *99)
         print *, "call sum(i, s, *99): s = ", s
         exit try

      99 continue ! catch
         print *, "Program exception in sum"
         select case ( mysum_exception%e )
            case ( OBJECT_ALREADY_ALLOCATED%e )
               print *, "Exception raised: allocatable object is already allocated."
            case ( INSUFFICIENT_MEMORY%e )
               print *, "Exception raised: insufficient memory to allocate object."
            case default
               print *, "Exception raised: unidentified error."
         end select
         print *, "Resume execution"

      end block try

      call mysum(i, s)
      print *, "call sum(i, s): s = ", s

   end do

end program p

You can try above code with either gfortran or Intel Fortran and you should get output as follows:

i = 41
call sum(i, s, *99): s = 20.3514481
call sum(i, s): s = 18.7895145
i = 42
Program exception in sum
Exception raised: allocatable object is already allocated.
Resume execution
At line 33 of file C:\dev\Fortran\temp\sor\p.f90
Fortran runtime error: Attempting to allocate already allocated variable 'a'

Error termination. Backtrace:

Could not print backtrace: libbacktrace could not find executable to open
#0 0xffffffff
#1 0xffffffff
#2 0xffffffff
#3 0xffffffff
#4 0xffffffff
#5 0xffffffff
#6 0xffffffff
#7 0xffffffff
#8 0xffffffff
#9 0xffffffff
#10 0xffffffff

Process returned 2 (0x2) execution time : 0.043 s

As mimicked in this example with a 'throw' invocation, it will be nice if the return value of a raised exception is an enumeration type.

@vansnyder
Copy link
Author

vansnyder commented May 20, 2020 via email

@FortranFan
Copy link
Member

@certik wrote on May 14, 2020, 5:54 PM EDT:

.. The Swift link is I think very close if not exactly what we want. Very similar to Peter's proposal, although with different syntax.

@certik, @klausler, and to everyone with Fortran compiler developer experience:

Prima facie, Swift language error handing facility does indeed appear close to what can benefit Fortran coders. Swift also appears to be mindful of performance in terms of its semantics of a throw statement being made comparable to a return, consider

NOTE

Error handling in Swift resembles exception handling in other languages, with the use of the try,
catch and throw keywords. Unlike exception handling in many languages—including Objective-C
error handling in Swift does not involve unwinding the call stack, a process that can be
computationally expensive. As such, the performance characteristics of a throw statement are
comparable to those of a return statement.

Given then, what would it take to get a project going to prototype a facility in one or more compiler implementations, perhaps building on @klausler's ideas? What kind of support can non-compiler developers provide to compiler developers such as yourself to get such an effort rolling?

Also, any comments on the Swift approach to avoid unwinding of the stack vis-a-vis Fortran and its language semantics? Is it a non-starter or a non-issue given current Fortran standard and its legacy, or can it be an option worth a consideration with Fortran?

@vansnyder
Copy link
Author

vansnyder commented May 24, 2020 via email

@FortranFan
Copy link
Member

@vansnyder wrote May 23, 2020 11:42 PM EDT:

Performance of exception handling shouldn't be an issue. Exceptions are supposed to be exceptional. .. I proposed that a BLOCK construct, CRITICAL construct, CHANGE TEAM construct, or procedure header could have an ENABLE suffix that specifies which exceptions can be handled. .. One way that I can imagine to propagate exceptions to a calling scope is for the processor to create the same sort of code as for an alternate return, except hidden, ..

All this makes sense, the question really is how to gain traction and start to "move the needle" on this feature?

"Performance" considerations keep coming up again and again in the context of this feature, either by vendors or by some who think it's been this way since FORTRAN I so why change. Even though I personally think the concerns with performance are either overstated or an attempt at misdirection, it seems to hinder any progress on this work item with Fortran. So then, how does one either put the "performance" issue to bed once and for all, or illustrate what can work with Fortran and what semantics to avoid?

Separately, I think syntax is the easier part though the debates might get "passionate" and agreement on the terms may prove elusive. Does one go with verbs that are similar in sound and meaning as most other popular languages in scientific and technical programming and which will make it easier for younger Fortranners to relate to and employ the facility e.g., try, throw/throws, catch, defer, etc? Or does Fortran end up with terms that are deliberately intended to make it stand apart and even sound archaic ("classic" in some minds) such as enable, raise, handle, fallible/fall and so forth.

However there are a couple of big bridges to cross before Fortran can arrive at the "world of fun" with syntax.

How does one get started?

@certik, with your vision toward WG5, would more liberal and robust use of the "Technical Specification (TS)" approach be worth a consideration to push progress on certain long-standing but dormant items such as exception handling?

@certik
Copy link
Member

certik commented May 25, 2020

@FortranFan I think we have identified 3 proposals worth prototyping above, and if people propose more proposals in the future, we can prototype those too.

I think the next step is to prototype this in a compiler, preferably two compilers. That is the only way to settle the performance worries and to truly understand the complexity of this feature and to help nail down various design issues. I truly believe at least Flang and LFortran will be able to do this in a relatively near future (in a year or so).

Once we settle on the underlying mechanism / semantics and convince ourselves that this is doable and we understand the performance implications, then we have to agree on the final syntax and after that we should write a paper and propose this either directly to the standard, or using a TS. I am positive we can agree on the best way forward once we get to this point.

I am now concentrating my efforts into LFortran to bring it to the state that people can start using it, and also so that we can prototype such features, and the Flang team is making good progress on their compiler, and I truly believe we can achieve the goal of having at least two independent prior implementations of such wide ranging features such as exceptions.

@FortranFan
Copy link
Member

@certik wrote May 25, 2020 3:55 PM EDT:

..
I am now concentrating my efforts into LFortran to bring it to the state that people can start using it, and also so that we can prototype such features, and the Flang team is making good progress on their compiler, and I truly believe we can achieve the goal of having at least two independent prior implementations of such wide ranging features such as exceptions.

Thanks much for your feedback, that's really good. It'll be really interesting to see how things work out.

Fyi here's a paper by @sblionel at a J3 meeting last year on user and library exceptions: https://j3-fortran.org/doc/year/19/19-207.txt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Clause 11 Standard Clause 11: Execution control
Projects
None yet
Development

No branches or pull requests

4 participants