cp-07 — Stack-VM Execution Engine

Status: ✅ Built — mlr runs MiniLang programs end-to-end on the stack VM.

This lab takes the bytecode Chunk produced by cp-06 and gives it a runtime: an operand stack, call frames, globals, and a dispatch loop. After cp-07 you have a real compiler + virtual machine pair — source goes in, output comes out, no AST walking at runtime.

Pipeline

source ──► Lexer ──► Parser ──► Resolver ──► TypeChecker ──► Compiler ──► Function(script) ──► VM
                                                                                 │
                                                                          (with --dump:
                                                                           Disassembler)

The compiler emits a single top-level Function named <script> whose chunk is just the program's bytecode. The VM bootstraps execution by calling that function on an empty stack. Every subsequent function call follows the same mechanism — the script is just function #0.

What You'll Build

  • A Value tagged union with a new Fn case that carries a shared_ptr<Function>.
  • A Function record: name, arity, owned Chunk.
  • The VM itself:
    • operand stack (std::vector<Value>),
    • call-frame stack (std::vector<CallFrame>),
    • globals table (std::unordered_map<string,Value>),
    • a big switch-based dispatch loop.
  • A Compiler that nests one FunctionState per function being compiled, so fn foo() { … } compiles to a fresh chunk with its own locals.
  • A driver mlr that compiles then runs.

Reading Order

  1. CONCEPTS.md — the why: stack vs registers, dispatch cost, call frames, why closures need a separate mechanism.
  2. steps/ — implementation walkthrough:
    1. Operand stack, values, dispatch loop
    2. Call frames and slot-base addressing
    3. Compiling functions as nested compilers
    4. Globals hash, locals slots, name resolution at runtime
    5. Control flow on a stack machine (jumps & loops)
    6. Closures and upvalues — sketch, deferred
    7. Runtime errors and stack traces
  3. src/cpp/ — the implementation.

Build & Run

cd src/cpp
cmake -S . -B build -G "Unix Makefiles"
cmake --build build -j

# Run a program from a file
./build/mlr program.ml

# …or pipe it
echo 'print 1 + 2 * 3;' | ./build/mlr

# Disassemble the script chunk before running
echo 'fn add(a, b) { return a + b; } print add(3, 4);' | ./build/mlr --dump

# Run tests
ctest --test-dir build --output-on-failure

Status

  • ✅ 19 VM tests, 41 sub-checks, 100% green.
  • ✅ Arithmetic, locals, globals, control flow, functions, recursion.
  • ✅ Compile-time errors (typecheck) flagged; runtime errors print line + message.
  • Closures over outer locals are deliberately rejected at compile time. Capturing globals works fine; the upvalue machinery lands in cp-12 when we build the JIT.

Prereqs

  • cp-06 complete (chunks, opcodes, compiler).

Outcomes

  • Run a compiled MiniLang program with no AST present at runtime.
  • Explain why CPython, JVM bytecode, V8 Ignition, and Lua share the same architectural sketch (stack + frames + dispatch loop + constant pool).
  • Diagnose and reason about stack-balance bugs — the single biggest source of hard-to-debug VM crashes.