- Python: qudit, qudit_check, wasmq (an envelope's bytes, a path or an example's name), wasmq_verify (with the envelope, run again byte for byte) and wasmq_examples, the extension module running an envelope on wai-wasmq's own interpreter (no compiler in the wheel); Service.wasmq and Service.qudit run them on a service. Tests: both kinds run and verified here, a qudit run equal to `wai-quantum qudit run`'s bits and a wasmQ/1 job equal to `wai-quantum run`'s byte for byte, and both run on a service built from this checkout. - npm (wai-web, quantum_engine): QuantumEngine.qudit and QuantumEngine.wasmq (the interpreter, the only engine on wasm32), engine_qudit_check, engine_wasmq_verify, engine_wasmq_examples and engine_wasmq_example. The smoke test runs every example in Node, each sealed, held to its form and run again, a fault sealed and W013 refused. - Quantum Studio: the "Qudits" and "wasmQ" panes (closed until opened), in the browser and in the desktop app (its qudit, qudit_check, wasmq, wasmq_verify and wasmq_examples commands): the qudit counts by digits, amplitudes as integers, bounds, the seal checked and the request run again; an example envelope or a .wai file opened there, its outcome, calls, fuel, counts and energy, its seal and form checked and the envelope run again; a refusal with its code. The browser e2e drives each. - quantum-wasmq §11.9 and quantum-sim §9.10 name every surface. Tested on the build hosts: wai-web for wasm32-unknown-unknown and the npm smoke test in Node 26 (macOS arm64); the Python package's wheel (maturin 1.15.0), clippy -D warnings and pytest (95 passed, the widgets' node test skipped there) on Linux x86_64; the Studio's browser e2e (macOS arm64, STUDIO_SKIP_SERVICE=1), every qudit and wasmQ check passing. Co-Authored-By: Claude Opus 5.5 <[email protected]>
81 KiB
WAI Extension: Hybrid Quantum-Classical Execution (wai.quantum.wasmq)
Status: Draft. Reference implementation: wai-wasmq (the profile, src/profile.rs;
the prepared module, src/meter.rs; the interpreter, src/interp.rs; the engines,
src/engine.rs; the lowering, src/abi.rs; the envelope, src/envelope.rs; the run and
its receipt, src/run.rs), the OpenQASM reader's callouts (wai-quantum,
qasm::read_callouts), and the command line's pack-wasmq, check-wasmq, run and
verify (wai-quantum-cli, src/wasmq.rs). A job's form (§6.5) is the engine's, where
every verifier of its receipts reaches it (wai-quantum, engine::wasmq). Interface: quantum-wasmq.wit.
Vectors: ../quantum-conformance/wasmq/.
Keywords MUST, MUST NOT, SHOULD and MAY are RFC 2119/8174.
0. What this extension adds
A hybrid quantum program runs classical code between its gates: it decodes a syndrome
and corrects the error before the next round, or plans the moves that fill an atom
array. OpenQASM 3 declares such a function with extern and leaves its body, its binary
form and how it is linked outside the language. This extension fills that gap for WAI
Quantum, and nothing else:
- One envelope. A WAI1 envelope of capability
wai.quantum.wasmqcarries the OpenQASM 3 program, the WebAssembly modules that define itsexterns, and the modules' parameter files (priors), each pinned by BLAKE3 in a canonical contract (§2). - One profile. A module is a core WebAssembly module in the Lime1 feature set, which imports nothing, whose memory, size and stack are bounded, and which uses nothing whose result a host may choose (§3).
- The same bytes and the same fuel everywhere. Fuel, the stack's weight and the NaN a float instruction makes are defined on the module as written (§4), so every runner gives a callout the same results, the same fuel and the same fault. The reference runs each module on two engines, a native one and an interpreter that also runs on wasm32, and every vector holds on both, on every host.
- A receipt. A run seals its result in the job receipt the engine already signs
(
engine::receipt): the counts, the fuel each callout used, the modules' digests, a digest of every call's inputs and outputs, and the fault that stopped the run, if one did (§6).
This version of the profile runs Clifford programs, in whole numbers from end to end
(§5.1, §6.1). A run simulates the quantum program and runs the classical code for real.
§7 says what holds when the quantum part runs on hardware, and §10 what this version
leaves out. Its second version, wai.quantum.wasmq/2 (§11), adds a method that runs t,
Toffoli and gates at dyadic angles on the engine's whole-number state vector, the same bits
on every host; angle arguments for callouts; and the energy a run took, read where its host
meters it and stated unmetered where it does not. A first-version envelope runs, seals and
verifies under it exactly as it did.
1. Principles
- Nothing outside reaches the result. A module imports nothing: no clock, no random source, no file, no network, no host function. Its only inputs are its bytes, its priors, the seed the run gives it and the bits the program passes (§3.2, §5.3).
- The host chooses nothing. Every place where WebAssembly lets an engine choose (a NaN's bits, a relaxed vector instruction, how deep calls may go, when memory runs out, when to stop a long computation) is fixed by the profile or refused (§3, §4).
- Fuel is the measure, not time. A callout's budget is a number of weighted instructions (§4.2). It is the same on every host; wall-clock time is not, and the profile does not claim one.
- The direction is the hardware's. The quantum program calls the module, as a real-time controller calls classical code between gates. A module drives no gates.
- One form for one bundle. A bundle has exactly one envelope, so its BLAKE3 names it, and a receipt that names the digest names the program, the modules, the priors and the run (§2.4).
- What is read is what runs. A reader refuses on reading every bundle a run could not finish for a reason of the bundle's own (§2.3). A run that starts ends in a receipt, unless the host itself fails (§6.3).
2. The envelope
2.1 Container and manifest
The envelope is a WAI1 container (SPEC §2). Its manifest is, in this order and with no other member:
{"wai":"1.0","media":"binary","intent":"replicate",
"model_requirement":{"capability":"wai.quantum.wasmq","fallback":null},
"conditioning":{"kind":"wasmq"},
"target":{"qubits":17,"shots":32,"seed":1234}}
fallback is null: a sink without this capability holds no way to run the program's
callouts, so the envelope is inert there (SPEC §4). target is informational (SPEC §3)
and is the contract's: the program's qubits (§2.2), the shots and the seed.
conditioning carries no prior: SPEC §3.1's pin names a parameter set the sink
supplies, and a wasmQ envelope carries its modules' priors itself, each pinned in the
contract (§2.2).
2.2 Payload and contract
payload = "WQM1" | u32 contract_len | contract | frames
frames = program | for each module, in order: the module, then each of its priors in order
contract_len is little-endian. The contract is JSON in the canonical form of RFC 8785
(JCS), with exactly these members:
| member | |
|---|---|
profile |
"wai.quantum.wasmq/1" |
program |
{ "blake3", "bytes", "inputs" }: the OpenQASM 3 source's digest and length, and the values of its integer inputs (an object, empty when there are none) |
modules |
for each module: { "name", "blake3", "bytes", "priors": [{ "name", "blake3", "bytes" }] } |
callouts |
one for each extern the program declares, in the order it declares them: { "extern", "module", "export" }, the module by its index |
run |
{ "shots", "seed", "method", "state", "fuel": { "per_call", "per_job", "per_instance" } } (§6.1, §4.2) |
blake3 is the BLAKE3 digest of the frame as 64 lowercase hexadecimal digits; bytes
its length. A name is for people. method is stabilizer, the one method of this
version (§5.1). state is shot (a fresh instance of each module every shot) or job
(one instance for the run, its shots in order). shots is 1 to 2^20. seed and the
size of each input are at most 2^53 − 1, so that every JSON reader holds them exactly, and
each budget at most 2^39 (§4.8). per_job is held to less with any module: its eight
operations for each unit of fuel take all of a run's work at 2^39, and a run's other work
is never nothing (§4.8). Every module is named by
at least one callout. The program's text is at most 1 MiB, and the modules'
bytes at most 512 KiB among them.
The program's qubits are the qubits it declares, all its registers together; a program
that declares none and names physical qubits ($0, $5) has one more than the highest
index it names, the qubits below it among them.
2.3 Reading an envelope
A reader MUST refuse an envelope, with the code of §3.3, when:
- it is longer than 128 MiB, its first four bytes are not
WAI1, its capability is notwai.quantum.wasmq, its manifest is not the one §2.1 gives for its contract byte for byte, or any byte follows the payload (W013); - the payload's magic is not
WQM1, the contract is not in its RFC 8785 form, holds a member §2.2 does not list or lacks one, or the frames do not end exactly where the payload ends (W013); - a frame's BLAKE3 is not the one the contract pins: the program, a module, a prior
(
W014); - a module breaks the profile (§3: its code), the modules hold more than 4096 pages among
them (
W003, §4.7), or more than 512 KiB of bytes (W010); - the program is longer than 1 MiB, is refused by the reader, calls nothing, has a gate
outside §5.1's, or is past a bound §4.8 reads it within (
W016); - the callouts are not one for each
externin the order declared, a module is named by none, a callout's export is not the type itsexternlowers to, or a module the host must write to or read a list from does not export what §5.2 asks (W015); - the run is outside §2.2's ranges, or past what §4.8 lets a run ask of its host (
W017).
A writer checks the same rules and writes nothing that breaks one. An envelope that passes them runs: nothing about a bundle is left for a run to find (§6.3).
2.4 One envelope for one bundle
A writer MUST write the frames in §2.2's order, the contract in its canonical form with
its callouts in the order the program declares its externs, and the manifest exactly
as §2.1 gives it. A reader that holds these rules reads every bundle from exactly one
sequence of bytes, so the envelope's BLAKE3 (envelope_blake3) names the bundle, and a
receipt that binds it binds every frame.
3. The module profile
3.1 A core module
A module is a core WebAssembly module (WebAssembly 3.0, §5 Binary Format), not a
component. It MAY be the core module of a component: its exports are the canonical
lowering of the functions quantum-wasmq.wit declares, so a
component host can wrap it, and a host of this profile calls it directly.
3.2 The rules
A module MUST validate under the Lime1 feature set of the WebAssembly tool
conventions: WebAssembly 1.0 with mutable globals, multi-value, sign extension,
non-trapping float-to-int conversion, memory.copy and memory.fill, extended constant
expressions and the overlong call_indirect encoding. Every instruction is in the
encoding that feature set gives it, and every function body ends with the end of its
own block. And:
| rule | why |
|---|---|
| It imports nothing. | Nothing outside the module reaches its result. A clock, a random source or any host function would. |
| It has at most one memory, 32-bit, unshared, of 64 KiB pages, with a maximum of at most 4096 pages (256 MiB). | A memory without a maximum grows as far as a host lets it, which differs by host. |
| It has no start function. | Nothing runs when an instance is made: every instruction runs in a metered call (§5.3). |
| Nothing shared, no atomic instruction. | Lime1 has no threads. |
| No vector instruction or type, relaxed or not. | A relaxed vector instruction's result is the engine's choice; the profile takes none. |
| No instruction or type of another feature outside Lime1: references, exceptions, tail calls, garbage collection, passive segments. | Lime1's values are numbers. |
At most one table, of funcref, at most 2^16 entries, filled once from active segments of function indices; no instruction reads or changes it but call_indirect. |
table.grow stays non-deterministic in the deterministic profile (below). |
| In every function, at most 4096 parameters, locals and operands at once. | It bounds what one frame holds in an interpreter. |
| At most 512 KiB of bytes, which is also what the modules of a bundle hold among them; at most 2^16 types, functions, globals, data segments and element segments each; at most 2^10 exports, each name at most 256 bytes; a function type of at most 2^8 parameters and 2^8 results; a function's entry in the code section of at most 2^14 bytes. | They bound what a reader, an engine and a frame need (§4.3), with room for what preparing a module adds (§4.4): a body of calls grows some 53 times prepared, and an engine compiles a function whole. |
| Every active data and element segment fits where it is placed. | Instantiation then cannot fail. |
No export begins __wasmq, and an export a host calls has exactly its type (§5.2, §5.3). |
The first are the prepared module's names (§4.4); a host passes the second arguments of those types. |
WebAssembly 3.0 defines a deterministic profile: every NaN an instruction makes is
canonical and positive, and every relaxed vector instruction has a fixed behaviour. Even
under it, memory.grow and table.grow remain non-deterministic, to indicate resource
exhaustion, and the depth of calls an engine allows is an implementation limit. This
profile takes the deterministic profile's rule for NaNs (§4.5) and closes the others: no
vector instruction is taken, the table never grows, the memory grows only within the
maximum the module declares (§4.7), and the stack is weighed (§4.3).
The limits of the last rows are each below the limits of the validator and the native
engine the reference uses, and stay below them once a module is prepared: a test makes a
module of the most bytes, its largest body all calls, with the most exports, names, table
entries and memory and as many of each other item as fit beside them, and prepares,
compiles and runs it on every engine (wai-wasmq/tests/engines.rs).
3.3 Refusal codes
| code | the module or envelope |
|---|---|
W001 |
is not a valid core module in Lime1, for a reason no other code names (a component, a malformed module, a body that does not end, an encoding of another feature) |
W002 |
imports something |
W003 |
has more than one memory, a 64-bit one, one with a page size of its own, one without a maximum or past 4096 pages; or (envelope) has modules with more than 4096 pages among them |
W004 |
has a start function |
W005 |
has a shared memory, global, table or type, or an atomic instruction |
W006 |
has a vector instruction or type, relaxed or not |
W007 |
has an instruction or a type of a feature outside Lime1 that no other code names |
W008 |
has more than one table, one not of funcref, past 2^16 entries, or an instruction that reads or changes it other than call_indirect |
W009 |
has a function whose parameters, locals and operands can pass 4096 |
W010 |
is past a size the last rows of §3.2 give; or (envelope) has modules of more than 512 KiB among them |
W011 |
has a data or element segment that does not fit where it is placed |
W012 |
exports a name the profile reserves, or an export a host calls with another type than its own |
W013 |
(envelope) is not one this profile writes, or is past its size (§2.3) |
W014 |
(envelope) has a frame whose digest is not the one its contract pins |
W015 |
(envelope) has callouts that are not one for each extern in order, a module no callout names, an export of another type than its extern lowers to, or a module the host cannot reach as §5.2 asks |
W016 |
(envelope) has a program longer than 1 MiB, that the reader refuses, that calls nothing, with a gate outside §5.1's, or past a bound it is read within (§4.8) |
W017 |
(envelope) has a run outside the profile's ranges, or past what §4.8 lets a run ask of its host |
A module or an envelope that breaks more than one rule is refused with the code of one
of them. Each refusal vector (§9) breaks exactly one.
wai-quantum check-wasmq MODULE.wasm holds a module to the profile and names the code.
4. Running a module the same everywhere
4.1 Instances
An instance is made from the module with no imports. Its memory starts at its initial size, zeroed, with the active data segments copied in; its table holds the active element segments; its globals hold their initial values. A host MUST be able to hold the memory's maximum before it calls the instance (§4.7).
4.2 Fuel
Every call a host makes into an instance has a limit of fuel. Fuel is defined on the module as written, not on any engine:
- A function's body is a sequence of runs. A run starts at the function's first
instruction and right after each
block,loop,if,else,end,br,br_if,br_table,return,call,call_indirectandunreachable; it ends with the first of them that follows, or with the body. - The weight of an instruction is 0 for
nop,drop,block,loop,unreachable,return,elseandend, and 1 for every other. Entering a function weighs 1. - When execution reaches the start of a run, the run's weight (with 1 more for the first
run of a body) is charged. Before
memory.copyandmemory.fillexecute, the byte count they take is charged; beforememory.growexecutes, 1024 for each page it asks for is charged, whether or not the growth succeeds: zeroing a page costs a host what 1024 instructions do. - When a charge takes the fuel below zero, the call stops with
fuel_exhausted.
So a charge happens at the start of a run, before any of the run's instructions, and a call that traps in the middle of a run has been charged the whole run. On every path that does not stop early, the fuel a call uses is the number of instructions it executed under these weights, and one for each function it entered.
Limits. The contract gives three budgets. A callout's limit is per_call, and it
covers every call the host makes for it (§5.2). The limit of an instance's lifecycle
calls is per_instance, for each of them (§5.3). per_job is the run's: when less than
a call's limit is left of it, what is left is the call's limit. The fuel a call used is
its limit less what it left, and is taken from what is left of the run's; a call that
stops with fuel_exhausted used its whole limit.
The weights are the default operator costs of the native engine the reference uses, with
one for a byte of memory.copy or memory.fill and 1024 for a page of memory.grow. A
test holds the profile's fuel equal to that engine's own count, on the module as written,
over control flow, calls and memory instructions, with one difference it states: for
memory.grow the profile charges 1024 for each page asked for, and that engine's own
meter one for each page it gets (wai-wasmq/tests/engines.rs).
Fuel is a count of work, the same on every host. It is not a time: a host that must answer within a deadline measures its own instructions per second and sets the budget.
4.3 The stack
Calls are bounded by weight, not by count, so that the bound holds for a frame of any size on any engine. The weight of a function's frame is
576 + its parameters + its results + its locals + its operands + the bytes of its entry in the code section
its parameters and results those of its type, its locals the ones it declares, its
operands the most values its body holds on the operand stack at once (as a validator
counts them after each instruction: a call's results among them), and its entry's bytes
the value of the entry's own size field. The stack's weight is zero
when a host calls an export. Before a call or call_indirect executes, the weight of
the function that holds the instruction is added to it; when the callee returns, the
same weight is taken off. A call that would take the stack's weight past 8 388 608
(2^23) stops with stack_exhausted, before the instruction does anything else: a
call_indirect that is both past the stack and through an entry it cannot call stops
for the stack.
So the frames suspended at calls never weigh more than 2^23 together, and one more
frame runs on top of them. An interpreter that keeps a frame's parameters, locals and
operands in slots needs at most one slot for each unit. The reference's native engine
needs at most eight bytes for each unit: a test runs modules made to keep as much in a
frame as the largest body allows (locals held across a call, a new value held at every
step, the results of calls held on the operand stack across a call, the largest function
types), each recursing to the limit, on a stack of exactly that size, and shows a smaller
one fails (wai-wasmq/tests/engines.rs). It gives each call nearly twice that.
A function of a few instructions recurses some 14 000 deep; a function at the largest body the profile takes, some 490.
4.4 The prepared module
A host MAY run a module with an engine that implements §4.2, §4.3 and §4.5 itself (the reference interpreter does). The reference's native engine does not, so it runs the prepared module: the module with every instruction kept byte for byte, and added
- two mutable globals, after the module's own, exported as
__wasmq_fuel(i64) and__wasmq_stack(i32); - at the start of each run whose weight is not zero, the charge
global.get F; i64.const w; i64.sub; global.set F; global.get F; i64.const 0; i64.lt_s; if; unreachable; end; - before
memory.copyandmemory.fill, the same charge of the byte count, and beforememory.grow, of 1024 times the page count, each taken from the operand withlocal.teeof one addedi32local; - around each
callandcall_indirect, withwthe weight of the function they are in,global.get S; i32.const w; i32.add; global.set S; global.get S; i32.const 8388608; i32.gt_u; if; unreachable; endbefore it andglobal.get S; i32.const w; i32.sub; global.set Safter it; - after each instruction of §4.5, a call of an added function that returns its argument, or the canonical NaN if the argument is a NaN;
with every custom section dropped. The host sets __wasmq_fuel to the call's limit and
__wasmq_stack to 0 before each call. When the prepared module traps at unreachable
with __wasmq_fuel below zero, the fault is fuel_exhausted; with __wasmq_stack past
2^23, stack_exhausted; otherwise the module's own unreachable.
The prepared module is one way to hold §4.2, §4.3 and §4.5, and its bytes are the reference's own: this text does not fix them, and no receipt names them. What a conforming runner must reproduce is what those sections define.
4.5 NaNs
The result of f32 and f64 add, sub, mul, div, min, max, sqrt, ceil,
floor, trunc and nearest, of f32.demote_f64 and of f64.promote_f32 is, when it
is a NaN, the canonical positive NaN (0x7fc00000, 0x7ff8000000000000): WebAssembly
3.0's deterministic profile. Every other instruction already has one result everywhere.
A test shows that a module whose result depends on a NaN's bits reads what the host chose
when neither the preparation nor the engine canonicalises, and the canonical NaN when
either does (wai-wasmq/tests/adversarial.rs).
4.6 Engines
The reference runs every module on two engines and holds them to the same bytes:
- native: the repository's one pinned release of wasmtime, compiling the prepared module with Cranelift, with NaN canonicalisation on, relaxed vector instructions fixed and refused, vector instructions, tail calls and every proposal past Lime1 off, its own fuel on as a backstop under the profile's, and a run's calls made on one thread with a stack of the size §4.3 bounds;
- interpreter:
wai-wasmq's own, which runs the module as written and implements §4.2, §4.3 and §4.5 itself, and which also runs on wasm32 (where the native engine does not run). It also runs the prepared module, with its own metering off, as a check of the preparation on the vectors; that use is held to none of §4.8's bounds.
wai-quantum run ENVELOPE.wai --engine native|interpreter chooses; the receipt is the
same.
4.7 Memory
memory.grow past the module's declared maximum returns −1, on every engine. Within the
maximum, it succeeds: a host that cannot grow the memory within the maximum (it has run
out of memory) MUST stop the run and seal nothing (§6.3), because what it would seal
would be its own condition, not the module's.
The modules of a bundle hold at most 4096 pages among them, by the maxima they declare
(W003), so that a 32-bit host holds any bundle's memory: a limit for each module alone
would leave the number of modules to decide which hosts can run an envelope.
4.8 What a run asks of its host
Fuel is the work of the modules. A run also asks work and memory of its host that no fuel counts: reading the program, its steps on the tableau, a fresh instance's memory, compiling its modules, the result. So that an envelope that opens on one host opens and finishes on every host, a 32-bit one among them, a reader bounds all of it from the bundle alone, before anything runs, and refuses past the bounds; nothing here differs by engine. A receipt shows part of what these bounds count, and §6.5 holds a job to that part (rules 29 to 32).
Reading. The program is read within these bounds, each checked as it is read, so that
a short program cannot make a reader hold or do more than a host has before it is refused
(W016):
| bound | |
|---|---|
| its text | 1 MiB (a reader holds some eighty bytes of tokens for each) |
| nesting | 64 levels in the text and 256 in all through the bodies calls read (Q006): a reader's stack. A statement, each statement that holds it, and each level of its expressions' trees (parentheses, operators, brackets, calls, casts and types) count one; a run of operators of one precedence and an else if chain each count one, at any length; an array has at most 7 dimensions |
| qubits | 32 768 |
| bits: the program's and its blocks' own together, any one value's, and those of any one array, its elements times each element's | 2^20 |
| steps, its loops unrolled | 2^21 |
| the bits its calls take and give, summed over its steps | 2^23 |
| the work of reading: one for each statement and loop iteration, for each value worked out one and one more for each bit or element it makes, and one for each bit declared | 2^26 |
Past one, the reader's code is Q323, or Q111 for the qubits. A run ends: the
reader unrolls every loop, a while included, to the bounds it reads or is told
(@wai.max_iterations), so every jump in a program's steps goes forward.
Memory. A run's shots times the bits a shot holds (all the program's bits, its blocks'
own among them, though an outcome shows only the program's, §6.1) are at most 2^28
(W017). With the most memory (§4.7), the largest
envelope (§2.3), the most code (§3.2), the most qubits, bits and steps and the bits of
calls, these leave a run within a 32-bit host's address space at every limit at once,
which a test holds the reference to under wasm32 (wai-wasmq/tests/sweep.rs).
Work. A run's work is at most 2^42 operations on a word (W017), where, with n the
program's qubits and r = ⌈n/32⌉ (at least 1) the words of a tableau's run for one
qubit:
| for each shot | operations |
|---|---|
| its fresh tableau | 2·n·r |
| each gate | r |
| each measurement or reset | 2·n·r |
| each callout | 4096 for each call of the module's (its export, cabi_realloc for each list argument, and cabi_post_<export> after a list result), 4 for each bit it takes and gives, and 1 |
| each other step | 1 |
for each instance, one for each shot, or one for the run when state is job |
operations |
|---|---|
each module, and each call of wasmq-seed, init, and two for each prior |
4096 |
| each byte of each module | 1 |
| each page of the memory a module starts with | 8192 |
| for the run | operations |
|---|---|
| each byte of each module, compiled | 131 072 |
each unit of the most fuel it may be given, per_job |
8 |
A random measurement rewrites the tableau's rows that hold an X at its qubit; a determined
one's outcome is the sign of the product of up to n stabilizers, which the reference
finds a machine word of rows at a time without copying the tableau. Either reads the
tableau about once, which is the n·r of its weight. A page a module grows is paid for in fuel (§4.2), as
a page it starts with is counted here. This is a bound on a host's effort from the bundle
alone, not a measure of it: fuel is the profile's measure of the modules' work (§4.2).
The reference was measured at a 64th of the limit, each kind of work alone, on x86_64
Linux natively and under wasm32: at the limit it would take at most about three hours.
5. Callouts
5.1 In the program
A program declares each function it calls with extern, its arguments and its result
written as types:
extern decode64(bit[4]) -> bit[9];
bit[4] s;
bit[9] corr;
s = measure a;
corr = decode64(s);
if (corr[0]) x d[0];
An argument is bit[n], uint[k] or int[k] (k from 1 to 64); the result is bit[n]
or nothing. An argument is a bit register or an integer known on reading (a constant, a
loop's variable, an input); the result goes into a bit register, in a declaration or
an assignment, and a later condition reads it. Calls inside for loops unroll; calls
under if run only on the shots that take the branch.
The gates. This version runs the Clifford gates that take no angle: id, x, y,
z, h, s, sdg, sx, sxdg, cx, cy, cz and swap, and gates a program
defines from them, with measurement, reset and conditions on bits, on at most 32 768
qubits. A program with any other gate is refused (W016), whatever its angle: rz(pi/2)
acts as s, and is refused all the same, so that no reader decides in floating point
which gate a program holds. With these gates a state is a stabilizer tableau and every
step of a run is whole-number arithmetic (§6.1).
OpenQASM 3 schedules an extern call and lets it run concurrently with other
operations. This profile fixes one schedule: a call runs when the program reaches it, in
program order, and completes before the next statement; it is atomic. A program read
with any other reader of this repository keeps refusing an extern call (Q125).
5.2 Lowering
A callout has one of two forms, by its widths, as quantum-wasmq.wit
declares them:
- scalar, when every argument and the result have at most 64 bits: each lowers to one
i64(WITu64), bitiof the register bitiof the integer, anint[k]sign-extended. The export's type is(i64, …) -> (i64), or-> ()with no result. - list, otherwise: each
bit[n]argument lowers to alist<u8>(ani32pointer and ani32length) and the result to onei32that points to a pointer and a length;uint[k]andint[k]arguments stayi64. The list's bytes areceil(n/8)long, bitiin bytei/8at biti % 8, the rest of the last byte zero: the Component Model's canonical ABI.
A host reaches a module's memory only as its export named memory. A module with a
list-form callout exports it, and cabi_realloc when the callout has a list argument
(W015). The exports a host calls have exactly these types (W012):
| export | type |
|---|---|
cabi_realloc |
(i32, i32, i32, i32) -> (i32) |
cabi_post_<export> |
(i32) -> () |
wasmq-seed |
(i64) -> () |
wasmq-prior |
(i32, i32, i32) -> () |
init |
() -> () |
A host makes a callout in these steps, each guest call under the callout's one limit, its stack's weight starting at zero:
- For each list argument, in order: it calls
cabi_realloc(0, 0, 1, len), chargeslenfuel, and writes the bytes at the address returned. - It calls the export.
- For a scalar result: a bit set at or above the result's width ends the run.
- For a list result at address
r:ris a multiple of 4; it reads a pointer and a length atr; the length isceil(n/8); it charges that many fuel and reads the bytes; no bit past the width is set in the last byte; then it callscabi_post_<export>(r)if the module exports it.
A step that cannot be done as written (bytes that do not fit in the memory at an
address, an address not a multiple of 4, another length, a bit past the width) ends the
run with bad_result, with the fuel used up to that step.
5.3 Lifecycle
Instances are made at the start of a shot, before its first step, one for each module in
the contract's order (each shot when state is shot, once at the first shot when it is
job); a fault in making one ends the run there, and no later module's is made. When an
instance is made, the host calls, in this order and each if the module exports it, each
under the limit per_instance:
wasmq-seed(seed: u64), with the run's seed, or, whenstateisshot, the shot's seed (§6.1);wasmq-prior(index: u32, bytes: list<u8>)for each of the module's priors, in the contract's order, the bytes passed as a list argument is (§5.2, step 1);init().
A module with priors exports wasmq-prior, cabi_realloc and its memory (W015).
5.4 Priors
A prior is a parameter file a module reads: a code's parity checks, a target register, a
noise model, a calibration. It travels in the envelope and is pinned in the contract, so
a module is never run against another prior than the one it was packed with (W014).
6. A run and its receipt
6.1 Shots and outcomes
A run goes shot by shot, in order. Each shot starts from |0…0⟩ with every bit 0, and
has a stream: the outputs of SplitMix64 from the state s XOR 0x51574d31, where s,
the shot's seed, is SplitMix64's output for the state
seed + (shot + 1) · 0x9e3779b97f4a7c15 (all sums modulo 2^64).
A measurement of a qubit in the computational basis has an outcome that is determined by
the state, or is 0 or 1 with equal chance; with §5.1's gates there is no other case. A
determined outcome is the outcome, and takes nothing from the stream. A random outcome
takes the stream's next value x, and is 1 when x < 2^63. A reset measures its qubit
the same way, without writing a bit, and applies x to it when the outcome is 1; a
reset of a qubit whose outcome is determined takes nothing from the stream.
A shot's outcome is its program's bits, n of them: every bit it declares at its top
level, whether or not anything writes it, numbered in the order the program declares them
(its first declared bit is bit 0, a register's bits in their order) and written bit
n − 1 first; the counts sum the shots that read each outcome. A bit that a block or a
subroutine declares is its own (Q322): the steps read and write it, and no outcome holds
it. Each measurement names the bit it writes: a measure with no target bit is refused
in a program that calls externs (Q324), since its outcome is the bits it declares. A
program that measures nothing has its bits as its outcome all the same, those its calls
write and 0 for each bit nothing writes: no qubit is read for it.
6.2 Faults
A run stops at the first fault, seals what it ran, and reports:
| reason | |
|---|---|
fuel_exhausted |
a charge took the fuel below zero (§4.2); budget says whose limit it was: call, instance, or run when the limit was what was left of the run's |
stack_exhausted |
§4.3 |
unreachable, memory_out_of_bounds, table_out_of_bounds, indirect_call_null, indirect_call_type, integer_divide_by_zero, integer_overflow, invalid_conversion_to_integer |
the module trapped (WebAssembly 3.0, §4 Execution) |
bad_result |
§5.2 |
A fault has the members reason, shot, phase (seed, prior, init or call),
module (its name), fuel (what the stopped call used), callout (the extern's
name) when the phase is call, and budget when the reason is fuel_exhausted, and no
other. The counts are of the shots that finished. A fault receipt is a receipt like any
other: it verifies, and running the envelope again reproduces it.
6.3 What is never sealed
A host that could not run the bundle seals nothing: a refused envelope (§2.3, §3.3), a host that cannot hold the memory's maximum or grow within it (§4.7), an engine that fails for its own reasons. These are the host's condition, not the bundle's, and a receipt that recorded them would differ by host. Nothing a bundle holds can lead here: what a bundle could cause is refused on reading (§2.3, §4.7, §4.8) or sealed as a fault.
6.4 The result
The job body is the engine's (engine::job), with exactly four members:
{ "status", "method", "request", "result" }. The request is
{ "wasmq": <the contract>, "envelope_blake3" }. status is done when the result's
status is ok and fault otherwise; method is the result's. The receipt signs the
request and the result, as the provider protocol's §8.2
seals every job; status and method only repeat what is signed. They are the host's
word, unsealed: a verdict takes nothing from them, and a check notes where they, or any
other member of the body, differ from what is sealed (§6.5).
The result has exactly these members:
| member | |
|---|---|
format |
"wai.quantum.wasmq.result/1" |
profile |
"wai.quantum.wasmq/1" |
status |
ok, or fault when the run stopped |
method, qubits, shots, seed |
the contract's method, the program's qubits (§2.2), the contract's shots and seed |
shots_run |
the shots that finished |
counts |
outcome to shots (§6.1) |
callouts |
the callouts made, one that stopped the run among them |
fuel |
{ "used", "instances", "budget": { "per_call", "per_job", "per_instance" }, "callouts": { name: { "calls", "fuel", "max" } } }: all the fuel used, the part of it lifecycle calls used, the contract's budgets, and for each extern the run called, its calls, their fuel and the most one of them used (an extern it did not call has no entry) |
modules |
for each, in the contract's order: { "name", "blake3" } |
transcript_blake3 |
BLAKE3 over wai:wasmq-transcript\x01 and, for each callout that returned, in order: its shot (u64), the index of its extern (u32), its input's length (u32) and bytes, its output's length (u32) and bytes, and its fuel (u64), little-endian, the input the arguments' bits in order and the output the result's, packed as a list's bytes are (§5.2); after a fault, the bytes fault, the shot (u64), the reason's bytes and the fault's fuel (u64) |
fault |
§6.2, when the run stopped |
accuracy |
{ "class": "exact" } |
energy |
{ "unmetered": "the runner reads no energy meter; fuel is the profile's measure of work" } |
receipt |
the engine's job receipt (engine::receipt): Ed25519 over BLAKE3 of the request and the result in RFC 8785 form |
Result-equivalence. Every conforming runner, on every host and every engine, gives an envelope the same result: the same bytes in its RFC 8785 form, the receipt aside. So the receipt's digest is the same everywhere, and under one key the whole job is.
6.5 Verifying
A wasmQ job is a job like any other: every verifier of the engine's job receipts reads it
and judges it by the provider protocol's §8.4,
and so gives it one verdict, wherever it is checked. wai-quantum verify (and its
--json and --lines), the job service's POST /v1/verify, both MCP servers given the
job's text, the Python package, the desktop app and a page read the job from its text as
§8.2 reads every job's, so its seal and its numbers are held as every receipt's are. A
surface given the job already parsed (a view, an MCP server given its members) cannot see
what parsing loses, as the provider protocol's §9 says, and gives every other reason; the
Python package judges a dict as the text it writes for it. A result
whose format is wai.quantum.wasmq.result/1 is a wasmQ job's, and §8.4 adds one reason
for it, wasmq_form: the job breaks a rule below. Only what is sealed (the request, and
the result less its receipt) and the form of its seal decide it. A signature shows that a key sealed a job and says
nothing of what the job holds, so without the envelope to run again these rules are what
hold a result to its contract and to itself: a key can seal any result of this form, and
only running the envelope again shows that a result is the one it gives.
In these rules a number's value is the double it reads as (§8.2). A count is a number
whose value is a whole number from 0 to 2^53 − 1, however it is written (1, 1.0 and
1e0 are one count, sealed as one); "at most" and "equals" compare counts, and a rule on
a count fails when the number is not one. A digest is a string of 64 lowercase
hexadecimal digits. Two values are equal as JSON values are, numbers by their value and
an object's members in any order. "Exactly the members" means every one named and no
other; a value that is not an object has none. A member is present when its object names
it, whatever its value, null among them: "has no fault" and "has a callout" read
so. A list or a map named below that is not an
array or an object is read as an empty one, after its own rule has failed. C is the
request's wasmq (the contract), run is C's run, r is the result and F is
r's fuel. The budget per_call, per_job or per_instance is that member of run's
fuel. The job breaks the form when any of these fails:
- No number in the request, or in the result less its receipt, has a magnitude past 2^53 − 1.
- The request has exactly the members
wasmqandenvelope_blake3, andenvelope_blake3is a digest. Chas exactly the members §2.2 gives:profile,program,modules,calloutsandrun; itsprogramexactlyblake3,bytesandinputs;runexactlyshots,seed,method,stateandfuel; andrun'sfuelexactlyper_call,per_jobandper_instance.C'sprofileiswai.quantum.wasmq/1.- The program's
blake3is a digest; itsbytesa count from 1 to 2^20; itsinputsan object each of whose values is a number whose value is whole and at most 2^53 − 1 in size. C'smodulesis an array that is not empty. Each module has exactly the membersname,blake3,bytesandpriors:namea string,blake3a digest,bytesa count of at least 1, andpriorsan array each of whose priors has exactly the membersname(a string),blake3(a digest) andbytes(a count). The modules'bytesthat are counts add up to at most 2^19, so each is at most that; rule 31 bounds a prior's.C'scalloutsis an array that is not empty. Each callout has exactly the membersextern,moduleandexport:externandexportstrings, andmodulea count less than the number of modules. No two callouts have the sameextern, and each module's index is themoduleof a callout.run'sshotsis a count from 1 to 2^20, itsseeda count, itsmethodstabilizer, itsstateshotorjob, and each of its fuel's three budgets a count,per_callandper_instanceeach of at most 2^39 (§4.8; rule 30 holdsper_jobto less).rhas exactly the members of §6.4 (format,profile,status,method,qubits,shots,shots_run,seed,counts,callouts,fuel,modules,transcript_blake3,accuracy,energyandreceipt), and may havefault.- The receipt is an object whose
canonisrfc8785: the first form (§8.2) is older than this profile, and no host sealed a wasmQ job in it. r'sformatiswai.quantum.wasmq.result/1, itsprofilewai.quantum.wasmq/1, itsaccuracy{ "class": "exact" }and itsenergythe one §6.4 gives; and itsmethod,shotsandseedequalrun's.r'squbitsis a count from 1 to 2^15 (a program declares a qubit at least,Q100), and itstranscript_blake3a digest.Fhas exactly the membersused,instances,budgetandcallouts; itsbudgetequalsrun'sfuel; andusedis a count of at most the budgetper_job(0 when that is not a count).F'scalloutsis an object. Each of its members is named by theexternof one ofC's callouts, and has exactly the memberscalls,fuelandmax:callsa count of at least 1;maxa count of at most the budgetper_call(0 when that is not a count) and at most its ownfuel; andfuela count (rule 20 bounds it by its calls and theirmax).F'sinstancesand thefuelof each member ofF'scallouts, added, areF'sused; and thecallsof each member, added, arer'scallouts. Each of them is a count.r'smoduleshas as many entries asC's, each with exactly the membersnameandblake3, itsblake3a digest, and itsnameandblake3equal to those ofC's module at its index.F'sinstancesis a count of at most M × S × the budgetper_instance(0 when that is not a count): S is the sum, overC's modules, of 2 and the number of its priors (each instance's seed, its init and each prior, §4.2, §5.3); M, the instances made, is 1 whenrun'sstateisjob, whatever the status; for any otherstateit isr'sshots_run(0 when it is not a count), plus 1 whenr'sstatusisfault.- When
r'sstatusisok:rhas nofault, and itsshots_runequals itsshots. When it isfault: the fault has exactly the membersreason,shot,phase,moduleandfuel, and may havecalloutandbudget;reasonis one §6.2 names;shots_runis less thanshots;shotequalsshots_run;phaseisseed,prior,initorcall;moduleequals thenameof one ofC's modules;fuelis a count, and in a phase other thancallat most the budgetper_instance(0 when that is not a count: a call's is held by its callout'smax, below, and rule 14; rules 20 and 25, with rule 15, hold either withinF'sused); it has acalloutexactly when its phase iscall, and that is theexternof one ofC's callouts that names a member ofF'scalloutswhosemaxis at least the fault'sfuel(the call that stopped is one of the calls made, §6.4); and it has abudgetexactly when its reason isfuel_exhausted, and that isrun, orcallin a call andinstanceotherwise. Any otherstatusbreaks the form. r'scountsis an object; each of its names holds only0and1(the empty name among them), all of one length; each of its values is a count of at least 1; and its values, added, arer'sshots_run.
Rules 20 to 35 tie members to each other, as the writer does. In them the fault is r's
fault when r's status is fault and that is an object, and there is none
otherwise; a rule about the fault holds when there is none. A call's fault is a fault
whose phase is call, and a lifecycle fault one that has a phase other than call.
- For each member of
F'scallouts, letsbe 1 when the fault is a call's fault whosecalloutis that member's name, and 0 otherwise, andethe fault'sfuelwhensis 1 (0 when that is not a count) and 0 otherwise.T, itsfuellesse, the fuel of its calls that returned, is not less than 0, and is fromktok×max, wherekis itscallslesss(0 when that is less than 0): the call that stopped used the fault's fuel, and each call that returned used at least 1 (§4.2: entering a function weighs 1) and at most the most one call used. Whenmaxis more thane, the most was a call's that returned, and each of the others used at least 1, soTis also at leastmax+k− 1.calls,fuelandmaxare counts. F'sinstancesis a count of at leastW×P+E, the least that making the instances took outside a stopped call (§5.3: a shot's instances are made module by module inC's order, each one'swasmq-seed, then its priors, then itsinit; passing a prior callscabi_realloc, at least 2 since it returns a value, then charges the prior's bytes, then callswasmq-prior, at least 1, §4.2, §5.2):P, what each set of instances took at least: the sum, over every prior of every one ofC's modules, of 3 and the prior'sbytes(0 when that is not a count), and 1 more when the fault is a lifecycle fault in phaseseedorinit: its module exports that function, and each set called it;W, the sets made whole:r'sshots_run(0 when that is not a count), plus 1 when there is a call's fault; whenrun'sstateisjobit is that or 1, whichever is less;E, when the fault is a lifecycle fault, what the set it stopped took at least: the same sum, of 3 and itsbytes, over the priors of each module before the one the fault names (the first ofC's modules with thatname), and, when its phase isinit, over that module's own; 0 otherwise, and when no module has thatname.
- A call's fault's
moduleequals thenameofC's module whose index is themoduleof each ofC's callouts whoseexternis the fault'scallout(rule 7 allows one): a call runs in its callout's module (§2.2, §6.2). It fails when there is no such callout, or no such module. - A fault whose
phaseispriorhas as itsmodulethenameof one ofC's modules whosepriorsis not empty: a prior is passed only to a module that has one (§5.3). - A fault whose
reasonisbad_resulthas thephasecallorprior: only these pass bytes or take a result (§5.2). - A lifecycle fault's
fuelis a count, andF'sinstances(a count) is at least it added to rule 21'sW×P+E: what the stopped lifecycle call used is part of the instances' fuel, beside what the calls before it took. - When
run'sstateisjob, a lifecycle fault'sshotis the count 0: the run's one instance of each module is made at the first shot (§5.3). - A lifecycle fault whose
shotis the count 0 comes withr'scalloutsthe count 0: the first shot's instances are made before its first step (§5.3). (Rule 32 holds this too, so rule 27 is never broken alone.) - A fault whose
reasonisfuel_exhaustedused its whole limit (§4.2). When itsbudgetiscall, itsfuelis a count equal to the budgetper_call; when it isinstance, to the budgetper_instance(each 0 when it is not a count). When it isrun,F'susedis a count equal to the budgetper_job(a count), and the fault'sfuelis a count, less than its own limit: the budgetper_callin a call's fault, and the budgetper_instancein a lifecycle fault, each read as 0 when it is not a count (a fault with nophasehas no limit here, and breaks rule 18): the run's budget was the limit only because less of it was left than the call's own. - When
r'scountshas a name, the length of the longest, in characters (Unicode code points), is at most 2^20, andrun'sshots(0 when that is not a count) times that length at most 2^28: a program holds at most 2^20 bits, and a run's shots times them are at most 2^28 (§4.8). (Rule 19 holds each name to0and1, one byte each; a verifier counts a name that breaks it in characters too.) - The part of a run's work that a receipt shows is at most 2^42 (§4.8):
S× 2·n·w+I× Σ (4096 × (1 + 2 ×p) +b) + 131072 × Σb+ 4097 ×K+ 8 × the budgetper_job.Sisrun'sshots;nisr'squbits;wis ⌈n/32⌉, or 1 when that is 0;Iis 1 whenrun'sstateisjobandSotherwise; each sum is overC's modules,pa module's number of priors andbitsbytes; andKisr'scallouts. Every number here that is not a count is read as 0. The host's work is at least this: each shot's fresh tableau, each instance's module and its calls ofwasmq-priorand ofcabi_reallocfor it, the run's compiling, each call made (its module's part, 4096, and its step, 1: a callout's step is counted in each shot whether or not it runs) and the run's fuel. - The envelope the contract names is at most 2^27 bytes (§2.3): 20 bytes of magic and
lengths, its manifest (§2.1's members, in that order and with no space: 192 bytes and the
decimal digits of its
qubits,r's, and itsshotsandseed,run's, each read as 0 when it is not a count),Cin RFC 8785 form (§2.2; no bytes when the request has nowasmq), and its frames (the program'sbytes, each module'sbytesand each prior'sbytes, each 0 when it is not a count), added, are at most 2^27. r'scalloutsis at most 2^21 ×H(each 0 when it is not a count), whereH, the shots that ran steps, isr'sshots_runand 1 more when there is a call's fault: a shot runs each of its steps at most once, every jump going forward, and a program has at most 2^21 steps (§4.8), each call one of them.- Each prior passed whole fits within one lifecycle call's limit: 3 and its
bytes(0 when that is not a count) are at most the budgetper_instance(0 when that is not a count), for every prior of every one ofC's modules whenW(rule 21) is at least 1, and for each priorEcounts otherwise. Passing a prior is one stretch under one limit:cabi_realloc, its bytes andwasmq-prior(§5.2, §5.3). - A fault's
fuel, when it is a count, is at least what itsreasonshows was charged (§4.2), when it has one: 1 for every reason butfuel_exhausted, one §6.2 does not name ornullamong them (a call stops any other way only after its entry was charged); 2 forbad_result(a value came back: the entry, and an instruction that made it); and 788 forstack_exhausted. A frame weighs at most 576 + 4096 + 2^8 + 2^14 = 21 312 (§3.2, §4.3), so the stack's weight passes 2^23 only at a call in the 394th frame entered or deeper, and each frame entered was charged at least 2: its entry, and the run that holds its call. - When no call returned, the transcript is what §6.4 gives such a run: when
r'scalloutsis 0, or 1 and the fault is a call's fault (each a count),r'stranscript_blake3is BLAKE3 over the byteswai:wasmq-transcriptand the byte 0x01 and, when there is a fault, the bytesfault, the fault'sshot(u64), itsreason's UTF-8 bytes and itsfuel(u64), little-endian (shotandfueleach 0 when it is not a count, andreasonno bytes when it is not a string).
What the rules leave free, and why. What else the writer fixes, the rules leave free,
each for the reason given, and only running the envelope again (--bundle) holds a job to
it:
- the
transcript_blake3's value once a call returned: it digests each call's input and output, which no receipt holds (rule 35 holds it when none returned); and eachblake3against the bytes it pins, which no receipt holds; r'squbitswithin rule 12, the outcomes' length within rule 29, and the number of callouts within rules 30 and 32, against the program: its text is not sealed, only its digest and length, so how many qubits and bits it declares, and how many calls each shot makes, are its own;- which shot read which outcome, and the counts' spread: only the run shows them;
- whether a module exports
wasmq-seedorinit, but for the one a fault in that phase names (rule 21): only its code shows it. So a fault in phaseseedorinitmay name any module; - the instances' fuel within rules 17, 21 and 25: what each lifecycle call used past its least, and how far the module a fault names got, are its code's;
- the fuel of each call within rules 14, 18, 20, 28 and 34, and a fault's fuel within rules 18, 20, 21, 25, 28 and 34: where in its work a call traps, and how many bytes its arguments pass, are its module's and its program's;
- a trap's
reasonagainst its phase: any guest call can trap any way, and onlybad_resultis the host's (rule 24); - the rest of §4.8's bounds (the steps of a shot past rule 32's count, the gates and measurements, the bits of calls, the pages a module starts with, the program's reading): each counts what only the program or the modules hold, so rules 29 to 32 take what a receipt shows, and a job a writer can write never breaks them;
- the least length of a program and of a module, and the spelling of an
extern: a program that packs declares anextern, a qubit and a call, a module that packs has a type, a function, an export and its code, and anexternis a name the reader reads, its letters any of Unicode's. So each has a least length, and anexterna form, that a rule could state; but they follow from the grammar, WebAssembly's binary format and Unicode's tables rather than from anything a run does, bound nothing a host does, and a rule at them would hold every verifier to those tables. Rules 5, 6 and 7 hold the program's and a module'sbytesto at least 1 and anexternto a string, and leave the rest free.
Beside §8.4's reasons a verifier of the engine states, for a wasmQ job, wasmq:
outcome, the sealed result's status; problems, what breaks each rule the job breaks,
in words, each naming its rule first (rule 19: ...), the first 64 of them and then how
many more; and notes, where the host's unsealed words
differ from what is sealed or are missing ("the host's unsealed status says done; the
sealed result says fault"). A note is no part of the verdict: the verdict is the sealed
content's alone, so that every surface that checks a job gives the same one.
wai-quantum verify JOB.json prints the receipt's check, the outcome and each problem and
note; --json gives the verdict as JSON. --lines reads a file of one job a line, each
line on its own as a job's text (a line that is not UTF-8, or blank, is that line's
unreadable; the newline that ends the file ends its last line), and gives one verdict a
line, each a job's whatever its kind; a file with no line is refused. A job found
unreadable, no job, with no receipt, or longer than the surface reads (too_large) is
reported alone (§8.4), whatever kind of job it says it is: verify says that its form was
not judged, and runs no envelope for it. verify reads at most 384 MiB and 2^22 values
(§8.4), more than any job holds, in about 1 GiB of memory at most on every host: a file past the
bytes is too_large by its size, before it is read, and one past the values once read,
before it is parsed; a file of lines past the bytes is refused, and a line past the values
is that line's too_large. With --bundle ENVELOPE.wai it also checks
that the envelope is the one the receipt binds (its BLAKE3 is the request's
envelope_blake3, and its contract is C), and runs it again here: the result must be
the same, byte for byte, the receipt aside; with --json the verdict then also says
bundle and reproduced, and is valid only when both hold. A wasmQ job carries no
history, so --history is refused for one.
7. On hardware
The bring-your-own-quantum boundary is unchanged. WAI Quantum never pays for or
resells hardware time; a person's providers speak wai.quantum.provider/1
(quantum-providers), whose program is a fixed sequence of native
gates measured at the end. A wasmQ bundle calls its modules between gates, which that
protocol does not carry, so run ENVELOPE.wai --provider NAME is refused. A wasmQ run
simulates the quantum part on the host that runs it: the person's machine (the command line,
its MCP server, the Python and npm packages, the Studio and the desktop app), or the job
service, which runs an envelope as a job (POST /v1/wasmq, the MCP tool run_wasmq) within
its own limits, refusing one past them before it runs rather than stopping it part way, and
seals the job with its key; a second-version run's energy there is what the service read of
the worker that ran it (§11.5).
Nothing here has run a module beside a quantum processor. This repository has run wasmQ modules only on the hosts its tests name. What holds for a module on a machine that runs classical code between gates, and what does not:
- A scalar-form module (§5.2) is a function of integers that imports nothing. Whether a given machine takes it is that machine's to say.
- The quantum part's measurements on hardware are not reproducible, so a hardware run
has no single result to reproduce. Its classical part does: given the inputs each call
received, a module gives the same outputs and the same fuel on every host, so a
record of the calls (
run --transcript) replays through the modules alone and is checked call by call (run ENVELOPE.wai --replay TRANSCRIPT), each of a shot the run has, in the order of its shots. Each call shows only itself; that a record is the whole of a run's shows in its digest, which a replay states and the receipt of a run that endedokseals astranscript_blake3. A machine that ran the module would have to report those inputs and outputs for anyone to check them. - Fuel is not time (§4.2). A real-time deadline is the machine's; the profile states the work.
8. Reference modules
Two modules of record are built from code the engine already has, for
wasm32-unknown-unknown with Lime1 (wai-wasmq/modules/, build.sh), and committed in
../quantum-conformance/wasmq/modules/ with
the digests of the sources that made them. rustc's wasm32 output depends on the host it
runs on, so they are built on x86_64 Linux, where CI rebuilds them and compares.
8.1 The qLDPC decoder
quantum_qec's decoders: union-find matching for a code whose qubits each lie in at most
two checks (the surface code), Relay-BP in fixed-point arithmetic otherwise (the
bivariate-bicycle codes), as the engine itself chooses. Prior 0 is a code's parity
checks in CssCode::check_matrix_bytes's form. Exports: decode (list) and decode64
(scalar). Tests hold every result to the same functions compiled natively, on every
weight-two error of the distance-3 surface code and on random errors of the 72,12
bivariate-bicycle code (CssCode::gross()), and run memory rounds of the distance-3 and
distance-5 surface codes and of that code (a 129-qubit circuit) with the decoder called
between the syndrome and the correction, each shot ending in the code, on every engine
with the same bytes.
8.2 The atom rearrangement planner
quantum_atom::solve, an exact Hungarian assignment of loaded atoms to target sites.
Prior 0 is the target register in target_bytes's form. Exports: plan (list) and
plan64 (scalar). A circuit loads each trap with probability ½, measures the occupancy,
calls the planner, applies its moves and reads the array: every shot whose plan is
feasible fills the target, and atoms are moved, never made or lost.
9. Conformance
../quantum-conformance/wasmq/ holds:
envelopes/: envelopes, each with the result it MUST give on every engine and every host (vectors.json,runs): the status, the fault, the fuel, the transcript's digest, the result's digest, the receipt's digest and, under the vectors' key, the whole job's digest. Among them are runs that end in every fault reason §6.2 names, in every phase and of every budget; callouts of the list form, with acabi_postcall and with each way a list result is not one; a signed argument; two modules made in order; a program that declares no qubit and names physical ones, its bits in two registers; a program that measures nothing; one with bits nothing writes, and one whose block holds a bit of its own, which pin §6.1's outcome; and a program whose resets and measurements pin §6.1's stream;refuse/: modules and envelopes a runner MUST refuse, each with its code (vectors.json,refuse), at least one for every code of §3.3, each one defect away from a module or an envelope that is accepted; the modules also as WebAssembly text where they have one. An envelope past §2.3's size has no vector, for its size: a test inwai-wasmq/tests/sweep.rsmakes one;modules/: the modules of record (§8).
Six wasmQ jobs the command line sealed are among the job receipts of
../quantum-conformance/receipts/ (jobs/cli-wasmq-*):
a run of the surface-code round, one of a single instance for the whole run, and faults in
a call, of the run's budget, in an instance's init and in a prior. Beside them are jobs
crafted from them, sealed again, each of rules 2 to 31 broken alone by one at least, so the
two verifiers of that corpus, which share no code, are held to each other on wasmq_form
and on the rules each states broken, and on every change of one member of each of the six.
wasmq_vectors verify wai/quantum-conformance/wasmq checks them on every engine of the
build and prints a digest of what it computed (each run's job and each refusal's code),
the same on every host that conforms.
10. Versioning
wai.quantum.wasmq/1 is this text. A change to anything that moves a result's bytes or
a module's fuel (the weights, the runs, the stack's weights, the lowering, the stream,
the transcript, the result's members) is a new profile version, and a receipt names the
version it was made under.
This version leaves out, and a later one may add under a name of its own (the second version, §11, adds the first three in part):
- gates that take an angle and gates outside the Clifford group, which need a method
whose arithmetic this text would have to fix bit for bit. The state-vector method,
which the engine runs in floating point, is not part of this version for that reason:
stabilizeris its one method (§2.2); - floating-point arguments and results of a callout;
- noise, and observables other than the counts.
11. The second version (wai.quantum.wasmq/2)
wai.quantum.wasmq/2 is this text with the changes of this section; what §11 does not
change holds for it as written. A runner of the second version runs a first-version
envelope (its contract's profile wai.quantum.wasmq/1) as the first version runs it, and
seals the same result: the vectors of §9 hold under it, byte for byte. The reference is
wai-wasmq (src/exact.rs beside the files §0 names) and the engine's engine::wasmq2.
11.1 The envelope and the callouts
The envelope is §2's. The contract's profile is wai.quantum.wasmq/2, and its run's
method is stabilizer (the tableau of §5.1 and §6.1) or exact (§11.3). A writer that
is given no method writes stabilizer when every gate of the program is one of §5.1's, and
exact otherwise; a reader refuses a method the program's gates do not fit (W016).
A callout's argument may also be angle[k], k from 1 to 64: an angle of m of 2^k
parts of a turn, 2πm/2^k, as OpenQASM 3's angle[k] holds it, its bits those of m, the
lowest first. It lowers as a uint[k] does (§5.2): one i64 that holds m, zero-extended,
in either form, WIT u64 (quantum-wasmq.wit, angle-callout). Its
value is
- a bit register of
kbits, read as the run writes them: a measurement's, or a callout's result; or - an angle known on reading: a sized
angle[w]value by its own bits, or a real value read as §11.2 reads an angle, asm'of2^32parts of a turn.
Either way it is a whole number of 2^-k turns, or the program is refused (W016): with
w the bits it is known to (32 for a real value), m is the value shifted up by k − w
when w ≤ k, and when w > k its w − k lowest bits must be zero and m is the value
shifted down by them. Nothing is rounded to fit, and no floating-point value crosses into a
module. In the transcript (§6.4) an angle argument's k bits are the input's bits as any
argument's are. The first version refuses a program with an angle argument (W016).
A callout's result is still bit[n] or nothing (§5.1). A module that computes a rotation
returns its angle's numerator as bits, and the program applies it as the engine decomposes
any dyadic phase, one phase gate under a condition for each bit (if (th[0]) p(pi / 4) q;
for the eighths of a turn, and so on): a run of feedback with non-Clifford steps, and no
angle read back as a float (vector v2-feedback-rotation.wai).
11.2 Gates and angles
The exact method runs §5.1's gates and t, tdg, p (phase), rz, rx, ry, ch,
cp (cphase), crz, rzz, ccx, ccz and cswap, and gates a program defines from
them, with measurement, reset and conditions on bits, on at most 26 qubits (§11.3). A
program with any other gate (u, cu, crx, cry, iswap, rxx, ryy) is refused
(W016).
An angle. A gate's angle θ is the binary64 value the reader computes for its
expression: OpenQASM 3's arithmetic in IEEE 754 binary64, each operation rounded to
nearest with ties to even, and the reader's functions (sin, cos, arccos, exp, ln
and the rest) its own implementations, which give the same bits on every host
(wai-quantum, repro). It is read as m of 2^32 parts of a turn by the engine's rule
(qasm::dyadic), in binary64:
t = (θ / τ) mod 1,τthe binary64 nearest 2π andmodthe Euclidean remainder;- for
j = 0, 1, …, 32, the firstjwith|t·2^j − r_j| ≤ 10^-14·2^j, wherer_jist·2^jrounded to the nearest whole number with halves away from zero, givesm = (r_j mod 2^j)·2^(32−j); when none does, the angle is not dyadic; - and the angle is
monly when it is that angle to within its double's own rounding (qasm::dyadic_exact): witha = m/2^32andg = min(|t − a|, 1 − |t − a|),g ≤ 16·2^-52·|θ/τ|.
An angle that fails 2 or 3 is refused (W016): an angle the engine's own exact method would
move to its grid, or run as no phase, runs on no wasmQ host. Each step is exact or one
correctly rounded operation, so every host reads the same m; the reader's floats end here.
The lowering. Each gate is the engine's dyadic gates (qasm::Program::dyadic, the gate
set of quantum-sim §2.2), in this order, with phase(λ) on t under
the controls C one P(32 − j) on t under C for each set bit j of λ's m, j
from 0 to 31 in order, and none when m is 0:
| gate | dyadic gates, in order |
|---|---|
id |
none |
x, y, z, h, s, sdg, t, tdg on q |
X, Y, Z, H, S, S†, T, T† on q |
sx, sxdg on q |
H, then S (S† for sxdg), then H, on q |
p(λ), rz(λ) on q |
phase(λ) on q |
rx(θ) on q |
H; phase(θ); H |
ry(θ) on q |
S†; H; phase(θ); H; S |
cx, cy, cz, ch on c, t |
X, Y, Z, H on t under c |
cp(λ) on c, t |
phase(λ) on t under c |
crz(θ) on c, t |
phase(−θ/2) on c; phase(θ) on t under c |
swap on a, b |
X on b under a; X on a under b; X on b under a |
rzz(θ) on a, b |
X on b under a; phase(θ) on b; X on b under a |
ccx, ccz on a, b, c |
X, Z on c under a, b |
cswap on a, b, c |
X on b under c; X on c under a, b; X on b under c |
−θ/2 is the binary64 the reader's arithmetic gives, read by the same rule. rz, rx,
ry and rzz are their phase forms: each drops the global phase e^{−iθ/2} of its matrix,
which no count shows. A gate a program defines, or writes with a modifier, is the gates the
reader expands it to.
11.3 The method exact
The state. 2^n amplitudes for a program of n qubits, each a pair (re, im) of
64-bit whole numbers at 2^-30, with qubit q at bit q of an amplitude's index; a shot
starts from |0…0⟩, the amplitude at index 0 (2^30, 0) and every other (0, 0). A
dyadic gate updates it as quantum-sim §3.0 gives, with the phase table
of wai.quantum.circuit2 (§3.2 there, Appendix B.2): the engine's own arithmetic, its
error bound the one stated there (§5 there, about 1.71 units of 2^-30 a gate).
A measurement of qubit q takes the state's weight W, the sum of re² + im² over
every amplitude, and W1, the same sum over the amplitudes whose index has bit q set,
each exactly (a 128-bit sum holds them). The outcome is 0 when W1 is 0 and 1 when W1 is
W, and neither takes anything from the shot's stream (§6.1). Otherwise it takes the
stream's next value x and is 1 when ⌊x·W / 2^64⌋ < W1, an exact comparison of whole
numbers (the product of a 64-bit and a 127-bit number), so that x/2^64 falls below
W1/W. Then every amplitude of the other outcome becomes (0, 0), and with K the weight
kept (W1, or W − W1) and s = ⌊√K⌋, each component v of every amplitude kept becomes
⌊(v·2^30 + ⌊s/2⌋) / s⌋, the floor of the quotient. An outcome the state determines leaves
the state as it is.
A reset measures its qubit as a measurement does, without writing a bit, and when it
read 1 exchanges each pair of amplitudes whose indices differ only in that bit (X).
Shots, outcomes, counts and the stream are §6.1's. Where a random outcome's two weights
are equal, W1 is exactly W/2 and ⌊x·W/2^64⌋ < W/2 holds exactly when x < 2^63: the
draw is the tableau's, value for value, so a Clifford program whose every random outcome
is even gives the same counts by either method (vector
v2-exact-of-a-clifford-program.wai, wai-wasmq/tests/exact.rs).
Its accuracy. The counts are those of this arithmetic: the same on every host, and the
ideal circuit's only to within the fixed point's error, which grows with the gates, as
quantum-sim §5 states. A result says so by its class, bit-exact (§11.4).
The ceiling. A program the exact method runs declares at most 26 qubits
(W016 past it), the engine's own exact method's limit. Its state is then 1 GiB, the
largest state a 32-bit host makes as one allocation: 27 qubits are 2 GiB, past the most one
object may span there, and the reference's attempt under wasm32 ends with a capacity
overflow. Measured on the reference, on an arm64 macOS host and an x86_64 Linux host, each
natively and under wasm32-wasip2 on the pinned wasmtime, both hosts shared with other work
(a one-minute load of about two a core):
-
Memory. Under wasm32, states of 24, 25 and 26 qubits are made and run beside 512 MiB of other memory, and 27 is not (
probe_the_state_past_the_ceiling). The largest bundle the bounds admit, by theexactmethod at 26 qubits (its state, a module's 256 MiB of memory, an envelope of 128 MiB and 2^20 bits a shot) runs under wasm32 in 8.4 s, the wasmtime process's resident set 2.1 GB (wai-wasmq/tests/sweep.rs,the_largest_exact_bundle_fits_a_32_bit_host, which CI runs under wasm32). -
Time, in nanoseconds an amplitude at 26 qubits, each dyadic gate alone (
wai-wasmq/tests/exact.rs,measure_the_exact_method; the same within a few percent at 20 to 24):arm64, native arm64, wasm32 x86_64, native x86_64, wasm32 H2.1 11.0 2.8 20.7 T1.1 5.3 1.5 10.7 P(k)1.5 5.3 1.6 10.6 Xunder a control0.7 2.8 0.7 1.4 Xunder two0.3 0.3 0.6 0.6 a measurement 0.8 4.6 1.2 9.8 So a gate on 26 qubits takes 0.14 s natively and up to 1.4 s under wasm32, and §11.7 weighs a gate's amplitude at 8 of §4.8's units: a run at the bound of 2^42 then takes at most about three hours on the slowest of these (
Halone, under wasm32 on x86_64), as a first-version run at the bound does (§4.8).
11.4 The result
A second-version result is §6.4's, with these members changed:
| member | |
|---|---|
format |
"wai.quantum.wasmq.result/2" |
profile |
"wai.quantum.wasmq/2" |
method |
the contract's: stabilizer or exact |
accuracy |
{ "class": "exact" } for stabilizer; { "class": "bit-exact" } for exact |
energy |
§11.5 |
Result-equivalence (§6.4) holds for the result less its energy and its receipt: every
conforming runner, on every host and every engine, gives an envelope those same bytes. The
energy is what the host read of its own run; verify --bundle runs the envelope again and
compares the rest. A runner that reads no meter writes the unmetered form, which is the same
everywhere, so the vectors (§9, §11.8) pin whole results.
11.5 Energy
energy is { "run", "callouts" }. run is the run's figure; callouts has one member
for each extern the run called, the names of fuel's callouts, each that callout's
figure. A figure is either
- the unmetered form, exactly
{ "unmetered": why },whya non-empty string; or - a reading: an object with
joules_micro, a count of at least 1;energy_provenance, a reading's class of energy-measurement §2 (CalibratedInstrument,OnChipCounterorHwShunt; an estimate is no reading); andenergy_uncertainty, whoserelative_ppmandwindow_usare counts of at least 1 and whoserelative_ppmtimesjoules_microis at least 288 676 (§2.3 there: the rounding of a reading to whole microjoules). It may say more (what it covers and leaves out, its attribution, its source).
A figure never reads zero joules: a run that read nothing is unmetered, and says why.
What the reference reads. wai-quantum run ENVELOPE.wai runs a second-version envelope
in a process of its own and reads it whole once it has exited, on the meter its host has:
on macOS the process's CPU energy (proc_pid_rusage, ri_energy_nj), attributed to the
process, read after it exits and before it is reaped; on Linux the processor packages'
counters (/sys/class/powercap), the whole packages over the run's window, attributed to
nothing; the figures are the engine's (engine::energy::figure, package_figure), as the
job service seals them. Where the host reads no meter (no counter, no privilege to read
one, a counter that is not live, a wasm32 build), run is the unmetered form with that
reason. A library run (wai_wasmq::run) reads no meter: its run says
the runner read no energy meter for this run.
Each callout's figure is the unmetered form, saying that no meter resolves one call: a call takes microseconds, and every counter the reference reads is of a whole process or a whole processor package, updated in steps far longer than a call. The run's figure covers the callouts' work with the rest of the run; fuel (§4.2) is each callout's measure of work, on every host. A host with a meter that does resolve a call (a calibrated instrument read around it, say) seals its reading in the callout's member.
11.6 Verifying
A second-version job is held to §6.5's rules 1 to 35, each read with the names of this
version (a verifier of the engine reads it through engine::wasmq2, whatever surface it
is checked on), with these changes, and rule 36:
- rule 4:
C'sprofileiswai.quantum.wasmq/2; - rule 8:
run'smethodisstabilizerorexact; - rule 11:
r'sformatiswai.quantum.wasmq.result/2, itsprofilewai.quantum.wasmq/2, itsaccuracythe one §11.4 gives its method, and itsmethod,shotsandseedequalrun's; rule 11 says nothing ofenergy, which rule 36 holds; - rule 12: when the method is
exact,r'squbitsare at most 26; - rule 30: when the method is
exact, the first term, a shot's fresh state, isS × 2·2^n(§11.7), notS × 2·n·w; - rule 1 holds every number in the energy too;
- rule 36:
r'senergyhas exactly the membersrunandcallouts;runis a figure;calloutsis an object whose members are exactly the names ofF'scallouts, each a figure (§11.5).
What a job's result format is decides which version's rules hold it: a result whose
format is wai.quantum.wasmq.result/1 is held to §6.5 as written, so a first-version job
whose contract or result names wai.quantum.wasmq/2 breaks rule 4 or rule 11.
11.7 What a run asks of its host
§4.8 holds, with the exact method's state in place of the tableau: for each shot, its
fresh state takes 2·2^n operations, and each dyadic gate a step lowers to (§11.2) and each
measurement or reset 8·2^n, where the tableau's take 2·n·r, r and 2·n·r. A run's
work is still at most 2^42 (W017); the weights are the reference's measured times (§11.3),
so that the bound holds a run to about three hours on every host measured.
11.8 Vectors
../quantum-conformance/wasmq/ holds second-version vectors beside the first's, each made
and checked as §9 says, on every engine and every host:
envelopes/v2-t-toffoli.wai:h,t,hand a Toffoli under two set controls, by theexactmethod: outcomes in the ratio of sin²(π/8);envelopes/v2-feedback-rotation.wai: a callout that turns a measured bit into three eighths of a turn, applied as its bits' phases between two Hadamards (§11.1);envelopes/v2-angle-arguments.wai: anangle[8]known on reading and anangle[4]of measured bits, given back by the module (§11.1);envelopes/v2-rotations-and-a-reset.wai: a random reset and each kind of gate §11.2 takes;envelopes/v2-stabilizer.waiandenvelopes/v2-exact-of-a-clifford-program.wai: one Clifford program by each method, with the same counts (§11.3);refuse/: an angle that is not dyadic (v2-angle-not-dyadic), one moved past its double's rounding (v2-angle-off-the-double-s-rounding), a gate the method does not take (v2-gate-outside-the-exact-gates), atgate by thestabilizermethod, one qubit past the ceiling (v2-past-the-exact-method-s-qubits), a run of the state vector past §4.8's work, an angle argument off its turns, and an angle argument in a first-version envelope, each beside the same bundle without its defect.
The job receipts of ../quantum-conformance/receipts/
hold the second version's jobs the command line sealed from these envelopes
(jobs/cli-wasmq2-*), each with the energy its host read, and jobs crafted from them that
break §11.6's rules each alone; both verifiers of that corpus, which share no code, give every
one the same verdict and state the same rules broken, and are held to each other at each
bound of rules 4, 8, 11, 12, 30 and 36 as this section reads them.
11.9 What the second version leaves out
- noise, and observables other than the counts;
- floating-point arguments and results of a callout: an angle crosses as its numerator, an integer as itself, and a module that needs a real number scales one of them itself;
- the gates of §11.2's refusal, angles off the
2^-32-turn grid, and more than 26 qubits by theexactmethod; - a reading of one callout's energy on any host the reference runs on (§11.5);
- running beside a quantum processor (§7 holds as written). Every surface runs a bundle of
either version:
wai-quantum run, both MCP servers (run_wasmq), the job service (POST /v1/wasmq), the Python package (wasmq), the npm package (QuantumEngine.wasmq, on the interpreter alone), the Studio and the desktop app; and every surface of §6.5 verifies it. Only the command line, its MCP server and the job service read a run's energy; the others state it unmetered, with the reason.