RFC: New Exception Handling Proposal

I've been looking into a new way to implement exception handling in LLVM. The current model has many disadvantages, in my opinion. I try to address them with this proposal. I also try to make exception handling much more understandable to the normal human reader. :slight_smile: Any new proposal will need to address all present and future languages' exception handling methodologies. I believe that I created something which is generic enough.

Please read and let me know your opinions!

N.B. I'm not wedded to the name I chose here. Nor the implementation of some if the intrinsics - some may be better placed as an attribute of the function.

Also, this does *not* address the general issue of how we handle all exceptional situations, i.e. floating point exceptions and the like.

                             NEW EXCEPTION HANDLING

Here it is in a .txt attachment. It might line-wrap badly.

-bw

Bill’s EH Proposal.txt (11.6 KB)

I'm definitely not an expert in this domain but the proposal looks
very good. I prefer to have the filters and personality as part of
the function symbol itself, not in the entry block or at the definition.
That way one doesn't need a function definition to know some things
about the function. Perhaps it's part of the function type. I'm not
sure what the best way to go is.

I'm not sure if that's feasible in all languages with separate compilation,
though. I believe C++ requires a function declaration throw clause to match
the function definition. Someone with more knowledge will have to answer.

And even if the language requires consistency, we all know programmers
provide something less. :slight_smile:

                                 -Dave

I've been looking into a new way to implement exception handling in
LLVM. The current model has many disadvantages, in my opinion. I try
to address them with this proposal. I also try to make exception
handling much more understandable to the normal human reader. :slight_smile: Any
new proposal will need to address all present and future languages'
exception handling methodologies. I believe that I created something
which is generic enough.

Please read and let me know your opinions!

I'm definitely not an expert in this domain but the proposal looks
very good.

Thanks!

I prefer to have the filters and personality as part of
the function symbol itself, not in the entry block or at the definition.
That way one doesn't need a function definition to know some things
about the function. Perhaps it's part of the function type. I'm not
sure what the best way to go is.

I tend to agree. It's a cleaner way to view this data, and will be faster to query the function for. If it were on the function declaration and it specified type filters, then we may be able to optimize away catches which don't match the filter types in the caller function.

I'm not sure if that's feasible in all languages with separate compilation,
though. I believe C++ requires a function declaration throw clause to match
the function definition. Someone with more knowledge will have to answer.

And even if the language requires consistency, we all know programmers
provide something less. :slight_smile:

-bw

Please read and let me know your opinions!

This definitely seems like an improvement on the current situation.

       \.\-\-\-\-\-\-\-\-\-\.
       > convoke |
       \`\-\-\-\-\-\-\-\-\-'
            >
            v
\.\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\.
>                       >
v                       |

%normal .----------------+---------------.
> > ... |
v v v
select.1 = 1 select.2 = 2 select.n = n
> > >
`----------------+---------------'
>
v
.----------------------------------------.
> %sel = phi [%select.1, ..., %select.n] |
> %eh_ptr = llvm.eh.exception() |
> [cleanup code] |
> switch on %sel |
`----------------------------------------'
>
v
.--------------------.
> > ... |
v v v
%catch.1 %catch.2 %catch.n

One thing I don't like about this (not that it's any worse than the
current situation) is that the llvm.eh.exception() call is now quite a
long way away from the landing pads.

As I understand it, when you generate native code, at each landing pad
the exception pointer appears magically in a well-known native
register. Do you think there's a way to model this more closely in the
LLVM IR? (You could just specify that the llvm.eh.exception call has
to be at the start of a landing pad, but I think that is too prone to
being broken by optimisation passes.)

To illustrate why I don't like having the llvm.eh.exception() call
dissociated from the landing pads, what is the *exact* definition of
llvm.eh.exception:

- Does there have to be a one-to-one correspondence between convokes
and llvm.eh.exception calls?
- Does llvm.eh.exception have to be dominated by landing pads?
- Does it just return the exception pointer from the *most recent*
landing pad that execution flowed through?

etc.

Maybe (this has just occurred to me) the llvm.eh.exception call could
explicitly refer to a convoke, which must dominate it?

Thanks,
Jay.

Please read and let me know your opinions!

This definitely seems like an improvement on the current situation.

           .---------.
           > convoke |
           `---------'
                >
                v
    .-----------------------.
    > >
    v |
%normal .----------------+---------------.
           > > ... |
           v v v
      select.1 = 1 select.2 = 2 select.n = n
           > > >
           `----------------+---------------'
                            >
                            v
         .----------------------------------------.
         > %sel = phi [%select.1, ..., %select.n] |
         > %eh_ptr = llvm.eh.exception() |
         > [cleanup code] |
         > switch on %sel |
         `----------------------------------------'
                            >
                            v
                 .--------------------.
                 > > ... |
                 v v v
              %catch.1 %catch.2 %catch.n

