Wado

WEP: NIR Interpreter (niri) Evolution Plan

Context

The Wado optimizer relies on constant folding to reduce literal-only expressions to a single literal node. Until now the folding logic lived inside optimize/const_folding.rs as a hand-rolled set of operator helpers, mixed with the NIR visitor that drove it.

This commit splits the engine out into a new top-level module, wado_compiler::niri ("NIR Interpreter"), exposing Interpreter::reduce(&NirExpr) -> NirExpr as the canonical API. const_folding.rs becomes a thin visitor that delegates each visited node's local rewrite to Interpreter::reduce_local.

This WEP records the planned trajectory so future contributors don't have to re-litigate the design every time we want to fold a richer expression.

Decision

niri evolves into a partial evaluator for NIR. The public surface keeps a single shape — reduce(&NirExpr) -> NirExpr — and each stage extends what kinds of expressions can be reduced. Beyond a single-process interpreter, a complementary wasm-CTFE backend runs full pure function calls through wasmtime, leveraging Wado's effect system as a type-checked purity gate.

Why two backends

niri (in-process) wasm execution
Sweet spot 2+3 → 5, identity simplification, branch pruning fib(20), lookup-table generation, full pure-call CTFE
Cost / call µs, memoizable ms (codegen + instantiate), amortized via module cache
Partial eval Yes (residuals) No (whole-call)
Coverage Whatever we hand-write All of Wado, for free

These are complementary, not alternatives. niri stays sub-quadratic and fine-grained; wasm execution covers anything niri would balk at.

Stages

Stage 0 — split & rename

Status: done.

Out of scope at Stage 0 (matches the previous behaviour, deferred to later stages): float-to-int and int-to-float casts (only int-to-int casts fold), heap-allocated Value payloads, and any cross-function reasoning. Stage 1 and onward extend the engine; Stage 0 is purely a relocation + API reshape with zero behaviour change.

Stage 1 — local environment + lattice

Status: done.

Stage 1.5 — memoization (deferred)

Status: deferred to Stage 3.

Stage 2 — if reduction

Status: done. match reduction is split into its own Stage 2.5 below; payload-aware variant matching is deferred to Phase B/C of that stage, since the lattice work for variant payloads is more involved than scalar if.

Stage 2.5 — match reduction

Status: Phase A done. Phases B and C are scoped but not implemented.

Match reduction is split into three phases by pattern shape, in increasing order of representation cost. The split lets us land scalar / payload-free matching today without committing to a heap-aware [Value] type that the WEP otherwise reserves for Stage 4+.

Phase A — payload-free patterns (done)

Phase B — definite-no enum / variant tag pruning (deferred)

Phase C — payload-aware variant matching (deferred)

Stage 3 — pure call inlining (in-process)

Status: done. Multi-stmt bodies, MethodCall / IndirectCall, and heap-aware return values stay deferred (call them Stage 3.5+).

Stage 4 — bounded loop unrolling

Stage 5 — wasm-CTFE backend (via CompilerHost)

Stage 6 — aggregate global & field/element projection

Status: planned. Independent of Stages 4-5; builds on Phase C's heap-aware [Value]. Motivated by Constant Object Globalization, which turns read-only constant aggregates into immutable module-scope globals and needs niri to see through them so the fold cascades.

Cost model

The in-process engine stays sub-quadratic by following the patterns production compilers use:

The wasm backend's cost is dominated by module compilation (~1-100 ms) and instantiation overhead (~µs-ms). Both amortize with the per-(fn, monomorph) Module cache, so a fib(_) invoked 100 times incurs one codegen.

Determinism

Consequences

Out of scope

Open questions

Resolved questions

Lattice ownership vs. Option<Value>

Resolved as part of Stage 1: niri owns Lattice { Unevaluated, Const(Value), NonConst }. Option<Value> is dropped — it conflated four meanings, making memoization unsafe to add later. Lattice is exposed via reduce_to_lattice; the Lattice::as_const() projection covers the simple "is this a literal?" case without re-introducing the ambiguity at the API surface.