Wado

Wado Optimizer

The optimizer rewrites the Normalized IR (NIR; see WEP: NIR Layer) in place before lowering to WIR, then runs a smaller set of WIR-level passes before Wasm emission. Pass span names used by WADO_LIST_PASSES / WADO_SKIP_PASS / WADO_DUMP_PASS_* carry a nir/ prefix.

The module-level doc in src/optimize.rs is the authoritative pass index and ordering; per-pass design detail lives in each pass's source. This document covers the architecture and gives a one-line summary per pass.

Philosophy

When WebAssembly provides a native instruction for a feature, prefer it over a complex compiler transformation — it keeps the compiler small, leverages the runtime JIT, and produces smaller output (e.g. select for branchless conditionals, array.copy/array.fill for bulk ops, br_table for dense matches).

Optimization levels

All levels run DCE on functions, types, and globals.

Flag Iterations Inline threshold Notes
-O0 0 N/A DCE only + match_to_switch + post-loop rewrites
-O1 2 4
-O2 (default) 10 13
-O3 30 32
-Os 10 13 strips the Wasm name section

The fixed-point loop exits early on convergence. The post-loop rewrites the Wasm backend depends on (select_lowering, multi_value_return) and match_to_switch run at every level, including -O0.

Architecture

Live value graph

Pure values are the optimizer's source of truth, not re-derived per pass. Every operand position in the skeleton arena is either a skeleton subtree (Operand::Expr) or a promoted pure value (Operand::Value) interned in the per-function value pool (Body::value_graph / body.values), hash-consed so congruent values share one ValueId. Operand promotion ("born as operands") puts a pure leaf's value straight in the operand, so a pass reads it off the operand instead of looking it up — there is no ExprId→value side-table. The graph is built once per function (lazily, on the first value query) and maintained in place across passes via e-class union, never rebuilt. Pure-value CSE therefore falls out of the pool (congruent values already share a node); constant folding (niri) reads pooled values, and bounds-check elimination (condition_implication) recognises them structurally. See WEP: The Live ValueGraph.

Worklist rewrite engine

Genuinely-local NIR rewrites run as [Rule]s on a worklist engine (nir_engine.rs) over one function's arena Body: a node is revisited only when an edit might have made it reducible, rather than via repeated whole-tree sweeps. The engine owns the session state (parent map, local use index, worklist) and a mutating edit API (replace_expr_kind, set_block_stmts, become_expr, alloc_*, clone_expr) that keeps that state coherent. Flow-sensitive passes that need per-block dataflow (field_scalarize, licm, tmpl_hoist, value_copy_demote, store_load_forward, the flow-sensitive half of const_folding) keep their own walkers. See WEP: NIR Rewrite Engine.

Unified peephole session

optimize/peephole.rs runs the position-flexible local rules — string_push, array_literal, elide_local, the environment-free subset of const_folding (literal arithmetic + pure CTFE), and const_branch_prune — together over one engine session per function, interleaved on a single worklist. It is invoked twice per iteration: before inline (so string_push sees the push_str MethodCall) and after (so array_literal sees the exposed array_new + push window).

Per-function dirty-set gating

A FunctionGate (optimize/gate.rs) lets every loop pass skip functions that have not changed since it last ran. Each function has a monotonic revision and each pass a per-function watermark; a per-function pass processes a function only when revision > watermark (run_gated), and any pass marks the functions it changed dirty (mark_changed), which also bumps 1-hop call-graph neighbours. Interprocedural passes (inline, dae, drve, sroa_param, value_copy_demote) scan all functions but report exactly the ones they touched. FunctionId is a function's index in NirPackage::functions (stable within one optimizer run).

Gating affects only which functions a pass visits, never the IR a visit produces; since every loop pass is an optimization, an imprecise gate can cost only optimization quality (a missed rewrite), never correctness. The call graph is built once and not refreshed when a pass shifts a function's call edges, since stale edges only reduce propagation precision.

Pipeline

optimize.rs orchestrates the NIR stages; wir_optimize.rs runs the WIR stages.

  1. Early DCE — remove unreachable functions/types/globals.
  2. Fixed-point loop (skipped at -O0), in order: container_sroa, peephole (pre-inline; hosts match_to_switch as a rule), value_copy_demote, sroa_param, inline, peephole (post-inline; hosts array_literal, labeled_block_fusion, ref_elim, and elide_box_local as rules), sroa, copy_prop, dae, drve, const_folding, licm, tmpl_hoist. (match_to_switch on global initializers runs once before the loop; -O0 lowers everything via match_to_switch_all.)
  3. Post-loop, once: field_scalarize; store_load_forward (fold the scalarization shadow inits); branch_prune_final (flatten __tmpl: wrappers); const_object_globalization + a final const_folding/const_branch_prune/condition_implication cleanup.
  4. Final DCE.
  5. Backend-required rewrites (all levels): select_lowering, multi_value_return.
  6. WIR-level passes — see WIR optimizations.

NIR passes

Allocation-elimination and aggregate passes:

There is no value-copy elision pass. Every defensive $value_copy$T is inserted precisely at the lower phase (lower::plan::value_copy): the ownership analysis decides move / copy / share per consumption site — last-use liveness (last_use), interprocedural freshness (ownership), per-parameter confinement (confine, the caller-side replacement for the former optimize::escape), and a field-sensitive read-only-share refinement — so no copy is emitted for the elider to recover. optimize::escape and optimize::value_copy_elide are deleted (WEP 2026-05-21).

Scalar / dataflow passes:

Loop and field passes:

niri (src/niri.rs) is the partial evaluator backing const_folding; see WEP: NIR Interpreter Evolution Plan. Unit tests: wado-compiler/tests/niri.rs.

Whole-program / backend passes:

