Making the build name every caller: the default that hid a live bug

← All field notes

When climate became a gradient laid across the world, the maths behind it had to be told how wide that world actually is. We added the size as an optional argument with a sensible default, so every caller already in the code kept working untouched. That courtesy is why a real defect sat in the largest worlds for ten days with nothing able to see it.

On a large world the cold country and the warm one were arriving twice as fast as they should. The gradient was being measured against a world half the size of the one you were standing in.

A number the climate had to be told

In the middle of July, climate stopped being a property of a world and became a property of a place inside it: temperature grading along one axis of the map, moisture along the other, both bent by the same seed noise that shapes the land, so all eight kinds of country live somewhere in a single world. Walking across that is written up separately. This post is about one argument.

The gradient is normalised against the world’s half-width, which is what makes a bigger world spread the same climate across a longer journey. So the routine that decides which biome sits at a given spot needed a fact it had never needed before: how far the world reaches.

It got one, as an optional last argument with a friendly default. If nobody says how wide the world is, assume a half-width of a hundred metres, the standard reach. That let all one hundred and twenty-nine call sites, spread across a hundred and ten files, keep compiling exactly as they were, and nothing about a shipped world moved by a hair. Landing a change of that size without disturbing anything is the right instinct. It also bought us ten days of a wrong world.

The caller that never said which world it was

Ten days earlier, the largest world option had been stretched to twice the standard reach, the day Large stopped being a label. A large world’s true half-width is two hundred metres.

Production world generation never passed the size along. The configuration was loaded a few lines above and used elsewhere in the same function; at exactly one call, it was dropped. So every large world had been normalising its climate against a hundred while standing in a world that reaches two hundred: a gradient at double reach across the entire map, both ends of the journey arriving twice as early as intended, and none of the extra land buying any of the extra travelling it was meant to buy.

A voxel landscape at golden hour where snowy pine ridges give way to warm dunes across a very short stretch of ground.Concept art · pre‑alpha
A large world running its climate at double reach: the cold country and the warm one arriving twice as early as the land intended.

Three things had to hold at once for that to survive as long as it did.

  • The checks that prove a world regenerates identically prove stability, never correctness. A number that comes out wrong the same way every time passes all of them, which is a lesson two months of a perfect zero had already taught us once.
  • The default was a plausible value rather than an impossible one, so a forgotten argument read exactly like a medium world.
  • The one runtime check guarding the number is stripped out of a shipping build, which disqualifies it as a last line of defence.

And the sweep that was supposed to find every affected caller had searched the source for the word that names the largest size. Production reads its size from your seed at run time. It does not spell that word out anywhere.

A search finds only what it happens to match. A build has no blind spot to exploit.

Zero remainder, then delete the default

The fix that mattered was structural. First a sweep visited every call site whose correct answer really was “no world size here” and made it say so out loud, with an explicit unset marker: a no-op at every one of them, changing nothing about what ran, and stating an intent that had until then been assumed. A hundred and fourteen sites took it. Two were already handing over a real configuration and stood as witnesses that the argument worked at all.

That left thirteen, and the arithmetic closed with no remainder, which meant the next step could be a proof instead of another hunt. With the default deleted, the code will not compile until every caller states which world it means, so the build had to name the leftovers, and it named exactly those thirteen. Threading the configuration into the production world builder was the repair itself. Every large world now normalises its gradient by the true two hundred.

The value of that was never the one-time sweep. Every caller written from here on is forced to answer the same question before the build will accept it.

A default that cannot be mistaken for real

One hole stays out of the build’s reach. An object hand-assembled somewhere else would inherit whatever the world’s reach starts out at, and a plausible starting value reads as a perfectly valid smaller world sitting inside a larger one. So that starting value changed from a hundred to zero, a marker that cannot possibly be correct. A missing size now divides by zero, the biome lookup comes back with nothing, and the world fails at the instant it is built rather than handing you a world stretched twice as far as it should be. An existing check demanding the number be positive, which could never fire while the default was positive, became the thing that catches it. A second check at construction time moved the failure off a background worker in the middle of generation and onto the main thread, at the site of the mistake.

A clamp was on the table and refused. Clamping an invalid size would clamp it straight back to a hundred, manufacturing the exact plausible, silent value the whole change exists to destroy.

The construction check landed alongside the invalid marker rather than instead of it, and the distinction matters. On its own it would have passed cleanly on the very defect it was written for: production had a configuration in hand, it was the handover that went missing. A guard that cannot fire on its own target is worse than no guard, because it reads as coverage.

What the build cannot tell you

The build proves the set of places that call something. It can never say which of them will ever run on a large world, or whether the value each one passes is the right one. That judgement stayed ours at all thirteen sites, and the compiler’s only contribution was refusing to let us skip any of them.

The same day turned up a second blind instrument. Our tests only parse the files they load, so a profiling tool that had gone unparseable in the middle of June sat broken for a month with nothing able to notice. A pre-step now walks every script in the project before any test runs, names each file that will not parse, and refuses to continue. It costs about eleven seconds, we proved it works by breaking a file on purpose and watching it fire, and the tree came back clean for the first time in a month. That habit of demanding a check demonstrate it can fail has since grown into machinery of its own.

Then the paperwork. Thirteen files still carried comments describing the world-size relationship that the shipped data had abandoned ten days earlier. One of them argued a safety property from the false number, and a plan had already been written on the strength of it.

Reading back over the day, the pattern is uncomfortable. We had written down careful rules about checking call sites, and every one of them was followed, and none of them caught anything.

Every fix that held was mechanical. Not one of them was a rule we had promised ourselves to remember.

Three days later, in the middle of unrelated work on how water wears the ground down, the same backstop refused to compile over a caller we had missed. That one never reached a world.

Keep reading

Concept art · pre‑alpha