Code Optimization

An optimizer is a correctness-preserving transformer: it rewrites IR into a faster program that behaves exactly the same. Measure first, transform last, and remember that for a DSL the biggest speedups usually come from the language semantics, not the passes.

Two Principles

Safe-Only Transformations

Every optimization must be provably meaning-preserving under the language’s semantics — floating-point rearrangements, exception order, and integer overflow are the classic places where “obvious” rewrites break programs.

Measure, Then Transform

Profile on real inputs before writing a pass. Unoptimized bytecode is already fast enough for most DSL workloads; an optimizer with no measured target becomes the second system syndrome from Design Considerations.

Local Passes

Constant Folding

Compute constants at compile time: t1 = 2 * 3 becomes t1 = 6. The first pass everyone writes; also the first to be haunted by division by zero and overflow — handle them before folding.

Algebraic Simplification

x + 0, x * 1, x - x are the low-hanging fruit after folding. Each rule needs its “is this true in my semantics?” check (strings? NaN? unsigned?) — write the rules with the specification open.

Dataflow & Loops

Beyond single statements, information must flow: liveness (which values are still needed), reaching definitions (which assignment produced this value), and dominators underpin dead-code elimination, copy propagation, and loop hoisting.

Liveness in One Idea

A value is live at a point if its result is still needed later. Values that are never live lead to dead code, which is removed; liveness also feeds the register allocator from Code Generation.

Loop Basics

Hoist invariant computations out of loops and keep induction-variable strength in check. For DSLs, the honest move is usually: let LLVM (Phase 3) do these — the passes are well-understood but finicky.

The DSL Truth

Semantic Speed Wins

For a domain-specific language, the largest speedups are semantic, not syntactic: choose empowered operations (a fused matrix multiply instead of a loop), strong typing (Types & Values) so fallback paths vanish, and a good back-end target. Optimizing an awkward semantic is polishing a design error.

Example: Folding Before You Run

The whole philosophy, in a commented pass: constant expressions die at compile time.

fold.py (commented)

# fold.py — replace constant IR statements with their results
def fold(stmts):
    out = []
    for s in stmts:
        if is_const(s.lhs) and is_const(s.rhs) and is_safe(s.op):
            value = s.op.eval(s.lhs.value, s.rhs.value)   # compute NOW
            out.append(Set(s.dst, Const(value)))          # emit the answer
        else:
            out.append(s)                                  # keep it as-is
    return out

How to Read It

One rule, one safety check, one rewrite — that is a whole optimizer’s unit of currency. Add rules until profiling stops asking; then stop.

Next Steps

Continue the Roadmap

Phase 2 is complete: your language can be designed, parsed, analyzed, compiled, and run. Phase 3 hands the machinery to production compilers (LLVM IR, Triton, Mojo), and Phase 4’s AI languages show the same ideas at work in the wild.

Resources