9 — Store Passing

Tuesday, 04 February 2020

Presenters (1) F. Greenwald, Vire Patel (2) Vincent Carlino, John Nahil

Evaluating Declarations Once and For All

Consider this program:
(decl "x" 0
         (decl "y" (set "x" (node + "x" 1))
               (if-0 (node + "y" "y")
If interpret evaluates the initialization expression for "y" every time it looks up the variable, the value of the test expression would be 1. Why? Because set returns the old value of "x", which is 0 the first time around. and 1 the second one.

But, if the interpreter evaluates the initialization only once—like every language you know—the value of "y" is always 0 (and "x" is 1). So if-0 selects the then branch, and we get 0.



  #lang racket
  (require "../8/ass-as-data.rkt")
  (require "old-interpreter.rkt")
  (define weird-example-1
    (decl "f" (node + "f" 1)
  ; (interpret weird-example-1)
  (define weird-example-2
    (decl "f" (fun "g"
                   (fun "x"
                        (call "g" (node * "x" "x"))))
          (decl "h" (call "f" "h")
  (interpret weird-example-2)
  (define weird-example-3
    (call (decl "f" (fun "g"
                         (fun "x"
                              (call "g" (node * "x" "x"))))
                (decl "h" (call "f" "h")
  (interpret weird-example-3)

Figure 37: Some More Oddities

Figure 37 shows some more oddities of the implemented language. Other dynamically typed languages—for example, JavaScript, Python, TypeScript, which some of you use—suffer from the exact same problems. Statically (explicitly or implicitly) typed languages tend to impose severe restrictions on recursive definitions so that these problems disappear, at the cost of having to write complex code.

See General [Organization] for "wat" presentations if you want to show off oddities in context.

Recursion From Assignment

Now here is another curious program:
(decl "f" 0
      (sequ (set "f"
                 (fun "x"
                      (if-0 "x"
                            (node * "x" (call "f" (node + "x" -1))))))
            [list (call "f" 10)]))
When you interpret this program, you actually get 3628800, which everyone knows is the factorial of 10.

Stop! What happened?

The assignment to "f" creates a cycle in the memory. When the call to the function eventually triggers a reference to "f", the value (not its meaning!) is the function-value in the location.

This creation of cycle is how recursion actually works in real implementations, though for many definitions at the same time.


Yours truly created a lambda calculus that proves the fix point equation for this function. Talcott proved they denote the same function in the canonical model. Detour If you think the Y combinator from 4 — Compilers; Fun [Y~~~A Phenomenon] is cool, take a look a the Y-bang function in figure 38. It computes fixpoints and thus creates recursive functions, too—but without the funny self-applications of Y.


  #lang racket
  (define Y!
    (lambda (f-maker) ;; no recursion, no self-application 
      ((lambda (fixpoint)
         (set! fixpoint (f-maker (lambda (x) (fixpoint x))))
  ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
  ;; plain recursive function for determining the length of a list
  (define the-lyst '("a" "b" "c"))
  (define recursive-length
    (lambda (lyst)
      (if (empty? lyst)
          (+ 1 (recursive-length (rest lyst))))))
  (recursive-length the-lyst)
  ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
  ;; using the Y! operator to  create a fixpoint via mutation 
  (define imperative-length ;; is _not_ recursive 
    (Y! (lambda (g)
          (lambda (lyst)
            (if (empty? lyst)
                (+ 1 (g (rest lyst))))))))
  (imperative-length the-lyst)

Figure 38: The Real Y Combinator

Problems with the Interpretation of AssExpr

Here is a final, rather problematic property of this interpreter. The following program
(decl "f" (node + "f" 1)
triggers a rather curious error in the interpreter.

Stop! Why?

The problem is that the truly initial value in the location of a declared variable may escape via a premature interpretation.

But, depending on where it goes, this may not immediately or perhaps never raise an exception.

Stop! Does it help to stick a proper AssExpr value into this location?

One solution is to trap every variable reference and check for #f; if the check succeeds, the interpreter can warn the programmer of this mistake.

Another solution is to identify syntactic values and use only those for initialization expressions. This restriction disallows the above program.

Stop! Does it truly restrict a programmer’s ability to express thoughts clearly?

Do Boxed Values Explain Mutation?



PL researchers firmly embrace the idea that a mathematical representation of mutable variables—without use of assignments in the implementation language—is superior to the one we have seen.

A mutable variable in a program has two distinct attributes: its scope and its current value. When the interpreter deals with a variable reference in isolation, the two attributes are determined by different aspects of the variable’s context. So the question is how to represent this contextual information.

The transformation of a substitution interpreter into an environment interpreter demonstrates the basic idea:

contextual information is represented by an additional argument to the interpreter.

In the terminology of Fundamentals I and II, we call this an accumulator.

Since mutable variable have two distinct contextual attributes, the idea then is to use two distinct accumulators: the environment for scope and a store for the current values. Keep in mind though that the scope of a mutable variable never changes, while its current value does. Hence what goes into the environment for a variable remains the same, while the store changes. In terms of Fundamentals I,

In sum, the interpreter now has this signature:

; FVExpr Environment Store ->* Value Store

The ->* indicates that the function returns several results at once (here, two).

Stop! Use one of the previous examples or a simplified one to justify that Store must be a result.

Now the question is how the two accumulators relate. Intuitively, the store represents the memory of a computer, that is, a bunch of addressable memory locations in which actual values reside. Mathematically, this is an association between addresses and values. In PL, an address is usually called a location. In turn, this suggests that instead of boxed values, the revised interpreter sticks locations into the environment and uses the store to look up the current value of a location.

Where do locations come from? The interpreter must know which locations have already been used for some variable and which ones are free to use when the interpreter encounters a declaration or a function parameter. If we ignore the problem that computers have a finite number of locations, we can just count up from 0 to infinity as long as we know which locations are in use.

To summarize, the store is an association of locations and values and supplies three functions: one for allocating a fresh location, one for (re)associating a value with a location, and one for retrieving the value given some variable’s location.


  #lang racket
   #; {type Store} 
   #; {type Location}
   plain ;; Store 
   #; {Store Value ->* Location Store}
   #; {Store Location -> Value}
   #; {Store Location Value -> Store}
  ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
  (require "../4/possible-values.rkt")
  (module+ test
    (require rackunit))
  ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
  (define plain '())
  (define START -1)
  (define (alloc store val)
    (define last-address-used (apply max START (map first store)))
    (define new (+ last-address-used 1))
    (values new (cons (list new val) store)))
  (define (retrieve store loc)
    (define r (assoc loc store))
    (if r (second r) (error 'store-lookup "dangling pointer ~e" loc)))
  (define (update store loc val)
    (cons (list loc val) store))

Figure 39: A Functional Store Implementation

Figure 39 displays a completely functional representation of the Store. You can understand it as “executable mathematics.”

Stores and Store-Passing

Given a store, we now have to figure out when to allocate locations for variables, when and how to look into locations, and when and how to update them.


  #lang racket
  ;; an interpreter that uses store passing
  ;; the meaning of Variables are Locations
  ;; .. NEVER changes because their scope never changes 
  ;; the meaning of Locations are Values
  ;; .. which change to represent mutation 
  (require "../6/environment.rkt")
  (require "../8/ass-as-data.rkt")
  (require "../8/examples.rkt")
  (require "../8/examples-fun.rkt")
  (require "../8/examples-ood.rkt")
  (require "../8/left-hand-side-value.rkt")
  (require "../4/possible-values.rkt")
  (require "store.rkt")
  (require SwDev/Debugging/spy)
  #; {Value = Number || (function-value parameter FExpr Env)}
  (define UNDECLARED "undeclared variable ~e")
  #; {FExpr -> Value}
  ;; determine the value of ae via a substitutione semantics 
  (define (interpret ae0)
    #; {FExpr Env Store ->* Value Store}
    ;; ACCUMULATOR env tracks all declarations between ae and ae0
    (define (interpret ae env store)
      (match ae
        [(? integer?)
         (values ae store)]
        [(node o a1 a2)
         (define-values (right0 store+) (interpret a2 env store))
         (define right (number> right0))
         (define-values (left0 store++) (interpret a1 env store+))
         (define left  (number> left0))
         (values (o left right) store++)]
        [(decl x a1 a2)
         (define-values (loc store+) (alloc store #f))
         (define env++ (add x loc env))
         (define-values (val store++) (interpret a1 env++ store+))
         (interpret a2 env++ (update store++ loc val))]
        [(? string?)
         (if (defined? ae env)
             (values (retrieve store (lookup ae env)) store)
      (error 'value-of UNDECLARED ae))]
        [(call ae1 ae2)
         (define-values (right store+) (interpret ae2 env store))
         (define-values (left store++) (interpret ae1 env store+))
         (fun-apply (function> left) right store++)]
        [(fun para body)
         (values (function-value para body env) store)]
        [(if-0 t thn els)
         (define-values (test-value store+) (interpret t env store))
         (if (and (number? test-value) (= test-value 0))
             (interpret thn env store+)
             (interpret els env store+))]
        [(set lhs rhs)
         (define loc (lookup lhs env))
         (define old (retrieve store loc))
         (define-values (val store+) (interpret rhs env store))
         (values old (update store+ loc val))]
        [(sequ fst rst)
         (define-values (_ store+) (interpret fst env store))
         (interpret rst env store+)]))
    #; {Value Value Store ->* Value Store}
    (define (fun-apply function-representation argument-value store)
      (match function-representation
        [(function-value fpara fbody env)
         (define-values (loc store+) (alloc store argument-value))
         (interpret fbody (add fpara loc env) store+)]))
    (define-values (result store) (interpret ae0 empty plain))
  #; {Any -> Number}
  (define (number> x)
    (if (number? x)
        (error 'interpreter "number expected, given ~e " x)))
  #; {Any -> Function}
  (define (function> x)
    (if (function-value? x)
        (error 'interpreter "closure expected, given ~e " x)))
  ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  (provide interpret)
  ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  (module+ test
    (require rackunit)
    (check-equal? (interpret (if-0 0 1 2)) 1)
    (check-equal? (interpret (if-0 1 0 2)) 2)
    (check-equal? (interpret example1) (example1-asl))
    (check-equal? (interpret example1) (example1-object)))

Figure 40: A Store-Passing Interpreter