Every JVM method body is a flat sequence of bytecode instructions
operating over two pieces of per-frame state: an operand stack
(for intermediate values mid-computation) and a locals array (for
method arguments and local variables). There's no register allocator
at this level -- everything routes through the stack. This is the
same "stack machine" shape as CPython's ceval.c (if you've done the
Language Runtimes course's Bytecode Stack Machine exercise, this is
its JVM-flavored counterpart: same operand-stack-and-locals model,
different real VM, different instruction set) -- but the JVM's actual
mnemonics and control-flow instructions.
Implement JVMInterpreter.run(instructions), a fetch-decode-execute
loop over a program counter (pc) walking a flat instruction list.
Support:
("BIPUSH", const)-- pushconstonto the operand stack.("ILOAD", index)-- pushself.locals[index].("ISTORE", index)-- pop the top value intoself.locals[index].("IADD", None)-- popvalue2, popvalue1, pushvalue1 + value2.("ISUB", None)-- popvalue2, popvalue1, pushvalue1 - value2(note the order: the value pushed FIRST is the minuend).("IFEQ", target)-- pop a value; if it's0, jump (pc = target); otherwise fall through to the next instruction.("GOTO", target)-- unconditional jump:pc = target.("IRETURN", None)-- pop and return the value; this endsrun().
Example
interp = JVMInterpreter(locals_count=0)
interp.run([
("BIPUSH", 3),
("BIPUSH", 4),
("IADD", None),
("IRETURN", None),
])
# -> 7
A loop is just IFEQ + GOTO around a body -- e.g. summing 1..n by
decrementing a counter local to 0 and jumping back to the top each
iteration (see the test file's _sum_loop_program for a fully worked
example).
Constraints
- Targets for
IFEQ/GOTOare absolute indices intoinstructions, not relative offsets. ISUB's operand order matters: the operand pushed onto the stack FIRST isvalue1(the minuend); the one pushed SECOND (so popped first) isvalue2(the subtrahend).- Every test program is well-formed and eventually reaches an
IRETURN-- you don't need to handle malformed bytecode.
Real-world grounding
- HotSpot's portable C++ bytecode interpreter (used by the "Zero"
build, a JIT-free interpreter for platforms without a
template-interpreter assembly backend -- the clearest place to read
the actual fetch-decode-execute loop over an operand stack and
locals array):
src/hotspot/share/interpreter/zero/bytecodeInterpreter.cpp - The default (non-Zero) interpreter HotSpot actually ships with per
platform generates real machine code per bytecode instead of
switching on an enum in a C++ loop -- the platform-independent
table of per-bytecode behavior it's generated from:
src/hotspot/share/interpreter/templateTable.cpp
Real HotSpot bytecodes also carry type-specific variants
(iadd/ladd/fadd/dadd, iload_0..3 as fast paths for the first
four locals, etc.) and a full 256-entry opcode space -- this exercise
implements the int-only core of the dispatch loop underneath all of
it.