Your goal in this assignment is to familiarize yourself with Rust, the programming language we'll be using for much of this course. To do so, you'll implement a couple small programs, first a Reverse Polish Notation calculator (about which more below); then a second program that analyzes the space complexity of RPN programs.
Before you get started, make sure you've completed the reading for the first two weeks of the course -- Chapters 1-6 and 8 of The Rust Book.
Follow the instructions at Starting with Rust or those in Chapter 1 of The Rust Book to install Rust on your local machine. When we run your code, we'll be using Rust version 1.57.0
in a Linux environment. Make sure you have exactly that version.
You will develop and submit your assignment through GitHub. To start this process, first accept the assignment by visiting https://classroom.github.com/a/hll4LLyE (this will create a new private repository in which you should develop your solution). Clone the repository onto your local machine in order to begin working. Your last commit to this repository before the due date will count as your assignment submission. If you forget how to use Git and/or GitHub, read more online (e.g., https://product.hubspot.com/blog/git-and-github-tutorial-for-beginners).
Rust uses a build system and package manager called Cargo. For this assignment, you'll create three Cargo projects corresponding to the assignment's three parts by typing the following commands from within your GitHub repo for the assignment:
cargo init rpn #Part I
cargo init analyze #Part II
cargo init optimize #Part III
To compile the code for, e.g., Part I, cd
into the rpn
directory, then type cargo build
. To run your code directly, do cargo run
.
A Reverse Polish Notation (RPN) calculator uses a stack to perform arithmetic operations expressed in so-called post-fix style -- the operation to be performed appears after, or post, the operands on which the operation is executed.
For example, here's a valid input to an RPN calculator like the one you'll be building in this assignment:
1 2 + done
the result of which is 3
. At the level of an underlying stack operation of an RPN calculator, this program executes roughly the following pseudocode:
Operation | Stack |
---|---|
push 1 | [1] |
push 2 | [1; 2] |
op + | [3] |
The Stack column depicts the stack (with head to the right) after the Operation listed directly to the left.
Here's a slightly longer RPN program
1 2 + 4 + 5 - 6 7 8 * + + done
and its associated trace of stack operations:
Operation | Stack |
---|---|
push 1 | [1] |
push 2 | [1; 2] |
op + | [3] |
push 4 | [3; 4] |
op + | [7] |
push 5 | [7; 5] |
op - | [2] |
push 6 | [2; 6] |
push 7 | [2; 6; 7] |
push 8 | [2; 6; 7; 8] |
op * | [2; 6; 56] |
op + | [2; 62] |
op + | [64] |
Implement a calculator for arithmetic programs written in RPN style, as specified by the following grammar:
Binary Operators
b ::= + | - | * | /
Terms
t ::= n // 32-bit signed integers, e.g., -1, 2, 256, 0, ...
| b // Binary operator
| save // Pop from the main stack, pushing the value onto an auxiliary stack
| restore // Pop from the auxiliary stack, pushing the value onto the main stack
RPN Programs
p ::= t_0 t_1 ... t_n done
That is, a valid RPN program is a series of n
terms, t_0 t_1 ... t_n
, followed by the keyword done
. Both the example programs given above are valid according to this grammar.
The special commands save
and restore
allow the programmer to save values from the calculator's main stack onto an auxiliary stack for later computation. Here's a program that calculates the sum of two numbers, then uses save
and restore
to subtract the result from 0.
7 8 + save 0 restore - done
with stack trace:
Operation | Main Stack | Auxiliary Stack |
---|---|---|
push 7 | [7] | [] |
push 8 | [7; 8] | [] |
op + | [15] | [] |
save 15 | [] | [15] |
push 0 | [0] | [15] |
restore 15 | [0, 15] | [] |
op - | [-15] | [] |
The special term done
must appear at the end of every valid RPN program, and has the effect of printing out the top value on the main stack (if any), then terminating the program. It's an error if done
is encountered with no values on the stack.
Note: If there is anything after the keyword done
, the program should compile and run, it will just ignore everything after done
.
Examples
Program | Result |
---|---|
1 2 3 done |
3 |
1 2 + 3 + 4 + 5 + done |
15 |
1 2 3 4 5 + + + + done |
15 |
1 2 + save 3 restore + done |
6 |
1 2 + done 3 4 5 done |
3 |
10 2 / save 7 restore - done |
2 |
-
Write a program,
<your-repo>/rpn/src/main.rs
, that reads RPN programs onstdin
and prints their results onstdout
, assuming the keyworddone
is encountered. Your program may assume, as in the grammar above, that the terms in each RPN program are whitespace-separated. -
When printing the result of a valid RPN program, include no newline characters (hint: use
print!
instead ofprintln!
). -
On valid RPN programs, your RPN interpreter should return exit code
0
. On invalid RPN programs (those that attempt to perform operations without sufficient arguments for example), your interpreter should return with a non-zero exit code. -
On or before the due date, commit your final submission to your GitHub repo for this assignment.
The following are errors on which your interpreter should exit with a non-zero error code:
- Performing an operation, like
*
, with insufficient integers on the stack - Attempting to
save
when there are no values on the main stack - Attempting to
restore
when there are no values on the auxiliary stack - Performing
done
when there are no values on the main stack
Certain RPN programs are ill-formed, like the following which attempts to perform an addition operation with too few integers on the stack:
1 2 + + done
Operation | Main Stack |
---|---|
push 1 | [2] |
push 2 | [1; 2] |
op + | [3] |
op + | [?] |
In the second part of this assignment, you'll implement a program that analyzes an RPN program to determine (1) whether it is well formed, and (2) if it is well formed, how large a stack the program will require to run.
In general, RPN programs may require stacks that scale with the size of the program being executed. For example:
1 2 3 4 5 6 7 8 ... N + ... + + + + + + + done
pushes a bunch of integers, from 1
to N
for some large N
, then repeatedly adds the results until only the sum of all the values 1
to N
is left on the stack.
Write a program, <your-repo>/analyze/src/main.rs
, that reads the same input as the program from Part I but instead outputs:
-1
if the RPN program onstdin
was invalid according to the grammar or will attempt an operation with insufficient arguments on the stack; orN
if the the RPN program onstdin
was valid, whereN
is the maximum number of stack slots required total, in both the main and auxiliary stacks, to execute the program.
Examples
Program | Result | Reason |
---|---|---|
1 2 3 done |
3 |
|
1 2 3 |
-1 |
Missing done |
1 2 + 3 + 4 + 5 + done |
2 |
|
1 2 3 4 5 + + + + done |
5 |
|
1 2 + save 3 restore + done |
3 |
Main stack 2 + auxiliary stack 1 |
1 2 + done 3 4 5 done |
2 |
All code after the first done is ignored |
2 0 / done |
2 |
Note that RPN programs containing division by zero are not considered invalid. Consider, for example, the program 2 1 1 - / done
. It is not clear statically that a division by zero will occur; we must first evaluate 1 1 -
in order to see it. In general, we must be willing to evaluate the entire program if we want to detect division by zero errors, and thus must maintain a main and auxiliary stack just as the RPN program does. By choosing not to detect division by zero errors, we can, in principle (and I would suggest that you do in practice), implement the analyzer using constant space (that is, without a stack that could grow linearly in the size of the program as the analyzer executes).
-
Write a program,
<your-repo>/analyze/src/main.rs
, that reads RPN programs onstdin
and outputs either-1
orN
onstdout
, whereN
is the number of stack slots required to execute the program and-1
indicates an invalid RPN program as described above. -
On or before the due date, commit your final submission to your GitHub repo for this assignment.
This part is extra credit.
Certain RPN programs, like 1 2 3 4 5 + + + + done
, can be rewritten to save space (an equivalent program that uses only two stack slots is 1 2 + 3 + 4 + 5 + done
). For a small amount of extra credit:
-
Implement a third program,
<your-repo>/optimize/src/main.rs
, that reads an RPN program fromstdin
and prints tostdout
a version of the program that uses as few stack slots as possible. The program you print out should be equivalent to the original program, but could perform different operations, possibly in a different order. You may assume that the input program contains no 'save' or 'restore' operations (and thus does not make use of the auxiliary stack), and the optimized program should also not use 'save' or 'restore'. -
In a comment at the top of your program, explain your optimization strategy or algorithm.
-
Commit your program to your GitHub repo for this assignment.
Submit a translation for https://www.codewars.com/kata/52f78966747862fc9a0009ae and get it accepted. (At most one transaction will be accepted. Note that you have to reach certain level before you can submit translations to CodeWars.)
Submit the URL of your github repo to Blackboard, which should include your Github ID. If not, make sure to identify your Github ID in submission comments.