Note: If you’re completing the advanced tutorial remotely and plan to submit your RKT file, please make sure your file name has in it the string adv, otherwise we will assume you are submitting the classic version of the tutorial.

In this tutorial, we’ll add support for if and lambda to the interpreter.

Advance Tutorial 4 Starter Files


Updates from Last Week’s Template

Note, we’ve changed the code for the old interpreter a little from last week.

Sussman Form

The following is part of the language we will use to WRITE our interpreter but does not need to be processed by the interpreter itself.

So you know how we have been using lambda to denote the creation of functions? Even when we make named functions? You don’t actually have to do that. Instead there’s a shortcut (in programming language design, this is sometimes called syntactic sugar).

Say we want to make the function my-sum that takes as input a list of numbers and returns the sum. Before we’d do it by saying:

(define my-sum
  (lambda (a-list)
    (apply + a-list)))

Well there’s actually a short hand for this which is called “Sussman form” (named after one of the authors of our auxiliary textbooks.)

(define (my-sum a-list)
  (apply + a-list))

In other words, the lambda symbol itself can be skipped. Instead, we just say (define (func-name input-1 input-2 ...) output-expression) as shorthand. You’ll see this in the template file. Sussman form is equivalent to what we have been using (actually when Racket processes your code it transpiles it to include that lambda symbol) and is literally just an optional shorthand you can use.

More dictionaries

Since we’re going to be adding lambda expressions to the interpreter, and lambdas have arguments (variables to hold the inputs to the procedure), we need some way to let the dictionary change over time. To do that, we’ve modified the old interpreter so that most of the procedures take a dictionary as an additional input. That way we can use different dictionaries in different contexts.

We also added a new procedure, extend-dictionary, that takes a list of variables, a list of values for those variables, and an existing dictionary, and returns a new dictionary that has all the new variables and values, followed by all the old ones.

A new kind of procedure

In last week’s interpreter, when you ran (+ 1 1), the interpreter evaluated + and got back Racket’s + procedure. So all it had to do to finish the procedure call was to use apply to run Racket’s + procedure.

However, now we have to allow for the possibility that when the interpreter runs something like (f 1), that f will be the result of a lambda expression like (lambda (x) (+ x 1)). In that case, we run the procedure call (f 1) by calling evaluate on the source code (+ x 1) from the lambda, only now in a dictionary in which x is set to 1.

All of this means we need a new kind of data object to hold that source code in it so when the interpreter tries to call it, it can both recognize that it’s not a built-in Racket procedure, and also know what source code it needs to run. These data objects are traditionally called closures (you’ll learn more about these in COMP_SCI 321), but in our code, we’ve called them interpreted-procedures since that’s more explicit. There’s a define-struct in the code for interpreted procedures. They have three fields:

  • inputs: a list of the names of the variables for the procedure’s arguments
  • result: the expression to run to compute the output of the procedure
  • dictionary: the dictionary that was in use when the lambda was executed to make this interpreted-procedure. Remembering the dictionary will allow a lambda to use the local variables of the code around it.

So for example, if we run (lambda (x) (+ x 1)) while the dictionary is ((x 1) (y 2)), we’ll get back an interpreted-procedure whose:

  • inputs are '(x)
  • result is '(+ x 1)
  • dictionary is '((x 1) (y 2)).

Take a moment and read through the code. We’ve marked what’s been added, and what’s been changed.


Adding if expressions

In our old version, evaluate-complex always called evaluate-procedure-call. Write a new version of evaluate-complex that calls evaluate-if, when the expression starts with the symbol if, and otherwise calls evaluate-procedure-call. You can check if a value is the symbol if by saying:

(eq? value 'if)

Now all you have to do is write evaluate-if. We have a skeleton for it in the code. Fill it in.


Adding lambda expressions

Great. Now modify evaluate-complex so that if the expression starts with lambda, it calls evaluate-lambda. Then write evaluate-lambda. Again, we have a skeleton for you to start from in the code. Remember that evaluate-lambda is going to return an interpreted-procedure object, so it needs to call make-interpreted-procedure with values for the inputs, the result, and the dictionary.


Reimplementing procedure calls

As before, procedure calls are implemented by first recursively evaluating all the subexpressions, then calling the value of the procedure expression on the values of the argument expressions. However, this time we have two kinds of procedures to worry about: built-in, primitive Racket procedures, like +; and interpreted-procedures created by lambda.

Write evaluate-procedure-call so that it recursively finds the values of the subexpressions, as before. But now, instead of always using apply to run the procedure on the arguments, it instead calls apply-interpreted-procedure, if the procedure is an interpreted-procedure object. Otherwise, it can use apply as before.

Finally, write apply-interpreted-procedure. It should make a new dictionary using the argument names from the interpreted-procedure object and the argument values being passed to it, as well as the dictionary from the interpreted-procedure object. Then it can just call evaluate on the result expression from the interpreted-procedure object, and the new dictionary.


This is almost a complete programming language

The one thing we haven’t implemented is define, which will have to wait until we get to imperative programming. That’s why we have test cases that look like:

(check-expect (evaluate '((lambda (x) (+ x 1))
                          1)
                        dictionary)
              2)

It’s the only way we can test running an interpreted procedure for the moment, but we’ll get to define soon.

Not having define also means we also don’t have recursion itself (in our constructed language), which is a serious bummer. There is technically a way of implementing recursion using only what we have here. It’s called the Y combinator, and there’s a famous startup accelerator named after it. But both are way outside the scope of this course.


Getting Credit for Your Work

See the Canvas assignment for details on how to get credit for your work.