Session 26
Building a Language Interpreter

A Quick Puzzle: Add with Memory

Write a new + function that not only adds up its arguments but also displays the number of times it has been called and a running total of all the numbers it has seen. For example:
> (+ 1 1)
... running total: 2
2
> (+ 5 7 11 13)
... running total: 38
36
> (+ 100 (+ 6 1234))
... running total: 1278
... running total: 2618
1340
Note that the blue values are printed by Dr. Racket's REPL, because that is the value returned by +.

How can we remember the value of a variable like + and give it a new value?

A Quick Solution or Two

This code file works up to a solution, using several ideas we have learned in this unit. It demonstrates a common pattern in Racket that we also find in many other languages: using a temporary variable to hold a return value so that we can take some other action (here, write some output) before returning it. We see this pattern in Python and Java, too.

Naming our function + creates an infinite loop. So instead we provide a function of a different name and let the client program alias + to the new function.

We can also do this with a modified version of either the provide clause in the source file or the require clause in the client file. In this case, the latter seems a lot safer socially... An inattentive client could clobber + inadvertently with the former.

Where Are We?

You are implementing a small language, Huey. For today, you did a short code review of a sample implementation. Later today, you will begin working on Homework 10, which adds primitive names and local variables to Huey. For Homework 11, you will extend the language and its processors one more time with a few stateful features.

For Homework 9, you had to implement the specified behavior of a language and a simple interpreter for it. But you also had to make the many software engineering decisions that come with writing a small set of programs, from decomposing functions to naming parameters and local variables.

The questions you asked while solving Homework 9 show that many of you are thinking deeply about how to write a larger program. The questions you asked as a part of your code review were insightful as well. Let's take the time to discuss them in class, along with any other questions that come up.

You probably have as many questions as answers at this point. Whenever I write a program this large, I usually have a lot of questions, too.

Design Review

Note: The notes in this section are rougher than usual. They simply outline the kinds of questions you asked in your code review. We discussed questions such as these, and more, in class.

You asked a lot of questions in your code review. They were both interesting and challenging.

General 1
  • Why are there all these files? Could they be in a single file?
  • [ questions about language design ]
Utilities
  • Why are list-index and displayln in the utilities file if you don't use them?
  • Why define (list-of? n) at all? Isn't (= (length exp) 2)good enough?
Syntax procedures
  • Why do you use (all-defined-out) in your provide clause?
  • Why not create syntax procedures for each specific unary and binary operator?
  • What does the *convention* mean? Why do you use it?
  • Don't all of the functions in the interpreter file and the syntax procs file know the operators?
  • Why define the lists for the unary and binary operator outside the functions that use them?
  • Could we have separate lists for the core and sugar operator types?
  • Why are the constructors named unary-exp rather than something that implies their function like make-unary-exp?
  • Wouldn't it be better to put the general type predicate at the beginning of the file?

  • Why don't you type-check the constructors?
  • Are there other possible unary operators?
The Preprocessor and the Interpreter

General questions:

  • Could we separate preprocess and eval-exp into two files?
  • What are the tradeoffs on factoring preprocess and eval-exp into helper functions?
    • readability, extensibility
    • Can we go too far? (This piece is small.)
    • Can we not go far enough? (This piece is complex.)

The preprocessor:

  • "A few comments would make preprocess easier to read."
  • In preprocess, is there a way to reduce the number of recursive calls to preprocess?
  • What are the benefits and costs of using the let in preprocess?

The evaluator:

  • Why did you use a cond in eval-unary-op instead of if? You only have 2 options.
  • "Is there a more efficient way than a cond expression to find the operation to perform? Is there a way to store Racket operators in a list?"
  • Could we shorten the evaluation functions for operators...
    • ... by using map?
    • ... by using eval?
    • ... by using an operator/function environment?
  • Why do you have unreachable ~a error cases? "What could cause eval-huey-exp to reach that code?"
Tests
  • The second part of your tests have both a value (the expected answer) and some text. What does the text mean? Why does it not cause an error?
  • What are the final two tests checking for?

  • Can we test for more specific exceptions?
  • Can we create custom exceptions?
General 2
  • Decomposing functions into smaller functions makes the code easier to read. Does it add any run-time complexity?
  • Would we be build an interpreter in a similar way in Python or Java?
  • When will I be able to write code like this?

Wrap Up