Program | ::= |
Expression | a-program (exp1) |
Expression | ::= |
Number | const-exp (num) |
::= |
-( Expression
, Expression) |
diff-exp (exp1 exp2) |
|
::= |
zero? ( Expression) |
zero?-exp (exp1) |
|
::= |
if Expression
then Expression
else Expression |
if-exp (exp1 exp2 exp3) |
|
::= |
Identifier | var-exp (var) |
|
::= |
let Identifier
= Expression
in Expression |
let-exp (var exp1 body) |
For example,
(scan&parse "let x = 4 in -(x,-(1,x))")
evaluates to the abstract syntax tree that is the result of
(a-program (let-exp 'x (const-exp 4) (diff-exp (var-exp 'x) (diff-exp (const-exp 1) (var-exp 'x)))))
For any programming language, the expressed values are the possible values of an expression, and the denoted values are the values to which a variable can be bound in some environment.
For LET, the expressed and denoted values happen to be the same:
ExpVal = Int + Bool
DenVal = Int + Bool
The expressed and denoted values will be abstract data types with this algebraic specification:
num-val
: Int → ExpVal
bool-val
: Bool → ExpVal
expval->num
: ExpVal → Int
expval->bool
: ExpVal → Bool
(expval->num (num-val
n))
= n
(expval->bool (bool-val
b))
= b
We use the following abbreviations:
ρ ranges over environments
[] denotes the empty environment
[var = val]ρ denotes(extend-env
varval
ρ
)
[var = val] denotes [var = val][]
const-exp
: Int → Exp
zero?-exp
: Exp → Exp
if-exp
: Exp × Exp × Exp → Exp
diff-exp
: Exp × Exp → Exp
var-exp
: Symbol → Exp
let-exp
: Symbol × Exp × Exp → Exp
value-of
: Exp × Env → ExpVal
(value-of (const-exp
n)
ρ)
=(num-val
n)
(value-of (var-exp
var)
ρ)
=(apply-env
ρvar
)
(value-of (diff-exp
exp1exp2
)
ρ)
=(- (expval->num (value-of
exp1ρ
)) (expval->num (value-of
exp2ρ
)))
For LET, specifying the behavior of programs amounts to specifying the initial environment. For most programming languages, the initial environment consists of a standard set of predefined libraries that every implementation of the language is supposed to provide. For LET, we'll mimic that by providing three predefined identifiers.
(value-of-program
exp)
=(value-of
expρ0
)
where
ρ0 = [i=1,v=5,x=10
]
(value-of
exp1ρ
)
= val1
(expval->num
val1)
= 0
--------------------------------------------
(value-of (zero?-exp
exp1)
ρ)
=(bool-val #t)
(value-of
exp1ρ
)
= val1
(expval->num
val1)
= n
n ≠ 0
--------------------------------------------
(value-of (zero?-exp
exp1)
ρ)
=(bool-val #f)
(value-of
exp1ρ
)
= val1
(expval->bool
val1)
=#t
----------------------------------------------------
(value-of (if-exp
exp1exp2
exp3
)
ρ)
=(value-of
exp2ρ
)
(value-of
exp1ρ
)
= val1
(expval->bool
val1)
=#f
----------------------------------------------------
(value-of (if-exp
exp1exp2
exp3
)
ρ)
=(value-of
exp3ρ
)
let
(value-of
exp1ρ
)
= val1
------------------------------------
(value-of (let-exp
varexp1
body
)
ρ)
=(value-of
body[var=val1]ρ
)
LET
The tokens of LET are specified by an SLLgen lexical specification:
(define the-lexical-spec '((whitespace (whitespace) skip) (comment ("%" (arbno (not #\newline))) skip) (identifier (letter (arbno (or letter digit "_" "-" "?"))) symbol) (number (digit (arbno digit)) number) (number ("-" digit (arbno digit)) number) ))
The context-free syntax of LET is specified by an SLLgen grammar:
(define the-grammar '((program (expression) a-program) (expression (number) const-exp) (expression ("-" "(" expression "," expression ")") diff-exp) (expression ("zero?" "(" expression ")") zero?-exp) (expression ("if" expression "then" expression "else" expression) if-exp) (expression (identifier) var-exp) (expression ("let" identifier "=" expression "in" expression) let-exp) ))
From those specifications, SLLgen generates these definitions:
(define-datatype program program? (a-program (a-program13 expression?))) (define-datatype expression expression? (const-exp (const-exp14 number?)) (diff-exp (diff-exp15 expression?) (diff-exp16 expression?)) (zero?-exp (zero?-exp17 expression?)) (if-exp (if-exp18 expression?) (if-exp19 expression?) (if-exp20 expression?)) (var-exp (var-exp21 symbol?)) (let-exp (let-exp22 symbol?) (let-exp23 expression?) (let-exp24 expression?)))
Figure 3.6 of our textbook was obtained from those definitions by renaming components.
The data type of expressed values is implemented by:
;;; an expressed value is either a number, a boolean or a procval. (define-datatype expval expval? (num-val (value number?)) (bool-val (boolean boolean?))) ;;; extractors: (define expval->num (lambda (v) (cases expval v (num-val (num) num) (else (expval-extractor-error 'num v))))) (define expval->bool (lambda (v) (cases expval v (bool-val (bool) bool) (else (expval-extractor-error 'bool v))))) (define expval-extractor-error (lambda (variant value) (eopl:error 'expval-extractors "Looking for a ~s, found ~s" variant value)))
Since environments are an abstract data type, our interpreters will be independent of their representation and implementation, so we can use any implementation of environments.
We'll need to define an initial environment, however:
;; init-env : -> Env ;; (init-env) builds an environment in which i is bound to the ;; expressed value 1, v is bound to the expressed value 5, and x is ;; bound to the expressed value 10. (define init-env (lambda () (extend-env 'i (num-val 1) (extend-env 'v (num-val 5) (extend-env 'x (num-val 10) (empty-env))))))
Now we implement a help procedure that makes it easy to run LET programs:
;; run : String -> ExpVal (define run (lambda (string) (value-of-program (scan>parse string))))
Finally, we implement our interpreter for LET:
;; value-of-program : Program -> ExpVal (define value-of-program (lambda (pgm) (cases program pgm (a-program (body) (value-of body (init-env)))))) ;; value-of : Exp * Env -> ExpVal (define value-of (lambda (exp env) (cases expression exp (const-exp (num) (num-val num)) (var-exp (id) (apply-env env id)) (diff-exp (exp1 exp2) (let ((val1 (expval->num (value-of exp1 env))) (val2 (expval->num (value-of exp2 env)))) (num-val (- val1 val2)))) (zero?-exp (exp1) (let ((val1 (expval->num (value-of exp1 env)))) (if (zero? val1) (bool-val #t) (bool-val #f)))) (if-exp (exp0 exp1 exp2) (if (expval->bool (value-of exp0 env)) (value-of exp1 env) (value-of exp2 env))) (let-exp (id rhs body) (let ((val (value-of rhs env))) (value-of body (extend-env id val env)))) )))
We can make LET more useful by adding procedures, which gives us a new language we'll call PROC.
Program | ::= |
Expression | a-program (exp1) |
Expression | ::= |
Number | const-exp (num) |
::= |
-( Expression
, Expression) |
diff-exp (exp1 exp2) |
|
::= |
zero? ( Expression) |
zero?-exp (exp1) |
|
::= |
if Expression
then Expression
else Expression |
if-exp (exp1 exp2 exp3) |
|
::= |
Identifier | var-exp (var) |
|
::= |
let Identifier
= Expression
in Expression |
let-exp (var exp1 body) |
|
::= |
proc ( Identifier)
Expression |
proc-exp (var body) |
|
::= |
( Expression
Expression) |
call-exp (exp1 exp2) |
(define the-grammar '((program (expression) a-program) (expression (number) const-exp) (expression ("-" "(" expression "," expression ")") diff-exp) (expression ("zero?" "(" expression ")") zero?-exp) (expression ("if" expression "then" expression "else" expression) if-exp) (expression (identifier) var-exp) (expression ("let" identifier "=" expression "in" expression) let-exp) (expression ("proc" "(" identifier ")" expression) proc-exp) (expression ("(" expression expression ")") call-exp) ))
In
(proc-exp
var
body)
,
we say the variable var is the
bound variable or formal parameter.
In a procedure call
(call-exp
exp1
exp2)
,
we say
the expression exp1 is the operator
and exp2 is the operand or
actual parameter.
An actual parameter is an expression, but its value is not;
we say the value of an actual parameter is an argument.
Here are a couple of programs we can write in the PROC language:
let f = proc (x) -(x,11) in (f (f 77))
(proc (f) (f (f 77)) proc (x) -(x,11))
For PROC, the expressed values are still the same as the denoted values:
ExpVal = Int + Bool + Proc
DenVal = Int + Bool + Proc
As before, we regard the expressed and denoted values as abstract data types:
num-val
: Int → ExpVal
bool-val
: Bool → ExpVal
proc-val
: Proc → ExpVal
expval->num
: ExpVal → Int
expval->bool
: ExpVal → Bool
expval->proc
: ExpVal → Proc
(expval->num (num-val
n))
= n
(expval->bool (bool-val
b))
= b
(expval->proc (proc-val
p))
= p
We must also specify the Proc data type:
procedure
: Symbol × Exp × Env → Proc
apply-procedure
: Proc × ExpVal → ExpVal
(apply-procedure (procedure
varbody
ρ
)
val)
=
(value-of
body[var = val]ρ
)
Now we can write the
proc
expressions and procedure calls
(value-of (proc-exp
varbody
)
ρ)
=(proc-val (procedure
varbody
ρ
))
(value-of (call-exp
ratorrand
)
ρ)
=(apply-procedure (expval->proc (value-of
ratorρ
)) (value-of
randρ
))
We can implement that specification by adding two new
cases
clauses to our interpreter:
(define value-of (lambda (exp env) (cases expression exp ... (proc-exp (bvar body) (proc-val (procedure bvar body env))) (call-exp (rator rand) (let ((proc (expval->proc (value-of rator env))) (arg (value-of rand env))) (apply-procedure proc arg) )))))
Our implementation of PROC is not yet complete, because we haven't implemented the Proc data type. We have specified the Proc data type, however, and we can use that specification to work through examples.
If x is the plain text for a source program
or some fragment thereof, then we will write
<<x>>
to mean the abstract syntax tree for x.
(value-of <<let x = 200 in let f = proc (z) -(z,x) in let x = 100 in let g = proc (z) -(z,x) in -((f 1), (g 1))>> rho) = (value-of <<let f = proc (z) -(z,x) in let x = 100 in let g = proc (z) -(z,x) in -((f 1), (g 1))>> [x=(num-val 200)]rho) = (value-of <<let x = 100 in let g = proc (z) -(z,x) in -((f 1), (g 1))>> [f=(proc-val (procedure z <<-(z,x)>> [x=(num-val 200)]rho))] [x=(num-val 200)]rho) = (value-of <<let g = proc (z) -(z,x) in -((f 1), (g 1))>> [x=(num-val 100)] [f=(proc-val (procedure z <<-(z,x)>> [x=(num-val 200)]rho))] [x=(num-val 200)]rho) = (value-of <<-((f 1), (g 1))>> [g=(proc-val (procedure z <<-(z,x)>> [x=(num-val 100)] [f=(proc-val (procedure z <<-(z,x)>> [x=(num-val 200)]rho))] [x=(num-val 200)]rho))] [x=(num-val 100)] [f=(proc-val (procedure z <<-(z,x)>> [x=(num-val 200)]rho))] [x=(num-val 200)]rho) = (- (value-of <<(f 1)>> [g=(proc-val (procedure z <<-(z,x)>> [x=(num-val 100)] [f=(proc-val (procedure z <<-(z,x)>> [x=(num-val 200)]rho))] [x=(num-val 200)]rho))] [x=(num-val 100)] [f=(proc-val (procedure z <<-(z,x)>> [x=(num-val 200)]rho))] [x=(num-val 200)]rho) (value-of <<(g 1)>> [g=(proc-val (procedure z <<-(z,x)>> [x=(num-val 100)] [f=(proc-val (procedure z <<-(z,x)>> [x=(num-val 200)]rho))] [x=(num-val 200)]rho))] [x=(num-val 100)] [f=(proc-val (procedure z <<-(z,x)>> [x=(num-val 200)]rho))] [x=(num-val 200)]rho)) = (- (apply-procedure (expval->proc (proc-val (procedure z <<-(z,x)>> [x=(num-val 200)]rho))) (num-val 1)) (apply-procedure (expval->proc (proc-val (procedure z <<-(z,x)>> [x=(num-val 100)] [f=(proc-val (procedure z <<-(z,x)>> [x=(num-val 200)]rho))] [x=(num-val 200)]rho))) (num-val 1))) = (- (value-of <<-(z,x)>> [z=(num-val 1)] [x=(num-val 200)]rho) (value-of <<-(z,x)>> [z=(num-val 1)] [x=(num-val 100)] [f=(proc-val (procedure z <<-(z,x)>> [x=(num-val 200)]rho))] [x=(num-val 200)]rho)) = (- (- 1 200) (- 1 100)) = -100
We still need to implement the Proc data type. Section 3.3.2 of the textbook (page 79) shows two different implementations of that data type. We can use either one, because Proc is an abstract data type.
We can represent PROC procedures by Scheme procedures:
;; procedure : Symbol * Exp * Env -> Proc (define procedure (lambda (bvar body env) (lambda (arg) (value-of body (extend-env bvar arg env))))) ;; apply-procedure : Proc * ExpVal -> ExpVal (define apply-procedure (lambda (proc arg) (proc arg)))
Or we can represent PROC procedures as closures:
(define-datatype proc proc? (procedure (bvar symbol?) (body expression?) (env environment?))) (define apply-procedure (lambda (proc1 arg) (cases proc proc1 (procedure (bvar body saved-env) (value-of body (extend-env bvar arg saved-env))))))
These two representations may remind you of two of the representations we have considered for environments. In some real implementations of real programming languages, the representations of environments and procedures are very similar or even identical.
Last updated 11 February 2008.