Wado

WEP: Normalized IR (NIR) Layer

Context

The compiler pipeline from lower to wir_build runs entirely on FlatPackage with TIR-shaped function bodies:

... → link → monomorphize → erase → lower → optimize → wir_build → ...
                                    └─── FlatPackage (TIR bodies) ───┘

lower/ is a series of FlatPackage → FlatPackage transformations (wide_int, pattern, globals, boxing, closure, string, value_copy). The output is structurally the same type as the input.

This produces three problems that compound over time:

A second pressure reinforces the first. Removing the AST-level desugar phase (see LSP architecture) would push additional variants into TIR. Without a downstream type boundary, every optimize pass would grow further. A boundary at lower contains the blast radius.

Decision

Introduce NIR (Normalized IR) — a separately-typed body IR. lower/'s output type changes from FlatPackage to NirPackage. optimize/ and wir_build/ consume NirPackage.

What NIR Is

NIR is structurally derived from TIR. At the moment NIR lands, its expression, statement, type, and container shapes are identical to their TIR counterparts. They are distinct Rust types — separate enums, separate visitor traits — but they represent the same data.

The content of this Decision is the type boundary, not a shape redesign:

What NIR Is Not

Why "Normalized"

The name signals that NIR is the canonical form of a function body after lowering has run. "Normalized" captures the subtractive direction: only variants that survive lowering are part of NIR's enum, and TIR is free to drop variants that exist solely for post-lower passes. Both enums shrink toward their minimum useful shapes, while semantic behaviour stays unchanged.

(The Mesa shader compiler also names its IR NIR. The collision is in a different domain.)

Implementation

Source files

File Description
nir.rs NIR metadata: functions, globals, params, locals, and the shared leaf enums the body nodes reference.
nir_arena.rs The body itself — the skeleton arena and its value operands.
nir_visitor.rs The immutable visitor over arena nodes.
nir_unparse.rs NIR → pseudo-Wado renderer for wado dump --nir.

A function body is not a tree of owned nodes. It is the per-function skeleton arena, and pure operand positions carry hash-consed value ids rather than sub-expressions — see NIR Optimizer Architecture. That is a representation choice inside NIR, not a change to the boundary this WEP draws.

Data sharing with TIR

NIR reuses, without copying, everything whose identity is not expression shape: the post-erase TypeTable (types are interned globally, not per IR), symbol and entity ids, the name interners, and Literal. It owns its own node kinds and its own visitor. This is the convention WirPackage already set: WirTypeId borrows TIR-level type identity while WirInstr is a fresh enum.

Lower phase role

lower is plan + translate. plan mutates the type table, pushes additive synthesized entities into FlatPackage, and edits function declaration shapes (parameter shadows, lifted mut bindings); every TIR body rewrite — boxing's &local / *ref forms, closure's Closure → StructLiteral, value-copy insertion — runs inside the translate fold, consulting the plan structs the analysis built. So TIR is read-only between plan and translate at the expression-shape level.

Two body rewrites sit outside the fold.

lift_mut stays an analyze step in plan. The Let mut original = fresh_local statements it synthesises are themselves value-copy-eligible, and the fold is what decides their copies, so they must exist before it runs.

Pattern lowering runs at the top of translate. It rewrites expression shape, so it cannot live in plan, and it mints the scrutinee and destructure temps whose copies the fold decides. Those temps come after the seed. A seed reading expressions cannot see them, and predicting them reproduces the same choice one pass later, so the seed reads declared types instead (Resource Ownership). Folding pattern lowering into the translator becomes possible once match-arm bindings are ordinary projections the fold already sees.

Inspection

wado dump --nir renders NirPackage as pseudo-Wado, paralleling --tir and --wir.

Pattern-derived forms

Three forms coexist in NIR, each with a distinct role. This is settled design, not a migration in progress.

The variant trio is permanent in both TIR and NIR. Rewriting it into Match was tried and rejected: ~30 optimize passes pattern-match the trio directly, and folding it into Match arms scatters the scalar fact across statements and forces every pass to re-derive it — a measured regression. cm_binding's uses (the CM discriminant byte at offset 0, and statically-known payload extractions in lift / lower helpers) are expression-value uses of exactly this kind: correct and final, not migration debt.

TirStmtKind::LetDestructure stays for the same reason — the elaborator emits it and NIR preserves it for the multi-value core-builtin optimisation.

Additions

NIR's node set is not frozen. A node may be added when it makes a construct's canonical form more analyzable — normalization, not the re-introduction of a lowered concept.

The standing example is ArrayLiteral: list literals reach NIR as imperative builder push sequences, so NIR could not see a constant array as a value at all. optimize::array_literal materializes the node and wir_build lowers it to array.new_fixed. See NirExprKind::ArrayLiteral — a NIR-Materialized List Node.

Consequences

Benefits

Trade-offs

Out of scope

See also