# Stack Original assessment brief: https://github.com/turner-townsend/backend-assessment/blob/master/README.md#stack A Python implementation of the Fifth stack-based programming language, demonstrating clean code practices, robust error handling, and modern Python features. ## What is Fifth? Fifth is a stack-based language that operates on a stack of integers. All operations work with the top elements of the stack, following the last-in-first-out (LIFO) principle. This implementation provides an interactive REPL (read-eval-print loop) for executing Fifth commands. ## Features - **Stack operations**: `PUSH`, `POP`, `SWAP`, `DUP` - **Arithmetic operations**: `+`, `-`, `*`, `/` (floor division) - **Comprehensive error handling**: Input validation, stack state checking, and clear error messages - **Interactive REPL**: Command-line interface with readline support for command history (↑/↓) - **Case-insensitive commands**: Commands work in both upper and lowercase ## Installation and usage ### Requirements - Python 3.9+ (uses modern type hint syntax `list[int]`) - No external dependencies ### Running the interpreter ```bash python .py ``` The interpreter will start with a help message and enter interactive mode, displaying the current stack state before each command. ## Commands reference | Command | Description | Stack effect | Example | | ---------- | --------------------------- | ----------------------------- | --------- | | `PUSH ` | Push integer n onto stack | `[] → [n]` | `PUSH 42` | | `POP` | Remove top element | `[..., n] → [...]` | `POP` | | `SWAP` | Swap top two elements | `[..., a, b] → [..., b, a]` | `SWAP` | | `DUP` | Duplicate top element | `[..., n] → [..., n, n]` | `DUP` | | `+` | Add top two elements | `[..., a, b] → [..., (a+b)]` | `+` | | `-` | Subtract (second - top) | `[..., a, b] → [..., (a-b)]` | `-` | | `*` | Multiply top two elements | `[..., a, b] → [..., (a*b)]` | `*` | | `/` | Floor divide (second / top) | `[..., a, b] → [..., (a//b)]` | `/` | | `HELP` | Show available commands | No change | `HELP` | | `EXIT` | Quit the program | - | `EXIT` | ### Example session ``` Available commands: HELP, PUSH, POP, SWAP, DUP Available operations: +, -, *, / stack is [] PUSH 3 stack is [3] PUSH 11 stack is [3, 11] + stack is [14] DUP stack is [14, 14] PUSH 2 stack is [14, 14, 2] * stack is [14, 28] SWAP stack is [28, 14] / stack is [2] + ERROR: two numbers required POP stack is [] exit ``` ## Tests Run the test suite: ```bash pytest -v ``` Example output: ```shell ➜ stack git:(main) ✗ pytest -v ================================================================ test session starts ================================================================ platform linux -- Python 3.12.2, pytest-8.3.4, pluggy-1.5.0 -- /home/jamey/.venv/bin/python cachedir: .pytest_cache rootdir: /home/jamey/Projects/turner-townsend-backend-assessment/stack plugins: mock-3.14.0, asyncio-0.21.2 asyncio: mode=Mode.STRICT collected 22 items test_stack.py::test_push_and_pop PASSED [ 4%] test_stack.py::test_push_invalid_value PASSED [ 9%] test_stack.py::test_pop_empty_stack PASSED [ 13%] test_stack.py::test_swap PASSED [ 18%] test_stack.py::test_swap_insufficient_elements PASSED [ 22%] test_stack.py::test_dup PASSED [ 27%] test_stack.py::test_dup_empty_stack PASSED [ 31%] test_stack.py::test_addition PASSED [ 36%] test_stack.py::test_subtraction PASSED [ 40%] test_stack.py::test_multiplication PASSED [ 45%] test_stack.py::test_division PASSED [ 50%] test_stack.py::test_division_by_zero PASSED [ 54%] test_stack.py::test_binary_op_insufficient_elements PASSED [ 59%] test_stack.py::test_execute_unknown_command PASSED [ 63%] test_stack.py::test_execute_push_command PASSED [ 68%] test_stack.py::test_execute_pop_command PASSED [ 72%] test_stack.py::test_execute_swap_command PASSED [ 77%] test_stack.py::test_execute_dup_command PASSED [ 81%] test_stack.py::test_help PASSED [ 86%] test_stack.py::test_execute_empty_command PASSED [ 90%] test_stack.py::test_execute_whitespace_command PASSED [ 95%] test_stack.py::test_case_insensitivity PASSED [100%] ================================================================ 22 passed in 0.03s ================================================================= ``` ## Design decisions and architecture ### Core design philosophy The implementation prioritises **clarity and maintainability** over performance optimisation, following the principle of being "explicit over implicit" and "discoverable over performant" as requested in the brief. ### Key architectural choices #### 1. Class-based structure (`FifthStack`) - **Reasoning**: Encapsulates state (the stack) with behaviour (operations), making the code more organised and testable - **Alternative considered**: Functional approach with global state, rejected for testing difficulties and state management complexity #### 2. Command dictionary pattern ```python self.commands: dict[str, Callable] = { "help": self.help, "push": self.push, # ... } ``` - **Reasoning**: Makes adding new commands trivial and eliminates long if/elif chains - **Benefit**: Extensible design - new commands require only adding to the dictionary - **Trade-off**: Slight memory overhead for the dictionary, but gains significant maintainability #### 3. Separate binary operations dictionary ```python self.binary_ops: dict[str, Callable[[int, int], int]] = { "+": operator.add, # ... } ``` - **Reasoning**: Leverages Python's `operator` module for built-in mathematical operations - **Benefit**: Consistent behaviour with Python's arithmetic, less error-prone than custom implementations - **Design**: Separate from commands because they have different signatures and error handling needs #### 4. Method extraction for each operation Each stack operation is implemented as a separate method rather than inline logic: - **Testing**: Each operation can be unit tested independently - **Debugging**: Stack traces clearly identify which operation failed - **Extensibility**: New operations are self-contained and don't affect existing code - **Readability**: Each method has a single, clear responsibility #### 5. Defensive programming with `_require()` helper ```python def _require(self, number, message): if len(self.stack) < number: print(f"ERROR: {message}") return False return True ``` - **Reasoning**: Centralizes stack size validation logic - **Benefit**: Consistent error messages and reduces code duplication - **DRY principle**: Single source of truth for stack requirement checking #### 6. Error recovery strategy - **Division by zero**: Restores original stack state (`self.stack.extend([a, b])`) - **Invalid input**: Provides clear error messages without crashing - **Insufficient stack**: Fails gracefully with descriptive messages - **Reasoning**: Interpreter should be robust and not crash on user errors #### 7. Modern Python features used **Type hints with generic collections (Python 3.9+)**: ```python self.stack: list[int] = [] ``` - **Reasoning**: Improves code readability and enables better IDE support - **Choice**: Used `list[int]` instead of `typing.List[int]` for cleaner, modern syntax **Input processing centralization**: ```python def _normalize_input(self, command: str) -> list[str]: return command.lower().strip().split() ``` - **Reasoning**: Single source of truth for command normalization and tokenization - **Benefit**: Both REPL and direct execution paths use identical processing logic - **Design choice**: Avoided walrus operator in favour of explicit method extraction for better testability and clarity **f-string formatting**: - **Reasoning**: More readable than `.format()` or `%` formatting - **Performance**: Faster than alternative string formatting methods #### 8. REPL design choices **Stack display**: Shows stack state after each command - **Reasoning**: Follows the specified output format from the brief - **Implementation choice**: Used Python's native list representation for consistency with the expected format - **Benefit**: Maintains familiar syntax for Python developers while meeting requirements **Case insensitive commands**: ```python command.lower().strip().split() ``` - **Reasoning**: More user-friendly, follows principle of least surprise - **Benefit**: Reduces user errors from capitalisation mistakes **Graceful exit**: Handles both `exit` command and Ctrl+C/EOF - **Reasoning**: Proper CLI applications should handle interrupts gracefully - **Implementation**: Try/catch for `EOFError` and `KeyboardInterrupt` ### Alternative approaches considered 1. **Parser-based approach**: Could have used a formal parser (e.g. PLY), but rejected as over-engineering for this simple language 2. **Functional approach**: Could have used pure functions with immutable data but rejected because: - Stack operations naturally mutate state - Would require returning new stack state from every function - Less intuitive for this particular problem domain 3. **Command pattern with classes**: Could have made each command a separate class but rejected due to: - Unnecessary complexity for simple operations - Would require more boilerplate code - Current approach is more discoverable 4. **eval() for arithmetic**: Could have used `eval()` for mathematical expressions but rejected due to: - Security concerns - Need for custom stack-based evaluation order - Loss of control over error handling ### Testing strategy The design facilitates testing through: - **Isolated methods**: Each operation can be tested independently - **Dependency injection**: Stack state can be set up for specific test scenarios - **Clear return values**: Methods have predictable behaviour and side effects - **Error conditions**: All error paths are reachable and testable ### Extensibility Adding new features is straightforward: - **New commands**: Add to `self.commands` dictionary and implement method - **New operators**: Add to `self.binary_ops` dictionary - **New data types**: Modify type hints and validation logic - **New output formats**: Modify display logic in main loop This architecture balances simplicity with software engineering practices, to keep the code readable and maintainable.