This project implement an interpreter for version 2 of Trefoil, which supports nested syntax based on parenthesized symbol trees (PSTs).
This tour is careful to discuss every file, so might want to only skim this tour on first reading.
made it into the hw3/ subdirectory. Here's what see.
README.md: this file, the guide to HW3LANGUAGE.md: the specification for Trefoil v2FEEDBACK.md: our usual questions about homework, to fill out after you're donesrc/: Java files implementing the interpretertst/Trefoil2Test.java: Unit tests for the interpreter
In src/ there are several files.
trefoil2/Trefoil2.java: main entry point of the interpreter- Skim the main method. We won't ask change it, but it might be useful for debugging (see comments)
- Similar to HW1,
mainsupports either keyboard or file input - It constructs a
PSTParserand then repeatedly reads PSTs off the input - For each PST, it parses it as an AST using
Binding.parsePST, and then interprets the binding. - This is the semantics of Trefoil v2 programs!
- At the end of the program, main prints the final environment.
- Similar to HW1,
- This file also declares
TrefoilErrorand its several subclasses, which need to throw, as well asInternalInterpreterError, which might need to throw
- Skim the main method. We won't ask change it, but it might be useful for debugging (see comments)
- Several files in the
parser/package implementPSTParserthat do not need to understand or edit. Of course, are free to read these files if are curious how they work. They are not very complicated.PeekCharReader.java: buffers characters one at a time while tracking line numbersTokenizer.java: splits the input into parentheses, comments, and symbolsPSTParser.java: constructs a PST from a token stream
- need to understand and edit the files in the
trefoil2package. ParenthesizedSymbolTree.java: represents a PST- need to understand and use (but not edit) the
SymbolandNodeclasses.
- need to understand and use (but not edit) the
Expression.java: represents the abstract syntax tree (AST) of an expression- For every kind of expression in the language, there be a inner public static subclass of
Expression, similar to the examples likeIntegerLiteral,Plus, etc. in the starter code. - We ask to use the existing Expression subclasses as well as add several new ones.
- The method
parsePSTconverts (or tries to convert) a PST into an expression AST.- For each subclass of
Expressionthere should be a case inparsePSTthat produces it from the corresponding PST. - Check out the examples in the starter code of symbol keywords and node keywords. We ask to add more of each kind of keyword as produce new ASTs that add.
- For each subclass of
- For every kind of expression in the language, there be a inner public static subclass of
Binding.java: represents the AST of a top-level binding- For every kind of binding, there is a subclass of
Binding. - need to understand the existing bindings. Start with variable bindings. Ignore function bindings until later in the assignment..
- We ask to add one new top-level binding.
- For every kind of binding, there is a subclass of
Interpreter.java: the interpreter!!- Interprets expressions and bindings in the context of a dynamic environment.
- need to understand the starter code in this file thoroughly. the primary task on HW3 is to extend it.
interpretExpressionevaluates an expression to a value in a given dynamic environmentinterpretBindingtransforms and returns a new dynamic environment as the result of executing the given binding- extend both of these functions with several new cases.
- This file also defines the
DynamicEnvironmentclass, which implements a map from strings to "entries". implement a few of its methods.- Variable names map to their values
- Function names map to a pair of their function binding and their defining environment.
The starter code makes fairly extensive use of @Data and @EqualsAndHashcode
from Project Lombok. These annotations
automatically generate tedious methods such as toString, equals, and
hashCode, as well as getter and setter methods. We think mostly won't
have to understand how this works at all. But have to copy these
annotations on to the own AST classes that add. We have included a copy of
Lombok in the dependencies of the starter code, and modern versions of IntelliJ
have native support for Lombok.
A Trefoil v2 program is a sequence of bindings which are evaluated in order in a (initially empty) dynamic environment. The environment can be transformed by each binding as that binding executes.
There are four kinds of bindings: variable bindings, top-level expressions, test bindings and (part 2) function bindings.
We have discussed all but test bindings in lecture. See the slides and demo code
for an informal description of those. See LANGUAGE.md for a formal description.
Test bindings are written like this example:
(test (= 3 (+ 1 2)))
A test binding evaluates its expression. If expression evaluates to true, the test passes and nothing happens to the environment. If the expression evaluates to anything other than true (false or any other value, e.g., 17), the test fails and a (nice) error is reported to the Trefoil programmer.
There are 16 kinds of expressions. See the lecture material for an informal
introduction and LANGUAGE.md for a formal list and description.
The only expressions we did not discuss in lecture are:
- Equality on integers, written
(= (+ 1 2) 3).=is a binary operator that takes two integers, just like+does, except that=returns a boolean indicating whether its arguments are equal. Note that=only works on integers. It is a runtime error to pass anything besides an integer to=. - Checking if a value is a pair, written
(cons? (+ 1 2)).cons?is a unary operator similar tonil?that returns true if its argument is a pair. Unlikenil?, which returns true only onnil, there are many different values on whichcons?returns true: for example,(cons 0 1),(cons true (cons false nil)), etc.
Complete the starter code to implement the Trefoil v2 language as specified in
LANGUAGE.md. Write tests that cover the normal and error case behavior for all
language features.
We have left several TODOs for throughout the code. can use IntelliJ's
TODO tab to keep track of them all easily.
Familiarize with the provided files:
- Skim this file without looking at the starter code.
- Read the tour more carefully and poke around at the starter code files so you know where too look later.
- Press the "Build" button in IntelliJ and ensure there are no errors.
- Run the tests in
Trefoil2Test.javaand ensure that 33 fail and 8 pass. (!) The passing tests are due to features we have implemented for in the starter code. - Look at
LANGUAGE.mdto get an idea of the list of expressions and bindings have to add.
Implement new expressions:
- Start with
true. The AST node is already defined (BooleanLiteral), andExpression.parsePSTis done for in the starter code. All that's left is to implement the case inInterpreter.interpretExpression.- Hint: it is similar to the case for
IntegerLiteral - Hint: the autogenerated getter method for the
datafield onBooleanLiteralis calledisData, which is confusing, but whatever. - Ensure that
Trefoil2Test.testBoolLitTruepasses.
- Hint: it is similar to the case for
- Now implement with
false.- The AST node is the same as
true, just with a differentdatafield value. - This time, have to implement the case in
Expression.parsePST.- Hint: it is similar to the case for
true. - Ensure that
Trefoil2Test.testBoolLitFalseParsingpasses.
- Hint: it is similar to the case for
- Make sure the interpreter handles
falseas well. Depending on how
handledtrue, may not need any code changes. - Ensure that
Trefoil2Test.testBoolLitFalsepasses.
- The AST node is the same as
- Implement better error handling in
Interpreter.interpretExpressionfor+.- Ensure that
Trefoil2Test.testPlusTypeErrorpasses.
- Ensure that
- Implement
-and*. They are similar to+.- First, define their AST classes by copying
Plusand renaming it.- Be sure to include the
@Dataand@Equals...annotations, and to extendExpression, or get very confusing bugs later.
- Be sure to include the
- Next, extend
Expression.parsePSTwith cases for-and*.- Write tests analogous to
testBoolLitFalseParsingbut for-and*.- can compare the pared result with a manually constructed AST by
calling
new Minus(Expression.ofInt(3), Expression.ofInt(17))or whatever. - Ensure these tests pass.
- can compare the pared result with a manually constructed AST by
calling
- Write tests analogous to
- Finally, extend
Interpreter.interpretExpressionwith cases for-and*.- Ensure that
testMinusandtestTimespass.
- Ensure that
- Write error tests for minus and times that check for incorrectly typed arguments,
and ensure the new tests pass.
- Be sure are handling type errors for these operators just like are for
+. must throwTrefoil2.TrefoilError.RuntimeErrorand not anything else.
- Be sure are handling type errors for these operators just like are for
- Write error tests for minus and times that check what happens when too few or too many arguments are given. Ensure they pass.
- First, define their AST classes by copying
- Implement
=following the workflow below.- Remember that this operator only works on integers!
- The interpreter should throw
TrefoilError.RuntimeErrorif=is given anything other than integers.
- The interpreter should throw
- Workflow for all new expression forms
- Be sure understand the syntax and semantics as specified in
LANGUAGE.md. - Declare new AST class
- Write new case in
Expression.parsePST - Write a test for the new case in
parsePSTby comparing it to a manually constructed AST, and ensure the test passes. - Write new case in
Interpreter.interpretExpression. - Ensure that the provided normal case test for this expression passes (if any).
- Write (additional) normal case tests to cover all behavior of the feature.
- Write error case tests for the feature covering the following:
- wrong type of data passed as argument (should throw
TrefoilError.RuntimeError) - wrong number of arguments passed to primitive operation (should throw
TrefoilError.AbstractSyntaxError)
- wrong type of data passed as argument (should throw
- Be sure understand the syntax and semantics as specified in
- Remember that this operator only works on integers!
- Implement
iffollowing the workflow.
Implement the missing methods about variables in DynamicEnvironment:
- Refamiliarize with the starter code for
DynamicEnvironment - Fix the first
TODOinDynamicEnvironment.getVariableand ensuretestVarNotFoundpasses. - Fix the second
TODOinDynamicEnvironment.getVariable. - Implement
DynamicEnvironment.putVariableand ensuretestPutVariable - After implementing
getVariableandputVariable, ensure the teststestVarandtestVarBindingLookuppass.
Implement the new test binding:
- Implement the
testbinding using this modified workflow.- Since
testis a binding not an expression:- Declare the new AST class as a public static inner subclass of
Binding. - Extend
Binding.parsePSTinstead of the similar method for expressions - Extend
Interpreter.interpretBindinginstead of the similar method for expressions
- Declare the new AST class as a public static inner subclass of
- Ensure the
testTestBinding...provided tests pass - Still write a
parsePSTtest - Write any additional normal case or error case tests think are appropriate and ensure they pass.
- Since
Finish implementing the other expressions (except function calls):
- Implement
letfollowing the workflow.- Hint: use
extendVariable, which callsputVariablefor on a copy of the environment. If callputVariabledirectly, it is too easy to forget to make a copy. - Ensure the
testLet...tests pass.
- Hint: use
- Implement
nil,cons,nil?,cons?,car, andcdrfollowing the workflow for each.- For
nilandcons, fix the TODOs in the factory methodsnil()andcons()inExpression.java. - No provided tests for
nil?andcons?. should write both normal and error case tests for these.
- For
Implement the missing methods about functions in DynamicEnvironment:
- Implement
getFunctionandPutFunctioninDynamicEnvironment.- Write normal case tests for these and ensure they pass.
Implement functions:
- Function bindings are implemented for in the starter code.
- The hardest part is actually parsing them. don't need to understand this.
- The easy part is interpreting them. We just call
extendFunctionon the dynamic environment. Notice that this saves the defining environment as well viaputFunction.
- The job is to implement function calls (which are expressions) following the expression workflow.
- Don't forget to evaluate the function body in an extended version of its
defining environment, not the environment of the caller!
- This implements lexical scope.
- Don't forget to evaluate the function body in an extended version of its
defining environment, not the environment of the caller!
Implement other own feature:
- Design and implement a new expression or binding.
- Spend as much or as little time as want on this. It's fine if the feature is super simple. If can't think of any ideas, make a post on Ed, and we help brainstorm.
- Write normal and error case tests for the feature
updated the root Makefile with the following targets:
makeormake allcompile the projectmake runrun Trefoil v2 interpreter interactivelymake testrun the Trefoil v2 unit tests