Reproducibility is a compiler correctness contract:
The same complete inputs, compiler revision, and effective options produce byte-identical output.
Complete inputs include source contents, project file order, source paths when debug data embeds them, configuration, target, optimization policy, and codegen controls. The contract is determinism, not independence from inputs that deliberately change the artifact.
Why bytes matter
Stable WebAssembly supports:
- module caches keyed by raw-byte hash;
- trace and debugger locations keyed by Wasm offsets;
- reliable incremental-emission parity checks;
- content-addressed deployment;
- useful binary diffs during compiler changes;
- confidence that scheduling did not alter semantics or identity.
A module that behaves the same but changes byte order nondeterministically still breaks those consumers.
Stage contracts
Determinism begins before encoding:
| Stage | Observable order |
|---|---|
| Syntax | String IDs, AST arena order, diagnostics |
| Analysis | Symbol and type IDs, HIR arenas, layouts, diagnostic order |
| Specialization | Worklist order, specialization symbols, concrete layouts |
| MIR | Function, block, operation, frame, and helper order |
| WasmIR | Virtual registers, blocks, locals, structured control flow |
| Module | Types, imports, functions, globals, exports, data, custom sections |
| Linker | Unit order, dedup decisions, remapped indices, metadata |
Stabilizing only the final section writer is insufficient if an earlier randomized traversal already chose IDs that the writer serializes.
Canonical reduce
Parallelism follows one rule:
parallel map in isolated local state
↓
sequential or ordered canonical reduceWorkers may finish in any order. The reduce assigns public IDs and appends outputs by semantic order such as project file order, declaration order, canonical specialization key, function order, or unit order.
This pattern is used by parallel parsing and semantic body checking. MIR’s per-function loop can run in parallel because each worker mutates only its own function and facts; results remain in the original function slots.
Collection policy
Choose collections by how they are observed:
| Use | Appropriate shape |
|---|---|
| Dense ID-indexed storage | Vec |
| Ordered key iteration | BTreeMap or a deliberately populated IndexMap |
| Ordered set iteration | BTreeSet or a deliberately populated IndexSet |
| Membership only, never iterated into output | Hash set or hash map |
| Temporary hash lookup followed by explicit sorting | Hash collection plus a mandatory canonical sort |
Insertion-ordered collections are deterministic only when insertion itself is
deterministic. Filling an IndexMap from a HashMap does not repair the
problem.
String identity
Interned strings are a recurring risk because StringId can leak into type
schemas, literal pools, names, and stable keys. Parallel parse therefore uses
private overlays and replays new strings in canonical file order. Sharing one
mutable interner across workers would let scheduling choose IDs.
Independent compile state must also remain isolated. A prelude cache may share immutable data, but compiling project B between two builds of project A must not perturb A’s string pool, type IDs, or output.
Allowed differences
Some observations are deliberately outside the byte fingerprint:
- wall-clock stage and pass timings;
- memory and performance measurements;
- trace delivery order where the trace API explicitly normalizes or treats it as side-channel data;
- codegen quality metrics that describe bytes but are not themselves encoded;
- debug sections when source paths or positions intentionally differ.
Lexical perturbation tests therefore compare executable code sections when comments, whitespace, paths, and debug positions differ. With identical complete inputs—including paths and debug policy—the full artifact remains the stronger contract.
Effective options
Optimization level, debug information, safepoints, profiling hooks, pass selection, tracing-related emission, and runtime ABI choices can change bytes. Tests must hold those options fixed.
Environment variables are especially easy to omit from a fingerprint because they are ambient. Library APIs should receive explicit option structs; build systems that rely on compatibility environment controls must include their effective values in the cache key.
Test layers
The repository protects the contract at several levels:
- sema fingerprints arena sizes, type/symbol order, and diagnostics across repeated compiles;
- Mono fingerprints specialization tables and order;
- codegen compiles varied fixtures repeatedly and compares complete Wasm bytes;
- a fresh-process CLI test builds a multi-module project several times;
- cross-process diff tooling localizes the first divergent byte to a Wasm section;
- sequential and parallel MIR builds are compared byte-for-byte;
- parallel per-unit builds are repeated under varying worker schedules;
- frontend parallel and sequential paths have byte-parity gates.
No single fixture covers every ordering leak. A new output-visible table needs a fixture that exercises at least two competing entries.
Triage
When bytes diverge:
- compare lengths; a changed length often means membership or section-shape drift;
- locate the first differing byte and its Wasm section;
- compare IR or WasmIR snapshots before the divergent section was encoded;
- force sequential parse, sema, MIR, and unit emission paths one at a time;
- disable optional passes to isolate transformation order;
- audit iteration over hash collections in the producing stage and all upstream ID producers;
- check ambient options, source paths, prelude cache state, and static counters;
- add the minimized reproducer to the nearest determinism suite.
The fix should establish canonical order at the earliest leaking boundary, not sort final bytes after identities have already drifted.