Passes and optimization levels

tinyc/driver.py builds the pipeline; tinyc/passes/manager.py runs it.

-O1:  shape-infer, FIXPOINT{canonicalize, const-fold, cse, dce}, fuse, range-check, [legalize-<target>]
-O0:  shape-infer, range-check, [legalize-<target>]

The four simplifications run as a fixpoint group (passes.manager.Fixpoint): they enable each other, so the whole group repeats until a full round changes nothing. A single sweep is not enough: CSE can merge an op into an earlier one and leave a commutative add with its operands in non-canonical order (Hypothesis found this; tests/passes/test_manager.py keeps the shrunk program). Every sub-pass is still verified, recorded and dumped on every round, so --print-after-all shows each round.

Pass What it does
shape-infer Recomputes every result type from its operands (repairs parsed or hand-edited IR).
canonicalize Broadcast add puts the matrix first; same-shape add orders operands by definition order; relu(relu(x)) becomes relu(x).
const-fold Evaluates ops whose operands are all constants, using the oracle’s own eval_op. A constant expression that overflows i32 is a compile error.
cse Merges identical ops and identical constants (same data and dtype).
dce Removes ops that never reach an output. Inputs are always kept.
fuse matmul -> add(bias[N]) -> relu becomes one matmul{epilogue=bias,relu}; any prefix works (bias, relu, bias,relu). A stage is absorbed only if the previous result has exactly one use and is not an output. The fused op sits where the last absorbed op was and takes that op’s source location (the outermost expression).
range-check Interval analysis: proves every value fits in i32 or rejects the program (below).
legalize-tpu Every matmul operand must be an i8 input or constant.
legalize-gpu Every op result needs at most gpu_max_threads (1024) threads; total buffers must fit gpu_mem_words (a conservative bound).

Range analysis

Verification

CLI

tinyc examples/mm_bias_relu.tiny --target gpu --print-after-all --verify-each --time-passes
tinyc examples/mm_relu.tiny -O0                 # no optimizations
tinyc examples/mm_relu.tiny --disable fuse      # switch a pass off

Dumps and timings go to stderr; the final IR goes to stdout. Exit codes: 0 ok, 1 compile or legalization error, 2 usage error, 3 internal compiler error (a pass broke the program).

Tile passes (tiny-tpu backend)

After lowering to Tile IR (tinyc/tpu/lower.py) the -O1 pipeline runs skip-zero-passes: a pass whose A or B window is a constant all-zero tile is removed together with its accumulation (its product is zero). If that accumulation carried the output tile’s epilogue, the epilogue moves to the previous kept accumulation, becomes a standalone epilogue (bias present), or disappears (relu only: relu of the zero-initialized tile is a no-op). Pass ids are renumbered. --verify-each validates the schedule after lowering and after each tile pass, by interpreting the emitted JSON.

GPU backend stages

After the graph pipeline, compile_gpu_program runs: memory plan (gpu/memplan.py) -> lowering to Kernel IR (gpu/lower.py, one kernel per op) -> kernel passes (gpu/kernel_passes.py) -> verify -> flatten -> linear-scan register allocation (gpu/regalloc.py) -> .tgpu emission (gpu/emit.py).

-O Kernel passes
0 none (the naive kernels)
1 licm, strength-reduce, kernel-dce
2 licm, strength-reduce, shared-tile, unroll, fold, kernel-dce

With --verify-each the structured kernels are interpreted against the numpy oracle after lowering and after every kernel pass, and the emitted assembly is run on the real simulator against the oracle. --print-after-all shows lower-to-kernel, each kernel pass, and regalloc-emit. Each pass can be disabled by name. The measured effect of each is in ARCHITECTURE.md.