8.2 Wrappers

Once a function has been defined, one may want it to do something a little different, or just a little bit more. Breaking and tracing functions for debugging purposes can be thought of in this way. Having a function keep track of the time spent in its execution can also be thought of in this way. The Wrappers module provides a convenient facility to support this.

When a function is wrapped, its name becomes a reference for two different function definitions. In PSL it is possible to create distinct identifiers which have the same name. The original name of the function is associated with a new function called the wrapper. In general a wrapper does additional work before and/or after applying the original definition. The original definition is associated with an identifier whose name is identical with the original name but which is not interned. Application of getd to the original name will return the original definition. Since a wrapper is identified by an indicator on the property list of its name getd knows when to look elsewhere for the original function definition, and putd knows to alter the original definition, not the wrapper.

Typically a wrapper may be added to a function and later removed. A function potentially may be wrapped up inside more than one wrapper at the same time. It is possible to redefine a wrapped function but if the order or number of formal parameters is changed then it will be necessary to unwrap all wrappers first.

8.2.1 Notes on Writing Wrappers

This section describes guidelines for writing wrappers.

If a wrapper is put on a function that is used either directly or indirectly by the wrapper body an infinite recursion may result. Functions used by the PSL interpreter are particularly susceptible to this problem. We require a means to avoid infinite recursion. In general it is not easy to restrict the set of functions which can be called indirectly by a piece of code. Many PSL system functions use other system functions. Furthermore, a modification to the system may change these relationships. Some system functions call functions through functional variables or functional values stored on property lists. This means that some of the relationships between system functions change over time.

Instead of avoiding recursion introduced by wrappers, we can detect and recover from it. Wrapper bypasses do this. A wrapper bypass is implemented through a fluid variable, called the controlling variable for that wrapper. On entry to a wrapper, the value of the controlling variable is checked. A non-nil value implies that a previous invocation of this wrapper has not been completed. To avoid a recursive call on the wrapper, a call on the wrapped function replaces evaluation of the wrapper.

If the value of the controlling variable is nil the wrapper is evaluated. During evaluation of the wrapper the controlling variable is set to t except during the application of the wrapped function, at which point it should be set to nil.

Syntactically, wrapper bodies of the following form are supported:

