TL;DR how does exception handling really work? how is throw detected? is there any way to force exception handlers to be maintained without preventing optimizations?
Background:
I’m trying to implement implicit exception handling, i.e. translation of some signals into exceptions in Tyr (see example).
After analyzing the behavior for some days in Tyr and C++ now, I can see that optimizers seem to assume that the code cannot throw an exception and start throwing away exception handlers.
I attached the C++ example, but also the Tyr example and .ll file (with demangled names) to show that this is likely not a C++ problem.
min.cpp (706 Bytes)
test.1.ll.txt (390.2 KB)
mar.tyr.txt (528 Bytes)
The strange thing with the current semantics is that optimizations can result in throw from signal to bypass an arbitrary number of signal handlers including all. For the Tyr example, the naively expected behavior is observed with no optimizations and all exception handlers are discarded starting from -Og. In the C++ example, toggling the comment on the unreachable printf toggles the exception handler catching the exception.
I think I tried all flags that seem exception- or unwind-related but could not observe any changes here. Also, adding uwtable flags in the Tyr backend seemed not to change anything.
Note: generally speaking, I think it makes sense to rule that asynchronous exception handling has no defined semantics wrt. ordering of instruction execution or exception handlers that are really hit. On the other hand, I would not expect that the behavior changes on -Og or when adding or removing other function calls that do not themselves have any side effects wrt. exception handling.
Question 1:
There is a flag nounwind. The documentation states that
However, functions marked nounwind may still trap or generate asynchronous exceptions. Exception handling schemes that are recognized by LLVM to handle asynchronous exceptions, such as SEH, will still provide their implementation defined semantics.
Does that also mean that asynchronous exceptions do not work with ELF/DWARF?
Is there any means to mark a function as “mayunwind” to prevent it from getting a nounwind flag by whatever analysis/optimization?
My naive assumption was that nounwind + uwtable means that llvm must expect asynchronous exceptions to be thrown in the body of the respective function.
Question 1b:
Is this a bug in the DWARF exception handling related code emission? Initially, I thought it could be. But currently I consider it more of a lack of clarity of semantics of uwtable and invoke.
Question 2:
The more I digged into this, the more I realized that it isn’t really clear how throw is detected. I kind of assume that llvm somehow magically detects __cxa_throw or simply assumes that calls followed by unreachable must throw an exception. Can someone clarify this? Is this specified somewhere? Or is this simply that any external function without nounwind is expected to throw?
Maybe the answer could be used to derive a pattern to mark functions that, based on their type in Tyr, must be assumed to throw exceptions without preventing optimizations.
Question 3:
Optimizations seem to try to replace invoke with call and throw away the exception handlers. This makes a lot of sense in general. Is there any way to prevent this replacement other than not using optimizations?
My naive assumption would be that it should have semantics of “do not discard exception handlers unless the called function has an empty/stateless body and is inlined”.
To me, this seems a bit like a design issue of exception handling in llvm atm. because I do not see a way to keep the handler alive if there is no invoke instruction. It seems as if the SEH-related intrinsics need to be generalized to something that works with DWARF too.
Or copy llvm.donothing to llvm.try.start and llvm.try.end and optimize it only if there is no instruction between start and end to allow deletion of handlers in effective-empty try bodies.
Versions OS:
I tried Ubuntu 22.04 clang 14 and Ubuntu 25.04 clang 20; the behavior seems to be the same. With clang 20 I also tried a version with noexcept(false) but the .ll files seemed to be unaffected.