D209 - The table generators are a crate, behind a seam that replays a recording

decided ยท 2026-08-24

crates/orbistoun-gen solves the shader data tables under crates/orbistoun-shader/data/ by assembling probes and reading the bytes back, rather than transcribing them from a document (D085).

The constraint that shapes it

The reference assembler is not available where this is worked on. The solvers need llvm-mc built with the AMDGPU target; the LLVM most hosts have does not include it, and on Windows llvm-mc is not shipped at all. tools/toolchain/setup.sh builds a VM that has it.

A generator only runnable on one machine has a worse consequence than inconvenience: nothing can check that a committed table still matches what produces it. A hand-edited row would survive indefinitely, and the tables are exactly the kind of thing somebody edits by hand when a value looks wrong.

The seam

assembler::Source is either a live llvm-mc or a replay of a committed recording of one. Replay needs nothing installed, so ./orbistoun.sh tables regenerates and diffs on any machine, in check, and fails on a hand-edited table. Tested by making it fire.

Only the two generators whose output is committed are covered. encodings writes no file to diff, and its sweeps are 9.5 MB against 772 KB for the other two - one file of which exceeds the provenance guard's own 1 MB limit.

A recording carries its own input and replaying checks it. A solver whose probe list has changed would otherwise be handed the old answers to new questions, which surfaces as a wrong table rather than as a stale recording.

Keys are derived from the probe text, not from call order. This is not a nicety, and it was found by the diff rather than by reasoning: derive_symbolic_codes makes forty-seven probes, and under a single shared key replay handed all of them one canned answer. The symbolic codes came out empty and two whole families - EXP and VINTRP - dropped out of the operand table. Live it is invisible, because each call re-invokes the assembler. It is wrong only on replay, which is the mode that has to be trusted.

Recording is a separate act (--record) rather than a cache. A cache decides for itself when it is stale; a committed recording is a decision somebody made.

A run that produces nothing must fail

fixtures and operands refuse rather than writing an empty table. Without a toolchain every source is skipped - and the skip path has already deleted each .txt on the way past. Without the guard the run then writes a mnemonics.toml with no mnemonics in it, prints "0 fixtures", and exits zero: the reference output the differential suite exists to compare against is gone, and nothing has said so. On any machine without an AMDGPU-enabled LLVM, which is most of them.

Where the difficulty is, and where the tests are

The subprocess call is a dozen lines; the bit arithmetic is two thousand. So solve, table, patterns and operands are pure functions over numbers, unit-tested with no toolchain - including four cases that produce a wrong table rather than an error: a rejected probe shifting the input pairing of every later sample; a partial trailing word padded rather than dropped; an operand accepted with different bits recorded as implicit; and an implicit slot at word 0 bit 0 colliding with a real field there.

tests/rendering.rs covers the other half - it reads each committed table, hands the rows back to the renderer, and requires the result to match the file it came from. Formatting is where a generator silently differs, and hundreds of rows of repeated structure is exactly where one wrong separator still looks plausible in a diff.

encodings reports rather than writes

data/encodings.toml is not purely generated: it carries the reasoning behind each row and citations into the published reference, which is where a wrong row gets corrected from. A person edits it, acting on what the solver says. Overwriting it would throw the prose away and leave a table nobody could check without the document that produced it.