SICP 4.1.4 Running the Evaluator as a Program
2024-07-02 Tue (updated on 2025-06-29 Sun)
The evaluator provides a description of the process to evaluate a Lisp
expression. Such a description is in a programming language (Lisp),
so we can run it. Given that the evaluator ``reduces expressions
ultimately to the application of primitive procedures'', we need a way
to use the underlying Lisp system to apply those procedures. The
global environment, consequently, shall contain bindings for each
primitive procedure name. The global environment shall also contain
bindings for true
and false
.
(define (setup-environment) (let ((initial-env (extend-environment (primitive-procedure-names) (primitive-procedure-objects) the-empty-environment))) (define-variable! 'true true initial-env) (define-variable! 'false false initial-env) initial-env)) (define the-global-environment (setup-environment))
Primitive procedure are represented as tagged lists. This specific
representation is hidden behind the primitive-procedure?
and
primitive-implementation
abstractions so it could be replaced by a
different one.
(define (primitive-procedure? proc) (tagged-list? proc 'primitive)) (define (primitive-implementation proc) (cadr proc))
(define primitive-procedures (list (list 'car car) (list 'cdr cdr) (list 'cons cons) (list 'null? null?) <MORE PRIMITIVES> )) (define (primitive-procedure-names) (map car primitive-procedures)) (define (primitive-procedure-objects) (map (lambda (proc) (list 'primitive (cadr proc))) primitive-procedures))
Here is how we apply a primitive
procedure. Apply-in-underlying-scheme
is nothing more than the
apply
procedures from the underlying scheme we have used in the
previous chapters.
(define (apply-primitive-procedure proc args) (apply-in-underlying-scheme (primitive-implementation proc) args))
Authors provide what they call a ``driver loop'' as a convenience for running the evaluator, which ``models the read-eval-print loop of the underlying Lisp system.''
(define input-prompt ";;; M-Eval input:") (define output-prompt ";;; M-Eval value:") (define (driver-loop) (prompt-for-input input-prompt) (let ((input (read))) (let ((output (eval input the-global-environment))) (announce-output output-prompt) (user-print output))) (driver-loop)) (define (prompt-for-input string) (newline) (newline) (display string) (newline)) (define (announce-output string) (newline) (display string) (newline))
User-print
is used to avoid printing the the environment part of a
compound procedure:
(define (user-print object) (if (compound-procedure? object) (display (list 'compound-procedure (procedure-parameters object) (procedure-body object) '<procedure-env>)) (display object)))
Finally:
(define the-global-environment (setup-environment)) (driver-loop) ;;; M-Eval input: (define (append x y) (if (null? x) y (cons (car x) (append (cdr x) y)))) ;;; M-Eval value: ok ;;; M-Eval input: (append '(a b c) '(d e f)) ;;; M-Eval value: (a b c d e f)
What follows is the whole code for the evaluator presented so far. I've evaluated it in DrRacket and I can confirm that it works.
Notice: I've renamed apply
into apply-evaluator
. Racket would
otherwise complain about the first definition ((define
apply-in-underlying-scheme apply)
), telling me that I cannot use
apply
before defining it.
#lang sicp (define apply-in-underlying-scheme apply) (define (eval exp env) (cond ((self-evaluating? exp) exp) ((variable? exp) (lookup-variable-value exp env)) ((quoted? exp) (text-of-quotation exp)) ((assignment? exp) (eval-assignment exp env)) ((definition? exp) (eval-definition exp env)) ((if? exp) (eval-if exp env)) ((lambda? exp) (make-procedure (lambda-parameters exp) (lambda-body exp) env)) ((begin? exp) (eval-sequence (begin-actions exp) env)) ((cond? exp) (eval (cond->if exp) env)) ((application? exp) (apply-evaluator (eval (operator exp) env) (list-of-values (operands exp) env))) (else (error "Unknown expression type -- EVAL" exp)))) (define (apply-evaluator procedure arguments) (cond ((primitive-procedure? procedure) (apply-primitive-procedure procedure arguments)) ((compound-procedure? procedure) (eval-sequence (procedure-body procedure) (extend-environment (procedure-parameters procedure) arguments (procedure-environment procedure)))) (else (error "Unknown procedure type -- APPLY" procedure)))) (define (list-of-values exps env) (if (no-operands? exps) '() (cons (eval (first-operand exps) env) (list-of-values (rest-operands exps) env)))) (define (eval-if exp env) (if (true? (eval (if-predicate exp) env)) (eval (if-consequent exp) env) (eval (if-alternative exp) env))) (define (eval-sequence exps env) (cond ((last-exp? exps) (eval (first-exp exps) env)) (else (eval (first-exp exps) env) (eval-sequence (rest-exps exps) env)))) (define (eval-assignment exp env) (set-variable-value! (assignment-variable exp) (eval (assignment-value exp) env) env) 'ok) (define (eval-definition exp env) (define-variable! (definition-variable exp) (eval (definition-value exp) env) env) 'ok) (define (self-evaluating? exp) (cond ((number? exp) true) ((string? exp) true) (else false))) (define (variable? exp) (symbol? exp)) (define (quoted? exp) (tagged-list? exp 'quote)) (define (text-of-quotation exp) (cadr exp)) (define (tagged-list? exp tag) (if (pair? exp) (eq? (car exp) tag) false)) (define (assignment? exp) (tagged-list? exp 'set!)) (define (assignment-variable exp) (cadr exp)) (define (assignment-value exp) (caddr exp)) (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) ; formal parameters (cddr exp)))) ; body (define (lambda? exp) (tagged-list? exp 'lambda)) (define (lambda-parameters exp) (cadr exp)) (define (lambda-body exp) (cddr exp)) (define (make-lambda parameters body) (cons 'lambda (cons parameters body))) (define (if? exp) (tagged-list? exp 'if)) (define (if-predicate exp) (cadr exp)) (define (if-consequent exp) (caddr exp)) (define (if-alternative exp) (if (not (null? (cdddr exp))) (cadddr exp) 'false)) (define (make-if predicate consequent alternative) (list 'if predicate consequent alternative)) (define (begin? exp) (tagged-list? exp 'begin)) (define (begin-actions exp) (cdr exp)) (define (last-exp? seq) (null? (cdr seq))) (define (first-exp seq) (car seq)) (define (rest-exps seq) (cdr seq)) (define (sequence->exp seq) (cond ((null? seq) seq) ((last-exp? seq) (first-exp seq)) (else (make-begin seq)))) (define (make-begin seq) (cons 'begin seq)) (define (application? exp) (pair? exp)) (define (operator exp) (car exp)) (define (operands exp) (cdr exp)) (define (no-operands? ops) (null? ops)) (define (first-operand ops) (car ops)) (define (rest-operands ops) (cdr ops)) (define (cond? exp) (tagged-list? exp 'cond)) (define (cond-clauses exp) (cdr exp)) (define (cond-else-clause? clause) (eq? (cond-predicate clause) 'else)) (define (cond-predicate clause) (car clause)) (define (cond-actions clause) (cdr clause)) (define (cond->if exp) (expand-clauses (cond-clauses exp))) (define (expand-clauses clauses) (if (null? clauses) 'false ; no `else' clause (let ((first (car clauses)) (rest (cdr clauses))) (if (cond-else-clause? first) (if (null? rest) (sequence->exp (cond-actions first)) (error "ELSE clause isn't last -- COND->IF" clauses)) (make-if (cond-predicate first) (sequence->exp (cond-actions first)) (expand-clauses rest)))))) (define (true? x) (not (eq? x false))) (define (false? x) (eq? x false)) (define (make-procedure parameters body env) (list 'procedure parameters body env)) (define (compound-procedure? p) (tagged-list? p 'procedure)) (define (procedure-parameters p) (cadr p)) (define (procedure-body p) (caddr p)) (define (procedure-environment p) (cadddr p)) (define (enclosing-environment env) (cdr env)) (define (first-frame env) (car env)) (define the-empty-environment '()) (define (make-frame variables values) (cons variables values)) (define (frame-variables frame) (car frame)) (define (frame-values frame) (cdr frame)) (define (add-binding-to-frame! var val frame) (set-car! frame (cons var (car frame))) (set-cdr! frame (cons val (cdr frame)))) (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 (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)) (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)))) (define primitive-procedures (list (list 'car car) (list 'cdr cdr) (list 'cons cons) (list 'null? null?) (list '+ +) (list '- -) (list '* *) (list '/ /))) (define (primitive-procedure-names) (map car primitive-procedures)) (define (primitive-procedure-objects) (map (lambda (proc) (list 'primitive (cadr proc))) primitive-procedures)) (define (setup-environment) (let ((initial-env (extend-environment (primitive-procedure-names) (primitive-procedure-objects) the-empty-environment))) (define-variable! 'true true initial-env) (define-variable! 'false false initial-env) initial-env)) (define (primitive-procedure? proc) (tagged-list? proc 'primitive)) (define (primitive-implementation proc) (cadr proc)) (define (apply-primitive-procedure proc args) (apply-in-underlying-scheme (primitive-implementation proc) args)) (define input-prompt ";;; M-Eval input:") (define output-prompt ";;; M-Eval value:") (define (driver-loop) (prompt-for-input input-prompt) (let ((input (read))) (let ((output (eval input the-global-environment))) (announce-output output-prompt) (user-print output))) (driver-loop)) (define (prompt-for-input string) (newline) (newline) (display string) (newline)) (define (announce-output string) (newline) (display string) (newline)) (define (user-print object) (if (compound-procedure? object) (display (list 'compound-procedure (procedure-parameters object) (procedure-body object) '<procedure-env>)) (display object))) (define the-global-environment (setup-environment)) (driver-loop)
A summary of how the evaluator works
Here is a summary of how the evaluator works.
The evaluator works is a repl fashion. The repl is started by calling
driver-loop
. Driver-loop
waits for the user input, evaluates it,
prints the result, and waits again.
The evaluation process is performed by eval
in the global
environment, which contains one frame containing the primitive
bindings.
For an expression to be executed in an environment it means that variable resolution — finding the value of a variable — is performed by looking at the last-created frame and, if not found, in the following frame, etc.
The global environment is a list that contains a list (which represents the uniquely present frame):
GE: [ GE-frame ]
Any other environment will be a list which contains more than one list, and whose last element is the frame uniquely contained by the global environment:
nonGE: [ frameA frameB ... GE-frame ]
That is the way Author chose to implement what we saw in Chapter 3: the environment model of evaluation.
For example,
(define (foo a b) (lambda (c d) 1)) (define my-fun (foo 1 1)) (my-fun 00)
will lead (at some point in the evaluation) to the existence of three
frames — the frame created by applying my-fun
, the frame created
by applying foo
, and the global environment's frame.
Procedure objects are represented as compound objects which contain i) a list of parameters, ii) the sequence of statements that constitute their body, and iii) a pointer to an environment.
This latter environment1 is used as a base to create the environment in which the statements of the body are evaluated.
Eval
is the procedure that evaluates an expression. It distinguishes
ten cases:
- Self-evaluating expressions. E.g.:
1
. The value of self-evaluating expression is the expression itself. - Variables. E.g.:
foo
. The value of a variable has to be looked for in the relevant environment. - Quotations. E.g.:
'hola
. The value of a quoted expression is the quoted text. - Assignments. E.g.:
(set! foo 5)
. Evaluating an assignment consists in finding the variable name in the environment and set the value bound to it. - Definitions. E.g.:
(define foo 5)
. The evaluation of a definition consists in creating a new variable in the environment and setting its value. If a variable with that name already exists, then we just set its value. - Ifs. E.g.:
(if foo bar)
. We evaluate the predicate. If true we evaluate the then-expression, otherwise we evaluate the else-expression. - Lambdas. E.g.:
(lambda (a) a)
. The evaluation of a lambda creates a procedure object. - Begins. E.g.:
(begin foo bar baz)
. The evaluation of abegin
consists in the ordered evaluation of a sequence of expressions. We just iterate over the expressions and applyeval
to each. - Conds. E.g.:
(cond ((pred1 (do-something))) (else (do-something-else)))
. Conds are derived expressions: we can just translate them into equivalent if expressions and evaluate those. - Procedure applications. E.g.:
(add 1 2)
. If the procedure is primitive, then we apply it to the arguments using the underlying scheme'sapply
. If the procedure is not primitive, then its application requires i) evaluating the operator, ii) evaluting the operands, iii) creating a new environment the way described above, iv) evaluating the sequence of expressions in the body within the newly created environment.
Exercise 4.14
Exercise:
Eva Lu Ator and Louis Reasoner are each experimenting with the metacircular evaluator. Eva types in the definition of
map
, and runs some test programs that use it. They work fine. Louis, in contrast, has installed the system version ofmap
as a primitive for the metacircular evaluator. When he tries it, things go terribly wrong. Explain why Louis'smap
fails even though Eva's works.
Answer:
Apply
is called in both scenarios. The arguments
that apply
receives are the already-evaluated operands of the combination
(evaluated by list-of-values
). I think that the problem with using
the primitive map
is that one the arguments received by apply
is
an entity representing a procedure using our implementation (our
implementation is a list whose car
is the tag 'procedure
, among
other aspects). But the primitive car
expects a different kind of
procedure object (whatever Scheme natively uses).
Footnotes:
This is the environment in which the procedure object was created.