Throwing and catching exceptions – Advanced IR Generation-1


With IR generation introduced in the previous chapters, you can already implement most of the functionality required in a compiler. In this chapter, we will look at some advanced topics that often arise in real-world compilers. For example, many modern languages make use of exception handling, so we’ll look at how to translate this into LLVM IR.

To support the LLVM optimizer so that it can produce better code in certain situations, we must add additional type metadata to the IR code. Moreover, attaching debug metadata enables the compiler’s user to take advantage of source-level debug tools.

In this chapter, we will cover the following topics:

  • Throwing and catching exceptions: Here, you will learn how to implement exception handling in your compiler
  • Generating metadata for type-based alias analysis: Here, you will attach additional metadata to LLVM IR, which helps LLVM to better optimize the code
  • Adding debug metadata: Here, you will implement the support classes needed to add debug information to the generated IR code

By the end of this chapter, you will have learned about exception handling, as well as metadata for type-based alias analysis and debug information.

Throwing and catching exceptions

Exception handling in LLVM IR is closely tied to platform support. Here, we will look at the most common type of exception handling using libunwind. Its full potential is used by C++, so we will look at an example in C++ first, where the bar() function can throw an int or double value:
int bar(int x) {
  if (x == 1) throw 1;
  if (x == 2) throw 42.0;
  return x;
}

The foo() function calls bar(), but only handles a thrown int. It also declares that it only throws int values:
int foo(int x) {
  int y = 0;
  try {
    y = bar(x);
  }
  catch (int e) {
    y = e;
  }
  return y;
}

Throwing an exception requires two calls into the runtime library; this can be seen in the bar() function. First, memory for the exception is allocated with a call to __cxa_allocate_exception(). This function takes the number of bytes to allocate as a parameter. The exception payload (the int or double value in this example) is copied to the allocated memory. The exception is then raised with a call to __cxa_throw(). This function takes three arguments: the pointer to the allocated exception, type information about the payload, and a pointer to a destructor, in case the exception payload has one. The __cxa_throw() function initiates the stack unwinding process and never returns. In LLVM IR, this is done for the int value, as follows:


%eh = call ptr @__cxa_allocate_exception(i64 4)
store i32 1, ptr %eh
call void @__cxa_throw(ptr %eh, ptr @_ZTIi, ptr null)
unreachable

_ZTIi is the type information describing an int type. For a double type, it would be _ZTId.

So far, nothing LLVM-specific is done. This changes in the foo() function because the call to bar() can raise an exception. If it is an int type exception, then the control flow must be transferred to the IR code of the catch clause. To accomplish this, the invoke instruction must be used instead of the call instruction:


%y = invoke i32 @_Z3bari(i32 %x) to label %next
                                 unwind label %lpad

Leave a Reply

Your email address will not be published. Required fields are marked *