Parsing turns source into a tree; compiling turns that tree into a flat sequence of instructions a stack machine can actually execute. This is the same shape as the Bytecode Stack Machine exercise from Language Runtimes -- if you've done that one, you already know the target instruction set.
Node shapes: ("num", value) a literal; ("neg", operand) unary negation
(exactly ONE child); ("binop", op, left, right) a binary operation with op
in "+", "-", "*", "/".
Implement compile_ast(node): walk the tree POST-ORDER -- compile all of a
node's operands first, then emit the operator -- and append a final PRINT so
the result is observable. Emit:
("PUSH", value)for a literal("NEG",)for unary negation, once its ONE operand is already compiled("ADD",)/("SUB",)/("MUL",)/("DIV",)for the corresponding binop, once BOTH operands are already compiled
Example
compile_ast(("binop", "+", ("num", 2), ("num", 3)))
# -> [("PUSH", 2), ("PUSH", 3), ("ADD",), ("PRINT",)]
The missing link: not every node has the same shape
Everything up to now (the Parser exercise, the first version of this compiler)
only ever dealt with binops -- two children, always. ("neg", operand) breaks
that assumption: it's a node with exactly ONE child. A compiler that blindly
does _, op, left, right = node on every non-literal node crashes with a
ValueError (too few values to unpack) the moment it meets a neg node -- the
dispatch on node[0] has to check for "neg" as its own case, compile its
single operand, and emit NEG, all before falling through to the binop
unpacking. This is the real shape of a compiler's node dispatch: every distinct
node KIND needs its own case, not just every distinct operator.
Constraints
- Post-order matters: for
(2 + 3) * 4, BOTH operands of*must be fully compiled (including the nested+) before theMULinstruction is emitted. neg's ONE operand must be fully compiled beforeNEGis emitted -- same post-order principle, just with one child instead of two.
Real-world grounding
This is the same conceptual step as LLVM lowering its own IR to a target's
instruction set, just far simpler: llvm/llvm-project
-- specifically the instruction selection / codegen passes that turn LLVM IR
trees (which also mix unary, binary, and n-ary node shapes) into a flat
machine-instruction stream.