3. Code Generation

AST-to-Bytecode Compiler

midcompiler-internals

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 the MUL instruction is emitted.
  • neg's ONE operand must be fully compiled before NEG is 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.

hints
ast_compiler.pysign in to run
Loading...
test result

This is a preview. Sign in to run this exercise against the real tests.