D103 - The builder checks that its identifiers resolve

Status: decided (2026-08-21) - it has since paid out on new work

Builder::check verifies three properties before a module leaves the crate: every identifier used is defined somewhere, the declarations and function sections define before they use, and nothing is defined twice. translate calls it and turns a failure into TranslateError::MalformedModule.

The case that prompted it: an identifier was reserved for an array's length and the OpConstant defining it was never emitted. Every instruction was well-formed. The module meant nothing, and the driver did not say so - it faulted, and the fault arrived as STATUS_ACCESS_VIOLATION inside the graphics driver with no indication of which identifier or which instruction was responsible. That is the second driver fault in this subsystem whose diagnosis came from spirv-val in a virtual machine; the first was an access chain given one index where the buffer's shape needs two.

It has since caught one on its own

The second look this asked for is answered by use rather than by argument. Adding the subgroup level meant four new opcodes, and one of them - OpTypeVector - was given a shape entry listing its own result as one of its uses. The check reported it used before it was defined, naming the identifier and the opcode, before any driver saw the module.

That is exactly the failure it was written for, arriving from a different direction, and it was diagnosed from the message rather than from a virtual machine. Narrow was the right size.

The builder hands out every identifier, so it is the one place that can say which were never given a meaning. Doing it here turns a driver fault into a named error, which is the same trade finish already makes for the identifier bound.

Deliberately not a validator. spirv-val exists, is authoritative, and a second-guessing reimplementation would be worse than silence. This checks the three properties a builder is uniquely placed to check and skips any opcode absent from its shape table rather than guessing - guessing would mean reading a literal as an identifier and complaining confidently about a module that is fine.

The shape table is data rather than a match, listing for each opcode where its result sits and which operands name others. Expressed as match arms it tripped clippy::match_same_arms, and the lint was right: the same four facts about each opcode, with nothing computed, is a table.