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.
- DCE.
- Before the loop:
cold_outline,match_to_switchover global initializers, and the early arithmetic promotion. - 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(withcondition_implication),tmpl_hoist. Once it converges,inlinereleases the callees it held back, and the loop runs again. - After the loop, once:
field_scalarize,store_load_forward,const_branch_prune,const_object_globalization,const_folding,scalar_forward,clone_forward. - DCE.
promote_fields,condition_implication(withconst_branch_prune),loop_version_bce.- Backend rewrites:
select_lowering,multi_value_return,multi_value_param,freeze_pure_arith. - 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:
- Both runs:
aggregate_forward,const_branch_prune, part ofconst_folding,elide_local,identity_cast,known_case,string_push, andtuple_projection. - Before
inlineonly:if_chain_to_match,match_to_bitset, andmatch_to_switch, so a copied body is already lowered. - After
inlineonly:closure_devirt,drop_value,elide_box_local,labeled_block_fusion,ref_elim, andslot_temp_sroa. Each cleans up a shape that inlining leaves behind.
Allocation and aggregates:
inline— replace a call to a small, non-recursive function with its body.#[inline],#[inline(always)], and#[inline(never)]adjust the decision, and a cold call site opts out. A callee that reaches no safepoint (no call, no allocation) is admitted at four times the threshold where the splice pays back: at its only call site, or at its only site in a loop.cold_outline— move the region acold_path()marks into a function of its own.sroa— split into scalar locals a struct, tuple, or array local used only for element access, or a local bound to a borrow of a struct or tuple literal. The highest-impact WasmGC pass.container_sroa— turn aListof structs or tuples into one list per field.sroa_param— pass the one field a callee reads instead of the struct.sroa_variant_return— return a variant, or hold a variant local, as a[tag, slots…]tuple (WEP: Variant Return Scalarization).elide_box_local— collapse a box bound once and read once into its value.drop_value— keep only the effects of a labeled block whose value is discarded.string_push— specialize a constant ASCII push, and reserve a run of appends at once.value_copy_demote— make a deep list copy shallow when its elements are never mutated.clone_forward— collapse a clone of a clone into one.
Lower's ownership analysis chooses the defensive copies before NIR exists, so no pass here elides them (WEP: Ownership Analysis).
Variants and references:
labeled_block_fusion— remove theOptionorResultan inlined helper builds only for its caller to test, as?andif letdo.slot_temp_sroa— split the aggregate an inlined helper leaves where fusion cannot reach.closure_devirt— call a closure directly when its callee is known.ref_elim— replace a reference read only through fields with its source.aggregate_forward— hand a freshly built aggregate to its consumer directly.known_case— decide amatchover a variant whose case is known.tuple_projection—[a, b, c].1→b.identity_cast— drop a cast between types that share one representation, and fold*&etoe.
Scalars and dataflow:
copy_prop— propagate a trivial copy and drop the binding.param_spec— specialize a callee on the constant arguments its callers pass.dae— drop a parameter the callee never reads.drve— drop a return value every caller discards.store_load_forward— forward a stored value to a later load.elide_local— drop a binding that is never read.let_block_flatten— hoist the statements out of a block a statement evaluates first: a binding's value, a call's leading argument, a condition.scalar_forward— fold a single-use scalar temp into its use.const_folding— partial evaluation: constant arithmetic, compile-time calls, constant globals, and constant aggregates (WEP: NIR Interpreter).const_branch_prune— simplify trivial blocks, and take the constant side of anif.
Loops and fields:
licm— hoist loop-invariant field reads and arithmetic out of a loop.condition_implication— drop a bounds or range check a dominating condition already decides.loop_version_bce— split a loop into a check-free fast path and the original, and turn a fill loop intoarray.fill.tmpl_hoist— reuse a template string's buffer across loop iterations.field_scalarize— keep hot GC fields in locals across a loop.
Whole program and backend:
dce— remove unreachable functions, types, globals, literals, and imports.promote_fields/freeze_pure_arith— fold pure field reads and arithmetic into the value graph.match_to_bitset— test membership in a set of literals and ranges with one bit mask.match_to_switch— lower a dense integer or enummatchtobr_table.if_chain_to_match— fuse a run ofif K == xstatements into onematch.select_lowering— lower anifwhose arms are pure and cannot trap toselect.multi_value_return— return a tuple or struct as one Wasm result per field.multi_value_param— pass an aggregate read only by field as one parameter per field.const_object_globalization— share a constant aggregate as an immutable global (WEP).
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).
- Trivial copy propagation.
- Box-local elimination.
- Constant struct-field forwarding, for constant-index bounds checks.
- Array rewrites: constant arrays to data segments, large literals split, and the zero fill of a fresh array removed.
- Peephole: repeated field loads reused, instruction selection (
select, rotates), and variant result slots flattened. - Write-only local elimination.
- Global cleanup (constant initializers, duplicate globals, dead data), then
br_ifselection, the calls to a function left empty and the once-flags guarding them, and branch hints. - DCE and compaction.
- 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.
- Tee fusion: a
local.setand the read right after it become alocal.tee. A local this leaves unread is removed. - 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.
- [ ] Sparse conditional constant propagation, intra- and interprocedural.
- [ ] Global value numbering across effectful nodes.
- [ ] Instruction combining over identical operands (
x - x,x & x,~(~x)). - [ ] Dead store elimination.
- [ ] Strength reduction, reassociation, jump threading, and CFG simplification.
- [ ] Cross-block copy propagation.
- [ ] Sinking a pure definition into the branch that uses it.
- [ ] Forwarding a local bound to a global read.
- [ ] Devirtualizing effect dispatch.
- [ ] Specializing in
param_speconly where a constant decides a branch. - [ ] Compile-time evaluation through a
&mutheld in a struct field. - [ ] Argument promotion: passing a by-reference parameter's fields by value.
- [ ] Factoring a conjunctive
ifchain into a decision tree. - [ ] Scalarizing a struct borrowed by
&mut. - [ ] Tail calls (
return_call). - [ ] Bounds-check elimination across sequential accesses (
a[0]; a[1]; a[2]). - [ ] Folding an effect-free call on constants whose callee exceeds the inline budget.
- [ ] Removing an unused call by its callee's declared effects, without inlining it first.
- [ ] Pricing a splice by what its call site keeps: one arm of a dispatch
every site decides (
$hole_fmt), or one element of a returned tuple (default_slot). Both carry a synthesized#[inline(always)]until then. - [ ] An array literal's length as a known constant.
Tried and Found Ineffective
- An empty-array singleton for default
Stringfields: no measurable gain.
