Remember to follow the pledge you read and signed at the beginning of the semester. For this assignment, you may consult any outside resources, including books, papers, web sites and people, you wish except for materials from previous cs1120, cs150, and cs200 courses. You may consult an outside person (e.g., another friend who is a CS major but is not in this class) who is not a member of the course staff, but that person cannot type anything in for you and all work must remain your own. That is, you can ask general questions such as "can you explain recursion to me?" or "how do lists work in Python?", but outside sources should never give you specific answers to problem set questions. If you use resources other than the class materials, lectures and course staff, explain what you used in your turn-in.
You are strongly encouraged to take advantage of the scheduled help hours and office hours for this course.
In this problem set, we provide a working interpreter for a mini-Python language, which is approximately a subset of Python. The interpreter implements the Python evaluation rules with state. Your assignment involves understanding the interpreter and making some additions and changes to it. If you are successful, you will produce an interpreter for our mini-Python language.
|
|
|
|
Our interpreter is divided into three different phases: tokenization, parsing, and evaluation. These roughly correspond to the main activities involved in reading English or Text Messages. For example, to understand the text message I<3U Alan you must:
To see how these different phases help us interpret a program, let's consider the following simple program in mini-Python:
x = 5 if x < 6: print 1 else: print 0In order to interpret this program, we need to read it in from a file as one long String. We can turn a file into a Java String using the loadFile function provided in Main.java:
String myProgram = loadFile("code/Example1");In tokenization stage, the interpreter breaks up the input string into smaller parts called "tokens". To tokenize a String, we use the tokenize method defined in the Tokenizer class. For this particular program, we would get the following list of tokens:
System.out.println(Tokenizer.tokenize(myProgram))); // [x, =, 5, NEWLINE, if, x, <, 6, :, NEWLINE, // TAB, print, 1, NEWLINE, else, :, NEWLINE, TAB, print, 0, NEWLINE]Breaking the input into pieces is a good first step, but it doesn't tell us much about the program we are trying to run. In the next phase, parsing, we use the grammar for our mini-Python language in order to build a more helpful structure: an abstract syntax tree (AST). We'll discuss more about abstract syntax trees later in the problem set. For now, an abstract syntax tree is just a fancy computer representation of those sentence diagrams you may have seen in elementary school.
In order to parse an input String, we use the parse method defined in the Parser class:
System.out.println(Parser.parse(myProgram)); /* [[assign, x, 5], [if, [<, x, 6], [[print, 1]], [[print, 0]]]] */Now we're starting to see the different components of the program take shape. The AST helps us understand the structure of the input program. The if statement, for example, is now represented as a list with four elements: the String "if", the condition, the code to execute if the condition is True, and the code to execute if the condition is False.
With the program parsed, we are now ready to run the program. Given an AST and an Environment object, the meval method in the Evaluator class will run (i.e., evaluate) the input program in the given environment:
// Create a new Environment to run our code Environment env = Environment.createGlobalEnvironment(); // Parse the program ArrayList<Object> ast = Parser.parse(myProgram); // Run the program Evaluator.meval(ast.get(0),env); // Evaluate x=5 Evaluator.meval(ast.get(1),env); // Evaluate the if statement // => 1
Our example program printed out 1, just as we expected. Since meval only works on one mini-Python statement at a time, we had to break our AST in to two parts and evaluate them separately.
Breaking the AST into pieces is a bit of a pain. To help out, Evaluator defines another helpful function evalSequence, which automatically evaluates all of the AST:
Environment env = Environment.createGlobalEnvironment(); ArrayList<Object> ast = Parser.parse(myProgram); Evaluator.evalSequence(ast, env); // => 1We will be running programs in this way quite frequently, so Main.java defines another helper function runRile, which does all of this for us:
runFile("code/Example1"); // => 1
It may be challenging to understand all of the parts of our interpreter the first time you see them. We will clarify each of the parts of the interpreter and provide more detail throughout the problem set.
def square(x): return x*x print square(4) print squareWe can run this mini-Python code in Main.java by typing:
runFile("code/ExampleSquare") // 16 // Interpreter.Procedure@6ba8fb1b
If you run this code on your own machine, the second printed line may be slightly different but should generally look the same.
Hint: To answer this question you may need to read through the PS7 code and/or try out that same program in PyCharm/Python.
def factorial(x): #Your code here print factorial(5)
When you've done it correctly, you should get the following output (when run in Main.java):
runFile("code/YourFactorial") // => 120
Copy down (or print out) the YourFactorial code you wrote and turn it in with the written part of this assignment.
Hint: Review the mini-Python grammar. For example, mini-Python requires an else: for every if.
The Parser takes a sequence of tokens from the tokenizer (as a TokenStream, which is really just an ArrayList of strings), and attempts to derive them using the rules of the grammar for our mini-Python language (just like you did in Problem Set 3). If the sequence of tokens is in the language of the grammar, our Parser produces a nice tree structure, which exactly diagrams the structure of our program.
To see how this works, let's see how our interpreter parses if statements (recall that mini-Python requires an else: branch for each if). In the Parser class, we define the method parseIf as follows:
public static ArrayList<Object> parseIf(TokenStream tokens) { // Make a new ArrayList to store our parsed If statement ArrayList<Object> retval = new ArrayList<Object>(); // Make "if" the first element of our output list retval.add("if"); tokens.munchAssert("if"); // Parse the condition retval.add(parseExpression(tokens)); // Parse the true clause tokens.munchAssert(":"); retval.add(parseSequence(tokens)); // Parse the false clause tokens.munchAssert("else"); tokens.munchAssert(":"); retval.add(parseSequence(tokens)); return retval; }The goal of parseIf is to take a TokenStream tokens and transform it into an ArrayList<Object> where the elements have the following format:
["if", condition, true-clause, false-clause]
This is done by reading off strings from the TokenStream. A TokenStream is a subclass of ArrayList<String>, which means that any methods you can use on an ArrayList, you can also use on a TokenStream. The TokenStream class also defines a few extra helpful functions:Now that we know how TokenStream works, let's see what happens when we run parseIf with a subset of the tokens from our earlier example:
["if", "x", "<", "5", ":", "INDENT", "print", "1", "DEDENT", "else", ":", "INDENT", "print", "0", "DEDENT"]First, parseIf makes a new ArrayList<String> to store the parsed if statement. It then uses the line tokens.munchAssert("if"); to ensure that the current token is indeed "if". Since munchAssert removes the first token, our list of tokens now has the value:
["x", "<", "5", ":", "INDENT", "print", "1", "DEDENT", "else", ":", "INDENT", "print", "0", "DEDENT"]We now need to consult the grammar rule for if statements to know what to do next. In mini-Python, if statements have the following rule:
IfStatement := if Expression : Sequence else : SequenceAccording to this rule, since we've just seen the "if" token, we should now expect an Expression representing the condition of the if statement. An Expression is just a snippet of mini-Python code that returns a value like 1+1, (1+x)<y, etc. Since we expect an Expression, we call the parseExpression method. In this case, parseExpression will return ["<","x","5"] representing our original mini-Python expression x<5. At this point, tokens has the value:
[":", "INDENT", "print", "1", "DEDENT", "else", ":", "INDENT", "print", "0", "DEDENT"]Next, the line tokens.munchAssert(":"); ensures that : is the next token and removes it. Our grammar rule now tells us that we expect a Sequence, so we call the appropriately named parseSequence method, which will munch the next four tokens and return the list [["print", "1"]]. We then repeat this process for the false-clause, which is also a Sequence.
After performing all of these calculations, evalIf returns the following list:
["if", ["<", "x", "5"], [["print", "5"]], [["print", "6"]]]]
which represents the abstract syntax tree for our if statement. Note that you do not need to understand how parseSequence and parseExpression work, but you may refer to their commented definitions in Parser if you are curious.
i = 1 while i <=10: print i i = i + 1Here is the same code in Java:
int i = 1; while(i <= 10) { System.out.println(i); i = i + 1; // or i++; }In our code, we initialize an integer i to 1. Our condition is i <= 10 and in our body we print out i and then increments it. It is very important that we increment i at the end of the body. If we don't, then we will have an "infinite loop" where mini-Python will continue evaluating the while loop without stopping. It would be bad to cause such an infinite loop.
In the next few problems, we will add support for while loops to mini-Python. We will do this in two parts. We first have to add the while loop to the Parser, then we will add the code that actually runs the while loop in the Evaluator.
WhileStatement := while Expression : SequenceYour implementation of parseWhile should produce an ArrayList<Object> with the following format: ["while", condition, body].
Hint: Your code for parseWhile will likely look very similar to the code parseIf, so that may be a good place to start.
Once completed, you should be able to get the following results by running this code in Main.java:
System.out.println(Parser.parse(loadFile("code/TestQuestion3"))); // => [[while, [<, i, [-, max, 1]], [[print, i], [assign, i, [+, i, 1]]]]]where code/TestQuestion3 has the following mini-Python code:
while i < max-1: print i i = i + 1
public static Object meval(Object expr, Environment env) { ... ArrayList<Object> exp = (ArrayList<Object>) expr; // To keep things simple, the first element of // the list is always the operation. That's why we // went to the trouble of representing "1<2" as ["<","1","2"] Object operation = exp.get(0); // Function Definitions if (operation.equals("def")) { return evalDef(exp,env); } // If Statements else if (operation.equals("if")) { return evalIf(exp,env); } ... // Multiplication else if (operation.equals("*")) { return evalMult(exp,env); } ... // Less Than else if (operation.equals("<")) { return evalLessThan(exp, env); } ... }meval takes in a statement or expression as an ArrayList, determines what kind of statement or expression it is, and then calls the appropriate "eval" function to evaluate that statement or expression. In this way, meval is like a switchboard for all of our methods in Evaluator.
throw new EvalError("error_message");and replace "error_message" with a good description of the error.
Note that for these last two examples, we cannot immediately compare two numbers, since we do not know the value of x or (x*y/2) right away. To determine the value of these expressions, we need to first evaluate them using meval. After evaluating both expressions, we should have two integers, which we can then compare and return True or False accordingly.
This is demonstrated in the evalLessThan method included below, which evaluates ComparisonExpressions that have the format ["<", expression1, expression2] :
public static Boolean evalLessThan(ArrayList<Object> exp, Environment env) { // Evaluate the two expressions we'll be comparing Object v1 = meval(exp.get(1),env); Object v2 = meval(exp.get(2),env); // Check that both values are indeed integers if (v1 instanceof Integer && v2 instanceof Integer) { // Perform the actual calculation return (Integer)v1<(Integer)v2; } // Throw an error if we try to compare non-Integers throw new EvalError("Cannot compare " + v1 + " and " + v2); }Even though we are expect that both expressions that we are comparing should be Integers, we double check their type anyway, and throw a convenient error message in the event that they are not both Integers.
Your method should evaluate the two expressions in the ArrayList, check to make sure they are integers, and then return true if the first integer is less than or equal to the second integer (and false otherwise). If the two expressions do not both evaluate to Integers, you should throw an EvalError.
Note that since your code is in the YourCode class rather than the Evaluator class, methods like meval(...) need to be written as Evaluator.meval(...).
Once completed, you should be able to get the following results by running code in Main.java:
runFile("code/TestQuestion4"); // => True, True, False, Falsewhere code/TestQuestion4 has the following mini-Python code:
def return6(): return 6 x = 5 print (x <= 5) print (x <= (return6())) print (x <= (return6()-2)) print (10 <= (10-20))
Using this information, you should write code that runs the body of the while loop for as long as condition evaluates to true (this means you will have to re-evaluate the condition each time you run the body). If the condition does not evaluate to a Boolean at any point, you should throw an EvalError.
Note that when testing this, you can assume that loop bodies contain no mini-Python return statements. This means that your evalWhile method should always return null.
Hints: You will probably have to use a Java while loop for this (although you could also use Java recursion). Also, recall that Evaluator.meval works only on a single statement or expression, while Evaluator.evalSequence works on a list of statements. Looking at evalIf might also help you write this method.
runFile("code/TestQuestion5"); // => 0, 0, 1, 0, 1, 2where code/TestQuestion5 has the following mini-Python code:
def looptest(x): i = 0 sum = 0 while (i < x): j = 0 while j < i: print j j = j+1 i=i+1 looptest(4)
Our implementation for parseElement is based on the mini-Python grammar rule for list element accesses:
ElementExpression := PrimitiveExpression ElementAccessNote that this rule allows you to perform element accesses on nested lists. For example, the following is valid mini-Python code according to this production rule:
ElementAccess := [ Expression ] ElementAccess | ε
x = [[1,[3,4,5]],[2,3],[7,9]] print x[0][1][1] # => 4
Hint: You may want to look at parseList in Parser to get a feel for what your rule should look like.
Hint: You may want to use some sort of loop for this problem.
Once completed, you should be able to get the following results by running code in Main.java:
runFile("code/TestQuestion7"); // => [1,2,3], [2,4,3,-5], 6where code/TestQuestion7 has the following mini-Python code:
print [1,2,3] print [1+1,2*2,9/3,5-10] print |[1,2,3,4,5,6]|
Note that the vertical bar syntax |x| in mini-Python is equivalent to len(x) in normal Python. You will not need to implement anything related to this syntax in this problem set.
You should evaluate both the list_expression and index_expression, which should return an ArrayList and Integer, respectively. Your code should then return the appropriate element from the ArrayList. You should produce an EvalError in the following three cases:
Note that mini-Python does not support the list[1:] syntax.
Hint: In order to check if an object obj is an ArrayList, use the syntax: (obj instanceof ArrayList<?>). To make Java happy you actually do need to type a question mark ? there, not the word Object or anything else.
Once completed, you should be able to get the following results by running code in Main.java:
runFile("code/TestQuestion8"); // => 9, 1, 7, 3, 2, 5where code/TestQuestion8 has the following mini-Python code:
myList = [9,1,7,3,2,5] i = 0 while i<|myList|: print myList[i] i = i+1
Once completed, you should be able to get the following results by running code in Main.java:
runFile("code/TestQuestion9"); // => True, False, True, False, Truewhere code/TestQuestion9 has the following mini-Python code:
print [1, 2, 3] == [1, 2, 3] print [1, 2] == [1,2,3] print [[1,2], True] == [[1,2], True] print [5] == False print [[]] == [[]]
pleased = "What are you most pleased about regarding this course?"
displeased = "What are you most displeased about regarding this course?"
hopetolearn = "What did you most hope to learn in this course that we have not yet covered?"
tacomments = "Who is your favorite TA, and why? What have the TAs done well? What could the TAs do better?"