LETREC: A Language with Recursive Procedures

Can we write an infinite loop in the LET language?

Can we write an infinite loop in the PROC language?

What value is computed by the following program?

    let f = proc (x) (x x)
    in (f f)

We will now extend PROC by adding declarations of recursive procedures.

Syntax for the LETREC 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)
::= letrec Identifier (Identifier) = Expression
in
Expression
letrec-exp
(proc-name bvar proc-body letrec-body)

  (define the-grammar
    '((program (expression) a-program)

      ...

      (expression
        ("letrec"
          identifier "(" identifier ")" "=" expression
           "in" expression)
        letrec-exp)
      
      ))

Specification of letrec expressions

(value-of (letrec-exp proc-name bound-var proc-body letrec-body) ρ)
= (value-of letrec-body (extend-env-rec proc-name bound-var proc-body ρ))

where the environment ADT is extended as follows:

empty-env : → Env
extend-env : Var × Val × Env → Env
extend-env-rec : Var × Var × Exp × Env → Env
apply-env : Env × Var → Val

(apply-env (extend-env var val env) var)
= val
(apply-env (extend-env var1 val env) var2)
     = (apply-env env var2)   (var1var2)

(apply-env (extend-env-rec var bvar body env) var)
= (proc-val (procedure bvar body (extend-env-rec var bvar body env)))
(apply-env (extend-env-rec var1 bvar body env) var2)
     = (apply-env env var2)   (var1var2)

Implementing that extension is a simple programming exercise. Once we have implemented the extended ADT of environments, we can implement the LETREC interpreter:

  (define value-of
    (lambda (exp env)
      (cases expression exp

        ...

        (letrec-exp (proc-name bvar proc-body letrec-body)
          (value-of letrec-body
            (extend-env-rec proc-name bvar proc-body env)))

	    )))

Scope and Binding of Variables

Variables may be declared and may also be referenced. A variable declaration is said to bind the variable to a denoted value. We say the declared variable is bound to its denoted value.

In most programming languages, a variable reference refers to some declaration of the variable. There may be several different declarations of variables that have the same name. Scoping rules tell us the declaration to which a variable reference refers.

In most programming languages, we can determine the declaration to which a variable reference refers without running the program. That means the scoping rules are static.

Lexical scoping rules are the most common. With lexical scoping, each kind of declaration has an associated region in which it is possible to refer to the declared variable unless some inner declaration of a variable with the same name has created a hole in the scope.

Some authors, such as the authors of our textbook, define the scope of the declared variable to be that region, while other authors define the scope of the declared variable to exclude the holes created by inner declarations.

The region is also called a contour, and we can draw contour diagrams to help us figure out which variables refer to which declarations.

The extent of a binding is the interval of time during which the variable remains bound to its denoted value. In PROC and LETREC, as in Scheme, the bindings have semi-infinite extent (which is also called indefinite extent). An automatic process called the garbage collector determines when a binding is no longer reachable, and recovers the storage occupied by unreachable bindings and other objects.

Some programming languages restrict the extent of a binding to the time required to evaluate some expression. This is called dynamic extent. It is not as powerful as semi-infinite extent, but is easier to implement.

Eliminating Variable Names

The lexical depth (or static depth) of a variable reference is the distance from reference to the declaration to which it refers, measured in the number of contours that must be crossed to get from the reference to the declaration.

If we know the lexical depth of a variable reference in the PROC or LETREC languages, then we can locate the declaration of the variable without knowing its name.

By modifying the environment ADT so the apply-env operation takes a lexical address instead of a variable name, we could look up the value denoted by a variable without using its name.

Compilers take advantage of this fact by translating each variable reference into a sequence of machine instructions that fetches the value of the variable out of the current environment. Suppose, for example, that the lexical address of x is ‹3,12›. Then the code generated to fetch the value of x into register r1 might be

        load    r23,0(r0)
        load    r23,0(r23)
        load    r23,0(r23)
        load    r1,12(r23)

Implementing Lexical Addressing

If we wanted to, we could write a translator that removes all variable names from a program, replacing variable references by lexical addresses.

The Translator

Section 3.7.1 of our textbook (pages 94-96) develops that translator for the PROC language.

The Nameless Interpreter

Section 3.7.2 of our textbook (pages 96-100) develops an interpreter for the nameless programs that are output by the translator of section 3.7.1.


State

Computational Effects

Expressions are generally intended to evaluate to some value. In addition, they may have an effect such as altering the state of an i/o device, file system, computer network, or location in memory.

Effects, unlike values, may have global influence; they may affect the entire computation. For example: Binding is local, but assignments to shared variables can affect the behavior of modules that do not themselves contain any assignments to the variable.

The store is a finite map from locations to storable values. The storable values are often the same as the expressed values, but do not have to be.

A variable or data structure that represents a location is called a reference. The reference is said to denote the location.

References are sometimes called L-values because the left side of an assignment should evaluate to a reference. Storable values are sometimes called R-values because the right side of an assignment should evaluate to a storable value.

For the last couple of decades, references have been implicit in most popular programming languages. Explicit references make these concepts easier to understand, however, so we will start by considering a language with explicit references.

EXPLICIT-REFS: A Language with Explicit References

In a language with explicit references, references are usually a kind of expressed value:

ExpVal = Int + Bool + Proc + Ref(ExpVal)
DenVal = ExpVal

The parameterized data type of references is a mutable data type, so we can't specify it with an algebraic specification.

