Three interpreters can agree that a + b means addition while arranging the work differently. One follows a tree, another takes values from a stack and the third names the slots where those values live. These inputs to an operation are its operands.
I'm drawn towards registers, but I want to compare the work before turning that preference into a design. For a + b, all three look reasonable. That's useful. A small example shouldn't have to become a terrible stack machine to make my favourite look good.
Follow the tree
A tree representation might give the addition node an operator and two children:
Add
├── Name a
└── Name b
The evaluator visits the children to get a and b, then adds them. The tiny interpreter walkthrough follows the earlier steps from text to this tree. Here I'm comparing the work of running it.
The cost of walking a tree changes with the representation of nodes and values as well as the work of each visit and how often the tree runs. A complicated operation can easily dominate the code that reaches it. Value representation, decoding and storage layout matter too. A stack machine can store typed values directly, and a tree can live in a compact pool. Registers don't own those choices, so the visits and instruction counts in these examples won't tell us which machine is faster.
When the operation is cheap and happens often, I start looking at how I keep reaching it. Follow a node, check its kind, call back into the evaluator. The arithmetic may take less work than reaching it. A flatter representation interests me because I can organise the trip in advance.
Put intermediate values on a stack
A stack machine can represent the expression as bytecode, instructions its runtime can execute:
PUSH a
PUSH b
ADD
After the first two instructions, the stack contains the values of a and b. ADD removes the top two values and pushes their sum.
The instruction doesn't name the operands because the convention tells it where to find them. That can make the encoding compact. The compiler has to put values on the stack in an order that makes each operation find the right ones.
I can trace this small machine with a pencil, which is a large part of its appeal.
Name the operands instead
Subtraction is where I'd check my picture of the stack because with addition I could reverse the two inputs and get the answer I expected anyway. Pushing nine then four means I pop four first. Subtract the next value from that and I get minus five although 9 - 4 should give five. The top holds the right operand, so I pop that first but keep it on the right of the subtraction. The storage can look perfectly orderly while I subtract the wrong way round. Where's the left operand? Addition would hide this mistake from named inputs as politely as it hid it from the pushes. I'd still check subtraction when connecting the operands to a register instruction.
A register machine names slots explicitly:
LOAD r0, a
LOAD r1, b
ADD r2, r0, r1
Here “register” means a slot in the virtual machine, which need not be a CPU register. The addition reads two slots and writes a third.
If the function's inputs already occupy r0 and r1, the body can begin with ADD. That is why a register example can appear to perform the same task in one instruction while a stack example needs three. Where did those two loads go? The input placement has moved outside the counted body. That neat ADD is exactly the sort of thing I can get carried away with, I can almost see the smaller loop already, then I remember the loads, moves and argument setup. The runtime dispatches each instruction to the code that executes it. Fewer trips through that step can be useful, but I need to count the trips I moved elsewhere too.
And after the addition? The stack is also a useful way to describe temporary lifetime. An intermediate value remains there until an operation consumes it. The representation gives the runtime a regular place to put results rather than keeping the original tree traversal active.
Naming slots gives the compiler more choices too. A value can be read again without being pushed onto a stack again as long as it still occupies a slot and remains valid for that read. The compiler then has to allocate and reuse those slots and keep enough information to know when reuse is safe.
The runtime instruction is pleasantly short. Arranging the slots becomes the compiler's work, including deciding when they can be reused. That's the work I'm taking on with this preference.
Use the same expression
Let's give the machines a slightly less tiny expression:
a + b / (a + b) * (c + d)
Set a = 3 and b = 1 with c = 2 and d = 6, and each machine should reach five. The division gives a quarter, which we multiply by eight to get two. Add the outer three and we're there.
The explorer uses an intentionally plain encoding. It includes every input read, performs no optimisation and gives each register result a fresh slot. That lets us follow the same work through all three machines.
The register trace records which earlier slots each operation reads, while the stack trace shows the stack after each instruction. For the tree, completed visits follow the children before their parent.
All three use the same numeric arithmetic in this teaching model. If you choose values that make a + b zero, the division exposes JavaScript's non-finite results. Every machine gets the same ones.
I'm choosing explicit slots for my Go interpreter. The compiler knows an operation's types. Naming the storage too gives the runtime a concrete job, and gives me a way to inspect its inputs without following a tree of calls.
That leaves me managing slots, temporary values and call frames, the state needed by each active function call. Some variables need to outlive the call that created them too, so the simple instruction has moved a lot of work into my compiler. I still like it. I can see where the work goes now.