tashrique-ahmed

PostScript-based calculator

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, and dup 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 the switch 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 and Reader 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 long switch statement, making the interpreter easier to extend.
  • Testing: Add unit tests for individual commands to catch edge cases early.