Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Partial Evaluation

Partial evaluation is milang’s core compilation model. There is no separate optimisation pass — the compiler itself evaluates every expression whose inputs are known at compile time and emits C code only for what remains. The result is that high-level abstractions (helper functions, configuration records, computed constants) often carry zero runtime cost.

How it works

When the compiler processes a binding it walks the expression tree with a recursive reducer (reduceD). At each node it checks whether the operands are concrete — literal integers, floats, strings, lambdas, or records whose fields are themselves concrete. If they are, the expression is evaluated immediately and replaced by its result. If any operand is unknown (a function parameter, an IO result, etc.) the expression is left as residual code for the C back-end to emit.

-- Fully reduced at compile time:
x = 6 * 7              -- becomes: x = 42
f a = a * 2
y = f 21                -- becomes: y = 42

-- Stays as residual code (parameter unknown):
double a = a * 2        -- emitted as a C function

SCC dependency analysis

Bindings are sorted into strongly connected components so that each group is reduced in dependency order. Mutually-recursive bindings land in the same SCC and are handled together.

Depth-limited recursion

Recursive functions are unrolled only when every argument is concrete, and reduction is capped at a fixed depth (128 steps). This prevents the compiler from looping on unbounded recursion while still collapsing finite recursive computations at compile time.

Zero-cost abstractions

Because the reducer runs before code generation, any abstraction that is fully known at compile time disappears entirely from the output:

-- Configuration record — reduced away at compile time
config = {width = 800; height = 600}
pixels = config.width * config.height
build =  {target = c, os = linux, arch = x86_64}
config =  {width = 800, height = 600}
pixels = 480000

The binding pixels is reduced to the integer 480000 before any C code is generated. No record allocation, no field lookup — just a constant.

Inspecting the reducer output

milang ships two commands for inspecting what the compiler sees:

milang dump file.mi      -- parsed AST (before reduction)
milang reduce file.mi    -- AST after partial evaluation (what codegen sees)

Comparing the two on the same file shows exactly which expressions were collapsed and which remain as residual code. This is the primary tool for understanding compile-time behaviour.

What stays as residual code

Anything that depends on a value unknown at compile time is left for the C back-end:

main world =
  line = world.io.readLine    -- runtime IO — cannot reduce
  world.io.println line       -- emitted as C call

Function parameters, IO results, and any expression transitively depending on them are residual. Everything else is reduced.