spir::Interpreter

Runs a SPIR-V module, one instruction at a time.

Declared in src/libs/libspir/spir/Interpreter.hpp
Group spir

Detailed description

execute() runs the code of a spir::ExecutionState until the code ends, an error occurs or the invocation is discarded. A run that goes past one million instructions stops with an InfiniteLoop error.

The other functions are the pre-flight. They examine a module before it runs, so that a module this interpreter cannot run is refused before evaluation, with a reason that names the instruction (spirv-unified- lowering §SUT-3a).

Member function documentation

execute

[static] void Interpreter::execute(spir::ExecutionState &state)

Runs the code of state until the code ends, state has an error, or the invocation is discarded. An opcode with no case in the dispatch sets an UnimplementedOpcode error. A run longer than one million instructions sets an InfiniteLoop error.

firstUnimplementedOpcode

[static] uint16_t Interpreter::firstUnimplementedOpcode(const spir::Module &module, QString *detail = nullptr)

Returns the first instruction in module that this interpreter cannot run.

The function returns 0 when every opcode is implemented. 0 is OpNop, which is implemented, so 0 is not ambiguous as a sentinel. When detail is not null, it receives a readable name for the refusal.

firstUnsupportedWideFloatType

[static] uint32_t Interpreter::firstUnsupportedWideFloatType(const spir::Module &module)

Returns the first 64-bit float type in module, or 0.

This is `SUP-83`. This interpreter executes every float as 32 bits. IdSlot::stride is documented as "1 = 32-bit, 2 = 64-bit", and ValueView::f64 exists "for future stride=2 support". But nothing allocates at stride 2, and no opcode dispatches on the width. Thus a double is stored as two 32-bit components, and the interpreter reads only the first of them.

That is not a decline. It is a wrong answer. Measured on 2026-08-28: float(dvec3(a, 2.0, 3.0).y + 0.5), with a runtime a, returned 0 instead of 2.5. float(dvec2(a, 9.5).x + 0.25) returned the input unchanged, without the addend.

The slot-type guard could not see it. glslTypeNameFor refuses a Double slot, but these have Float slots with a double intermediate. glslang emits OpCapability Float64 and OpFAdd %double for them in both cases. This is the lesson of §SUR-54 again: a type predicate cannot see a defect in generated code.

Thus the pre-flight refuses the module until the arithmetic exists, and the AST walker answers instead. The function returns the type id that it found, so the diagnostic can name it.

isGlslExtOpcodeImplemented

[static] bool Interpreter::isGlslExtOpcodeImplemented(uint32_t extOpcode)

Returns whether the interpreter implements the GLSL.std.450 extended instruction extOpcode. This is isOpcodeImplemented() for the instructions inside an OpExtInst.

isOpcodeImplemented

[static] bool Interpreter::isOpcodeImplemented(uint16_t opcode)

Returns whether the dispatch of execute() has a case for opcode.

This is spirv-unified-lowering `SUP-6`. Backend::Interpreted compiles a program to SPIR-V and runs it here. Before this check, a module with one instruction that the dispatch did not implement gave an evaluation-time error. The error came halfway through a batch, in a program that the caller had been told compiled.

A check before the run changes that error into a refusal before anything runs. The lane then declines cleanly, and the caller falls back with a reason that names the instruction (§SUT-3a).

The set below is `Interpreter::execute`'s own case labels, and the two must move together. `testSpir` scans real corpus modules and asserts that every opcode it reports implemented also executes without an `UnimplementedOpcode` error, so a case added to one and forgotten in the other fails a test rather than drifting quietly — which is the only reason a second list is tolerable at all.

In this section