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

196 lines
11 KiB
Markdown

# 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](qasm-diagnostics.md)) 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
```sh runnable
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`).