open-standards/wai/wai-quantum/docs/formats.md
Transaction Science cb059d7a0b wai-quantum: other formats bounded against hostile input, in CI
A jeff register's size reached Vec::with_capacity before the qubit cap,
so one of 2^32 - 1 qubits asked for 34 GB: under wasm32-wasip2 the test
written first trapped in qureg_op. It is now refused (Q111) before
anything is made. The audit of every allocation and loop sized by the
input:

- the wire reader bounds every pointer, segment and list in 64 bits (a
  segment count of u32::MAX overflowed a 32-bit usize), and counts each
  list element against the words it may visit;
- the jeff reader counts every register slot, array element (an array
  copied by setIndex included), call frame and condition term against
  one budget, caps arrays, values per function, operations run and gates
  written, shares conditions between copies, and no longer truncates a
  64-bit size, index, selector or shift to fit;
- the QIR reader no longer copies a block's lines on each visit, bounds
  the nesting of branches, the instructions run over every path and the
  state each path copies, and dedups a condition's reads;
- a condition is at most 1 024 terms and 32 deep once written, a run of
  one operator written flat;
- a conversion's check is not run past 262 144 gates applied or 4e9
  amplitudes, and says so; both readers take modules of up to 16 MiB.

tests/formats_mutation.rs mutates every QIR and jeff file of the
conformance set from a fixed seed (45 153 inputs: bits flipped, files cut
short and spliced, every length, count, offset and 32-bit field inflated)
and holds each to converting or a refusal with a code of the table,
within 20 s and 1 GiB measured by a counting allocator.
wai-quantum-formats.yml runs it natively and under wasm32-wasip2 (a
release build with overflow checks), the count pinned.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 07:16:32 -04:00

11 KiB

Other formats: QIR and jeff

The engine reads programs in two formats beside OpenQASM and writes them (wai_quantum::engine::formats). A reader gives the program as OpenQASM 3, which the one reader every surface uses then reads, so a program from any format plans, runs, checks and draws as one written in OpenQASM. A construct a reader cannot hold exactly is refused with a code (Q700 to Q721, in the table of codes) and where it stands: a line of a QIR module, or a function and an operation of a jeff module.

Format Read Written
QIR, LLVM text 1.x (typed pointers) and 2.x (opaque pointers), under the base and adaptive profiles 2.0, opaque pointers: the base profile, or the adaptive profile for a program that measures in the middle, resets or branches
jeff, its Cap'n Proto message 0.2 and 0.3 0.3.1

Converting

wai-quantum convert prog.qasm --to qir > prog.ll           # QIR 2.0, base profile
wai-quantum convert teleport.qasm --to qir > teleport.ll   # it branches: the adaptive profile
wai-quantum convert prog.ll --to jeff -o prog.jeff         # the format read is found from the file
wai-quantum convert prog.jeff --to qasm                    # as OpenQASM 3
wai-quantum convert prog.jeff --to qasm --json > back.json # with the notes, what was read and the check
wai-quantum run prog.ll --shots 1000                       # every verb that reads a program reads these
cp prog.jeff prog.bin
wai-quantum lint --from jeff prog.bin                      # a name the extension does not give

Every conversion is checked. What is written is read back and held to the program read by the equivalence ladder (a tableau, every basis state at up to 8 qubits, rewrites, random product states, or unchecked with the reason). A program read from QIR or jeff is written back to its format and read again, and the two readings are held to each other the same way. A program that measures in the middle or branches is held step for step, and where its steps differ (a condition on several bits written as one branch per bit, say) by the probability of every outcome, branch by branch with its weight, which is evidence and says so.

The same conversion is the MCP tool convert_program on both servers, the job service's POST /v1/convert, and engine_convert in the browser engine. A job may carry its program in another format: {"source": {"format": "qir", "program": "…"}} in place of "qasm" ("program_base64" for jeff); it is read before anything else reads the job, and the request keeps its source.

QIR

