Mutation & Recursion

Assignment 4

parse-interpret.png

parse-desugar-interpret.png

Our Racket interpreter is split over a number of Lisp packages. This was because we wrote a numbre of iterpreters for various mini-languages.

For your Python interpreter we will use just one package: :tolk/python. This package is already defined in src/python/package.lisp so when you begin writing your Python interpreter you only need to make a file called src/python/interpreter.lisp, you only need to put the following line at the top:

(in-package :tolk/python)

You might then continue with:

(defgeneric interpret (node))

(defmethod interpret ((c constant))
  (slot-value c 'value))

(defmethod interpret ((b binop))
  (funcall (case (slot-value b 'op)
             (:add '+))
           (interpret (slot-value b 'left))
           (interpret (slot-value b 'right))))

The first one interprets constant values and the second binary operators (only addition!).

Or if you prefer to use defmethod-bind then:

(defmethod-bind interpret ((constant value) env)
    ()
  value)

(defmethod-bind interpret ((binop left op right) env store)
    ((left-val (interpret left env store))
     (right-val (interpret right env store1)))
  (ecase op
    (:add (+ left-val right-val))))

Be sure to put a line like this in the tolk.asd file:

(:file "interpreter")

Now make a test file tests/python/interpreter.lisp and add tests similar to those in tests/python/parsing.lisp Be sure to import the functions you defined in src/python/interpreter.lisp into tests/python/suite.lisp

Let Over Lambda

In the previous class we saw how we can separate

  • making a function value with lambda, and
  • assigning that value to a variable with define
(define (f x y)
  (+ (* 2 x) y))

(define f (lambda (x y)
            (+ (* 2 x) y)))

The first form is simply syntactic sugar for the second.

The second form is no different than

  • making an integer value with +, and
  • assigning that value to a variable with define

    (define seven (+ 3 4))
    

    But what use is this separation?

    (define counter
      (let ((count 0))
        (lambda ()
          (set! count (+ 1 count))
          count)))
    

Now we have

> (counter)
1
> (counter)
2
> (counter)
3
> (counter)
4

Note how this implements information hiding; the variable count cannot be accessed in any way except through the counter function.

Here is an example of a bank with two accounts, for Jack and Jill.

(define bank
  (let ((jack 100)
        (jill 100))
    (lambda (amount)
      (set! jack (- jack amount))
      (set! jill (+ jill amount))
      (list jack jill))))

We can transfer money from Jack's account to Jill's or vice versa but the total amount remains constant.

> (bank 0)
'(100 100)
> (bank 20)
'(80 120)
> (bank -50)
'(130 70)
>

Database transactions need to satisfy the so-called ACID properties. The above is an example of transaction atomicity.

Mutation

In Python we use the same notation to mutate a (simple) value as to introduce it.

x = 1
x = 2

Here the first line is an introduction and the second a mutation.

In Racket we use different notations.

(define x 1)
(set! x 2)

The convention in Racket is to end the name of a mutation operator with an exclamation mark (pronounced "bang", as in "set bang"). The reason for this is that Racket is more often used for functional programming so it is helpful to draw attention to any explicit mutation.

Recursion

We have seen above that let does not support recursion because the environment for the recursive call does not contain a binding for the function itself. To address this, we introduce a letrec form that makes recursive function definitions possible.

The letrec form has the same shape as let1 but uses a different evaluation strategy: the value expression is interpreted in an environment that already contains the binding for the variable being defined.

(letrec (fact (lambda n
                (if (= n 0)
                    1
                    (* n (fact (- n 1))))))
  (fact 5))

We add a letrec class to the AST in ./src/racket/recursive-functions.lisp.

(defclass letrec (ast-node)
  ((var :type var :initarg :var)
   (value :type ast-node :initarg :value)
   (body :type ast-node :initarg :body)))

The parser adds a letrec clause:

(`(letrec (,var ,value) ,body) (make-instance 'letrec
                                 :var (make-instance 'var :v var)
                                 :value (parse value)
                                 :body (parse body)))

The interpretation of letrec uses the store machinery from the mutation language to create a location for the new binding before interpreting the value. This way the binding is in scope for recursive calls.

(defmethod-bind interpret ((letrec var value body) env store)
    (((:slots (var-name v)) var)
     (loc (new-location))
     (new-env (extend var-name loc env))
     ((:values val store1) (interpret value new-env store)))
  (interpret body new-env (override loc val store1)))

Note how new-env already contains the binding before value is interpreted, and the store is updated afterwards with the actual computed val.

Tests for letrec include factorial, Fibonacci, and nested recursive definitions.

(5am::explain! (5am:run 'tolk/tests/recursive-functions::letrec-factorial-direct))

Author: Breanndán Ó Nualláin <o@uva.nl>

Date: 2026-05-21 Thu 08:35