Sean McDirmid wrote:> ... I would be very interested> to see how it was done by Sun or other compilers with exception> support (modula 2, C++, etc..).

I wrote a pair of articles, some years back, in JCLT (then-published
by the comp.compilers moderator) that described roughly how Sun dealt
with exception handling in a C++ compiler. For synchronous
exceptions, the changes to flow analysis are relatively trivial;
branches are branches. I suspect, seeing the extended discussion that
is going on here, that trivial is in the eye of the beholder (I've
only been thinking about this problem off and on for the last ten
years, it seems pretty simple now :-).

In the synchronous case, exceptions arise at procedure calls. The
call instruction has to be modeled as having a possible transfer of
control to a handler. This transfer of control eventually needs to be
encoded into data structures fed to the runtime system; a good time to
do this is very late in the code generation pipeline (certainly after
all register and transfer-of-control altering optimizations).

One responsibility of the runtime system and data structures used to
describe this transfer of control is that this look like just like a
transfer of control, so that the optimizer can be free of special
cases. One responsibility of the compiler is to generate appropriate
handlers for the no-handler case, so that the dispatcher does not need
to know about register save/restore conventions (or lack of
conventions, as the case may be); the no-handler-handler simply
restores callee-saves registers and disappears the stack frame, then
calls back into the exception dispatcher as if from the caller's
frame. That is, it performs a tail-call back into the exception
dispatcher. This conveniently reduces the exception dispatch problem
to local dispatch in the current frame; the dispatcher does not need
to search the stack, nor does it need to fool around with restoring
registers; that is the compiler's problem.

For asynchronous exceptions (this is going beyond what the Sun
compilers did then, not sure what they do now), and in the presence of
complicated calling conventions (recall that unwinding on a Sparc is
typically a no-brainer) life is not impossible. Any callee-saves
registers can be restored in something like an assembly-level
try-finally (before leaving this block, put these registers back).
Asynchronous exceptions delivered during the execution of one of these
asm-level-try-finally blocks are dealt with by breaking them up into a
sequence of idempotent try-finally blocks, where each block is
self-guarded -- that is, if interrupted in the middle of restoring a
register X, simply start over from the beginning of restoring register
X (you cannot write this in any language that I know of, but it is a
neat trick). (Note that this assumes the stack is mapped, not always
a valid assumption, but it is a bad condition that you can check for.
This also assumes that the average rate of asynchronous exception
delivery is low enough that progress can occur.)

I tried hard to get this adopted into the Sparc V9 ABI, but I don't
think it made it; I left Sun before that was finished, and I believe
it was regarded as a little too weird and unnecessary at the time
(doesn't help C or Fortran, and C++ at the time was correctly viewed
as a rapidly mutating language). There's also the implicit assumption
made that if exceptions arrive extremely rapidly, it is allowed to
discard all but the last one; this did not sit well with one of the
companies involved in that discussion, though I failed to understand
how it was possible to do otherwise, for sufficiently high rates of
exception delivery.
--
David Chase, chase@world.std.com
[For info on back issues of the JCLT, see http://jclt.iecc.com. -John]