JavaScript Bytecode Interpreter
Build a simple interpreter in JavaScript that can execute a custom bytecode format. This challenge will help you understand the fundamental principles of interpreters, virtual machines, and how programs are executed at a lower level, which is crucial for comprehending language runtimes and compilers.
Problem Description
Your task is to implement a JavaScript interpreter that can execute a defined set of bytecode instructions. You will define your own simple instruction set and then create a virtual machine (VM) that fetches, decodes, and executes these instructions from a given bytecode array. The interpreter should manage a stack for operations and a simple memory space or register set.
Key Requirements:
- Instruction Set Definition: Define a small, manageable set of bytecode instructions. Examples could include arithmetic operations (ADD, SUB, MUL), stack manipulation (PUSH, POP), control flow (JUMP, JUMP_IF_TRUE, JUMP_IF_FALSE), and input/output (PRINT).
- Bytecode Array: The interpreter will receive an array of numbers representing the bytecode. Each number can represent an opcode, and subsequent numbers might be operands.
- Stack-Based Execution: The interpreter should primarily use a stack to store intermediate values and arguments for operations.
- Program Counter (PC): A program counter will keep track of the current instruction being executed.
- Execution Loop: Implement a loop that continues as long as the PC is within the bounds of the bytecode array.
- Instruction Decoding and Execution: Inside the loop, fetch the current instruction (opcode), decode it, and execute the corresponding action.
- Error Handling: Implement basic error handling for invalid opcodes or stack underflow.
Expected Behavior:
The interpreter should take a bytecode array and execute it. The result of the execution (e.g., the value at the top of the stack, or a printed output) should be returned or observable.
Edge Cases:
- Empty bytecode array.
- Bytecode with invalid opcodes.
- Stack underflow (trying to pop from an empty stack).
- Division by zero (if division is implemented).
Examples
Example 1: Simple Addition
Input:
bytecode = [1, 5, 2, 3, 0] // Assuming: 1=PUSH, 2=ADD, 0=HALT
// PUSH 5
// PUSH 2
// ADD
// HALT
Output: 7
Explanation: The bytecode pushes 5 onto the stack, then pushes 2. The ADD instruction pops these two values, adds them (5 + 2 = 7), and pushes the result back onto the stack. The HALT instruction terminates execution. The final result is the value on top of the stack.
Example 2: Conditional Jump
Input:
bytecode = [1, 10, 4, 1, 20, 5, 2, 0, 6]
// Assuming: 1=PUSH, 4=JUMP_IF_TRUE, 5=PRINT, 6=HALT, 10=true (or a non-zero value), 20=false (or zero)
// PUSH 10 (true)
// JUMP_IF_TRUE to instruction at index 5
// PUSH 20 (false) - this will be skipped
// PRINT (would print 20 if reached)
// HALT
// Expected behavior: The JUMP_IF_TRUE should cause the interpreter to jump to the HALT instruction.
// Let's refine this with a more explicit jump target and a better example for clarity:
bytecode = [
1, 1, // PUSH 1
1, 5, // PUSH 5
11, 4, // JUMP_IF_TRUE to address 4 (index of PRINT)
1, 10, // PUSH 10 (this should be skipped)
5, // PRINT (this should be executed)
0 // HALT
]
// Assuming: 1=PUSH, 5=PRINT, 11=JUMP_IF_TRUE, 0=HALT, and a value of 1 (true) implicitly on top of stack before JUMP_IF_TRUE.
// A more concrete bytecode definition:
// OPCODES:
// 0: HALT
// 1: PUSH (operand: value)
// 2: ADD
// 3: SUB
// 4: JUMP_IF_TRUE (operand: jump_address)
// 5: PRINT
// Example 2 Revised with Opcode Definitions
bytecode = [
1, 1, // PUSH 1 (true)
4, 4, // JUMP_IF_TRUE to index 4
1, 10, // PUSH 10 (skipped)
5, // PRINT (this should be executed)
0 // HALT
]
Output: 1
Explanation: The interpreter pushes 1 onto the stack. Then, it encounters JUMP_IF_TRUE. Since the top of the stack is 1 (truthy), it jumps to index 4. It then executes PRINT, which outputs the value on top of the stack (1). Finally, it encounters HALT.
Example 3: Stack Underflow
Input:
bytecode = [2, 0] // Assuming: 2=ADD, 0=HALT
// ADD (expects two values on stack, but stack is empty)
Output: Error: Stack underflow
Explanation: The ADD instruction requires two operands on the stack. Since the stack is empty, it results in a stack underflow error.
Constraints
- The bytecode will be an array of integers.
- Each instruction will be represented by an opcode. Some opcodes may be followed by one or more operands.
- The maximum number of instructions in a bytecode program is 1000.
- The maximum value of any operand or data on the stack is 2^31 - 1.
- The interpreter should execute within a reasonable time, aiming for O(N) complexity where N is the number of bytecode instructions executed.
Notes
- You are free to design your own instruction set. Make sure to document your chosen opcodes and their behavior clearly.
- Consider how you will manage the program counter (PC). It typically increments by 1 for each opcode, but jumps will alter its flow.
- Think about how to handle operands for instructions. For example, if
PUSHis opcode1, and you encounter1, 10,10is the operand forPUSH. - A good starting point for instruction set design would be:
PUSH,POP,ADD,SUB,PRINT,JUMP,JUMP_IF_TRUE,JUMP_IF_FALSE,HALT. - Ensure your solution is robust against malformed bytecode (e.g., an instruction expecting an operand but reaching the end of the bytecode array).