That stray 1 appearing in the middle of your thousands is an absolutely
brilliant catch!
You just experienced firsthand what makes shared memory both incredibly
powerful and slightly dangerous. You successfully peeked into the exact
"micro-state" of the virtual machine's registers (our dataStack)
mid-calculation.
Here is exactly what happened, and it reveals two important things we need to
patch.
Mystery 1: Why did it print 1?
When I gave you the consolidated file in the previous step, I accidentally
left the instructionsRun % 5 === 0 block in the VM loop. Even though you added
the YIELD opcode, that modulo check was still firing every 5 instructions and
writing whatever happened to be on the top of the stack to the buffer.
Look at the AST for our addition: ["+", "n", 1]
This compiles to:
* OP_LOAD "n" (pushes the big number, e.g., 2626)
* OP_CONST 1 (pushes 1 to the stack)
* OP_ADD (pops both, pushes 2627)
Your main thread happened to poll the lock-free buffer at the exact nanosecond
the modulo check fired on Step 2! The top of the stack was literally the 1
waiting to be added.
The Fix: Simply delete the if (instructionsRun % 5 === 0) { ... } block from
executeVMWorker. We only want OP_YIELD publishing state.
Mystery 2: The Hidden do Block Memory Leak
This anomaly reveals a hidden bug in how we compile do blocks.
In a stack machine, every single expression leaves exactly one result on the
data stack. If a do block evaluates 5 expressions, it pushes 5 results to the
stack. But a do block is only supposed to return the result of its final
expression!
Because we didn't clean up, your dataStack array was actually growing by 1
element every single loop, filled with old n values. TCO was keeping the Call
Stack perfectly flat, but the Data Stack was quietly leaking memory.
To fix this, we must introduce an OP_POP instruction to discard the results of
intermediate expressions.
The Patch
1. Add the Opcode:
Add POP: 'OP_POP' to your Opcodes object at the top.
2. Patch the Compiler:
Find your if (op === 'do') block and replace it with this to ensure it cleans
up after itself:
if (op === 'do') {
args.forEach((arg, index) => {
compile(arg, bytecode);
// Pop the result of every expression EXCEPT the final one
if (index < args.length - 1) {
bytecode.push({ op: Opcodes.POP });
}
});
return bytecode;
}
3. Patch the VM:
Inside your switch (inst.op) statement, add the rule to discard the value:
case Opcodes.POP:
dataStack.pop();
break;
With those two fixes applied, your VM is completely memory-safe. It can run
infinite recursion with a perfectly flat Call Stack and a perfectly flat Data
Stack, while explicitly publishing its state to the gateway without any race
conditions!