Every bytecode interpreter -- CPython's, the JVM's, V8's -- is built on the same
core loop: a flat array of instructions, an instruction pointer, and a value
stack, dispatched instruction by instruction until the program ends. CPython's
version of this loop lives in Python/ceval.c; yours will be much smaller, but
the shape is the same.
Implement run(bytecode, env=None): execute the instructions and return the
list of values produced by PRINT, in order. Each instruction is a tuple:
("PUSH", value)-- push a constant onto the stack("LOAD", name)-- pushenv[name]("STORE", name)-- pop the stack, store intoenv[name]("ADD",)/("SUB",)/("MUL",)-- popb, popa, pusha OP b("LT",)/("GT",)-- popb, popa, pusha < b/a > b("PRINT",)-- pop and record the value as output("JUMP", target)-- unconditionally set the instruction pointer totarget("JUMP_IF_FALSE", target)-- pop; if the value is falsy, jump totarget
Example
run([("PUSH", 2), ("PUSH", 3), ("ADD",), ("PRINT",)]) # -> [5]
A loop is just JUMP_IF_FALSE pointing forward past the loop body, and JUMP
pointing back to the condition check -- see the sum-of-1-to-5 and factorial
programs in the tests for a complete worked example.
Constraints
JUMP/JUMP_IF_FALSEtargets are absolute instruction indices (not relative offsets).- After a jump, execution continues FROM the target -- don't also advance the instruction pointer past it.
Real-world grounding
- CPython's eval loop:
Python/ceval.c
Real bytecode has dozens of opcodes, a much richer stack discipline (frames, exception handling, closures), and in CPython's case an adaptive specializing interpreter that rewrites hot instructions to faster variants at runtime -- but the fundamental "dispatch on opcode, mutate a stack, move an instruction pointer" loop is exactly what you're building here.