One thing I don't like about this (not that it's any worse than the
current situation) is that the llvm.eh.exception() call is now quite a
long way away from the landing pads.

As I understand it, when you generate native code, at each landing pad
the exception pointer appears magically in a well-known native
register. Do you think there's a way to model this more closely in the
LLVM IR? (You could just specify that the llvm.eh.exception call has
to be at the start of a landing pad, but I think that is too prone to
being broken by optimisation passes.)

Well, it's not entirely magical. :slight_smile: It gets the exception object by calling __cxa_get_exception_ptr and __cxa_begin_catch (though both calls can be combined into __cxa_begin_catch, which is what we do). It's feasible to simply generate the llvm.eh.exception at the start of the catch block code and have that convert into the call to __cxa_begin_catch. My only concern is that we need to make sure that all EH conventions work in a similar way.

To illustrate why I don't like having the llvm.eh.exception() call
dissociated from the landing pads, what is the *exact* definition of
llvm.eh.exception:

- Does there have to be a one-to-one correspondence between convokes
and llvm.eh.exception calls?
- Does llvm.eh.exception have to be dominated by landing pads?
- Does it just return the exception pointer from the *most recent*
landing pad that execution flowed through?

etc.

Maybe (this has just occurred to me) the llvm.eh.exception call could
explicitly refer to a convoke, which must dominate it?

Well, be warned that there can be multiple convokes which jump to the same landing pads. So there cannot be a one-to-one correspondence between them. However, an llvm.eh.exception should be dominated by at least one convoke. The only other problem I can see is if code branches into a catch block. It's gross, but could happen. In that case, the llvm.eh.exception should *not* be executed, because it's metadata constructed by the compiler, not user code. It would have to be peeled out into its own basic block in such instances.

-bw

I have a couple of questions/concerns. The main one has to do with the opacity of exception types - in other words, will it still be true that the exception types are opaque identifiers, and are only interpreted by the personality function?

Since my object representations are not C++-like, I am not using any of the cxa_* C++ library functions. Instead, my code calls the _Unwind functions directly, and I have my own personality function (which can be viewed here: http://code.google.com/p/tart/source/browse/trunk/runtime/lib/tart_eh_personality.c)

My compiler generates the list of filter parameters as pointers to Type objects. In the personality function, it examines each such pointer and does an IsSubclass test. The action code returned by the personality function is a simple integer index (0, 1, 2, ...etc) which represents the number of the catch block to transfer control to. This index is then fed into a IR switch instruction in the landing pad.

The concern I have is that the 'convoke' instuction takes a list of catch types, but it doesn't say what those catch types are. If the code does not assume anything about them, then I'm guessing that I would be able to adapt my code to work with it. On the other hand, if those catch types are assumed to be C++ exceptions or RTTI identifiers, then I'm hosed.

Bill Wendling wrote:

Since my object representations are not C++-like, I am not using any of
the cxa_* C++ library functions. Instead, my code calls the _Unwind
functions directly, and I have my own personality function (which can be
viewed here:
http://code.google.com/p/tart/source/browse/trunk/runtime/lib/tart_eh_personality.c)

The basic summation is that any currently dwarf eh compatible exceptions will continue to work as well as any other ideas of exceptions. The new format isn't tied or specific to dwarf at all, just a manner of table implemented exception handling. The new bits aren't specific to C++ either - and even the dwarf eh spec isn't, just that the dwarf spec is most commonly used for C++. Non-dwarf exceptions are treated as "foreign" exceptions by c++ and propagated the same way. Also, any personality function for a code region will continue to be called and propagate.

-eric

As I understand it, when you generate native code, at each landing pad
the exception pointer appears magically in a well-known native
register.

Well, it's not entirely magical. :slight_smile: It gets the exception object by calling
__cxa_get_exception_ptr

__cxa_get_exception_ptr returns an "adjusted" pointer to the exception
object, but you still have to pass into it a "raw" (unadjusted)
pointer to the exception object. It's this raw pointer that appears
magically in a native register at the start of a landing pad.

Well, be warned that there can be multiple convokes which jump to the same
landing pads. So there cannot be a one-to-one correspondence between them.
However, an llvm.eh.exception should be dominated by at least one convoke.
The only other problem I can see is if code branches into a catch block.
It's gross, but could happen. In that case, the llvm.eh.exception should
*not* be executed, because it's metadata constructed by the compiler, not
user code. It would have to be peeled out into its own basic block in such
instances.

Are you saying that, in the LLVM IR, it would be legal to have an
llvm.eh.exception that *isn't* dominated by convokes (because there's
a direct branch to that catch block), and in that case the call
returns an undefined value? And it would be up to codegen to "peel it
out into its own basic block"?

Thanks,
Jay.

Hi Jay,

Are you saying that, in the LLVM IR, it would be legal to have an
llvm.eh.exception that *isn't* dominated by convokes (because there's
a direct branch to that catch block), and in that case the call
returns an undefined value?