(lambda <args>  
  (cond (<var> <wrapped-fn-call>)  
        (t (prog (<var> <id>⋆)  
             (setq <var> t)  
<call-expr> ::= <expr>  
                | (<call-expr>⋆)  
                | (setq <var> nil) <setq-and-wrapped-fn-call> (setq <var> t)  
<setq-and-wrapped-fn-call> ::=  
               | <wrapped-fn-call>  
               | (setq <id> <wrapped-fn-call>)  
<wrapped-fn-call> ::= (wrapped-function <expr>⋆)  
                  | (funcall 'wrapped-function <expr>⋆)  
<var> ::= <id>  
<id> is any PSL identifier <expr> is any PSL s-expression.

The controlling variable of the wrapper is < var >. It must be a fluid variable whose top level value is nil.

The notation is BNF with the addition of the regular expression ”*” operator. This operator is used to indicate zero or more repetitions of an item.

Note that setf is not supported in place of setq. You should not use apply in place of funcall or let in place of prog. You can eliminate redundant instances of (setq < var > t) and (setq < var > nil). For example, if a <wrapped-fn-call> appears at the beginning of a prog then the form (setq <var> nil) can be omitted.

8.2.2 Exported Functions

(wrap name:id wtype:id BODY:list COMPILE?:boolean): id expr
Wrap creates a wrapper. name is the name of the function to be wrapped. wtype is an id which identifies the wrapper type, it must be a member of the list wrapper-standard-order (documented below). BODY is a list which should be a lambda expression.
Wrap redefines the functional value of name. The original function is renamed. The print representation of this new name is identical to name but the identifiers are distinct.

When BODY is a lambda expression (you are encouraged to use a lambda expression for this argument), then the new name of the function which is being wrapped is substituted for each occurance of wrapper-function. The result of this substitution becomes the definition of the wrapper.

When BODY is not a lambda expression a different method is used to create the wrapper. BODY is embedded within a lambda expression which has the same number of arguments as the function which is being wrapped. Each occurance of the form (wrapped-invocation), is replaced by an application of the wrapped function.

If COMPILE? is t then the wrapper will be compiled.

The wrappers package permits a function to have any number of wrappers of any permitted type. For a given type of wrapper, a new wrapper always encloses preexisting wrappers of that type. However, you are advised against depending upon this ordering.

(remove-wrapper name:id TYPE:id): boolean expr
Removes a wrapper of type TYPE from the identifier name. If name does not have a wrapper of this type then the return value will be nil, otherwise t is returned.

(wrapped? name:id): boolean expr
Returns t if there is at least one wrapper on name.

(wrapper-of-type? name:id TYPE:id): name:id nil expr
Searches for a wrapper of the given type on name. name is returned if a wrapper is found, otherwise nil is returned.

(wrapper-types name:id): list expr
Returns a list of all the types of all the wrappers around the function named name.

(function-lambda-list FN:id, code-pointer, lambda): list expr
Returns a list suitable for use as the formal parameter list of a wrapper for that function. If the function is interpreted, the argument list of the definition is returned. If the actual argument list is not available, names for the arguments are invented. If FN is not a lambda expression, code-pointer, or identifier then it is an error.
⋆⋆⋆⋆⋆ FN is not a function.

It is also an error if FN is an identifier which does not have a functional value.

⋆⋆⋆⋆⋆ FN is not defined as a function.

(function-basic-definition name:id): name:id expr
Returns the basic definition of name, whether or not a set or wrappers is associated with it. Note that getd and putd operate on this basic definition. If you wish to redefine a wrapped function and the number or order of formal parameters will change it is necessary to first unwrap all wrappers first. Applications of the wrapped function within the wrapper will not be updated when the wrapped function is redefined.

wrapper-standard-order = [Initially: (trace break meter)] global
Only wrapper types which are members of this list are legal arguments to primitive functions that operate on wrappers. The wrappers of each function are nested so that each wrapper type appears earlier in wrapper-standard-order than the type of any wrapper it encloses. Wrap wraps according to this ordering, but changing this list does not cause the ordering of existing wrappers to be changed.

8.2.3 Examples

Assume that foo is a commonly called function whose argument is a large data structure. You have found that foo is sometimes called with an argument of nil and would like to cause a break when this situation arises so that you can discover where foo is being called from.

Since foo is a commonly used function a break on each entry to foo is unsatisfactory. A trace of each entry and exit is not sufficient – it doesn’t tell you who called foo in the critical case. The solution is to wrap foo is an advice wrapper. Such a wrapper will print a backtrace and enter a continuable break loop when an argument of nil is discovered.

First, load the break-trace module. This will result in the wrappers module being loaded and the breaktrace function being defined (see Chapter 17 for more information on breaktrace). The wrapper-type advice is added as the innermost wrapper on the ordered list referenced by wrapper-standard-order (assuming it is not already on the list).

  (load break-trace)               % loads wrappers &  
                                     breakpoint function  
  (if (not member 'advice wrapper-standard-order) then  
    (setf wrapper-standard-order  
          (append wrapper-standard-order '(advice))))  
  (de debug-foo ()  
     'foo                          % function to wrap  
     'advice                       % wrapper type  
     ‘(lambda (x)                  % wrapper body generator  
        (cond ((null x)  
               (breakpoint "Foo (x=nil)")))  
        (wrapped-function x))  
     nil))                          % don't compile  
                                    % the wrapper body

The above will work fine as long as foo is an expr which is not used by the interpreter or by the wrappers module itself. In the more general case, the function debug-foo must have three changes:

  1. If foo is not an expr, then all occurrences of (wrapped-function < args >) must be replaced by (funcall ’wrapped-function < args >).
  2. If foo is used by the PSL interpreter, by the wrappers module, or by expression in the wrapper body then it will be necessary to know when foo is called during the evaluation of foo’s wrapper. This will allow you to bypass evaluation of the wrapper, you can simply apply the wrapped function. Otherwise an infinite recursion could result. The definition below uses the fluid variable (called a controlling variable) in-foo-wrapper? to achieve this effect.
  3. If foo is possibly used within the interpreter, then the wrapper must be compiled. This means that if (not (interpretive-wrapper-ok? ’foo)), then the fourth argument to the function wrap must be t, or wrap will signal an error.

The revised definition of debug-foo follows.

(fluid  '(in-foo-wrapper?))     % necessary initializations of  
  (setq in-foo-wrapper? nil)      % the controlling  variable  
(change 2)

  (de debug-foo ()  
     ‘(lambda (x)  
        (cond (in-foo-wrapper?                  % change 2  
               (funcall 'wrapped-function x))   % change 1  
               (prog (in-foo-wrapper?)  
                 (setq in-foo-wrapper? t)  
                 (cond ((null x)  
                        (breakpoint "Foo (x=nil)")))  
                 (setq in-foo-wrapper? nil)  
                  (funcall 'wrapped-function x) % change 1  
     (not (interpretive-wrapper-ok? 'foo))))    % change 3

The function break-on-condition will cause entry into a continuable break loop just before the function fn is applied if the result of evaluating the form bound to bool-expr is non-nil. A sample call might be (break-on-condition ’foo ’(eq x 2)).

(setf in-wrapper? nil)  
(fluid '(in-wrapper?))  
(de break-on-condition (fn bool-expr)  
    % First, get the parameter-list of the function.  
  (let ((parameter-list (function-lambda-list fn)))  
     ‘(lambda ,parameter-list  
        (cond (in-wrapper?  
                (funcall 'wrapped-function ,@parameter-list))  
               (prog (in-wrapper?)  
                 (setq in-wrapper? t)  
                 (cond (,bool-expr  
                        (breakpoint "In %p, condition %p=%p"  
                          ',fn  ',bool-expr ,bool-expr)))  
                        (setq in-wrapper? nil)  
                         (funcall 'wrapped-function  
     (not (interpretive-wrapper-ok? fn))