Project 3 - Calculator Language Interpreter
Due by 11:59pm on June 7, 2025
Example Input and Output
python3 calculator.py
Welcome to the CS 111 Calculator Interpreter.
calc >> (+ 5 6)
11
calc >> (* 5 (+ 4 2))
30
calc >> exit
Goodbye!
Objectives
- Build a fully functional calculator language interpreter.
Starter Files
Download project03.zip. Inside the archive, you will find the pair.py library and test files for this project.
Introduction
In this project you will build an interactive interpreter for the Calculator language that we have discussed in class.
The interpreter will allow the user to enter valid Calculator expressions, in
which case it will return the value of the expression, or the word exit
in
order to end the interactive session. If an invalid expression is entered, it
will print an error and then prompt for another expression.
Valid Calculator expressions are of the form:
(<operator> <operand 1> <operand 2> [...])
where the operator is one of the valid operator symbols (+
, -
, *
, and /
)
and the operands are the values to be operated on. All operators require at
least two operands but can accept more.
When more than two operands are provided for a given operator, the operation is
applied between the first and second operand, then it is applied to the result
of the previous operation and the next operand, and so on until all the operands
are used. For example, the expression (+ 3 4 5 6)
evaluates to $((3+4)+5)+6 =
18$ while the expression (/ 20 4 5)
evaluates to $(20/4)/5 = 1$.
You will be writing nearly all of the interpreter from scratch but we will walk you through the process in this project. For grading purposes, you will need to implement the function names exactly as specified with the specified argument list. The names you give to the arguments are up to you but they should make sense.
Part 1 - The Interactive Loop
We’ll start by setting up the interactive part of the interpreter that prompts the user for input and displays the results. We’ll then add in the code to parse the input and evaluate it in later parts of the project. Let’s get started.
Task 1 - Getting Started
Start by copying your calculator.py file from
Homework 6. If you haven’t done it yet, add in our
if __name__ == "__main__"
statement to allow it to run as a program. Again,
you can create a main()
function or just write the main part of the program
below that if
statement.
Add code to your program that will print a single line with the following greeting:
Welcome to the CS 111 Calculator Interpreter.
Add a comment for yourself that the main loop should follow that greeting. We’ll come back and fill in that main loop in the next task.
Add code that prints only the following farewell message:
Goodbye!
If you run your program now, it should just print the greeting followed by the farewell.
Task 2 - The Main Loop
For this task, only implement steps 1, 2, 3, 7, & 8. For now, in step 7, you should just print out the string containing the expression the user supplied. That will change as you implement more of the interpreter.
The main loop of your program should do the following:
- Print a prompt. The cursor should remain on the line with the prompt after
printing. For this project your prompt should be
calc >>
. The autograder will be looking for this prompt in the output so be sure you get it exactly correct with the space between the word ‘calc’ and the greater than symbols and a space after the greater than symbols. - Read input from the user. The program should read the entire line as a single string to be parsed.
- If the supplied string is the word
exit
, the program should stop. - Parse the supplied string and validate that it is a valid expression.
- If not, print an error and return to step 1.
- Evaluate the valid expression to get the result
- Print the result
- Return to step 1
Try running the program now. It should print the greeting and give you the
calc >>
prompt and print out anything you type until you type exit
at which
point the program should print the farewell and end.
Now that the interactive loop is done, it’s time to actually start processing the input.
Part 2 - Parsing the Input
In this part of the project, you will write code that will parse the input from the user, validate that it is correct, and build a syntax tree that we can evaluate to get the value of the provided expression. If there are problems we will report the error to the user via exceptions. These are steps 4 & 5 from the main loop. You’ve already written most of the code for steps 4 and 5 as your work in Lab 17 and Homework 6.
Task 1 - Getting the Token List
This task generates the token list from the user supplied expression. All you
need to do is add code to your main loop to call your tokenize()
function you
wrote in Lab 17. Modify your print code (step 7) to print the
token list instead of the user supplied string.
Task 2 - Creating the Expression Tree
You wrote most of the code for this task as your parse_tokens()
recursive
function in Homework 6; however, that function takes as
input both the token list and a starting index, and it also returns both the
expression tree and the next index to process. The code calling that function is
always going to pass in 0
as the starting index and has to worry about
catching the final index value that comes out; however, that index is really an
implementation detail that other programmers should not have to consider when
calling parse_tokens()
.
To hide that detail, you are going to write a parse()
function that hides the
implementation. Your parse function will simply take the token list as input and
return the final expression tree (a Pair
object).
def parse(tokens):
You could write this as a separate function that calls your parse_token()
function.
def parse_token(tokens, index):
# parse_token code here
def parse(tokens):
# code that uses parse_token()
Or you could make parse_token()
an inner function that is defined inside the
parse()
function.
def parse(tokens):
def parse_tokens(tokens, index):
# parse_token code here
# code that calls parse_token()
whichever you choose is up to you.
With the parse()
function written, we can call it in our main loop. Add code
to your main loop to pass the token list created by tokenize()
and capture the
Pair
object that is the root of our expression tree that comes out.
Since parse()
, by way of the recursive function you wrote, can throw
exceptions, you need to make the call in a try/except
block. Modify step 7 so
that if an exception is thrown, the program prints out the error. If not, the
program should print out the Pair
object returned. Pair
has a __str__()
function so you can just have your loop print that object instead of the token
list string.
Try some simple examples and make sure it’s working.
Part 3 - Evaluating the Syntax Tree
It’s now time to do the final steps (6 & 7) from the main loop, evaluating the expression and printing the results.
There are three parts to this process and we’ll tackle them in turn.
Task 1 - The reduce()
Function
Since the operators can have any number of operands instead of just two, we need a function that will apply the operator to any number of operands.
Create a reduce()
function, that takes as input a function to apply, a Pair
object that is the start of a list of arguments to apply the function to, and an
initial value to start with.
def reduce(func, operands, initial):
The passed in function should take two arguments and return a single value as a
result. The function we will be passing in are the math functions add
, sub
,
mul
, and truediv
. Import each of these functions from the operator
library. (This library comes preinstalled with Python.)
The reduce()
function should traverse the pair list, applying the function to
all the values in the pair list starting with the initial value. For the first
value in the operands
list, call the function with the initial value and the
list value and capture the return value. For all later list elements, apply the
function to the current list element and the result from the previous step. Once
all elements have been processed, return the final result.
from operator import add, sub, mul, truediv
operands = Pair(3, Pair(4, Pair(5, nil)))
the function call
reduce(add, operands, 0)
would return 12. It would call add(0, 3)
and get 3, then call add(3, 4)
and
get 7 and finally call add(7, 5)
and get 12.
Task 2 - Applying the Operators
Next up is our apply()
function that does the correct operation based on the
operator in the expression. Since the calculator language only has four
operators, this is a fairly straightforward function. You can imagine that if
you had a more complex language, this function have a lot of different parts.
The apply()
method takes two arguments: a string containing the operator and a
Pair
object that is a list of the operands to apply the operator on.
def apply(operator, operands):
For each operator (+
, -
, *
, and /
), you should call your reduce()
function with the appropriate arguments. For addition and multiplication, the
values are all added or multiplied together, so you can just pass the entire
Pair
object to the reduce()
function directly along with the appropriate
initial value. For division and subtraction, the initial
value should be the
first operand of the Pair
list, and the list passed to reduce()
should begin
with the second operand.
If the operator passed in is not one of the allowed operators, the function
should raise and TypeError
indicating that the operator is invalid.
Task 3 - Evaluating the Syntax Tree.
Now that we can process the operators, it’s time to tackle the full evaluations.
This will be done in our eval()
function that takes as its argument an
expression represented by a Pair
object that is the root of the syntax tree.
Since our syntax tree is a recursive structure, this will be a recursive
function as well.
def eval(syntax_tree):
The algorithm for evaluation is actually simpler than the one for parsing. Here is what the function needs to do:
- If the expression is a primitive (
int
orfloat
), we just return the value - If the expression is a
Pair
object it is some sort of expression- Get the operator from the
first
item of thePair
object - Form a
Pair
list from the remaining operands in the list- Call
eval()
on eachfirst
item in the rest of thePair
object - Hint: You can use a loop to traverse through the
Pair
object and update each object.
- Call
- Call the
apply()
function passing in the operator and the results of step 2.2 - Return the results of step 2.3
- Get the operator from the
- If the expression is not a primitive or a
Pair
, raise aTypeError
exception
Just like with the parsing algorithm, you should probably work through some simple examples by hand to make sure you understand what is happening. You should also write some doctests or pytests to verify that things are working properly.
Task 4 - Printing the Results
Now that we can evaluate the syntax tree, it’s time to add that into our main
loop. Add code that takes the results of the parse()
method and calls your
eval()
method. Again, this can throw exceptions so it should be inside the
try/except block that you added earlier. If no exceptions are raised, your loop
should print the value returned by evaluating the expression. If there was an
exception, it should print the exception message.
And that’s it. You’ve written a complete interpreter for the Calculator language.
Turn in your work
You’ll submit your calculator.py file on Canvas
via Gradescope where it will be checked via the auto grader. We’ll run a series
of tests that test the parse()
, reduce()
, apply()
, and eval()
functions
so make sure that they have the correct names and arguments. We’ll also run your
full program and pass it input to verify that all is working.
Going Further
Now that you have a basic interpreter, you can add additional functionality. You might consider adding any of the following operators to your interpreter:
//
- integer division%
- the modulus operatorsqrt
- the square root operatorpow
- the exponent operator- any other math operation you like to use
You might also consider how you could accept command line arguments to allow the program to:
- evaluate a single expression entered on the command line.
- accept a filename that it opens, reads, and evaluates the expressions contained therein, either a single expression or one per line in the file.
- when reading a file, you could output the results to the screen or also take an output filename as another command line argument and write the results to that file.