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/ or wir/ prefix.

The module-level docs in src/optimize.rs and src/wir_optimize.rs are the authoritative pass index and ordering; per-pass design lives in each pass's source. This document is an architectural overview with 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 (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 backend-required rewrites (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. Each operand position is either a skeleton subtree or a promoted pure value interned in a per-function pool, hash-consed so congruent values share one node. The graph is built once per function and maintained in place across passes via e-class union, never rebuilt — so pure-value CSE falls out of the pool, constant folding reads pooled values, and bounds-check elimination recognises them structurally. See WEP: The Live ValueGraph.

Worklist rewrite engine

Genuinely-local NIR rewrites run as rules on a worklist engine over one function's arena: a node is revisited only when an edit may have made it reducible, rather than via repeated whole-tree sweeps. The engine owns the session state and a mutating edit API that keeps it coherent. Flow-sensitive passes that need per-block dataflow keep their own walkers. See WEP: NIR Rewrite Engine.

Unified peephole session

The position-flexible local rules run together over one engine session per function, interleaved on a single worklist. It runs twice per iteration — before and after inline — so each rule sees the instruction window the other exposes.

Per-function dirty-set gating

A function gate lets every loop pass skip functions unchanged since it last ran; interprocedural passes still scan all functions but report only the ones they touched. Gating affects only which functions a pass visits, never the IR a visit produces, so an imprecise gate can cost optimization quality (a missed rewrite) but never correctness.

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): container SROA, peephole (pre-inline), value-copy demotion, parameter SROA, inlining, peephole (post-inline), SROA, copy propagation, dead-argument and dead-return elimination, constant folding, parameter specialization, LICM, template hoisting.
  3. Post-loop, once: field scalarization, store-load forwarding, template-wrapper cleanup, constant-object globalization, and a final folding pass.
  4. Final DCE.
  5. Backend-required rewrites (all levels): select lowering, multi-value returns.
  6. WIR-level passes — see WIR optimizations.

NIR passes

Allocation and aggregate:

There is no value-copy elision pass: defensive copies are chosen at the lower phase by the ownership analysis, before NIR exists, so none are reachable from here and an imprecise one is that analysis's to fix — see WEP: Ownership Analysis, which records the standing case (a by-value for binding copies each element of a List of aggregates).

Variant and reference:

Scalar and dataflow:

Loop and field:

Whole-program and backend:

Lowering optimizations

NIR→WIR lowering avoids a few redundant shapes, firing once during the build at all levels — for example treating the final arm of an exhaustive match as irrefutable, and lowering a primitive-element array clone to a bulk array.copy rather than an interpreted per-element loop. String and bytes literals lower to a generic aggregate, so length folding, &"…" collapse, and globalization all reuse the aggregate machinery with no string-specific paths.

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 lowering; small-variant returns to multi-value.
  2. Box-local elimination — substitute the field read for a Box<T> local lowering minted.
  3. Data flow — forward constant struct fields for constant-index bounds-check elimination.
  4. Library rewrites — short-string append expansion; constant-array data promotion (only where packing encodes smaller than the inline T.const operands, since a data segment stores each element at full width while an operand is LEB128-compressed); large-literal splitting; elision of a whole-array zero fill on a fresh array.new_default (the List::filled(n, 0) shape).
  5. Peephole — Wasm instruction-selection rewrites with no NIR analogue.
  6. Write-only local elimination — for locals only the WIR builder synthesises.
  7. Global cleanup — constant-initializer promotion, identical-global dedup, and dead-data pruning.
  8. Branch hints — br_if selection and trap-based cold/likely inference (also at -O0).
  9. Final DCE and compaction.

A pass earns its place here only by changing the emitted Wasm. Skip-scanning one over the benchmark, example, and fixture corpus — disabling it and diffing the output — is what settles that; anything NIR or a sibling WIR pass already covers leaves the bytes identical and does not belong. The exception is split_large_array_literals, which scans as byte-neutral because no corpus program reaches its bound: it is a JIT-pathology guard for >256-element literals, not an optimization.

A #![wasm_module(...)] core module — the allocator — runs this same list as a package of its own, since codegen emits it verbatim. Its passes are named wir/<module>:<pass> so WADO_SKIP_PASS / WADO_DUMP_PASS_* address the two runs separately.

Branch hints are transparent annotations on if/br_if conditions: a pass looks through a hint when matching, drops it when eliminating the branch, and flips it when negating the condition. wasmtime lays the cold side out of line; -f no-branch-hinting disables the feature for benchmarking.

Shared facilities

Not yet implemented

Tried and found ineffective

References