Session 26
Building a Language Interpreter
A Quick Puzzle: Add with Memory
+
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 1340Note 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
anddisplayln
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 yourprovide
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 likemake-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
andeval-exp
into two files? -
What are the tradeoffs on factoring
preprocess
andeval-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 topreprocess
? -
What are the benefits and costs of using the
let
inpreprocess
?
The evaluator:
-
Why did you use a
cond
ineval-unary-op
instead ofif
? 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?
- ... by using
-
Why do you have
unreachable ~a
error cases? "What could causeeval-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
-
Reading
- Check out this discussion of the different stages of a language interpreter.
-
Homework
- Homework 10 is available and due in one week.