Bytecode & VM: Making It Run
Work through examples, try the reasoning yourself, then put it into practice.
View SlidesBefore you start
- Finish v2 parsing and name analysis.
- Trace expression postfix instructions.
What you will practice
- Connect each source statement to stack and scope effects.
- Patch absolute jumps and preserve caller operands across function execution.
- Distinguish a VM stack result from discarded source expression statements.
In this lesson
Lesson
3 min readBytecode & VM: Making It Run
This module contains two exercises: compile a valid v2 Program, then execute
its Bytecode. Keep the boundary visible: the compiler consumes an AST and
emits instructions; the VM consumes only instructions and values. Calls are
still CALL name arity, with a name-based function table.
Worked reconstruction
Compile let x=3; while (x>1) { x=x-1; }:
0 PUSH_NUM 3 1 DEFINE_VAR x
2 LOAD x 3 PUSH_NUM 1 4 GT
5 JUMP_IF_FALSE 13
6 ENTER_SCOPE 7 LOAD x 8 PUSH_NUM 1
9 SUB 10 STORE x 11 EXIT_SCOPE
12 JUMP 2
A target of 13 is the legal one-past-end exit in this code slice. At x=3 and
x=2 the test consumes two numeric operands and produces true; the conditional
jump consumes that boolean. Each body returns to the same stack height and
exits its scope. At x=1 the jump exits. The final operand stack is empty, so
Run returns nil. Changing the back edge from 2 to 0 would reset x forever.
Stack, scopes, and calls
For binary operations pop the right operand first, then the left. STORE
consumes the value and updates the nearest existing mutable binding; it does
not create a new binding. ENTER_SCOPE/EXIT_SCOPE bracket blocks.
JUMP_IF_FALSE pops a condition and branches only for false or nil. Zero and
empty string are truthy here.
For CALL difference 2, arguments were pushed in source order. Popping them
must restore that order when binding parameters. The callee uses a local scope
with access to globals, not the caller's arbitrary block locals; v2 has no
closures. A call leaves one result for the caller. A compiler-generated
expression statement then emits POP, discarding that result. Thus a source
program ending in difference(9,2); normally returns nil, while hand-authored
Main bytecode leaving a 7 on its stack returns 7.
Implement in observable slices
First support literals and arithmetic, then binding/scopes, then jumps, then calls and returns. For each step trace instruction index, stack, and binding changes. Underflow, undefined loads, const writes, bad calls, type mismatches, and division by zero should fail clearly. The API does not require a public trace switch: a temporary local trace is a debugging tool, and must not change execution behavior.
Debugging lab
A while loop leaves one condition value on the stack after every test, and a program ending in f(); unexpectedly returns f’s result. Identify the two missing stack effects.
Compare your diagnosis after attempting the lab
JUMP_IF_FALSE must consume the condition. The compiler must emit POP for an expression statement. Both branches and loop back edges should preserve the expected stack height; the final source expression is not an implicit program return.
Transfer task
Replace the worked loop body with a nested block containing a shadowing declaration. Trace which x each LOAD/STORE sees and verify the outer loop still progresses. Independently hand-author bytecode computing 17-5 to test right-then-left pops.
Carry forward
Carry an executable Bytecode object into BC1 serialization. Preserve the instruction-index trace as a comparison point after decoding.
Build compile-bytecode, then bytecode-vm. 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.
- Step 1
Compile to Bytecode
- Step 2
Bytecode VM
- Step 3
Bytecode & VM: Making It Run Checkpoint
Predict fresh artifacts, diagnose a broken invariant, and test a version boundary. Select one answer per multiple-choice question; numeric responses use the stated integer format. Use review links to revisit the relevant representation, then attempt the independent transfer task.