Skip to content

Optimization

After your program type-checks, saQut can run an optimizer that rewrites the code into a simpler-but-equivalent form. This page explains exactly what it does, what it leaves alone, and how to watch it work.

Two principles shape the whole design:

  1. The optimizer never changes behavior. An optimized program produces the same output as the unoptimized one: optimization is a rewrite, not a reinterpretation.
  2. It works on a clone. Your original AST is preserved untouched; the optimizer transforms a copy. That’s why you can ask for the “before” and the “after” and diff them.

Every stage is inspectable, and optimization is no exception. Compare the tree before and after:

Terminal window
saqut ast myfile.sqt # original AST
saqut ast myfile.sqt --optimized # AST after optimization

Nodes the optimizer computed are tagged, so the change is visible rather than hidden:

VariableDecl (y : int)
Literal {14} integer [folded] ← was 2 + 3 * 4

If an expression can be computed at compile time, the optimizer computes it once and replaces the whole expression with the result. Your program then carries the answer instead of the recipe.

int x = 2 + 3 * 4; // folded to: int x = 14;

Folding respects precedence and associativity, so it always produces the value the program would have computed anyway. It applies to:

  • Integer arithmetic: 2 + 3 * 414
  • Boolean / logical expressions: true && falsefalse
  • Comparisons of constants: 5 > 3true

Because folded sub-expressions can unlock further folding, the optimizer runs in a fixpoint loop: it keeps folding until a full pass changes nothing, up to a safety cap on the number of rounds. So a nested constant like (2 + 2) * (3 + 3) collapses all the way to 24, not just one level.

Code that can never run is removed. The classic case is anything after a return, break, continue, or throw:

int f() {
return 1;
int x = 2; // removed, never reached
}

When DCE finds unreachable code, it also emits a W003 warning so you know something was dropped (it’s usually a mistake):

warning [W003]: this code is never reached (after return/break/continue)

W003 appears when the optimizer runs, e.g. saqut ast myfile.sqt --optimized.

While reasoning about constants, the compiler can also warn about a constant division by zero before the program ever runs. That is the W002 warning.


The boundaries are as important as the transforms. saQut’s optimizer is deliberately conservative. The goal is predictable speed with a determinism guarantee, not the removal of every possible cycle. Knowing what the optimizer leaves alone tells you what not to expect it to rewrite.

Integer constants fold; floating-point constants do not:

int a = 2 + 3 * 4; // → 14 (folded)
float b = 2.0 + 3.0 * 4.0; // stays as an expression, computed at runtime

Floating-point results can depend on rounding and evaluation order, so folding them at compile time risks producing a value subtly different from what the VM would compute. saQut keeps float math where you can see it, in the running program, rather than risk a mismatch.

Variables are not folded across statements

Section titled “Variables are not folded across statements”

Folding only looks at an expression’s own literals. It does not track a variable’s value from a previous line:

int a = 5;
int b = a + 1; // NOT folded to 6; 'a' is a variable, not a literal

There is no constant propagation, no copy propagation, and no common-subexpression elimination.

Deliberately absent: loop unrolling, function inlining, strength reduction, vectorization, and reordering that could change floating-point results or observable behavior. These are exactly the transforms that make an optimized program behave differently from a naive one, and behaving identically is the point.


saQut’s reference backend is a deterministic bytecode VM: the same input must always produce the same output, and future backends (a MIR JIT, an embedded-runtime AOT build) are validated against the VM with differential testing: every backend must agree on every program. Aggressive, float- reordering optimization is the natural enemy of that guarantee. By keeping the optimizer small and behavior-preserving, every backend stays in agreement and every stage stays inspectable.

If you want raw throughput, that job belongs to the future JIT/AOT backends operating on the IR, not to a frontend that rewrites your source into something you can no longer recognize.