Wado

WEP: Wasm IR (WIR) Layer

Context

codegen.rs is 14,000+ lines and mixes three concerns:

  1. Type layout decisions: Mapping TIR types to Wasm GC types, registering struct/variant/array/tuple/closure types across 15+ phases with topological sorting and deferred registration
  2. Function-level analysis: Pre-allocating locals, scalarization analysis, scratch local computation, copy context setup
  3. Instruction emission: Translating TIR expressions to Wasm instructions

The existing wasm_plan phase (WEP 2026-02-03) moved Component Model analysis out of codegen, but the core problem remains: codegen is doing extensive TIR-to-Wasm translation and analysis that is tangled with low-level wasm_encoder API calls. There is no inspectable intermediate form between TIR and binary.

A complementary approach — TIR-Level CM Binding Synthesis — can reduce the CM-specific surface area of codegen independently.

Decision

Introduce WIR (Wasm IR) — a tree-structured intermediate representation between TIR and Wasm binary. WIR is close to Wasm semantics but retains enough high-level information to be readable and debuggable.

New Pipeline

lower → link → optimize → wasm_plan → tir_to_wir → wir_emit → wasm binary
                                           ↓
                                       WirPackage (inspectable via dump --wir)

tir_to_wir translates the optimized FlatPackage into a WirPackage. wir_emit translates WirPackage into Wasm binary bytes. wasm_plan remains unchanged and provides ComponentPlan metadata consumed by tir_to_wir.

What WIR Is

WIR is a tree-structured IR that maps almost 1:1 to Wasm instructions, but with these ergonomic improvements over raw Wasm:

  1. Named locals: Variables are referenced by name, not pre-allocated indices. Locals can be declared inline via DeclareLocal — the emit phase collects them and pre-allocates as Wasm locals.
  2. Named types: Struct, variant, enum, and flags types retain their source-level names and field/case names. These are Wado-level type definitions — the emit phase expands them (e.g., a variant becomes N+1 Wasm struct types).
  3. Wado-level value types: WIR uses Bool, Char, I8, U8, etc. instead of Wasm's i32-for-everything. The emit phase lowers to Wasm ValType/StorageType.
  4. Structured control flow: Blocks, loops, if/else are tree nodes (not flat instruction sequences with labels).
  5. Explicit value copy: Copy operations are explicit ValueCopy nodes rather than inline instruction sequences.
  6. TIR metadata preserved: Module source, source spans, attributes, generic instantiation info, and newtype origin are carried through for debugging and unparse.
  7. Unparse support: WIR can be rendered as pseudo-Wado for inspection via wado dump --wir.

What WIR Is Not

Implementation

Source Files

File Description
wir.rs WIR data structures: WirPackage, WirTypeDef, WirType, WirInstr, WirTypeId, WirFuncId, WirName, etc.
wir_unparse.rs WIR → pseudo-Wado rendering for wado dump --wir
wir_build.rs Pipeline entry: compile_with_wir(&FlatPackage) -> Vec<u8> — orchestrates build → emit → validate → component wrapping
wir_build/context.rs WirContext — builder that accumulates types, functions, and module-level entries during translation
wir_build/types.rs Type registration: translates TIR type definitions to Vec<WirTypeDef> with multi-phase topological sorting
wir_build/functions.rs Function collection: gathers imports, entry/library functions, methods, data segments, exports
wir_build/translate.rs Function body translation: converts TIR expressions/statements to WirInstr trees
codegen/emit.rs WirEmitter: converts WirPackage to core Wasm bytes via wasm_encoder
codegen/component.rs Component Model wrapping: builds component from core module

Key Design Decisions

WirTypeId / WirFuncId with Rc<str>

WIR instructions reference types and functions via lightweight IDs instead of embedding names in every instruction. WirTypeId { index: u32, fq: Rc<str> } gives O(1) Eq/Hash via integer comparison, O(1) Clone via Rc refcount, and readable Debug output via the fq name. Definitions use WirName { display, fq } with both short and fully-qualified names.

Wado-Level Value Types

