Skip to content

Step 1 of 3 · Reading · ~3 min

Representing Values in the VM

Bytecode Virtual Machine

Values Aren't Just Bytecode Constants

Your VM's bytecode stream stores raw bytes — opcodes and operand indices. But the values those instructions produce and consume (numbers, booleans, strings, nil) need a runtime representation that the interpreter loop can inspect quickly: "is this a number?", "are these two values equal?", "print this value."

That representation is a tagged union (sometimes called a tagged variant or discriminated union): every Value carries a small tag saying what kind of thing it is, plus the payload for that kind.

Value = (tag, payload)

tag = NIL | BOOL | NUMBER | OBJ

A minimal encoding looks like:

python

Real VMs (like clox) pack this more efficiently — a C struct with a ValueType enum and a union of double/bool/Obj*, or even NaN-boxing where every value is a single 64-bit float and non-numbers are hidden inside NaN bit patterns. The tagged tuple above is the same idea at the source level: cheap to test, cheap to unwrap.

Truthiness

Only nil and false are "falsey" — every other value, including 0 and "", is truthy. This is the rule OP_JUMP_IF_FALSE and ! will rely on later, so get it right now:

python

Strings Are Heap Objects

Numbers and booleans are small enough to copy by value. Strings are not — they're heap-allocated objects. This distinction matters for two reasons you'll meet again soon:

  1. Equality. Two number values with the same bits are equal. Two string objects holding the same characters should also compare equal — but if your VM later interns strings (deduplicates identical literals into one heap object), equality becomes a cheap pointer comparison instead of a character-by-character scan.
  2. Garbage collection. Every object you allocate on the heap is something the collector must eventually be able to find and free. By modeling strings as heap objects now — rather than raw host-language strings — you're building the exact hook the mark-and-sweep collector (a few chapters ahead) will attach to. Keep strings immutable: once created, a string's contents never change. That sidesteps a whole class of aliasing bugs and makes interning safe.

Concatenation and Printing

OP_CONCAT pops two values, requires both to be strings (or coerces, if your language allows 1 + "x"-style mixing — decide that now and be consistent), and pushes a new heap string. OP_ADD on two numbers is separate: don't let + silently do both, or your runtime type errors get mushy. Finally, every value needs a stringify for print:

python

What to build

Add NIL/TRUE/FALSE opcodes and a tagged Value type to your VM. Implement OP_ADD for numbers, OP_CONCAT (or an overloaded +) for strings, OP_EQUAL that compares by tag-then-value, and stringify for print. Watch the edge cases: comparing a number to a string should be false, not a crash; concatenating nil should be a runtime error with a clear message, not a Python TypeError leaking through.

Up nextCompiling Expressions to BytecodeCompiling to Bytecode

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…