Simple Bytecode Interpreter in Python
This challenge asks you to build a rudimentary bytecode interpreter in Python. Bytecode interpreters are fundamental to many programming languages (like Java and Python itself!), translating intermediate code into machine-executable instructions. This exercise will help you understand the core concepts of interpreters and how they execute code.
Problem Description
You are tasked with creating a Python interpreter that executes a simplified bytecode. The bytecode consists of a list of instructions, each represented as a tuple. Your interpreter should take this bytecode as input and execute it, maintaining a stack to store intermediate values.
Instructions:
PUSH <value>: Pushes the givenvalueonto the stack.valuecan be an integer or a string.ADD: Pops the top two values from the stack, adds them (if both are numbers), concatenates them (if both are strings), and pushes the result back onto the stack. If the types are incompatible, raise aTypeError.SUB: Pops the top two values from the stack, subtracts them (if both are numbers), raises aTypeErrorotherwise.MUL: Pops the top two values from the stack, multiplies them (if both are numbers), raises aTypeErrorotherwise.DIV: Pops the top two values from the stack, divides them (if both are numbers), raises aTypeErrorandZeroDivisionErrorif necessary.PRINT: Pops the top value from the stack and prints it to the console.HALT: Stops the execution of the interpreter.
Key Requirements:
- Implement a
runfunction that takes the bytecode as a list of tuples and executes it. - Use a stack (a Python list will suffice) to store values during execution.
- Handle type errors appropriately when performing operations on incompatible types.
- Handle
ZeroDivisionErrorwhen dividing by zero. - Raise a
ValueErrorif the bytecode contains an invalid instruction. - The interpreter should halt gracefully when encountering the
HALTinstruction.
Expected Behavior:
The interpreter should execute the bytecode instructions in order, modifying the stack as needed. The PRINT instruction should output the top value of the stack to the console. The HALT instruction should terminate the interpreter.
Edge Cases to Consider:
- Empty bytecode list.
- Bytecode with invalid instructions.
- Stack underflow (attempting to pop from an empty stack).
- Division by zero.
- Incompatible types for arithmetic/concatenation operations.
Examples
Example 1:
Input: [('PUSH', 5), ('PUSH', 3), ('ADD'), ('PRINT'), ('HALT')]
Output:
5
Explanation: Pushes 5 and 3 onto the stack. Adds them (5 + 3 = 8). Prints 8. Halts.
Example 2:
Input: [('PUSH', 'hello'), ('PUSH', ' world'), ('ADD'), ('PRINT'), ('HALT')]
Output:
hello world
Explanation: Pushes "hello" and " world" onto the stack. Concatenates them ("hello" + " world" = "hello world"). Prints "hello world". Halts.
Example 3:
Input: [('PUSH', 10), ('PUSH', 2), ('DIV'), ('PRINT'), ('HALT')]
Output:
5.0
Explanation: Pushes 10 and 2 onto the stack. Divides them (10 / 2 = 5.0). Prints 5.0. Halts.
Example 4: (Edge Case - Division by Zero)
Input: [('PUSH', 10), ('PUSH', 0), ('DIV'), ('PRINT'), ('HALT')]
Output:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
Explanation: Pushes 10 and 0 onto the stack. Attempts to divide 10 by 0, raising a ZeroDivisionError.
Constraints
- The bytecode list will contain tuples of length 2. The first element of each tuple is the instruction name (string), and the second element is the argument (string or number).
- The interpreter should handle integer and string values.
- The bytecode list will not exceed 100 instructions.
- The interpreter should be reasonably efficient for the given constraints. Optimization is not the primary focus.
Notes
- Start by implementing the
PUSHinstruction and verifying that values are correctly pushed onto the stack. - Then, implement the arithmetic and concatenation operations (
ADD,SUB,MUL,DIV). - After that, implement the
PRINTandHALTinstructions. - Remember to handle errors gracefully and provide informative error messages.
- Consider using a dictionary to map instruction names to their corresponding functions for better code organization.
- Think about how to handle different data types and ensure type safety during operations.