LLVMdev Digest, Vol 86, Issue 5

Bill et al,
I haven’t really thought this through (I’ve never been a fan of exception specifications),
but it seems that inlining functions that do have exception specifications, and hence filter
operands in their landing-pads, isn’t so obvious…

if you want to inline

void foo () throw() { … } // empty throw() list means “no-throw”

into a function that does allow exceptions to be propagated out, then you’ll have a hard time
combining their landing pads !

it seems, at least on first thought, that having more than one filter list in a landing pad isn’t
useful ? (assuming the only way to get more than one is from inlining ?)

-Peter Lawrence.

The exception tables are set up to allow for multiple exception specifications per function. So if we inline a function with exception spec of A, B, C into one with an exception spec of D, E then it's totally doable. The table will look something like this:

  A
  B
  C
  0
  D
  E
  0

And the "action" part of the table will point to the correct exception spec start (A or D) depending on what state the exception is at a given time. (I.e., is it in a "resume" situation or "catch" situation.)

-bw

Bill,
         I suspect we're talking about two different aspects,
I think you are saying that there is an ability for the DWARF Actions Table
to contain multiple lists, including multiple filter lists - no disagreement
there,
I am saying that for any one landing-pad it might not make sense
for it to be able to have more than one filter list.

-Peter Lawrence.

In general, it's not bad. But there is one problem, which your example illustrates.

  void foo() throw() { /* ... */ }

When "foo()" is inlined into its caller, the resulting function's EH table should be set up to disallow exceptions thrown for the inlined contents of foo(). Our current scheme is to mark foo() as "nounwind" and make it a normal "call" instruction. Some care will have to be taken during inlining to keep the "no throwing" attribute around for the code that cares about it.

-bw

Bill,
        I believe the empty-exception-specification example is a red-herring,
but that if you can construct an example where you think a landing-pad
requires multiple filter lists then I think we can then have a productive
conversation about it.

I believe we can only get multiple filter lists in a landing-pad if we attempt
to merge exception-regions, and since filters are only an outer-function
concept (not an inner try-statement concept) this can only come about
from function inlining.

so say we're considering inlining "void foo() throw(A) { ... }" into
"void bar() throw (B) {... foo(); ... ; frob(); ...}"

if you attempt to merge the landing pad for these functions (perhaps
assuming it is going to be simple because there are no cleanups in either
function that would complicate things) then...

there has to be a filter for A that will terminate the program if A is thrown,
but you cannot terminate the program if exception A comes from frob()
since it has not exception specification.

my conclusion is that you can only merge landing-pads if they already
have *identical* filters, so multiple filter lists isn't mathematically possible.

thoughts ?
comments ?

-Peter Lawrence.

PS. I believe the empty-exception-specification example is a
red-herring because I believe you are making the same mistake that
everyone does concerning "exception specifications", which is assuming
they imply compile time correct information -- like "const" is enforced at
compile time -- which is false (and why exception specifications are a
bad design).

__attribute__((nothrow)) <--- user guarantee that the function
does not throw, the compiler is free to optimize based on this
assertion, and if the program violates that guarantee then the
the program is "undefined" and can "do anything".

throw() <--- not a user guarantee of anything,
rather a user requirement that the compiler generate runtime checking
code to enforce that no exception is propagated out.

Bill,
      I believe the empty-exception-specification example is a red-herring,
but that if you can construct an example where you think a landing-pad
requires multiple filter lists then I think we can then have a productive
conversation about it.

I believe we can only get multiple filter lists in a landing-pad if we attempt
to merge exception-regions, and since filters are only an outer-function
concept (not an inner try-statement concept) this can only come about
from function inlining.

so say we're considering inlining "void foo() throw(A) { ... }" into
"void bar() throw (B) {... foo(); ... ; frob(); ...}"

if you attempt to merge the landing pad for these functions (perhaps
assuming it is going to be simple because there are no cleanups in either
function that would complicate things) then...

there has to be a filter for A that will terminate the program if A is thrown,
but you cannot terminate the program if exception A comes from frob()
since it has not exception specification.

my conclusion is that you can only merge landing-pads if they already
have *identical* filters, so multiple filter lists isn't mathematically possible.

thoughts ?
comments ?

I think you might have the concept backwards. The call to foo() will call "unexpected" if anything *other* than A is thrown out of foo(). The EH tables can be set up to accommodate this situation. Let's say you have this example:

  void frob() { }
  void foo() throw (A) { }
  void bar() throw (B) { foo(); frob(); }

