CS 214: Programming Languages
Spring 2009

Home|Syllabus|Schedule
<<|>>|ANTLR API|CITkit API|PolyD API|EasyMock API

Your First Interpreter
Interpreters, Iteration 2

The First User Story

Now you get to your first interpreter. Strictly speaking, this interpreter is really boring in terms of what it does (and you'll spice it up only so much); but it's a surprising amount of work just to get this simple interpreter going. Fortunately, these are one-time startup costs, and future iterations will have you do a lot more work with the actual interpreter.

For each of these iterations, I will motivate the work with a user story (or stories). It's a common way for a client to explain the requirements of the software you need to write. In a later iteration, you'll start using a tool which will explicit specify these user stories in tests.

Let's start by checking out some user stories.

User Stories

It helps to see the complicated stories first so that you know the inspiration for the smaller ones.

User Story #5: Interpreter evaluates a two-value sum.

A Hobbes program can be the addition of two values (any combination of integers and strings).

Examples: 2 + 3, "hi " + "there".

User Story #4: Interpreter evaluates a string.

A Hobbes program can be a single string in double quotes. The interpreter prints the value of the string without the quotes.

User Story #2: Interpreter evaluates an integer.

A Hobbes program can be a single integer. The interpreter prints the integer.

The story number corresponds to the iteration where you'll deal with the corresponding story.

Two Most Basic Phases of Interpretation

So for this iteration, you need an interpreter that reads in an integer and prints it out again. Of course, you could just use UNIX cp and call it a day, but that won't work well in the long run.

Interperters and compilers are usually split into a front end and a back end. The front end handles in the input of the program and turns it into a data structure that makes processing it easy. The back end does the actual processing.

I've already written a front end for you in the Hobbes FrontEnd library. So you're job will be to provide the back end.

Getting Set Up

In your interpreter project:

Create a new package in the src folder called edu.calvin.username.cs214.interpreters.hobbes. (As you might imagine, replace username with your username.)

Create the same package in the tests folder.

Interpretation

You're actually going to write two interpreters for this same story so that we can do a compare and contrast with two different programming paradigms.

  1. In the src/.../hobbes package, create classes ProceduralHobbesInterpreter and OOHobbesInterpreter.
  2. In the tests/.../hobbes package, create classes ProceduralHobbesInterpreterTest and OOHobbesInterpreterTest.

A standard test-case class has at least these three things:

Initialization in a Test-Case Class

The first two things are accomplished like so:

private ProceduralHobbesInterpreter myInterpreter;
@Before
public void setUp() {
  myInterpreter = new ProceduralHobbesInterpreter();
}

Do this in both test-case classes. (Don't forget to copy-and-paste-and-tweak!)

The @Before annotation tells JUnit to run this method before each test method; setUp() is an old, traditional name. Generally, the purpose of this method is to initialize your instance variables. Since this method is called before each test method, each test method will have a fresh instance to play with.

Define enough of the interpreter class to get this code to compile.

If you run your unit tests, this class will fail because you don't have any actual tests yet.

Planning the Tests

My Hobbes FrontEnd library will generate at least three different types of output from a program. You're most interested in just two of these: ASTs from the parser and TIRs from the TIR builder.

An abstract syntax tree (AST) is a plain, tree representation of the program. A tree intermediate representation (TIR) is an object-oriented version of the same tree.

The trees will not have any interesting structure, but there will be some work building them up (especially the ASTs).

ASTs are created using the ANTLR library:

new CommonTree(new CommonToken(HobbesLexer.INTEGER, "5"))

This create a new AST with a root node (and only a root node) consisting of a integer token with the (string) value "5". HobbesLexer.INTEGER comes from my Hobbes FrontEnd library; all tokens from the front end use strings to represent the actual values.

TIRs are a bit simpler:

new IntegerETIR(5)

"ETIR" stands for "expression TIR". (This makes a difference for things like arrays which can be an expression, a type, or a declaration.)

These are the two inputs for the two interpreters (for new interpret() methods). The only other question is the return type. For a little bit of contrast, you'll return Strings for now.

Procedural Interpretation

Tests

Here's a test for ProceduralHobbesInterpreter:

@Test
public void shouldInterpretIntegers() {
    Tree tree = new CommonTree(new CommonToken(HobbesLexer.INTEGER, "5"));
    assertEquals("interpreting the integer 5", "5", myInterpreter.interpret(tree));
}

99% of the time I don't know exactly where the classes that I want to import are. Eclipse knows for me! Check out Source -> Organize Imports....

Add this to the appropriate test-case class. Import the necessary classes. You should get a compilation error for interpret().

In general, an assertion looks like this:

assertEquals(message, expected, computed);

The message is basically a comment for anyone reading the code or error output (when the test fails). expected is the result you expect; computed is the value you compute.

If you write the method yourself, you're doing too much work. Two words: quick fix! Put your cursor on the offending code, and press ctrl-1 (cmd-1 on a Mac).

Define just enough of ProceduralHobbesInterpreter#interpret() to make the compiler happy. Remember to set the return type to String; use Tree as the parameter type.

Again, define just enough code so that you can honestly run your tests and see that the proper test is failing. I find this very helpful:

throw new IllegalStateException("unimplemented!");

It's great for methods where the compiler wants me to return something and I'm not ready to return anything.

Run your tests for a red bar.

The fix for this one test is trivial: return "5". No, really, the literal string "5". That's all the test requires.

It seems like cheating, but it does allow my tests to pass with a green bar.

Fix the code as directed, and run for a green bar.

Refactoring Interlude

You're going to need to create two more ASTs with different integers in them. Consider how much code it takes to create just one AST. Do this three times, and you'll have a lot of code that looks the same. Agile developers call this a violation of the DRY Principle—Don't Repeat Yourself Principle.

Let's play around with Eclipse's Refactor menu to simplify the current test and make more assertions easier.

You will know pain if you only use the menus. Learn the keybindings! Your life will never be the same!

  1. Select the "5" in the call to the CommonToken constructor. Select Refactor -> Extract Local Variable.... Enter integer as the variable name.
  2. Re-run the tests for a green bar.
  3. Now select the whole new CommonTree(...) expression. (Try out Edit -> Expand Select To -> Enclosing Element a couple of times.) Select Refactor -> Extract Method..., and type createIntegerAST as the method name.
  4. Re-run the tests for a green bar.
  5. In the test method, inline both integer and tree using Refactor -> Inline....
  6. Re-run the tests for a green bar.

A refactoring changes the code without changing the end result. Managers hate it in the short run because it brings no immediate business value, but developers love it because it's fun to do and makes adding new features and debugging old problems that much easier.

But don't refactor without tests.

More Assertions

Write two more integer assertions in shouldInterpretIntegers(). Run the tests for the red bar.

CommonTrees have a getText() method which returns the string value of the root node.

Fix interpret(CommonTree). Run the tests for a green bar.

OO Interpretation

Write a shouldInterpretIntegers() method in OOHobbesInterpreterTest following (almost) the same process as above.

A couple differences: