PostScript-based calculator

Overview: This project implements a small interpreter for a subset of the stack-based language PostScript. It allows users to perform mathematical operations, manage symbols, and manipulate the operand stack interactively.
1. Core Interpreter Loop
public void read(Reader r) {
while (r.hasNext()) {
t = r.next();
if (t.isSymbol() && t.getSymbol().equals("quit")) {
break;
} else if (t.isSymbol() && table.contains(t.getSymbol())) {
Token f = table.get(t.getSymbol());
if (f.isProcedure()) {
Reader recRead = new Reader(f);
read(recRead);
} else {
stack.push(f);
}
} else if (t.isSymbol() && t.getSymbol().equals("if")) {
runIf();
} else if (t.isBoolean() || t.isProcedure() || t.isNumber()) {
stack.push(t);
} else if (t.isSymbol() && t.getSymbol().substring(0, 1).equals("/")) {
stack.push(t);
} else {
switch (t.getSymbol()) {
case "pop": stack.pop(); break;
case "add": add(); break;
case "sub": sub(); break;
case "mul": mul(); break;
case "div": div(); break;
case "dup": dup(); break;
case "exch": exch(); break;
case "eq": eq(); break;
case "ne": ne(); break;
case "lt": lt(); break;
case "def": def(); break;
case "ptable": ptable(); break;
case "pstack": pstack(); break;
default: break;
}
}
}
}
Explanation:
This is the main loop of the interpreter. It processes tokens from the input, classifying them as numbers, symbols, procedures, or commands. Each token triggers the appropriate behavior:
- Symbols are resolved using the symbol table.
- Commands like
add
,mul
, anddup
invoke their respective methods. - Procedures are recursively read by creating a new
Reader
.
Why I Did It This Way:
This approach creates a modular and extensible design. Each command has its own method, making it easy to add new features later. Recursive handling of procedures aligns naturally with PostScript's stack-based nature.
What I Could’ve Done Better:
- Error Handling: Add better error messages when encountering unknown symbols or malformed input.
- Switch Case Efficiency: Use a
Map<String, Runnable>
to replace theswitch
statement for better readability and scalability.
2. Arithmetic Operations
public void add() {
Assert.pre(stack.size() >= 2, "Stack should be bigger than or equal to 2");
Token temp1 = stack.pop();
Token temp2 = stack.pop();
stack.push(new Token(temp1.getNumber() + temp2.getNumber()));
}
public void sub() {
Assert.pre(stack.size() >= 2, "Stack should be bigger than or equal to 2");
Token temp2 = stack.pop();
Token temp1 = stack.pop();
stack.push(new Token(temp1.getNumber() - temp2.getNumber()));
}
public void mul() {
Assert.pre(stack.size() >= 2, "Stack should be bigger than or equal to 2");
Token temp1 = stack.pop();
Token temp2 = stack.pop();
stack.push(new Token(temp1.getNumber() * temp2.getNumber()));
}
public void div() {
Assert.pre(stack.size() >= 2, "Stack should be bigger than or equal to 2");
Token temp2 = stack.pop();
Token temp1 = stack.pop();
stack.push(new Token(temp1.getNumber() / temp2.getNumber()));
}
Explanation:
These methods implement basic arithmetic operations. They pop two operands from the stack, perform the operation, and push the result back onto the stack.
Why I Did It This Way:
This design keeps operations atomic and easy to test. Using Assert.pre
ensures the stack has enough operands before performing the operation, preventing runtime errors.
What I Could’ve Done Better:
- Division by Zero: Add explicit handling for division by zero to avoid unexpected crashes.
- Type Safety: Currently assumes operands are numbers. Adding type checks would improve robustness.
3. Stack Manipulation
public void dup() {
Assert.pre(stack.size() >= 1, "Stack should be bigger than or equal to 1");
Token temp = stack.peek();
stack.push(temp);
}
public void exch() {
Assert.pre(stack.size() >= 2, "Stack should be bigger than or equal to 2");
Token temp1 = stack.pop();
Token temp2 = stack.pop();
stack.push(temp1);
stack.push(temp2);
}
public void pstack() {
for (Token t : stack) {
System.out.println(t);
}
}
Explanation:
dup
: Duplicates the top element of the stack.exch
: Swaps the top two elements.pstack
: Prints all stack elements without modifying the stack.
Why I Did It This Way:
These operations closely mimic the PostScript language, maintaining its stack-based semantics. Keeping pstack
non-destructive ensures debugging is easy.
What I Could’ve Done Better:
- Use a helper method to validate stack size across all operations.
- Add the ability to format
pstack
output for easier readability.
4. Symbol Table Management
public void def() {
Assert.pre(stack.size() >= 2, "Stack should be bigger than or equal to 2");
Token value = stack.pop();
Token name = stack.pop();
String nameString = name.getSymbol().substring(1);
table.add(nameString, value);
}
public void ptable() {
System.out.print(table);
}
Explanation:
def
: Associates a symbol (e.g.,/pi
) with a value (e.g.,3.14
) in the symbol table.ptable
: Prints the current contents of the symbol table.
Why I Did It This Way:
Using the SymbolTable
class abstracts the complexity of managing key-value pairs, making the implementation cleaner and more focused.
What I Could’ve Done Better:
- Validation: Ensure the symbol being defined starts with a
/
to prevent malformed inputs. - Error Messages: Provide feedback if attempting to define an existing symbol without removing it first.
5. Conditional Logic
public void runIf() {
Token temp = stack.pop();
Token bool = stack.pop();
if (bool.getBoolean()) {
Reader readIf = new Reader(temp);
read(readIf);
}
}
Explanation:
This implements the if
command, which evaluates a boolean condition and executes a procedure if the condition is true
.
Why I Did It This Way:
The recursive call to read
allows seamless execution of nested procedures without duplicating logic.
What I Could’ve Done Better:
- Error Handling: Validate that the second operand is a boolean and the first is a procedure before executing.
- Logging: Add logs for debugging to trace why certain branches are executed.
Reflection and Improvements
What Worked Well:
- The modular design makes each command isolated and easy to debug or extend.
- Recursive handling of procedures aligns perfectly with the stack-based nature of PostScript.
- Using
SymbolTable
andReader
classes abstracted away low-level details, letting me focus on the interpreter logic.
What Could Be Improved:
- Error Handling: A centralized error-reporting mechanism would improve robustness and user feedback.
- Extensibility: A
Map<String, Runnable>
for commands would eliminate the need for a longswitch
statement, making the interpreter easier to extend. - Testing: Add unit tests for individual commands to catch edge cases early.