Skip to content

Reproducibility

Deterministic identities, canonical ordering, stable bytes, and failure triage.

Updated View as Markdown

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 reduce

Workers 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:

  1. compare lengths; a changed length often means membership or section-shape drift;
  2. locate the first differing byte and its Wasm section;
  3. compare IR or WasmIR snapshots before the divergent section was encoded;
  4. force sequential parse, sema, MIR, and unit emission paths one at a time;
  5. disable optional passes to isolate transformation order;
  6. audit iteration over hash collections in the producing stage and all upstream ID producers;
  7. check ambient options, source paths, prelude cache state, and static counters;
  8. 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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close