Design notes for Common Lisp control structures: The basic non-local jump constructs defined by Common Lisp are return, return-from and go. From now on I will use go to represent all of them, as the first two can be thought of as jumps to a special block-exit tag. The target of a go is a (frame, pc) pair. When a non-local go is executed, the stack must be unwound back to the target frame, any intermediate bindings undone, the eval-stack adjusted, possibly pushing new values onto it, and execution resumed at the new pc. In addition, targets of gos are lexically scoped and have dynamic extent. In particular, it is an error to go to a tag once the body containing it has been exited. The case where the target frame is the same as the one being jumped from can be adequately handled by existing mechanisms. If the frames are not the same, the jump must be from a lexically nested closure. NB: this means that targets of gos must be closed over. It is not sufficient to treat go targets exactly the same way as free variables that are closed over. Since go targets have dynamic extent, they must somehow be invalidated. The compiler can certainly know the target pc, but the target frame can be arbitrarily far from the jumper. Thus some runtime mechanism is needed that will uniquely identify the target of a jump. The current thinking is that when a contour containing a go target that is closed over is entered, a unique identifying mark will get pushed onto the stack. Because of the invalidation requirement, this mark must be a heap-allocated object, (either a CONS or a special datatype). The closure object then points to this mark. When the contour is exited, the marker is simply popped off the stack. [Where does the mark actually go? (A distingushed local variable just on the stack, you can recognize it if its a special datatype) What do you do for nested tagbodys? (New frame (don't need it if you can recongize them))] When a non-local go is executed, the stack is searched for the corresponding mark. The stack then gets unwound to that frame, and execution proceeds from the new pc. Associated with each tag is the absolute stack depth at that point. Before execution is resumed at the new location, the stack must be adjusted to the appropriate depth. [I'm not too clear on exactly how all this is to happen] As the stack is unwound because of a non-local go, frames that are about to be unwound are given a chance to handle the signal Unwind. This leads us to the error system. The error system borrows heavily from Mesa SIGNALs. We introduce the new forms raise-signal, enable, reject and resume. raise-signal sigtype &key :resumable :must-catch &allow-other-keys [Function] raise-signal raises a signal of type sigtype, which must be a subtype of Condition. It creates an object of the given type, passing along any keyword parameters given to be used as parameters to the creation function. It will look up the call chain for signal catchers set up by enable. If a matching catch phrase is found, it is executed. The catch phrase may cause a non-local transfer out of the enable, it may reject the signal, or it may resume the signal. In the latter case, the value of the raise-signal expression is the resumed value, unless resumable is false, in which case a resume error is signalled. If no catch phrase accepts the signal and must-catch is true, an uncaught signal will be raised and the uncaught signal handler invoked. This will typically cause a break. enable form {( {sig | ({sig}*)} {recovery-form}*)}* [Special form] enable is the primitive form for catching signals. It establishes a signal catcher similar to that established by the catch special form. form is evaluated, and if there are no conditions signalled during this evaluation, the value of the entire enable is simply the value of form. Following the form to be evaluated are a series of catch phrases. Each catch phrase consists of a signal or list of signals to be caught, followed by a series of forms which are the actions to be taken. A catch phrase must be exited in one of the ways described above. Signals are matched by type, so the order of the catch phrases within an enable is important. The enable is an implicit block. If form returns multiple values then so does the enable. resume [value] [Special form] resume stops the propagation of a signal and causes value to be returned as the value of the most recent raise-signal. resume must be within the lexical scope of a catch phrase . If the signal is not resumable, resume will cause an error. reject [Special form] reject returns from a catch phrase and causes the current signal to continue propagating up the call chain. If a catch phrase is not exited by either a non-local jump or a resume, it implicitly rejects the signal. An explicit reject form is provided for clarity. [This is similar to the way IL:PROG is now handled - (RETURN NIL) is tacked on to the end and then possibly optimized out.] reject must be within the lexical scope of a catch phrase. Signals are instances of subclasses of Condition. Their instance variables carry whatever information the signal needs to convey. The signaller maintains the invariant that during any invocation of the signaller, a catch phrase will a particular signal at most once. This is so that no state need be kept in the event that catch phrases themselves raise signals. An outsanding question is the meaning of "same signal". The choices are same object or same type of object. unwind-protect, catch and throw are easily implemented using this mechanism. Namely, (defmacro unwind-protect (form &rest recovery) `(enable (multiple-value-prog1 ,form ,@recovery) (Unwind ,@recovery (reject))) (defstruct (Throw (:include Condition)) tag result) (defmacro catch (tag &rest forms) (let ((tg (gensym))) `(let ((,tg ,tag)) (enable (progn ,@forms) (Throw (if (eq ,tg (Throw-tag condition)) (return (value-list (Throw-result condition))) (reject))) )) )) (defmacro throw (tag result) '(progn (raise-signal 'Throw :tag ,tag :result (multiple-value-list ,result) :resumable nil :must-catch nil) (uncaught-throw ,tag))) Implementation details: enable is actually implemented by the function enable!. This is a function of two arguments, a closure that represents the form to be evaluated, and another closure that contains all the catch phrases. When the signaller looks for a catch phrase that enables a particular signal, it actually looks for enable! frames, then applys the catch closure. The catch closure is what determines if there is a match or not. It will either go somewhere else, return nil, meaning that the signal is rejected by the catch, or return the symbol resume along with a list of values to be returned. There is a third case that is used internally to maintain the signaller invariant. A catch closure can return the symbol skip and a frame pointer. The signal is considered to be rejected, and the catch phrase search is resumed from the given frame instead of the next frame that would normally be used. Other issues: Since enable is an implicit block, returns from the protected form will only return from the enable. Is this OK for unwind-protect? This is a fair amount of overhead for catch and throw. Since unwinding after the throw needs to observe unwind-protects, there is not a lot we can do to speed up that end of the process. On the other hand, we can easily special-case the catch-finding phase if that turns out to be a serious performance problem. Having signals be matched by type is very handy. Unfortunately, having the signals themselves be instances of that type is somewhat clumsy. If an application wants to define a signal, it needs to do something like (defstruct (mySig (:include someOtherSig)) ...). I've hidden some of the grossness by having raise-signal take a signal type instead of a signal object, along with whatever keyword parameters you want to pass to the creator function. This leaves the implementation problem of finding the creation function given the type. It's simple enough to construct the default name, make-foo, but which package do you look in? [NOTE ALSO: type-of isn't guaranteed. Should instead use TYPEP? Maybe a simpler mechanism, e.g., with type tags, and inheritance just done by property lists or some other simpler data structure?] Not covered in this note: Interaction with current Interlisp error system, i.e., RESETLST, ERRORSET, NLSETQ, et al. [ERRORSET can be done as an unwind-protect. RESETLST uses ERRORSET, I think. Dunno if we want to change it. Only problem is that there are folks in the system who look up the stack for ERRORSETs and look at their 3rd argument, etc.).] (( GACHAMODERN MODERN MODERNMODERN MODERN GACHA GACHA ?1(DEFAULTFONT 1 (GACHA 10) (GACHA 8) (TERMINAL 8)) GACHA GACHA ?1(DEFAULTFONT 1 (GACHA 10) (GACHA 8) (TERMINAL 8)) 1 ? t)ßQ7ì5L9ÆË.'v/N%+    ,     År 2 ) ` y          o h5 *     .0 V   Ü›5 ' §7 / Q  4  "    2 @       I &    ) ø ’ I -y±   <  &    ÂvÄ8   ë"g?zº