Step 1 of 3 · Reading · ~3 min
Expanding the Instruction Set
Bytecode Virtual Machine
VM Instructions — Push, Pop, Arithmetic
Your VM currently only knows about numbers. Real programs need nil, booleans, comparisons, negation of truth values, printing, and a way to discard values that expression statements produce but nobody uses. This lesson extends the instruction set and — just as importantly — introduces runtime type checking now that the compiler no longer has a type-checked AST to lean on.
New opcodes
OP_NIL/OP_TRUE/OP_FALSE push a literal value with no operand — unlike OP_CONSTANT, these don't need to look anything up in the constant pool because there's only ever one nil, one true, one false:
Why the VM must type-check at runtime
In the tree-walking interpreter, every visit_* method was written for a specific node kind and Python's own dynamic typing quietly handled the rest. In the bytecode VM, OP_ADD doesn't know what kind of values are sitting on top of the stack — it just pops two things and combines them. Nothing stops a malformed or maliciously-compiled program from pushing a string and a number and hitting OP_ADD. Each arithmetic instruction now has to check types itself and raise a runtime error rather than let Python raise a confusing TypeError:
This is the bytecode-VM equivalent of the type checks your tree-walker did in each visit_binary — the checks moved, but they're still mandatory, and arguably more important here because there's no AST left to have validated anything ahead of time.
Truthiness and OP_NOT
Reuse the same truthiness rule you've had since if/while: only nil and false are falsy.
Comparisons return booleans, arithmetic doesn't
OP_EQUAL, OP_GREATER, OP_LESS are the first instructions whose result is a boolean rather than a number — and OP_EQUAL in particular has to work across any pair of value types (comparing a number to nil should yield false, not a type error), while OP_GREATER/OP_LESS should still require both operands to be numbers, the same way OP_ADD does.
OP_PRINT and OP_POP
Every expression statement in your language evaluates to a value that lands on the stack — but a bare statement like 1 + 2; on its own line doesn't print or store anything, so that value has to be explicitly discarded. This is what OP_POP is for: the compiler emits it after every expression statement to keep the stack from silently growing forever. print expr; is almost the same, except instead of discarding the value it consumes and prints it:
stringify needs its own formatting rules — nil prints as nil, true/false print lowercase, and numbers that happen to be whole (3.0) typically print without the trailing .0 to match how a user would type them.
Edge cases to watch
nil == nilmust betrue, andnilcompared to anything else (a number, a string,false) must befalse— not a runtime error.- String vs. number arithmetic:
"a" + 1should be a clear runtime error ("Operands must be two numbers or two strings."), unless your language supports string concatenation via+, in which case that needs its own branch checking both operands are strings. - Stack cleanliness: forgetting to emit
OP_POPafter expression statements causes the stack to grow unboundedly over a long-running program even though nothing looks obviously wrong.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…