Session 26
Building a Language Interpreter
A Quick Puzzle: Add with Memory
add.
add behaves like +, but it also
displays a running total of all the numbers it has seen.
For example:
> (add 1 1) ... running total: 2 2 > (add 5 7 11 13) ... running total: 38 36 > (add 100 (add 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 the function.
Can we name our function +?
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.
It turns out that Racket does protect its primitive
functions in ways that are different than user-defined
functions. That is valuable in making Racket suitable for
building large systems.
However, we can use features of Racket's provide
and require operators to achieve the goal of
renaming add to be +:
-
The client program
can use
requireto importaddwith the name+. -
We can use
provideto exportaddwith the name+.
In this case, the former seems a lot safer socially... An
inattentive client could clobber + inadvertently
with the latter.
Where Are We?
You are implementing a small language, Huey. For today, you did a short code review of a sample implementation. You will soon 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
Introduction
You asked a lot of questions in your code review. They were both interesting and challenging! Here is the summarized list of your questions that we went over in class. It is also included in today's zip file.
General 1
- Why are there all these files? Could they be in a single file?
- [ questions about language design ]
Utilities
-
Why are
list-indexanddisplaylnin 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 yourprovideclause? - 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-exprather than something that implies their function likemake-unary-exp? - Is it better to put the general type predicate at the beginning of the file or at the end?
- Why don't you type-check the constructors?
- Are there other possible unary operators?
The Preprocessor and the Interpreter
General questions:
-
Could we separate
preprocessandeval-expinto two files? -
What are the tradeoffs on factoring
preprocessandeval-expinto 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
preprocesseasier 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
letinpreprocess?
The evaluator:
-
Why did you use a
condineval-unary-opinstead ofif? You only have 2 options. -
Why not use
caseormatchinstead? -
"Is there a more efficient way than a
condexpression 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 ~aerror cases? "What could causeeval-huey-expto 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
- Read this discussion of the different stages of a language interpreter. It defines a lot of the vocabulary we use to describe interpreters and the design process.
-
Homework
- Homework 10 [LINK SOON] is available and due in one week.