The exception table for "foo" will look something like this for the action (the indexes are negative offsets into the exception spec table):

  .byte 0x7f # index into the exception specification table (-1)
  .byte 0 # index to the next "action"
...
  # Start of exception specification table
  .long _A # Only "A" can be thrown
  .byte 0 # The end of this exception spec.

This is as you'd expect. The EH table for bar is similar:

  .byte 0x7f # index into the exception specification table (-1)
  .byte 0 # index to the next "action"
...
  # Start of exception specification table
  .long _B # Only "B" can be thrown
  .byte 0 # The end of this exception spec.

Now, when we inline "foo()" into "bar()", we get something that looks like this:

  .byte 0x7f # index into the exception specification table (-1)
  .byte 0 # index to the next "action"
  .byte 0x7d # index into the exception specification table (-3)
  .byte 0 # index to the next "action"
...
  # Start of exception specification table
  .long _A # Only "A" can be thrown
  .byte 0 # The end of this exception spec.
  .long _B # Only "B" can be thrown
  .byte 0 # The end of this exception spec.

Any call from foo will point to the ".byte 7f" action, which says that it can only throw "A" All calls from bar will point to the ".byte 0x7d" entry, which says that it can only throw "B". So in that way we can combine two functions with different filters.

PS. I believe the empty-exception-specification example is a
red-herring because I believe you are making the same mistake that
everyone does concerning "exception specifications", which is assuming
they imply compile time correct information -- like "const" is enforced at
compile time -- which is false (and why exception specifications are a
bad design).

__attribute__((nothrow)) <--- user guarantee that the function
does not throw, the compiler is free to optimize based on this
assertion, and if the program violates that guarantee then the
the program is "undefined" and can "do anything".

throw() <--- not a user guarantee of anything,
rather a user requirement that the compiler generate runtime checking
code to enforce that no exception is propagated out.

I'm aware of the semantics of "throw()". This is taken care of with the exception handling table. We would have an action of something like this:

  .byte 0x7f
  .byte 0

where the exception specification table is this:

  .byte 0x0

This indicates to the personality function won't find a matching exception specification (there are none to match) and then we call the unexpected dealy.

-bw

Bill,
        ooops, yes, I described the meaning of "throw(A)" backwards, but I still
think my example shows why you cannot merge LandingpadInst while
inlining because multiple filter-lists on a LandingpadInst don't make sense.

Perhaps I'm reading your original spec wrong, perhaps I'm mis-reading
Duncan's emails, but I read them to mean that your syntax supports
multiple filter-lists on a single LandingpadInst.

first off, I think we agree on what I think you're saying below, that if foo calls
something, and bar calls somethingelse, and that if these have different
exception-specifications, then we can translate into different DWARF Action
and Types Tables, each call has it's own (in this case unique) Actions.

second, do you agree that if one exception-spec says to unexpected()
if anything other than 'A' is thrown, and another exception-spec says
to unexpected() if anything other than 'B' is thrown, that these cannot
be merged --> by that I mean they cannot have the same landingpad <-- ?

-Peter Lawrence.

I think you aren't understanding what merging means. Nobody's talking
about taking two invokes that lead to landing pads with different
semantics and making them share the same landing pad; obviously
that cannot be done. But you can certainly inline a function that has an
exception-specification into a function with a different
exception-specification: you just have to preserve that the inner
exception-specification applies before any filters or handlers in the
outer function. The existing C++ personalities are quite capable of
handling that.

John.

Bill,
      ooops, yes, I described the meaning of "throw(A)" backwards,

I thought that might be the case. :slight_smile:

but I still
think my example shows why you cannot merge LandingpadInst while
inlining because multiple filter-lists on a LandingpadInst don't make sense.

Perhaps I'm reading your original spec wrong, perhaps I'm mis-reading
Duncan's emails, but I read them to mean that your syntax supports
multiple filter-lists on a single LandingpadInst.

first off, I think we agree on what I think you're saying below, that if foo calls
something, and bar calls somethingelse, and that if these have different
exception-specifications, then we can translate into different DWARF Action
and Types Tables, each call has it's own (in this case unique) Actions.

second, do you agree that if one exception-spec says to unexpected()
if anything other than 'A' is thrown, and another exception-spec says
to unexpected() if anything other than 'B' is thrown, that these cannot
be merged --> by that I mean they cannot have the same landingpad <-- ?

Well, the only way they could be merged is through inlining. So I don't agree. One exception spec is encapsulated by the caller's exception spec. And merging them can be done even now. I give an example below.

-bw

Let's look at an example:

extern int printf(const char *, ...);