nir_visitor.rs provides the shared pre/post-order *MutVisitor/*OptVisitor traits; arena_query.rs holds shared arena queries (break-target search, mutation/place-root checks).

Lowering optimizations

NIR→WIR lowering (wir_build/) avoids redundant shapes in a few spots; these fire once during the build at all levels. Notably, exhaustive-match last-arm elision (wir_build/pattern_match.rs) treats the final arm of a fully-covering match as irrefutable, removing one pattern test and branch per ? on the hot path.

String / bytes literal representation: a string literal lowers to StructLiteral String { repr: PackedArray(bytes), used: <len> } and a bytes literal to the same shape over List<u8>, where ExprKind::PackedArray is a raw constant Array<u8> (no atomic ValueKind::String). The struct wrapping is the generic StructLiteral lowering; PackedArray lowers to array.new_fixed<u8> (≤ string_inline_max_bytes, a Wasm const so a const string/bytes global promotes eager) or a passive array.new_data<u8> segment (longer). Because the literal is now a real aggregate, .len()/.used folds via project_struct_literal, &"…" collapses via ref_elim, and const string/bytes globals globalize via const_object_globalization — all generic aggregate machinery, no string-specific paths.

builtin::array_clone::<T> / array_clone_shallow::<T> (the Array<T> clone every $value_copy$T helper emits, wir_build/calls.rs) — when T needs no per-element copy helper (a primitive/non-value-typed element, or a deliberate shallow copy), lowers directly to a fresh array plus a bulk ArrayCopy (build_bulk_array_clone) instead of WirInstr::ArrayClone. This reuses ArrayCopy's own native-array.copy-by-default codegen (the -f no-array-copy toggle) rather than giving ArrayClone a second copy of that choice; a value-typed element (needing a per-element helper call between array.get and array.set, which array.copy cannot express) still lowers to ArrayClone's JIT-compiled loop. Every deep copy of a struct with an Array<T> field for a primitive T (String's Array<u8> repr, List<T> for a primitive T, ...) previously paid one interpreted loop iteration per element regardless of T.

WIR optimizations

wir_optimize.rs mutates the WirPackage in place after WIR build; phases run in order and may iterate.

  1. Type representation — nullable-ref representation; pre-SROA copy propagation; variant-return SROA (small variants → multi-value returns).
  2. Single-field struct local elimination (round 1) — substitute StructGet(LocalGet(x), f) for LocalSet(x, StructNew{[inner]}) when re-evaluation-safe. Then adjacent-use box elision (elide_adjacent_box_locals): the same substitution for a heap-reading inner (which round 1 refuses to move), sound because it works on ordered statement lists and only fires when the single use is the leftmost side effect of the immediately following statement — every op between is pure and unconditional. Targets the Box<T> locals lowering mints for &primitive payload bindings (match r { Token(i) => f(*i) }), which never reach the NIR elide_box_local pass because the boxing scheme runs during NIR→WIR lowering.
  3. Data flow — forward struct field constants (stores-aware) for constant-index bounds-check elimination.
  4. Library rewrites — short-string append expansion; constant array data promotion (array.new_fixedarray.new_data for ≥16 primitive constants); large-literal splitting (>256 elements).
  5. Peephole + multi-field struct elimination — Wasm instruction-selection rewrites with no NIR analogue (constant-comparison/dead-If folding, eqz/negated-comparison folding, branchless increment, byte-mask/sign-extension folding, redundant ref.cast/ref.test elimination, nullability relaxation, local.tee fusion); multi-field struct local elimination; trivial labeled-block copy propagation.
  6. Write-only local elimination — for locals the WIR builder synthesises (__match_scrut_N, pair/multi-value temps) that no NIR pass can reach.
  7. Global cleanup — constant global-initializer promotion (const_global.rs); identical-const-global dedup (dedupe_const_globals.rs — merges byte-identical immutable const-expressible globals, e.g. duplicate short-string globals; bails if any ref.eq exists); trivial init-guard removal; unreferenced data-segment pruning (prune_dead_data.rs — drops a passive segment register_literal_data speculatively registered but no surviving array.new_data reads, and compacts/remaps the rest).
  8. Branch hints (branch_hint.rs) — br_if selection: if cond { br N } with an empty else collapses to br_if N-1, carrying any branch hint on the condition (runs after init_guard, whose matcher keys on the If { GlobalGet, [Br] } shape). Then trap-based hint inference: an if arm that always reaches an unreachable trap is hinted cold, and a br_if whose fall-through always traps is hinted likely-taken. Divergence alone (br / return) never counts as cold, and explicit hints (from builtin::cold_path()) always win. Inference also runs at -O0, keeping hints independent of the optimization level like the build-time apply_cold_path_hints.
  9. Final DCE + compaction — remove unreachable defined functions and unused GC types, then compact and reindex.

Branch hints are transparent annotations: a BranchHint wraps an if/br_if condition, and any pass that matches on a condition's shape must look through it via WirInstr::peel_hint (or the hint blocks the rewrite). A pass that eliminates the branch drops the hint with it (take_branch_hint); a pass that logically negates a hinted condition or swaps hinted arms must flip likely. The emitter records a metadata.code.branch_hint entry for hints on if and br_if conditions; wasmtime (with Config::wasm_branch_hinting, which wado run enables) lays out the cold side out of line. For benchmarking, -f no-branch-hinting disables the feature: cold_path() lowers to a no-op at WIR build (keeping the NIR inliner's cold-cost exclusion identical in both configurations) and the inference pass is skipped, so no hint section is emitted.

Shared facility: optimize/mod_ref.rs (ModRef::of_expr/of_stmt) returns a conservative mod/ref summary used by the move-safety predicates (may_clobber, can_move_past).

Not yet implemented

Tried and found ineffective

Testing

References