Profiles. The entry point's qir_profiles names the profile, and the module is held to it:

  • base: gates, then measurements, then output records. A branch, a classical instruction, reading a result, or a gate or reset on a qubit after its measurement is what the profile forbids (Q704).
  • adaptive: measurements anywhere; results read as booleans (__quantum__rt__read_result, or QIR 1's __quantum__rt__result_equal with result_get_one) and branched on, forward; i1 logic (and, or, xor, icmp eq and ne, select). An optional capability its module flags do not declare is forbidden (Q704). Integer and float computation the flags declare is computed on reading where its operands are known (an angle computed from constants); computation on a measurement anywhere but a condition, loops, switch, phi, IR-defined functions, dynamic qubits or results and arrays are not taken here, though declared (Q705).

A module that names no profile is read under the profile its instructions need, and one that leaves out the version flags or the qubit and result counts is read from what it holds; each says so (Q702). A profile other than these two is refused (Q703), as is a major version other than 1 or 2 (Q701).

The program. Qubit i is q[i] and result r is c[r]; the control flow, a tree of forward branches, is written as nested if and else, each branch running to the block where its paths meet again. A result read, measured again and only then branched on is refused (Q708): the text would read the later measurement. Output records are listed in what was read (recorded); classical values recorded as output are not carried (Q721).

Intrinsics read. __quantum__qis__<gate>__body, and __adj for the inverse:

Intrinsic OpenQASM Intrinsic OpenQASM
x y z h x y z h cnot, cx cx
s, s__adj s, sdg cy cz swap cy cz swap
t, t__adj t, tdg ccx ccx
sx sx ccz ctrl(2) @ z
rx ry rz (angle first) rx ry rz rxx ryy rzz their basis changes, two cx and one rz
r1 p mz, m, mresetz, reset, barrier measure, reset; a barrier is left out

A module of up to 16 MiB is read; its branches nest at most 64 deep, and are written out to at most 1 048 576 statements, 4 194 304 instructions run over every path and a bounded copy of each path's state (Q705). A condition holds at most 1 024 terms, nested at most 32 deep once written.

Any other intrinsic is refused (Q706), and so is a __ctl form, whose controls come in an array (Q705). Angles are read at their exact doubles, LLVM's hex form (double 0x400921FB54442D18) or decimal. A Clifford+T module stays Clifford+T and runs on the bit-exact method; a rotation at an angle that is not a dyadic multiple of 2π reads with a note (Q709): the double-precision methods take it, the bit-exact method refuses it.

Writing. QIR 2.0 with opaque pointers, the four mandatory module flags (and, under the adaptive profile, ir_functions, backwards_branching, multiple_target_branching and multiple_return_points, each off), one array_record_output and a result_record_output for every bit. The base profile's four blocks; under the adaptive profile, one block per step a branch reaches, a condition on several bits read bit by bit with __quantum__rt__read_result and br alone. Gates are written as the intrinsics above where one exists; the others are lowered exactly, up to a global phase (U as rz ry rz, cp and the controlled rotations by their standard identities, ch, cu, iswap and cswap by theirs), and a phase at a multiple of π/4 is written as z, s and t, so a Clifford+T program stays Clifford+T. Every angle is written as its exact hex double. A program whose steps jump backwards is refused (Q720), as is a program that branches when the base profile is asked for.

jeff

The module. A list of functions, each a dataflow graph of operations over values a qubit uses exactly once. The reader runs the entry point's graph in an order its values allow:

  • Qubits: each alloc is a new qubit, in the order the reader reaches it; a register (qureg) of a size known on reading is its qubits, extracted, inserted, sliced, split and joined by index. An entry point that takes qubits takes them as the program's first qubits, from |0⟩, and returns them unmeasured (Q716).
  • Gates: the well-known gates (x y z s t h, i as id, r1 as p, rx ry rz, u as U, swap, gphase), with their control qubits given after their targets, as the schema says, and adjoint and power (0 and 1 both apply the gate once) as ctrl @, inv @ and pow @. A Pauli-product rotation is exp(iθP), as the schema defines it, lowered exactly to basis changes, a parity ladder and one rz (Q712). A custom gate is read only when its name is a gate of the standard library, in any case, with that gate's qubits and parameters, and says so (Q711); any other is refused (Q710).
  • Measurement: measure and measureNd each write a new bit, in the order the reader reaches them; when the entry point returns its bits (one-bit integers, or arrays of them), the program's bits are those, in that order. reset resets.
  • Classical values known on reading are computed then, the same bits on every platform: integers at their width, floats through the crate's own functions. A measured bit may be negated, combined with and, or, xor, eq and select, widened, and branched on; other arithmetic on it is refused (Q714).
  • Control flow (Q715): a for whose bounds are known, and a while whose condition is, unrolled; a switch on a known value decided; a switch on a measured bit written as if … else, a bit set on one side joined with the other side's as a condition; and a call of a function the module defines inlined, up to 32 deep.

A value used twice, before it is defined or as the wrong type is refused (Q713); a value the reader needs on reading and cannot know is refused (Q714), as is a qubit the two sides of a branch leave in different places (Q708). Metadata is not read. The reader takes modules of up to 16 MiB, runs up to 4 194 304 operations (Q312), writes up to 1 048 576 gates (Q210) and 65 536 qubits (Q111, a register's size checked before it is made), and holds up to 65 536 elements in one array and 4 194 304 registers' slots, array elements, values and condition terms in all (Q714).

The wire format. A Cap'n Proto message, read and written by a reader and writer of its own (no dependency): segments, far pointers and every element size, each pointer bounds-checked in 64 bits, at most 4 Mi words visited (a list of values counting each element) and 64 pointers deep, so a message that points many times at one object, loops, or states a size past its end does no more work than that.

Writing. One function, main: every qubit allocated in order, each gate a well-known gate, exactly, global phase included (controlled gates with their control qubits, sx as rx(π/2) and a global phase, rxx, ryy and rzz as Pauli-product rotations, iswap by its Clifford identity, cu as a controlled u and the phase on its control), each measurement written into a bit array at its bit's index, which main returns, and every qubit not measured last freed. A program that branches on what it measured is refused (Q720); one that measures in the middle or resets is written with measureNd and reset.

Hostile input

Every reader refuses rather than allocates what a file's sizes ask for: each size, count, index and bound is held to a cap before anything is made from it, and every list a message holds counts against the words it may visit. The check a conversion runs is bounded too: past 262 144 gates applied or 4·10⁹ amplitudes touched it is not run, and the answer says unchecked with the reason (wai-quantum equivalent runs it on its own). tests/formats_mutation.rs holds every file of the conformance set, mutated by a seeded generator, to that: each input converts or is refused with a code, within its time and memory, natively and under wasm32-wasip2.

Conformance

wai/quantum-conformance/formats/ holds a module in each format for each construct, with the program it must read to, the checks that hold it (the same circuit by the ladder, the same counts, a round trip, the notes it gives) and the refusals with their codes and places; CI runs it natively and under wasm32-wasip2 (wai-quantum-formats.yml).