LET: A Simple Language


Specifying the Syntax


Syntax for the LET language

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)))))

Specification of Values

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 : IntExpVal
bool-val : BoolExpVal
expval->num : ExpValInt
expval->bool : ExpValBool

(expval->num (num-val n)) = n
(expval->bool (bool-val b)) = b

Environments

We use the following abbreviations:

ρ ranges over environments
[] denotes the empty environment
[var = val]ρ denotes (extend-env var val ρ)
[var = val] denotes [var = val][]

Specifying the Behavior of Expressions


Interface for expressions of LET

const-exp : IntExp
zero?-exp : ExpExp
if-exp : Exp × Exp × ExpExp
diff-exp : Exp × ExpExp
var-exp : SymbolExp
let-exp : Symbol × Exp × ExpExp

value-of : Exp × EnvExpVal

Specification for three kinds of expressions

(value-of (const-exp n) ρ) = (num-val n)

(value-of (var-exp var) ρ) = (apply-env ρ var)

(value-of (diff-exp exp1 exp2) ρ)
= (- (expval->num (value-of exp1 ρ)) (expval->num (value-of exp2 ρ)))

Specifying the Behavior of Programs

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]

Specifying Conditionals

(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 exp1 exp2 exp3) ρ) = (value-of exp2 ρ)
(value-of exp1 ρ) = val1
(expval->bool val1) = #f
----------------------------------------------------
(value-of (if-exp exp1 exp2 exp3) ρ) = (value-of exp3 ρ)

Specifying let

(value-of exp1 ρ) = val1
------------------------------------
(value-of (let-exp var exp1 body) ρ)
= (value-of body [var=val1]ρ)

Implementing the Specification of 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))))

        )))

PROC: A Language with Procedures

We can make LET more useful by adding procedures, which gives us a new language we'll call PROC.

Syntax for the PROC language

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 : IntExpVal
bool-val : BoolExpVal
proc-val : ProcExpVal
expval->num : ExpValInt
expval->bool : ExpValBool
expval->proc : ExpValProc

(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 × EnvProc
apply-procedure : Proc × ExpValExpVal

(apply-procedure (procedure var body ρ) val)
= (value-of body [var = val)

Now we can write the

Specification of proc expressions and procedure calls

(value-of (proc-exp var body) ρ)
= (proc-val (procedure var body ρ))

(value-of (call-exp rator rand) ρ)
= (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.

An Example

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

Representing Procedures

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.

Valid XHTML 1.0!