Master the fundamental concepts of build a bytecode vm through this focused micro-challenge.
You have read the whole brief, and the concepts above stay free on every task. Writing and running the code needs a plan.
Three hints are available for this task, revealed one at a time inside the code workspace so you can struggle productively before seeing them.
Every task includes starter code, theory, and hidden tests so you can implement and verify locally in the browser.
How it worksA compiler walks source text and emits opcodes your VM already understands. Recursive descent is the standard approach for small languages: one parsing function per grammar rule, each emitting instructions as it goes.
Expression compilation is postfix-friendly on a stack VM:
Control flow needs jump patching:
For example, `if 10 > 5 print 1 else print 0` compiles to a comparison, conditional jump, two print blocks, and two patched addresses.
This pipeline mirrors CPython's `compile.c` and Bob Nystrom's clox compiler in Crafting Interpreters.
Emit instructions in a single pass where possible, but keep the bytecode buffer growable. Real compilers use chunked buffers or vectors because function size is not known until parsing finishes. Reserve space for jump operands when you emit branch placeholders.
This exercise asks you to build a tokenizer, recursive-descent parser, and bytecode emitter with jump backpatching. You will implement compilation for arithmetic, `if/else`, and `print`, then run the result in your interpreter.
Write a single-pass compiler from a tiny language to this subtrack's stack bytecode, then run the result. The compiler has a tokenizer, a recursive-descent parser with correct precedence, variables mapped to local slots, and if/else and while compiled to labels and conditional jumps. It prints the generated assembly, then the program's output.
cLoading…
Expressions from lowest to highest precedence: one optional comparison (<, >, ==, non-associative), then + - (left-associative), then * / (left-associative), then unary - and primaries: integers (0..2147483647), variables and ( expr ).
| Source | Code |
|---|---|
| number | PUSH n |
| variable | LOAD slot |
a OP b | code for a, code for b, then ADD SUB MUL DIV LT GT EQ |
-x | code for x, NEG |
let / assignment | code for the expression, STORE slot |
print e | code for e, PRINT |
if (c) A else B | c, JZ Lelse, A, JMP Lend, Lelse:, B, Lend: (without else: c, JZ Lelse, A, Lelse:) |
while (c) B | Ltop:, c, JZ Lend, B, JMP Ltop, Lend: |
Labels are numbered L0, L1, … in the order they are allocated: if allocates its else label first (and its end label only when an else follows), and while allocates top then end. The program ends with HALT.
The listing (instructions indented two spaces, labels on their own lines), then ; N instructions, V variables (labels are not counted), --- run ---, and the program's output. At run time, int32 arithmetic wraps, INT_MIN / -1 gives INT_MIN, and division by zero prints runtime error: division by zero.
Compile errors stop everything and print one line, error at OFFSET: MESSAGE, where OFFSET is the character position in the input (0-based):
unexpected character Cnumber too largeexpected 'X' but found TOKEN (TOKEN is end of input at the end)expected an expression but found TOKENexpected a statement but found TOKENexpected a variable name but found TOKENundeclared variable NAMEvariable already declared: NAMEtoo many variables (8 max): NAMEInput:
cLoading…
Output:
cLoading…
Hidden tests cover nested if inside while, if without else, precedence and left-associativity (10 - 3 - 2, 2 + 3 * 4), unary minus, division by zero at run time, and a compile error for a redeclared variable.