Here is an example of two procedures that communicate via a shared reference:

    let x = newref(0)
    in letrec even(dummy)
                = if zero?(deref(x))
                     then zero?(0)
                     else begin
                           setref(x, -(deref(x),1));
                           (odd 888)
                          end
              odd(dummy)
                = if zero?(deref(x))
                     then zero?(1)
                     else begin
                           setref(x, -(deref(x),1));
                           (even 888)
                          end
       in begin setref(x,13); (odd 888) end

Here's an example of a procedure with hidden mutable state:

    let g = let counter = newref(0)
            in proc (dummy)
                begin
                 setref(counter, -(deref(counter), -1));
                 deref(counter)
                end
    in let a = (g 11)
       in let b = (g 11)
          in let c = (g 11)
             in c

References are first-class values in this language:

    let x = newref(newref(0))
    in begin
        setref(deref(x), 321);
        deref(deref(x))
       end

Store-Passing Specifications

We use the following abbreviations:

σ ranges over stores
[] denotes the empty store
[l = v]σ denotes the store that is like σ except location l holds storable value v

We'll use store-passing specifications, which pass the store as an explicit argument to value-of, which returns two results: an expressed value and a (possibly different) store.

Specification for three kinds of expressions

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

(value-of exp1 ρ σ) = (val1, σ1)
(expval->num val1) = n1
(value-of exp2 ρ σ1) = (val2, σ2)
(expval->num val2) = n2
n1 - n2 = n
----------------------------------------------------
(value-of (diff-exp exp1 exp2) ρ σ) = ((num-val n), σ2)

Specifying Operations on Explicit References

Syntax for the EXPLICIT-REFS 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)
::= letrec Identifier (Identifier) = Expression
in
Expression
letrec-exp
(proc-name bvar proc-body letrec-body)
::= (newref Expression) newref-exp (exp1)
::= (deref Expression) deref-exp (exp1)
::= (setref Expression , Expression) setref-exp (exp1 exp2)

Specification for the three new kinds of expressions

(value-of exp1 ρ σ) = (val1, σ1)
l ∉ dom(σ1)
----------------------------------------------------------------
(value-of (newref-exp exp1) ρ σ) = ((ref-val l), [l=val1]σ1)


(value-of exp1 ρ σ) = (val1, σ1)
(expval->ref val1) = l
σ1(l) = v
----------------------------------------------------------------
(value-of (deref-exp exp1) ρ σ) = (v, σ1)


(value-of exp1 ρ σ) = (val1, σ1)
(value-of exp2 ρ σ1) = (val2, σ2)
(expval->ref val1) = l
----------------------------------------------------------------
(value-of (setref-exp exp1 exp2) ρ σ) = ((num-val 23), [l=val2]σ2)



Implementation

In real implementations, only one store is active. (This is a critical difference between environments and stores!) We can take advantage of this by implementing the store as a global variable, so we don't have to pass it as an argument to value-of.

  ;; value-of-program : Program -> ExpVal

  (define value-of-program 
    (lambda (pgm)
      (initialize-store!)               ; new for explicit refs.
      (cases program pgm
        (a-program (exp1)
          (value-of exp1 (init-env))))))

We can implement operations on the store like this:

  ;;; World's dumbest implementation of stores:
  ;;; a store is a list and
  ;;; a reference is an integer index into the list.
  
  ;;; the-store : Store
  ;;; the-store is the current store

  (define the-store (empty-store))
  
  ;;; reference? : SchemeVal -> Boolean
  ;;; (reference? x) is true iff x represents a location

  (define reference? integer?)
  
  ;;; initialize-store! : -> Unspecified
  ;;; (initialize-store!) sets the store to the empty store

  (define initialize-store!
    (lambda ()
      (set! the-store (empty-store))))
  
  ;;; empty-store : -> Store
  ;;; (empty-store) returns an empty store

  (define empty-store
    (lambda () '()))
  
  ;;; newref : ExpVal -> Ref
  ;;; (newref v) returns a newly allocated reference
  ;;;     initialized to v

  (define newref
    (lambda (val)
      (let ((next-ref (length the-store)))
        (set! the-store
              (append the-store (list val)))
        (if (instrument-newref)
            (eopl:printf 
             "newref: allocating location ~s~%"
             next-ref))                     
        next-ref)))                     
  
  ;;; deref : Ref -> ExpVal
  ;;; (deref ref) returns the value currently contained in ref

  (define deref 
    (lambda (ref) (list-ref the-store ref)))
  
  ;;; setref! : Ref * ExpVal -> Unspecified
  ;;; (setref! ref val) changes the current store
  ;;;     by storing val into ref

  (define setref!                       
    (lambda (ref0 val)
      (set! the-store

            ;; setref-inner : Store * Ref -> Store
            ;; (setref-inner sigma l) returns a store
            ;;     that is like sigma except that,
            ;;     in the store returned by setref-inner,
            ;;     the location l contains val.

            (letrec ((setref-inner (lambda (store ref)
                              (cond
                                ((null? store)
                                 (eopl:error 'setref
                                             "illegal reference"))
                                ((zero? ref)
                                 (cons val (cdr store)))
                                (else
                                 (cons
                                  (car store)
                                  (setref-inner (cdr store)
                                                (- ref 1)))))))))
              (setref-inner the-store ref0)))))

Finally, we can extend our interpreter:

        (newref-exp (exp1)
          (let ((v1 (value-of exp1 env)))
            (ref-val (newref v1))))

        (deref-exp (exp1)
          (let ((v1 (value-of exp1 env)))
            (let ((ref1 (expval->ref v1)))
              (deref ref1))))

        (setref-exp (exp1 exp2)
          (let ((ref (expval->ref (value-of exp1 env))))
            (let ((v2 (value-of exp2 env)))
              (begin
                (setref! ref v2)
                (num-val 23)))))

Last updated 14 February 2008.

Valid XHTML 1.0!