struct A {
  ~A() { printf("A's d'tor\n"); }
};

struct B {
  ~B() { printf("B's d'tor\n"); }
};

void baz();

void foo() throw (A) __attribute__((always_inline));
void foo() throw (A) {
  baz();
}

void bar() throw (B) {
  try {
    foo();
  } catch (const char *s) {
    printf("%s\n", s);
  }
}

GCC outputs this:

[Irk:llvm] gcc-4.2 -S -o - -dA t.cpp -O3
  .text
  .align 4,0x90
.globl __Z3foov
__Z3foov:
  . . .
LEHB0:
  call __Z3bazv
LEHE0:
  . . .
GCC_except_table0:
LLSDA8:
  .byte 0xff # @LPStart format (omit)
  .byte 0x9b # @TType format (indirect pcrel sdata4)
  .byte 0x25 # uleb128 0x25; @TType base offset
  .byte 0x3 # call-site format (udata4)
  .byte 0x1a # uleb128 0x1a; Call-site table length
  .set L$set$0,LEHB0-LFB8
  .long L$set$0 # region 0 start
  .set L$set$1,LEHE0-LEHB0
  .long L$set$1 # length
  .set L$set$2,L6-LFB8
  .long L$set$2 # landing pad
  .byte 0x1 # uleb128 0x1; action
  .set L$set$3,LEHB1-LFB8
  .long L$set$3 # region 1 start
  .set L$set$4,LEHE1-LEHB1
  .long L$set$4 # length
  .long 0x0 # landing pad
  .byte 0x0 # uleb128 0x0; action
  .byte 0x7f # Action record table
  .byte 0x0
  .align 2
  .long __ZTI1A+4@GOTPCREL
  .byte 0x1 # Exception specification table
  .byte 0x0

So the call to "foo" is marked as being able to throw an "A" type. (The 0x7f in the action record table is a negative 1-based offset into the exception specification table, which itself is a positive 1-based offset into the type table.) Now look at bar:

  .align 4,0x90
.globl __Z3barv
__Z3barv:
  . . .
LEHB2:
  call __Z3bazv
LEHE2:
  . . .

GCC_except_table1:
LLSDA9:
  .byte 0xff # @LPStart format (omit)
  .byte 0x9b # @TType format (indirect pcrel sdata4)
  .byte 0x59 # uleb128 0x59; @TType base offset
  .byte 0x3 # call-site format (udata4)
  .byte 0x41 # uleb128 0x41; Call-site table length
  .set L$set$5,LEHB2-LFB9
  .long L$set$5 # region 0 start
  .set L$set$6,LEHE2-LEHB2
  .long L$set$6 # length
  .set L$set$7,L20-LFB9
  .long L$set$7 # landing pad
  .byte 0x5 # uleb128 0x5; action
  . . . [ elided for readability ]
  .byte 0x7d # Action record table
  .byte 0x0
  .byte 0x2
  .byte 0x7d
  .byte 0x7f
  .byte 0x7d
  .byte 0x0
  .byte 0x79
  .align 2
  .long __ZTI1B+4@GOTPCREL
  .long __ZTIPKc+4@GOTPCREL
  .long __ZTI1A+4@GOTPCREL
  .byte 0x1 # Exception specification table
  .byte 0x0
  .byte 0x3
  .byte 0x0

As you can see, foo() has been completely inlined into bar(). The entry for the call to "baz" (from the foo() function) is shown here in the EH table. The action associated with it is the 1-based, positive offset into the action table

  .byte 0x7f
  .byte 0x7d

This means it should look at the filter at offset -1 into the exception spec table. That is "0x1", which corresponds to "__ZTI1A+4@GOTPCREL". This is saying that the call to "baz" can throw only an "A" type. The "0x7d" means that the next action to apply is:

  .byte 0x2
  .byte 0x7d

This action says it can catch only "__ZTIPKc+4@GOTPCREL" types. The "0x7d" here means that the next action to apply is:

  .byte 0x7d # Action record table
  .byte 0x0

The "0x7d" means to look at offset -3 into the exception spec table. This is "0x3", which means that it's "__ZTI1B+4@GOTPCREL" in the type table. This means that after the catch, it can only throw a type "B". The "0x0" in the action record indicates that there are no more filters or catches to apply here.

So this is the way to merge two landing pads together which have different exception specs. (Apologies for my previous example, which had some errors in it.)

-bw

I guess more to the point, two landing pad instructions won't be 'merged' together except through inlining. It's true that two landing pad instructions with different filters may exist within the same function, but merging is not something that should happen via normal, non-inlining code motion.

-bw