\input ../6001mac
\newcommand{\Code}[1]{\mbox{\tt #1}}
\begin{document}
\psetheader{Sample Problem Set}{Evaluators}
\begin{center}\large
{\bf Scheme Evaluators}
\end{center}
\bigskip
You will be working with evaluators as described in Chapter 4 of the
notes. If you don't have a good understanding of how the evaluators are
structured, it is very easy to become confused between the programs that
the evaluator is interpreting, the procedures that implement the evaluator
itself, and Scheme procedures called by the evaluator. You will need to
have carefully studied Chapter 4 through subsection 4.2.2 in order to do
this assignment.
To help you focus on some of the important ideas in the Notes, we ask you
to prepare:
\section{Using the Evaluators}
The code for this problem set includes these files:
\begin{itemize}
\item {\tt meval.scm} is the metacircular evaluator described in
section 4.1.1 of the notes. It has been extended to allow declarations of
lazy procedure parameters, as described in section 4.2 and exercise 4.25.
In order to avoid confusing the {\tt eval} and {\tt apply} of this
evaluator with the {\tt eval} and {\tt apply} of the underlying Scheme
system, we have renamed these procedures {\tt meval} and {\tt m-apply}.
\item {\tt analyze.scm} is the {\tt analyze} evaluator of section 4.1.6,
extended to allow declarations of lazy procedure parameters, as described
in section 4.2 and exercise 4.25.
\item {\tt evdata.scm} contains the procedures that define the evaluator's
data structures, as in section 4.1.3. In order to interface the evaluator
to the underlying Scheme system, we have made the modification outlined in
exercise 4.14. Namely, any variable that is not found in the evaluator's
global environment will be looked up in the underlying Scheme; Scheme
procedures found in this way will treated by the evaluator as primitive
procedures. Programs being executed by the metacircular evaluator can
therefore make use of any procedure (primitive or compound) which may be
defined in your Scheme environment.
\item {\tt syntax.scm} contains the procedures that define the syntax of
expressions, as described in section 4.1.2.
\end{itemize}
You'll be switching back and forth between both of these evaluators. The
two evaluators use the same implementation of environment data structures.
This arrangement requires that you reinitialize the evaluator's environment
when you switch from one to the other. Here's how to manage this:
\begin{itemize}
\item
To start up, load the files {\tt syntax.scm}, {\tt evdata.scm},
{\tt meval.scm}, and {\tt analyze.scm}. Then evaluate
\Code{(start-meval)} in Scheme; this starts the read-eval-print loop
for the meta-circular evaluator with a freshly initialized global
environment.
To evaluate an expression, you may type the expression into the
\Code{*scheme*} buffer followed by {\tt ctrl-x ctrl-e}. (Don't use
{\tt M-z} to evaluate the expression in the \Code{*scheme*}
buffer---the presence of the evaluator prompts confuses the {\tt M-z}
mechanism. But {\tt M-z} works fine for sending expressions to the
\Code{*scheme*} buffer from other Scheme-mode buffers.)
We've arranged that each read-eval-print loop identifies itself by its
input and output prompts. For example,
\beginlisp
MEVAL=> (+ 3 4)
;;M-value: 7
\endlisp
shows an interaction with the {\tt meval} evaluator.
\item
You should keep in a separate file any procedure definitions you want to
install in an evaluator. If your Edwin Scheme buffer is running the
read-eval-print loop of an evaluator, you can then visit this definitions
file and type {\tt M-o} to enter the definitions into the evaluator. To
get you started, we have included an alternative implementation of lazy
lists in the buffer {\tt ps8defs.scm}.
\item You can interrupt an evaluator by typing {\tt ctrl-c ctrl-c}.
To restart it with the recent definitions still intact, evaluate {\tt
(eval-loop)}.
\item To start a read-eval-print loop for the analyzing evaluator in a
fresh global environment, return to Scheme and evaluate {\tt
(start-analyze)}.
\item The evaluators you are working with do not include error
systems. If you hit an error you will bounce back into ordinary
Scheme. You can restart the current evaluator, with its global
environment still intact, by evaluating {\tt (eval-loop)}.
\item The variable {\tt current-evaluator} is defined as an alias for
the current evaluator in each of the files {\tt meval.scm} and {\tt
analyze.scm}. This should ensure that {\tt current-evaluator} will be
bound to the most recently started evaluator at all times during your
lab session.
\item It can be instructive to trace {\tt current-evaluator}, {\tt
m-apply}, and/or {\tt exapply} during evaluator executions. (You will
also probably need to do this while debugging your code for this
assignment.)
\item Since environments are generally complex, circular list
structures, we have set Scheme's printer so that it will not go into
an infinite loop when asked to print a circular list. This was done by
\beginlisp
(set! *unparser-list-depth-limit* 7)
(set! *unparser-list-breadth-limit* 10)
\endlisp
at the end of the file {\tt evdata.scm}. You may want to alter the
values of these limits to vary how much list structure will be printed as
output.
\end{itemize}
\section{Exercises}
\paragraph{Lab exercise 1A:}
Start the \Code{meval} evaluator and evaluate a few simple expressions
and definitions. It's a good idea to make an intentional error and
practice restarting the read-eval-print loop. Notice that we have
already installed {\tt let} in the evaluator, so you can use this in
your programs.
\paragraph{Lab exercise 1B:}
Now start the analyzing evaluator. You can tell that you are typing
at the analyzing evaluator because the prompt will be {\tt AEVAL=>}
rather than {\tt MEVAL=>}. Now once again evaluate a few simple
expressions and definitions.
\paragraph{Lab exercise 1C:}
Start the meta-circular evaluator again. Now trace evaluation of the
application of some simple procedure to some argument.
\medskip
Ben Bitdiddle thinks it is silly to reinitialize the environment every
time he switches from one evaluator to the other. So, after having
run the meta-circular evaluator, he revises the \Code{start-analyze}
procedure so it does not reinitialize the global environment:
\beginlisp
(define (start-analyze)
(set! current-evaluator (lambda (exp env) ((analyze exp) env)))
(set! current-prompt "AEVAL=> ")
(set! current-value-label ";;A-value: ")
; (init-env) ;Ben comments-out this line
(eval-loop))
\endlisp
He reasons that now, if he types \Code{(start-analyze)}, the
definitions he already evaluated in the meta-circular evaluator will
be available to the analyzing evaluator. Eva Lu Ator agrees that the
definitions of the meta-circular evaluator will indeed be available to
the analyzing evaluator, but that this will usually crash the system
rather then being helpful.
\paragraph{Lab exercise 2A:}
Try Ben's suggestion and observe what Eva Lu Ator meant.
\paragraph{Post-Lab exercise 2B:}
Briefly, but clearly, explain what goes wrong with Ben's suggestion.
Are there any definitions it would be safe to preserve when switching
between evaluators?
\subsection{Comparing evaluation speeds}
This next problem asks you to demonstrate the improved efficiency of
the analyzing evaluator over the meta-circular one.
To time things, you can use the procedure {\tt show-time}, which you used
in Problem Set 2. Thus, for example, you can find out how long it takes
the evaluator to evaluate {\tt (fib 10)} by quitting out of the evaluator
and evaluating
\beginlisp
(show-time (lambda() (current-evaluator '(fib 10) the-global-environment)))
\endlisp
in Scheme. Be careful to define the procedure you are timing, e.g., {\tt
fib}, in the evaluator, not in ordinary Scheme! Otherwise, you'll end up
timing the underlying Scheme interpreter. This is because of the way
we've linked the evaluator into Scheme: if you define {\tt fib} in Scheme,
and not in the evaluator's global environment, {\tt lookup-variable-value}
will find the Scheme procedure {\tt fib} and {\tt m-apply} will treat this
as a primitive.
\paragraph{Lab exercise 3A:} Design and carry out an experiment to
compare the speeds of the {\tt meval} and {\tt analyze} evaluators on some
procedures. Use tests that run for a reasonable amount of time (say 10 or
20 seconds). It may be helpful to use test procedures for which small
changes in the input cause large changes in running time, so you can
rapidly increase the time by increasing the input.
Summarize what you observe about the relative speeds of the two evaluators
on your test programs.
\paragraph{Lab exercise 3B (Optional---Extra Credit):}
See if you can find an example where the interpreters run for at least
five seconds and the {\tt analyze} evaluator runs {\em no more} than 1.5
times the speed of {\tt meval}.
\subsection{Adding Language Constructs}
\paragraph{Pre-Lab exercise 4A:}
Lazy parameter declarations are already supported by both the \Code{meval}
and \Code{analyze} evaluators supplied to you. Suppose we are given some
{\em fixed} $n \geq 0$. With lazy parameters, it is possible to define
within an evaluator an $n$-argument {\em procedure}, {\tt and-proc}, that
yields the same results as the Scheme special form \Code{and} when it is
used with $n$ subforms. That is, \Code{(and $E_1$ \ldots $E_n$)} in
Scheme, and
\Code{(and-proc $E_1$ \ldots $E_n$)} in an evaluator, should produce exactly the
same results (including side-effects) for any expressions $E_1 \ldots
E_n$. Outline how to define the $n$-argument \Code{and-proc} within our
evaluators.
\paragraph{Lab exercise 4B:}
Evaluate the definition of a three-argument {\tt and-proc} in both the
\Code{meval} and \Code{analyze} evaluators.
\paragraph{Lab exercise 5:}
Since our evaluators do not yet have the ability to define procedures with
varying numbers of arguments, we can't add the general {\tt and} construct
with the approach of Exercise 4. So instead, add {\tt and} as a special
form to one of the evaluators (you may choose either one, but see exercise
7A below before you make the choice).
\medskip
We can also extend the evaluators to handle Scheme's convention
for defining procedures with varying numbers of arguments. For
example, an abstraction of the form \Code{(lambda (x y . z) \ldots)}
defines a procedure of two or more arguments, in which the value of
the first argument would be bound to \Code{x}, the value of the second
to \Code{y}, and a list consisting of the values of any further
arguments would be bound to \Code{z}. If, instead of \Code{z}, we had
\Code{(w lazy)}, then a list consisting of the {\em delayed} values of
any further arguments would be bound to \Code{w}. Similarly, if \Code{(w
lazy-memo)} appears instead of z, this indicates that a list consisting of
the memoized delayed values of any further arguments would be bound to
\Code{w}.
We accomplish the extension by modifying the procedure
\Code{process-arg-procs} in the file {\tt evdata.scm}\footnote{The
procedure \Code{parameter-names} in {\tt syntax.scm} must also be written
to handle the dotted-pair form of parameters, but we have done this
already.}. As written, \Code{(process-arg-procs params aprocs env)}
expects \Code{params} and \Code{aprocs} to be {\em lists} of equal length,
and it returns a list of parameter values of the same length. If
\Code{params} is not a list of parameter declarations, but instead ends
with a dotted pair of parameters, then we want \Code{process-arg-procs} to
return a list of values in which the last value is itself a list of all
the "extra" values, possibly all delayed or all memoized depending on
whether the final parameter is declared to be lazy or memoized. For
example,
\beginlisp
((lambda (x y . (w lazy)) (list x y (car w)))
1 2 (if \#t 3 4) (print "this should not print"))
\endlisp
will return \Code{(1 2 3)}, without printing anything. This is because in
the application, the variable \Code{x} will be bound to 1, variable
\Code{y} will be bound to 2, and variable \Code{w} will be bound to the
list consisting of the delayed values of
\beginlisp(if \#t 3 4)\endlisp
and
\beginlisp(print "this should not print").\endlisp
The first delayed value gets forced when the primitive procedure
\Code{list} is applied to it. The second delayed value is not ever
forced, so no printing occurs.
So we revise \Code{process-arg-procs} to reformat its arguments if
necessary, after which it proceeds as before:
\beginlisp
(define (process-arg-procs params aprocs env)
(let ((params (undot params))
(aprocs (matchup-args params aprocs)))
(map (lambda (param aproc)
(cond ((variable? param) (aproc env))
((lazy? param) (delay-it aproc env))
((memo? param) (delay-it-memo aproc env))
(else (error "Unknown declaration" param))))
params
aprocs)))
\endlisp
The procedure \Code{matchup-args} has the task of taking the list of
arguments and returning the list with all the "extra" arguments put
into a single list at the end. But at this point each argument in the
input list is actually packaged as an "argument procedure." The
argument procedure will yield the argument value when it is applied to
the environment. So \Code{matchup-args} actually has to create a
similar package which yields the list of extra argument values when it
is applied to the environment.
\beginlisp
(define (matchup-args params aprocs)
(cond
((null? params)
(if (null? aprocs) '() (error "matchup-args: too many args" aprocs)))
((variable? params)
(list
(lambda (env)
(map (lambda (aproc) (aproc env)) aprocs))))
((lazy? params)
(list
(lambda (env)
(map (lambda (aproc) (delay-it aproc env)) aprocs))))
((memo? params)
(list
(lambda (env)
(map (lambda (aproc) (delay-it-memo aproc env)) aprocs))))
((null? aprocs) (error "matchup-args: too few args" params))
(else (cons
(car aprocs)
(matchup-args (cdr params) (cdr aprocs))))))
\endlisp
\medskip
\beginlisp
(define (undot params)
)
\endlisp
\paragraph{Lab exercise 6A:}
Complete the definition of \Code{}, insert these definitions in
the file {\tt evdata.scm} and reload the file into Scheme. You will find
a copy of the above definitions in buffer \Code{ps8-ans.scm}.
\paragraph{Lab exercise 6B:}
Now we can add, as a procedure, a genuine {\tt and} construct to the
evaluators. Namely,
\beginlisp
(define and (lambda (l lazy) (and-lproc l)))
\endlisp
where {\tt and-lproc} is a procedure that takes as its argument a single
list\footnote{Both \Code{lazy} and \Code{lazy-memo} are now specified to
be keywords. This prevents misreading \Code{(lambda (l lazy) \ldots)} as
a procedure with two formals, \Code{l} and \Code{lazy}. Rather, it is a
procedure which may be applied to zero or more arguments---all of which
will be delayed.} of delayed values. Give the definition of {\tt
and-lproc}.
\medskip
Even with lazy and varying numbers of parameters, not all language
facilities can be defined as Scheme procedures. An example is the
special form {\tt (for-list )} where \Code{} is a
variable and the value of {\tt } is a list. Evaluation of this
expression causes \Code{} to be repeatedly evaluated as {\tt
} is successively assigned to be the elements of the value of
{\tt }. More precisely, {\tt (for-list )} is
syntactic sugar for\footnote{We're assuming that the local variables
\Code{iter} and \Code{rest-l} are not \Code{eq?} to \Code{} or
any variable free in \Code{}.}
\beginlisp
(let (rest-l ))
(define (iter)
(if (null? rest-l)
the-unspecified-value
(begin
(set! (car rest-l))
(set! rest-l (cdr rest-l))
(iter))))
(iter))
\endlisp
\paragraph{Lab exercise 7A:}
Add the {\tt for-list} construct as a special form ({\em not} as syntactic
sugar like {\tt let} or {\tt cond}) to whichever evaluator you did {\em not}
choose in exercise 4.
\paragraph{Lab exercise 7B:}
Exhibit a \Code{} for which {\tt (for-list x '(1 2) )}
does not have the same behavior as {\tt (map (lambda (x) >body>) '(1
2))}.
\paragraph{Lab and Post-Lab exercise 8 (Optional):}
The evaluators are not quite powerful enough to evaluate themselves. That
is, it doesn't quite work to evaluate all the \Code{meval} definition
files within the \Code{MEVAL} read-eval-print loop, and then to apply
\Code{meval} as a procedure defined {\em within} the \Code{MEVAL}
evaluator. You may want to try this to see what happens.
Discuss what further features or definitions would be required in order to
successfully run the evaluators within themselves and/or within each other.
\iffalse
\paragraph{Lab and Post-Lab exercise 9 (Optional):}
Ben Bitdiddle's idea from exercise 2 of having both evaluators share
the same environment---so that definitions evaluated in either evaluator
would be available and run correctly in the other---can be made to work.
It requires only a slight modification in the way that the \Code{meval}
evaluator represents procedure objects. The modification is already
suggested in the \Code{value-procs} procedure in {\tt meval.scm}, which
enabled the two evaluators to use the same parameter declaration and
environment structures. Explain more fully how to make Ben's idea work.
Do it if you have time.
\fi
\end{document}