Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

Turning a tree into stack code (code generation)

After this lesson: I can turn a syntax tree into instructions for a stack machine.

55 min lesson10 commandsCompilers and computer architecture lab: gcc, flex, bison and RISC-V in one VM

Ten thousand orders an hour

The shop's price rule from the last lesson now runs for ten thousand orders an hour. Walking the tree each time is too slow.

So the shop translates the tree once into a flat list of tiny steps, and runs the list for every order. That translation is the last job of a compiler.

By the end of this lesson

Note

I can turn a syntax tree into instructions for a stack machine.

Key words

WordWhat it meansExample
Code generationturning a syntax tree into instructions
Stacka pile where the last value in is the first out
Stack machinea machine whose instructions take values from a stackPUSH 2
Bytecodeinstructions for a stack machine run by software
Optimisationchanging code so it does the same thing with less work-O2
Constant foldingworking out a sum at compile time6 * 7 becomes 42

The cells students fill in are left blank in this preview.

A stack machine keeps values on a stack

A stack machine keeps its working values on a stack. PUSH 2 puts 2 on top. ADD takes the top two off, adds them, and pushes the answer.

To turn a tree into stack code, generate each child's code first, then the node's own instruction. When the code runs, each child leaves its value on the stack, ready for the node.

Worked example: the code for 2 + 3 * 4

InstructionStack after it, top on the right
PUSH 22
PUSH 32, 3
PUSH 42, 3, 4
MUL2, 12
ADD14

The cells students fill in are left blank in this preview.

A code generator for + and *

Code: python
# Code generation: children first, then the operation.
def generate(tree, code):
    if type(tree) == int:
        code.append(["PUSH", tree])
        return
    generate(tree[1], code)
    generate(tree[2], code)
    if tree[0] == "+":
        code.append(["ADD"])
    else:
        code.append(["MUL"])


code = []
generate(["+", 2, ["*", 3, 4]], code)
for line in code:
    print(line)
# Change the tree to ["*", ["+", 2, 3], 4] and run again.

Python runs a stack machine too

Python does not walk your tree each time. It compiles the tree to bytecode, instructions for a stack machine inside Python, and runs those.

The module dis prints the bytecode of a file. The Runs write a one-line file, sum.py, and show its bytecode. Ignore RESUME at the top and the two lines at the end: every file has them.

Go into the compiler folder

Command
cd ~/compiler

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write a one-line program

Command
echo "total = price * count + 1" > sum.py

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Its bytecode

Command
python3 -m dis sum.py

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Worked example: the bytecode of sum.py

BytecodeOur stack codeWhat it does
LOAD_NAME priceLOAD pricePush the value of price
LOAD_NAME countLOAD countPush the value of count
BINARY_OP (*)MULTake two off, push the product
LOAD_SMALL_INT 1PUSH 1Push the number 1
BINARY_OP (+)ADDTake two off, push the sum
STORE_NAME totalNone yetTake the top off and bind it to total

The cells students fill in are left blank in this preview.

An optimiser does work at compile time

gcc makes code for a real processor, with registers as well as a stack. With -O2 it also optimises.

The Runs write the assembly twice, once plain and once optimised. Then grep -w looks for the whole number 49, the answer to square(7), in each. The program never says 49 anywhere. Watch where it turns up.

Assembly with no optimisation

Command
gcc -S -O0 -o plain.s hello.c

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Assembly at -O2

Command
gcc -S -O2 -o fast.s hello.c

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Lines with 49 in the plain version

Command
grep -c -w 49 plain.s

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

And in the optimised version

Command
grep -w 49 fast.s

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Where 7 is in the plain version

Command
grep -w 7 plain.s

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Python folds constants as well

Command
echo "total = 6 * 7" > folded.py

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

The bytecode of the folded sum

Command
python3 -m dis folded.py

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

A common mistake

Warning

Many people think optimisation runs the same instructions faster. At -O2 gcc rewrote the work: it worked out square(7) itself, so main never multiplies at all. The program prints the same line either way. An optimiser may change any instruction, as long as what the program does stays the same.

Going further

Add constant folding to your compile_tree. Before you generate code for an operation, check whether both children are plain numbers. If they are, work the sum out and emit one PUSH.

Fold from the bottom up, so ["+", 2, ["*", 3, 4]] becomes [["PUSH", 14]]. Then say why a tree with a name in it can only be folded in part.

About this lab and its source: Compilers and computer architecture lab: gcc, flex, bison and RISC-V in one VM

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.