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.
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
Expression |
letrec-exp
|
(define the-grammar '((program (expression) a-program) ... (expression ("letrec" identifier "(" identifier ")" "=" expression "in" expression) letrec-exp) ))
letrec
expressions
(value-of (letrec-exp
proc-namebound-var
proc-body
letrec-body
)
ρ)
=(value-of
letrec-body(extend-env-rec
proc-namebound-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
varval
env
)
var)
= val
(apply-env (extend-env
var1val
env
)
var2)
=(apply-env
envvar2
)
(var1 ≠ var2)
(apply-env (extend-env-rec
varbvar
body
env
)
var)
=(proc-val (procedure
bvarbody
(extend-env-rec
varbvar
body
env
)))
(apply-env (extend-env-rec
var1bvar
body
env
)
var2)
=(apply-env
envvar2
)
(var1 ≠ var2)
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))) )))
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.
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)
If we wanted to, we could write a translator that removes all variable names from a program, replacing variable references by lexical addresses.
Section 3.7.1 of our textbook (pages 94-96) develops that translator for the PROC language.
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.
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.
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.
newref
takes a storable value,
allocates a new location,
stores its argument into the location,
and returns a reference to the location.
deref
takes a reference and returns its
current contents
setref
takes a reference and a storable value
and stores the value into the located denoted by the reference
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
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.
(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
exp1exp2
)
ρσ
)
= ((num-val
n)
, σ2)
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
Expression |
letrec-exp
|
|
::= |
(newref Expression) |
newref-exp (exp1) |
|
::= |
(deref Expression) |
deref-exp (exp1) |
|
::= |
(setref Expression ,
Expression) |
setref-exp (exp1 exp2) |
(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
exp1exp2
)
ρσ
)
= ((num-val 23)
, [l=val2]σ2)
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.