We now explain how to implement the dynamic allocation primitives of
LC. To avoid confusion with the Scheme cognates, we
will refer to the LC operations by
replacing `c' with `C' in the Scheme operations yielding
Car
,
Cdr
, Cons
and friends.
What does Cons
correspond to in C? It does two things:
it dynamically allocates two elements of memory, and it initializes
the elements with the values provided as arguments. In C, this
roughly corresponds to malloc
followed by initialization.
malloc
? Can't the
allocation be performed on the stack instead?
Moral: Data have dynamic extents (lifetimes).
All modern languages have the ability to create data whose extent is dynamic. However, many programming languages force the life-span of data to coincide with the execution period for the lexical scope in which they are created. As the ``dangling pointer'' problem of C and C++ programs illustrates, this brute-force approach to memory managment leads to insiduous, difficult-to-find bugs. Advanced languages instead put all complex data on the heap and let the program behavior determine the life-span of each datum. To make this approach work, such languages provide a garbage collector, which automatically de-allocates data when it is provably irrelevant.
Now we return to the question of how to model Cons
in the
interpreter. We can do it by adding a store as a component
of the evaluator, similar to the store in the Jam 2000 interpreter
used in Comp 210. A store consists of two things: (1) a pointer to
the next usable portion, and (2) an allocator that moves the next-use
pointer and returns a handle on the allocated space.
Currently, our stores have the following abstract specification:
setup: () -> () allocate: val x val -> loc lookup: loc -> val update: loc x val -> ()This can be implemented with the following code (which is written without error checks, to simplify presentation):
(define next-usable 0) ; of type `loc'ation (define memory '()) (define setup (lambda () (set! memory (vector BIG)))) (define allocate (lambda (v-1 v-2) (begin0 next-usable (vector-set! memory next-usable v-1) (vector-set! memory (+ next-usable 1) v-2) (set! next-usable (+ next-usable 2))))) (define lookup (lambda (loc) (vector-ref memory loc))) (define update (lambda (loc val) (vector-set! memory loc val)))
Given these, we can add Cons
to the interpreter:
((Cons? M) (let ((l (MEval (Cons-lhs M) env)) (r (MEval (Cons-rhs M) env))) (make-LC-pair (allocate l r))))(Recall that the value returned needs to be an
LC-pair
.)
Why do we have a danger sign? It is because the order of evaluation
of bindings in a let
expression in Scheme is not
specified. Hence, we may end up evaluating the rhs
before we do the lhs
. Since this is undesirable, we need
to use a let*
instead of let
.
We can similarly add Car
, Cdr
,
set-Car!
and set-Cdr!
:
((Car? M) (let ((p (MEval (Car-exp M) env))) (unless (LC-pair? p) (error ...)) (lookup (LC-pair-loc p)))) ((set-Cdr!? M) (let* ((p (MEval (set-Cdr!-exp M) env)) (v (MEval (set-Cdr!-val M) env))) (unless (LC-pair? p) (error ...)) (update (+ (LC-pair-loc p) 1) v)))