Trailing-Edge
-
PDP-10 Archives
-
decus_20tap1_198111
-
decus/20-0004/12lisp.doc
There are no other files named 12lisp.doc in the archive.
SECTION 12
VARIABLE BINDINGS, PUSH DOWN LIST FUNCTIONS,
AND THE SPAGHETTI STACK
A number of schemes have been used in different implementations of LISP
for storing the values of variables. These include:
1. Storing values on an association list paired with the variable
names.
2. Storing values on the property list of the atom which is the
name of the variable.
3. Storing values in a special value cell associated with the atom
name, putting old values on a pushdown list, and restoring
these values when exiting from a function.
4. Storing values on a pushdown list.
In INTERLISP, we currently use a variation on the fourth scheme, usually
used only for transmitting values of arguments to compiled functions.
When a function is entered, the association of names with the values of
variables (arguments) is accomplished by putting both names and values
in a block of storage called the basic frame for the function. These
basic frames are allocated on a stack or pushdown list. To find the
value of a variable used freely within a function, successive basic
frames are searched until a slot is found which contains the name of the
variable, and then the corresponding value is used. Compiled functions
know about the relative position of variables within their basic frame,
and can pick up these values immediately without search. However,
compiled functions still store both the names and values of variables so
that interpreted and compiled functions are compatible and can be freely
intermixed, i.e., free variables can be used with no special
declarations necessary. The names are also very useful in debugging,
for they make possible a complete symbolic backtrace in case of error.
In addition to the binding information, additional information is
associated with each function call: control information indicating the
calling function, access information indicating the path to search the
basic frames, and temporary results are also stored on the stack in a
block called the frame extension. The interpreter also stores
information about partially evaluated expressions as described below.
12.1 The Push-Down List and the Interpreter
In addition to the names and values of arguments for functions,
12.1
information regarding partially-evaluated expressions is kept on the
push-down list. For example, consider the following definition of the
function fact (intentionally faulty):
(FACT
[LAMBDA (N)
(COND
((ZEROP N)
L)
(T (ITIMES N (FACT (SUB1 N])
In evaluating the form (FACT 1), as soon as fact is entered, the
interpreter begins evaluating the implicit progn following the LAMBDA
(see Section 4). The first function entered in this process is cond.
cond begins to process its list of clauses. After calling zerop and
getting a NIL value, cond proceeds to the next clause and evaluates T.
Since T is true, the evaluation of the implicit progn that is the
consequent of the T clause is begun (see Section 4). This requires
calling the function itimes. However before itimes can be called, its
arguments must be evaluated. The first argument is evaluated by
searching the stack for the last binding of N; the second involves a
recursive call to fact, and another implicit progn, etc.
Note that at each stage of this process, some portion of an expression
has been evaluated, and another is awaiting evaluation. The output
below illustrates this by showing the state of the push-down list at the
point in the computation of (FACT 1) when the unbound atom L is reached.
12.2
_FACT(1)
u.b.a. L {in FACT} in ((ZEROP N) L)
(L BROKEN)
:BTV!
*TAIL* (L)
*ARG1 (((ZEROP N) L) (T (ITIMES N (FACT (SUB1 N)))))
COND
*FORM* (COND ((ZEROP N) L) (T (ITIMES N (FACT (SUB1 N)))))
*TAIL* ((COND ((ZEROP N) L) (T (ITIMES N (FACT (SUB1 N))))))
N 0
FACT
*FORM* (FACT (SUB1 N))
*FN* ITIMES
*TAIL* ((FACT (SUB1 N)))
*ARGVAL* 1
*FORM* (ITIMES N (FACT (SUB1 N)))
*TAIL* ((ITIMES N (FACT (SUB1 N))))
*ARG1 (((ZEROP N) L) (T (ITIMES N (FACT (SUB1 N)))))
COND
*FORM* (COND ((ZEROP N) L) (T (ITIMES N (FACT (SUB1 N)))))
*TAIL* ((COND ((ZEROP N) L) (T (ITIMES N (FACT (SUB1 N))))))
N 1
FACT
**TOP**
Internal calls to eval, e.g., from cond and the interpreter, are marked
on the push-down list by a special mark or blip which the backtrace
1
prints as *FORM*. The genealogy of *FORM*'s is thus a history of the
computation. Other temporary information stored on the stack by the
interpreter includes the tail of a partially evaluated implicit progn
(e.g., a cond clause or lambda expression) and the tail of a partially
evaluated form (i.e., those arguments not yet evaluated), both indicated
on the backtrace by *TAIL*, the values of arguments that have already
been evaluated, indicated by *ARGVAL*, and the names of functions
waiting to be called, indicated by *FN*. *ARG1, ... *ARGn are used by
the backtrace to indicate the (unnamed) arguments to subrs.
------------------------------------------------------------------------
1
Note that *FORM*, *TAIL*, *ARGVAL*, etc., do not actually appear on
the backtrace, i.e., evaluating *FORM* or calling stkscan to search
for it will not work. However, the functions blipval, setblipval,
and blipscan described below are available for accessing these
internal blips.
12.3
Note that a function is not actually entered and does not appear on the
2
stack, until its arguments have been evaluated. Also note that the
*ARG1, *FORM*, *TAIL*, etc. "bindings" comprise the actual working
storage. In other words, in the above example, if a (lower) function
changed the value of the *ARG1 binding, the cond would continue
interpreting the new binding as a list of cond clauses. Similarly, if
the *ARGVAL* binding were changed, the new value would be given to
itimes as its first argument after its second argument had been
evaluated, and itimes was actually called.
Blip Functions
The temporaries of the interpreter, or blips, can be accessed by the
following three functions, which currently know about four different
types of blips:
*FN* the name of a function about to be called
*ARGVAL* an argument for a function about to be called
*FORM* a form in the process of evaluation
*TAIL* the tail of a cond clause, implicit progn, prog, etc.
blipval[bliptyp;ipos;flg]
Returns the value of the specified blip of type
bliptyp. If flg is a number, finds the nth blip
of the desired type, searching the control chain
beginning at the frame specified by the stack
descriptor ipos. If flg is NIL, 1 is used. If
flg is T, returns the number of blips of the
specified type at ipos.
setblipval[bliptyp;ipos;n;val]
Sets the value of the specified blip of type
bliptyp. Searches for the nth blip of the
desired type, beginning with the frame specified
by the stack descriptor ipos, and following the
control chain.
blipscan[bliptyp;ipos] Returns a stack pointer to the frame in which a
blip of type bliptyp is located. Search begins
at the frame specified by the stack descriptor
ipos and follows the control chain.
12.2 The Pushdown List and Compiled Functions
Calls to compiled functions, and the bindings of their arguments, i.e.,
------------------------------------------------------------------------
2
except for functions which do not have their arguments evaluated
(although they themselves may call eval, e.g., cond).
12.4
names and values, are handled in the same way as for interpreted
functions (hence the compatibility between interpreted and compiled
functions). However, compiled functions treat free variables in a
special way that interpreted functions do not. Interpreted functions
"look up" free variables when the variable is encountered, and may look
up the same variable many times. However, compiled functions look up
3
each free variable only once. Whenever a compiled function is entered,
the stack is scanned and the most recent binding for each free variable
used in the function is found (or if there is no binding, the value cell
is obtained) and stored on the stack (and marked in a special way to
distinguish this 'binding' from ordinary bindings). Thus, following the
bindings of their arguments, compiled functions store on the stack
pointers to the bindings for each free variable used in the function.
12.3 The Spaghetti Stack
The Bobrow/Wegbreit paper, "A Model and Stack Implementation for
Multiple Environments" [Bob3], describes an access and control mechanism
more general than the simple pushdown stack. The access and control
mechanism used by INTERLISP is a slightly modified version of the one
proposed by Bobrow and Wegbreit. This mechanism is called the
"spaghetti stack."
The spaghetti system presents the access and control stack as a data
structure composed of "frames." The functions described below operate on
this structure. These primitives allow user functions to manipulate the
stack in a machine independent way. Backtracking, coroutines, and more
sophisticated control schemes can be easily implemented with these
primitives.
Overview of Spaghetti Stack
The evaluation of a function requires the allocation of storage to hold
the values of its local variables during the computation. In addition
to variable bindings, an activation of a function requires a return link
(indicating where control is to go after the completion of the
computation) and room for temporaries needed during the computation. In
the spaghetti system, one "stack" is used for storing all this
information, but it is best to view this stack as a tree of linked
objects called frame extensions (or simply frames).
A frame extension is a variable sized block of storage containing a
frame name, a pointer to some variable bindings (the blink), and two
pointers to other frame extensions (the alink and clink). In addition
to these components, a frame extension contains other information (such
as temporaries and reference counts) that does not interest us here.
The block of storage holding the variable bindings is called a basic
------------------------------------------------------------------------
3
A list of all free variables is generated at compile time, and is in
fact obtainable from the compiled definition. See Section 18.
12.5
frame. A basic frame is essentially an array of pairs, each of which
contains a variable name and its value. The reason frame extensions
point to basic frames (rather than just having them "built in") is so
that two frame extensions can share a common basic frame. This allows
two processes to communicate via shared variable bindings.
The chain of frame extensions which can be reached via the successive
alinks from a given frame is called the access chain of the frame. The
first frame in the access chain is the starting frame. The chain
through successive clinks is called the control chain.
A frame extension completely specifies the variable bindings and control
information necessary for the evaluation of a function. Whenever a
function (or in fact, any form which generally binds local variables) is
evaluated, it is associated with some frame extension.
In the beginning there is precisely one frame extension in existence.
This is the frame in which the top-level call to the interpreter is
being run. This frame is called the "top-level" frame.
Since precisely one function is being executed at any instant, exactly
one frame is distinguished as having the "control bubble" in it. This
frame is called the active frame. Initially, the top-level frame is the
active frame. If the computation in the active frame invokes another
function, a new basic frame and frame extension are built. The frame
name of this basic frame will be the name of the function being called.
The b-, a-, and clinks of the new frame all depend on precisely how the
function is invoked. The new function is then run in this new frame by
passing control to that frame, i.e., it is made the active frame.
If the active computation needs the value of some variable it is
obtained by searching the access chain of the active frame. Each blink
along the access chain is scanned for the variable name. If a binding
is found, the associated value is used. If none is found, the top-level
value is used.
Once the active computation has been completed, control normally returns
to the frame pointed to by the clink of the active frame. That is, the
frame in the clink becomes the active frame.
In most cases, the storage associated with the basic frame and frame
extension just abandoned can be reclaimed. However, it is possible to
obtain a pointer to a frame extension and to "hold on" to this frame
even after it has been exited. This pointer can be used later to run
another computation in that environment, or even "continue" the exited
computation.
A separate data type, called a stack pointer, is used for this purpose.
A stack pointer is just a cell that literally points to a frame
extension. Stack pointers print as #adr/framename, e.g., #117753/COND.
Stack pointers are returned by many of the stack manipulating functions
described below. Except for certain abbreviations (such as "the frame
with such-and-such a name"), stack pointers are the only way the user
can reference a frame extension. As long as the user has a stack
pointer which references a frame extension, that frame extension (and
all those that can be reached from it) may not (will not) be garbage
collected.
12.6
Note that two stack pointers referencing the same frame extension are
not necessarily eq, i.e., (EQ (STKPOS 'FOO) (STKPOS 'FOO))=NIL.
However, eqp can be used to test if two different stack pointers
reference the same frame extension.
12.4 Stack Functions
In the descriptions of the stack functions below, when we refer to an
argument as a stack descriptor, we mean that it is either a stack
pointer or one of the following abbreviations:
1. NIL means the active frame; that is, the frame of the stack
function itself.
2. T means the top-level frame.
3. Any other literal atom is equivalent to (STKPOS ATOM -1).
4. A number is equivalent to (STKNTH number).
In the stack functions described below, the following errors can occur.
ILLEGAL STACK ARG Occurs when a stack descriptor is expected and
the supplied argument is either not a legal
stack descriptor (i.e., not a stack pointer,
litatom, or number), or is a litatom or number
for which there is no corresponding stack frame
(e.g., (STKNTH -1 (QUOTE FOO)) where there is no
frame named FOO in the active control chain or
(STKNTH -10 (QUOTE EVALQT)).
STACK POINTER
HAS BEEN RELEASED Occurs whenever a released stack pointer is
supplied as a stack descriptor argument for any
purpose other than as a stack pointer to re-use.
Functions
stkpos[framename;n;ipos;opos]
Search for the nth frame with name framename.
The search begins with (and includes) the frame
specified by the stack descriptor ipos (initial
position). The search proceeds along the
control chain from ipos if n is negative, or
along the access chain if n is positive. If n
is NIL, -1 is used. Returns a stack pointer to
the frame if such a frame exists, otherwise
returns NIL. If opos is supplied and is a stack
pointer, it is reused. If opos is not a stack
pointer it is ignored. (Note that (STKPOS
(QUOTE STKPOS)) causes an error, ILLEGAL STACK
ARG; it is not permissible to create a stack
pointer to the active frame.)
12.7
stknth[n;ipos;opos] Returns a stack pointer to the nth frame back
from the frame specified by the stack descriptor
ipos. If n is negative, the control chain from
ipos is followed. If n is positive the access
chain is followed. If n equals 0, returns a
stack pointer to ipos, i.e., this provides a way
to copy a stack pointer. Returns NIL if there
are fewer than n frames in the appropriate
chain. If opos is supplied and is a stack
pointer, it is reused. If opos is not a stack
pointer it is ignored. (Note that (STKNTH 0)
causes an error, ILLEGAL STACK ARG; it is not
possible to create a stack pointer to the active
frame.)
stkname[pos] Returns the frame name of the frame specified
by the stack descriptor pos.
stknthname[n;ipos] Returns the frame name of the nth frame back
from ipos. Equivalent to (STKNAME (STKNTH n
ipos)) but avoids creation of a stack pointer.
In summary, stkpos converts function names to stack pointers, stknth
converts numbers to stack pointers, stkname converts stack pointers to
function names, and stknthname converts numbers to function names.
The following functions are used for accessing and changing bindings.
Some of functions take an argument, n, which specifies a particular
binding in the basic frame. If n is a literal atom, it is assumed to be
the name of a variable bound in the basic frame. If n is a number, it
is assumed to reference the nth binding in the basic frame. The first
binding is 1. If the basic frame contains no binding with the given
name or if the number is too large or too small, the error ILLEGAL ARG
results.
stkscan[var;ipos;opos] Searches beginning at ipos for a frame in which
a variable named var is bound. The search
follows the access chain. Returns a stack
pointer to the frame if found, otherwise returns
NIL. If opos is a stack pointer it is reused,
otherwise it is ignored.
framescan[atom;pos] Returns the relative position of the binding of
atom in the basic frame of pos.
stkarg[n;pos] Returns the value of the binding specified by n
in the basic frame of the frame specified by the
stack descriptor pos. n can be a literal atom
or number.
12.8
stkargname[n;pos] Returns the name of the binding specified by n,
in the basic frame of the frame specified by the
stack descriptor pos. n can be a literal atom
or number.
setstkarg[n;pos;value] Sets the value of the binding specified by n in
the basic frame of the frame specified by the
stack descriptor pos. n can be a literal atom
or a number. Returns value.
setstkargname[n;pos;name]Sets the name of the binding specified by n in
the basic frame of the frame specified by the
stack descriptor pos. n can be a literal atom
or a number. Returns name.
stknargs[pos] Returns the number of arguments bound in the
basic frame of the frame specified by the stack
descriptor pos.
As an example of the use of stknargs and stkargname:
variables[pos] returns list of variables bound at pos.
can be defined by:
(VARIABLES
[LAMBDA (POS)
(PROG (N L)
(SETQ N (STKNARGS POS))
LP (COND
((ZEROP N)
(RETURN L)))
(SETQ L (CONS (STKARGNAME N POS)
L))
(SETQ N (SUB1 N))
(GO LP])
The dual of variables is also available:
stkargs[pos] Returns list of values of variables bound at
pos.
The following functions are used to evaluate an expression in a
different environment, and/or to alter the flow of control.
enveval[form;apos;cpos;aflg;cflg]
evaluates form in the environment specified by
apos and cpos. That is, a new active frame is
created with the frame specified by the stack
descriptor apos as its alink, and the frame
12.9
specified by the stack descriptor cpos as its
clink. Then form is evaluated. If aflg is not
NIL, and apos is a stack pointer, then apos will
be released. Similarly, if cflg is not NIL, and
cpos is a stack pointer, then cpos will be
released.
envapply[fn;args;apos;cpos;aflg;cflg]
applys fn to args in the environment specified
by apos and cpos. aflg and cflg have the same
interpretation as with enveval.
stkeval[pos;form;flg] Evaluates form in the access environment of the
frame specified by the stack descriptor pos. If
flg is not NIL and pos is a stack pointer,
releases pos. The definition of stkeval is
(ENVEVAL FORM POS NIL FLG).
stkapply[pos;fn;args;flg]
Similar to stkeval but applies fn to args.
reteval[pos;form;flg] Evaluates form in the access environment of the
frame specified by the stack descriptor pos, and
then returns from pos with that value. If flg
is not NIL and pos is a stack pointer, then pos
is released. The definition of reteval is
equivalent to (ENVEVAL FORM POS (STKNTH -
1 POS) FLG T), except that reteval does not
create a stack pointer.
retapply[pos;fn;args;flg]
Similar to reteval except applies fn to args.
retfrom[pos;val;flg] Return from the frame specified by the stack
descriptor pos, with the value val. If flg is
not NIL, and pos is a stack pointer, then pos is
released. An attempt to retfrom the top level
(e.g., (RETFROM T)) causes an error, ILLEGAL
STACK ARG. Retfrom can be written in terms of
enveval as follows:
(RETFROM
(LAMBDA (POS VAL FLG)
(ENVEVAL (LIST (QUOTE QUOTE) VAL)
NIL
(COND
((STKNTH -1 POS (COND (FLG POS))))
(T (ERRORX (LIST 19 POS)))
NIL T)))
retto[pos;val;flg] like retfrom, except returns to frame specified
by pos.
12.10
evalv[x;pos] Evaluates x, where x is assumed to be a litatom,
in the access environment specifed by the stack
descriptor pos. While evalv could be defined as
(ENVEVAL X POS) it is in fact a subr which is
somewhat faster.
function[fn;env] If env is NIL, function is equivalent to quote
when interpreted and is also a signal to the
compiler that fn should be compiled. If env is
a stack pointer, then the value of function is
the expression (FUNARG fn env). When a funarg
expression is apply'd or is car of a form being
eval'd, the apply or eval takes place in the
access environment specified by env. For
example, if FOO is a funarg expression, then
(APPLY FOO FIE) is equivalent to
(ENVAPPLY (CADR FOO) FIE (CADDR FOO)). Env can
also be a list of variable names. In this case,
a new frame is created with the values of the
specified variables in the basic frame. The
variables are evaluated in the active access
environment (the environment of function). The
alink of the new frame is the active access
environment, and clink is the top level. The
value of function is (FUNARG fn pos), where pos
4
is a stack pointer to the new frame.
The following functions and variables are used to manipulate stack
pointers.
stackp[x] Returns x if x is a stack pointer, otherwise
returns NIL.
relstk[pos] Release the stack pointer pos. If pos is not a
stack pointer, does nothing. The value is pos.
clearstk[flg] If flg is NIL, releases all active stack
pointers, and returns NIL. If flg is T, returns
a list of all the active (unreleased) stack
pointers.
------------------------------------------------------------------------
4
Note that the effect of funarg in the spaghetti system is somewhat
different from what it was previously in non-spaghetti system. Now
when the funarg is apply'd or eval'd we see in the access
environment first the variables given in the list, and then the
access environment at the time the funarg was created. Formerly we
saw the variables in the list (the "own" variables) and then the
access environment at the time the funarg was used.
12.11
clearstklst Is a (global) variable used by top-level
evalqt. Every time evalqt is re-entered (e.g.,
following errors, or control-D), clearstklst is
checked. If its value is T, all active stack
pointers are released using clearstk. If its
value is a list, then all stack pointers on that
list are released. If its value is NIL, nothing
is released. clearstklst is initially T.
noclearstklst is a global variable used by top-level evalqt.
If clearstklst is T (see above) all active stack
pointers except those on noclearstklst are
released. noclearstklst is initially NIL.
Thus if one wishes to use multiple environments that survive through
control-D, either clearstklst should be set to T, or else those stack
pointers to be retained should be explicitly added to noclearstklst.
copystk[pos1;pos2] Copies the stack, including basic frames, from
the frame specified by the stack descriptor pos1
to the frame specified by the stack descriptor
pos2. That is, copies the frame extensions and
basic frames in the access chain from pos2 to
pos1 (inclusive). Pos1 must be in the access
chain of pos2, i.e., "above" pos2. Returns the
new pos2. This provides a way to save an entire
environment including variable bindings.
backtrace[ipos;epos;flags]
Performs a backtrace beginning at the frame
specified by the stack descriptor ipos, and
ending with the frame specified by the stack
descriptor epos. flags is a number in which the
options of the backtrace are encoded. If a bit
is set, the corresponding information is
included in the backtrace.
bit 0 - print arguments of non-subrs
bit 1 - print temporaries of the interpreter
bit 2 - print subr arguments and all temporaries
bit 3 - omit printing of UNTRACE: and function names
bit 4 - follow access chain instead of control chain.
For example: if flags=7, everything is printed; if flags=21Q, follows
the access chain, prints arguments.
12.12
mapdl[mapdlfn;mapdlpos] starts at mapdlpos and applies mapdlfn, a
function of two arguments, to the function name
at each frame, and the frame (stack pointer)
itself, until the top of the stack is reached.
Value is NIL.
For example, mapdl[(LAMBDA (X) (AND (EXPRP X) (PRINT X)))] will print
all exprs on the push-down list.
mapdl[(LAMBDA (X POS) (COND ((IGREATERP (STKNARGS POS) 2) (PRINT X] will
print all functions of more than two arguments.
searchpdl[srchfn;srchpos]
similar to mapdl, except searches the pushdown
list starting at position srchpos until it finds
a frame for which srchfn, a function of two
arguments, applied to the name of the function
and the frame itself is not NIL. Value is
(name . frame) if such a frame is found,
otherwise NIL.
12.5 Releasing and Reusing Stack Pointers
The creation of a single stack pointer can result in the retention of a
large amount of stack space. Furthermore, this space will not be freed
until the next garbage collection, even if the stack pointer is no
longer being used, unless the stack pointer is explicitly released or
reused. If there is sufficient amount of stack space tied up in this
fashion, a STACK OVERFLOW condition can occur, even in the simplest of
computations. For this reason, the user should consider releasing a
stack pointer when the environment referenced by the stack pointer is no
longer needed.
The effects of releasing a stack pointer are:
1. The link between the stack pointer and the stack is broken by
setting the contents of the stack pointer to the "released
mark" (currently unboxed 0). A released stack pointer prints
as #adr/#0.
2. If this stack pointer was the last remaining reference to a
frame extension; that is, if no other stack pointer references
the frame extension and the extension is not contained in the
active control or access chain, then the extension may be
reclaimed, and is reclaimed immediately. The process repeats
for the access and control chains of the reclaimed extension so
that all stack space that was reachable only from the released
stack pointer is reclaimed.
A stack pointer may be released using the function relstk, but there are
some cases for which relstk is not sufficient. For example, if a
function contains a call to retfrom in which a stack pointer was used to
specify where to return to, it would not be possible to simultaneously
release the stack pointer. (A relstk appearing in the function
following the call to retfrom would not be executed!) To permit release
of a stack pointer in this situation, the stack functions that
12.13
relinquish control have optional flag arguments to denote whether or not
a stack pointer is to be released. Note that in this case releasing the
stack pointer will not cause the stack space to be reclaimed immediately
because the frame referenced by the stack pointer will have become part
of the active environment.
Reusing Stack Pointers
Another way of avoiding creating new stack pointers is to reuse stack
pointers that are no longer needed. The stack functions that create
stack pointers (stkpos, stknth, and stkscan) have an optional argument
which is a stack pointer to reuse. When a stack pointer is reused, two
things happen. First the stack pointer is released (see above). Then
the pointer to the new frame extension is deposited in the stack
pointer. The old stack pointer (with its new contents) is the value of
the function. Note that the reused stack pointer will be released even
if the function does not find the specified frame.
Note that even if stack pointers are explicitly being released, creation
of many stack pointers can cause a garbage collection of stack pointer
space, GC: 5. Thus, if the user's application requires creating many
stack pointers, he definitely should take advantage of reusing stack
pointers.
12.6 Spaghetti and the Block Compiler
In order that the stack discipline be consistent, it is necessary that
all bindings that are either used freely or are used for communication
between processes, be contained in some basic frame. Basic frames are
never copied, except explicitly and presumably intentionally by copystk.
If we have a stack structure such as:
A
|
-------------
| |
B1-----B------B2
where frame extensions B1 and B2 share the basic frame B, then if B1
changes the value of one of its variables (in basic frame B), the new
value will be seen by B2. If a binding were stored in a frame
extension, which is copied, then the two processes, B1 and B2 would each
have their own copies of the binding. The block compiler produces
speedy code by avoiding function calls (and hence the creation of
frames). Bindings of variables (including specvars) in block compiled
code are contained in the frame extension. Therefore one should not
block compile any function that binds a "state variable" of some multi-
process computation. We hope to restructure the block compiler to
eliminate this problem - but for the moment it is a problem. For
example, if we have the following two functions:
12.14
(MARK (LAMBDA NIL (SETQ MARKPOS (STKNTH -1 (QUOTE MARK)))))
(FOO (LAMBDA (Y)
(SETQ Y 3)
(MARK)
(SETQ Y 4)
(ENVEVAL (QUOTE Y) MARKPOS)))
If FOO is interpreted or compiled individually (not block compiled) then
the value of the enveval is 4. However, if FOO is a function in a
compiled block and Y is a specvar, then the value of the enveval will be
3 because the active frame for the block containing FOO and the frame
retained by mark have their own private copies of the binding of Y.
5
12.7 Coroutines and Generators
This section describes an application of the spaghetti stack facility to
provide mechanisms for creating and using simple generators (with and
without CLISP, Section 23), generalized coroutines, and Conniver style
possibility lists.
A generator is like a subroutine except that it retains information
about previous times it has been called. Some of this state may be data
(for example, the seed in a random number generator), and some may be in
program state (as in a recursive generator which finds all the atoms in
a list structure). For example, if listgen is defined using defineq as:
(LISTGEN (L)
(IF L THEN (PRODUCE L:1) (LISTGEN L::1)))
we can use the function generator (described below) to create a
generator that uses listgen to produce the elements of a list one at a
time, e.g.,
GR_(GENERATOR (LISTGEN '(A B C))
creates a generator, which can be called by
(GENERATE GR)
to produce as values on successive calls, A, B, C. When generate (not
generator) is called the first time, it simply starts evaluating
(LISTGEN '(A B C)). produce gets called from listgen, and pops back up
to generate with the indicated value after saving the state. When
generate gets called again, it continues from where the last produce
left off. This process continues until finally listgen completes and
------------------------------------------------------------------------
5
Designed and implemented by D.G. Bobrow, who also did the
documentation. Early versions of the Conniver possibilites-list
package were written by Henry Thompson. Daryle Lewis found and
corrected a number of bugs, and wrote the compiler macros that go
with the package.
12.15
returns a value (it doesn't matter what it is). generate then returns
gr itself as its value, so that the program that called generate can
tell that it is finished, i.e., there are no more values to be
generated.
generator[form##;comvar##]
is an nlambda function that creates a generator
which uses form## to compute values. The value
of generator is a generator handle which is
represented by a dotted pair of stack pointers.
comvar## is optional. If its value (eval of) is
a generator handle, the list structure and stack
pointers will be reused. Otherwise, a new
generator handle will be constructed.
generator compiles open.
produce[val] is used from within (below) a generator to
return val as the value of the corresponding
call to generate.
generate[handle;val] restarts the generator represented by handle.
val will be returned as the value of the produce
which last suspended the operation of the
generator. When the generator runs out of
values, generate returns handle itself.
Examples
The following function will go down recursively through a list structure
and produce the atoms in the list structure one at a time.
[LEAVESG (L)
(if (ATOM L)
then (PRODUCE L)
else (LEAVESG L:1)
(if L::1
then (LEAVESG L::1]
The following function prints each of these atoms as it appears. It
illustrates how a loop can be set up to use a generator.
(PLEAVESG1 (L)
(PROG (X LHANDLE)
(LHANDLE_(GENERATOR (LEAVESG L)))
LP (X_(GENERATE LHANDLE))
(if X=LHANDLE
then (RETURN NIL))
(PRINT X)
(GO LP)))
Note that the loop terminates when the value of the generator is eq to
the dotted pair which is the value produced by the call to generator. A
12.16
CLISP iterative operator, OUTOF, is provided which makes it much easier
to write the loop in PLEAVESG1. OUTOF (or outof) can precede a form
which is to be used as a generator. On each iteration, the iteration
variable will be set to successive values returned by the generator; the
loop will be terminated automatically when the generator runs out. Thus
we can write
(PLEAVESG2 (L)
(for X outof (LEAVESG L) do (PRINT x))
as equivalent to the above program PLEAVESG1.
Here is another example:
(for X outof (MAPATOMS (FUNCTION PRODUCE))
as I from 1 to N do (PRINT X))
will print the first n atoms.
Coroutines
This package provides facilities for the creation and use of fully
general coroutine structures. It uses a stack pointer to preserve the
state of a coroutine, and allows arbitrary switching between n different
coroutines rather than just a call to a generator and return. This
package is slightly more efficient than the generator package described
above, and allows more flexibility on specification of what to do when a
coroutine terminates.
coroutine[callptr##;coroutptr##;coroutform##;endform##]
This nlambda is used to create a coroutine and
initialize the linkage. callptr## and
coroutptr## are the names of two variables,
which will be set to appropriate stack pointers.
If the values of callptr## or coroutptr## are
already stack pointers, the stack pointers will
be reused. coroutform## is the form which is
evaluated to start the coroutine; endform## is a
form to be evaluated if coroutform## actually
returns when it runs out of values. coroutine
compiles open.
resume[fromptr;toptr;val]
is used to transfer control from one coroutine
to another. fromptr should be the stack pointer
for the current coroutine, which will be smashed
to preserve the current state. toptr should be
the stack pointer which has preserved the state
of the coroutine to be transferred to, and val
is the value that is to be returned to the
latter coroutine as the value of the resume
which suspended the operation of that coroutine.
12.17
Examples
The following is the way one might write the LEAVES program using the
coroutine package:
(LEAVESC (L COROUTPTR CALLPTR)
(if (ATOM L) then (RESUME COROUTPTR CALLPTR)
else (LEAVESC L:1 COROUTPTR CALLPTR)
(if L::1 then (LEAVESC L::1 COROUTPTR CALLPTR))))
A function PLEAVESC which uses LEAVESC can be defined as follows:
(PLEAVESC (L)
(bind PLHANDLE LHANDLE first (COROUTNE PLHANDLE LHANDLE
(LEAVESC L LHANDLE PLHANDLE)
(RETFROM 'PLEAVESC))
do (PRINT (RESUME PLHANDLE LHANDLE))))
By RESUMEing leavesc repeatedly, this function will print all the leaves
of list L and then return out of pleavesc via the retfrom. The retfrom
is necessary to break out of the non-terminating do-loop. This was done
to illustrate the additional flexibility allowed through the use of
endform##.
We use two coroutines working on two trees in the example eqleaves,
defined below. eqleaves tests to see whether two trees have the same
leaf set in the same order, e.g., EQLEAVES((A B C)(A B (C))) is true.
(EQLEAVES (L1 L2)
(bind LHANDLE1 LHANDLE2 PE EL1 EL2
first (COROUTINE PE LHANDLE1 (LEAVESC L1 LHANDLE1 PE) 'NO-MORE)
(COROUTINE PE LHANDLE2 (LEAVESC L2 LHANDLE2 PE) 'NO-MORE)
do (EL1_(RESUME PE LHANDLE1))
(EL2_(RESUME PE LHANDLE2))
(if EL1~=EL2
then (RETURN NIL))
repeatuntil EL1='NO-MORE finally (RETURN T)))
6
Possibilities Lists
A possibilities list is the interface between a generator and a
consumer. The possibilities list is initialized by a call to
possibilities, and elements are obtained from it by using trynext. By
using the spaghetti stack to maintain separate environments, this
package allows a regime in which a generator can put a few items in a
possibilities list, suspend itself until they have been consumed, and be
subsequently aroused and generate some more.
------------------------------------------------------------------------
6
These functions are based on the CONNIVER system possibilities list
package.
12.18
possibilities[form##] This nlambda is used for the initial creation of
a possibilities list. form## will be evaluated
to create the list. It should use the functions
note and au-revoir described below to generate
possibilities. Normally, one would set some
variable to the possibilities list which is
returned, so it can be used later, e.g.,:
(SETQ PLIST (POSSIBILITIES (GENERFN V1 V2))).
possibilities compiles open.
note[val;lstflg] is used within a generator to put items on the
possibilities list being generated. If lstflg
is equal to NIL, val is treated as a single
item. If lstflg is non-NIL, then the list val
is nconced on the end of the possibilities list.
Note that it is perfectly reasonable to create a
possibilities list using a second generator, and
note that list as possibilities for the current
generator with lstflg equal to T. The lower
generator will be resumed at the appropriate
point.
au-revoir[val##] puts an item on the possibilities list if one is
given. If no argument is given to au-revoir then
7
the generator is just suspended; NIL is not put
on the possibilities list unless it is
explicitly given as an argument to au-revoir. A
call to au-revoir suspends the generator and
returns to the consumer in such a fashion that
control will return to the generator at the au-
revoir if the consumer exhausts the
possibilities list.
adieu[] returns to the consumer the items currently on
the possibilities list. It releases the
generator so that it can no longer be resumed.
trynext[plst##;endform##;val##]
This nlambda allows a consumer to use a
possibilities list. It removes the first item
from the possibilities list plst##, and returns
that item, provided it is not a generator
handle. If a generator handle is encountered,
------------------------------------------------------------------------
7
au-revoir is a lambda spread so that the case where no value is to
be returned can be distinguished from the case where a NIL value is
returned.
12.19
the generator is reawakened. When it returns a
possibilities list, this list is added to the
front of the current list. When a call to
trynext causes a generator to be awakened, val##
is returned as the value of the au-revoir which
put that generator to sleep. If plst## is
empty, it evaluates endform## in the caller's
environment.
trynext compiles open.
cleanposlst[plst] This function is provided to release any stack
pointers which may be left in the plst which was
not used to exhaustion.
Examples
(1) fib is a generator for fibonnaci numbers. It starts out by noteing
its two arguments, then suspends itself. Thereafter, on being re-
awakened, it will note two more terms in the series and suspends
again. printfib uses fib to print the first N fibonacci numbers.
[FIB (F1 F2)
(do (NOTE F1)
(NOTE F2)
(F1_F1+F2)
(F2_F1+F2)
8
(AU-REVOIR)]
[PRINTFIB (N)
(PROG ((FL (GENERATOR (FIB 0 1))))
(RPTQ N (PRINT (TRYNEXT FL)))
(CLEANPOSLST FL) ]
Note that fib itself will never terminate.
(2) nodes is somewhat subtler. It can be used to generate the nodes of
a tree in prefix, postfix, or infix order depending on the value of
type. It takes as its first argument L a structure which has three
fields; a head, left-daughter and right-daughter. Suppose these
fields are accessed by the functions HD, LD and RD respectively, and
R is the following tree:
------------------------------------------------------------------------
8
Note that this just suspends the generator and adds nothing to the
possibilities list except the generator.
12.20
(N1 _ '(+ N2 N3)) +
(N2 _ '(X)) / \
(N3 _ '( ** N4 N5)) x **
(N4 _ '(Y)) / \
(N5 _ '(3)) y 3
Each subnode is generated by using a possibilities list, and these
are oredered on the basis of type. Notice that since nodes ends
with an adieu, it does not persist. printnodes uses nodes to print
the nodes of a tree in any of the three orders.
[NODES (L TYPE)
(PROG [(VAL (HD L))
(LD L)
[LGEN (if (LD L)
then (POSSIBILITIES (NODE (LDL)
TYPE]
[RGEN (if (RD L)
then POSSIBILITIES (NODES (RD L)
TYPE]
(NOTE (SELECTQ TYPE
(PREFIX <! VAL LGEN RGEN>)
(INFIX <! LGEN VAL RGEN>)
(POSTFIX <! LGEN RGEN VAL>)
NIL)
T)
(ADIEU)]
[PRINTNODES (X TYPE)
(foreach E from (NODES X TYPE) do (PRIN1 E) finally (TERPRI)]
When printnodes is run, it produces the following.
_(PRINTNODES N1 'INFIX)
X+Y**3
_(PRINTNODES N1 'POSTFIX)
XY3**+
12.21