All exits are initially represented by EXIT nodes: How about an Exit node:
(defstruct (exit (:include node)) value)
The Exit node uses the continuation that is to receive the thrown Value. During optimization, if we discover that the Cont’s home-lambda is the same as the exit node’s, then we can delete the Exit node, substituting the Cont for all of the Value’s uses.
The successor block of an EXIT is the entry block in the entered environment. So we use the Exit node to mark the place where exit code is inserted. During environment analysis, we need only insert a single block containing the entry point stub.
We ensure that all Exits that aren’t for a NLX don’t have any Value, so that local exits never require any value massaging.
The Entry node marks the beginning of a block or tagbody:
(defstruct (entry (:include node)) (continuations nil :type list))
It contains a list of all the continuations that the body could exit to. The Entry node is used as a marker for the place to snapshot state, including the control stack pointer. Each lambda has a list of its Entries so that environment analysis can figure out which continuations are really being closed over. There is no reason for optimization to delete Entry nodes, since they are harmless in the degenerate case: we just emit no code (like a no-var let).
We represent CATCH using the lexical exit mechanism. We do a transformation like this:
(catch 'foo xxx) ==> (block #:foo (%catch #'(lambda () (return-from #:foo (%unknown-values))) 'foo) (%within-cleanup :catch xxx))
%CATCH just sets up the catch frame which points to the exit function. %Catch is an ordinary function as far as ICR is concerned. The fact that the catcher needs to be cleaned up is expressed by the Cleanup slots in the continuations in the body. %UNKNOWN-VALUES is a dummy function call which represents the fact that we don’t know what values will be thrown.
%WITHIN-CLEANUP is a special special form that instantiates its first argument as the current cleanup when converting the body. In reality, the lambda is also created by the special special form %ESCAPE-FUNCTION, which gives the lambda a special :ESCAPE kind so that the back end knows not to generate any code for it.
We use a similar hack in Unwind-Protect to represent the fact that the cleanup forms can be invoked at arbitrarily random times.
(unwind-protect p c) ==> (flet ((#:cleanup () c)) (block #:return (multiple-value-bind (#:next #:start #:count) (block #:unwind (%unwind-protect #'(lambda (x) (return-from #:unwind x))) (%within-cleanup :unwind-protect (return-from #:return p))) (#:cleanup) (%continue-unwind #:next #:start #:count))))
We use the block #:unwind to represent the entry to cleanup code in the case where we are non-locally unwound. Calling of the cleanup function in the drop-through case (or any local exit) is handled by cleanup generation. We make the cleanup a function so that cleanup generation can add calls at local exits from the protected form. #:next, #:start and #:count are state used in the case where we are unwound. They indicate where to go after doing the cleanup and what values are being thrown. The cleanup encloses only the protected form. As in CATCH, the escape function is specially tagged as :ESCAPE. The cleanup function is tagged as :CLEANUP to inhibit let conversion (since references are added in environment analysis.)
Notice that implementing these forms using closures over continuations eliminates any need to special-case ICR flow analysis. Obviously we don’t really want to make heap-closures here. In reality these functions are special-cased by the back-end according to their KIND.