Wado

Wado Optimizer

The optimizer rewrites NIR (WEP: NIR), then a smaller set of passes rewrites WIR before emission. This document lists what runs, one line per pass. The order is the one in src/optimize.rs and src/wir_optimize.rs, and WADO_LIST_PASSES prints it. Each pass's module doc says how it works.

Philosophy

When WebAssembly provides a native instruction for a feature, prefer it over a complex compiler transformation. That keeps the compiler small, lets the runtime JIT do the work, and produces smaller output. Examples are select for branchless conditionals, array.copy and array.fill for bulk operations, and br_table for dense matches.

A call to a function that declares no effects, or to an #[ambient] one, may be removed when its result is unused. A call that may trap or may not return stays, since removing it changes what the program does.

Optimization Levels

Flag Iterations Inline threshold Notes
-O0 0 N/A Part of the pipeline; see Pipeline
-O1 2 4
-O2 (default) 15 16
-O3 20 26
-Os 15 16 Strips the name section; a failed assert only traps

The inline threshold bounds what one inlined copy adds to its caller. That is the Wasm instructions on the callee's hot path, less the call it replaces. --optimize-iterations and --optimize-inline-threshold override a level's values. --optimize-inline-growth <pct> also bounds how far inlining may grow the whole program. No level sets it.

A pass reports a change only when it made one, because the fixed-point loop stops once no pass reports a change. At -O2, -O3, and -Os the loop must converge within the level's own count. At -O1, and wherever --optimize-iterations sets the count, it is only a budget.

Architecture

NIR has two tiers: a skeleton carrying effect order, control flow, and allocation, and a hash-consed graph of the pure values it reaches. Local rewrites are rules on one worklist engine, and a per-function dirty set lets a pass skip functions unchanged since it last ran. CSE, GVN, and pure copy propagation are not passes, because the hash-consing already does them. See WEP: NIR Optimizer Architecture.

Pipeline

-O0 runs only DCE, match_to_switch, step 7, and the part of step 8 that WIR Passes keeps at -O0. Every other level runs the whole list.

  1. DCE.
  2. Before the loop: cold_outline, match_to_switch over global initializers, and the early arithmetic promotion.
  3. The fixed-point loop: container_sroa, peephole, value_copy_demote, sroa_param, sroa_variant_return, inline, peephole, let_block_flatten, sroa, copy_prop, dae, drve, const_folding, param_spec, licm (with condition_implication), tmpl_hoist. Once it converges, inline releases the callees it held back, and the loop runs again.
  4. After the loop, once: field_scalarize, store_load_forward, const_branch_prune, const_object_globalization, const_folding, scalar_forward, clone_forward.
  5. DCE.
  6. promote_fields, condition_implication (with const_branch_prune), loop_version_bce.
  7. Backend rewrites: select_lowering, multi_value_return, multi_value_param, freeze_pure_arith.
  8. The WIR passes.

NIR Passes

peephole is one worklist per function shared by the rules that need no fixed position. It runs once before inline and once after, and each run has rules of its own:

Allocation and aggregates:

Lower's ownership analysis chooses the defensive copies before NIR exists, so no pass here elides them (WEP: Ownership Analysis).

Variants and references:

Scalars and dataflow:

Loops and fields:

Whole program and backend:

WIR Passes

A WIR pass stays only if it changes the emitted Wasm. One that NIR or another WIR pass already covers is removed. At every level, nullable references are lowered first. -O0 then only infers branch hints, removes dead items, and settles the locals (step 11).

  1. Trivial copy propagation.
  2. Box-local elimination.
  3. Constant struct-field forwarding, for constant-index bounds checks.
  4. Array rewrites: constant arrays to data segments, large literals split, and the zero fill of a fresh array removed.
  5. Peephole: repeated field loads reused, instruction selection (select, rotates), and variant result slots flattened.
  6. Write-only local elimination.
  7. Global cleanup (constant initializers, duplicate globals, dead data), then br_if selection, the calls to a function left empty and the once-flags guarding them, and branch hints.
  8. DCE and compaction.
  9. Local coalescing: locals of one Wasm type whose live ranges never overlap share a slot. A local's nullability is settled before it shares one.
  10. Tee fusion: a local.set and the read right after it become a local.tee. A local this leaves unread is removed.
  11. Local settling: a non-null local that Wasm's block-scoped rule sees read before any write is declared nullable. The locals are then grouped by Wasm type, the most used first.

A #![wasm_module(...)] core module, such as the allocator, runs the same list on its own, with its passes named wir/<module>:<pass>.

wasmtime lays out the cold side of a hinted branch out of line. -f no-branch-hinting disables hints for benchmarking.

Differential Testing (EMI)

wado-compiler/tests/emi.rs injects code behind a guard that is always false at run time, and checks that the program's output does not change at any level. mise run emi-calibrate and mise run emi-mutate run it, and CI runs it nightly. See WEP: Compiler Fuzzing.

Not Yet Implemented

Architectural work is tracked in WEP: NIR Optimizer Architecture.

Tried and Found Ineffective

References