CS 22
Clab 30: The Meta-circular evaluator III
We will continue to follow SICP's design (using abstraction
barriers, etc.) to build a small Scheme evaluator bottom-up.
Last clab we considered multi-frame environments. We
represented an environment as a list of frames and we modified
lookup-variable-value to lookup the value of a variable in
an environment (potentially multi-level) rather than in a simple
frame. We also introduced abstract helping functions:
enclosing-environment, first-frame, extend-environment,
add-binding-to-frame!. We noted that since our driver-loop and
mini-eval used abstraction barriers (like lookup-variable-value),
we could test our multi-level version by making the-global-environment
a multilevel one and running (driver-loop).
Here is the code we had (also at: /home/cfk/pub/cs22/week10/clab29.scm).
;;; make-frame constructs an SICP style frame from a list of
;;; variables (the parameter variables) and a list of values
;;; (the parameter values)
(define (make-frame variables values)
(cons variables values))
;;; given an SICP style frame (the parameter frame),
;;; frame-variables returns a list of the variable names
;;; in the frame
(define (frame-variables frame)
(car frame))
;;; given an SICP style frame (the parameter frame),
;;; frame-values returns a list of the values
;;; in the frame
(define (frame-values frame)
(cdr frame))
;;;returns the value of variable var in frame
;(define (lookup-variable-value var frame)
; (define (scan vars vals)
; (cond ((null? vars)
; (error "Unbound variable" var))
; ((eq? var (car vars))
; (car vals))
; (else (scan (cdr vars) (cdr vals)))))
; (scan (frame-variables frame)
; (frame-values frame)))
(define atom? (lambda (x)
(not (pair? x))))
(define my-sicp-frame
(make-frame '(x cat p) '(17 10 3)))
;;;returns #t if exp is a quote expression; returns #f otherwise
(define (quoted? exp)
(if (atom? exp)
#f
(eq? (car exp) 'quote)))
;;; if exp is a quoted expression, returns its value
(define (text-of-quotation exp) (cadr exp))
;;; returns #t if exp is a simple variable name; returns #f otherwise
(define (variable? exp) (symbol? exp))
;;; a really mini-mini eval. evaluates quoted expressions and
;;; simple variables only in one frame. The expression is
;;; parameter exp and the frame is parameter env.
(define (mini-eval exp env)
(cond
((quoted? exp) (text-of-quotation exp))
((variable? exp) (lookup-variable-value exp env))
(else (error "unknown expression--mini-eval" exp))))
;;; The driver loop reads an expression, evaluates it in the global
;;; environment, and prints the result. Note that the driver
;;; uses a prompt of "MC-EVAL==> " to help you avoid confusing typing to the
;;; metacircular evaluator with typing to the underlying SCHEME interpreter.
;;; (Also, we have called our evaluator procedures MINI-EVAL and MINI-APPLY
;;; to help you avoid confusing them with Scheme's EVAL and APPLY.)
;;; When/If your interaction with the evaluator bombs out in an error,
;;; you should restart it by calling DRIVER-LOOP again.
;;; make the global environment our little frame for now
(define the-global-environment my-sicp-frame)
(define (driver-loop)
(newline)
(display "MC-EVAL==> ")
(let ((input (read)))
(if (equal? input '(quit))
"exiting-mini-eval"
(begin
(display (mini-eval input ;;;;change display to user-print later
the-global-environment))
(driver-loop)))))
(define funframe
(make-frame
'(buchanan nader cat bush gore)
'((name too long) lost 56 maybeb maybeg)))
(define 2levenv (list funframe my-sicp-frame))
(define (enclosing-environment env) (cdr env))
(define (first-frame env) (car env))
(define the-empty-environment '())
(define (extend-environment vars vals base-env)
(if (= (length vars) (length vals))
(cons (make-frame vars vals) base-env)
(if (< (length vars) (length vals))
(error "Too many arguments supplied" vars vals)
(error "Too few arguments supplied" vars vals))))
(define (add-binding-to-frame! var val frame)
(set-car! frame (cons var (car frame)))
(set-cdr! frame (cons val (cdr frame))))
(define newframe (list '()))
(add-binding-to-frame! 'y 2 newframe)
(add-binding-to-frame! 'k 'isover newframe)
(define 3levenv
(extend-environment
(frame-variables newframe)
(frame-values newframe)
2levenv))
(define (lookup-variable-value var env)
(define (env-loop env)
(define (scan vars vals)
(cond ((null? vars)
(env-loop (enclosing-environment env)))
((eq? var (car vars))
(car vals))
(else (scan (cdr vars) (cdr vals)))))
(if (eq? env the-empty-environment)
(error "Unbound variable" var)
(let ((frame (first-frame env)))
(scan (frame-variables frame)
(frame-values frame)))))
(env-loop env))
I moved the construction of my-sicp-frame, funframe, newframe,
2levenv, and 3levenv to the definitions window so that you can use
them without having to retype.
Just to be sure we are all together, let's check this out using our
driver loop with 3levenv as the-global environment.
> (define the-global-environment 3levenv)
> the-global-environment
(((k y) isover 2)
((buchanan nader cat bush gore) (name too long) lost 56 maybeb maybeg)
((x cat p) 17 10 3))
> (driver-loop)
MC-EVAL==> k
isover
MC-EVAL==> y
2
MC-EVAL==> nadar
Unbound variable nadar
> (driver-loop)
MC-EVAL==> nader
lost
MC-EVAL==> cat
56
MC-EVAL==> x
17
MC-EVAL==> cat
56
MC-EVAL==> p
3
Note that the value of cat returned was that in the most
recently added frame of the environment.
We will also want to be able to set! existing variables and
define new variables, so look at set-variable-value! and
define-variable! on pp. 379 and 380 and below.
(define (set-variable-value! var val env)
(define (env-loop env)
(define (scan vars vals)
(cond ((null? vars)
(env-loop (enclosing-environment env)))
((eq? var (car vars))
(set-car! vals val))
(else (scan (cdr vars) (cdr vals)))))
(if (eq? env the-empty-environment)
(error "Unbound variable -- SET!" var)
(let ((frame (first-frame env)))
(scan (frame-variables frame)
(frame-values frame)))))
(env-loop env))
(define (define-variable! var val env)
(let ((frame (first-frame env)))
(define (scan vars vals)
(cond ((null? vars)
(add-binding-to-frame! var val frame))
((eq? var (car vars))
(set-car! vals val))
(else (scan (cdr vars) (cdr vals)))))
(scan (frame-variables frame)
(frame-values frame))))
Note that define-variable! looks very much like our old version
of lookup-variable-value (works in one frame) while
set-variable-value! looks more
like our new version of lookup-variable-value (might look through
several frames in an environment). Make sure you understand
how they work, how they are different, and why they are different.
Then test
them out. For example:
> 3levenv
(((k y) isover 2)
((buchanan nader cat bush gore) (name too long) lost 56 maybeb maybeg)
((x cat p) 17 10 3))
> (set-variable-value! 'y 56 3levenv)
> 3levenv
(((k y) isover 56)
((buchanan nader cat bush gore) (name too long) lost 56 maybeb maybeg)
((x cat p) 17 10 3))
> (set-variable-value! 'nader '2percent 3levenv)
> 3levenv
(((k y) isover 56)
((buchanan nader cat bush gore) (name too long) 2percent 56 maybeb maybeg)
((x cat p) 17 10 3))
> (set-variable-value! 'z 200 3levenv)
Unbound variable -- SET! z
> (define-variable! 'z 200 3levenv)
> 3levenv
(((z k y) 200 isover 56)
((buchanan nader cat bush gore) (name too long) 2percent 56 maybeb maybeg)
((x cat p) 17 10 3))
> (define-variable! 'cat 'meow 3levenv)
> 3levenv
(((cat z k y) meow 200 isover 56)
((buchanan nader cat bush gore) (name too long) 2percent 56 maybeb maybeg)
((x cat p) 17 10 3))
> (define-variable! 'k 9 3levenv)
> 3levenv
(((cat z k y) meow 200 9 56)
((buchanan nader cat bush gore) (name too long) 2percent 56 maybeb maybeg)
((x cat p) 17 10 3))
Note that define-variable! only affects the first (most recent) frame in
an environment, while set-variable-value! can go deeper.
But set-variable-value! must find the variable already existing while
define-variable! will define the variable if it is not already in the
first frame.
Here is our (mini-evaluator so far:
(define (mini-eval exp env)
(cond
((quoted? exp) (text-of-quotation exp))
((variable? exp) (lookup-variable-value exp env))
(else (error "unknown expression--mini-eval" exp))))
It can recognize a quoted expression and a simple variable name;
nothing else.
We now have the tools to handle a user definition. To make our
interpreter handle a user definition, it must recognize an expression
that is a user definition and be able to get at its parts. For
example, if a user types: (define x cat) , we must recognize
- that this is a definition,
- that the variable to be defined is x and
- that the value to assign to x is the value of cat in the current
environment.
If this were the only kind of definition we had to deal
with we could
- recognize that an expression exp is a definition if (car exp) is equal to 'define. We could
- get the variable to be defined by (cadr exp) and we could
- get the value by (mini-eval (caddr exp) env).
Although we will not deal with function definitions today,
we will soon have to deal with definitions that look like
(define (f y) someBody). Here, 2, the identifier to be defined is
(caadr exp).
We will make our selectors general enough to handle either case
although we will work only with the first case (variable definition)
today.
Define, think about, and test: tagged-list?, definition?,
definition-variable, and definition-value.
(define (tagged-list? exp tag)
(if (pair? exp)
(eq? (car exp) tag)
#f))
(define (definition? exp)
(tagged-list? exp 'define))
(define (definition-variable exp)
(if (symbol? (cadr exp))
(cadr exp)
(caadr exp)))
(define (definition-value exp)
(if (symbol? (cadr exp))
(caddr exp)
;;; (make-lambda (cdadr exp) worry about later
;;; (cddr exp))
))
(define def1 '(define x 43))
Let's check them out:
> def1
(define x 43)
> (definition? def1)
#t
> (definition? '(this is a test))
#f
> (definition-variable def1)
x
> (definition-value def1)
43
> (define funct1 '(define (sqr x) (* x x )))
> funct1
(define (sqr x) (* x x))
> (definition? funct1)
#t
> (definition-variable funct1)
sqr
Note that definition-variable works for both (define x ... and (define (f
...
But we'll put off dealing with function definitions until next week.
With these selectors and define-variable!, we can write a procedure that
will carry out a
definition of the first sort, given
an expression that is such a definition. Define and think about:
;;;the parameter exp is assumed to be a valid scheme
;;;definition. the parameter env is a valid environment.
;;;eval-definition evaluates the definition value in
;;;env and effects the definition in the environment
;;;env.
(define (eval-definition exp env) ...
Your definition evaluator should work as follows:
> (define def2 '(define x cat))
> def2
(define x cat)
> 3levenv
(((k y) isover 2)
((buchanan nader cat bush gore) (name too long) lost 56 maybeb maybeg)
((x cat p) 17 10 3))
> (eval-definition def2 3levenv)
ok
> 3levenv
(((x k y) 56 isover 2)
((buchanan nader cat bush gore) (name too long) lost 56 maybeb maybeg)
((x cat p) 17 10 3))
Note that cat was correctly evaluated to 56 in 3levenv and then the variable
x was defined in the first frame of 3levenv.
We will now add user-definitions to our interpreter. To do this, we
just need to modify our evaluator to test whether the input expression
is a definition and if it is a definition to pass it to
eval-definition.
This requires one additional line to our
conditional (shown below). Redefine mini-eval by adding
((definition? exp) (eval-definition exp env))
to the conditional in mini-eval.
Let's test our slightly more powerful interpreter:
> (define the-global-environment 2levenv)
> the-global-environment
(((buchanan nader cat bush gore) (name too long) lost 56 maybeb maybeg)
((x cat p) 17 10 3))
> (driver-loop)
MC-EVAL==> nader
lost
MC-EVAL==> cat
56
MC-EVAL==> x
17
MC-EVAL==> (define y cat)
ok
MC-EVAL==> y
56
MC-EVAL==> (define p (quote 40))
ok
MC-EVAL==> p
40
MC-EVAL==> (quit)
"exiting-mini-eval"
> the-global-environment
(((p y buchanan nader cat bush gore)
40
56
(name too long)
lost
56
maybeb
maybeg)
((x cat p) 17 10 3))
Back in drscheme, we can see that the global environment really changed.
Well, we are making progress. But the only thing that we can use as
values are things which are already defined
(in the-global-environment) or quoted expressions.
We can't even use ordinary numbers. Now, we
could add bindings like ((1 2 3). (1 2 3 )) etc. to
the-global-environment but this gets rather tedious. Furthermore,
later we want
to use Drscheme's underlying numerical operators so we need Drscheme's
numbers. So we'll just steal them. While we are at it, we'll
steal strings also. We can think of these as primitive units
being supplied by the underlying hardware. We will make our evaluator
recognize
numbers and strings as things to be passed through to Drscheme for evaluation
instead of
using our own mini-eval. We'll do this by defining a predicate
self-evaluating? that
returns #t if an expression should just be passed through to Drscheme for
evaluation. Then we will add another case to the cond in mini-eval that will
recognize self-evaluating expressions. If the action taken is just exp,
then exp will be
evaluated by Drscheme's eval. If we want our evaluator to evaluate an
expression,
we invoke one of our own evaluators like: mini-eval, lookup-variable-value , or
eval-definition. So define self-evaluating? and re-define mini-eval as
follows:
(define (self-evaluating? exp)
(cond ((number? exp) #t)
((string? exp) #t)
((eq? exp #t) #t) ;; added to handle #truth
((eq? exp #f) #t) ;; as used in drscheme
(else #f)))
;;; a really mini eval. evaluates quoted expressions, defines,
;;; simple variables, numbers, and strings. The expression is
;;; parameter exp and the frame is parameter env.
(define (mini-eval exp env)
(cond
((self-evaluating? exp) exp)
((quoted? exp) (text-of-quotation exp))
((variable? exp) (lookup-variable-value exp env))
((definition? exp) (eval-definition exp env))
(else (error "unknown expression--mini-eval" exp))))
Now we can do a quick test:
> the-global-environment
(((buchanan nader cat bush gore) (name too long) lost 56 maybeb maybeg) ((x cat p) 17 10 3))
> (driver-loop)
MC-EVAL==> x
17
MC-EVAL==> (define y 3)
ok
MC-EVAL==> y
3
MC-EVAL==> (quit)
"exiting-mini-eval"
>
A more interesting test is to start with an empty environment:
> (define the-global-environment (list (list '())))
> the-global-environment
((()))
> (driver-loop)
MC-EVAL==> (define x 17)
ok
MC-EVAL==> (define cat 2000)
ok
MC-EVAL==> x
17
MC-EVAL==> cat
2000
MC-EVAL==> (define alis (quote (dog horse 4)))
ok
MC-EVAL==> alis
(dog horse 4)
MC-EVAL==> (quit)
"exiting-mini-eval"
> the-global-environment
(((alis cat x) (dog horse 4) 2000 17))
So we can now start our interpreter with an environment that consists of
one empty
frame and the user can put stuff into it and get stuff out of it.
It is now time for you to test and exercise your understanding of all this
by getting started on your homework.
Exercise: Give our interpreter the ability to handle set!.
This is similar to handling define but slightly different. (define x
exp) will evaluate
exp in the current environment to get some value (call it val). If x is
defined in the
first frame of the current environment then define will change the
definition of x to
val. If x is not defined in the first frame of the current environment
then define
will add a binding of x to val in the first frame.
Contrast the above with the following: (set! x exp) will evaluate exp in
the current
environment to get some value (call it val). If x is defined in any frame
of the
current environment then set! will change the definition of x to val in the
first
frame where x is defined. If x is not defined in the current environment
our set!
will generate an error.