cp-07 — Stack-VM Execution Engine
Status: ✅ Built —
mlrruns 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
Valuetagged union with a newFncase that carries ashared_ptr<Function>. - A
Functionrecord: name, arity, ownedChunk. - The
VMitself:- operand stack (
std::vector<Value>), - call-frame stack (
std::vector<CallFrame>), - globals table (
std::unordered_map<string,Value>), - a big
switch-based dispatch loop.
- operand stack (
- A
Compilerthat nests oneFunctionStateper function being compiled, sofn foo() { … }compiles to a fresh chunk with its own locals. - A driver
mlrthat compiles then runs.
Reading Order
CONCEPTS.md— the why: stack vs registers, dispatch cost, call frames, why closures need a separate mechanism.steps/— implementation walkthrough: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.