Sometimes using
OCAMLRUNPARAM="b" returns a disappointingly
unhelpful backtrace, because the program you're trying to debug
has a non-trivial usage of exceptions that destroys the
backtrace information you were expecting. Here is a new library
that should solve this problem, by allowing you to reliably
register backtraces. Read more for the source, and the juicy
internal details: weak tables, semi-manual garbage collection
logic, and an optional change to the OCaml runtime to
improve the library performances.

The problem

When debugging OCaml programs, it is sometimes hard to get
useful backtraces for exceptions. Let's take an example: you are
hacking the Coq implementation, and your patch launches an
exception (any resemblance to real persons, living or dead, is
purely coincidental). Of course, you have no idea why this
exception has been raised, so a little bit of context would be
appreciated: getting a backtrace would be very useful, because
it tells you where the exception has been raised, and what were
the state of the call stack at that time.

There is already some support for getting backtraces in
OCaml. If you compile in debug mode (eg. with
the -g option), and then run your code
with OCAMLRUNPARAM="b" in the environment, then you
can get exception backtraces. They will be automatically printed
for uncaught exceptions (those that stop the program execution),
and inside your programs you can use
the Printexc
module of the OCaml standard library, to print the backtrace
from the "raise" of the last exception that was raised up to the
"try..with" that caught it (or the end of the program).

However, this system is not always satisfactory: in OCaml,
exceptions are very efficient, and it is a common idiom to use
them for control flow (rather than only to handle errors). As
a consequence, the exception handling mechanism in big OCaml
programs can be complicated, because it has to filter
exceptional exceptions (which are a sign of an error), and
exceptions used for control. It can also be the case that the
error handlers themselves uses exceptions internally, for
example some error messages formatter do this. This is
problematic, because it erases the original, relevant
backtrace. Asking for a backtrace after the fact will show you
traces for exceptions that were raised and caught internally in
the message formatter, and then you're lost, which (in the
good case) makes you write frustrated blog posts such
as this
one.

The solution of other languages

In some other languages, like Java, every exception stores its
own backtrace. So, even if you do strange things with exceptions
internally, as long as you have access to the exception that has
been raised, you can print its backtrace. However, this has
a non-negligible memory and performance cost, because the
runtime has to copy the exception backtrace into the heap.

In OCaml, it would be unpleasant to do that in the general case,
because exception can be heavily used. We could imagine to do
that only in debug mode and when backtraces are active, but this
poses non-trivial questions, because we have to choose at
compile time whether to allocate a backtrace-compliant exception
object or not, for example. Besides, it would require quite
invasive changes to the compiler and runtime, probably add bugs,
etc. Not something you want to try over a week-end.

A less invasive approach: a user library

If we cannot add a pointer from the exception to the backtrace,
we can consider building an global map from exceptions to
backtraces. Each time we catch an exception, we can store its
backtrace in the map. Then, when needed, we can lookup the
backtrace from the exception in this map.

This has the advantage of not requiring any change to the
compiler. You can implement this big table as a library, and let
users call a library function, in their exception handler, to
add their current exception to the table.

But this requires user intervention to work well: the user must
explicitly call the registration function in each error
handler. This is good, as it allows to decide whether to store
traces in a fine-grained way, making performance/traceability
compromises or simply not tracing exceptions that you know do
not correspond to bugs. But this can be painful for legacy or
third-party code for which you just want a backtrace quick: you
don't really want to go over all the `try..with` handlers in
Coq's implementation to add a call to our registration function,
by hand. Gabriel Scherer implemented a small preprocessor
(as a Camlp4 extension and a `-ppx` rewriter) to add them
automatically at each handler if desired.

Implementing the map itself is more challenging: inserting in it
should be efficient (while lookup operations can be slow); it
should operate with physical equality, because two occurrences
of Not_found should see their backtraces
distinguished; it should not leak memory, that is, it should
throw away the backtrace when the exception is no longer
accessible -- and it should not be considered as accessible by
the Garbage Collector (GC) trough this map. Finally, we need to
be able to store multiple backtrace for the same exception,
because an exception can be re-raised: in that case, we want to
be able to get all the backtraces.

Our solution is to use two arrays: one for exceptions, the other for
backtraces. At the same indices, we store corresponding
elements. Inserting is very cheap in amortized time, even if it
involves resizing the arrays if necessary. The lookup is in linear
time, but we almost never need it. The array of exceptions is a
weak
array: this data structures makes the GC able to collect
exceptions even if they are still referenced in the map. At the
end of each major GC cycle, we compact those arrays, manually
removing traces corresponding to collected exceptions.

Performances considerations

Storing all traces in this way is very slow
for raise-happy programs (which are the one for
which you need better traces in the first place). The
performances of Coq are divided by 3 to 4 by this
modification. The main reason for this cost is that the only way
in the OCaml standard library to get the backtrace information
is to convert it to a string, with English sentences, file
names, line numbers, and so on. It takes a long time to generate
such strings for each caught exception, and uses a lot of
memory, stressing the GC. But still, it works, and if you really
need good backtraces you are ready to pay the performance
price... which only exists when you turn on backtrace recording.

The next idea was to modify the OCaml runtime and standard
library, in order to expose a more low-level representation of
backtraces without the processing costs. Actually, this already
exists: when using the existing mechanism for recording
backtraces in OCaml, the runtime generate a compact
representation of the backtrace in a global variable for each
raised exception. The only thing we have to do is to split the
collecting-and-formatting function in two, and expose an API for
copying on demand this information in the ocaml heap, for
a possible future conversion to the string representation.

We implemented this as a small change to the OCaml runtime system
and Printexc
module -- much less invasive than the compiler-wide change we
are avoiding with our user-side solution. Using this new
feature, Coq only get 10% slower (tested on the compilation
of CompCert). It still uses more memory (about 30%). We think
this is very reasonable, given that Coq uses quite a lot of
exceptions for backtracking, and that there is no cost when
backtrace recording is not activated.

Of course, performances could be improved even further by
letting users decide which exception handlers really correspond
to errors that needs to store traces, and which are
purely control-flow logic (loop exit, backtracking...) that does
not need to record anything. But getting good traces out the box
with reasonable performance impact is already satisfying enough
for now.

The rewriter that adds exception handling logic to all try
.. with .. in your code (to use with care) is available
for Camlp4, add_smartcatch.ml,
or for the new -ppx AST rewriting mechanism
available in OCaml
trunk, smartcatch_ppx.ml.

The patch to the available runtime is available
as abstract-backtraces.patch.txt. It
is mostly of historical curiosity, as the maintainers found the
change non-invasive enough to accept it into the OCaml trunk, so
you should be able to get fast backtrace recording in the next
OCaml version.

Our quick-and-dirty patch for Coq 8.4pl1
is coq-8.4pl1.patch (it mostly
hacks around the Coq build system to add an additional camlp4
preprocessing step to all files). In order to use it, you must
add either version¹ of the exception handling library in
the lib subdirectory, under the
name exceptionHandling.ml. Then, simply type:

./configure -debug
make

And voilà, pixel-perfect backtraces!

¹: Note that for obscure camlp5 reasons, Coq will not build
easily against the OCaml trunk, so you should apply the runtime
patch to the 4.00 branch instead; as the patch adds a new
primitive, you will need to throw a make coreboot
somewhere in the OCaml compilation process. Even then, building
Coq won't be for the faint of heart.