skip to content

Two builds from identical inputs emit generated files that differ byte for byte — what causes that?

level: seniorimportance: nice to knowfreq 30%

answer

  1. the inputs did not change, so what did?
  2. ordering and environment
  3. hash-keyed walk, parallel completion order
  4. timestamps, absolute paths, host identity
  5. sort by a total order from the input

basics

~20 s

Almost always the generator, not the inputs: members walked in an unordered collection's iteration order, work split across parallel units that finish in varying order, or embedded timestamps, absolute paths and machine identity. Impose a total order and emit nothing environmental.

solid answer

~40 s

Byte-instability comes from the generator deciding something the inputs did not. The usual sources are **ordering** — walking a hash-keyed collection of declarations, or appending output as parallel units complete — and **environment** — a generation timestamp, an absolute path, a host or user name, a generator version string, or locale-dependent sorting of names. Synthesized identifiers drawn from a counter that depends on visit order belong to the same family. The fix is to impose a total order the generator derives from the input itself rather than inheriting from a collection, and to emit nothing that came from the machine rather than from the inputs. It matters because unstable output turns every rebuild into a diff, defeats content-keyed build caching, and makes "re-derive and compare" useless as a drift check.

code

pseudocode · 7 lines
pseudocode
// unstable: iteration order of a hash-keyed collection is not part of its contract
for each (name, field) in fieldsByName
    emit("  write(" + name + ")")

// stable: the generator imposes an order derived from the input
for each name in sortByCodePoint(keysOf(fieldsByName))
    emit("  write(" + name + ")")

go deeper

for a junior

Recall the idea that the same inputs should produce the same generated text, and that a timestamp written into the output breaks it on every single run.

for a middle

Explain the two families of cause — ordering the generator inherited from a collection or from parallel completion, and environment it recorded from the clock, path or host — and name the fix for each.

for a senior

Show the consequence you have lived with: rebuild diffs burying a real change, caches missing, and a drift check nobody could keep enabled because comparison always failed.

for a principal

Treat it as an enabling property rather than tidiness: byte-stable emission is what makes re-derive-and-compare a usable guard, and that guard is what lets committed output be trusted at all.

Reproducible emission means one thing: the same inputs produce the same bytes. It sounds like a purity exercise until you try to check that committed output still matches its input, at which point it becomes the precondition for the check. ## Where the instability comes from The inputs did not change, so the variation was introduced by the generator. It comes from two families. **Ordering the generator inherited instead of chose:** - Walking a **hash-keyed collection** of declarations, whose iteration order is not part of its contract and may differ between runs or between environments. - **Parallel work** whose results are appended as each unit finishes, so output order tracks completion order. - Deduplicating through a set and emitting in whatever order the set yields. - Sorting names with a **locale-dependent or case-folding comparison**, so the same list orders differently on a differently configured machine. - Synthesized names drawn from a **counter that advances in visit order**, which makes every downstream reference shift when the visit order shifts. **Environment the generator recorded instead of the inputs:** - A **generation timestamp** in the header — the single most common cause, and the one that guarantees a change on every run. - **Absolute paths**, host names, user names, or a working-directory-dependent path in a comment. - A **generator version string** embedded in output, which is defensible but means every toolchain bump rewrites every file. - Any value read from the clock, the random source or the environment during emission. ## Why it is worth fixing | Cost | What it looks like day to day | |---|---| | Diff noise | Every rebuild shows hundreds of changed files; the one real change hides inside them | | Merge conflicts | Two people rebuild, and reordered members conflict although neither changed behaviour | | Cache misses | A build cache keyed on file contents re-runs everything downstream of the output | | No drift check | "Re-derive and compare with what is committed" always reports a difference, so it is switched off | | Hard bisection | Comparing two builds' outputs cannot isolate a change, because everything differs | The fourth row is the one that matters most, because it removes the only cheap guard against committed output drifting away from its inputs. ## Making emission deterministic 1. **Order from the input.** Collect declarations, then sort by a total order derived from the input — a fully qualified name compared by code point, or the declaration's position in its description — before emitting anything. Never let a collection's iteration order decide file content. 2. **Merge parallel results in a fixed order.** Parallelism is fine; appending in completion order is not. Assign each unit an index up front and assemble by index. 3. **Emit no clock, path or identity.** If a header must carry a version, derive it from the inputs or the declared generator version, not from the moment of emission. The build already records when it ran. 4. **Pin the comparison.** Sort by an ordering that does not vary with locale or case-folding configuration. 5. **Derive synthesized names from the input.** A name built from a hash of the input element is stable; a name built from a visit counter is only as stable as the visit order. 6. **Test it.** Emit twice into different directories and compare byte for byte, as an ordinary test. It catches every item above and costs one test. ## What determinism does not mean It does not mean output never changes — a changed input should change the output, and that is the signal you were protecting. It does not mean the emitted text must be minimal or canonical in any formal sense; two generators may legitimately emit different but equivalent source for the same input. The property is narrower and entirely practical: **this generator, these inputs, these bytes, every time**. Anything the output records that did not come from the inputs is a candidate for deletion, and a header timestamp is almost always the first one to go.

  • The generator parallelises emission across declarations. How do you keep the output stable?
    Give each unit of work an index derived from the sorted input before dispatching, and assemble results by that index rather than appending as each finishes. Parallelism then affects only how long emission takes, never what it produces. The same rule applies to any concurrent collection of results the output order depends on.
  • How would you test that a generator emits byte-stable output?
    Run it twice over the same inputs into two directories and compare the results byte for byte in an ordinary test. To catch environment leakage as well, vary what should not matter between the two runs — working directory, locale configuration, the clock — and require the bytes to stay identical anyway.
  • Is a generator version string in the emitted header a violation?
    Not of determinism, since it is constant for a given generator, but it does mean every toolchain upgrade rewrites every emitted file. That is acceptable if the team wants upgrades to be visible in the diff, and unhelpful if it buries real changes. Decide deliberately rather than inheriting it.

saying these in an interview costs you the question

  • Blames the inputs when only the emission order changed
  • Thinks a hash-keyed collection guarantees a stable iteration order
  • Calls a header timestamp harmless because it is only a comment
  • Assumes parallel emission cannot affect what is written
  • Believes determinism means the output must never change
  • Sorts names with a comparison that varies by machine configuration