Capstone: Source to Bytecode to VM

Work through examples, try the reasoning yourself, then put it into practice.

View Slides

Before you start

  • Complete v2 lexer, parser, analyzer, compiler, VM, and BC1 helpers.

What you will practice

  • Wire exact helper return types and stop at the earliest failing stage.
  • Reconstruct a full source-to-artifact trace with observable runtime checks.
  • Use independent boundary evidence instead of treating round trips as sufficient.
In this lesson

Lesson

3 min read

Capstone: Source to Bytecode to VM

This is the v2 integration gate. The provided helpers surround a small CompileAndRun(src) function, but the learning work is to justify every edge:

Lex → Parse → Analyze → Compile → Encode → Decode → Run

Lex/Parse/Decode/Run return errors; Analyze returns a slice of diagnostics; Compile and Encode do not return errors in this API. Do not invent tuple shapes or a typed graph. Analysis validates the existing AST; BC1 is the artifact boundary; there is no linker yet.

Worked reconstruction

Use fn difference(a,b){ return a-b; } let result=difference(9,2);. Parsing creates a function declaration and a variable declaration. Analysis resolves the function and two parameters, then accepts the two-argument call. Compilation puts LOAD a; LOAD b; SUB; RETURN in the function body. Main pushes 9, pushes 2, calls difference with arity 2, then defines result. BC1 encodes those instructions; Decode reconstructs them before Run is called.

The result binding is 7, but the VM's final operand stack is empty, so the capstone returns nil. To make that internal value observable through a source program, append if (result != 7) { 1/0; }. Successful completion now proves that this assertion path did not run. It does not turn result into a top-level return value.

Earliest failure table

Source fragment First failure
@ Lexing
let x = ; Parsing
return 1; Analysis: return outside function
fn f(a){} f(); Analysis: arity
let x="a"-1; Runtime: numeric operands required
1/0; Runtime: division by zero

If analysis reports several errors, return its first one and stop. Calling Run anyway would execute a program the front end already rejected. Use a fresh runtime state for each invocation. The same source should encode consistently and produce the same success/error behavior on repeat runs.

Integration gate

Before continuing to v3, retain a valid source, its expected AST outline, selected instructions, and one assertion about execution. Also retain a lexical, syntactic, semantic, and runtime failure. Diagnose each without reading the full reference implementation. These artifacts are your regression baseline when later versions change function representation and runtime values.

Debugging lab

A capstone returns the second analyzer error and calls Run directly on Compile’s object. Both ordinary examples seem to work. Name two contract violations and tests that distinguish them.

Compare your diagnosis after attempting the lab

Return the first analyzer error in encounter order and stop. The required Encode→Decode boundary must also be exercised; malformed-format/round-trip fixtures expose bypassing it. Runtime success alone cannot prove either contract.

Transfer task

Create a program with an undefined name in a branch that would not execute at runtime, and another with a valid branch guarding division by zero. Explain why static name traversal and dynamic branch execution give different failure outcomes.

Carry forward

Carry the v2 regression baseline into v3. The next milestone replaces named-call assumptions with closure values and extends the heap; it does not discard parsing, stack, or phase-boundary discipline.

Build compile-and-run. Keep a before/after trace of one failing test. Use the checkpoint to diagnose specific skills; passing it does not replace implementing and testing the artifact.


Practice sequence

Use the order below for a guided path, or choose an item to revisit. A pass records a successful attempt; try a fresh example later to check what you can reconstruct.

Join Discord