this is already the case (with invoke substituted for convoke).

And it would be up to codegen to "peel it

out into its own basic block"?

Not sure what that means?

Ciao,

Duncan.

Are you saying that, in the LLVM IR, it would be legal to have an
llvm.eh.exception that *isn't* dominated by convokes (because there's
a direct branch to that catch block), and in that case the call
returns an undefined value?

this is already the case (with invoke substituted for convoke).

Good to know. It would be nice if this was clearly documented!

And it would be up to codegen to "peel it out into its own basic block"?

Not sure what that means?

I was trying to work out what Bill meant by this:

The only other problem I can see is if code branches into a catch block. It's gross, but could happen. In that case, the llvm.eh.exception should *not* be executed, because it's metadata constructed by the compiler, not user code. It would have to be peeled out into its own basic block in such instances.

What exactly would be "peeled out into its own basic block", by whom,
at what stage of the compilation process?

Thanks,
Jay.

And it would be up to codegen to "peel it out into its own basic block"?

Not sure what that means?

I was trying to work out what Bill meant by this:

The only other problem I can see is if code branches into a catch block. It's gross, but could happen. In that case, the llvm.eh.exception should *not* be executed, because it's metadata constructed by the compiler, not user code. It would have to be peeled out into its own basic block in such instances.

What exactly would be "peeled out into its own basic block", by whom,
at what stage of the compilation process?

I think he is saying: "what happens if the unwind label for an invoke is a
basic block which is also jumped to by ordinary code, not just by invokes?"
The answer is that this is all already taken care of and you get the correct
value.

Ciao,

Duncan.

Yes. The whole point is not to tie the catch type to any language-specific type. And it should be able to handle using the Type objects. There may be some bitcasting involved, but that should be fairly straight-forward to deal with. Your way of implementing the EH control transfer to catch blocks is essentially how I'm handling it in my pseudo-example - assign each catch in the convoke an integer, and then switch on that integer after any potential clean-up code.

-bw

Well, be warned that there can be multiple convokes which jump to the same
landing pads. So there cannot be a one-to-one correspondence between them.
However, an llvm.eh.exception should be dominated by at least one convoke.
The only other problem I can see is if code branches into a catch block.
It's gross, but could happen. In that case, the llvm.eh.exception should
*not* be executed, because it's metadata constructed by the compiler, not
user code. It would have to be peeled out into its own basic block in such
instances.

Are you saying that, in the LLVM IR, it would be legal to have an
llvm.eh.exception that *isn't* dominated by convokes (because there's
a direct branch to that catch block), and in that case the call
returns an undefined value?

No. The opposite of that.

And it would be up to codegen to "peel it out into its own basic block"?

The front-end should be able to take care of this.

-bw

That wasn't clear. :slight_smile: This should be totally doable by the front-end, since it's the one generating the llvm.eh.exception() call.

-bw

Addendum: It was pointed out to me in a private email that this new "throw" instruction may not be useful. If you have two functions being merged into the same module, but both with different instructions to do the actual "throw", then this instruction as described here won't work. There's the possibility of adding a function parameter to this, but that obviates this new instruction (why not emit a direct call instead?). The function may be added to the function type, similar to the personality function. But that's even more baggage on the function type.

There would only be a marginal conceptual win, which isn't enough to warrant a new instruction.

-bw

The current exception handling mechanism encodes the exception
handling metadata
by using intrinsics and the CFG itself.

...

Exception handling metadata is no longer encoded in the CFG. It is
much more
sanely specified, and thus easier to understand by normal humans. The
optimizers
are free to modify the code as they see fit. In fact, they may be able
to do a
better job at it. For instance, they could perform optimizations
mixing the
cleanup and catch code. If the "filters" were part of the function
instead of an
intrinsic, there is the potential for optimizations based upon
knowledge that a
function cannot throw a particular type.

Catch blocks are attractive targets for hot/cold optimizations of
various kinds (-Os, move the block to a different VM page, Thumb ISA
vs ARM ISA, etc.)

If EH isn't modeled directly on the CFG, how can these optimizations be handled?

deep

Exception handling is still modeled directly in the CFG. That is, there are unique branches to catches, cleanups, etc. from a call that may throw. The only thing which isn't encoded in the CFG anymore is the metadata involved with EH. Things like specifying what is a landing pad, what is a post pad, etc., which are entities that code gen generates for whatever type of EH mechanism is being used. All of the optimizations you mention here will still be doable with the new scheme.

-bw