Same inputs, same output, everywhere.

Same really means same

Running the same program twice and getting the same answer sounds like a pretty low bar. It isn't.

A different CPU might round a number another way. A compiler might fuse two operations together. A hash table might choose a new order, a garbage collector might run at a different moment, or two threads might finish in the opposite order. Every one of those choices can be perfectly reasonable on its own. They are a disaster if we are trying to reconstruct an execution and the program slowly wanders away from what happened the first time.

Wax makes a stronger promise. Given the same program, initial state, and inputs, it produces the same observable result everywhere it runs.

'Observable' is carrying a lot of weight here. It includes errors, panics, persistent program state, object identity, collection order, parallel results, and anything the program sends back to its host. If Wax can see it or the host can observe it, it is part of the answer.

The operating system, CPU, compiler backend, optimization level, and worker count are all allowed to change how quickly the program gets there. They are not allowed to change where it ends up.

It's fine for implementation details to change. Results cannot.

The front door

Okay, but there is an obvious hole in this promise. Wax can control everything happening inside the program, then the program asks a server for some data and gets a different answer. Or it reads the clock. Or a file changed. Or the user clicked somewhere else.

Wax cannot make the internet, a clock, a file system, or a person deterministic. It would be ridiculous to try.

What Wax can do is make all of that come through a door it knows about. The host calls declared Wax APIs and sends data through channels. Wax calls declared host functions when it needs something from the outside. Time enters at the frame boundary. If a value does not come through one of those doors, Wax cannot see it.

Once we have that rule, recording the outside world becomes a much smaller problem. Save the API calls and arguments, the answers and errors from host functions, frame time, channel data, and the few runtime decisions we cannot safely work out again later.

During replay, Wax feeds those values back instead of asking the original host again. The file can change. The server can disappear. The clock obviously moved on. Wax still sees exactly what it saw the first time.

We are not making the host deterministic and we are definitely not recording the whole machine. We only record Wax's experience of the outside world. Give it that same experience again and the rest of the promise holds.

The weird stuff counts too

When most people talk about making a program deterministic, they mean giving the random number generator a fixed seed. That is a nice start. It is nowhere close to enough.

Floating point is the first trap. Different CPUs, compilers, and math libraries are all allowed to make choices that produce slightly different answers. Wax is not. Decimal literals are rounded directly to their declared width, operation order is fixed, and the compiler cannot quietly fuse a multiply and add into something with different rounding. The runtime installs the same floating point settings whenever Wax runs, and functions like sin, pow, and log use the same implementations everywhere instead of whatever the platform happens to provide.

NaNs are even weirder. The same calculation can produce different NaN bits on different hardware even though every result prints as NaN. Wax gives them one representation whenever those bits become visible to the program.

The trick is to canonicalize at escape, not after every floating point operation. Wax fixes the bits when a value is stored, crosses the host boundary, is folded into a constant, or passes through Reinterpret. A NaN that only lives in a register costs nothing to normalize because Wax code cannot observe it. The optimizer also removes the work whenever it can prove the value is already safe.

Reinterpret is a performance oriented low level feature which lets you look at the bits in a float and treat them as an int. It is useful for advanced hashing algorithms and the kind of problems performance nuts care about. It also means Wax code can observe the exact bits of a NaN, so we cannot just declare NaN is NaN and call it a day.

The same rule applies to less obvious places too. Padding bytes in structs cannot leak old memory into a comparison. Hash tables do not pick an order from a random seed or an allocation address. Collections define the order they return things. Objects have identities that survive moving through memory instead of borrowing an identity from whatever address they landed at.

Even garbage collection is part of the result. Its decisions and ordering have to be the same during replay, so collection cannot quietly nudge the program onto a different path.

Parallelism is another easy place to cheat without meaning to. Before work starts, Wax checks the memory each task can change, the values it captured, whether it allocates, and whether it can reach the host. The runtime checks that the actual writable ranges do not overlap. Native code can spread that work across several cores while another backend runs it serially, and both must produce the same result.

Determinism is the sum of all these boring little rules. It is brutal to get right, but it is also what makes some of the magical things we do possible. We take it extremely seriously.

Where the buck ends

Wax promises to reproduce the Wax program world. It does not promise to reproduce the universe around it.

State that never crossed the host boundary is not part of the program as far as Wax is concerned. Neither is the lifetime of a native resource, the original machine code, the rest of the process, or how long the work took in wall clock time. Wax can replay what a host resource told the program. It cannot bring the resource itself back from the dead.

The guarantee is not free either. The compiler has to give up optimizations that could change floating point answers. Observable NaNs need to be normalized. Hash tables cannot choose a fresh random order, and parallel work has to prove that scheduling does not affect the answer. Recording the outside values needed for replay takes time and space too.

Those are deliberate costs, and most of them are tiny. Wax is happy to let another backend, CPU, or worker count find a faster way to run the program. It is not allowed to find a different answer.

The determinism corpus →

The Wax memory model →