WIR uses Bool, Char, I8, U8, I16, U16, Enum { type_id }, etc. instead of Wasm's i32. The emit phase lowers to ValType (locals) or packed StorageType (struct fields) depending on context. This eliminates the ValType/StorageType split at the WIR level and makes unparse output readable.

ValueCopy as Compound Instruction

Value copy involves complex dispatching (struct copy, array loop, variant discriminant check). A single ValueCopy { type_id, source_type, expr } node preserves semantic intent and lets the emitter choose the lowering strategy.

Tree-Structured Instructions

WIR uses trees where operands are children (not stack values). I32Add(StructGet { ... }, StructGet { ... }) is inspectable and debuggable. Flattening to stack-machine instructions is trivial (post-order traversal).

Unparse Format

wado dump --wir outputs pseudo-Wado:

struct Point { mut x: i32, mut y: i32 }

fn "run"() with Stdout {
    let p: ref null Point;
    p = Point { x: 10, y: 20 };
    call core:cli/println(block -> ref null String {
        ...
    });
}

fn "core:cli/println"(message) with Stdout {  // from core:cli
    ...
}

Principles:

Migration Plan

Strategy: Strangler Fig Pattern

The migration builds a complete WIR pipeline alongside the existing codegen, without modifying codegen.rs. Both pipelines consume the same &FlatPackage after wasm_plan. Once all tests pass, the old codegen is replaced and deleted.

                                  ┌→ codegen → wasm binary (existing, untouched)
lower → optimize → wasm_plan ─────┤
                                  └→ tir_to_wir → wir_emit → wasm binary (new, tested in parallel)

Benefits of this approach:

Phase 1: Scaffolding and Inspection (complete)

Created wir.rs (data structures), wir_unparse.rs (pseudo-Wado output), and --wir flag for the dump command.

Phase 2: Parallel E2E Test Infrastructure (complete)

Created compile_with_wir(&FlatPackage) -> Vec<u8> in wir_build.rs. Created parallel test harnesses (wir_e2e.rs, wir_progress.rs) gated by WADO_WIR_TEST=1 to track incremental progress. Both harnesses were removed after cutover; tests/e2e.rs is now the sole E2E test.

Phase 3: Core Translation (complete)

All translation steps implemented: type registration (structs, variants, enums, flags, arrays, tuples, closures, function types, rec groups), module skeleton (imports, globals, data, elements, exports, name section), function body translation (constants, locals, arithmetic, comparisons, casts, control flow, calls, GC ops, value copy, match, closures, globals), and Component Model wrapper (WASI imports, bundled modules, world exports). CM binding functions are handled as ordinary TIR functions via TIR-Level CM Binding Synthesis.

Phase 4: Cutover (complete)

Phase 5 (Future): Optimizer Migration

After WIR is stable, split optimizations into two levels:

lower → tir_optimize → tir_to_wir → wir_optimize → wir_emit

Development Workflow

Checking Progress

tests/wir_progress.rs runs all E2E fixtures through the WIR pipeline and reports aggregate pass/fail counts. It is gated by WADO_WIR_TEST=1 and always succeeds (informational only).

WADO_WIR_TEST=1 cargo test -p wado-compiler --test wir_progress -- --nocapture 2>&1 | grep -E '═|Passed|Failed|TODO'

This outputs a summary like:

═══════════════════════════════════════════════════════
  WIR Pipeline Progress (O2)
═══════════════════════════════════════════════════════
  Passed:   481 / 671  (71.7%)
  Failed:   190
═══════════════════════════════════════════════════════

Running Individual Fixtures

WADO_WIR_TEST=1 cargo test -p wado-compiler --test wir_e2e -- hello_world

Debugging with WIR Unparse

cargo run --bin wado -- dump --wir file.wado

This shows the full WirPackage as pseudo-Wado, allowing inspection of the planned Wasm output before binary emission. Use this to diagnose type registration issues, incorrect instruction translation, or missing functions — rather than relying solely on E2E test pass/fail.

Consequences

Benefits

Trade-offs