Download Game! Currently 124 players and visitors. Last logged in:ViittaRauskisDoloresWarund

Blitzer's Blog >> 72224

Back to blogs index
Posted: 10 Sep 2026 04:47 [ permalink ]
3. Building the VM Execution Engine
The VM now requires two stacks: a Data Stack (for mathematical operations and
passing arguments) and a Call Stack (for managing execution flow and scoping).
function executeVM(mainBytecode) {
  const dataStack = [];
  const globalEnv = {};
  
  // The Call Stack tracks nested execution contexts (Frames)
  const callStack = [{ 
    ip: 0, 
    code: mainBytecode, 
    env: globalEnv 
  }];

  while (callStack.length > 0) {
    const frame = callStack[callStack.length - 1]; // Peek at the current
frame
    
    // If we've reached the end of the current function/script, pop the frame
    if (frame.ip >= frame.code.length) {
      callStack.pop();
      continue;
    }

    const inst = frame.code[frame.ip++]; // Fetch instruction and increment IP

    switch (inst.op) {
      case Opcodes.CONST: 
        dataStack.push(inst.val); 
        break;
        
      case Opcodes.LOAD:
        // Prototype chain allows falling back to global scope if not in local
        if (!(inst.val in frame.env)) throw new Error(`Undefined:
${inst.val}`);
        dataStack.push(frame.env[inst.val]);
        break;

      case Opcodes.STORE:
        frame.env[inst.val] = dataStack.pop();
        break;

      case Opcodes.MAKE_FUNC:
        // Push the Code Object to the stack
        dataStack.push(inst.val); 
        break;

      case Opcodes.CALL:
        const func = dataStack.pop(); // Pop the Code Object
        
        // Create a new local environment inheriting from global (or parent)
        const localEnv = Object.create(globalEnv);
        
        // Pop arguments off the data stack in reverse order and bind them
        for (let i = inst.argCount - 1; i >= 0; i--) {
          localEnv[func.params[i]] = dataStack.pop();
        }
        
        // Push the new Call Frame
        callStack.push({ ip: 0, code: func.code, env: localEnv });
        break;

      case Opcodes.RET:
        // The return value is whatever is currently sitting on top of the
data stack.
        // We pop the Call Frame, returning execution to the previous IP.
        callStack.pop();
        break;

      case Opcodes.ADD:
        dataStack.push(dataStack.pop() + dataStack.pop());
        break;
        
      case Opcodes.MUL:
        dataStack.push(dataStack.pop() * dataStack.pop());
        break;
    }
  }

  // The final result of the program rests on top of the data stack
  return dataStack.pop();
}

Why this architecture scales well
By separating the compilation of Code Objects from the main execution thread,
the VM handles function calls exactly like standard data manipulation. Because
local environments utilize Object.create(globalEnv), lexical scoping and
variable shadowing are handled natively by the JavaScript engine's own
prototype chain, avoiding the overhead of traversing a complex custom scope
array during LOAD operations.