wai-quantum: no anonymous request ends the job service or holds it without limit #676

Closed
dcharlot wants to merge 27 commits from wai/quantum-parse-depth into main
73 changed files with 10603 additions and 1652 deletions

View file

@ -79,7 +79,7 @@ jobs:
set -euo pipefail
cargo test --locked --release --features full --lib qasm 2>&1 | tee "$RUNNER_TEMP/qasm.log"
grep -qE '^test result: ok\. [1-9][0-9]+ passed; 0 failed' "$RUNNER_TEMP/qasm.log" || { echo "::error::the reader's tests did not pass"; exit 1; }
for m in qasm::diag qasm::lex qasm::parse qasm::value qasm::lang_tests qasm::whole_tests qasm::tests quantum_toolchain::tests::qasm_round_trip; do
for m in qasm::diag qasm::lex qasm::parse qasm::value qasm::lang_tests qasm::whole_tests qasm::depth_tests qasm::tests quantum_toolchain::tests::qasm_round_trip; do
grep -q "^test $m.* \.\.\. ok$" "$RUNNER_TEMP/qasm.log" || { echo "::error::no test of $m ran"; exit 1; }
done
# A gate held whole, each test by name.

View file

@ -74,7 +74,7 @@ jobs:
run: |
set -euo pipefail
cargo test --locked --release --features full --lib -- qasm::pulse:: quantum_pulse:: engine::pulse:: 2>&1 | tee "$RUNNER_TEMP/pulse.log"
grep -qE '^test result: ok\. 26 passed; 0 failed' "$RUNNER_TEMP/pulse.log" || { echo "::error::the pulse tests did not all pass (26 expected)"; exit 1; }
grep -qE '^test result: ok\. 28 passed; 0 failed' "$RUNNER_TEMP/pulse.log" || { echo "::error::the pulse tests did not all pass (28 expected)"; exit 1; }
for t in qasm::pulse::tests::a_program_reads_to_its_schedule_on_frames \
qasm::pulse::tests::barriers_align_frames_and_a_gate_starts_its_frames_together \
qasm::pulse::tests::phase_and_frequency_follow_the_frame_and_the_carrier_runs_on \
@ -84,6 +84,7 @@ jobs:
qasm::pulse::tests::ports_map_by_name_and_by_the_options \
qasm::pulse::tests::what_the_pulse_reader_does_not_take_is_refused_with_its_code_and_place \
qasm::pulse::tests::the_schedule_writes_as_json_with_every_instruction \
qasm::pulse::tests::a_calibration_s_expression_nested_past_the_limit_is_refused \
quantum_pulse::tests::a_resonant_drive_turns_the_qubit_as_rabi_says \
quantum_pulse::tests::a_quarter_turn_of_phase_turns_the_axis_to_minus_y \
quantum_pulse::tests::a_detuned_drive_turns_less_and_faster \
@ -100,7 +101,8 @@ jobs:
engine::pulse::tests::a_pulse_job_is_checked_by_running_it_again \
engine::pulse::tests::a_schedule_is_drawn_with_its_digests \
engine::pulse::tests::the_request_schema_holds_the_options_the_engine_reads \
engine::pulse::tests::a_pulse_job_is_the_same_bits_on_every_platform; do
engine::pulse::tests::a_pulse_job_is_the_same_bits_on_every_platform \
engine::pulse::tests::no_pulse_result_passes_the_bound_its_plan_gave; do
grep -q "^test $t \.\.\. ok$" "$RUNNER_TEMP/pulse.log" || { echo "::error::$t did not run and pass"; exit 1; }
done
# Without warnings: a feature gate that leaves code unreachable or a
@ -206,4 +208,4 @@ jobs:
run: |
set -euo pipefail
cargo test --locked --target wasm32-wasip2 --features full --lib -- qasm::pulse:: quantum_pulse:: engine::pulse:: 2>&1 | tee "$RUNNER_TEMP/wasip2.log"
grep -qE '^test result: ok\. 26 passed; 0 failed' "$RUNNER_TEMP/wasip2.log" || { echo "::error::the pulse tests did not all pass under wasip2 (26 expected)"; exit 1; }
grep -qE '^test result: ok\. 28 passed; 0 failed' "$RUNNER_TEMP/wasip2.log" || { echo "::error::the pulse tests did not all pass under wasip2 (28 expected)"; exit 1; }

View file

@ -101,6 +101,9 @@ jobs:
quantum_qec_text::tests::any_layout_reads_to_its_rewritten_form \
quantum_qec_text::tests::what_does_not_read_is_refused_with_its_line \
quantum_qec_text::tests::a_model_unrolls_with_its_offsets_tags_and_parts \
quantum_qec_text::tests::blocks_nested_past_the_limit_are_refused_on_their_line \
quantum_qec_text::tests::an_empty_block_repeated_without_end_is_refused_not_run \
quantum_qec_text::tests::an_index_past_what_a_text_holds_is_refused_on_its_line \
quantum_window::tests::narrow_windows_correct_every_edge_set_below_half_the_distance \
quantum_window::tests::a_window_over_everything_is_the_batch_decoder \
quantum_window::tests::narrow_windows_track_the_batch_decoder \
@ -113,6 +116,7 @@ jobs:
quantum_frame::tests::pauli_targets_agree_between_sampler_and_model \
quantum_frame::tests::tags_reach_the_model_and_its_text \
quantum_frame::tests::the_text_round_trips \
quantum_frame::tests::blocks_that_write_nothing_end_and_unrun_ones_are_checked_once \
quantum_frame::tests::a_pauli_channel_s_rates_have_its_law \
quantum_frame::tests::a_pauli_channel_samples_its_law \
quantum_frame::tests::the_cycling_gates_cycle_the_axes \
@ -132,6 +136,9 @@ jobs:
engine::mitigate::tests::a_job_carries_its_mitigation_and_its_refusals_name_their_code \
engine::mitigate::tests::refusals_say_why \
engine::qec::tests::every_family_reads_and_certifies_its_distance \
engine::qec::tests::a_time_holds_the_shots_sampled_and_decoded \
engine::qec::tests::a_time_is_measured_apart_from_setup_and_holds_every_threshold \
engine::qec::tests::a_circuit_s_pilot_runs_for_its_floor_within_its_shots \
engine::qec::tests::each_decoder_corrects_every_single_error \
engine::qec::tests::circuit_level_decoding_reads_every_model_form \
engine::qec::tests::a_threshold_is_measured_and_distance_helps_below_it \

View file

@ -85,18 +85,26 @@ jobs:
# sweeps and their Merkle paths, a result's provenance, the package
# counter's reader and probe (on fake trees; a container hides the real
# one), and a real-valued input bound by the reader.
- name: The engine's sets, provenance and package energy
# The bound on a result the service admits a job by (a circuit's, a
# set's and a pulse run's): no result passes it, for jobs of every kind
# drawn at random, and it is close where the method knows its outcomes.
- name: The engine's sets, provenance, result bounds and package energy
shell: bash
working-directory: wai/wai-quantum
run: |
set -euo pipefail
cargo test --locked --release --features full --lib -- engine::sets:: engine::energy:: merkle:: engine::run::tests::a_result_s_provenance qasm::tests::a_bound_parameter 2>&1 | tee "$RUNNER_TEMP/engine.log"
cargo test --locked --release --features full --lib -- engine::sets:: engine::energy:: merkle:: engine::run::tests::a_result_s_provenance engine::run::tests::no_result_passes_the_bound engine::pulse::tests::no_pulse_result_passes_the_bound engine::bound_tests:: qasm::tests::a_bound_parameter 2>&1 | tee "$RUNNER_TEMP/engine.log"
grep -qE '^test result: ok\. [1-9][0-9]* passed; 0 failed' "$RUNNER_TEMP/engine.log" || { echo "::error::the engine's tests did not pass"; exit 1; }
for t in engine::sets::tests::one_receipt_binds_every_item_and_any_change_breaks_it \
engine::sets::tests::a_sweep_is_its_grid_each_point_the_program_written_out \
engine::sets::tests::a_failed_item_is_bound_by_its_error \
merkle::tests::every_leaf_s_path_leads_to_the_root_and_nothing_else_does \
engine::run::tests::a_result_s_provenance_names_the_passes_its_certificate_ran \
engine::run::tests::no_result_passes_the_bound_its_plan_gave \
engine::pulse::tests::no_pulse_result_passes_the_bound_its_plan_gave \
engine::bound_tests::no_random_job_s_result_passes_its_bound \
engine::bound_tests::a_failing_intent_trajectories_and_notes_stay_within_their_bounds \
engine::bound_tests::a_ghz_state_and_a_long_loop_are_bounded_by_what_they_can_list \
engine::energy::tests::the_probe_finds_a_live_counter_s_cadence_and_refuses_a_stalled_one \
engine::energy::tests::only_top_level_package_zones_are_read_and_a_frozen_one_is_not_live \
engine::energy::tests::a_package_figure_names_its_scope_and_declares_its_edges \

View file

@ -110,6 +110,8 @@ jobs:
for t in engine::answer::tests::streaming_at_every_split_ends_as_the_whole_text_reads \
engine::answer::tests::every_problem_has_its_code_and_says_what_becomes_of_the_statement \
engine::answer::tests::an_answer_s_calls_run_on_the_engine_and_its_seal_verifies_when_run_again \
engine::answer::tests::a_chain_of_statements_past_the_limit_is_refused_not_overflowed \
engine::answer::tests::the_chain_limit_refuses_the_same_statements_in_any_order \
engine::telemetry::tests::the_failures_become_a_corpus_in_the_conformance_format; do
grep -q "^test $t \.\.\. ok$" "$RUNNER_TEMP/answer.log" || { echo "::error::$t did not run"; exit 1; }
done

View file

@ -271,9 +271,10 @@ jobs:
# run whose final density matrix is the native build's bits (its pinned
# digest), a program converted between formats to the QIR bytes the
# conformance set pins, the variational library's gradient to the engine's
# pinned bytes beside a program built and checked, and every worked example of
# an energy-landscape job to the native build's pinned digest. The count is the
# four tests above and these eighteen.
# pinned bytes beside a program built and checked, every worked example of
# an energy-landscape job to the native build's pinned digest, and a program
# nested thousands deep refused at every door (no trap). The count is the
# four tests above and these nineteen.
- name: The job engine in a page (wasm32 tests under Node)
shell: bash
working-directory: wai/wai-web
@ -281,7 +282,7 @@ jobs:
set -euo pipefail
CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner \
cargo test --locked --target wasm32-unknown-unknown --features quantum_engine 2>&1 | tee "$RUNNER_TEMP/wai-web-engine-wasm.log"
grep -q "test result: ok. 22 passed; 0 failed" "$RUNNER_TEMP/wai-web-engine-wasm.log"
grep -q "test result: ok. 23 passed; 0 failed" "$RUNNER_TEMP/wai-web-engine-wasm.log"
# Its native half: the service's form and receipt, any angle on the state
# vector, what does not run saying why, plan, lint, draw and methods as JSON,
# critique, equivalence and variants as JSON, the classical shortcuts, the

View file

@ -0,0 +1,189 @@
### WAI Quantum: the reader holds its nesting and its memory, and no request can end the job service or hold it without limit
- **The reader's nesting** (`wai_quantum::qasm`). A program nested past what the reader takes is refused with
the new code `Q006` rather than read by a recursion that ran a thread's stack out: 64 levels in its text
(`qasm::parse::MAX_NESTING`: statements in blocks and bodies, parentheses, operators, brackets, calls, casts
and types together, and every expression's tree as deep as its statement leaves room for), and 256 in all
through the gates and subroutines it calls (the elaborator's statements, expressions, conditions and calls).
An array has at most 7 dimensions (`MAX_ARRAY_DIMS`): they are a comma apart in the text, and each is a level of
its value. What people write is not refused: a run of operators of one precedence (`a + b - c`, a condition of
a thousand `&&`s) and an `else if` chain are held flat and read, run and dropped in loops, at any length, on a
stack of 1 MiB, left to right, so a floating-point sum is the one it was; a switch's case list on measured bits
is flat too, where it crashed the reader at about ten thousand values. What newly refuses a program main read:
more than about 60 levels of real nesting (parentheses, blocks, a `**` chain, an index chain), a `++` chain of
that length, and a recursive subroutine whose body nests more than about seven levels, which reaches 256 before
its 32 calls (Q006). QIR and jeff conditions now stop at the 30 levels of parentheses the reader takes (Q705),
from 32, with the converter's own refusal. `docs/qasm-feature-matrix.md` and `docs/qasm-diagnostics.md` state
the limits.
- **Every other reader of a client's text**: the generator's writer (`qasm::instantiate`: its integer
expressions, loops and conditions), the pulse reader's calibrations (through the parser), the error-correction
circuit and model texts (`REPEAT` blocks at most `quantum_qec_text::MAX_BLOCK_DEPTH`, 32, deep, refused on their
line; a block's every pass counts toward the bound on what a text unrolls to, so one repeated without end that
writes nothing ends, refused, and `REPEAT n { H 0 }` is refused from about 25 000 000 passes, not 50 000 000
operations; blocks repeated no times are checked once on one copy), the editor's index (a statement nested past
the reader's limit is passed over without recursion), and the answer layer's statements (at most
`engine::answer::eval::CHAIN_DEPTH`, 32, each naming the next, the same statements refused in any order). The
exact expectation values of a program that measures in the middle walk its branches with a list rather than a
recursion, in the same order, so a program that branches thousands of times is refused at `MAX_LEAVES`, not run
out of stack.
- **An index in a few bytes of text** (`quantum_qec_text::MAX_QUBITS`, `MAX_DETECTORS`, each 2²⁴). What is made for
a detector or a qubit is as long as its index: `shift_detectors 100000000000000` and a `detector(1) D0` asked for
2.4 PB in 79 bytes, and an allocation that fails ends the process. Each index is refused where it is read, on
its line, offsets counted, however it is named. A circuit's width with its correlated errors' carrier is held to
the same cap, refused at the first such error.
- **The reader's memory** (`Q130`, new). A program whose reading would hold more than the reader takes is
refused before it is made: one classical value (an array or a bit register, each element counted at its width,
so `array[bit[1048576], 1000]` is a gigabyte) of more than 64 MiB, or more than 512 MiB in all of its values and
of its gates, steps, measurements and timing as they grow (a `reset` of a wide register in a loop grew them
without end). A declaration of a hundred bytes, `array[int[8], 4294967295] a;`, asked for 137 GB. Making or
copying a value counts toward the reader's steps, at one for every 128 bytes; an element read from an array
copies the element alone; a loop over a range makes each value as it reaches it, not all of them first. A
statement on a register of tens of thousands of qubits is checked in its own length, where each qubit was
compared with every one before it. Each operand of a long run or a set counts as a statement. A bit string wider
than an integer, given to one, is refused (`Q112`) where the reader panicked.
- **A budget of reading** (`qasm::within_steps`, `qasm::steps_left`): every program read on a thread within it
is read once (a program read again with no inputs is answered from its first reading) and every reading
draws on one budget of steps: each statement elaborated, each gate written, what is made or copied by its
size, each pass of a loop a generator is written out with and what it writes. Past it, a reading is refused
(`Q312`), and `steps_left` is `Some(0)`.
- **The job service: admission by cost.** A submission (`POST /v1/jobs`, `POST /v1/run`, a dry run, and the
MCP run tools and tasks) sent again under its key is answered first, reading nothing: a job given in another
format (`"source"`) is matched by the digest of the request as received, kept with the job. Then come the refusals
that read nothing and are not counted (the client's jobs at once, the memory they hold, a full queue, a spent
window), then a turn, then the count, so every request refused after that counts and one told to try again has
read and spent nothing; there is no refund. Then what reads nothing (at most 16 intents, each of a set's items
too; at most 100 items in a set; exact whole numbers), and only then are its programs read, each once, within
one budget of 22 000 000 steps for the whole request (`read_steps` in `GET /v1`'s limits), the reader's bounds on
a job's statements and gates together, so a job's own program that reads within them reads here. A request past
it, by its own program, its references or its items, is refused, `422` and `J031`. `Retry-After` on a `429` is
now the pace at which a refusal that counts would not spend the window (20 s by default), not 5.
- **The job service: every check.** A check (`/v1/critique` and the others, and the estimate, error-correction,
pulse and variational calls) is counted once it has its turn and before its bounds read anything, so their
`422`s count. A check's answer reads within the same budget as its bounds, each program once, so a repair of forty
fixes to a slow program ends at the budget where it ran for minutes. A budget spent is a refusal whatever a bound
made of it: an equivalence bound read a refusal as no work and let the check run. `/v1/verify` and MCP
`verify_quantum_receipt` on a set are counted and read as a check is, as its sweep is written out again, and a
seal that does not hold is not. A decode is held to 1 024 fired detectors, as matching grows faster than their
square, and a memory experiment to 21 rounds. An error-correction decode or threshold measures its shots as they
run (`engine::qec::within_time`): each point is set up and sampled, and a pilot of its own shots decoded (one in a
hundred, and on for at least 200 ms where its shots allow: 20 ms of a light decode read three times slow on a
loaded host and refused a call that fit), before the rest of any, and the call is refused at once when what is measured says it would take more than
`WAI_QSVC_QEC_SECS` (25), stopped only if it runs a fifth past that; setup is not multiplied by the shots, so a call
that fits is run, and code-capacity thresholds are measured on a pilot of trials of each point too. One at the
published bounds ran for many minutes and held its turn to the reading's cap.
- **The job service: in a child.** Admission, a dry run, every check's bounds and answer, the error-correction,
pulse and variational calls and the estimates, a set's receipt, and the verdicts the MCP tools state beside an
answer are read by a child, the service's own binary run as `wai-quantum-service read`, sandboxed as a worker
is. Four requests are read at once and four checks beside them. A client has its share of each,
`WAI_QSVC_CLIENT_READS` (2) and `WAI_QSVC_CLIENT_CHECKS` (1), smaller than the pool, so one client holds no pool
for longer than a quick call; past its share one more turn is lent to it while the pool keeps two free, for at most
five seconds once others wait (so its quick calls pass its own slow one; a call on a lent turn moves onto the client's own turn as one comes free, and is refused, `503`, only if others wait for a turn after five seconds),
and the rest wait behind its own turns in order (an agent's burst of eight is answered one after another), eight
at most, 32 in all, each for ten seconds; past any of these a request is refused, `503` and `J032`, saying whether
its own requests or the pool held it up. People behind one address share one client's turns. A child is polled every 50 ms from a thread of its own (on macOS its
physical footprint, compressed pages included; on Linux resident and swapped pages) and killed past
`WAI_QSVC_READ_GB` (4) or `WAI_QSVC_READ_SECS` (45), its request refused, `422` and `J031`; the kill is a signal the
service sends itself, to the child's process group, again until it has ended, so it works when no new process can
be started. The poll is not a kernel limit; on Linux a child also holds its address space
to twice the cap and a gigabyte, and the kernel ends readers and workers before the service. What a reading
gives back is held to `WAI_QSVC_READ_ANSWER_MIB` (128) as it takes once read (about 80 bytes a value), refused in
the child before it is written, and read off the request threads; a dry run gets back its plans and points alone.
This newly refuses a check whose answer is that large: a critique of a program whose gates each draw a remark
(`qubit q; for int i in [1:N] { x q; }`) is refused, `422` and `J031`, at 200 000 gates, which main answered (150 000
are answered). One that panics,
overflows or aborts is answered `500`; the service runs on. A client that hangs up ends its child.
- **The job service: MCP.** A tool that says nothing about a program takes no turn. A verdict reads the program only
for a call that was counted; a poll states what its job holds, and reads nothing, however often it is asked (so a
poll of a queued or running job, or of an ended one sent without `verdict` or `intent`, now gives a verdict with
empty `diagnostics` and `critique`, and a view on a poll no drawing, where they read the program before); a
call refused before it was counted (a bad key, a tool the service does not have, past the window) or told to try
again states a verdict that gives the reason. One that finds no turn for its verdict does the same, so every
answer about a program has one, and follows its schema. A refused task is the same, where it read the program
on a runtime worker with no budget. A run's MCP arguments that are not an object are refused, where they dropped
the connection.
- **The job service: one answer within a proxy's wait.** Every request is answered within `WAI_QSVC_ANSWER_SECS` (55)
of its arrival: its waits for a turn and its readings end by then, a run waits for its job only for what is left
(`/v1/run` and the MCP run tools answer with the job, still running, to poll, rather than past a proxy's 60 s), and
an MCP answer's verdict is read only if time is left, saying so where it is not.
- **The job service: what ended jobs keep.** A job keeps its request as the canonical text the store keeps, where it
kept the parsed request (about 80 bytes a value, 37 times a body of numbers) for a day, across restarts and
counted against nothing; a set's items are let go once it ends. What ended jobs keep, their requests' text and
their results as read, is held to `WAI_QSVC_CLIENT_KEPT_MIB` (128) a client and `WAI_QSVC_KEPT_MIB` (2048) in all,
the oldest dropped first, before their `WAI_QSVC_RETAIN_SECS`. A request's body is parsed only once its client's
window has room (a resend under its key, answered before any limit, first), and a large MCP message from a client
past its window is answered under its own id with a tool result that says why, read only for its id, method, tool
and key (a resend under a key the client holds, and a receipt check, are read whole). A body is read before its
window only for a resend under a key its client holds a job under. A receipt check has a window of its own,
`WAI_QSVC_CLIENT_VERIFIES` (120). An ended job not yet read is kept for `WAI_QSVC_KEPT_MIN_SECS` (600) whatever it
weighs, and past the service's cap the client keeping the most loses its results first. A job's report is one
RFC 8785 text on every route and after a restart, its request and result written in as kept, never parsed again, and the
memory a large request or answer was parsed into is given back once it is answered. On macOS the service starts
itself with the allocator's large-block cache off (`MallocLargeCache=0`), which kept every large block a parsed
request freed, dirty and charged to the service: on arm64, 30 requests of a megabyte kept 1 GiB; with it off, 2 MiB.
- **The job service: connections, updates and restarts.** A request's header section is held to
`WAI_QSVC_HEADER_SECS` (150, which also closes an idle connection, so it sits above a proxy's two-minute idle
limit) and its body to `WAI_QSVC_BODY_SECS` (30; past it, `408`). It raises its open-file limit to its hard
limit as it starts (on macOS to `kern.maxfilesperproc` and 10240 at most) and holds at most `WAI_QSVC_CONNECTIONS`
(4096) connections under it: past them the one idle longest is closed for the new one, so a health check is always
answered, or the new one is answered `503` at once; a client may have `WAI_QSVC_CLIENT_REQUESTS` (32) requests open,
refused `503` past them before a body is read. `tasks/result` holds its answer within what is left of the answer's
time. Four connections past the cap are held for health checks alone, and long polls and event streams are held to
`WAI_QSVC_CLIENT_STREAMS` (4) a client and `WAI_QSVC_STREAMS` (512, a quarter of the connections at most); past them
a long poll of a job still queued or running is not held, and says so: `429` (its client's) or `503` (everyone's),
`J032`, with `Retry-After: 2` and the job's status (the command line's `follow` waits and polls again). A reading
that moves from a lent turn onto its client's own gives the lent one back; one still on a lent turn past 5 s goes
back once another client's request waits for a turn or one of its own client's has waited 7 s, so two long checks
of one client never hold its quick calls past their wait. A client's eleventh request at once is refused, of
readings (the reading pool lends no turn at the default share of two) and of checks alike, with eight waiting. Results read and not yet read are counted apart: only read ones are dropped before their
time, the largest holder's first; a client's next job waits while its unread results, with each job running at
its result's bound and the new one at `WAI_QSVC_RESULT_MIB` (64), would pass `WAI_QSVC_CLIENT_UNREAD_MIB` (256),
and every next job while all unread, with every job queued or running at its result's bound and the new one, would
pass `WAI_QSVC_UNREAD_MIB` (8192), so what ended jobs keep stays under the two caps. A job's or a set's result is
bounded before it runs, part by part (`catalog::plan_bounded`, `catalog::plan_bound` and `catalog::Bound`,
`sets::plan_bounded`, new): each item's counts at most the samples it lists (its shots, or a `trajectories` run's
trajectories) among the outcomes its measured bits can give (a noiseless `stabilizer` run's, two to the gates
that branch: a GHZ state's two) and 65,536; a verdict's critique by the remarks its program's gates can draw
(`critique::smells_bound`, new: the walk the critique makes), each intent's check with the 64 outcomes it lists
at a time, and the reader's notes and repair again; every other part by what it may hold or at its length as
RFC 8785 writes it, each number in its own form (`receipt::canonical_len`, new); a pulse run's from its schedule,
`pulse::plan_bounded`, through `Method::plan_bounded`. A sampled intent's fit names at most 64 of the outcomes it
rules out and says how many there were (`impossible_outcomes`, new), and its reason says them once. A request
whose bound passes `WAI_QSVC_RESULT_MIB` is refused at once (`422`, `J031`), its bound, its largest part and what
makes that part smaller in the reason; a dry run states the bound (`result_bytes`), is held to what its
submission's reading would take back, and over MCP plans the job as the run tools run it, its verdict asked; and
`GET /v1`, the dry run's bounds and the OpenAPI document the cap. At 64 MiB, 100 points of 65,536 outcomes (about
150 MB) are refused, and 100 points of 16 bits at 30,000 shots are taken and run to the end: a set's worker keeps
each item's result as text as it ends, where it kept every item's as a tree and passed the memory it was admitted
with. 100 copies of a 24-qubit GHZ state at 30,000 shots, and a loop of 124,700 gates with its verdict, are taken.
The check as a result ends stays as a backstop, for a job kept across a restart that lowered the cap.
MCP answers echo each JSON-RPC id as the text it was sent as (`1.0`, `1e3`, an integer past 64 bits). Readers and workers start from the service's own
binary as it was started (Linux: its running image; elsewhere a copy kept beside the signing key), so the binary
can be replaced before the restart. A restart reads no program: a job keeps the plan it was admitted with and a
set its items, where a queue of slow programs held the port unbound for the sum of their reads.
- **Tests**: every nesting construct at thousands of levels on a 2 MiB thread, refused; runs, `else if` chains and
case lists of a hundred thousand on 1 MiB, read to what the tree gives; a seeded property test over mixed nesting;
the service binary started in a test and sent each of these through each door, answering `/v1/health` after each;
refused MCP tasks that hold no worker; heavy requests from eight clients beside a health probe; a client's share
of the turns; a reading stopped at its deadline; a hang-up that frees its turn; stuck connections let go; and a
restart that reads nothing. Two tests that could only end by the job's own limit now fail by name. A property test
draws jobs of every kind (failing sampled intents, trajectories, verdicts over notes and smells, random Clifford
circuits, noise, observables, amplitudes, mitigations, repairs, compiles, measurements in the middle, device
targets, pulse runs, intents carrying long numbers) and holds each result to its bound; the service's tests run
the README's largest sweep to the end, a GHZ set, the long loop, an MCP dry run against its run, a dry run against
its submission, a set's long numbers, two long checks beside quick calls, raw ids, a long poll past its holds and
the unread cap.
- **Breaking:** `api:rust:wai_quantum::qasm::ast::E::Chain`: a run of left-associative operators of one precedence
is one node, `E::Chain(first, [(op, operand), …])`, so a run of any length holds no recursion; a match over every
variant of `E` outside the crate adds this one.
- **Breaking:** `api:rust:wai_quantum::qasm::ast::S::If`: an `if` and its `else if`s are one statement,
`S::If { arms: [(cond, then), …], other }`, where it was `S::If { cond, then, other }` with the `else if`s nested in
`other`; code that builds or destructures it reads each arm in turn.
- **Breaking:** `api:rust:wai_quantum::engine::catalog::Request::smells_bytes`: a request read for a verdict carries
the most its critique takes, so a result's bound counts the smells its program can draw rather than every gate;
code outside the crate that builds a `Request` by its fields sets it to 0 (or to `critique::smells_bound` of its
program, where it asks for a verdict).
- **Breaking:** `api:rust:wai_quantum::qasm::MAX_NESTING`: main's bound of 128 levels (a `u32`, from 15ae7d457, in no
release) is replaced by this reader's, `qasm::parse::MAX_NESTING` (64 levels in the text, a `usize`, and 256 through
calls); read that constant instead.
- **Breaking:** `api:rust:wai_quantum::qasm::MAX_ELSE_IF`: an `else if` ladder is read flat, at any length, so it has no
bound to name (main's 1024, from 15ae7d457, in no release); code that held a ladder to it holds nothing now.

View file

@ -1,25 +1,20 @@
### The OpenQASM reader bounds its nesting, counted as the program's text nests
- Every reading of a program (`qasm::read`, `read_whole`, `tangents`, and every surface over
them: the job service, the command line, both MCP servers, the views) refuses a program
nested deeper than 128 with a code of its own, `Q130`, before a reader's stack can run out.
A statement at the top level is at depth 1. Each statement in a block or a body, each
expression a statement holds, each operand of an operator, what a bracket holds (of an
expression or a type) and each index's indexed value are one deeper than what holds them.
Operators group as they bind (`a + b + c` is `(a + b) + c`), so a chain of 127 operators is at
the bound. An `else if` is at its `if`'s depth, and one `if` holds at most 1024 of them. A
gate's or a subroutine's body is read in place of its call, its statements one deeper than
the call, so nesting through calls is bounded as nesting in one body is (`Q209` still bounds
calls at 32 deep).
- The reader had no bound, and recursed as deep as a program nested. A flat sum of 2000 terms,
a 4 KB program, overflowed the job service's 2 MiB worker stack and ended the service
(`POST /v1/jobs`, `/v1/estimate`, `/v1/gradient`, and `/v1/verify` given an estimate's
request). Binary operators are now read by precedence climbing, in one frame for a chain of
any length and any mix of operators. Every program within the bounds reads on a 1 MiB stack
in a release build, natively and on wasm32 (`wai-quantum`, `qasm::parse`, `qasm::elab`;
`tests/reader_stack.rs` at each bound: each shape nested 128 deep, chains of 25 bodies of 4
loops and of 31 of 2, ladders of 1024 arms; the service's `tests/verify.rs`).
- **Breaking:** a program nested deeper than 128, or an `if` with more than 1024 `else if`
arms, is refused with `Q130`, where it read when the reader's stack held it. Write a long
sum as several statements (`x += …`) or a `for` loop, a deep expression as named `const`
values, a long ladder as a `switch`, and nest fewer blocks or calls.
request).
- As merged with the reader's nesting and memory bounds (`2026-11-06-quantum-parse-depth.md`),
which hold in place of the bound this entry first took (`Q130` at 128 levels, a chain of at
most 127 operators, at most 1024 `else if` arms; no release carried it): every reading of a
program refuses one nested past the reader's bound with `Q006`, 64 levels in its text and
256 through the bodies its calls read, and `Q130` is the reader's memory refusal. A run of
one operator and an `else if` ladder are flat, read at any length, so the sum of 2000 terms
reads.
- `tests/reader_stack.rs` reads every program at those bounds on a 1 MiB and a 2 MiB thread in
a release build (16 MiB in debug), and on wasm32: each shape nested to just inside the text's
bound, chains of 25 bodies of 4 loops and of 31 of 2, the 31-of-2 chain ending in a deep
angle, ladders of 1024 arms decided on reading and at run time, and runs of 5000 of one
operator; the same shapes nested 5000 deep, and a chain of 31 bodies of 8 loops, are refused
(`Q006`) on the same stacks. The service's `tests/verify.rs` sends the sum, 1000 brackets and
1000 blocks to seven routes: the sum is read, the others refused, and the service stays up.

View file

@ -844,7 +844,7 @@ package of several components: the
media classes `graphics` (7) and `text` (8) beside packaging's others, the rungs each medium
takes, how each component is cut at the lead's piece starts, each piece's object and `target`,
and the decoded-output digest each entry states. The reference package is written by
`wai-pack` under `compose/policy.toml`: the lead `v`, a 320 × 180 test pattern (the fixture
`wai-pack` under `compose/policy.toml`: the lead `v`, a test pattern of 320 × 180 (the fixture
`compose-01`) with an integer base and a supplied AV1 floor for every piece; `g`,
`vector-conformance`'s title card and credit roll; `t`, three captions; `s`, a score with a
FLAC floor of each piece's mixdown, which this page does not decode (no route: it holds no

View file

@ -672,7 +672,7 @@ Parameters:
### `dry_run_quantum_job`: Plan a quantum job or set without running it
What the service would do with a job or a set, running nothing: the method and why, the memory, the estimated time and energy (each with its basis: this service's own timings and readings where it has them, otherwise the planner's nominal rate, and no energy figure without readings), the bounds that apply, where you stand against your limits, and whether the service would take it now, or the refusal it would give, with its code. Takes what run_quantum_circuit or run_quantum_set takes. It counts as one submission.
What the service would do with a job or a set, running nothing: the method and why, the memory, the estimated time and energy (each with its basis: this service's own timings and readings where it has them, otherwise the planner's nominal rate, and no energy figure without readings), the bounds that apply, where you stand against your limits, and whether the service would take it now, or the refusal it would give, with its code. Takes what run_quantum_circuit or run_quantum_set takes, and plans it as they run it, its verdict asked: what it says they take, they take. It counts as one submission.
Parameters:
- `qasm` (string): The program, OpenQASM 3 (or 2), read as the whole language: registers, aliases and slices; the standard gates, `U` and `gphase`; user `gate` definitions and `def` subroutines, inlined; every modifier (`ctrl @`, `negctrl @`, `inv @`, `pow(k) @`) on any gate, lowered exactly; classical types (int, uint, float, angle, complex, bool, bit, arrays), their arithmetic and built-in functions, computed on reading; `for`, `while`, `switch`, `break`, `continue`; measurement and `reset` anywhere; classical control on measured bits (`if (c[0]) x q[1];`, `if (c >= 2) { … } else { … }`, `&&`, `||`); a `while` on measured bits takes `@wai.max_iterations N` before it. `output` declarations come back in the result; `delay`, `box` and `stretch` are carried and labelled as not applied; `defcal` and `cal` are kept, not run. A program that measures in the middle or acts on what it measured runs on statevector or stabilizer: counts split their shots at each measurement; expectation values are exact, of the state before the final measurements. What the reader refuses comes back with a code, as `[Q103]`; qasm_diagnostics says what each code means.
@ -991,6 +991,9 @@ Each construct below is supported (read as the language defines it), lowered (re
| Comments `//` and `/* */` | supported; an unclosed block comment is refused | Q003 |
| Identifiers, Unicode letters included (`θ`, `π`) | supported | |
| Statements and blocks `{ }`, scopes | supported; qubits, gates, subroutines and externs are declared at the top level | Q105 |
| Nesting: statements in blocks and bodies, parentheses, operators, brackets, calls, casts and types | supported to 64 levels together in the text (a statement, each enclosing statement, and each level of its expressions' trees count one). A run of operators of one precedence (`a + b - c`, `c[0] && c[1] && …`) is one level, and an `else if` chain one level, at any length: both are read and run in loops, left to right. Deeper is refused, never read by a recursion that could run out of stack; an array has at most 7 dimensions | Q006 |
| Nesting in all, through the gates and subroutines a program calls | supported to 256 levels (each statement, expression and call the reader runs counts one), and calls 32 deep; deeper is refused. Both hold: a recursive subroutine whose body nests more than about seven levels reaches 256 before 32 calls | Q006, Q209 |
| What a program holds on reading: its classical values, gates, steps, measurements and timing | supported to 64 MiB for one value (an array or a bit register, each element counted at its width) and 512 MiB in all; more is refused before it is made. Making or copying a value counts toward the reader's 20 000 000 steps at one step for every 128 bytes, and an element read from an array copies the element alone | Q130, Q312 |
| `pragma …`, `#pragma …`, annotations `@keyword …` | warned: kept, not applied; `@wai.max_iterations N` bounds the `while` after it | Q013 |
#### Literals
@ -1075,6 +1078,7 @@ Each construct below is supported (read as the language defines it), lowered (re
| `pow(k) @`, `k` a whole number | lowered exactly: angles scaled or the gate repeated; negative `k` repeats the inverse | Q203 |
| `pow(r) @`, `r` fractional | lowered where an exact principal root exists (`pow(1/2) @ x` is `sx`; rotations by less than 2π; phases; controlled forms); refused otherwise (`u`, `cu`, user gates) | Q204, Q205 |
| A modifier on a subroutine, or nested as deep as 32 | refused | Q211, Q209 |
| A chain of modifiers (`inv @ inv @ …`) | read at any length: a list, not a nesting | |
#### Measurement and reset
@ -1110,7 +1114,6 @@ Each construct below is supported (read as the language defines it), lowered (re
| Bits declared in a subroutine | lowered: bits of its own, fed forward | Q322 |
| A subroutine called as a gate (`f(1) q, r;`) | supported | |
| Recursion | refused past 32 calls deep | Q209 |
| Statements, operators, brackets and indexes nested deeper than 128, counted through the bodies calls read in place; an `if` with more than 1024 `else if` arms | refused, before a reader's stack could run out | Q130 |
#### Timing
@ -1196,6 +1199,7 @@ Every code:
| `Q003` | refused | A string, a block comment, a bracket or a calibration block that is not closed. |
| `Q004` | refused | Something else was expected here: a `;`, a name, an expression or a statement (the message says which). |
| `Q005` | refused | A statement in a place the language does not allow it: `else` with no `if`, `case` outside `switch`, a declaration inside a gate. |
| `Q006` | refused | A program nested deeper than the reader takes: statements, parentheses, operators, brackets and types more than 64 levels deep in its text (a run of one precedence, `a + b + c`, and an `else if` chain each count one, however long), an array of more than 7 dimensions, or more than 256 levels in all through the gates and subroutines it calls; refused, never read by a recursion that could run out of stack. |
| `Q010` | warned | An `OPENQASM` version other than 2.x or 3.x: the program is read as OpenQASM 3. |
| `Q011` | warned | An `include` of a file other than the standard libraries (`stdgates.inc`, `qelib1.inc`): its definitions are not read, so a gate it defines is unknown. |
| `Q012` | lowered | `include "stdgates.inc"` or `"qelib1.inc"`: the standard gates, `U`, `gphase` and OpenQASM 2's names are built in. |
@ -1230,7 +1234,7 @@ Every code:
| `Q127` | refused | An assignment to a constant, an input or a loop variable. |
| `Q128` | refused | A measurement where a value is needed that is not a bit (measurement gives bits, into a bit register). |
| `Q129` | refused | A parameter (`input float`, `input angle`) a gradient cannot follow to the gates: under a modifier, in a classical variable, a condition, an index, a size, a loop's range, a subroutine or `gphase`, or one whose type rounds it (`angle[n]`, `float[32]`); a gradient follows a parameter through arithmetic, the built-in functions and gate definitions into a gate's arguments. |
| `Q130` | refused | A program nested deeper than 128, before a reader's stack could run out: a statement at the top level is at depth 1, and each statement in a block or a body, each expression a statement holds, each operand of an operator, what a bracket holds (of an expression or a type) and each index's indexed value, one deeper than what holds it; operators group as they bind (`a + b + c` is `(a + b) + c`), an `else if` is at the depth of its `if` (one `if` holds at most 1024 of them), and a call's body is read in its place, its statements one deeper than the call. |
| `Q130` | refused | A program whose reading would hold more memory than the reader takes: a classical value (an array, a bit register) of more than 64 MiB, each element counted at its width, or more than 512 MiB in all of its values, gates, steps, measurements and timing; refused before it is made. |
| `Q200` | lowered | A `gate` the program defines, inlined at each call with its parameters bound. |
| `Q201` | lowered | `ctrl @` and `negctrl @` on any gate, lowered exactly to standard gates (a negative control is a control between two `x`). |
| `Q202` | lowered | `inv @` on any gate, user gates included: each gate's exact inverse, in reverse order. |
@ -1256,7 +1260,7 @@ Every code:
| `Q309` | lowered | `while` whose condition is known on reading: unrolled until it fails. |
| `Q310` | refused | A `while` whose condition reads measured bits and that declares no bound: add `@wai.max_iterations N` before it. |
| `Q311` | warned | A `while` on measured bits run at most its declared bound (`@wai.max_iterations N`): a run that still meets its condition after N iterations stops looping there. |
| `Q312` | refused | A program that does not finish unrolling within the reader's bound (20 000 000 statements and loop iterations). |
| `Q312` | refused | A program that does not finish unrolling within the reader's bound (20 000 000 statements and loop iterations), or that runs out a caller's budget of reading (`qasm::within_steps`: statements, gates and a generator's loop passes, shared by every program read under it). |
| `Q313` | refused | A range with a zero step, or that does not read as `[start:end]` or `[start:step:end]`. |
| `Q314` | refused | `break` or `continue` outside a loop, or `return` outside a subroutine. |
| `Q315` | refused | A `return` with a value under a condition on measured bits. |

View file

@ -2368,7 +2368,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -6212,7 +6217,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -8401,7 +8411,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -8809,7 +8824,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -9804,7 +9824,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -10212,7 +10237,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -10700,7 +10730,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -11101,7 +11136,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -11511,7 +11551,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -12044,7 +12089,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -12452,7 +12502,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -12994,7 +13049,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -13395,7 +13455,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -13805,7 +13870,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -14253,7 +14323,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -14661,7 +14736,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -15742,7 +15822,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -16143,7 +16228,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -16553,7 +16643,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -17066,7 +17161,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -17467,7 +17567,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -17877,7 +17982,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -18869,7 +18979,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -19277,7 +19392,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -20515,7 +20635,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -20923,7 +21048,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -22724,7 +22854,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -23125,7 +23260,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -23535,7 +23675,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -24237,7 +24382,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -24617,7 +24767,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -24997,7 +25152,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -25401,7 +25561,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -25777,7 +25942,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -26216,7 +26386,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -26626,7 +26801,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -32834,7 +33014,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -36678,7 +36863,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -52883,7 +53073,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -56727,7 +56922,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"

View file

@ -2384,7 +2384,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -6228,7 +6233,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -8417,7 +8427,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -8516,7 +8531,7 @@
},
"accepted": {
"type": "boolean",
"description": "Whether the service would take it now, by every limit."
"description": "Whether the service would take it now, by every limit, its reading held to what the submission's would take; the MCP tools plan it as their runs run it, its verdict asked."
},
"status": {
"type": "integer",
@ -8636,9 +8651,34 @@
},
"bounds": {
"type": "object",
"description": "The bounds that apply here: memory, time, queue and items of a set.",
"description": "The bounds that apply here: memory, time, queue, items of a set, and result_bytes, the most a job's or a set's result may take as served.",
"additionalProperties": true
},
"result_bytes": {
"description": "The most its result takes as served, bytes, bounded before it runs: past bounds.result_bytes the job is refused (422, J031). A pulse run is bounded from its schedule. For a method that gives no bound before it runs (no built-in is such): {\"bytes\": null, \"why\"}, and it is counted at bounds.result_bytes.",
"oneOf": [
{
"type": "integer",
"minimum": 0
},
{
"type": "object",
"required": [
"bytes",
"why"
],
"properties": {
"bytes": {
"type": "null"
},
"why": {
"type": "string"
}
},
"additionalProperties": false
}
]
},
"client": {
"type": "object",
"description": "Where the client stands: its active jobs, the memory they hold, the submissions left in its window and when the window resets.",
@ -8986,7 +9026,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -9394,7 +9439,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -9771,7 +9821,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -10181,7 +10236,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -12642,7 +12702,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -16486,7 +16551,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -18675,7 +18745,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -18774,7 +18849,7 @@
},
"accepted": {
"type": "boolean",
"description": "Whether the service would take it now, by every limit."
"description": "Whether the service would take it now, by every limit, its reading held to what the submission's would take; the MCP tools plan it as their runs run it, its verdict asked."
},
"status": {
"type": "integer",
@ -18894,9 +18969,34 @@
},
"bounds": {
"type": "object",
"description": "The bounds that apply here: memory, time, queue and items of a set.",
"description": "The bounds that apply here: memory, time, queue, items of a set, and result_bytes, the most a job's or a set's result may take as served.",
"additionalProperties": true
},
"result_bytes": {
"description": "The most its result takes as served, bytes, bounded before it runs: past bounds.result_bytes the job is refused (422, J031). A pulse run is bounded from its schedule. For a method that gives no bound before it runs (no built-in is such): {\"bytes\": null, \"why\"}, and it is counted at bounds.result_bytes.",
"oneOf": [
{
"type": "integer",
"minimum": 0
},
{
"type": "object",
"required": [
"bytes",
"why"
],
"properties": {
"bytes": {
"type": "null"
},
"why": {
"type": "string"
}
},
"additionalProperties": false
}
]
},
"client": {
"type": "object",
"description": "Where the client stands: its active jobs, the memory they hold, the submissions left in its window and when the window resets.",
@ -19244,7 +19344,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -19652,7 +19757,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -20029,7 +20139,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -20439,7 +20554,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -20528,7 +20648,7 @@
{
"name": "dry_run_quantum_job",
"title": "Plan a quantum job or set without running it",
"description": "What the service would do with a job or a set, running nothing: the method and why, the memory, the estimated time and energy (each with its basis: this service's own timings and readings where it has them, otherwise the planner's nominal rate, and no energy figure without readings), the bounds that apply, where you stand against your limits, and whether the service would take it now, or the refusal it would give, with its code. Takes what run_quantum_circuit or run_quantum_set takes. It counts as one submission.",
"description": "What the service would do with a job or a set, running nothing: the method and why, the memory, the estimated time and energy (each with its basis: this service's own timings and readings where it has them, otherwise the planner's nominal rate, and no energy figure without readings), the bounds that apply, where you stand against your limits, and whether the service would take it now, or the refusal it would give, with its code. Takes what run_quantum_circuit or run_quantum_set takes, and plans it as they run it, its verdict asked: what it says they take, they take. It counts as one submission.",
"inputSchema": {
"type": "object",
"properties": {
@ -21101,7 +21221,7 @@
},
"accepted": {
"type": "boolean",
"description": "Whether the service would take it now, by every limit."
"description": "Whether the service would take it now, by every limit, its reading held to what the submission's would take; the MCP tools plan it as their runs run it, its verdict asked."
},
"status": {
"type": "integer",
@ -21221,9 +21341,34 @@
},
"bounds": {
"type": "object",
"description": "The bounds that apply here: memory, time, queue and items of a set.",
"description": "The bounds that apply here: memory, time, queue, items of a set, and result_bytes, the most a job's or a set's result may take as served.",
"additionalProperties": true
},
"result_bytes": {
"description": "The most its result takes as served, bytes, bounded before it runs: past bounds.result_bytes the job is refused (422, J031). A pulse run is bounded from its schedule. For a method that gives no bound before it runs (no built-in is such): {\"bytes\": null, \"why\"}, and it is counted at bounds.result_bytes.",
"oneOf": [
{
"type": "integer",
"minimum": 0
},
{
"type": "object",
"required": [
"bytes",
"why"
],
"properties": {
"bytes": {
"type": "null"
},
"why": {
"type": "string"
}
},
"additionalProperties": false
}
]
},
"client": {
"type": "object",
"description": "Where the client stands: its active jobs, the memory they hold, the submissions left in its window and when the window resets.",
@ -21571,7 +21716,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -21979,7 +22129,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -23920,7 +24075,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -27764,7 +27924,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -29953,7 +30118,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -30052,7 +30222,7 @@
},
"accepted": {
"type": "boolean",
"description": "Whether the service would take it now, by every limit."
"description": "Whether the service would take it now, by every limit, its reading held to what the submission's would take; the MCP tools plan it as their runs run it, its verdict asked."
},
"status": {
"type": "integer",
@ -30172,9 +30342,34 @@
},
"bounds": {
"type": "object",
"description": "The bounds that apply here: memory, time, queue and items of a set.",
"description": "The bounds that apply here: memory, time, queue, items of a set, and result_bytes, the most a job's or a set's result may take as served.",
"additionalProperties": true
},
"result_bytes": {
"description": "The most its result takes as served, bytes, bounded before it runs: past bounds.result_bytes the job is refused (422, J031). A pulse run is bounded from its schedule. For a method that gives no bound before it runs (no built-in is such): {\"bytes\": null, \"why\"}, and it is counted at bounds.result_bytes.",
"oneOf": [
{
"type": "integer",
"minimum": 0
},
{
"type": "object",
"required": [
"bytes",
"why"
],
"properties": {
"bytes": {
"type": "null"
},
"why": {
"type": "string"
}
},
"additionalProperties": false
}
]
},
"client": {
"type": "object",
"description": "Where the client stands: its active jobs, the memory they hold, the submissions left in its window and when the window resets.",
@ -30522,7 +30717,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -30930,7 +31130,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -31307,7 +31512,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -31717,7 +31927,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -32475,7 +32690,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -32876,7 +33096,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -33286,7 +33511,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -33696,7 +33926,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -34140,7 +34375,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -34548,7 +34788,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -34958,7 +35203,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -36086,7 +36336,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -36487,7 +36742,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -36897,7 +37157,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -37307,7 +37572,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -37813,7 +38083,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -38214,7 +38489,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -38624,7 +38904,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -39034,7 +39319,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -40013,7 +40303,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -40421,7 +40716,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -40831,7 +41131,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -42161,7 +42466,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -42569,7 +42879,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -46943,7 +47258,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -50787,7 +51107,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"
@ -52710,7 +53035,7 @@
},
"accepted": {
"type": "boolean",
"description": "Whether the service would take it now, by every limit."
"description": "Whether the service would take it now, by every limit, its reading held to what the submission's would take; the MCP tools plan it as their runs run it, its verdict asked."
},
"status": {
"type": "integer",
@ -52830,9 +53155,34 @@
},
"bounds": {
"type": "object",
"description": "The bounds that apply here: memory, time, queue and items of a set.",
"description": "The bounds that apply here: memory, time, queue, items of a set, and result_bytes, the most a job's or a set's result may take as served.",
"additionalProperties": true
},
"result_bytes": {
"description": "The most its result takes as served, bytes, bounded before it runs: past bounds.result_bytes the job is refused (422, J031). A pulse run is bounded from its schedule. For a method that gives no bound before it runs (no built-in is such): {\"bytes\": null, \"why\"}, and it is counted at bounds.result_bytes.",
"oneOf": [
{
"type": "integer",
"minimum": 0
},
{
"type": "object",
"required": [
"bytes",
"why"
],
"properties": {
"bytes": {
"type": "null"
},
"why": {
"type": "string"
}
},
"additionalProperties": false
}
]
},
"client": {
"type": "object",
"description": "Where the client stands: its active jobs, the memory they hold, the submissions left in its window and when the window resets.",

View file

@ -163,7 +163,11 @@ impl Client {
}
loop {
let (code, v) = self.get(&format!("/v1/jobs/{id}?wait=25"))?;
if code >= 400 {
// A poll past the client's holds (or everyone's) is not held: it
// says so (429 or 503, J032) with the job's status, and is sent
// again after the pause below.
let unheld = matches!(code, 429 | 503) && v["code"] == "J032" && v["status"].is_string();
if code >= 400 && !unheld {
return Err(Self::refused(code, &v));
}
let s = v["status"].as_str().unwrap_or("").to_string();
@ -174,7 +178,8 @@ impl Client {
if start.elapsed() > FOLLOW_FOR {
return Err(format!("the job {id} is still {s}; read it later with `wai-quantum task status {id}`"));
}
// A service that does not hold the poll answers at once.
// A service that does not hold the poll answers at once (and one
// past its holds says to wait 2 s).
std::thread::sleep(Duration::from_secs(2));
}
}

View file

@ -124,3 +124,34 @@ fn the_docs_and_the_stdio_server_s_resources_agree() {
assert_eq!(reply["result"]["contents"][0]["text"].as_str(), Some(language.as_str()));
let _ = std::fs::remove_dir_all(&home);
}
/// A program nested thousands deep, and an error-correction circuit whose
/// blocks are, is refused by each command that reads one, with the
/// reader's code, and the command ends as a refusal does: by its own exit
/// code, never by a signal.
#[test]
fn a_program_nested_past_the_reader_s_limit_is_refused_by_each_command() {
let home = scratch("deep");
let deep = home.join("deep.qasm");
std::fs::write(&deep, format!("OPENQASM 3.0;\nqubit q;\nrz({}0.5{}) q;\n", "(".repeat(5_000), ")".repeat(5_000))).unwrap();
let generator = home.join("generator.qasm");
std::fs::write(&generator, format!("OPENQASM 3.0;\ninput int n;\nconst int a = {}n{};\nqubit q;\nx q;\n", "(".repeat(5_000), ")".repeat(5_000))).unwrap();
let circuit = home.join("deep.stim");
std::fs::write(&circuit, format!("{}H 0\n{}", "REPEAT 1 {\n".repeat(100_000), "}\n".repeat(100_000))).unwrap();
let (deep, generator, circuit) = (deep.to_str().unwrap(), generator.to_str().unwrap(), circuit.to_str().unwrap());
for (args, says) in [
(vec!["lint", deep], "Q006"),
(vec!["run", deep], "Q006"),
(vec!["plan", deep], "Q006"),
(vec!["critique", deep], "Q006"),
(vec!["equivalent", deep, deep], "Q006"),
(vec!["instance", generator, "--input", "n=1"], "Q006"),
(vec!["qec", "decode", "--circuit", circuit], "nested deeper"),
] {
let o = run(&home, &args);
assert!(o.status.code().is_some(), "{args:?} ended by a signal: {}", text(&o));
assert!(text(&o).contains(says), "{args:?}: {}", text(&o));
}
let _ = std::fs::remove_dir_all(&home);
}

View file

@ -124,6 +124,23 @@ def test_a_refusal_raises_the_engines_message_with_its_line():
wq.run(BELL, noise={"depolarising": 0.1}, key=DEMO)
def test_a_program_nested_past_the_readers_limit_raises_and_the_engine_goes_on():
# Nested thousands deep: refused with the reader's code, in this process.
deep = "OPENQASM 3.0;\nqubit q;\nrz(" + "(" * 5000 + "0.5" + ")" * 5000 + ") q;\n"
with pytest.raises(wq.EngineError) as e:
wq.run(deep, key=DEMO)
assert e.value.code == "Q006"
assert wq.lint(deep)["ok"] is False
with pytest.raises(wq.EngineError):
wq.critique(deep)
blocks = "OPENQASM 3.0;\nqubit q;\n" + "{ " * 5000 + "x q;" + " }" * 5000 + "\n"
with pytest.raises(wq.EngineError) as e:
wq.run(blocks, key=DEMO)
assert e.value.code == "Q006"
# The interpreter goes on: the next program runs.
assert wq.run(BELL, shots=10, seed=1, key=DEMO).status == "done"
def test_values_json_cannot_carry_are_refused_not_guessed():
with pytest.raises(ValueError, match="NaN"):
wq.run(BELL, options={"x": float("nan")}, key=DEMO)

View file

@ -876,6 +876,8 @@ dependencies = [
"blake3",
"futures-util",
"getrandom 0.4.3",
"hyper",
"hyper-util",
"libc",
"serde",
"serde_json",

View file

@ -9,11 +9,15 @@ publish = false
[dependencies]
axum = "0.8"
# The connection loop (http::serve): axum's own takes no header timeout.
hyper = { version = "1", features = ["http1", "server"] }
hyper-util = { version = "0.1", features = ["tokio", "service"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "process", "time", "sync", "net", "io-util"] }
serde = { version = "1", features = ["derive"] }
# float_roundtrip: a number a client sends back reads as the double it was
# (the default reader rounds twice), so its receipt still verifies.
serde_json = { version = "1", features = ["float_roundtrip"] }
# raw_value: a JSON-RPC id is echoed as the text it was sent as (src/http.rs).
serde_json = { version = "1", features = ["float_roundtrip", "raw_value"] }
getrandom = "0.4"
# The job store's content addresses (src/store.rs), as the engine's receipts use.
blake3 = "=1.8.7"
@ -21,8 +25,9 @@ blake3 = "=1.8.7"
futures-util = { version = "=0.3.34", default-features = false }
wai-quantum = { path = "../wai-quantum", default-features = false, features = ["quantum_engine", "quantum_receipt", "quantum_qec", "quantum_noise", "quantum_fabric", "quantum_sv_metal", "quantum_cutrank_metal"] }
# proc_pid_rusage and waitid, for each job's energy (src/energy.rs).
[target.'cfg(target_os = "macos")'.dependencies]
# proc_pid_rusage and waitid, for each job's energy (src/energy.rs) and a
# child's memory (src/jobs.rs); setrlimit, for a reader's (src/offload.rs).
[target.'cfg(unix)'.dependencies]
libc = "0.2"
[profile.release]

View file

@ -47,7 +47,9 @@ One request can run many programs, as one job:
- **A set:** `"programs"`, a list, each item a program's text or a job (`{"qasm", …}`). The request's other fields (`shots`, `observables`, `seed`, `intent`, …) apply to every item that does not set them itself.
- **A sweep:** `"qasm"` with `"sweep"`, each of the program's inputs and its values. Every combination runs, the names in sorted order and the first varying slowest. An integer input (`input uint n;`) takes whole numbers, and the program is written out at each. A float or angle input (`input float theta;`, `input angle phi;`) takes any number, bound to exactly the double given. Every input the program declares needs values; a list of one fixes it.
A set holds at most 100 items. They run one after another in one worker, inside one job's time limit, holding the memory of the largest item. Each item is a plain job: its request is complete in itself, and it runs, plans and verifies as it would alone. A set with an item that does not plan is refused, naming the item.
A set holds at most 100 items. They run one after another in one worker, inside one job's time limit, holding the memory of the largest item; each item's result is kept as text as it ends, so the results of earlier items take no more than they will as served. Each item is a plain job: its request is complete in itself, and it runs, plans and verifies as it would alone. A set with an item that does not plan is refused, naming the item.
A job's result, or a set's with every item's request and result in it, may take at most `WAI_QSVC_RESULT_MIB` (64 MiB) as served. That is checked before anything runs, from a bound of the result read from the request: each item's counts list at most its shots (a `trajectories` run's, its trajectories), the outcomes its measured bits can give (a noiseless `stabilizer` run's, two to the power of the gates that branch, `h`, `sx` and `sxdg`: a GHZ state's two, however wide) and 65,536 outcomes, each its bit string, quotes, a colon, a comma and its count's digits; a verdict counts the critique's remarks the program can draw (one for each gate that does nothing and each pair that cancels or merges, not one for every gate), each intent's check with the 64 outcomes it lists at a time, and the reader's notes again; each other part is counted by what it may hold or at its length as RFC 8785 writes it, every number in its own form (`1e20` is 21 characters). A request whose bound passes the cap is refused at once, `422` and `J031`, with the bound, its largest part (the counts, the verdict, the reader's notes, the items' requests or the rest) and what makes that part smaller in the reason, and a dry run gives the bound as `result_bytes`. The cap is less than a set at its most can give: at about 22 bytes a 16-bit outcome, 100 points of 65,536 outcomes each are about 150 MB, and are refused; 100 points of 16 measured bits are taken, and run to the end, up to about 30,000 shots a point (30,878 without a verdict; 30,003 over MCP, whose runs ask for one). Send fewer points or shots, split the set, or run it on your own machine (`wai-quantum run`), where no cap applies. A pulse run is bounded the same way, from its schedule as read: its populations and its distribution by the qubits it simulates and the captures it reads, each keyed as wide as the program's bits, its counts by its shots, and each capture with its frame's and bit's names. The check as a result ends stays only as a backstop, for a job kept across a restart that lowered the cap.
The result is a set (`"format": "wai.quantum.set/1"`). Each item has its own result and its own receipt. Each item's leaf is the digest its receipt signs (a failed item's: the digest of its request and its error). The head holds the Merkle root of the leaves, under the JWP Merkle domains, with the count, and the set's receipt seals the set's request and its head. Each item carries its `path` to the root, so one item can be shown sealed by its set without the others. `POST /v1/verify` checks the whole set: its receipt, every item as the request gives it, each item's receipt, the root, and each path.
@ -67,7 +69,8 @@ curl -s https://wai.transaction.science/quantum-jobs/v1/run -H 'content-type: ap
| `memory_bytes`, `work` | the memory it would be admitted with, and the planner's work |
| `time` | `seconds` with its `basis`: the median rate of the last jobs of the method this service timed, or, before it has timed three, the planner's nominal rate; or `seconds: null` with why, where the planner's work does not predict the time |
| `energy` | `joules_micro_estimate`, class `Estimator`, from the median power of the last metered jobs of the method here; or `unestimated` with why. Never a reading |
| `bounds` | the limits that apply: memory and time per job, jobs and memory per client, submissions per window, the queue, the most items in a set |
| `bounds` | the limits that apply: memory and time per job, jobs and memory per client, submissions per window, the queue, the most items in a set, the most a result may take (`result_bytes`), the most intents, the steps a request's programs may take to read, the largest body |
| `result_bytes` | the most its result takes as served, bounded before it runs; past `bounds.result_bytes` it is refused, `422` and `J031` |
| `client` | where you stand: active jobs, memory held, submissions left |
| `accepted` | whether the service would take the job now, by every limit; if not, `status` and `refusal` (its message and code) say why |
| `items` | for a set: each item's plan and estimates |
@ -142,10 +145,10 @@ Counts are always seeded samples.
| `GET /v1` | the service: its methods, limits, load and receipt key |
| `POST /v1/run` | submit a job and wait up to 25 s: the finished job, or `202` and the job if it is still running |
| `POST /v1/jobs` | submit a job or a set: `202` and the job, with its `id`, once it is in the store; with `dry_run`, `200` and the plan |
| `GET /v1/jobs/{id}` | the job: its status, the request as submitted, its result when it is done, and the digests it is kept under; `?wait=S` (at most 25) holds the answer until its status changes |
| `GET /v1/jobs/{id}` | the job, as RFC 8785 text, the same on every route and after a restart: its status, the request as sealed (the canonical form of the one submitted), its result when it is done, and the digests it is kept under; `?wait=S` (at most 25) holds the answer until its status changes |
| `GET /v1/jobs/{id}/events` | the job's status as server-sent events: a `status` event at each change, then one `job` event with the whole job when it ends |
| `DELETE /v1/jobs/{id}` | cancel a queued or running job |
| `POST /v1/verify` | `{"request": ..., "result": ...}`: whether the result's receipt seals that request and that result and every rule of [the provider protocol's section 8.4](../extensions/quantum-providers.md#84-what-a-verifier-checks) holds, read from the body's own text: `valid`, `reasons`, `key`, `form`, and what a provider's result says of itself |
| `POST /v1/verify` | `{"request": ..., "result": ...}` (a set's is counted and read as a check is, since its sweep is written out again; one whose seal does not hold is not): whether the result's receipt seals that request and that result and every rule of [the provider protocol's section 8.4](../extensions/quantum-providers.md#84-what-a-verifier-checks) holds, read from the body's own text: `valid`, `reasons`, `key`, `form`, and what a provider's result says of itself |
| `POST /v1/critique` | `{"qasm"}`: the program's measures (gates by kind, two-qubit gates, depth, T count, whether it is Clifford, a modelled fidelity) and its smells, each with its lines |
| `POST /v1/equivalent` | `{"a", "b"}`: whether two programs are the same circuit up to a global phase, and how that was decided |
| `POST /v1/convert` | `{"to", "program" \| "program_base64", "from"?, "profile"?, "check"?}`: the program carried between OpenQASM 3, QIR and jeff (as base64), with the program as OpenQASM 3, each note with its code, what was read and written, and the check (the output read back and held to the program by the equivalence ladder); a refusal is the reader's, with its code and place ([the formats page](../wai-quantum/docs/formats.md)) |
@ -175,19 +178,42 @@ The three error-correction calls and the five of the variational and algorithm l
A job may give a `target`: a built-in target's name, or a target document (`wai.quantum.target/1`, its schema in `GET /v1/openapi.json`). The program is compiled to the target's gates, placed on the qubits and routes most likely to succeed, and simulated under the noise its calibration gives. The result's `target` names the layout, its expected success, the noise and the target's hash, inside the receipt. The service reads the document it is given and fetches nothing.
Every refusal says why and ends with its code, which the body also carries as `code` ([the job layer's codes](../wai-quantum/docs/job-refusals.md), beside the reader's): `400` for a program or job that does not read (a set that does not read: `J007`), `422` for one larger than a job may be (`J031`), `429` past the per-client limits (`J030`), `503` when the queue is full (`J032`), on a private deployment `401` without one of its API keys (`J033`), and an `Idempotency-Key` that does not read (`400`) or was sent before with another request (`422`): `J034`. `GET /v1` states the limits.
Every refusal says why and ends with its code, which the body also carries as `code` ([the job layer's codes](../wai-quantum/docs/job-refusals.md), beside the reader's): `400` for a program or job that does not read (a set that does not read: `J007`), `422` for one larger than a job may be (`J031`), `429` past the per-client limits (`J030`), `503` when the queue is full or no turn to read it came (`J032`), on a private deployment `401` without one of its API keys (`J033`), and an `Idempotency-Key` that does not read (`400`) or was sent before with another request (`422`): `J034`. `GET /v1` states the limits.
## Status and limits
A job is `queued`, `running`, `done`, `failed`, `cancelled`, or `interrupted`: the service stopped while it ran, after it had run it again once, so it has no result, and `error` says so. Follow a job by its event stream (`GET /v1/jobs/{id}/events`), or by polling `GET /v1/jobs/{id}`, with `?wait=S` to hold each poll until the status changes.
A job is `queued`, `running`, `done`, `failed`, `cancelled`, or `interrupted`: the service stopped while it ran, after it had run it again once, so it has no result, and `error` says so. Follow a job by its event stream (`GET /v1/jobs/{id}/events`), or by polling `GET /v1/jobs/{id}`, with `?wait=S` to hold each poll until the status changes. A client holds at most `WAI_QSVC_CLIENT_STREAMS` (4) long polls and event streams at once: a long poll past them is not held, and says so, `429` (`503` past everyone's), `J032`, with `Retry-After: 2` and the job's `status`; wait that long before polling again, or poll without `?wait`.
Every answer carries the client's submission window in the rate-limit fields of the IETF HTTP API working group: `RateLimit-Policy: "submissions";q=30;w=600` (at most 30 submissions in 600 s) and `RateLimit: "submissions";r=12;t=34` (12 left, one more in 34 s). A refusal past the limits carries `Retry-After`.
A submission is admitted in the order of what each step costs:
1. A job sent again under its `Idempotency-Key` is answered with the key's job, before any limit: it reads nothing, takes no turn and counts as nothing. A job given in another format (`"source"`) is matched as it was received, so it too comes back past the window. Sent again in some other form, it is read and counted to tell whether it is the same request.
2. What reads nothing and refuses whatever is sent: your jobs at once (`429`), the memory they hold (`429`), a full queue (`503`), a spent window (`429`). None of these is counted, so waiting as told spends nothing.
3. It waits for a turn to be read. If none comes, it is refused (`503`, `J032`): nothing was read and nothing is counted.
4. With its turn the submission is counted against your window. Every refusal after this counts.
5. What reads nothing: every whole number exact, at most 16 intents (each of a set's items too), at most 100 items in a set.
6. Its programs are read: the job's own, a set's items and a sweep's points, and every intent's reference, each once however often admission reads it, and all of them within one budget of 22,000,000 steps (statements the reader runs, gates it writes, a generator's loop passes and what it writes out, and every 128 bytes of a value it makes or copies). That is as many as one program may take, its statements and its gates each to the reader's bound, so a job whose own program reads within the reader's bounds reads here. A request whose programs take more is refused, `422` and `J031`, with the command that runs it on your machine.
7. The job is kept and queued, within your and the queue's limits, and within the most a result may take: one whose result's bound passes `WAI_QSVC_RESULT_MIB` is refused, `422` and `J031`, before it runs.
`Retry-After` is the pace at which a refusal that counts would not spend your window (the window over its submissions, at least 5 s), or, past the window, when the next submission opens. A `503` carries it too.
Reading happens outside the service's own process, as each job's run does: each request is read by a child, this binary run as `wai-quantum-service read`, sandboxed as a worker is.
- **Turns.** Four requests are read at a time (jobs, dry runs and the MCP tools' verdicts), and four checks beside them. A client has its share of each: `WAI_QSVC_CLIENT_READS` (2) of the first and `WAI_QSVC_CLIENT_CHECKS` (1) of the checks, so no client holds a whole pool. Past its share, one more turn is lent to a client while the pool keeps two free for others, so its quick calls (a critique, a lint, an estimate) pass its own slow one: of the checks, at their share of one; the reading pool's four turns, at its share of two, leave none to lend. A call on a lent turn moves onto the client's own turn as soon as one comes free, and then has the whole of its time and gives the lent turn back, so the client's quick calls pass it on that one; until then it runs past five seconds only while no other client's request waits for a turn and none of its own client's has waited seven seconds, and once one has it is refused, `503` and `J032`, to send again: two long calls of one client never hold its quick ones past their wait. Its other requests wait behind its own turns, in order, so an agent's burst of parallel calls is answered one after another: up to eight of them may wait (so its eleventh request at once, of readings or of checks, is refused), at most 32 requests wait in all, each for ten seconds, and past any of these a request is refused, `503` and `J032`, having read and spent nothing, saying whether its own requests or the pool held it up. A client is its address, so people behind one address (an office, a carrier's NAT, a class) share one client's turns and its window: raise the shares, or give such a group an API key of its own on a private deployment.
- **Memory.** A child's memory is polled every 50 ms from a thread of its own, and the child is killed once it holds more than `WAI_QSVC_READ_GB` (4); the request is refused, `422` and `J031`. The kill is a signal the service sends itself (`SIGKILL`, to the child's process group, and again every 100 ms until it has ended), so it needs no new process. On macOS the poll reads the child's physical footprint, which counts its compressed and swapped pages; on Linux its resident and swapped pages. A poll is not a kernel limit: a child can pass the cap by what it touches in one interval before it is killed. On Linux the child also holds its own address space to twice the cap and a gigabyte, so an allocation past that fails at once (`422`, `J031`), and the kernel ends a reader or a worker before the service when the host runs short. macOS gives an unprivileged process neither, and has no limit that counts compressed memory.
- **Time.** A child that reads for longer than `WAI_QSVC_READ_SECS` (45) is killed and the request refused, `422` and `J031`. Every request is answered within `WAI_QSVC_ANSWER_SECS` (55) of its arrival, under the 60 s a proxy in front commonly waits for an answer: its waits for a turn and its readings end by then, `POST /v1/run` and the MCP run tools wait for their job only for what is left and then answer with the job, still running, to poll, and an MCP answer's verdict is read again only if time is left (its verdict says so where it was not). An error-correction call's shots are measured as they run: each point is set up and sampled, and a pilot of its own shots decoded, before the rest of any; it is refused at once when what is measured says it would take more than `WAI_QSVC_QEC_SECS` (25), and stopped if it runs a fifth past that.
- **Its answer.** What a reading gives back comes into the service's memory, which every job shares: one that would take more than `WAI_QSVC_READ_ANSWER_MIB` (128) once read, about 80 bytes a value, is refused in the child before it is written (`422`, `J031`), and the service reads it off its request threads. A dry run gets back the plans and points alone. A check's answer can be that large: a critique of a program of 200,000 gates that each draw a remark (`qubit q; for int i in [1:200000] { x q; }`) is refused so; one of 150,000 is answered.
- **What ended jobs keep.** A job keeps its request as the canonical text the store keeps, and a set lets go of its items once it ends. What ended jobs keep in memory, their requests' text and their results as RFC 8785 text, is counted in two parts. Results that have been read (or ended more than `WAI_QSVC_KEPT_MIN_SECS`, 600 s, ago) are held to `WAI_QSVC_CLIENT_KEPT_MIB` (128) a client, its oldest dropped first, and `WAI_QSVC_KEPT_MIB` (4096) in all, those of the client keeping the most dropped first (between two keeping as much, the newer); only these are ever dropped before `WAI_QSVC_RETAIN_SECS`. Results not yet read are dropped by neither cap: a client's next job is refused, `429`, while its unread results, with each job it has running at its result's bound and the new one at `WAI_QSVC_RESULT_MIB` (64), would pass `WAI_QSVC_CLIENT_UNREAD_MIB` (256), and every next job is refused, `503`, while all unread results, with every job queued or running at its result's bound and the new one, would pass `WAI_QSVC_UNREAD_MIB` (8192). A job whose result's bound passes `WAI_QSVC_RESULT_MIB` is refused before it runs (see [Sets and sweeps](#sets-and-sweeps)). The bounds that remain: memory for ended jobs stays under the two caps, 12 GiB at the defaults, the results of jobs still queued or running counted within the unread cap at their bounds; reaching the unread cap takes 32 clients each holding its whole allowance unread, and holds every next job for at most 600 s; a client is its address, so people behind one address share its allowances, and one of them who leaves 256 MiB unread holds the others' next jobs until it is read or 600 s pass; and an honest client's read result is dropped early only where it, of all clients, keeps the most. A request's body is parsed only once its client's window has room (or, for a resend under a key the client holds a job under, first, as it is answered before any limit). A receipt check has a window of its own, `WAI_QSVC_CLIENT_VERIFIES` (120), asked before its body is read. An MCP message over 64 KiB from a client past its window is read only for its id, method, tool and key, and answered under its own id with a tool result that says why; a resend under a key the client holds, and a receipt check, are read whole. A job's report is one RFC 8785 text on every route (a poll, its events, a cancel, and in the MCP job tools' answers, written with RFC 8785's numbers, the job's own bytes, with each JSON-RPC id echoed as the text it was sent as: `1.0`, `1e3` and an integer past 64 bits as written) and after a restart: its request and its result are written in as the text each is kept as, never parsed again, and the memory a large request or answer was parsed into is given back to the system once it is answered. On macOS the service starts itself with the allocator's large-block cache off (`MallocLargeCache=0`): that cache kept every large block a parsed request had freed, dirty and charged to the service (on arm64, 30 requests of a megabyte kept 1 GiB; with it off, 2 MiB).
- **Ending.** A child that panics, overflows its stack or aborts is answered `500`, and the service runs on. A client that hangs up ends its child.
- **Connections.** A request whose header section takes longer than `WAI_QSVC_HEADER_SECS` (150) to arrive is closed; the same limit closes a connection left idle between requests, so it is set above a proxy's own idle limit (two minutes, commonly), and the proxy, not the service, closes its idle connections. A body that takes longer than `WAI_QSVC_BODY_SECS` (30) is refused, `408`. A client may have `WAI_QSVC_CLIENT_REQUESTS` (32) requests open at once; past it a request is refused, `503`, before its body is read (a health check is answered whatever that is). Long polls and event streams are held to `WAI_QSVC_CLIENT_STREAMS` (4) a client and `WAI_QSVC_STREAMS` (512, at most a quarter of the connections) in all: past them a long poll is answered at once, `429` (its client's) or `503` (everyone's), `J032`, with `Retry-After: 2` and the job's status, and an event stream is refused, `503`. Four connections past the connection cap are held for health checks alone, one request each, for 2 s. The service raises its open-file limit to its hard limit as it starts (on macOS no further than `kern.maxfilesperproc` and 10240), says so in its log, and holds at most `WAI_QSVC_CONNECTIONS` (4096) connections, fewer where that limit leaves less room: past them the connection idle longest (or the one whose body has been arriving longest) is closed for the new one, so a health check is always answered, and with none to close a new connection is answered `503` at once.
- **Updating.** Readers and workers start from the service's own binary as it was started: on Linux its running image, elsewhere a copy it keeps beside its signing key, named by its digest. So the binary can be replaced before the restart (by a copy over it, or, on Linux, where a running binary cannot be written, by a rename), and the running service goes on with its own until it is restarted.
## Sending a job again
A connection can fail after the service took a job and before its answer came back. To send the job again safely, give `POST /v1/jobs` or `POST /v1/run` an `Idempotency-Key` (the same working group's field): 1 to 255 visible ASCII characters, with no quote or backslash, as a structured field's string or bare. Make it unguessable, a UUID or 128 random bits, since it is kept per client and a client is an address.
- Sent again with the same key and the same request, the job is answered with the job the key names, before any limit, and counts as no submission.
- Sent again with the same key and the same request, the job is answered with the job the key names, before any limit, and counts as no submission. A send that overtakes its first, both still being read, is read and counts, and gets the one job.
- The same key with another request is refused, `422` and `J034`.
- A key names its job for as long as the job is kept, whatever its status, with one exception: a job the service interrupted. That job's key runs the request again.
- The record keeps a digest of the key with the client's name, never the key.
@ -203,15 +229,17 @@ Every job is in the service's store before its submission is answered, and again
- a job the service stops again while it runs once more comes back `interrupted`, with the reason, so a job that stops the service whenever it runs does not run for ever;
- a queued one is queued again, in its order.
A restart reads no program: a job keeps the plan it was admitted with, and a set its items, so what is queued delays no restart. One too large for the service's limits as they now stand fails, saying why.
Finished jobs are kept for `WAI_QSVC_RETAIN_SECS` (a day here), then dropped. A record names its client by a keyed hash of the address, never the address. [docs/self-host.md](docs/self-host.md#the-job-store) describes the store.
The six checks answer at once, in the service's own process. Each counts against the client's submissions, as a job does. At most two run at a time, and a check that waits ten seconds for its turn is refused (`503`). Some checks are refused (`422`) with the command that runs them on your own machine:
The six checks answer at once, each read and answered in a reading child as a job's admission is. Each counts against the client's submissions, as a job does, once it has its turn and before it reads anything (a check that waits ten seconds and finds no turn is refused, `503`, having read and spent nothing). At most four run at a time, one of them a client's (and one more lent for a quick call), its others waiting behind it. Its bounds and its answer then read its programs within one budget, each read once: a check that runs the budget out is refused, `422` and `J031`, whatever it had made of it, so a repair of many fixes to a slow program ends at the budget. Some checks are refused (`422`) with the command that runs them on your own machine:
- an equivalence check that would apply more than 4·10⁹ gate amplitudes;
- an improvement whose candidates' checks would apply more than that;
- a thorough search for shortcuts;
- a compile of more than 20,000 gates, or with more than 256 rotations to approximate, or a budget that leaves a rotation less than 10⁻¹² (`wai-quantum compile`). A job that asks to be compiled first is held to the same bounds;
- an error-correction call past its bounds here (`wai-quantum qec`): a decode of more than 200,000 shots, of a memory experiment past distance 7, or of more than 2 MiB of circuit or model text; a threshold of more than 16 points or 200,000 shots in all, or at circuit level past distance 7; the distance certificate of `bb:288`;
- an error-correction call past its bounds here (`wai-quantum qec`): a decode of more than 200,000 shots, of more than 1,024 fired detectors, of a memory experiment past distance 7 or 21 rounds, or of more than 2 MiB of circuit or model text; a threshold of more than 16 points or 200,000 shots in all, or at circuit level past distance 7 or 21 rounds; a decode or a threshold whose shots, measured as they run, would take more than `WAI_QSVC_QEC_SECS` (25 s); the distance certificate of `bb:288`;
- a gradient, an optimisation, a VQE or a QAOA run past its bounds here (`wai-quantum gradient`, `optimize`, `vqe`, `qaoa`): more than 16 qubits, more than 500 steps, or about 4·10⁹ gate amplitudes in all; phase estimation on more than 12 counting qubits, or amplitude estimation on more than 8 (`wai-quantum build`);
- an estimate of more than 200,000 gates, or with more than 256 distinct rotation angles to synthesise, or a budget that leaves a rotation less than 10⁻¹² (`wai-quantum estimate`). Verifying an estimate re-runs it, within the same bounds.
- a pulse check whose run takes more than 2·10¹⁰ of the simulator's multiply-adds, or a pulse call on more than 1 MiB of program (`wai-quantum pulse check`). A pulse job itself is held to the limits every job is.
@ -223,11 +251,13 @@ The six checks answer at once, in the service's own process. Each counts against
- resources: the language the engine reads (`wai-quantum://docs/language`), its diagnostics, the methods with their accuracy classes (their reach in a job's memory here), and worked examples, each with what it must do;
- prompts: writing and verifying a circuit for an intent, explaining a result and its receipt, and repairing a program from its diagnostics. Each names this server's own tools.
Every answer about a program states a verdict. Reading the program again for it costs what reading it cost, so it is read, in a reading child, only for a call that was counted. A poll (`get_quantum_job`) states what its job holds: the result's verdict where the job asked for one (`verdict` or `intent`); otherwise, and for a job not yet ended, that it reads, with empty `diagnostics` and `critique`, and a view on a poll draws nothing. A tool that says nothing about a program takes no turn. A call refused before it was counted, told to try again, or that finds no turn for its verdict states a verdict that gives the reason and nothing of the program.
A client whose `initialize` says its host draws interactive views (the extension `io.modelcontextprotocol/ui`, version 2026-01-26) gets the engine's answers as views, as from the command line's server: each tool that has one names its `ui://wai-quantum/*` resource, the six views are listed and read, and each answer carries what its view draws in its `_meta`. The service keeps no session, so the client's word is carried in the session id it is given at `initialize` (`wq-views-` and 32 random hex digits, kept nowhere), which a client sends back with every request (`Mcp-Session-Id`); a client that draws no views gets no session id and none of this. For a job still running when the run's wait ends, the result view follows it with `poll_quantum_job`, which the host keeps from the model (`visibility: ["app"]`); a model reads a job with `get_quantum_job`.
A client that speaks MCP 2025-11-25 may call the three runs as tasks (`tools/call` with `task`): `initialize` declares `tasks` (`cancel`, and `requests.tools.call`), and each run's `execution.taskSupport` is `optional`.
- **The task is the job.** Its `taskId` is the job's id. `tools/call` answers at once with the task, `working`. A program that does not read makes no task: the answer is an error (`-32602`) whose `data` is the refusal, with its codes and verdict.
- **The task is the job.** Its `taskId` is the job's id. `tools/call` answers at once with the task, `working`. A program that does not read makes no task: the answer is an error (`-32602`) whose `data` is the refusal, with its codes and verdict (of the program where the request was counted, as above).
- **Statuses.** `tasks/get` gives the job as a task: `working` while it is queued or running, `completed` once done, `cancelled`, or `failed` for a job that failed or that the service interrupted, with why. Its `ttl` is the service's retention, which a job outlasts from its creation, since it is kept that long after it ends.
- **The result.** `tasks/result` gives what `get_quantum_job` gives for the job that ended, naming the task in its `_meta` (`io.modelcontextprotocol/related-task`), an error (`isError`) where the job failed.
- Asked while the task still works, by a client that takes `text/event-stream`, the answer is an event stream: kept open, and the answer its one event when the job ends.
@ -317,6 +347,26 @@ These variables set the limits:
| `WAI_QSVC_CLIENT_GB` | the job size | memory one client's queued and running jobs may hold together |
| `WAI_QSVC_CLIENT_BURST` | 30 | submissions one client may make per window |
| `WAI_QSVC_CLIENT_WINDOW_SECS` | 600 | the window |
| `WAI_QSVC_READ_GB` | 4 | the memory one request's reading may hold, in its child |
| `WAI_QSVC_READ_SECS` | 45 | the time one request's reading may take |
| `WAI_QSVC_READ_ANSWER_MIB` | 128 | the most one request's reading may give back to the service, once read |
| `WAI_QSVC_QEC_SECS` | 25 | the time an error-correction call's shots may take, measured as they run |
| `WAI_QSVC_ANSWER_SECS` | 55 | the longest any answer takes, its waits and readings included |
| `WAI_QSVC_CLIENT_KEPT_MIB` | 128 | what one client's ended jobs may keep in memory |
| `WAI_QSVC_KEPT_MIB` | 4096 | what all ended jobs whose results have been read may keep in memory |
| `WAI_QSVC_CLIENT_UNREAD_MIB` | 256 | what one client's results not yet read may hold, each of its running jobs counted at its result's bound and the next at `WAI_QSVC_RESULT_MIB`; past it, its next job is refused (`429`) |
| `WAI_QSVC_UNREAD_MIB` | 8192 | what all results not yet read may hold; past it, every next job is refused (`503`) |
| `WAI_QSVC_RESULT_MIB` | 64 | the most one job's or set's result may take, as served: a request whose result's bound passes it is refused before it runs (`422`, `J031`) |
| `WAI_QSVC_CLIENT_STREAMS` | 4 | long polls (`?wait=`) and event streams one client may hold at once |
| `WAI_QSVC_STREAMS` | 512 | those of all clients, at most a quarter of the connections held |
| `WAI_QSVC_KEPT_MIN_SECS` | 600 | how long an ended job not yet read is kept whatever the two caps above say |
| `WAI_QSVC_CLIENT_VERIFIES` | 120 | receipts one client may have checked in its window (`POST /v1/verify`, `verify_quantum_receipt`) |
| `WAI_QSVC_CLIENT_REQUESTS` | 32 | requests one client may have open at once; past it, `503` before the body is read |
| `WAI_QSVC_CONNECTIONS` | 4096 | connections held at once, at most; lowered to what the open-file limit leaves room for |
| `WAI_QSVC_CLIENT_READS` | 2 | how many of one client's jobs, dry runs and verdicts are read at once (more wait) |
| `WAI_QSVC_CLIENT_CHECKS` | 1 | how many of one client's checks run at once (more wait) |
| `WAI_QSVC_HEADER_SECS` | 150 | how long a client may take to send a request's header section, and how long an idle connection stays open |
| `WAI_QSVC_BODY_SECS` | 30 | how long a client may take to send a request's body |
| `WAI_QSVC_MAX_QUEUE` | 256 | jobs waiting at once |
| `WAI_QSVC_RETAIN_SECS` | 86400 | how long finished jobs are kept, across restarts |
| `WAI_QSVC_STORE` | `~/.local/share/wai-quantum-service/store` | the job store's directory, or `memory` |

View file

@ -51,4 +51,4 @@ Before committing an allocation to it, a job can be planned without running: `wa
## Limits and keys
The public service counts a client by its address. A batch system submits from a few login or compute nodes on behalf of many people, so on a shared deployment give the scheduler its own API key (`WAI_QUANTUM_API_KEY`, see [self-host.md](self-host.md#api-keys-for-a-private-deployment)). Its limits are then counted by the key's name, and `WAI_QSVC_CLIENT_ACTIVE`, `WAI_QSVC_CLIENT_GB` and `WAI_QSVC_CLIENT_BURST` size them. Every answer carries the client's submission window in `RateLimit-Policy` and `RateLimit`, and a refusal past it carries `Retry-After`. The adapter exits 75 on such a refusal, which a scheduler can treat as "requeue".
The public service counts a client by its address. A batch system submits from a few login or compute nodes on behalf of many people, so on a shared deployment give the scheduler its own API key (`WAI_QUANTUM_API_KEY`, see [self-host.md](self-host.md#api-keys-for-a-private-deployment)). Its limits are then counted by the key's name, and `WAI_QSVC_CLIENT_ACTIVE`, `WAI_QSVC_CLIENT_GB`, `WAI_QSVC_CLIENT_BURST`, `WAI_QSVC_CLIENT_READS` and `WAI_QSVC_CLIENT_CHECKS` (how many of its submissions and checks are read at once; more wait behind them) size them. Every answer carries the client's submission window in `RateLimit-Policy` and `RateLimit`, and a refusal past it carries `Retry-After`. The adapter exits 75 on such a refusal, which a scheduler can treat as "requeue".

View file

@ -50,6 +50,26 @@ Everything is set by environment variables.
| `WAI_QSVC_CLIENT_ACTIVE` | 3 | queued or running jobs one client may hold |
| `WAI_QSVC_CLIENT_GB` | the job size | the memory one client's jobs may hold together |
| `WAI_QSVC_CLIENT_BURST` | 30 | submissions one client may make in a window |
| `WAI_QSVC_READ_GB` | 4 | the memory one request's reading may take (polled; see the README's reading section) |
| `WAI_QSVC_READ_SECS` | 45 | the time one request's reading may take |
| `WAI_QSVC_READ_ANSWER_MIB` | 128 | the most one request's reading may give back to the service, once read |
| `WAI_QSVC_QEC_SECS` | 25 | the time an error-correction call's shots may take, measured as they run |
| `WAI_QSVC_ANSWER_SECS` | 55 | the longest any answer takes, its waits and readings included |
| `WAI_QSVC_CLIENT_KEPT_MIB` | 128 | what one client's ended jobs may keep in memory |
| `WAI_QSVC_KEPT_MIB` | 4096 | what all ended jobs whose results have been read may keep in memory |
| `WAI_QSVC_CLIENT_UNREAD_MIB` | 256 | what one client's results not yet read may hold, each of its running jobs counted at its result's bound and the next at `WAI_QSVC_RESULT_MIB`; past it, its next job is refused (`429`) |
| `WAI_QSVC_UNREAD_MIB` | 8192 | what all results not yet read may hold; past it, every next job is refused (`503`) |
| `WAI_QSVC_RESULT_MIB` | 64 | the most one job's or set's result may take, as served: a request whose result's bound passes it is refused before it runs (`422`, `J031`) |
| `WAI_QSVC_CLIENT_STREAMS` | 4 | long polls (`?wait=`) and event streams one client may hold at once |
| `WAI_QSVC_STREAMS` | 512 | those of all clients, at most a quarter of the connections held |
| `WAI_QSVC_KEPT_MIN_SECS` | 600 | how long an ended job not yet read is kept whatever the two caps above say |
| `WAI_QSVC_CLIENT_VERIFIES` | 120 | receipts one client may have checked in its window (`POST /v1/verify`, `verify_quantum_receipt`) |
| `WAI_QSVC_CLIENT_REQUESTS` | 32 | requests one client may have open at once; past it, `503` before the body is read |
| `WAI_QSVC_CONNECTIONS` | 4096 | connections held at once, at most; lowered to what the open-file limit leaves room for |
| `WAI_QSVC_CLIENT_READS` | 2 | how many of one client's jobs, dry runs and verdicts are read at once (more wait) |
| `WAI_QSVC_CLIENT_CHECKS` | 1 | how many of one client's checks run at once (more wait) |
| `WAI_QSVC_HEADER_SECS` | 150 | how long a client may take to send a request's header section, and how long an idle connection stays open |
| `WAI_QSVC_BODY_SECS` | 30 | how long a client may take to send a request's body |
| `WAI_QSVC_CLIENT_WINDOW_SECS` | 600 | the window |
| `WAI_QSVC_MAX_QUEUE` | 256 | jobs waiting at once |
| `WAI_QSVC_SANDBOX` | 1 | 0 runs the macOS worker unsandboxed, for debugging |
@ -123,6 +143,10 @@ Each finished job carries its energy inside the signed result: a counter reading
- **Linux:** the processor packages' energy counters, read through the kernel's power-capping interface before the worker starts and after it exits. They count whole packages, so the figure is the packages' energy over the job's window, every process on the host included, with no attribution to the job (`attribution: none`). It names how many other jobs ran in the window (`concurrent_jobs`). The counter is used only when every package zone reads and its count is live; its declared uncertainty covers the two reads and the counter's update edges, measured at start.
- **Permissions:** many Linux hosts let only root, or a group such as `energy`, read the count. Add the service's user to that group. In a container the interface is usually hidden; a container that should meter needs it visible and readable, which widens what the container sees of the host. Without it, jobs say they are unmetered and why.
## Open files
Each connection takes a file. The service raises its soft open-file limit to its hard one as it starts and logs the result (`open files: …`), and holds connections under it with room for its readers' and workers' pipes and its store's files (`connections: at most …`). On macOS a launchd service inherits `launchctl limit maxfiles`, soft 256 and hard unlimited by default: the service raises its soft limit itself (to 10240, or `kern.maxfilesperproc` if lower) and holds `WAI_QSVC_CONNECTIONS` (4096). Only where the hard limit is low does the cap fall: at a hard limit of 256 it holds 192 connections, so a plist that sets `HardResourceLimits` (`NumberOfFiles`) should set it to 10240 or more. Whatever the cap, a few connections past it are held for health checks alone, and long polls and event streams take at most a quarter of it.
## Behind a reverse proxy
Run the service on a loopback address and let the proxy take the public traffic.
@ -130,8 +154,18 @@ Run the service on a loopback address and let the proxy take the public traffic.
- **The client's address.** The proxy sets `X-Forwarded-For`, and the service trusts it only from `WAI_QSVC_TRUSTED_PROXIES`. Have the proxy set the header from the connection itself, not pass on what the request carried, or a client could choose its own limits.
- **A path prefix.** Mount the service under a prefix (the public one is `/quantum-jobs`) by stripping the prefix before passing the request on. Set `WAI_QSVC_PUBLIC_BASE` to the prefixed URL, so the links the service hands out are right. A well-known path belongs at the origin's root: map `/.well-known/wai-quantum-receipt-keys` there to the service's own.
- **Event streams.** `GET /v1/jobs/{id}/events` is a server-sent event stream. The proxy must pass it through unbuffered (the service sends `X-Accel-Buffering: no` and a comment every 15 s), and allow it to stay open as long as `WAI_QSVC_JOB_SECS`. Polls with `?wait=` are held up to 25 s.
- **Bodies.** A request body is at most 1 MiB, and `POST /v1/run` waits up to 25 s before answering 202.
- **Bodies.** A request body is at most 1 MiB, and `POST /v1/run` waits up to 25 s before answering 202. An answer's body can be as large as a result may be as served, `WAI_QSVC_RESULT_MIB` (64 MiB), with the job's request beside it: a proxy that buffers or caps answers must allow that. A request whose result could be larger is refused before it runs (`422`, `J031`), its bound in the reason.
- **Rate-limit fields.** Pass the `RateLimit-Policy`, `RateLimit` and `Retry-After` headers through.
- **Timeouts.** Every answer comes within `WAI_QSVC_ANSWER_SECS` (55) of the request's arrival, so within a proxy's 60 s wait for an answer's header. The worst case of each route, from arrival: a check, a dry run or a submission waits at most 10 s for a turn and reads for at most `WAI_QSVC_READ_SECS` (45), 55 s in all; `POST /v1/run` and the MCP run tools then wait for their job only for what is left of the 55 s (less 5 s, on MCP, for its verdict) and answer with the job, still running, to poll; an MCP answer's verdict waits for a turn and is read only within what is left, and says where no time was left; `tasks/result` holds a reply for what is left of the 55 s, 50 s at most. With a shorter wait in the proxy, lower `WAI_QSVC_ANSWER_SECS` below it. Keep the proxy's idle connections shorter than `WAI_QSVC_HEADER_SECS` (150), which also bounds an idle connection on the service's side: the proxy then closes its idle connections itself, and never sends a request on one as the service closes it.
- **One address, many people.** The service counts a client by the address the proxy gives it. People behind one address share one client's turns (`WAI_QSVC_CLIENT_READS`, `WAI_QSVC_CLIENT_CHECKS`) and window; raise those, or give such a group an API key.
## Updating it
Readers and workers start from the service's own binary as it was started: on Linux its running image (`/proc/<pid>/exe`), elsewhere a copy kept beside the signing key (`bin/wai-quantum-service-<digest>`). So:
1. Put the new binary in place: a copy over the old one, or a copy beside it renamed over it (on Linux a running binary cannot be written, so rename). The running service goes on reading and running jobs with its own binary.
2. Restart the service. It keeps a copy of the new binary on its first start (on Linux, nothing to keep).
3. Check `GET /v1/health`, then `smoke.sh`. Copies of earlier binaries beside the key are not removed; delete them once no service started from them runs.
A minimal site, in one common proxy's configuration format:
@ -141,6 +175,9 @@ quantum.example.org {
reverse_proxy 127.0.0.1:8241 {
header_up X-Forwarded-For {remote_host}
flush_interval -1
transport http {
keepalive 60s
}
}
}
}

View file

@ -304,7 +304,7 @@
},
"accepted": {
"type": "boolean",
"description": "Whether the service would take it now, by every limit."
"description": "Whether the service would take it now, by every limit, its reading held to what the submission's would take; the MCP tools plan it as their runs run it, its verdict asked."
},
"status": {
"type": "integer",
@ -390,9 +390,34 @@
},
"bounds": {
"type": "object",
"description": "The bounds that apply here: memory, time, queue and items of a set.",
"description": "The bounds that apply here: memory, time, queue, items of a set, and result_bytes, the most a job's or a set's result may take as served.",
"additionalProperties": true
},
"result_bytes": {
"description": "The most its result takes as served, bytes, bounded before it runs: past bounds.result_bytes the job is refused (422, J031). A pulse run is bounded from its schedule. For a method that gives no bound before it runs (no built-in is such): {\"bytes\": null, \"why\"}, and it is counted at bounds.result_bytes.",
"oneOf": [
{
"type": "integer",
"minimum": 0
},
{
"type": "object",
"required": [
"bytes",
"why"
],
"properties": {
"bytes": {
"type": "null"
},
"why": {
"type": "string"
}
},
"additionalProperties": false
}
]
},
"client": {
"type": "object",
"description": "Where the client stands: its active jobs, the memory they hold, the submissions left in its window and when the window resets.",

View file

@ -1,12 +1,13 @@
//! Design checks the engine answers at once, without a job: a program's
//! critique, two programs' equivalence, the improvements its fixes make, its
//! classical shortcuts and its compile (`wai_quantum::engine::{critique,
//! equiv, shortcuts, compile}`). They run in this process, off the request's
//! thread, within bounds:
//! equiv, shortcuts, compile}`). Each is read and answered by a reading
//! child ([`crate::offload`]), whose memory and time are capped, within
//! bounds:
//!
//! - each counts against the client's submissions, as a job does;
//! - at most [`AT_ONCE`] run at a time, and a request that waits [`WAIT`] for
//! a turn is refused (503);
//! - at most [`crate::offload::CHECKS_AT_ONCE`] run at a time, and a request
//! that waits [`crate::offload::WAIT`] for a turn is refused (503);
//! - an equivalence check that would apply more than [`EQUIV_MAX_WORK`] gate
//! amplitudes is refused (422) with the command that runs it on the client's
//! own machine. So is an improvement whose candidates' checks would, and so
@ -24,8 +25,9 @@
//! - a job's intents are held to the same bound as the equivalence check
//! ([`intent_bounds`]), so a check that would take long is refused before
//! the job is queued, with the command that runs it locally;
//! - a repair ([`Check::Repair`]) is bounded by the program's size: it reads
//! the program once a repair, at most `engine::repair::MAX_ROUNDS` times;
//! - a repair ([`Check::Repair`]) reads the program again for each fix, at
//! most `engine::repair::MAX_ROUNDS` times, within the check's one budget
//! of reading: a fix makes a new text, so each round costs what it reads;
//! - a conversion between formats ([`Check::Convert`]) reads at most
//! [`CONVERT_MAX_BYTES`], and its check is held to the equivalence check's
//! bound.
@ -33,22 +35,24 @@
//! Every refusal ends with its code: J031 for what is larger than the
//! service takes, J032 when every check is running.
//!
//! **In the order of what each step costs.** A check past the client's
//! window is refused first; then it waits for a turn; with its turn it is
//! counted, so every check refused after that counts, and one told to try
//! again (`503`) has read and spent nothing. Then, in its reading child, its
//! bounds read its programs and its answer is made, each program read once,
//! all within one budget of reading ([`crate::http::READ_STEPS`]).
//!
//! The critique and the quick shortcuts are bounded by the program's size.
//! The tableau, the cut-rank planner's search and the light cones are linear
//! in the gates or close to it.
//! in the gates or close to it. The other engine calls are held to their own
//! work bounds above, and in their child to its memory and its time.
use crate::http::App;
use serde_json::Value;
use std::time::Duration;
use tokio::sync::Semaphore;
use wai_quantum::engine::synth::lower;
use wai_quantum::engine::refusal::{self, tag};
use wai_quantum::engine::{compile, critique, equiv, repair, shortcuts};
/// Checks that run at once; the rest wait their turn.
pub const AT_ONCE: usize = 2;
/// How long a check waits for a turn before it is refused.
pub const WAIT: Duration = Duration::from_secs(10);
/// Gate applications times amplitudes one equivalence check may take here.
pub const EQUIV_MAX_WORK: f64 = 4e9;
/// The most gates a compile here takes.
@ -64,7 +68,10 @@ pub const ESTIMATE_MAX_ANGLES: usize = 256;
/// The smallest share of the synthesis budget a rotation may get here.
pub const ESTIMATE_MIN_EACH: f64 = 1e-12;
static TURNS: Semaphore = Semaphore::const_new(AT_ONCE);
/// The most intents a job states (a set's items each): an intent's
/// reference is a program to read and to check against the job's, so their
/// number is held before any is read.
pub const MAX_INTENTS: usize = 16;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Check {
@ -77,6 +84,27 @@ pub enum Check {
Convert,
}
impl Check {
const ALL: [(Check, &'static str); 7] = [
(Check::Critique, "critique"),
(Check::Equivalent, "equivalent"),
(Check::Improve, "improve"),
(Check::Shortcuts, "shortcuts"),
(Check::Compile, "compile"),
(Check::Repair, "repair"),
(Check::Convert, "convert"),
];
/// The check's name, as a reading child is given it.
pub fn name(self) -> &'static str {
Self::ALL.iter().find(|(c, _)| *c == self).map(|(_, n)| *n).expect("every check is named")
}
pub fn named(name: &str) -> Option<Check> {
Self::ALL.iter().find(|(_, n)| *n == name).map(|(c, _)| *c)
}
}
fn program<'a>(body: &'a Value, key: &str) -> Result<&'a str, (u16, String)> {
body.get(key).and_then(Value::as_str).ok_or_else(|| (400, format!("give \"{key}\": an OpenQASM program")))
}
@ -169,6 +197,14 @@ pub const QEC_MAX_POINTS: usize = 16;
pub const QEC_MAX_CIRCUIT_DISTANCE: u64 = 7;
/// The most bytes of circuit or model text a decode reads here.
pub const QEC_MAX_TEXT: usize = 2 << 20;
/// The most rounds of a memory experiment a decode or a threshold runs here:
/// three times the largest distance, past which a fit gains nothing a
/// distance does not.
pub const QEC_MAX_ROUNDS: u64 = 3 * QEC_MAX_CIRCUIT_DISTANCE;
/// The most detectors a decode here is given as fired. Matching grows faster
/// than the square of them, in time and in memory: a thousand take about a
/// second and a hundred MiB.
pub const QEC_MAX_FIRED: usize = 1024;
/// Whether the service takes the error-correction call `op` on `body`: the
/// reason it does not, with the status to answer, or nothing. The engine's
@ -194,9 +230,15 @@ pub fn qec_bounds(op: &str, body: &Value) -> Result<(), (u16, String)> {
if body.get("shots").and_then(Value::as_u64).is_some_and(|s| s > QEC_MAX_SHOTS) {
return far(format!("more than {QEC_MAX_SHOTS} shots; {}", local("decode")));
}
if body.get("detectors").and_then(Value::as_array).is_some_and(|d| d.len() > QEC_MAX_FIRED) {
return far(format!("more than {QEC_MAX_FIRED} fired detectors; {}", local("decode")));
}
if body.pointer("/memory/distance").and_then(Value::as_u64).is_some_and(|d| d > QEC_MAX_CIRCUIT_DISTANCE) {
return far(format!("a memory experiment past distance {QEC_MAX_CIRCUIT_DISTANCE}; {}", local("decode")));
}
if body.pointer("/memory/rounds").and_then(Value::as_u64).is_some_and(|r| r > QEC_MAX_ROUNDS) {
return far(format!("a memory experiment of more than {QEC_MAX_ROUNDS} rounds; {}", local("decode")));
}
}
_ => {
let n = |k: &str, d: usize| body.get(k).and_then(Value::as_array).map_or(d, Vec::len);
@ -205,6 +247,9 @@ pub fn qec_bounds(op: &str, body: &Value) -> Result<(), (u16, String)> {
if points > QEC_MAX_POINTS || shots.saturating_mul(points as u64) > QEC_MAX_SHOTS {
return far(format!("{points} points of {shots} shots, more than a threshold here measures ({QEC_MAX_POINTS} points, {QEC_MAX_SHOTS} shots in all); {}", local("threshold")));
}
if body.get("rounds").and_then(Value::as_u64).is_some_and(|r| r > QEC_MAX_ROUNDS) {
return far(format!("more than {QEC_MAX_ROUNDS} rounds; {}", local("threshold")));
}
let wide = body.get("distances").and_then(Value::as_array).into_iter().flatten().filter_map(Value::as_u64).any(|d| d > QEC_MAX_CIRCUIT_DISTANCE);
if wide && body.get("family").and_then(Value::as_str).unwrap_or("surface") == "surface" {
return far(format!("a circuit-level threshold past distance {QEC_MAX_CIRCUIT_DISTANCE}; {}", local("threshold")));
@ -312,19 +357,58 @@ pub fn variational_bounds(op: &str, body: &Value) -> Result<(), (u16, String)> {
Ok(())
}
/// `work` for `who` within the bounds above (counted, one of [`AT_ONCE`],
/// off the request's thread): its answer, or 422 with its refusal.
pub async fn bounded<F>(app: &App, who: &str, work: F) -> Result<Value, (u16, String)>
where
F: FnOnce() -> Result<Value, String> + Send + 'static,
{
app.service.count(who).map_err(|crate::jobs::Refused(code, why)| (code, why))?;
let Ok(Ok(turn)) = tokio::time::timeout(WAIT, TURNS.acquire()).await else {
return Err((503, format!("{AT_ONCE} checks are running; try again shortly")));
};
let v = tokio::task::spawn_blocking(work).await.map_err(|e| (500, format!("the check failed: {e}")))?;
drop(turn);
v.map_err(|e| (422, e))
/// The engine call `kind` (`estimate`, `estimate_verify`, `qec`, `pulse`,
/// `variational`) on `body`, held to its bounds first, within one budget of
/// reading: its answer, or the refusal. What a reading child runs; `key` is
/// the receipt key's secret in hex, for an estimate, which seals as it goes.
pub fn call(kind: &str, op: &str, body: &Value, key: Option<&str>) -> Result<Value, (u16, String)> {
let engine = |r: Result<Value, String>| r.map_err(|e| (422, e));
match kind {
"estimate" => {
crate::http::within_budget(|| estimate_bounds(body))?;
let hex = key.ok_or((500, "an estimate is sealed, and no key was given".to_string()))?;
let bytes: Vec<u8> = (0..hex.len()).step_by(2).filter_map(|i| hex.get(i..i + 2).and_then(|b| u8::from_str_radix(b, 16).ok())).collect();
let secret: [u8; 32] = bytes.try_into().map_err(|_| (500, "the receipt key did not read".to_string()))?;
engine(wai_quantum::engine::estimate::sealed(body, &wai_quantum::ed25519_dalek::SigningKey::from_bytes(&secret)))
}
"estimate_verify" => {
crate::http::within_budget(|| estimate_bounds(&body["request"]))?;
engine(wai_quantum::engine::estimate::verify(body))
}
"qec" => {
let op = ["code", "decode", "threshold"].into_iter().find(|o| *o == op).ok_or((400, format!("no error-correction call {op}")))?;
qec_bounds(op, body)?;
// Its shots are measured as they run, and refused as soon as the
// rest would pass the time: a shot's cost is the noise's as much
// as the counts'.
let secs = crate::jobs::Config::from_env().qec_secs;
engine(wai_quantum::engine::qec::within_time(std::time::Duration::from_secs(secs), || wai_quantum::engine::qec::call(op, body)))
}
"pulse" => {
let op = ["schedule", "check"].into_iter().find(|o| *o == op).ok_or((400, format!("no pulse call {op}")))?;
crate::http::within_budget(|| pulse_bounds(op, body))?;
engine(wai_quantum::engine::pulse::call(op, body))
}
"variational" => {
let op = ["gradient", "optimize", "vqe", "qaoa", "build"].into_iter().find(|o| *o == op).ok_or((400, format!("no variational call {op}")))?;
crate::http::within_budget(|| variational_bounds(op, body))?;
engine(wai_quantum::engine::variational::call(op, body))
}
other => Err((400, format!("no engine call {other}"))),
}
}
/// The engine call `read` for `who` within the bounds above: refused past its
/// window, then, one of [`crate::offload::CHECKS_AT_ONCE`] in its turn,
/// counted, held to its bounds and run in a reading child whose memory and
/// time are capped ([`crate::offload`]): its answer, or the refusal. Told to
/// try again (`503`), it has read nothing, and is not counted.
pub async fn bounded(app: &App, who: &str, read: crate::offload::Read) -> Result<Value, (u16, String)> {
let refused = |crate::jobs::Refused(code, why)| (code, why);
app.service.window_open(who).map_err(refused)?;
let turn = app.service.gate.turn(crate::offload::Pool::Checks, who).await?;
app.service.count(who).map_err(refused)?;
crate::offload::read(&app.service.config, turn, read).await
}
/// Whether the service takes `check` on `body`: the reason it does not, with
@ -392,10 +476,22 @@ pub fn convert_bounds(body: &Value) -> Result<(), (u16, String)> {
Ok(())
}
/// Whether a job states at most [`MAX_INTENTS`] intents, read without
/// reading any of them: the reason it does not (400, `J020`), or nothing.
pub fn intent_count(request: &Value) -> Result<(), (u16, String)> {
match request.get("intent") {
Some(Value::Array(list)) if list.len() > MAX_INTENTS => Err((400, tag(refusal::BAD_INTENT, format!("{} intents; a job here states at most {MAX_INTENTS}", list.len())))),
_ => Ok(()),
}
}
/// Whether the service checks a job's intents beside its run: each
/// reference within the equivalence check's bound, or the reason it does
/// not (422, with the command that runs it locally).
/// not (422, with the command that runs it locally). It reads the program
/// and each reference: within a budget of reading ([`crate::http::plan_job`]),
/// each is read once however often its intents name it.
pub fn intent_bounds(request: &Value) -> Result<(), (u16, String)> {
intent_count(request)?;
let Some(i) = request.get("intent").filter(|i| !i.is_null()) else { return Ok(()) };
let intents = wai_quantum::engine::verdict::intents(i).map_err(|e| (400, e))?;
let qasm = request.get("qasm").and_then(Value::as_str).unwrap_or("");
@ -439,15 +535,10 @@ pub fn answer(check: Check, body: &Value) -> Value {
/// `check` on `body` for `who`, within the bounds above: the answer, or the
/// status and reason it was refused.
/// It is held to its bounds and answered in a reading child, within one
/// budget ([`bounded`]).
pub async fn analyse(app: &App, who: &str, check: Check, body: Value) -> Result<Value, (u16, String)> {
bounds(check, &body)?;
app.service.count(who).map_err(|crate::jobs::Refused(code, why)| (code, why))?;
let Ok(Ok(turn)) = tokio::time::timeout(WAIT, TURNS.acquire()).await else {
return Err((503, tag(refusal::BUSY, format!("{AT_ONCE} checks are running; try again shortly"))));
};
let v = tokio::task::spawn_blocking(move || answer(check, &body)).await.map_err(|e| (500, format!("the check failed: {e}")))?;
drop(turn);
Ok(v)
bounded(app, who, crate::offload::Read::Check { check: check.name().into(), body }).await
}
#[cfg(test)]
@ -455,6 +546,48 @@ mod tests {
use super::*;
use serde_json::json;
/// A check that has run its budget out has no answer, whatever its
/// reading made of that: a program it could not read is not one of no
/// work. Each bound that reads refuses, under a budget too small for it,
/// where each used to read the refusal as nothing to do.
#[test]
fn a_budget_spent_is_a_refusal_whatever_the_check_made_of_it() {
let wide = |k: u32| format!("OPENQASM 3.0;\nqubit[12] q;\nint a = 0;\nfor int i in [0:99999] {{ a += 1; }}\nrz(0.1) q[0];\n// {k}\n");
for (what, read) in [
("equivalent", Box::new(|| bounds(Check::Equivalent, &serde_json::json!({ "a": wide(0), "b": wide(1) }))) as Box<dyn Fn() -> Result<(), (u16, String)>>),
("intent", Box::new(|| intent_bounds(&serde_json::json!({ "qasm": wide(2), "intent": { "equivalent": wide(3) } })))),
("improve", Box::new(|| bounds(Check::Improve, &serde_json::json!({ "qasm": wide(4) })))),
("gradient", Box::new(|| variational_bounds("gradient", &serde_json::json!({ "qasm": wide(5) })))),
("convert", Box::new(|| convert_bounds(&serde_json::json!({ "program": wide(6), "from": "qasm", "to": "qasm" })))),
] {
let e = wai_quantum::qasm::within_steps(1_000, || crate::http::within_budget(read)).unwrap_err();
assert!(e.0 == 422 && e.1.ends_with("[J031]") && e.1.contains("steps"), "{what}: {e:?}");
}
// An answer made under it is held the same way.
let e = wai_quantum::qasm::within_steps(1_000, || crate::http::within_budget(|| Ok(answer(Check::Critique, &serde_json::json!({ "qasm": wide(7) }))))).unwrap_err();
assert!(e.0 == 422 && e.1.ends_with("[J031]"), "{e:?}");
}
/// Rounds are held, in a decode's memory experiment and in a threshold.
#[test]
fn a_memory_experiment_is_held_to_its_rounds() {
for (op, body) in [("decode", serde_json::json!({ "memory": { "distance": 7, "rounds": 50 }, "shots": 10 })), ("threshold", serde_json::json!({ "distances": [3], "rounds": 50 }))] {
let e = qec_bounds(op, &body).unwrap_err();
assert!(e.0 == 422 && e.1.contains("rounds") && e.1.ends_with("[J031]"), "{op}: {e:?}");
}
assert!(qec_bounds("decode", &serde_json::json!({ "memory": { "distance": 7, "rounds": QEC_MAX_ROUNDS }, "shots": 10 })).is_ok());
}
/// Matching grows faster than the square of the detectors fired: a decode
/// is held to how many it is given, and told so with the local command.
#[test]
fn a_decode_is_held_to_the_detectors_it_is_given() {
let body = |n: usize| serde_json::json!({ "model": "error(0.1) D0 D1\n", "detectors": vec![0; n] });
assert!(qec_bounds("decode", &body(QEC_MAX_FIRED)).is_ok());
let e = qec_bounds("decode", &body(QEC_MAX_FIRED + 1)).unwrap_err();
assert!(e.0 == 422 && e.1.contains("wai-quantum qec decode") && e.1.ends_with("[J031]"), "{e:?}");
}
#[test]
fn the_bounds_refuse_what_would_take_long_and_say_where_it_runs() {
let small = json!({ "qasm": "qubit[2] q; h q[0]; h q[0]; cx q[0], q[1];" });

View file

@ -104,7 +104,7 @@ pub fn examples() -> Value {
}
/// The API as an OpenAPI 3.1 document.
pub fn openapi(server: &str) -> Value {
pub fn openapi(server: &str, result_bytes: u64) -> Value {
let prog = json!({ "type": "object", "required": ["qasm"], "properties": { "qasm": { "type": "string", "description": "The program, OpenQASM 3 or 2." } } });
let pauli = json!({ "oneOf": [
{ "type": "string", "description": "A Pauli string: factors such as `Z0`, `X3`, `Y12`, separated by spaces; qubit indices count from 0.", "examples": ["Z0 Z1", "X3 Y4"] },
@ -139,7 +139,7 @@ pub fn openapi(server: &str) -> Value {
"intent": { "description": "What the program must do, checked with the run: one intent or a list, each {\"distribution\": {\"00\": 0.5, \"11\": 0.5}, \"tolerance\"}, {\"equivalent\": \"<the reference program>\"} or {\"prepares\": \"<the reference program>\"}, with an optional note and \"require\": \"proof\". The result's `verdict` states the level the engine checked to (proof, evidence, executed, parsed, refused) and whether the intents hold, and the receipt seals it." },
"verdict": { "type": "boolean", "description": "true: the result carries the verdict with no intent (how far the engine checked)." },
"repair": { "description": "true, or the choices {\"max_iterations\", \"dyadic_bits\", \"sizes\"}: the program is repaired first on the lines its diagnostics flag (as POST /v1/repair does), and the result's `repaired` holds the program that ran and each repair." },
"programs": { "type": "array", "minItems": 1, "maxItems": wai_quantum::engine::sets::MAX_ITEMS, "items": { "oneOf": [{ "type": "string" }, { "type": "object", "required": ["qasm"] }] }, "description": "A set: in place of qasm, the programs to run as one job, each a program's text or a job whose own fields win over the set's. Every other field applies to each item. The result is a set (SetResult)." },
"programs": { "type": "array", "minItems": 1, "maxItems": wai_quantum::engine::sets::MAX_ITEMS, "items": { "oneOf": [{ "type": "string" }, { "type": "object", "required": ["qasm"] }] }, "description": format!("A set: in place of qasm, the programs to run as one job, each a program's text or a job whose own fields win over the set's. Every other field applies to each item. The result is a set (SetResult). A job or set whose result could take more than {result_bytes} bytes as served (result_bytes), by its bound before it runs (each item's counts at most its shots or a trajectories run's trajectories, the outcomes its measured bits can give and 65,536 outcomes; a verdict's critique by the remarks its gates can draw; its other parts at their length as read, every number as RFC 8785 writes it), is refused at once (422, J031), naming its largest part: at about 22 bytes a 16-bit outcome, 100 items of 65,536 outcomes each is about 150 MB.") },
"sweep": { "type": "object", "additionalProperties": { "type": "array", "minItems": 1, "items": { "type": "number" } }, "description": "A set over qasm's inputs: each input's values, every combination run (the names in sorted order, the first varying slowest). An integer input (`input uint n;`) takes whole numbers and the program is written out at each; a float or angle input (`input float theta;`) is bound to the double given. Every input the program declares is given values." },
"dry_run": { "type": "boolean", "description": "true: run nothing, and answer 200 with the plan (DryRun): the method, memory, estimated time and energy with their basis, the bounds, and whether the service would take it now." },
"pulse": wai_quantum::engine::pulse::options_schema(),
@ -152,7 +152,7 @@ pub fn openapi(server: &str) -> Value {
"properties": {
"dry_run": { "const": true },
"ran": { "const": false },
"accepted": { "type": "boolean", "description": "Whether the service would take it now, by every limit." },
"accepted": { "type": "boolean", "description": "Whether the service would take it now, by every limit, its reading held to what the submission's would take; the MCP tools plan it as their runs run it, its verdict asked." },
"status": { "type": "integer", "description": "With accepted false: the HTTP status the submission would get." },
"refusal": { "type": "object", "description": "With accepted false: the refusal as data (message, code, codes, outcome, meaning)." },
"method": { "type": "string" }, "why": { "type": "string" }, "qubits": { "type": "integer" },
@ -161,7 +161,8 @@ pub fn openapi(server: &str) -> Value {
"time": { "type": "object", "description": "{\"seconds\", \"basis\"}: an estimate from this service's own timings of the method, or the planner's nominal rate; or {\"seconds\": null, \"why\", \"at_most\"} where the work does not predict the time." },
"energy": { "type": "object", "description": "{\"joules_micro_estimate\", \"energy_provenance\": \"Estimator\", \"basis\"} from the power of past metered jobs of the method here, or {\"unestimated\": why}. Never a reading." },
"within_time_limit": { "type": ["boolean", "null"] },
"bounds": { "type": "object", "description": "The limits that apply: job_memory_bytes, job_seconds, active_jobs_per_client, memory_per_client_bytes, submissions_per_window, window_seconds, queue, set_items, body_bytes." },
"bounds": { "type": "object", "description": "The limits that apply: job_memory_bytes, job_seconds, active_jobs_per_client, memory_per_client_bytes, submissions_per_window, window_seconds, queue, set_items, result_bytes (the most a job's or a set's result may take as served, by its bound before it runs), intents (the most a job states), read_steps (the steps all of a request's programs may take to read together), body_bytes." },
"result_bytes": { "type": ["integer", "object"], "description": "The most its result takes as served, bytes, bounded before it runs; past bounds.result_bytes the submission is refused (422, J031). {\"bytes\": null, \"why\"} for a method that gives no bound before it runs (no built-in), counted at bounds.result_bytes. A pulse run is bounded from its schedule." },
"client": { "type": "object", "description": "Where the client stands: active_jobs, memory_held_bytes, submissions_left, window_resets_in_seconds." },
"items": { "type": "array", "description": "A set's items, each with its index, point, method, memory, work, time and energy." }
}
@ -248,10 +249,11 @@ pub fn openapi(server: &str) -> Value {
"200": { "description": "The finished job, or with dry_run the plan (DryRun)", "headers": { "RateLimit": { "$ref": "#/components/headers/RateLimit" }, "RateLimit-Policy": { "$ref": "#/components/headers/RateLimit-Policy" } }, "content": { "application/json": { "schema": { "oneOf": [{ "$ref": "#/components/schemas/Job" }, { "$ref": "#/components/schemas/DryRun" }] } } } },
"202": { "description": "Still running: poll GET /v1/jobs/{id}", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Job" } } } },
"400": { "description": "A program, job or Idempotency-Key that does not read", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
"422": { "description": "Larger than one job may be, or an Idempotency-Key sent before with another request (J034)" },
"422": { "description": "Larger than one job may be (J031: its plan, its result's bound past result_bytes, or its reading past read_steps, the reading child's memory or its time), or an Idempotency-Key sent before with another request (J034)" },
"401": { "description": "A private deployment's key missing or not one it holds (J033)" },
"429": { "description": "Past the per-client limits (with Retry-After)" },
"503": { "description": "The queue is full" }
"503": { "description": "The queue is full, or no turn to read it came (J032): nothing was read, and nothing counted" },
"500": { "description": "Its reading failed inside the service: nothing was admitted, and the service runs on" }
}
} },
"/v1/jobs": { "post": {
@ -262,18 +264,19 @@ pub fn openapi(server: &str) -> Value {
"202": { "description": "Accepted, and kept in the service's store before this answer; sent again with its Idempotency-Key, the key's job", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Job" } } } },
"200": { "description": "With dry_run: the plan, nothing run", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DryRun" } } } },
"400": { "description": "A program, job or Idempotency-Key that does not read", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
"422": { "description": "Larger than one job may be, or an Idempotency-Key sent before with another request (J034)" },
"422": { "description": "Larger than one job may be (J031: its plan, its result's bound past result_bytes, or its reading past read_steps, the reading child's memory or its time), or an Idempotency-Key sent before with another request (J034)" },
"401": { "description": "A private deployment's key missing or not one it holds (J033)" },
"429": { "description": "Past the per-client limits (with Retry-After)" },
"503": { "description": "The queue is full" }
"503": { "description": "The queue is full, or no turn to read it came (J032): nothing was read, and nothing counted" },
"500": { "description": "Its reading failed inside the service: nothing was admitted, and the service runs on" }
}
} },
"/v1/jobs/{id}": {
"parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
"get": {
"summary": "A job: its status, and its result when done. Kept across the service's restarts for its retention",
"parameters": [{ "name": "wait", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 25 }, "description": "Hold the answer until the job's status changes, or this many seconds pass." }],
"responses": { "200": { "description": "The job", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Job" } } } }, "404": { "description": "No such job" } }
"parameters": [{ "name": "wait", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 25 }, "description": "Hold the answer until the job's status changes, or this many seconds pass. A client holds at most WAI_QSVC_CLIENT_STREAMS (4) long polls and event streams at once: past them a poll of a job still queued or running is not held, and is answered 429 (503 past everyone's) with Retry-After; wait that long, or poll without wait." }],
"responses": { "200": { "description": "The job", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Job" } } } }, "404": { "description": "No such job" }, "429": { "description": "A long poll past its client's holds, not held (J032): {\"error\", \"code\", \"id\", \"status\"}, with Retry-After" }, "503": { "description": "A long poll past everyone's holds, not held (J032): {\"error\", \"code\", \"id\", \"status\"}, with Retry-After" } }
},
"delete": { "summary": "Cancel a queued or running job", "responses": { "200": { "description": "Cancelled" }, "409": { "description": "Already finished" } } }
},
@ -284,32 +287,32 @@ pub fn openapi(server: &str) -> Value {
"/v1/verify": { "post": {
"summary": "Whether a result's receipt seals that request and that result and every rule of its form holds ({\"valid\", \"reasons\", \"key\", \"form\"}); an estimate is also re-run, and each of its resource receipts checked ({\"valid\", \"signed\", \"receipts\", \"reproduced\"}); a set's receipt is checked over its head, every item as its request gives it, each item's own receipt, the root and each path ({\"valid\", \"signed\", \"derived\", \"items_sealed\", \"root\"})",
"requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["request", "result"], "properties": { "request": { "$ref": "#/components/schemas/JobRequest" }, "result": { "type": "object" } } } } } },
"responses": { "200": { "description": "{\"valid\": bool, \"reasons\": [string]}" }, "400": { "description": "The body is not JSON, not a job, or has no receipt" } }
"responses": { "200": { "description": "{\"valid\": bool, \"reasons\": [string]}" }, "400": { "description": "The body is not JSON, not a job, or has no receipt" }, "422": { "description": "A set whose sweep takes more to write out again than a request may read (J031)" }, "429": { "description": "A set, counted as a check is, past the per-client limits (with Retry-After)" }, "503": { "description": "A set, with every check running or your share of them (J032)" } }
} },
"/v1/critique": { "post": {
"summary": "A program's measures and smells, each with its lines",
"requestBody": { "required": true, "content": { "application/json": { "schema": prog.clone() } } },
"responses": { "200": { "description": "The critique, or {\"ok\": false, \"error\", \"line\"} for a program that does not read" }, "429": { "description": "Past the per-client limits" }, "503": { "description": "Every check is running" } }
"responses": { "200": { "description": "The critique, or {\"ok\": false, \"error\", \"line\"} for a program that does not read" }, "429": { "description": "Past the per-client limits" }, "503": { "description": "Every check is running, or your share of them (J032): nothing was read, and nothing counted" }, "422": { "description": "Larger than the service takes (J031): its bounds, the budget of reading, the reading child's memory or time, or an answer past what the service takes back" }, "500": { "description": "Its reading failed inside the service: nothing was answered, and the service runs on" } }
} },
"/v1/equivalent": { "post": {
"summary": "Whether two programs are the same circuit up to a global phase, and how that was decided",
"requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["a", "b"], "properties": { "a": { "type": "string" }, "b": { "type": "string" } } } } } },
"responses": { "200": { "description": "{\"verdict\": \"equivalent\" | \"different\" | \"unchecked\", \"how\", \"why\"}" }, "422": { "description": "A check longer than the service takes; the reason names the command that runs it locally" }, "429": { "description": "Past the per-client limits" }, "503": { "description": "Every check is running" } }
"responses": { "200": { "description": "{\"verdict\": \"equivalent\" | \"different\" | \"unchecked\", \"how\", \"why\"}" }, "422": { "description": "A check longer than the service takes; the reason names the command that runs it locally" }, "429": { "description": "Past the per-client limits" }, "503": { "description": "Every check is running, or your share of them (J032): nothing was read, and nothing counted" }, "500": { "description": "Its reading failed inside the service: nothing was answered, and the service runs on" } }
} },
"/v1/convert": { "post": {
"summary": "A program carried between formats (OpenQASM 3, QIR as LLVM text, jeff as base64), the output read back and held to it by the equivalence ladder",
"requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["to"], "properties": { "program": { "type": "string", "description": "The program's text: OpenQASM or QIR." }, "program_base64": { "type": "string", "description": "The program's bytes as base64: jeff." }, "from": { "enum": ["qasm", "qir", "jeff"] }, "to": { "enum": ["qasm", "qir", "jeff"] }, "profile": { "enum": ["base", "adaptive"] }, "check": { "type": "boolean" } } } } } },
"responses": { "200": { "description": "{\"ok\": true, \"format\", \"from\", \"to\", \"program\" | \"program_base64\", \"qasm\", \"notes\", \"read\", \"written\", \"check\"}, or the refusal {\"error\", \"code\", \"codes\", \"outcome\", \"meaning\"} with the reader's code (Q700 to Q721) and place" }, "422": { "description": "A program or a check larger than the service takes" }, "429": { "description": "Past the per-client limits" }, "503": { "description": "Every check is running" } }
"responses": { "200": { "description": "{\"ok\": true, \"format\", \"from\", \"to\", \"program\" | \"program_base64\", \"qasm\", \"notes\", \"read\", \"written\", \"check\"}, or the refusal {\"error\", \"code\", \"codes\", \"outcome\", \"meaning\"} with the reader's code (Q700 to Q721) and place" }, "422": { "description": "A program or a check larger than the service takes" }, "429": { "description": "Past the per-client limits" }, "503": { "description": "Every check is running, or your share of them (J032): nothing was read, and nothing counted" }, "500": { "description": "Its reading failed inside the service: nothing was answered, and the service runs on" } }
} },
"/v1/improve": { "post": {
"summary": "The program with its smells fixed, each candidate checked the same circuit",
"requestBody": { "required": true, "content": { "application/json": { "schema": prog.clone() } } },
"responses": { "200": { "description": "{\"base\", \"candidates\": [{\"label\", \"qasm\", \"changes\", \"measures\", \"equivalence\"}]}" }, "422": { "description": "Checks longer than the service takes" }, "429": { "description": "Past the per-client limits" } }
"responses": { "200": { "description": "{\"base\", \"candidates\": [{\"label\", \"qasm\", \"changes\", \"measures\", \"equivalence\"}]}" }, "422": { "description": "Checks longer than the service takes" }, "429": { "description": "Past the per-client limits" }, "503": { "description": "Every check is running, or your share of them (J032): nothing was read, and nothing counted" }, "500": { "description": "Its reading failed inside the service: nothing was answered, and the service runs on" } }
} },
"/v1/shortcuts": { "post": {
"summary": "Each exact classical route to simulating a program, priced on it, and the cheapest for each question",
"requestBody": { "required": true, "content": { "application/json": { "schema": prog } } },
"responses": { "200": { "description": "{\"routes\", \"cheapest\", \"summary\", \"note\"}" }, "422": { "description": "A thorough search, which runs locally" }, "429": { "description": "Past the per-client limits" } }
"responses": { "200": { "description": "{\"routes\", \"cheapest\", \"summary\", \"note\"}" }, "422": { "description": "A thorough search, which runs locally" }, "429": { "description": "Past the per-client limits" }, "503": { "description": "Every check is running, or your share of them (J032): nothing was read, and nothing counted" }, "500": { "description": "Its reading failed inside the service: nothing was answered, and the service runs on" } }
} },
"/v1/compile": { "post": {
"summary": "The program compiled through checked passes, with a certificate of each pass and the distance, sealed with the service's key",
@ -319,7 +322,8 @@ pub fn openapi(server: &str) -> Value {
"400": { "description": "A program or options that do not read" },
"422": { "description": "A compile the service does not take (too many gates or rotations, or ε too fine for them: the reason names the local command), or one the engine refuses" },
"429": { "description": "Past the per-client limits" },
"503": { "description": "Every check is running" }
"503": { "description": "Every check is running, or your share of them (J032): nothing was read, and nothing counted" },
"500": { "description": "Its reading failed inside the service: nothing was answered, and the service runs on" }
}
} },
"/v1/estimate": { "post": {
@ -330,13 +334,14 @@ pub fn openapi(server: &str) -> Value {
"400": { "description": "A request, program or document that does not read" },
"422": { "description": "An estimate the service does not take (too many gates or distinct rotation angles, or a budget too fine for its rotations: the reason names the local command), or one the engine refuses (its code, QE02 to QE08)" },
"429": { "description": "Past the per-client limits" },
"503": { "description": "Every check is running" }
"503": { "description": "Every check is running, or your share of them (J032): nothing was read, and nothing counted" },
"500": { "description": "Its reading failed inside the service: nothing was answered, and the service runs on" }
}
} },
"/v1/repair": { "post": {
"summary": "The program repaired on the lines its diagnostics flag alone: the mechanical fixes, a proposed repair held to those lines, and what needs a person",
"requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["qasm"], "properties": { "qasm": { "type": "string" }, "proposed": { "type": "string", "description": "A repaired version: taken only if it changes no line the diagnostics do not flag." }, "max_iterations": { "type": "integer", "minimum": 1 }, "dyadic_bits": { "type": "integer", "minimum": 1, "maximum": 32 }, "sizes": { "type": "object", "additionalProperties": { "type": "integer" } }, "diagnostics": { "type": "array" }, "rules": { "type": "boolean" } } } } } },
"responses": { "200": { "description": "{\"ok\", \"changed\", \"qasm\", \"repairs\": [{\"code\", \"rule\", \"by\", \"flagged\", \"edits\"}], \"needs_person\", \"verdict\"}, or {\"ok\": false, \"error\", \"refusal\"} for a proposal rejected (J021)" }, "429": { "description": "Past the per-client limits" }, "503": { "description": "Every check is running" } }
"responses": { "200": { "description": "{\"ok\", \"changed\", \"qasm\", \"repairs\": [{\"code\", \"rule\", \"by\", \"flagged\", \"edits\"}], \"needs_person\", \"verdict\"}, or {\"ok\": false, \"error\", \"refusal\"} for a proposal rejected (J021)" }, "429": { "description": "Past the per-client limits" }, "503": { "description": "Every check is running, or your share of them (J032): nothing was read, and nothing counted" }, "422": { "description": "Larger than the service takes (J031): its bounds, the budget of reading, the reading child's memory or time, or an answer past what the service takes back" }, "500": { "description": "Its reading failed inside the service: nothing was answered, and the service runs on" } }
} },
"/mcp": { "post": { "summary": "The Model Context Protocol endpoint (streamable HTTP, JSON responses): tools to run circuits and sets, plan a job without running it, read jobs, verify receipts, list examples, and critique, compare, improve, price, compile, estimate and repair programs", "responses": { "200": { "description": "A JSON-RPC response" } } } }
}
@ -344,12 +349,31 @@ pub fn openapi(server: &str) -> Value {
for (path, item) in qec_paths().into_iter().chain(pulse_paths()).chain(variational_paths()) {
doc["paths"][path] = item;
}
// What every route can answer before its handler runs: a body that did
// not arrive in time, one past the size a request may be, and a client
// with as many requests open as it may have, or a service with as many
// connections as it holds.
let before = [
("408", "The body did not arrive within WAI_QSVC_BODY_SECS (30 s)"),
("413", "The body is larger than the 1 MiB a request may be"),
("503", "The client has WAI_QSVC_CLIENT_REQUESTS (32) requests open at once, or the service holds as many connections as it may (J032); Retry-After says when to try again"),
];
if let Some(paths) = doc["paths"].as_object_mut() {
for item in paths.values_mut() {
for op in item.as_object_mut().into_iter().flat_map(|o| o.values_mut()) {
let Some(responses) = op.get_mut("responses").and_then(Value::as_object_mut) else { continue };
for (code, why) in before {
responses.entry(code).or_insert_with(|| json!({ "description": why, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }));
}
}
}
}
doc
}
/// The pulse calls' paths.
fn pulse_paths() -> Vec<(&'static str, Value)> {
let responses = |ok: &str| json!({ "200": { "description": ok }, "400": { "description": "A body that does not read" }, "422": { "description": "A program the pulse reader refuses (its Q code), a request the engine refuses (J040, J041), or a check larger than the service takes (J031, naming the local command)" }, "429": { "description": "Past the per-client limits" }, "503": { "description": "Every check is running" } });
let responses = |ok: &str| json!({ "200": { "description": ok }, "400": { "description": "A body that does not read" }, "422": { "description": "A program the pulse reader refuses (its Q code), a request the engine refuses (J040, J041), or a check larger than the service takes (J031, naming the local command)" }, "429": { "description": "Past the per-client limits" }, "503": { "description": "Every check is running, or your share of them (J032): nothing was read, and nothing counted" }, "500": { "description": "Its reading failed inside the service: nothing was answered, and the service runs on" } });
vec![
("/v1/pulse/schedule", json!({ "post": {
"summary": "A pulse program's schedule on frames: every play, delay, phase and frequency change, capture and barrier at its sample, each waveform's samples, the gate calls, and the digests a pulse run's receipt binds; sealed with the service's key",
@ -367,7 +391,7 @@ fn pulse_paths() -> Vec<(&'static str, Value)> {
/// The variational and algorithm library's paths, apart, so the document's
/// macro stays within its depth.
fn variational_paths() -> Vec<(&'static str, Value)> {
let responses = |ok: &str| json!({ "200": { "description": ok }, "400": { "description": "A body that does not read" }, "422": { "description": "A request the engine refuses (J050 params, J051 a gradient, J052 an optimisation, J053 a builder, Q129 a parameter it cannot follow) or larger than the service takes (J031, naming the local command)" }, "429": { "description": "Past the per-client limits" }, "503": { "description": "Every check is running" } });
let responses = |ok: &str| json!({ "200": { "description": ok }, "400": { "description": "A body that does not read" }, "422": { "description": "A request the engine refuses (J050 params, J051 a gradient, J052 an optimisation, J053 a builder, Q129 a parameter it cannot follow) or larger than the service takes (J031, naming the local command)" }, "429": { "description": "Past the per-client limits" }, "503": { "description": "Every check is running, or your share of them (J032): nothing was read, and nothing counted" }, "500": { "description": "Its reading failed inside the service: nothing was answered, and the service runs on" } });
let body = || json!({ "required": true, "content": { "application/json": { "schema": { "type": "object" } } } });
vec![
("/v1/gradient", json!({ "post": {
@ -401,7 +425,7 @@ fn variational_paths() -> Vec<(&'static str, Value)> {
/// The error-correction calls' paths, apart, so the document's macro stays
/// within its depth.
fn qec_paths() -> Vec<(&'static str, Value)> {
let responses = |ok: &str| json!({ "200": { "description": ok }, "400": { "description": "A body that does not read" }, "422": { "description": "A request the engine refuses (J009) or larger than the service takes (J031, naming the local command)" }, "429": { "description": "Past the per-client limits" }, "503": { "description": "Every check is running" } });
let responses = |ok: &str| json!({ "200": { "description": ok }, "400": { "description": "A body that does not read" }, "422": { "description": "A request the engine refuses (J009) or larger than the service takes (J031, naming the local command)" }, "429": { "description": "Past the per-client limits" }, "503": { "description": "Every check is running, or your share of them (J032): nothing was read, and nothing counted" }, "500": { "description": "Its reading failed inside the service: nothing was answered, and the service runs on" } });
vec![
("/v1/qec/code", json!({ "post": {
"summary": "An error-correcting code (surface:D, toric:L, steane, color:D, bb:72 to bb:288, gross): its checks, stabilizers, k, layout, the decoders it takes, and its distance certificate (a witness and every lighter support ruled out); with a certificate, its check; sealed with the service's key",
@ -494,12 +518,21 @@ mod tests {
#[test]
fn the_openapi_document_names_every_route() {
let d = openapi("https://example.org/q");
let d = openapi("https://example.org/q", 64 << 20);
for p in ["/v1", "/v1/methods", "/v1/examples", "/v1/targets", "/v1/run", "/v1/jobs", "/v1/jobs/{id}", "/v1/jobs/{id}/events", "/.well-known/wai-quantum-receipt-keys", "/v1/verify", "/v1/compile", "/v1/estimate", "/v1/convert", "/v1/qec/code", "/v1/qec/decode", "/v1/qec/threshold", "/v1/gradient", "/v1/optimize", "/v1/vqe", "/v1/qaoa", "/v1/build", "/v1/pulse/schedule", "/v1/pulse/check", "/mcp"] {
assert!(d["paths"].get(p).is_some(), "{p}");
}
assert_eq!(d["openapi"], "3.1.0");
// What any route answers before its handler: a body late, too large,
// or a client or the service with as many open as it may have.
for (path, item) in d["paths"].as_object().unwrap() {
for (method, op) in item.as_object().unwrap().iter().filter(|(m, _)| ["get", "post", "delete"].contains(&m.as_str())) {
for code in ["408", "413", "503"] {
assert!(op["responses"].get(code).is_some(), "{method} {path} lists no {code}");
}
}
}
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -11,6 +11,8 @@
//! wai-quantum-service serve the HTTP API (WAI_QSVC_ADDR, 127.0.0.1:8241)
//! wai-quantum-service worker one job: {"request", "plan"} (or a set's
//! {"set": [...]}) on stdin, the result on stdout
//! wai-quantum-service read one request's reading (crate::offload::Read)
//! on stdin, its answer on stdout
//! wai-quantum-service run FILE one job or set from a JSON file, here, printed
//! wai-quantum-service rotate-key retire the receipt key and make a new one
//! wai-quantum-service api-key NAME a new API key for a private deployment
@ -30,6 +32,7 @@ mod energy;
mod http;
mod jobs;
mod mcp;
mod offload;
use wai_quantum::engine::{catalog, run, sets};
mod receipt;
mod store;
@ -42,6 +45,11 @@ fn main() {
let args: Vec<String> = std::env::args().collect();
match args.get(1).map(String::as_str) {
Some("worker") => worker(),
Some("read") => {
watch_parent();
jobs::expendable();
offload::child()
}
Some("run") => match args.get(2) {
Some(path) => run_file(path),
None => usage(),
@ -90,6 +98,7 @@ fn watch_parent() {
/// `{"items": [{"result"} | {"error"}, ...]}`.
fn worker() {
watch_parent();
jobs::expendable();
let mut input = String::new();
if let Err(e) = std::io::stdin().read_to_string(&mut input) {
println!("{}", serde_json::json!({ "error": format!("the job did not arrive: {e}") }));
@ -99,11 +108,29 @@ fn worker() {
let out = match serde_json::from_str::<serde_json::Value>(&input) {
Ok(v) if v.get("set").is_some() => {
let pairs: Option<Vec<(serde_json::Value, catalog::Plan)>> = v["set"].as_array().and_then(|a| a.iter().map(|it| Some((it.get("request")?.clone(), plan_of(&it["plan"])?))).collect());
drop(v);
match pairs {
Some(pairs) => serde_json::json!({ "items": sets::run_items(&pairs, HOST).into_iter().map(|o| match o {
Ok(r) => serde_json::json!({ "result": r }),
Err(e) => serde_json::json!({ "error": e }),
}).collect::<Vec<_>>() }),
// Each item's result written out as it ends, and its tree
// dropped: a counted outcome takes about 21 bytes as text and
// ten times that as a tree, so a set of a hundred items held
// as trees passed the memory its largest item was admitted
// with. The set holds one item's tree at a time, and the text.
Some(pairs) => {
let mut text = String::from("{\"items\":[");
for (i, (request, plan)) in pairs.iter().enumerate() {
if i > 0 {
text.push(',');
}
let item = match run::run_with_provenance(request, plan, HOST) {
Ok(r) => serde_json::json!({ "result": r }),
Err(e) => serde_json::json!({ "error": e }),
};
text.push_str(&item.to_string());
}
text.push_str("]}");
println!("{text}");
return;
}
None => serde_json::json!({ "error": "the set lacks an item's request or plan" }),
}
}
@ -163,9 +190,77 @@ fn run_file(path: &str) {
println!("{}", serde_json::to_string_pretty(&serde_json::json!({ "plan": plan, "result": result })).expect("JSON"));
}
/// On macOS, the allocator keeps large blocks it freed, dirty, in a cache
/// charged to the process: a parsed request of a megabyte takes tens of
/// megabytes in blocks that large, so each one read stayed in the service's
/// footprint (measured on arm64: 30 requests of a megabyte, 1 GiB kept; with
/// the cache off, 2 MiB). The allocator reads its settings as the process
/// starts, so the service starts itself again with the cache off, as the same
/// process (`exec`); its readers and workers inherit the setting.
#[cfg(target_os = "macos")]
fn without_large_cache() {
use std::os::unix::process::CommandExt;
if std::env::var_os("MallocLargeCache").is_some() {
return;
}
let Ok(exe) = std::env::current_exe() else { return };
let e = std::process::Command::new(exe).args(std::env::args_os().skip(1)).env("MallocLargeCache", "0").exec();
eprintln!("starting again with the allocator's large cache off: {e}; going on with it on");
}
/// The open-file limit raised as far as the system lets this process: its
/// soft limit to its hard one (on macOS no further than `kern.maxfilesperproc`
/// and `OPEN_MAX`, as an infinite limit is refused there). Logged, and the
/// limit it ends at returned: the connections the service holds are set
/// under it ([`http::serve`]).
fn open_files() -> u64 {
// SAFETY: getrlimit and setrlimit read and write the struct given, and
// sysctlbyname writes at most the size it is told into the integer given.
unsafe {
let mut l = libc::rlimit { rlim_cur: 0, rlim_max: 0 };
if libc::getrlimit(libc::RLIMIT_NOFILE, &mut l) != 0 {
eprintln!("open files: the limit could not be read; connections are held under 256");
return 256;
}
let before = l.rlim_cur;
#[allow(unused_mut)]
let mut want = l.rlim_max;
#[cfg(target_os = "macos")]
{
const OPEN_MAX: libc::rlim_t = 10240;
let mut per: libc::c_int = 0;
let mut size = std::mem::size_of::<libc::c_int>();
if libc::sysctlbyname(c"kern.maxfilesperproc".as_ptr(), (&raw mut per).cast(), &mut size, std::ptr::null_mut(), 0) == 0 && per > 0 {
want = want.min(per as libc::rlim_t);
}
want = want.min(OPEN_MAX);
}
if want > before {
let raised = libc::rlimit { rlim_cur: want, rlim_max: l.rlim_max };
if libc::setrlimit(libc::RLIMIT_NOFILE, &raised) == 0 {
eprintln!("open files: {want} (raised from {before}; the hard limit is {})", l.rlim_max);
return want;
}
eprintln!("open files: {before}; raising it to {want} was refused: {}", std::io::Error::last_os_error());
return before;
}
eprintln!("open files: {before}, the hard limit");
before
}
}
fn serve() {
#[cfg(target_os = "macos")]
without_large_cache();
let files = open_files();
let config = jobs::Config::from_env();
let key_path = receipt::default_path();
// Readers and workers start from what this keeps, so the binary can be
// replaced before the restart.
let bin = key_path.parent().map(|d| d.join("bin")).unwrap_or_else(|| std::path::PathBuf::from("bin"));
if let Err(e) = jobs::keep_self(&bin) {
eprintln!("keeping the service's binary for its readers and workers: {e}; they start from the file it was started from");
}
let fail = |what: &str, e: String| -> ! {
eprintln!("{what}: {e}");
std::process::exit(1)
@ -211,8 +306,18 @@ fn serve() {
eprintln!("binding {addr}: {e}");
std::process::exit(1)
});
eprintln!("wai-quantum-service {} on {addr}, receipt key {}", env!("CARGO_PKG_VERSION"), app.signer.public_key());
// The address bound, which names its port where port 0 asked for any.
let bound = listener.local_addr().unwrap_or(addr);
eprintln!("wai-quantum-service {} on {bound}, receipt key {}", env!("CARGO_PKG_VERSION"), app.signer.public_key());
let router = http::router(Arc::clone(&app));
axum::serve(listener, router.into_make_service_with_connect_info::<SocketAddr>()).await.expect("the server");
let header_wait = app.service.config.header_wait;
// Connections held under the open-file limit, with room beside them
// for readers' and workers' pipes, the store's files and the listener.
let max = app.service.config.connections.min(files.saturating_sub((files / 4).max(64)) as usize).max(16);
// Long polls and event streams held to a quarter of the connections.
let streams = app.service.config.streams.min(max / 4).max(1);
app.service.stream_cap.store(streams, std::sync::atomic::Ordering::SeqCst);
eprintln!("connections: at most {max} at once, {} open requests a client; {streams} long polls and event streams, {} a client", app.service.config.client_requests, app.service.config.client_streams);
http::serve(listener, router, header_wait, max).await;
});
}

View file

@ -82,13 +82,46 @@ fn output_of(tool: &str) -> Option<&'static str> {
})
}
/// What an answer's verdict may read. Reading costs what reading a request
/// costs, so it is done in a reading child, for a request that was counted;
/// a poll, or a call that was refused before it was counted, reads nothing.
#[derive(Clone, Copy, Debug)]
pub enum Reading<'a> {
/// The programs, again: the verdict of what they say.
Read,
/// What the job holds: admitted, it read, and a poll reads it no more.
Stored,
/// Nothing: the verdict says why it is not of the program.
Unread(&'a str),
}
impl Reading<'_> {
/// The verdict of `request` refused for `error`.
fn of_refusal(self, request: &Value, error: &str) -> Value {
match self {
Reading::Read => verdict::of_refusal(request, error),
Reading::Stored => verdict::of_admitted(Some(error)),
Reading::Unread(_) => verdict::of_refusal(&Value::Null, error),
}
}
/// The verdict of the program `src`.
fn of_text(self, src: &str) -> Value {
match self {
Reading::Read => verdict::of_text(src),
Reading::Stored => verdict::of_admitted(None),
Reading::Unread(why) => verdict::of_refusal(&Value::Null, why),
}
}
}
/// The verdict a dry run states: of the refusal it would give, or of what
/// it read (each item of a set).
fn dry_run_verdict(sc: &Value, args: &Value) -> Option<Value> {
fn dry_run_verdict(sc: &Value, args: &Value, reading: Reading) -> Option<Value> {
Some(match sc["refusal"]["message"].as_str().or_else(|| sc["error"].as_str()) {
Some(e) => verdict::of_refusal(args, e),
None if wai_quantum::engine::catalog::is_set(args) => set_verdict(&json!({ "request": args })).unwrap_or(Value::Null),
None => verdict::of_text(args["qasm"].as_str().unwrap_or("")),
Some(e) => reading.of_refusal(args, e),
None if wai_quantum::engine::catalog::is_set(args) => set_verdict(&json!({ "request": args }), reading).unwrap_or(Value::Null),
None => reading.of_text(args["qasm"].as_str().unwrap_or("")),
})
}
@ -114,6 +147,9 @@ pub fn versions() -> Value {
/// How long a `run_quantum_circuit` call waits before handing back the job id.
pub const CALL_WAIT: Duration = Duration::from_secs(50);
/// What an answer keeps, of its time, to read its verdict after a run.
pub const VERDICT_TIME: Duration = Duration::from_secs(5);
const INSTRUCTIONS: &str = "Run quantum programs written in OpenQASM 3 (or 2). Call run_quantum_circuit with the program and what you want back: shots for measurement counts, observables (Pauli strings such as \"Z0 Z1\") for expectation values, amplitudes (bit strings, qubit n-1 first) for amplitudes, and `intent` for what the program must do. The service picks the strongest method the circuit allows and says which and why; every result names its accuracy, states its verdict (how far the engine checked: proof, evidence, executed, parsed or refused) and carries a signature verify_quantum_receipt can check. Counts are keyed by the classical bits, the highest bit first. If a call answers with status \"running\", call get_quantum_job with its id. A client that speaks tasks (MCP 2025-11-25) may call the three runs as tasks: the task is the job, its id the job's, and tasks/get, tasks/result and tasks/cancel follow it. run_quantum_set runs several programs, or one over a grid of its inputs, as one set with one receipt; dry_run_quantum_job says what a job or a set would take (method, memory, time, energy, bounds) and whether the service would take it, running nothing. quantum_circuit_examples gives ready-to-run programs.";
/// What `initialize` tells an agent: how to use the service, then the
@ -272,7 +308,7 @@ fn tool_list() -> Value {
{
"name": "dry_run_quantum_job",
"title": "Plan a quantum job or set without running it",
"description": "What the service would do with a job or a set, running nothing: the method and why, the memory, the estimated time and energy (each with its basis: this service's own timings and readings where it has them, otherwise the planner's nominal rate, and no energy figure without readings), the bounds that apply, where you stand against your limits, and whether the service would take it now, or the refusal it would give, with its code. Takes what run_quantum_circuit or run_quantum_set takes. It counts as one submission.",
"description": "What the service would do with a job or a set, running nothing: the method and why, the memory, the estimated time and energy (each with its basis: this service's own timings and readings where it has them, otherwise the planner's nominal rate, and no energy figure without readings), the bounds that apply, where you stand against your limits, and whether the service would take it now, or the refusal it would give, with its code. Takes what run_quantum_circuit or run_quantum_set takes, and plans it as they run it, its verdict asked: what it says they take, they take. It counts as one submission.",
"inputSchema": { "type": "object", "properties": plan_props },
"annotations": { "readOnlyHint": true }
},
@ -419,18 +455,19 @@ pub use wai_quantum::engine::views::job_text as summary;
/// A set's verdict: its weakest item's (an item not yet run, or whose run
/// stated none, what the reader said of it), as the set's, the intents
/// holding only if every item's do.
pub fn set_verdict(job: &Value) -> Option<Value> {
pub fn set_verdict(job: &Value, reading: Reading) -> Option<Value> {
let each: Vec<Value> = match job.pointer("/result/items").and_then(Value::as_array) {
Some(list) => list
.iter()
.map(|it| {
it.pointer("/result/verdict").cloned().unwrap_or_else(|| match it["error"].as_str() {
Some(e) => verdict::of_refusal(&it["request"], e),
None => verdict::of_text(it["request"]["qasm"].as_str().unwrap_or("")),
Some(e) => reading.of_refusal(&it["request"], e),
None => reading.of_text(it["request"]["qasm"].as_str().unwrap_or("")),
})
})
.collect(),
None => wai_quantum::engine::sets::expand(job.get("request").unwrap_or(job)).ok()?.iter().map(|it| verdict::of_text(it.request["qasm"].as_str().unwrap_or(""))).collect(),
None if matches!(reading, Reading::Read) => wai_quantum::engine::sets::expand(job.get("request").unwrap_or(job)).ok()?.iter().map(|it| verdict::of_text(it.request["qasm"].as_str().unwrap_or(""))).collect(),
None => vec![reading.of_text("")],
};
let n = each.len();
let holds = if each.iter().any(|v| v["holds"] == false) {
@ -454,6 +491,46 @@ fn fail(id: &Value, code: i64, message: &str) -> Value {
json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } })
}
/// The answer to a message over 64 KiB from a client past its window, read
/// only for its id, its method, and its tool's name and key (the rest passed
/// over, unread): a tool result under the message's own id, stating why. A
/// resend under a key the client holds, and a receipt checked within the
/// client's own window for checks, are read whole as any message is: `None`.
pub fn past_window(app: &App, who: &str, body: &[u8], why: &str) -> Option<Value> {
#[derive(serde::Deserialize)]
struct Args {
idempotency_key: Option<String>,
}
#[derive(serde::Deserialize)]
struct Params {
name: Option<String>,
arguments: Option<Args>,
}
#[derive(serde::Deserialize)]
struct Message {
id: Option<Value>,
method: Option<String>,
params: Option<Params>,
}
// Not a message: read whole, it is answered as one that is not.
let m: Message = serde_json::from_slice(body).ok()?;
let id = m.id.unwrap_or(Value::Null);
if m.method.as_deref() != Some("tools/call") {
return Some(fail(&id, -32000, why));
}
let (name, key) = m.params.map(|p| (p.name.unwrap_or_default(), p.arguments.and_then(|a| a.idempotency_key))).unwrap_or_default();
if key.is_some_and(|k| app.service.holds_key(who, &k)) {
return None;
}
if name == "verify_quantum_receipt" {
return match app.service.verify_window(who, false) {
Ok(()) => None,
Err(crate::jobs::Refused(_, w)) => Some(ok(&id, tool_result(format!("refused: {w}"), json!({ "error": w }), true))),
};
}
Some(ok(&id, finish(&name, &Value::Null, tool_result(format!("refused: {why}"), json!({ "error": why }), true), false, Reading::Unread(why))))
}
/// A tool's answer: text for the model, the whole object as structured content.
fn tool_result(text: String, structured: Value, is_error: bool) -> Value {
json!({ "content": [{ "type": "text", "text": text }], "structuredContent": structured, "isError": is_error })
@ -511,7 +588,7 @@ fn task_job(app: &App, who: &str, task: &str) -> Option<Value> {
if app.keys.is_some() && !app.service.owned_by(task, who) {
return None;
}
app.service.get(task)
app.service.peek(task)
}
fn no_task(id: &Value, app: &App) -> Value {
@ -519,12 +596,30 @@ fn no_task(id: &Value, app: &App) -> Value {
}
/// A run's job admitted as a task: its id, or the refusal as data.
fn admit_task(app: &App, who: &str, name: &str, args: Value) -> Result<String, String> {
async fn admit_task(app: &App, who: &str, name: &str, args: Value) -> Result<String, String> {
let (args, key) = take_key(args)?;
if let Some(r) = run_refusal(name, &args) {
return Err(r["structuredContent"]["error"].as_str().unwrap_or("refused").to_string());
}
admit_run(app, who, args, key.as_deref()).map_err(|r| r["structuredContent"]["error"].as_str().unwrap_or("refused").to_string())
admit_run(app, who, args, key).await.map_err(|r| r["structuredContent"]["error"].as_str().unwrap_or("refused").to_string())
}
/// The verdict of a task that was refused, `why`. It reads the program only
/// for a request that was counted and read before: refused before that, or
/// told to try again, it reads nothing.
async fn task_verdict(app: &App, who: &str, args: Value, why: String, counted: bool) -> Value {
if !counted || told_to_wait(&why) {
return verdict::of_refusal(&Value::Null, &why);
}
let Ok(turn) = app.service.gate.turn(crate::offload::Pool::Reading, who).await else { return verdict::of_refusal(&Value::Null, &why) };
let unread = verdict::of_refusal(&Value::Null, &why);
crate::offload::read(&app.service.config, turn, crate::offload::Read::Refusal { args, why }).await.unwrap_or(unread)
}
/// Whether `why` tells the client to come back: nothing of its request was
/// read, or the service is too busy to read more of it.
fn told_to_wait(why: &str) -> bool {
[wai_quantum::engine::refusal::RATE, wai_quantum::engine::refusal::BUSY].iter().any(|code| why.ends_with(&format!("[{code}]")))
}
/// What `tasks/result` answers for a task that has ended: the answer
@ -550,29 +645,75 @@ fn run_refusal(name: &str, args: &Value) -> Option<Value> {
}
/// A run's job admitted as the run tools admit it, its verdict asked: its
/// id, or the tool's refusal.
fn admit_run(app: &App, who: &str, args: Value, key: Option<&str>) -> Result<String, Value> {
/// id, or the tool's refusal. It is admitted as `POST /v1/jobs` admits one
/// (`http::admit`): counted before anything is read, and read in a child.
async fn admit_run(app: &App, who: &str, args: Value, key: Option<String>) -> Result<String, Value> {
let refused = |why: String| tool_result(format!("refused: {why}"), json!({ "error": why }), true);
// Every run an agent asks for states its verdict.
let mut args = crate::http::with_source(args).map_err(|(_, why)| refused(why))?;
if !verdict::asked(&args) {
if !args.is_object() {
return Err(refused(wai_quantum::engine::refusal::tag(wai_quantum::engine::refusal::BAD_FIELD, "a run's arguments are an object: {\"qasm\": …, \"shots\": …}")));
}
crate::http::admit(app, who, as_run(args), key).await.map_err(|(_, why)| refused(why))
}
/// A run tool's arguments as the job it admits: every run an agent asks for
/// states its verdict. A dry run of the tools plans the same job
/// ([`dry_run_answer`]), so what it says is taken is taken.
fn as_run(mut args: Value) -> Value {
if args.is_object() && !verdict::asked(&args) {
args["verdict"] = json!(true);
}
crate::http::admit(app, who, args, key.map(crate::jobs::Key)).map_err(|(_, why)| refused(why))
args
}
/// A tool's answer as `tools/call` gives it: the call's, the verdict of
/// what it read beside it, and what a view draws, for a host that draws it.
/// Reading the programs again costs what reading them cost, so only a call
/// that was counted does, in a reading child ([`crate::offload`]) within one
/// budget. A tool that states nothing about a program takes no turn. A poll
/// states what its job holds. A call refused before it was counted, told to
/// try again, or that finds no turn, states why its verdict is not of the
/// program: every answer about a program has a verdict.
async fn tool_answer(app: &App, who: &str, name: &str, args: Value, base: &str, views: bool) -> Value {
let mut answer = call(app, who, name, args.clone(), base).await;
let (answer, counted) = crate::jobs::noting_count(call(app, who, name, args.clone(), base)).await;
if !states_verdict(name) && !(views && views::for_tool(name).is_some()) {
return answer;
}
let sc = &answer["structuredContent"];
let error = sc["error"].as_str().filter(|_| answer["isError"] == true).map(str::to_string);
// A job's answer, polled or sent again, or with its verdict in hand.
let job = sc.get("id").is_some() && sc.get("status").is_some();
if job && (!counted || (sc.pointer("/result/verdict").is_some() && !views) || matches!(name, "get_quantum_job" | "poll_quantum_job")) {
return finish(name, &args, answer, views, Reading::Stored);
}
if !counted || error.as_deref().is_some_and(told_to_wait) {
let why = error.unwrap_or_else(|| "this answer was not read again".to_string());
return finish(name, &args, answer, views, Reading::Unread(&why));
}
// No time left within the answer to read the program again: said so.
if crate::offload::left().is_some_and(|l| l < Duration::from_secs(1)) {
return finish(name, &args, answer, views, Reading::Unread("no time was left within this answer to read the program again; poll the job for its verdict"));
}
let finished = match app.service.gate.turn(crate::offload::Pool::Reading, who).await {
Ok(turn) => crate::offload::read(&app.service.config, turn, crate::offload::Read::Finish { name: name.to_string(), args: args.clone(), answer: answer.clone(), views }).await,
Err(e) => Err(e),
};
match finished {
Ok(v) => v,
Err((_, why)) => finish(name, &args, answer, views, Reading::Unread(&why)),
}
}
/// [`tool_answer`]'s verdicts and view added to the call's `answer`, reading
/// what `reading` says it may.
pub(crate) fn finish(name: &str, args: &Value, mut answer: Value, views: bool, reading: Reading) -> Value {
// A set's answer, and a dry run's, state the verdict of what they read.
let sc = &answer["structuredContent"];
let extra = match name {
"run_quantum_circuit" | "run_on_target" | "run_quantum_set" if crate::http::is_dry_run(&args) && sc.get("dry_run").is_some() => dry_run_verdict(sc, &args),
"run_quantum_set" if sc.is_object() && sc.get("error").is_none() => set_verdict(sc),
"run_quantum_set" => sc["error"].as_str().map(|e| verdict::of_refusal(&args, e)),
"get_quantum_job" if sc["method"] == "set" => set_verdict(sc),
"dry_run_quantum_job" => dry_run_verdict(sc, &args),
"run_quantum_circuit" | "run_on_target" | "run_quantum_set" if crate::http::is_dry_run(args) && sc.get("dry_run").is_some() => dry_run_verdict(sc, args, reading),
"run_quantum_set" if sc.is_object() && sc.get("error").is_none() => set_verdict(sc, reading),
"run_quantum_set" => sc["error"].as_str().map(|e| reading.of_refusal(args, e)),
"get_quantum_job" if sc["method"] == "set" => set_verdict(sc, reading),
"dry_run_quantum_job" => dry_run_verdict(sc, args, reading),
_ => None,
};
if let Some(v) = extra.filter(|v| !v.is_null()) {
@ -581,7 +722,7 @@ async fn tool_answer(app: &App, who: &str, name: &str, args: Value, base: &str,
// What the view draws, to a host that draws it.
if views
&& answer["structuredContent"].is_object()
&& let Some(d) = views::data(name, &args, &answer["structuredContent"])
&& let Some(d) = views::data_of(name, args, &answer["structuredContent"], matches!(reading, Reading::Read))
{
answer["_meta"] = Value::Object([(views::META.to_string(), d)].into_iter().collect());
}
@ -593,12 +734,16 @@ async fn tool_answer(app: &App, who: &str, name: &str, args: Value, base: &str,
Some(e) if answer["isError"] == true => {
let mut r = wai_quantum::engine::refusal::refusal_json(e);
r["error"] = json!(e);
r["verdict"] = verdict::of_refusal(&args, e);
r["verdict"] = reading.of_refusal(args, e);
answer["structuredContent"] = r;
None
}
_ if name == "get_quantum_job" && sc["method"] == "set" => None,
_ => verdict::for_tool(name, &args, sc),
_ => match reading {
Reading::Read => verdict::for_tool(name, args, sc),
// What the answer holds, or what can be said without reading.
_ => sc.pointer("/result/verdict").or_else(|| sc.get("verdict").filter(|v| v.is_object())).cloned().or_else(|| Some(reading.of_text(""))),
},
};
if let Some(v) = v {
answer["structuredContent"][verdict::verdict_key(name)] = v;
@ -680,11 +825,11 @@ pub async fn handle(app: &App, who: &str, msg: &Value, base: &str, views: bool)
if crate::http::is_dry_run(&args) {
return Some(fail(&id, -32602, "a dry run runs nothing and answers at once: call it without a task"));
}
match admit_task(app, who, name, args.clone()) {
Ok(job) => ok(&id, json!({ "task": task_of(&app.service.get(&job).unwrap_or(json!({ "id": job })), app.service.config.retain.as_millis() as u64) })),
Err(why) => {
match crate::jobs::noting_count(admit_task(app, who, name, args.clone())).await {
(Ok(job), _) => ok(&id, json!({ "task": task_of(&app.service.peek(&job).unwrap_or(json!({ "id": job })), app.service.config.retain.as_millis() as u64) })),
(Err(why), counted) => {
let mut data = wai_quantum::engine::refusal::refusal_json(&why);
data["verdict"] = verdict::of_refusal(&args, &why);
data["verdict"] = task_verdict(app, who, args, why.clone(), counted).await;
json!({ "jsonrpc": "2.0", "id": id, "error": { "code": -32602, "message": format!("no task was made: {why}"), "data": data } })
}
}
@ -704,14 +849,16 @@ pub async fn handle(app: &App, who: &str, msg: &Value, base: &str, views: bool)
"tasks/cancel" if !active => fail(&id, -32602, &format!("the task has ended ({}): a task that has ended is not cancelled", task_of(&job, ttl)["status"].as_str().unwrap_or(""))),
"tasks/cancel" => {
app.service.cancel(task);
ok(&id, task_of(&app.service.get(task).unwrap_or(job), ttl))
ok(&id, task_of(&app.service.peek(task).unwrap_or(job), ttl))
}
_ => {
// An answer held until the task ends, as long as a reply
// may be held here; the HTTP endpoint streams it to a
// client that takes an event stream, for as long as it takes.
if active && app.service.wait(task, CALL_WAIT).await.is_some_and(|s| s.active()) {
return Some(fail(&id, -32603, &format!("the task is still working after {} s, longer than one reply is held here: ask tasks/get until it has ended, then tasks/result, or ask for tasks/result with Accept: text/event-stream and the answer waits until it ends", CALL_WAIT.as_secs())));
// Held within the answer's time from the request's arrival.
let hold = crate::offload::left().map_or(CALL_WAIT, |l| l.min(CALL_WAIT));
if active && app.service.wait(task, hold).await.is_some_and(|s| s.active()) {
return Some(fail(&id, -32603, &format!("the task is still working after {} s, as long as one reply is held here: ask tasks/get until it has ended, then tasks/result, or ask for tasks/result with Accept: text/event-stream and the answer waits until it ends", hold.as_secs())));
}
ok(&id, task_result(app, who, task, base, views).await)
}
@ -770,15 +917,20 @@ async fn call(app: &App, who: &str, name: &str, args: Value, base: &str) -> Valu
Err(why) => tool_result(format!("refused: {why}"), json!({ "error": why }), true),
}
}
"dry_run_quantum_job" => dry_run_answer(app, who, &args),
"run_quantum_circuit" | "run_on_target" | "run_quantum_set" if crate::http::is_dry_run(&args) => dry_run_answer(app, who, &args),
"dry_run_quantum_job" => dry_run_answer(app, who, &args).await,
"run_quantum_circuit" | "run_on_target" | "run_quantum_set" if crate::http::is_dry_run(&args) => dry_run_answer(app, who, &args).await,
"run_quantum_circuit" | "run_on_target" | "run_quantum_set" => {
let id = match admit_run(app, who, args, key.as_deref()) {
let id = match admit_run(app, who, args, key).await {
Ok(id) => id,
Err(refused) => return refused,
};
app.service.wait(&id, CALL_WAIT).await;
let job = app.service.get(&id).unwrap_or(json!({ "id": id }));
// Within the answer's time, whatever admission took; a verdict
// read after it needs time too.
let wait = crate::offload::left().map_or(CALL_WAIT, |l| l.saturating_sub(VERDICT_TIME).min(CALL_WAIT));
app.service.wait(&id, wait).await;
let Some(job) = app.service.get(&id) else {
return tool_result(format!("the job {id} is no longer kept; send the run again"), json!({ "id": id, "error": "no such job" }), true);
};
let failed = job["status"] == "failed";
tool_result(summary(&job), job, failed)
}
@ -802,32 +954,43 @@ async fn call(app: &App, who: &str, name: &str, args: Value, base: &str) -> Valu
Err((_, why)) => tool_result(format!("refused: {why}"), json!({ "error": why }), true),
},
"verify_quantum_receipt" if args["result"]["format"] == wai_quantum::engine::estimate::FORMAT => {
if let Err((_, why)) = crate::analysis::estimate_bounds(&args["request"]) {
return tool_result(format!("refused: {why}"), json!({ "error": why }), true);
}
let body = args.clone();
match crate::analysis::bounded(app, who, move || wai_quantum::engine::estimate::verify(&body)).await {
match crate::analysis::bounded(app, who, crate::offload::Read::Call { kind: "estimate_verify".into(), op: String::new(), body: args.clone(), key: None }).await {
Ok(v) => tool_result(if v["valid"] == true { "valid: the seal, every estimate's receipt, and the request re-run here all hold".into() } else { format!("not valid: signed {}, receipts {}, reproduced {}", v["signed"], v["receipts"], v["reproduced"]) }, v, false),
Err((_, why)) => tool_result(format!("refused: {why}"), json!({ "error": why }), true),
}
}
"verify_quantum_receipt" if args["result"]["format"] == wai_quantum::engine::sets::FORMAT => {
let v = wai_quantum::engine::sets::verify(&args);
let text = if v["valid"] == true {
format!("valid: the set's receipt seals its request and head, and every one of its {} items is sealed by its own receipt and its path to the root", v["items"])
} else {
format!("not valid: signed {}, items as the request gives them {}, each item sealed {}, root {}", v["signed"], v["derived"], v["items_sealed"], v["root"])
};
tool_result(text, v, false)
}
// A set's programs are written out again to check its items: it is
// counted and read as a check is.
"verify_quantum_receipt" if args["result"]["format"] == wai_quantum::engine::sets::FORMAT => match crate::analysis::bounded(app, who, crate::offload::Read::VerifySet { job: args.clone() }).await {
Ok(v) => {
let text = if v["valid"] == true {
format!("valid: the set's receipt seals its request and head, and every one of its {} items is sealed by its own receipt and its path to the root", v["items"])
} else {
format!("not valid: signed {}, items as the request gives them {}, each item sealed {}, root {}", v["signed"], v["derived"], v["items_sealed"], v["root"])
};
tool_result(text, v, false)
}
Err((_, why)) => tool_result(format!("refused: {why}"), json!({ "error": why }), true),
},
"verify_quantum_receipt" if args["job"].is_string() => {
// The job's text, read as the protocol's section 8.2 reads it.
let text = args["job"].as_str().unwrap_or_default();
let v = wai_quantum::engine::job::verdict_of_text(text).unwrap_or_else(|_| wai_quantum::engine::job::verdict_of(&wai_quantum::engine::job::check_text(text)));
let is_set = serde_json::from_str::<Value>(text).is_ok_and(|j| j["result"]["format"] == wai_quantum::engine::sets::FORMAT);
let v = if is_set {
match crate::analysis::bounded(app, who, crate::offload::Read::Verify { text: text.to_string() }).await {
Ok(v) => v,
Err((_, why)) => return tool_result(format!("refused: {why}"), json!({ "error": why }), true),
}
} else {
wai_quantum::engine::job::verdict_of_text(text).unwrap_or_else(|_| wai_quantum::engine::job::verdict_of(&wai_quantum::engine::job::check_text(text)))
};
let line = if v["valid"] == true { "valid: the receipt seals exactly this request and this result, and every rule of its form holds".to_string() } else { format!("not valid: {}", v["reasons"]) };
tool_result(line, v, false)
}
"verify_quantum_receipt" => {
if let Err(crate::jobs::Refused(_, why)) = app.service.verify_counted(who) {
return tool_result(format!("refused: {why}"), json!({ "error": why }), true);
}
let (request, result) = (&args["request"], &args["result"]);
if result.get("receipt").is_none() {
return tool_result("the result carries no receipt".into(), json!({ "valid": false }), true);
@ -849,7 +1012,7 @@ async fn call(app: &App, who: &str, name: &str, args: Value, base: &str) -> Valu
},
"limits": { "job_seconds": c.job_timeout.as_secs(), "active_jobs_per_client": c.per_client_active, "memory_per_client_bytes": c.per_client_mib << 20, "submissions_per_window": c.per_client_burst, "window_seconds": c.per_client_window.as_secs() },
"load": { "queued": queued, "running": running },
"sets": { "most_items": wai_quantum::engine::sets::MAX_ITEMS },
"sets": { "most_items": wai_quantum::engine::sets::MAX_ITEMS, "result_bytes": c.result_mib << 20, "note": "a job or set whose result could take more as served, by its bound before it runs, is refused at once (J031)" },
"store": { "kind": app.service.store().kind(), "durable": app.service.store().durable(), "retention_seconds": c.retain.as_secs() },
"energy": crate::energy::meter().describe(),
"receipt_keys": format!("{base}/.well-known/wai-quantum-receipt-keys"),
@ -979,9 +1142,10 @@ async fn call(app: &App, who: &str, name: &str, args: Value, base: &str) -> Valu
}
}
/// A dry run's answer, as a tool gives it.
fn dry_run_answer(app: &App, who: &str, args: &Value) -> Value {
match crate::http::dry_run(app, who, args) {
/// A dry run's answer, as a tool gives it: of the job the run tools would
/// admit ([`as_run`]), its verdict asked.
async fn dry_run_answer(app: &App, who: &str, args: &Value) -> Value {
match crate::http::dry_run(app, who, &as_run(args.clone())).await {
Ok(v) => {
let secs = v["time"]["seconds"].as_f64().map(|t| format!("about {t:.3} s ({})", v["time"]["basis"].as_str().unwrap_or(""))).unwrap_or_else(|| format!("time not estimated ({})", v["time"]["why"].as_str().unwrap_or("")));
let energy = v["energy"]["joules_micro_estimate"].as_u64().map(|u| format!("about {:.3} J, an estimate", u as f64 / 1e6)).unwrap_or_else(|| format!("energy not estimated ({})", v["energy"]["unestimated"].as_str().unwrap_or("")));
@ -1094,6 +1258,79 @@ mod tests {
rt.block_on(handle(&app, "test", &msg, "http://127.0.0.1:1", views)).unwrap()
}
/// Reading a program again costs what reading it cost, so a call that
/// was not counted reads nothing: each states a verdict, as every answer
/// about a program does, of why it was refused, with no code of the
/// reader's, as reading this program gives.
#[test]
fn a_call_that_was_not_counted_reads_nothing_and_still_states_a_verdict() {
let bad = "OPENQASM 3.0;\nqubit q;\nno_such_gate q;\n";
let unread = |v: &Value, what: &str| {
assert_eq!(v["level"], "refused", "{what}: {v}");
assert!(v["parsed"].get("code").is_none() && v["parsed"]["ok"] == false, "{what}: {v}");
};
for (tool, args) in [
("lint_qasm", json!({ "qasm": bad })),
("get_quantum_job", json!({ "id": "no such job", "qasm": bad })),
("run_quantum_circuit", json!({ "qasm": bad, "idempotency_key": 5 })),
("run_quantum_circuit", json!({ "qasm": bad, "idempotency_key": "" })),
] {
let r = rpc("tools/call", json!({ "name": tool, "arguments": args }));
assert_eq!(r["result"]["isError"], true, "{tool}: {r}");
unread(&r["result"]["structuredContent"]["verdict"], tool);
}
// A refused task is the same, on the error it answers with.
let r = rpc("tools/call", json!({ "name": "run_quantum_circuit", "arguments": { "qasm": bad, "idempotency_key": 5 }, "task": {} }));
unread(&r["error"]["data"]["verdict"], "task");
// Counted, it is read: the same program, the reader's own code.
let r = rpc("tools/call", json!({ "name": "run_quantum_circuit", "arguments": { "qasm": bad } }));
assert!(r["result"]["structuredContent"]["verdict"]["parsed"]["code"].is_string(), "{r}");
}
/// With every turn held, a tool that reads nothing answers at once, and
/// one that needs a turn for its verdict still has one, saying why it is
/// not of the program; past its window a run reads nothing either.
#[tokio::test]
async fn the_tools_are_answered_with_no_turn_to_be_had() {
let dir = std::env::temp_dir().join(format!("wai-qsvc-mcp-turns-{}", std::process::id()));
let signer = std::sync::Arc::new(crate::receipt::Signer::load_or_create(&dir.join("key")).unwrap());
let service = crate::jobs::Service::new(crate::jobs::Config { per_client_burst: 3, ..crate::jobs::Config::from_env() });
let app = App::bare(service, signer, "http://127.0.0.1:1");
let call = |name: &str, args: Value| json!({ "jsonrpc": "2.0", "id": 7, "method": "tools/call", "params": { "name": name, "arguments": args } });
let mut held = Vec::new();
for i in 0..crate::offload::AT_ONCE {
held.push(app.service.gate.turn(crate::offload::Pool::Reading, &format!("other {i}")).await.ok().unwrap());
}
let t = std::time::Instant::now();
let r = handle(&app, "test", &call("list_targets", json!({})), "http://127.0.0.1:1", false).await.unwrap();
assert!(r["result"]["structuredContent"]["targets"].is_array() && t.elapsed() < std::time::Duration::from_secs(1), "{r}");
let r = handle(&app, "test", &call("critique_qasm", json!({ "qasm": "OPENQASM 3.0; qubit q; h q;" })), "http://127.0.0.1:1", false).await.unwrap();
let v = &r["result"]["structuredContent"]["verdict"];
assert!(v["statement"].as_str().unwrap().contains("[J032]") && v["level"] == "refused", "{r}");
let tool = tools().as_array().unwrap().iter().find(|t| t["name"] == "critique_qasm").cloned().unwrap();
let bad = schemas().check_value(&tool["outputSchema"], &r["result"]["structuredContent"]);
assert!(bad.is_empty(), "{bad:?}");
drop(held);
app.service.count("test").unwrap();
app.service.count("test").unwrap();
let r = handle(&app, "test", &call("run_quantum_circuit", json!({ "qasm": "OPENQASM 3.0;\nqubit q;\nno_such_gate q;\n" })), "http://127.0.0.1:1", false).await.unwrap();
let sc = &r["result"]["structuredContent"];
assert!(sc["error"].as_str().unwrap().ends_with("[J030]") && sc["verdict"]["parsed"].get("code").is_none(), "{r}");
}
/// A poll states what its job holds, read at its admission, and reads
/// nothing more, however often it is asked.
#[test]
fn a_poll_reads_nothing() {
let v = verdict::of_admitted(None);
assert!(v["level"] == "parsed" && v["parsed"]["ok"] == true, "{v}");
let job = json!({ "id": "j", "status": "queued", "request": { "qasm": "this does not read" } });
let answer = tool_result("queued".into(), job, false);
let done = finish("get_quantum_job", &json!({ "id": "j" }), answer, true, Reading::Stored);
assert_eq!(done["structuredContent"]["verdict"]["level"], "parsed", "{done}");
assert!(done["_meta"][views::META].get("layout").is_none(), "{done}");
}
/// A client whose host draws views gets them here as from the command
/// line's server, and the view's own tool for a running job, kept from
/// the model; any other client gets none of it.
@ -1462,7 +1699,7 @@ mod tests {
let bell = "OPENQASM 3.0; qubit[2] q; bit[2] c; h q[0]; cx q[0], q[1]; c = measure q;";
// The job a first call made, as the call admits it (with its verdict
// asked), cancelled so that no call waits for it here.
let id = crate::http::admit(&app, "test", json!({ "qasm": bell, "shots": 20, "verdict": true }), Some(crate::jobs::Key("agent-1"))).unwrap();
let id = rt.block_on(crate::http::admit_to(&app.service, "test", json!({ "qasm": bell, "shots": 20, "verdict": true }), Some("agent-1"))).unwrap();
assert_eq!(app.service.cancel(&id), Some(true));
let again = run(json!({ "qasm": bell, "shots": 20, "idempotency_key": "agent-1" }));
assert_eq!(again["isError"], false, "{again}");

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -35,7 +35,9 @@ fn start(dir: &Path, port: u16) -> Child {
.env("WAI_QSVC_BUDGET_GB", "4")
.env("WAI_QSVC_WORKERS", "1")
.env("WAI_QSVC_ENERGY", "0")
.env("WAI_QSVC_RATE", "100000")
// A set's receipt counts, as a check does: the corpus holds more than a window's.
.env("WAI_QSVC_CLIENT_BURST", "100000")
.env("WAI_QSVC_CLIENT_VERIFIES", "100000")
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
@ -110,9 +112,10 @@ fn try_http(port: u16, method: &str, path: &str, body: &[u8]) -> Option<(u16, St
Some((status, out.split_once("\r\n\r\n").map(|(_, b)| b.to_string()).unwrap_or_default()))
}
/// A program nested past the reader's bound (Q130) is refused on every route that reads one,
/// and the service stays up: a flat sum of 2000 terms, 4 KB, used to overflow a worker's
/// stack and end the service, on the routes that read a program on an async worker.
/// A program nested past the reader's bound (Q006) is refused on every route that reads one,
/// a flat sum of 2000 terms (one level, however long) is read, and the service stays up: the
/// sum, 4 KB, used to overflow a worker's stack and end the service, on the routes that read
/// a program on an async worker.
#[test]
fn a_program_past_the_nesting_bound_is_refused_and_the_service_stays_up() {
let dir = std::env::temp_dir().join(format!("wai-qsvc-deep-{}", std::process::id()));
@ -147,9 +150,14 @@ fn a_program_past_the_nesting_bound_is_refused_and_the_service_stays_up() {
child = start(&dir, port);
continue;
}
// Read at once, it is refused by its code; queued, the job fails by it.
if (200..300).contains(&code) && !answer.contains("Q130") && path != "/v1/jobs" {
wrong.push(format!("{what} at {path}: {code} without Q130: {}", answer.chars().take(300).collect::<String>()));
// Read at once, a nested one is refused by its code; queued, the job fails by it.
// The sum is flat, and read.
let nested = *what != "a sum of 2000 terms";
if nested && (200..300).contains(&code) && !answer.contains("Q006") && path != "/v1/jobs" {
wrong.push(format!("{what} at {path}: {code} without Q006: {}", answer.chars().take(300).collect::<String>()));
}
if !nested && (answer.contains("Q006") || code >= 500) {
wrong.push(format!("{what} at {path}: {code}: {}", answer.chars().take(300).collect::<String>()));
}
}
}

View file

@ -65,12 +65,12 @@ browser engine. A test holds this page to the code's table.
| `J011` | refused | The method named cannot take this job: a gate it does not hold, more qubits, memory or work than it takes here, or an answer it does not give (noise it cannot take is `J061`); the message says which, and `auto` picks a method that can. A job on `mps` runs under a meter of the bonds it reaches, so this can also come from a job that has started: it stopped where its work passed what a job may take. |
| `J012` | refused | No method takes this job: each method's reason is listed, so the job can be changed to suit one. |
| `J013` | refused | A placement on the fabric (`fabric`) this host cannot honour: no member by that name, no certified accelerator member here, a state or a partition larger than a member holds, a partition count that is not a power of two, or a method or program the fabric's members do not run (the message says which, and what is here). |
| `J020` | refused | An intent that does not read: not one of `distribution`, `equivalent` or `prepares`, a distribution whose outcomes are not the program's bits or whose probabilities do not sum to 1, or a reference program that does not read. |
| `J020` | refused | An intent that does not read: not one of `distribution`, `equivalent` or `prepares`, a distribution whose outcomes are not the program's bits or whose probabilities do not sum to 1, or a reference program that does not read, or more intents than the host takes. |
| `J021` | refused | A proposed repair that changes a line no diagnostic flags, or adds lines away from one: rejected, and the program left as it was. |
| `J022` | refused | A repair's choices that do not read (`max_iterations`, `dyadic_bits`, `sizes`), or a proposal that is not a program's text. |
| `J030` | retry | The host takes no more from this client for now (submissions in a window, or jobs at once): send it again shortly. |
| `J031` | refused | A job or a check larger than this host takes: run it on your own machine, with the command line or its MCP server. |
| `J032` | retry | The host is full (its queue, or the checks running at once): send it again later. |
| `J031` | refused | A job or a check larger than this host takes, its reading included: run it on your own machine, with the command line or its MCP server. |
| `J032` | retry | The host is full (its queue, the checks running at once, or the requests being read, yours or all of them): send it again later. |
| `J033` | refused | A host that takes requests only with one of its API keys: give yours as `Authorization: Bearer <key>`; a key it does not hold is refused the same way. |
| `J034` | refused | An idempotency key (`Idempotency-Key`, or a tool's `idempotency_key`) that does not read, or that this client sent before with another request: a key names one job, so another request needs another key. A key is 1 to 255 visible ASCII characters, and should be unguessable (a UUID, or 128 random bits). |
| `J040` | refused | A pulse run that does not read or cannot be done: a field it does not take, options or device parameters that do not read, a target without the qubit, or a schedule outside the simulator's model (two tones on one qubit at once, a play after its qubit's capture, more levels and qubits than it holds, a frame far from its qubit); the message says which. |

View file

@ -45,6 +45,7 @@ engine. A test holds this page to the code's table.
| `Q003` | refused | A string, a block comment, a bracket or a calibration block that is not closed. |
| `Q004` | refused | Something else was expected here: a `;`, a name, an expression or a statement (the message says which). |
| `Q005` | refused | A statement in a place the language does not allow it: `else` with no `if`, `case` outside `switch`, a declaration inside a gate. |
| `Q006` | refused | A program nested deeper than the reader takes: statements, parentheses, operators, brackets and types more than 64 levels deep in its text (a run of one precedence, `a + b + c`, and an `else if` chain each count one, however long), an array of more than 7 dimensions, or more than 256 levels in all through the gates and subroutines it calls; refused, never read by a recursion that could run out of stack. |
| `Q010` | warned | An `OPENQASM` version other than 2.x or 3.x: the program is read as OpenQASM 3. |
| `Q011` | warned | An `include` of a file other than the standard libraries (`stdgates.inc`, `qelib1.inc`): its definitions are not read, so a gate it defines is unknown. |
| `Q012` | lowered | `include "stdgates.inc"` or `"qelib1.inc"`: the standard gates, `U`, `gphase` and OpenQASM 2's names are built in. |
@ -79,7 +80,7 @@ engine. A test holds this page to the code's table.
| `Q127` | refused | An assignment to a constant, an input or a loop variable. |
| `Q128` | refused | A measurement where a value is needed that is not a bit (measurement gives bits, into a bit register). |
| `Q129` | refused | A parameter (`input float`, `input angle`) a gradient cannot follow to the gates: under a modifier, in a classical variable, a condition, an index, a size, a loop's range, a subroutine or `gphase`, or one whose type rounds it (`angle[n]`, `float[32]`); a gradient follows a parameter through arithmetic, the built-in functions and gate definitions into a gate's arguments. |
| `Q130` | refused | A program nested deeper than 128, before a reader's stack could run out: a statement at the top level is at depth 1, and each statement in a block or a body, each expression a statement holds, each operand of an operator, what a bracket holds (of an expression or a type) and each index's indexed value, one deeper than what holds it; operators group as they bind (`a + b + c` is `(a + b) + c`), an `else if` is at the depth of its `if` (one `if` holds at most 1024 of them), and a call's body is read in its place, its statements one deeper than the call. |
| `Q130` | refused | A program whose reading would hold more memory than the reader takes: a classical value (an array, a bit register) of more than 64 MiB, each element counted at its width, or more than 512 MiB in all of its values, gates, steps, measurements and timing; refused before it is made. |
| `Q200` | lowered | A `gate` the program defines, inlined at each call with its parameters bound. |
| `Q201` | lowered | `ctrl @` and `negctrl @` on any gate, lowered exactly to standard gates (a negative control is a control between two `x`). |
| `Q202` | lowered | `inv @` on any gate, user gates included: each gate's exact inverse, in reverse order. |
@ -105,7 +106,7 @@ engine. A test holds this page to the code's table.
| `Q309` | lowered | `while` whose condition is known on reading: unrolled until it fails. |
| `Q310` | refused | A `while` whose condition reads measured bits and that declares no bound: add `@wai.max_iterations N` before it. |
| `Q311` | warned | A `while` on measured bits run at most its declared bound (`@wai.max_iterations N`): a run that still meets its condition after N iterations stops looping there. |
| `Q312` | refused | A program that does not finish unrolling within the reader's bound (20 000 000 statements and loop iterations). |
| `Q312` | refused | A program that does not finish unrolling within the reader's bound (20 000 000 statements and loop iterations), or that runs out a caller's budget of reading (`qasm::within_steps`: statements, gates and a generator's loop passes, shared by every program read under it). |
| `Q313` | refused | A range with a zero step, or that does not read as `[start:end]` or `[start:step:end]`. |
| `Q314` | refused | `break` or `continue` outside a loop, or `return` outside a subroutine. |
| `Q315` | refused | A `return` with a value under a condition on measured bits. |

View file

@ -30,6 +30,9 @@ and in the browser build.
| Comments `//` and `/* */` | supported; an unclosed block comment is refused | Q003 |
| Identifiers, Unicode letters included (`θ`, `π`) | supported | |
| Statements and blocks `{ }`, scopes | supported; qubits, gates, subroutines and externs are declared at the top level | Q105 |
| Nesting: statements in blocks and bodies, parentheses, operators, brackets, calls, casts and types | supported to 64 levels together in the text (a statement, each enclosing statement, and each level of its expressions' trees count one). A run of operators of one precedence (`a + b - c`, `c[0] && c[1] && …`) is one level, and an `else if` chain one level, at any length: both are read and run in loops, left to right. Deeper is refused, never read by a recursion that could run out of stack; an array has at most 7 dimensions | Q006 |
| Nesting in all, through the gates and subroutines a program calls | supported to 256 levels (each statement, expression and call the reader runs counts one), and calls 32 deep; deeper is refused. Both hold: a recursive subroutine whose body nests more than about seven levels reaches 256 before 32 calls | Q006, Q209 |
| What a program holds on reading: its classical values, gates, steps, measurements and timing | supported to 64 MiB for one value (an array or a bit register, each element counted at its width) and 512 MiB in all; more is refused before it is made. Making or copying a value counts toward the reader's 20 000 000 steps at one step for every 128 bytes, and an element read from an array copies the element alone | Q130, Q312 |
| `pragma …`, `#pragma …`, annotations `@keyword …` | warned: kept, not applied; `@wai.max_iterations N` bounds the `while` after it | Q013 |
## Literals
@ -114,6 +117,7 @@ and in the browser build.
| `pow(k) @`, `k` a whole number | lowered exactly: angles scaled or the gate repeated; negative `k` repeats the inverse | Q203 |
| `pow(r) @`, `r` fractional | lowered where an exact principal root exists (`pow(1/2) @ x` is `sx`; rotations by less than 2π; phases; controlled forms); refused otherwise (`u`, `cu`, user gates) | Q204, Q205 |
| A modifier on a subroutine, or nested as deep as 32 | refused | Q211, Q209 |
| A chain of modifiers (`inv @ inv @ …`) | read at any length: a list, not a nesting | |
## Measurement and reset
@ -149,7 +153,6 @@ and in the browser build.
| Bits declared in a subroutine | lowered: bits of its own, fed forward | Q322 |
| A subroutine called as a gate (`f(1) q, r;`) | supported | |
| Recursion | refused past 32 calls deep | Q209 |
| Statements, operators, brackets and indexes nested deeper than 128, counted through the bodies calls read in place; an `if` with more than 1024 `else if` arms | refused, before a reader's stack could run out | Q130 |
## Timing

View file

@ -131,7 +131,15 @@ a rate above 1/2 among them where the law needs one; a channel with no such
flips (`PAULI_CHANNEL_1(0.1, 0.1, 0)`) is refused, since a model of
independent mechanisms cannot state it. A circuit's model written back keeps
its tags and coordinates. What the reader does not take it refuses with the
line, never skipping it. The
line, never skipping it. Blocks nest at most 32 deep (`MAX_BLOCK_DEPTH`), a
circuit names qubits below 2²⁴ (`MAX_QUBITS`; with its correlated errors'
carrier, it holds at most that many) and a model detectors below 2²⁴
(`MAX_DETECTORS`, offsets counted): what is made for an index is as long as
the index, so each is refused on its line, as is a block nested deeper, on the
line that opens it, so neither reading, unrolling nor writing a text recurses
past that. Every pass through a block counts toward the bound on what a text
unrolls to, so a block repeated without end that writes nothing still ends,
refused. The
[conformance vectors](../../quantum-conformance/qec-text/) hold each form,
the model each circuit gives, and each refusal.

View file

@ -312,7 +312,12 @@
"integer",
"array",
"boolean"
]
],
"description": "The outcomes sampled that the stated distribution gives probability 0: the first 64, in order."
},
"impossible_outcomes": {
"type": "integer",
"description": "How many outcomes sampled the stated distribution gives probability 0."
},
"note": {
"type": "string"

View file

@ -34,14 +34,14 @@ pub struct Host<'a> {
/// The value of the statement `name`: what its view shows or its call
/// gives, or why the engine refused it.
pub fn value(a: &Answer, name: &str, host: &Host) -> Result<Value, String> {
Ev { a, host, memo: BTreeMap::new() }.stmt(name)
Ev::new(a, host).stmt(name)
}
/// Every statement that is ready, carried out: `{"format", "digest",
/// "results": {name: {"ok": true, "value"} | {"ok": false, "error", "code",
/// …}}}`, in the order of their lines.
pub fn evaluate(a: &Answer, host: &Host) -> Value {
let mut ev = Ev { a, host, memo: BTreeMap::new() };
let mut ev = Ev::new(a, host);
let mut results = serde_json::Map::new();
for c in &a.stmts {
if c.status != Status::Ready || matches!(c.stmt.body, Body::Program { .. }) {
@ -111,7 +111,7 @@ pub fn verify(text: &str, job_bytes: u64) -> Result<Value, String> {
return Err(format!("the receipt names {name}, which is not a seal the answer can run ({})", c.status.as_str()));
}
let host = Host { job_bytes, key: None };
let mut ev = Ev { a: &a, host: &host, memo: BTreeMap::new() };
let mut ev = Ev::new(&a, &host);
let (request, result) = ev.sealed(seal_list(&c.stmt.body).ok_or("the seal lists nothing")?)?;
let key = m["receipt"]["key"].clone();
let signer = key.as_str().and_then(job::did_of);
@ -134,30 +134,84 @@ fn seal_list(b: &Body) -> Option<&[Expr]> {
}
}
/// How many statements deep one statement's value may reach through the
/// statements it names (`a = @Seal([b])`, `b = @Seal([c])`, …): each is run
/// under the one that names it.
pub const CHAIN_DEPTH: usize = 32;
struct Ev<'a> {
a: &'a Answer,
host: &'a Host<'a>,
memo: BTreeMap<String, Result<Value, String>>,
/// Each statement's answer, whatever reached it, with how many statements
/// long its own chain is: reached too deep for that, it is refused as it
/// would be run, so the order statements are asked in changes nothing.
memo: BTreeMap<String, (Result<Value, String>, usize)>,
/// What a statement gave where the chain's limit cut it short, with the
/// depth it was reached at: an answer of that depth, not the statement's,
/// so it stands only as deep or deeper, and is never run again there.
cut: BTreeMap<String, (Result<Value, String>, usize)>,
/// How many statements are being run, each under the one that names it.
depth: usize,
/// The deepest the statement being run has reached, and whether the
/// limit cut it short.
reached: usize,
was_cut: bool,
}
impl Ev<'_> {
impl<'a> Ev<'a> {
fn new(a: &'a Answer, host: &'a Host<'a>) -> Self {
Ev { a, host, memo: BTreeMap::new(), cut: BTreeMap::new(), depth: 0, reached: 0, was_cut: false }
}
fn too_deep(&mut self, name: &str) -> Result<Value, String> {
self.was_cut = true;
Err(format!("{name} is reached through more than {CHAIN_DEPTH} statements, each naming the next: name fewer in a chain"))
}
fn stmt(&mut self, name: &str) -> Result<Value, String> {
if let Some(r) = self.memo.get(name) {
if let Some((r, long)) = self.memo.get(name) {
if self.depth + long > CHAIN_DEPTH {
return self.too_deep(name);
}
self.reached = self.reached.max(self.depth + long);
return r.clone();
}
let c = self.a.get(name).ok_or_else(|| format!("no statement {name}"))?;
let r = if c.status != Status::Ready {
Err(format!("{name} is {}: nothing to run", c.status.as_str()))
if let Some((r, at)) = self.cut.get(name)
&& self.depth >= *at
{
let r = r.clone();
self.was_cut = true;
return r;
}
if self.depth >= CHAIN_DEPTH {
return self.too_deep(name);
}
let at = self.depth;
self.depth += 1;
let (reached, was_cut) = (std::mem::replace(&mut self.reached, self.depth), std::mem::replace(&mut self.was_cut, false));
let r = self.stmt_here(name);
self.depth -= 1;
if self.was_cut {
self.cut.insert(name.to_string(), (r.clone(), at));
} else {
match &c.stmt.body {
Body::Program { qasm, .. } => Ok(json!({ "qasm": qasm, "digest": blake3::hash(qasm.as_bytes()).to_hex().to_string() })),
Body::Expr(e) => self.expr(e),
}
};
self.memo.insert(name.to_string(), r.clone());
self.memo.insert(name.to_string(), (r.clone(), self.reached - at));
}
self.reached = self.reached.max(reached);
self.was_cut |= was_cut;
r
}
fn stmt_here(&mut self, name: &str) -> Result<Value, String> {
let c = self.a.get(name).ok_or_else(|| format!("no statement {name}"))?;
if c.status != Status::Ready {
return Err(format!("{name} is {}: nothing to run", c.status.as_str()));
}
match &c.stmt.body {
Body::Program { qasm, .. } => Ok(json!({ "qasm": qasm, "digest": blake3::hash(qasm.as_bytes()).to_hex().to_string() })),
Body::Expr(e) => self.expr(e),
}
}
fn expr(&mut self, e: &Expr) -> Result<Value, String> {
match e {
Expr::View { name, args, .. } => match name.as_str() {

View file

@ -295,3 +295,67 @@ fn the_prompt_names_every_view_and_call() {
assert!(a.diags.is_empty(), "{:?}", a.diags);
assert!(p.ends_with(EXAMPLE));
}
/// A statement whose value runs through others, each sealing the next, is
/// refused past [`super::eval::CHAIN_DEPTH`] rather than run by a recursion
/// as deep as the answer is long; within it, it runs.
#[test]
fn a_chain_of_statements_past_the_limit_is_refused_not_overflowed() {
let chain = |n: usize| {
let mut t = String::from("```qasm p\nOPENQASM 3.0;\nqubit[1] q;\nbit[1] c;\nh q[0];\nc = measure q;\n```\n");
for k in 0..n {
t += &format!("s{k} = @Seal([s{}])\n", k + 1);
}
t + &format!("s{n} = @Sample(p, 10)\n")
};
let value = |text: String| {
std::thread::Builder::new()
.stack_size(2 << 20)
.spawn(move || {
let k = key();
super::eval::value(&parse(&text), "s0", &super::eval::Host { job_bytes: 1 << 28, key: Some(&k) })
})
.unwrap()
.join()
.unwrap()
};
let e = value(chain(2_000)).unwrap_err();
assert!(e.contains("more than 32 statements"), "{e}");
assert!(value(chain(4)).is_ok());
}
/// Whether a statement of a chain is refused is its own: the same statements
/// are refused, whatever order they are asked in, and they are the ones
/// whose chains are too long read alone. Statements that name each other
/// twice are not run again for each way down.
#[test]
fn the_chain_limit_refuses_the_same_statements_in_any_order() {
let head = "```qasm p\nOPENQASM 3.0;\nqubit[1] q;\nbit[1] c;\nh q[0];\nc = measure q;\n```\n";
let lines = |n: usize, list: &str| -> Vec<String> {
let mut v: Vec<String> = (0..n).map(|k| format!("s{k} = @Seal([{}])\n", list.replace('#', &format!("s{}", k + 1)))).collect();
v.push(format!("s{n} = @Sample(p, 10)\n"));
v
};
let refused = |text: String| {
let k = key();
let host = super::eval::Host { job_bytes: 1 << 28, key: Some(&k) };
let a = parse(&text);
let out = super::eval::evaluate(&a, &host);
let mut bad: Vec<u32> = out["results"].as_object().unwrap().iter().filter(|(_, r)| r["ok"] != true).map(|(n, _)| n[1..].parse().unwrap()).collect();
bad.sort_unstable();
let alone: Vec<u32> = out["results"].as_object().unwrap().keys().filter(|n| super::eval::value(&a, n, &host).is_err()).map(|n| n[1..].parse().unwrap()).collect();
(bad, alone)
};
let forward = lines(40, "#");
let (bad, alone) = refused(format!("{head}{}", forward.concat()));
let mut alone = alone;
alone.sort_unstable();
assert_eq!(bad, (0..=8).collect::<Vec<u32>>());
assert_eq!(alone, bad);
let reverse: Vec<String> = forward.iter().rev().cloned().collect();
assert_eq!(refused(format!("{head}{}", reverse.concat())).0, bad);
let t = std::time::Instant::now();
let twice = lines(60, "#, #");
assert!(!refused(format!("{head}{}", twice.concat())).0.is_empty());
assert!(t.elapsed() < std::time::Duration::from_secs(120), "{:?}", t.elapsed());
}

View file

@ -0,0 +1,344 @@
//! A job's result is never longer than the bound its plan gave before it ran
//! (`catalog::plan_bounded`), as a host writes it (RFC 8785, with its
//! provenance), for jobs of every kind drawn at random: sampled intents that
//! fail with many outcomes they rule out, distributions and references,
//! trajectories with their own count, verdicts over many notes and smells,
//! Clifford circuits of every shape on the stabilizer method, noise, observables,
//! amplitudes, compiles, repairs, mitigations, programs that measure in the
//! middle, device targets, pulse runs and intents carrying numbers RFC 8785
//! writes long. And where the method knows its outcomes, the bound is close:
//! a GHZ state's two outcomes, and a long loop with nothing to remark on.
use super::catalog::plan_bounded;
use super::run::run_with_provenance;
use serde_json::{Value, json};
const CAP: u64 = 8 << 30;
/// The longest a host writes `v`: as the engine's RFC 8785 writer does (every
/// number as a double), or with each whole number exactly as read and every
/// other number as RFC 8785 does (a job service's served text).
fn served(v: &Value) -> u64 {
struct Es;
impl serde_json::ser::Formatter for Es {
fn write_f64<W: ?Sized + std::io::Write>(&mut self, w: &mut W, x: f64) -> std::io::Result<()> {
w.write_all(super::receipt::es_number(x).as_bytes())
}
}
let mut out = Vec::new();
let mut ser = serde_json::Serializer::with_formatter(&mut out, Es);
serde::Serialize::serialize(v, &mut ser).unwrap();
(super::receipt::canonical(v).len() as u64).max(out.len() as u64)
}
/// A small generator of its own (SplitMix64), so a failure names its seed.
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
z ^ (z >> 31)
}
/// One of `lo..=hi`.
fn int(&mut self, lo: u64, hi: u64) -> u64 {
lo + self.next() % (hi - lo + 1)
}
fn coin(&mut self) -> bool {
self.next() & 1 == 1
}
fn pick<'a, T>(&mut self, xs: &'a [T]) -> &'a T {
&xs[(self.next() % xs.len() as u64) as usize]
}
}
/// Numbers RFC 8785 writes longer than serde does, and some it does not.
fn long_numbers(rng: &mut Rng, n: u64) -> Value {
let pool = [json!(1e20), json!(1e-6), json!(1.5e-5), json!(123456.0), json!(-0.0), json!(1e21), json!(3), json!(0.1)];
Value::Array((0..n).map(|_| rng.pick(&pool).clone()).collect())
}
/// The program `k` qubits under `h`, each fanned out by `cx` to the rest of
/// `w` qubits, every qubit measured into its bit: `2^k` outcomes `w` wide.
fn fanned(k: u64, w: u64) -> String {
let mut s = format!("OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[{w}] q;\nbit[{w}] c;\n");
for i in 0..k {
s += &format!("h q[{i}];\n");
}
for i in k..w {
s += &format!("cx q[{}], q[{i}];\n", i % k);
}
s + "c = measure q;\n"
}
/// A sampled intent that the program's run does not fit: it states one
/// outcome (or a few), so every other outcome sampled is one it rules out.
fn sampled_intent(rng: &mut Rng, w: u64) -> Value {
let zeros = "0".repeat(w as usize);
let mut want = serde_json::Map::new();
want.insert(zeros, json!(1.0));
let mut i = json!({ "sampled": want, "test": rng.pick(&["chi_squared", "g_test"]), "alpha": 0.01, "shots": rng.int(200, 6000), "seed": rng.int(1, 9) });
if rng.coin() {
i["name"] = json!(format!("intent {}", rng.int(0, 99)));
}
if rng.coin() {
let n = rng.int(1, 3000);
i["note"] = long_numbers(rng, n);
}
i
}
/// A Clifford circuit drawn at random: `n` qubits, its gates of every
/// Clifford kind, some qubits measured, into bits that may repeat.
fn clifford(rng: &mut Rng, n: u64, gates: u64) -> String {
let bits = rng.int(1, n + 4);
let mut s = format!("OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[{n}] q;\nbit[{bits}] c;\n");
for _ in 0..gates {
let a = rng.int(0, n - 1);
let b = (a + rng.int(1, n.max(2) - 1)) % n;
let g = *rng.pick(&["h", "s", "sdg", "x", "y", "z", "sx", "sxdg", "cx", "cz", "cy", "swap", "h", "cx"]);
if matches!(g, "cx" | "cz" | "cy" | "swap") && n > 1 {
s += &format!("{g} q[{a}], q[{b}];\n");
} else if !matches!(g, "cx" | "cz" | "cy" | "swap") {
s += &format!("{g} q[{a}];\n");
}
}
let measured = rng.int(1, n);
for _ in 0..measured {
s += &format!("c[{}] = measure q[{}];\n", rng.int(0, bits - 1), rng.int(0, n - 1));
}
s
}
/// A program with `pragmas` lines the reader notes, and gates that do
/// nothing, cancel or merge, among others, `gates` of them.
fn noted(rng: &mut Rng, pragmas: u64, gates: u64) -> String {
let n = rng.int(2, 6);
let mut s = format!("OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[{n}] q;\nbit[{n}] c;\n");
for i in 0..pragmas {
s += &format!("pragma wai.unknown_{i} {}\n", "x".repeat(rng.int(0, 40) as usize));
}
for _ in 0..gates {
let a = rng.int(0, n - 1);
s += &match rng.int(0, 5) {
0 => format!("h q[{a}];\nh q[{a}];\n"),
1 => format!("rz(0) q[{a}];\n"),
2 => format!("rx(0.1) q[{a}];\nrx(0.2) q[{a}];\n"),
3 => format!("cx q[{a}], q[{}];\ncx q[{a}], q[{}];\n", (a + 1) % n, (a + 1) % n),
4 => format!("t q[{a}];\ntdg q[{a}];\n"),
_ => format!("h q[{a}];\n"),
};
}
s + "c = measure q;\n"
}
/// A job of the kind `kind`, drawn at random.
fn job(rng: &mut Rng, kind: u64) -> (String, Value) {
match kind {
// Sampled intents that fail, ruling out many wide outcomes.
0 => {
let k = rng.int(3, 10);
let w = rng.int(k, 48);
let intents: Vec<Value> = (0..rng.int(1, 3)).map(|_| sampled_intent(rng, w)).collect();
("sampled intents that fail".into(), json!({ "qasm": fanned(k, w), "shots": rng.int(1, 200), "intent": intents, "seed": rng.int(1, 99) }))
}
// Trajectories, their own count apart from the shots.
1 => {
let n = rng.int(2, 9);
let w = rng.int(n, 30);
let mut q = format!("OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[{n}] q;\nbit[{w}] c;\nh q;\n");
for i in 0..n {
q += &format!("c[{}] = measure q[{i}];\n", rng.int(0, w - 1));
}
let mut j = json!({ "qasm": q, "method": "trajectories", "shots": rng.int(1, 3000), "noise": { "depolarizing_1q": 0.01, "readout": 0.01 }, "options": { "trajectories": rng.int(1, 40_000) } });
if rng.coin() {
j["verdict"] = json!(true);
}
("trajectories".into(), j)
}
// A verdict over many notes and smells.
2 => {
let (p, g) = (rng.int(0, 2500), rng.int(0, 3000));
let mut j = json!({ "qasm": noted(rng, p, g), "shots": rng.int(1, 500), "verdict": true });
if rng.coin() {
j["compile"] = json!({ "epsilon": 1e-3, "level": 2 });
}
("notes and smells, with a verdict".into(), j)
}
// A Clifford circuit, on the stabilizer method or as the planner picks.
3 => {
let n = rng.int(1, 30);
let g = rng.int(0, 200);
let mut j = json!({ "qasm": clifford(rng, n, g), "shots": rng.int(1, 20_000), "seed": rng.int(1, 99) });
if rng.coin() {
j["method"] = json!("stabilizer");
}
if rng.int(0, 3) == 0 {
j["noise"] = json!({ "depolarizing_1q": 0.01, "readout": 0.02 });
}
if rng.coin() {
j["verdict"] = json!(true);
}
("a Clifford circuit".into(), j)
}
// Observables, amplitudes, a mitigation, a repair, measured in the middle.
4 => {
let n = rng.int(2, 6);
let mut q = format!("OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[{n}] q;\nbit[{n}] c;\nh q;\nrx(0.3) q[0];\ncx q[0], q[1];\n");
let mut j = json!({ "shots": rng.int(0, 4000), "seed": 3 });
match rng.int(0, 4) {
0 => j["observables"] = json!((0..rng.int(1, 40)).map(|i| format!("{}.25 Z{} X{}", i % 5, i % n, (i + 1) % n)).collect::<Vec<_>>()),
1 => j["amplitudes"] = json!((0..rng.int(1, 20)).map(|i| format!("{:0w$b}", i % (1 << n), w = n as usize)).collect::<Vec<_>>()),
2 => {
j["observables"] = json!(["Z0 Z1"]);
j["noise"] = json!({ "depolarizing_2q": 0.02, "depolarizing_1q": 0.004 });
j["mitigate"] = json!({ "method": "zne", "shots": 2000 });
}
3 => {
q += "h q[0];\nh q[0];\n";
j["repair"] = json!(true);
j["verdict"] = json!(true);
}
_ => q += "c[0] = measure q[0];\nreset q[0];\nh q[0];\n",
}
j["qasm"] = json!(q + "c = measure q;\n");
// A seed the engine does not read, which the provenance names as sent.
match rng.int(0, 3) {
0 => j["seed"] = json!("s".repeat(rng.int(1, 40_000) as usize)),
1 => j["seed"] = long_numbers(rng, 3000),
_ => {}
}
if j["shots"] == 0 && j.get("observables").is_none() && j.get("amplitudes").is_none() {
j["shots"] = json!(100);
}
("observables, amplitudes, mitigation, repair or a measurement in the middle".into(), j)
}
// A device target, its trajectories its own count.
5 => {
let mut j = json!({ "qasm": "OPENQASM 3.0; qubit[3] q; bit[3] c; h q[0]; cx q[0], q[1]; cx q[1], q[2]; c = measure q;", "target": "synthetic-ring-8", "shots": rng.int(1, 3000), "seed": 2 });
if rng.coin() {
j["method"] = json!("trajectories");
j["options"] = json!({ "trajectories": rng.int(1, 30_000) });
}
("on a device target".into(), j)
}
// A pulse run with its bits named.
6 => {
let n = rng.int(1, 300) as usize;
let regs: String = (0..n.div_ceil(64)).map(|r| format!("bit[{}] b{r};\n", 64.min(n - 64 * r))).collect();
let q = format!("OPENQASM 3.0;\ndefcalgrammar \"openpulse\";\ncal {{\n extern port d0;\n extern port a0;\n frame q0 = newframe(d0, 5.0e9, 0);\n frame c0 = newframe(a0, 7.1e9, 0);\n}}\ndefcal x $0 {{ play(q0, gaussian(0.4, 64dt, 16dt)); }}\ndefcal measure $0 -> bit {{ return capture_v2(c0, 4dt); }}\n{regs}x $0;\nb{}[{}] = measure $0;\n", (n - 1) / 64, (n - 1) % 64);
("a pulse run".into(), json!({ "method": "pulse", "shots": rng.int(1, 2000), "qasm": q, "verdict": rng.coin() }))
}
// A distribution, a reference and a state, as intents, with long numbers.
_ => {
let k = rng.int(1, 6);
let w = rng.int(k, 12);
let src = fanned(k, w);
let mut d = serde_json::Map::new();
for i in 0..rng.int(1, 80) {
d.insert(format!("{:0w$b}", i % (1 << w.min(16)), w = w as usize), json!(0.0));
}
let first = d.keys().next().cloned().unwrap();
d.insert(first, json!(1.0));
let mut intents = vec![json!({ "distribution": d, "tolerance": 1e-6, "note": long_numbers(rng, 200) }), json!({ "equivalent": src, "name": "itself" }), json!({ "prepares": fanned(k, w) })];
intents.truncate(rng.int(1, 3) as usize);
("a distribution, a reference and a state".into(), json!({ "qasm": src, "shots": rng.int(1, 3000), "intent": intents }))
}
}
}
/// Every random job's result is within the bound its plan gave. The seed is
/// `WAI_BOUND_SEED` if set, so a failure can be read again.
#[test]
#[cfg(all(feature = "quantum_qasm", feature = "quantum_noise", feature = "quantum_receipt"))]
fn no_random_job_s_result_passes_its_bound() {
let seed = std::env::var("WAI_BOUND_SEED").ok().and_then(|s| s.parse().ok()).unwrap_or(676);
let mut rng = Rng(seed);
let (mut checked, mut refused) = (0, 0);
for i in 0..96u64 {
let (what, job) = job(&mut rng, i % 8);
let (p, bound) = match plan_bounded(&job, CAP) {
Ok((p, Some(b))) => (p, b),
Ok((_, None)) => panic!("job {i} (seed {seed}), {what}: no bound"),
Err(e) => {
eprintln!("job {i}, {what}: not planned: {e}");
refused += 1;
continue;
}
};
let result = match run_with_provenance(&job, &p, "a host 1.0") {
Ok(r) => r,
Err(e) => {
eprintln!("job {i}, {what}: did not run: {e}");
refused += 1;
continue;
}
};
let len = served(&result);
eprintln!("job {i}, {what}: {len} bytes, bound {bound}");
assert!(len <= bound, "job {i} (seed {seed}), {what}: its result is {len} bytes, past its bound of {bound}\n{job}");
checked += 1;
}
assert!(checked >= 80 && refused <= 16, "{checked} checked, {refused} not run");
}
/// The jobs of round 6's review, each past its bound before: a sampled
/// intent ruling out thousands of 64-bit outcomes, a trajectories run of one
/// shot and 30,000 trajectories, and a verdict over 3,000 notes.
#[test]
#[cfg(all(feature = "quantum_qasm", feature = "quantum_noise", feature = "quantum_receipt"))]
fn a_failing_intent_trajectories_and_notes_stay_within_their_bounds() {
let zeros = "0".repeat(64);
let jobs = [
("a sampled intent ruling out 4,095 outcomes", json!({ "qasm": fanned(12, 64), "shots": 1, "intent": [{ "sampled": { zeros.clone(): 1.0 }, "test": "chi_squared", "alpha": 0.01, "shots": 40_000 }, { "sampled": { zeros: 1.0 }, "test": "g_test", "alpha": 0.01, "shots": 40_000 }] })),
("one shot, 30,000 trajectories", json!({ "qasm": fanned(12, 12).replace("cx", "// cx"), "method": "trajectories", "shots": 1, "noise": { "depolarizing_1q": 1e-6 }, "options": { "trajectories": 30_000 } })),
("3,000 notes and a verdict", json!({ "qasm": format!("OPENQASM 3.0;\nqubit q;\nbit c;\n{}h q;\nc = measure q;\n", (0..3000).map(|i| format!("pragma s{i}\n")).collect::<String>()), "shots": 10, "verdict": true })),
];
let mut past = Vec::new();
for (what, job) in jobs {
let (p, bound) = plan_bounded(&job, CAP).unwrap();
let bound = bound.unwrap();
let result = run_with_provenance(&job, &p, "a host 1.0").unwrap();
let len = served(&result);
eprintln!("{what}: {len} bytes, bound {bound}");
if len > bound {
past.push(format!("{what}: its result is {len} bytes, past its bound of {bound}"));
}
}
assert!(past.is_empty(), "{past:#?}");
}
/// Where the method knows how many outcomes there can be, or the critique
/// has nothing to say, the bound is close to the result: a GHZ state of 24
/// qubits at 30,000 shots on the stabilizer method lists two outcomes, and a
/// loop of 124,700 gates that cancel nowhere has no remark, so neither is
/// bounded as if it listed every shot or remarked on every gate.
#[test]
#[cfg(all(feature = "quantum_qasm", feature = "quantum_noise", feature = "quantum_receipt"))]
fn a_ghz_state_and_a_long_loop_are_bounded_by_what_they_can_list() {
let mut ghz = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[24] q;\nbit[24] c;\nh q[0];\n".to_string();
for i in 1..24 {
ghz += &format!("cx q[{}], q[{i}];\n", i - 1);
}
ghz += "c = measure q;\n";
let lp = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[8] q;\nbit[8] c;\nfor int s in [1:4300] {\n cx q[0], q[1]; rz(0.1) q[1]; cx q[0], q[1];\n cx q[1], q[2]; rz(0.2) q[2]; cx q[1], q[2];\n cx q[2], q[3]; rz(0.3) q[3]; cx q[2], q[3];\n cx q[3], q[4]; rz(0.4) q[4]; cx q[3], q[4];\n cx q[4], q[5]; rz(0.5) q[5]; cx q[4], q[5];\n cx q[5], q[6]; rz(0.6) q[6]; cx q[5], q[6];\n cx q[6], q[7]; rz(0.7) q[7]; cx q[6], q[7];\n rx(0.1) q[0]; rx(0.1) q[1]; rx(0.1) q[2]; rx(0.1) q[3]; rx(0.1) q[4]; rx(0.1) q[5]; rx(0.1) q[6]; rx(0.1) q[7];\n}\nc = measure q;\n";
let mut loose = Vec::new();
for (what, job, most) in [
("GHZ24, 30,000 shots, stabilizer", json!({ "qasm": ghz.clone(), "shots": 30_000, "method": "stabilizer", "seed": 5 }), 32u64 << 10),
("GHZ24, 30,000 shots, planned", json!({ "qasm": ghz, "shots": 30_000, "seed": 5 }), 32 << 10),
("a loop of 124,700 gates, with its verdict", json!({ "qasm": lp, "shots": 1000, "verdict": true }), 256 << 10),
] {
let (p, bound) = plan_bounded(&job, CAP).unwrap();
let bound = bound.unwrap();
let result = run_with_provenance(&job, &p, "a host 1.0").unwrap();
let len = served(&result);
eprintln!("{what}: {len} bytes, bound {bound}, on {}", p.method);
assert!(len <= bound, "{what}: its result is {len} bytes, past its bound of {bound}");
if bound > most {
loose.push(format!("{what}: bounded at {bound} bytes, where its result is {len}"));
}
}
assert!(loose.is_empty(), "{loose:#?}");
}

View file

@ -322,6 +322,12 @@ pub struct Request {
/// With `"mitigate"`: the mitigation asked for, as given (read and
/// checked by [`read`]; run after the job, its result under `mitigation`).
pub mitigate: Option<Value>,
/// For a job that asks for a verdict: the most its critique takes as the
/// verdict lists it, bytes, from the program as read, before any compile
/// ([`super::critique::smells_bound`]; a program holding a gate whole,
/// which the critique reads in standard gates, at [`REMARKS_PER_GATE`] a
/// standard gate and a qubit). 0 for one that asks for none.
pub smells_bytes: u64,
}
impl Request {
@ -342,6 +348,7 @@ impl Request {
compile_passes: self.compile_passes.clone(),
repaired: self.repaired.clone(),
mitigate: self.mitigate.clone(),
smells_bytes: self.smells_bytes,
})
}
}
@ -620,6 +627,14 @@ pub(crate) fn read_form(reg: &Registry, v: &Value, whole: bool) -> Result<(Reque
if let Some(i) = v.get("intent").filter(|i| !i.is_null()) {
super::verdict::fit(&super::verdict::intents(i)?, &program)?;
}
// A verdict's critique reads the program as given, not as compiled.
let smells_bytes = if !super::verdict::asked(v) {
0
} else if program.has_whole() {
u64::try_from(program.standard_gates()).unwrap_or(u64::MAX).saturating_add(u64::from(program.n_qubits)).saturating_mul(REMARKS_PER_GATE)
} else {
super::critique::smells_bound(&program)
};
// "compile": {"epsilon", "level", "basis"}: the job runs the compiled
// program, and its result says how far that is from the one given.
let (program, compiled, compile_passes) = match v.get("compile") {
@ -709,6 +724,7 @@ pub(crate) fn read_form(reg: &Registry, v: &Value, whole: bool) -> Result<(Reque
compile_passes,
repaired,
mitigate,
smells_bytes,
}, uses))
}
@ -1249,6 +1265,198 @@ pub fn plan(v: &Value, job_bytes: u64) -> Result<Plan, String> {
plan_in(Registry::builtins(), v, job_bytes)
}
/// [`plan`], with the most its result may take as JSON text, in bytes,
/// beside what a host adds to it (a receipt, an energy figure), known before
/// it runs ([`result_bound`]); a method that reads its own program bounds
/// its own ([`Method::plan_bounded`]: a pulse run's is
/// [`super::pulse::plan_bounded`]), and `None` is one that gives no bound,
/// which no built-in is.
pub fn plan_bounded(v: &Value, job_bytes: u64) -> Result<(Plan, Option<u64>), String> {
plan_bounded_in(Registry::builtins(), v, job_bytes).map(|(p, b)| (p, b.map(|b| b.bytes())))
}
/// [`plan_bounded`], the bound given by part ([`Bound`]).
pub fn plan_bound(v: &Value, job_bytes: u64) -> Result<(Plan, Option<Bound>), String> {
plan_bounded_in(Registry::builtins(), v, job_bytes)
}
/// The most a job's result takes as JSON text, bytes, part by part, known
/// before it runs ([`result_bound`]): what a host holds a result to before
/// it runs, and what a refusal past its cap names (the largest part,
/// [`Bound::largest`]).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Bound {
/// Its counts (and a mitigation's), each listing up to the samples it
/// takes, the outcomes its program can give and 65,536.
pub counts: u64,
/// A verdict's: its critique's remarks, each intent's check, and the
/// reader's notes and the repair again.
pub verdict: u64,
/// The reader's warnings and notes on the program.
pub notes: u64,
/// Each item's request, which a set serves beside its result.
pub requests: u64,
/// The rest: expectation values, amplitudes, outputs, a compile's,
/// repair's or target's account, and the fields every result has.
pub rest: u64,
}
impl Bound {
/// The whole bound, bytes.
pub fn bytes(&self) -> u64 {
self.counts.saturating_add(self.verdict).saturating_add(self.notes).saturating_add(self.requests).saturating_add(self.rest)
}
/// Both bounds together, part by part: a set's items'.
pub fn plus(self, o: Bound) -> Bound {
Bound {
counts: self.counts.saturating_add(o.counts),
verdict: self.verdict.saturating_add(o.verdict),
notes: self.notes.saturating_add(o.notes),
requests: self.requests.saturating_add(o.requests),
rest: self.rest.saturating_add(o.rest),
}
}
/// The largest part: its bytes, what it is, and what makes it smaller.
pub fn largest(&self) -> (u64, &'static str, &'static str) {
let parts = [
(self.counts, "its counts, listing up to the shots (or a trajectories run's trajectories), the outcomes its measured bits can give and 65,536 outcomes an item", "send fewer points, fewer shots or fewer measured bits"),
(self.verdict, "its verdict: the critique's remark on each gate that does nothing, cancels or merges, each intent's check, and the reader's notes again", "ask for no verdict (leave out \"verdict\" and \"intent\", and send it to POST /v1/jobs, not an MCP run tool, which asks for one), or send fewer points"),
(self.notes, "the reader's warnings and notes on its program, one for each line they are on", "remove or fix the lines the reader notes (each `pragma` it does not use draws one), or send fewer points"),
(self.requests, "each item's request, which a set serves beside the item's result", "send fewer points, or keep large fields out of the set's request: each item serves the fields it shares"),
(self.rest, "its expectation values, amplitudes, outputs and its compile's, repair's or target's account", "ask for fewer observables, amplitudes or outputs, or send fewer points"),
];
parts.into_iter().fold(parts[0], |a, b| if b.0 > a.0 { b } else { a })
}
}
/// What a result holds beside the parts [`result_bound`] counts one by one:
/// its method, qubits and gates, its accuracy and its notes, its state's
/// hash, its schema, its timing and its provenance, the plan's reason apart.
const RESULT_REST: u64 = 16 << 10;
/// An expectation value's numbers and names, beside its observable's text
/// (a bound, a ladder of thresholds, a standard error).
const EXPECTATION_BYTES: u64 = 1 << 10;
/// An amplitude's numbers and names, beside its bit string.
const AMPLITUDE_BYTES: u64 = 1 << 9;
/// A verdict's own fields, beside its critique's remarks, its intents', the
/// notes and the repair it repeats.
const VERDICT_BYTES: u64 = 16 << 10;
/// One intent's check, beside the intent as given and the outcomes it may
/// list ([`super::verdict::MAX_LISTED`]).
const INTENT_BYTES: u64 = 16 << 10;
/// A critique's remarks on one gate of a program it reads in standard gates
/// where the program as read holds a gate whole, at most: a gate draws one
/// that it does nothing, and is one of a pair that cancels or merges, each
/// naming its lines and its qubits.
pub(crate) const REMARKS_PER_GATE: u64 = 512;
/// A mitigation's own fields, beside a value for each observable and a
/// count for each outcome.
const MITIGATION_BYTES: u64 = 64 << 10;
/// An outcome as an intent's check lists it, beside its bit string: quotes,
/// a colon, a comma, and a count or a probability at its longest.
const LISTED_BYTES: u64 = 32;
/// How many times an intent's check may list the outcomes it lists: a
/// sampled intent's counts, the outcomes of probability 0 it names (in its
/// fit, in the fit's note, and in its reason); a distribution's computed
/// outcomes and its largest gaps.
const LISTS_PER_INTENT: u64 = 4;
/// The samples a job's counts are drawn from: its shots, or for the
/// trajectories method (and a device target's), its trajectories
/// ([`trajectory_count`]), one a sample, which the counts list.
pub(crate) fn samples_listed(r: &Request, p: &Plan) -> u64 {
if r.shots > 0 && p.method == "trajectories" { trajectory_count(r).max(r.shots) } else { r.shots }
}
/// How many distinct outcomes a job's counts can list, at most: `2^b` for
/// the `b` classical bits its measurements write (the rest stay 0; every bit
/// of a program that measures in the middle), and for a noiseless run of the
/// stabilizer method, `2^d`, `d` its outcome dimension's bound: its measured
/// qubits, and the gates that branch (each `h`, `sx` and `sxdg` it runs as,
/// the rest permuting or phasing the basis states), each at most doubling
/// the outcomes a tableau can give. A GHZ state has 2, however wide.
pub(crate) fn outcomes_bound(r: &Request, p: &Plan) -> u64 {
let pow2 = |e: u64| if e >= 63 { u64::MAX } else { 1u64 << e };
let prog = &r.program;
if prog.dynamic.is_some() {
return pow2(u64::from(prog.n_bits));
}
let bits: std::collections::BTreeSet<u32> = prog.measured.iter().map(|&(_, b)| b).collect();
let mut e = bits.len() as u64;
if p.method == "stabilizer" && r.noise.is_none() && p.target.is_none() {
use crate::quantum_tableau::Clifford;
let qubits: std::collections::BTreeSet<u32> = prog.measured.iter().map(|&(q, _)| q).collect();
e = e.min(qubits.len() as u64);
if let Ok(gates) = prog.clifford() {
e = e.min(gates.iter().filter(|g| matches!(g, Clifford::H(_) | Clifford::SX(_) | Clifford::SXdg(_))).count() as u64);
}
}
pow2(e)
}
/// The most the result of the job `v`, read as `r` and planned as `p`, takes
/// as JSON text, bytes, by part ([`Bound`]): each part that grows with what
/// the job asks bounded by what it may list, from the job as read. Its
/// counts (and a mitigation's) by [`super::run::counts_bound_among`], of
/// the samples it lists ([`samples_listed`]) among the outcomes it can give
/// ([`outcomes_bound`]); each expectation value by its observable's text,
/// escaped, and a fixed allowance; each amplitude by its bit string and one;
/// what its reading put in it (its diagnostics, its outputs, its repair, its
/// compile and its target) at its length as RFC 8785 writes it
/// ([`super::receipt::canonical_len`], every number in its own form); a
/// verdict by its fields, the diagnostics and the repair it repeats, its
/// critique's remarks ([`Request::smells_bytes`]), and each intent as given
/// with the outcomes its check lists, [`super::verdict::MAX_LISTED`] at a
/// time (and two intents' reasons again, in its statement); and the rest at
/// [`RESULT_REST`]. The counts are written exactly so; a sixteenth more is
/// allowed for the rest, for the numbers the engine writes.
pub(crate) fn result_bound(v: &Value, r: &Request, p: &Plan) -> Bound {
let size = super::receipt::canonical_len;
// A string of the job's own, every character escaped at its longest.
let text = |t: &str| 6 * t.len() as u64 + 2;
let bits = r.program.n_bits;
let samples = samples_listed(r, p);
let outcomes = outcomes_bound(r, p);
let mut counts = super::run::counts_bound_among(bits, samples, outcomes, None);
// The provenance names the job's shots and seed as they were sent,
// whatever they are.
let mut rest = RESULT_REST + 2 * size(&json!(p.why)) + ["shots", "seed"].iter().filter_map(|k| v.get(*k)).map(size).sum::<u64>();
rest += r.observables.iter().map(|(t, _, _)| EXPECTATION_BYTES + text(t)).sum::<u64>();
rest += r.amplitudes.len() as u64 * (AMPLITUDE_BYTES + u64::from(r.program.n_qubits));
let notes = r.diagnostics.iter().map(|d| size(&d.to_json()) + 1).sum::<u64>();
rest += r.program.extra.outputs.iter().map(|o| 64 + text(&o.name) + text(&o.ty) + match &o.value {
qasm::OutputValue::Bits(bits) => 11 * bits.len() as u64,
qasm::OutputValue::Value(_, x) => size(x),
}).sum::<u64>();
let repaired = r.repaired.as_ref().map_or(0, size);
rest += repaired + r.compiled.as_ref().map_or(0, size) + r.compile_passes.as_ref().map_or(0, size);
rest += p.target.as_ref().map_or(0, size);
if r.mitigate.is_some() {
rest += MITIGATION_BYTES + r.observables.len() as u64 * EXPECTATION_BYTES;
counts = counts.saturating_add(super::run::counts_bound_among(bits, samples, outcomes, Some(24)));
}
let mut verdict = 0u64;
if super::verdict::asked(v) {
let intents: Vec<&Value> = match v.get("intent") {
Some(Value::Array(a)) => a.iter().collect(),
Some(i) if !i.is_null() => vec![i],
_ => Vec::new(),
};
let listed = super::verdict::MAX_LISTED as u64 * (u64::from(bits) + LISTED_BYTES);
let each = |i: &Value| INTENT_BYTES + 2 * size(i) + LISTS_PER_INTENT * listed;
// The statement says the reason of the first that fails and of the
// first left unchecked again.
let said = intents.iter().map(|i| each(i)).max().unwrap_or(0);
verdict = VERDICT_BYTES + repaired + notes + r.smells_bytes;
verdict = verdict.saturating_add(intents.iter().map(|i| each(i)).sum::<u64>()).saturating_add(2 * said);
}
let rest = rest.saturating_add((rest + notes + verdict) / 16);
Bound { counts, verdict, notes, requests: 0, rest }
}
/// The method `v` names, when it reads its job's program itself (a pulse
/// run: the pulse reader reads it, and the pulse simulator integrates it).
pub(crate) fn reads_own<'a>(reg: &'a Registry, v: &Value) -> Option<&'a dyn Method> {
@ -1257,8 +1465,18 @@ pub(crate) fn reads_own<'a>(reg: &'a Registry, v: &Value) -> Option<&'a dyn Meth
/// [`plan`] through `reg`: its methods, channels and passes.
pub(crate) fn plan_in(reg: &Registry, v: &Value, job_bytes: u64) -> Result<Plan, String> {
plan_bounded_in(reg, v, job_bytes).map(|(p, _)| p)
}
/// [`plan_bound`] through `reg`.
pub(crate) fn plan_bounded_in(reg: &Registry, v: &Value, job_bytes: u64) -> Result<(Plan, Option<Bound>), String> {
if let Some(m) = reads_own(reg, v) {
return m.plan(&Job::new(reg, v, job_bytes, None, &[]));
// The built-in pulse run bounds its result by part; another method
// that reads its own program, as one sum.
if m.name() == "pulse" && reg.identity(super::registry::Kind::Method, "pulse").is_some_and(|i| i.is_builtin()) {
return super::pulse::plan_bound(v, job_bytes).map(|(p, b)| (p, Some(b)));
}
return m.plan_bounded(&Job::new(reg, v, job_bytes, None, &[])).map(|(p, b)| (p, b.map(|rest| Bound { rest, ..Bound::default() })));
}
// A job that names a device target is compiled, placed and simulated as
// the device would run it (engine::targets::job).
@ -1268,7 +1486,10 @@ pub(crate) fn plan_in(reg: &Registry, v: &Value, job_bytes: u64) -> Result<Plan,
return Err(tag(refusal::FABRIC, "fabric: a job on a device target runs on the CPU, as the device's simulation; give fabric cpu, or leave it out"));
}
let p = crate::engine::targets::job::plan(v, job_bytes)?;
return with_intent(reg, v, &read_in(reg, v)?.0, p, job_bytes);
let r = read_in(reg, v)?.0;
let p = with_intent(reg, v, &r, p, job_bytes)?;
let bound = result_bound(v, &r, &p);
return Ok((p, Some(bound)));
}
#[cfg(not(feature = "quantum_receipt"))]
if v.get("target").is_some_and(|t| !t.is_null()) {
@ -1286,7 +1507,9 @@ pub(crate) fn plan_in(reg: &Registry, v: &Value, job_bytes: u64) -> Result<Plan,
}
let mut p = choose_placed(&job)?;
super::fabric::admit(v, &r, &mut p)?;
with_intent(reg, v, &r, p, job_bytes)
let p = with_intent(reg, v, &r, p, job_bytes)?;
let bound = result_bound(v, &r, &p);
Ok((p, Some(bound)))
}
/// [`choose`], except that a job asking for a member of the fabric

View file

@ -313,6 +313,84 @@ fn qubit_list(qs: &[u32]) -> String {
}
}
/// A smell's place in the program's ops: an op that does nothing, or two in a
/// row that cancel or merge.
enum Site {
Nothing(usize),
Cancels(usize, usize),
Merges(usize, usize),
}
/// Each smell of the gates of `p` as it is found, in order: the walk
/// [`smells`] lists and [`smells_bound`] bounds, so the two never differ.
fn sites(p: &Program, mut found: impl FnMut(Site)) {
let n = p.n_qubits as usize;
// The order the ops run in, with a barrier (None) at each measurement,
// reset or condition of a program that measures in the middle.
let order: Box<dyn Iterator<Item = Option<usize>>> = match &p.dynamic {
None => Box::new((0..p.ops.len()).map(Some)),
Some(steps) => Box::new(steps.iter().map(|s| if let Instr::Gate(k) = s { Some(*k) } else { None })),
};
let mut last: Vec<Option<usize>> = vec![None; n];
let mut used = vec![false; p.ops.len()];
for step in order {
let Some(j) = step else {
last.iter_mut().for_each(|l| *l = None);
continue;
};
let b = &p.ops[j];
if does_nothing(b) {
used[j] = true;
found(Site::Nothing(j));
}
let prev: Vec<Option<usize>> = b.qubits.iter().map(|&q| last[q as usize]).collect();
if let Some(Some(i)) = prev.first().copied() {
let a = &p.ops[i];
let adjacent = prev.iter().all(|x| *x == Some(i)) && a.qubits.len() == b.qubits.len() && !used[i] && !used[j];
if adjacent && cancels(a, b) {
used[i] = true;
used[j] = true;
found(Site::Cancels(i, j));
} else if adjacent && merges(a, b) {
used[i] = true;
used[j] = true;
found(Site::Merges(i, j));
}
}
for &q in &b.qubits {
last[q as usize] = Some(j);
}
}
}
/// What one smell takes as a verdict lists it (`{"kind", "message",
/// "lines"}`), bytes, at most, beside the names of its qubits: its kind, the
/// longest message's words and gate names, and three line numbers of ten
/// digits.
const SMELL_BYTES: u64 = 256;
/// A qubit's index in a smell's message, at most: ten digits and ", ".
const QUBIT_BYTES: u64 = 12;
/// The most the smells of `src` take as a verdict lists them, bytes, where
/// `p` is `src` as read: each smell the walk of its gates finds
/// ([`SMELL_BYTES`] and its qubits' names), and the idle and unmeasured
/// qubits, at most every qubit each. A program's critique grows with its
/// smells, not with its gates: a loop of a hundred thousand gates that
/// cancel nowhere has none.
pub fn smells_bound(p: &Program) -> u64 {
let arity = |k: usize| p.ops[k].qubits.len() as u64;
let mut b = 0u64;
sites(p, |s| {
b += SMELL_BYTES
+ QUBIT_BYTES
* match s {
Site::Nothing(j) => arity(j),
Site::Cancels(i, _) | Site::Merges(i, _) => arity(i),
};
});
b + 2 * (SMELL_BYTES + QUBIT_BYTES * u64::from(p.n_qubits))
}
fn smells(src: &str, p: &Program, lines: &[usize]) -> Vec<Smell> {
let text: Vec<&str> = src.split('\n').collect();
let n = p.n_qubits as usize;
@ -329,22 +407,9 @@ fn smells(src: &str, p: &Program, lines: &[usize]) -> Vec<Smell> {
statement(text.get(l.wrapping_sub(1))?)
};
let mut out = Vec::new();
// The order the ops run in, with a barrier (None) at each measurement,
// reset or condition of a program that measures in the middle.
let order: Vec<Option<usize>> = match &p.dynamic {
None => (0..p.ops.len()).map(Some).collect(),
Some(steps) => steps.iter().map(|s| if let Instr::Gate(k) = s { Some(*k) } else { None }).collect(),
};
let mut last: Vec<Option<usize>> = vec![None; n];
let mut used = vec![false; p.ops.len()];
for step in order {
let Some(j) = step else {
last.iter_mut().for_each(|l| *l = None);
continue;
};
let b = &p.ops[j];
if does_nothing(b) {
used[j] = true;
sites(p, |site| match site {
Site::Nothing(j) => {
let b = &p.ops[j];
out.push(Smell {
kind: "does_nothing",
message: format!("{} on {} (line {}) does nothing, up to a global phase", kind_name(b.kind), qubit_list(&b.qubits), lines[j]),
@ -354,43 +419,34 @@ fn smells(src: &str, p: &Program, lines: &[usize]) -> Vec<Smell> {
fix: alone(j).map(|_| vec![Edit::Delete(lines[j])]),
});
}
let prev: Vec<Option<usize>> = b.qubits.iter().map(|&q| last[q as usize]).collect();
if let Some(Some(i)) = prev.first().copied() {
Site::Cancels(i, j) => {
let (a, b) = (&p.ops[i], &p.ops[j]);
let what = if a.kind == b.kind { format!("{} on {} twice in a row", kind_name(a.kind), qubit_list(&a.qubits)) } else { format!("{} then {} on {}", kind_name(a.kind), kind_name(b.kind), qubit_list(&a.qubits)) };
let fix = (alone(i).is_some() && alone(j).is_some() && lines[i] != lines[j]).then(|| vec![Edit::Delete(lines[i]), Edit::Delete(lines[j])]);
out.push(Smell { kind: "cancels", message: format!("{what} (lines {} and {}): together they do nothing", lines[i], lines[j]), lines: vec![lines[i], lines[j]], ops: vec![i, j], qubits: a.qubits.clone(), fix });
}
Site::Merges(i, j) => {
let a = &p.ops[i];
let adjacent = prev.iter().all(|x| *x == Some(i)) && a.qubits.len() == b.qubits.len() && !used[i] && !used[j];
if adjacent && cancels(a, b) {
used[i] = true;
used[j] = true;
let what = if a.kind == b.kind { format!("{} on {} twice in a row", kind_name(a.kind), qubit_list(&a.qubits)) } else { format!("{} then {} on {}", kind_name(a.kind), kind_name(b.kind), qubit_list(&a.qubits)) };
let fix = (alone(i).is_some() && alone(j).is_some() && lines[i] != lines[j]).then(|| vec![Edit::Delete(lines[i]), Edit::Delete(lines[j])]);
out.push(Smell { kind: "cancels", message: format!("{what} (lines {} and {}): together they do nothing", lines[i], lines[j]), lines: vec![lines[i], lines[j]], ops: vec![i, j], qubits: a.qubits.clone(), fix });
} else if adjacent && merges(a, b) {
used[i] = true;
used[j] = true;
let fix = match (alone(i), alone(j)) {
(Some(sa), Some(sb)) if lines[i] != lines[j] && sa.name == sb.name => match (&sa.params, &sb.params) {
(Some(ea), Some(eb)) if !ea.is_empty() && !eb.is_empty() => Some(vec![
Edit::Replace(lines[i], format!("{}{}({ea} + {eb}) {};{}", sa.indent, sa.name, sa.args, if sa.comment.is_empty() { String::new() } else { format!(" {}", sa.comment) })),
Edit::Delete(lines[j]),
]),
_ => None,
},
let fix = match (alone(i), alone(j)) {
(Some(sa), Some(sb)) if lines[i] != lines[j] && sa.name == sb.name => match (&sa.params, &sb.params) {
(Some(ea), Some(eb)) if !ea.is_empty() && !eb.is_empty() => Some(vec![
Edit::Replace(lines[i], format!("{}{}({ea} + {eb}) {};{}", sa.indent, sa.name, sa.args, if sa.comment.is_empty() { String::new() } else { format!(" {}", sa.comment) })),
Edit::Delete(lines[j]),
]),
_ => None,
};
out.push(Smell {
kind: "merges",
message: format!("{} on {} twice in a row (lines {} and {}): one {} with the angles added", kind_name(a.kind), qubit_list(&a.qubits), lines[i], lines[j], kind_name(a.kind)),
lines: vec![lines[i], lines[j]],
ops: vec![i, j],
qubits: a.qubits.clone(),
fix,
});
}
},
_ => None,
};
out.push(Smell {
kind: "merges",
message: format!("{} on {} twice in a row (lines {} and {}): one {} with the angles added", kind_name(a.kind), qubit_list(&a.qubits), lines[i], lines[j], kind_name(a.kind)),
lines: vec![lines[i], lines[j]],
ops: vec![i, j],
qubits: a.qubits.clone(),
fix,
});
}
for &q in &b.qubits {
last[q as usize] = Some(j);
}
}
});
// Qubits nothing uses, and qubits acted on but never measured.
let mut touched = vec![false; n];
for op in &p.ops {

View file

@ -246,58 +246,73 @@ impl Walk<'_> {
/// The exact mixture's expectation values from step `pc`, the branch
/// weighted by `weight`, added into `sums`; `leaves` counts the branches.
///
/// The branches still to finish wait on a list, the latest first: at a
/// measurement with two outcomes, outcome 0 runs at once in a copy and
/// outcome 1 waits in this state. The leaves are reached in the order a
/// depth-first walk reaches them (so the sums are the same bits), and no
/// recursion follows the program's measurements, however many it makes.
#[allow(clippy::too_many_arguments)]
fn expect<E: Engine>(&mut self, mut pc: usize, mut state: E, mut bits: Vec<bool>, weight: f64, observables: &[Vec<(u32, char)>], sums: &mut [f64], leaves: &mut u64) -> Result<(), String> {
while pc < self.steps.len() {
match &self.steps[pc] {
Instr::Gate(i) => state.gate(*i)?,
Instr::Measure { .. } if self.skip.get(pc) == Some(&true) => {}
Instr::Measure { qubit, .. } | Instr::Reset(qubit) => {
let q = *qubit;
let (bit, reset) = step_target(&self.steps[pc]);
let p1 = state.prob_one(q)?;
let branches: Vec<(bool, f64)> = [(false, 1.0 - p1), (true, p1)].into_iter().filter(|&(_, p)| p > NEGLIGIBLE).collect();
if let [(o, p), _] = branches[..] {
// Outcome 0 in a copy; outcome 1 in this state.
let mut child = self.copy(&state)?;
settle(&mut child, q, o, reset)?;
let mut child_bits = bits.clone();
if let Some(b) = bit {
child_bits[b] = o;
fn expect<E: Engine>(&mut self, pc: usize, state: E, bits: Vec<bool>, weight: f64, observables: &[Vec<(u32, char)>], sums: &mut [f64], leaves: &mut u64) -> Result<(), String> {
let mut todo = vec![(pc, state, bits, weight)];
while let Some((mut pc, mut state, mut bits, mut weight)) = todo.pop() {
while pc < self.steps.len() {
match &self.steps[pc] {
Instr::Gate(i) => state.gate(*i)?,
Instr::Measure { .. } if self.skip.get(pc) == Some(&true) => {}
Instr::Measure { qubit, .. } | Instr::Reset(qubit) => {
let q = *qubit;
let (bit, reset) = step_target(&self.steps[pc]);
let p1 = state.prob_one(q)?;
let branches: Vec<(bool, f64)> = [(false, 1.0 - p1), (true, p1)].into_iter().filter(|&(_, p)| p > NEGLIGIBLE).collect();
let (o, p) = *branches.last().ok_or("a measurement with no possible outcome")?;
if let [(o0, p0), _] = branches[..] {
// Outcome 0 in a copy, run now; outcome 1 in this state, after it.
let mut child = self.copy(&state)?;
settle(&mut child, q, o0, reset)?;
let mut child_bits = bits.clone();
if let Some(b) = bit {
child_bits[b] = o0;
}
settle(&mut state, q, o, reset)?;
if let Some(b) = bit {
bits[b] = o;
}
todo.push((pc + 1, std::mem::replace(&mut state, child), std::mem::replace(&mut bits, child_bits), weight * p));
weight *= p0;
} else {
settle(&mut state, q, o, reset)?;
if let Some(b) = bit {
bits[b] = o;
}
}
self.expect(pc + 1, child, child_bits, weight * p, observables, sums, leaves)?;
self.live -= 1;
}
let (o, p) = *branches.last().ok_or("a measurement with no possible outcome")?;
settle(&mut state, q, o, reset)?;
if let Some(b) = bit {
bits[b] = o;
Instr::JumpUnless(cond, to) => {
if !cond.holds(&bits) {
pc = *to;
continue;
}
}
if branches.len() == 2 {
return self.expect(pc + 1, state, bits, weight * p, observables, sums, leaves);
}
}
Instr::JumpUnless(cond, to) => {
if !cond.holds(&bits) {
Instr::Jump(to) => {
pc = *to;
continue;
}
}
Instr::Jump(to) => {
pc = *to;
continue;
}
pc += 1;
}
*leaves += 1;
if *leaves > MAX_LEAVES {
return Err(format!(
"this program's measurements branch {MAX_LEAVES}+ ways, more than the exact expectation values follow; ask for counts, whose shots split only where they fall"
));
}
for (s, o) in sums.iter_mut().zip(observables) {
*s += weight * state.expectation(o)?;
}
// This branch's state is done with; the next waiting one is alive.
if !todo.is_empty() {
self.live -= 1;
}
pc += 1;
}
*leaves += 1;
if *leaves > MAX_LEAVES {
return Err(format!(
"this program's measurements branch {MAX_LEAVES}+ ways, more than the exact expectation values follow; ask for counts, whose shots split only where they fall"
));
}
for (s, o) in sums.iter_mut().zip(observables) {
*s += weight * state.expectation(o)?;
}
Ok(())
}
@ -382,3 +397,42 @@ fn settle<E: Engine>(state: &mut E, q: u32, outcome: bool, reset: bool) -> Resul
pub fn measurements(steps: &[Instr]) -> usize {
steps.iter().filter(|s| matches!(s, Instr::Measure { .. } | Instr::Reset(_))).count()
}
#[cfg(test)]
mod tests {
use serde_json::json;
/// A program whose measurements branch thousands of times, each under
/// the last one's outcome, is walked without recursion: past
/// [`super::MAX_LEAVES`] the walk is refused, on a 2 MiB stack, rather
/// than recursing as deep as the program measures.
#[test]
fn a_program_that_branches_thousands_deep_is_refused_not_recursed() {
let src = format!("OPENQASM 3.0;\nqubit q;\nbit c;\nx q;\nc = measure q;\n{}", "if (c) { h q; c = measure q; }\n".repeat(5_000));
let r = std::thread::Builder::new()
.stack_size(2 << 20)
.spawn(move || {
let req = json!({ "qasm": src, "observables": ["Z0"], "method": "statevector" });
let plan = crate::engine::catalog::plan(&req, 1 << 30)?;
crate::engine::run::run(&req, &plan)
})
.unwrap()
.join()
.unwrap();
let e = r.unwrap_err();
assert!(e.contains("4096+"), "{e}");
}
/// Walked with a list rather than a recursion, the branches are summed
/// into the exact mixture: q0 turned by 1 reads 1 with probability
/// sin²(½), and q1 copies it, so ⟨Z0⟩ = ⟨Z1⟩ = cos 1 and ⟨X1⟩ = 0.
#[test]
fn the_branches_sum_to_the_exact_mixture() {
let req = json!({ "qasm": "OPENQASM 3.0;\nqubit[2] q;\nbit c;\nry(1.0) q[0];\nc = measure q[0];\nif (c) x q[1];\n", "observables": ["Z0", "Z1", "X1"], "method": "statevector" });
let plan = crate::engine::catalog::plan(&req, 1 << 30).unwrap();
let r = crate::engine::run::run(&req, &plan).unwrap();
let v: Vec<f64> = r["expectations"].as_array().unwrap().iter().map(|x| x["value"].as_f64().unwrap()).collect();
assert!((v[0] - 1f64.cos()).abs() < 1e-12 && (v[1] - 1f64.cos()).abs() < 1e-12 && v[2].abs() < 1e-12, "{v:?}");
}
}

View file

@ -180,7 +180,7 @@ impl Index {
/// The index of a lexed program.
pub fn build(src: &str, lexed: &Lexed) -> Index {
let toks: Vec<Token> = lexed.tokens.iter().copied().filter(|t| !t.is_comment()).collect();
let mut b = Builder { src, t: &toks, pos: 0, ix: Index::default(), scope: 0 };
let mut b = Builder { src, t: &toks, pos: 0, ix: Index::default(), scope: 0, depth: 0 };
b.ix.scopes.push(Scope { kind: ScopeKind::Global, parent: None, span: (0, src.len()) });
while b.pos < toks.len() {
let before = b.pos;
@ -294,8 +294,16 @@ struct Builder<'a> {
pos: usize,
ix: Index,
scope: usize,
/// How many statements the builder is inside ([`MAX_DEPTH`]).
depth: usize,
}
/// How deep statements nest for the index, as the reader takes them
/// ([`crate::qasm::parse::MAX_NESTING`]): a statement deeper is passed over
/// whole, its names not indexed, so a text nested past it (which the reader
/// refuses) never runs the builder out of stack.
const MAX_DEPTH: usize = crate::qasm::parse::MAX_NESTING;
impl Builder<'_> {
fn tok(&self, i: usize) -> Option<Token> {
self.t.get(i).copied()
@ -474,6 +482,42 @@ impl Builder<'_> {
}
fn statement(&mut self) {
if self.depth >= MAX_DEPTH {
return self.pass_over();
}
self.depth += 1;
self.statement_here();
self.depth -= 1;
}
/// The statement at the cursor passed over without recursion: to its
/// `;` (read) or its block's `}` (read), or up to the `}` that closes
/// the block it stands in (not read).
fn pass_over(&mut self) {
let mut depth = 0usize;
while self.pos < self.t.len() {
match self.text(self.pos) {
"(" | "[" | "{" => depth += 1,
")" | "]" => depth = depth.saturating_sub(1),
"}" if depth == 0 => return,
"}" => {
depth -= 1;
if depth == 0 {
self.pos += 1;
return;
}
}
";" if depth == 0 => {
self.pos += 1;
return;
}
_ => {}
}
self.pos += 1;
}
}
fn statement_here(&mut self) {
let Some(tok) = self.tok(self.pos) else { return };
let stmt = tok.start;
match tok.kind {
@ -1038,4 +1082,30 @@ mod tests {
assert!(!ix.visible(0).iter().any(|&i| ix.decls[i].name == "i"));
assert_eq!(ix.callable_of(ix.scope_at(inside_gate)).map(|d| ix.decls[d].name.as_str()), Some("g"));
}
/// Statements nested past the reader's limit (which the reader refuses)
/// are passed over, not recursed into: braces a megabyte deep, unclosed,
/// or `if` after `if`, index on a 2 MiB stack, and what stands beside
/// them is still found.
#[test]
fn nesting_past_the_reader_s_limit_is_passed_over_not_recursed_into() {
let on_stack = |src: String| std::thread::Builder::new().stack_size(2 << 20).spawn(move || index(&src)).unwrap().join().unwrap();
for src in [
format!("qubit[2] q;\n{}h q[0];\n", "{ ".repeat(200_000)),
format!("qubit[2] q;\n{}h q[0];{}\nbit c;\n", "{ ".repeat(100_000), " }".repeat(100_000)),
format!("qubit[2] q;\n{}h q[0];\nbit c;\n", "if (true) ".repeat(100_000)),
format!("qubit[2] q;\ngate g a {{ {}h a;{} }}\nbit c;\n", "{ ".repeat(100_000), " }".repeat(100_000)),
] {
let closed = src.contains("bit c");
let ix = on_stack(src);
assert_eq!(decl(&ix, "q").kind, DeclKind::QubitRegister);
if closed {
assert_eq!(decl(&ix, "c").kind, DeclKind::BitRegister);
}
}
// Nested within the limit, every use is still found.
let src = format!("qubit[2] q;\n{}h q[0];{}\n", "{ ".repeat(40), " }".repeat(40));
let ix = on_stack(src);
assert!(ix.refs.iter().any(|r| r.name == "q" && r.decl.is_some()));
}
}

View file

@ -106,8 +106,11 @@ impl Cond {
/// The most terms a condition may have.
pub(crate) const MAX_COND: usize = 1024;
/// The deepest a condition's parentheses may nest once written.
pub(crate) const MAX_COND_DEPTH: usize = 32;
/// The deepest a condition's parentheses may nest once written: what the
/// reader takes of it (a level of parentheses costs it two of
/// [`crate::qasm::MAX_NESTING`], the parenthesis and the operand it is), so a
/// condition that fits this reads back.
pub(crate) const MAX_COND_DEPTH: usize = 30;
/// One statement.
#[derive(Clone, Debug, PartialEq)]
@ -222,6 +225,24 @@ pub(crate) fn cond_fits(c: &Cond) -> bool {
mod tests {
use super::*;
/// A condition within the converters' bounds reads back: a flat run as
/// long as they take, and parentheses as deep, of either operator.
#[test]
fn a_condition_that_fits_reads_back() {
let reads = |c: Cond| {
assert!(cond_fits(&c), "size {} depth {}", c.size(), c.depth());
let n = 64;
let body = vec![Stmt::Measure { qubit: 0, bit: 0 }, Stmt::If { cond: c, then: vec![Stmt::gate("x", vec![], vec![0])], other: vec![] }];
let text = Circuit { n_qubits: 1, n_bits: n, body }.to_qasm("read from a test");
crate::qasm::read(&text).unwrap_or_else(|d| panic!("{d}: {text}"));
};
let bit = |k: usize| Cond::Bit((k % 64) as u32);
let run = |and: bool, n: usize| (1..n).fold(bit(0), |a, k| if and { Cond::And(Box::new(a), Box::new(bit(k))) } else { Cond::Or(Box::new(a), Box::new(bit(k))) });
reads(run(true, MAX_COND / 2));
reads(run(false, MAX_COND / 2));
reads((0..MAX_COND_DEPTH).fold(bit(0), |c, k| if k % 2 == 0 { Cond::And(Box::new(bit(k + 1)), Box::new(c)) } else { Cond::Or(Box::new(bit(k + 1)), Box::new(c)) }));
}
#[test]
fn a_circuit_writes_as_openqasm_and_reads_back() {
let c = Circuit {

View file

@ -194,6 +194,8 @@ pub mod algorithms;
#[cfg(feature = "quantum_receipt")]
pub mod answer;
pub mod catalog;
#[cfg(test)]
mod bound_tests;
pub mod compile;
#[cfg(feature = "quantum_receipt")]
pub mod converge;

View file

@ -436,6 +436,88 @@ pub fn lint(request: &Value) -> Value {
/// memory and the work, and why.
pub fn plan(v: &Value, job_bytes: u64) -> Result<Plan, String> {
let (_, p) = prepare(v)?;
plan_of(&p, job_bytes)
}
/// [`plan`], with the most the run's result ([`run`]) takes as JSON text,
/// bytes, known before it runs, from the same reading: as
/// [`super::catalog::plan_bounded`] gives a circuit's.
pub fn plan_bounded(v: &Value, job_bytes: u64) -> Result<(Plan, u64), String> {
plan_bound(v, job_bytes).map(|(p, b)| (p, b.bytes()))
}
/// [`plan_bounded`], the bound given by part.
pub fn plan_bound(v: &Value, job_bytes: u64) -> Result<(Plan, super::catalog::Bound), String> {
let (_, p) = prepare(v)?;
let plan = plan_of(&p, job_bytes)?;
let bound = result_bound(&p, &plan);
Ok((plan, bound))
}
/// What a pulse result holds beside the parts [`result_bound`] counts one by
/// one: its accuracy, its integrator, its notes, its digests, its schema,
/// its verdict's own fields and its provenance, the plan's reason apart.
const RESULT_REST: u64 = 16 << 10;
/// A number as JSON text at its longest (`-2.2250738585072014e-308` is 24
/// characters), with room.
const NUMBER: u64 = 32;
/// One capture as the result lists it, beside its frame's and its bit's
/// names: its qubit, its start in samples and in ns, and its two
/// probabilities.
const CAPTURE_BYTES: u64 = 192;
/// The most [`run`]'s result of the job read as `p` and planned as `plan`
/// takes as JSON text, bytes, each part that grows with the program bounded
/// by what it may list: the populations, `2^n` outcomes of the `n` simulated
/// qubits; the distribution, the populations' where nothing is captured,
/// or the joint reading's, `2^d` outcomes of the `d` captures where each is
/// on its own qubit (else none), each keyed as wide as the program's bits
/// or the captures; the counts sampled from it
/// ([`super::run::counts_bound_among`]); the outcome bits' labels; each
/// capture with its frame's and bit's names; the leakage; the diagnostics,
/// in the result and in its verdict; the device as read and the target; and
/// the rest at [`RESULT_REST`], a sixteenth more for the forms RFC 8785
/// writes a number in.
fn result_bound(p: &Prepared, plan: &Plan) -> super::catalog::Bound {
let size = super::receipt::canonical_len;
// A name of the program's own as JSON text: RFC 8785 escapes a string's
// characters as serde does (`receipt::write_str`).
let text = |t: &str| serde_json::to_string(t).map_or(6 * t.len() as u64 + 2, |s| s.len() as u64);
let pow2 = |e: u64| if e >= 63 { u64::MAX } else { 1u64 << e };
let s = &p.reading.schedule;
let n = p.plan.qubits.len() as u64;
let caps: Vec<&ir::Instruction> = s.instructions.iter().filter(|i| matches!(i.op, Op::Capture { .. })).collect();
let mut read: Vec<u32> = caps.iter().map(|i| s.frame_qubit(i.frame)).collect();
read.sort_unstable();
read.dedup();
let bits = s.bits.len() as u64;
let (outcomes, width) = if caps.is_empty() {
(pow2(n), n)
} else if read.len() == caps.len() {
(pow2(read.len() as u64), bits.max(read.len() as u64))
} else {
(0, 0)
};
let entry = |w: u64| w + 4 + NUMBER;
// The populations, the distribution and the counts: what grows with the outcomes.
let mut counts = pow2(n).saturating_mul(entry(n));
counts = counts.saturating_add(outcomes.saturating_mul(entry(width)));
counts = counts.saturating_add(super::run::counts_bound_among(u32::try_from(width).unwrap_or(u32::MAX), p.shots, outcomes, None));
let mut b = RESULT_REST + 2 * size(&json!(plan.why));
b = b.saturating_add(16 * n + s.bits.iter().map(|x| text(x) + 1).sum::<u64>() + 16 * caps.len() as u64);
b = b.saturating_add(caps.iter().map(|i| CAPTURE_BYTES + text(&s.frames[i.frame].name) + match &i.op {
Op::Capture { bit: Some(k), .. } => s.bits.get(*k).map_or(0, |x| text(x)),
_ => 0,
}).sum::<u64>());
b = b.saturating_add(n * (NUMBER + 32));
let notes = 2 * p.reading.diagnostics.iter().map(|d| size(&d.to_json()) + 1).sum::<u64>();
b = b.saturating_add(size(&p.device_json) + p.target.as_ref().map_or(0, size));
let rest = b.saturating_add(b.saturating_add(notes) / 16);
super::catalog::Bound { counts, notes, rest, ..Default::default() }
}
/// [`plan`] of the job read as `p`.
fn plan_of(p: &Prepared, job_bytes: u64) -> Result<Plan, String> {
let s = &p.reading.schedule;
let held: usize = s.waveforms.iter().map(|w| w.samples.len()).sum();
let memory = (32u64 << 20) + 16 * held as u64 + 96 * s.instructions.len() as u64 + 64 * (p.plan.dim * p.plan.dim) as u64 + 64 * p.plan.samples;

View file

@ -197,3 +197,40 @@ fn a_pulse_job_is_the_same_bits_on_every_platform() {
let r = crate::engine::run::run(&req, &p).unwrap();
assert_eq!(r["pulse"]["density_hash"].as_str(), Some(XM_DENSITY), "the bits differ from the pinned ones: {}", r["pulse"]["density_hash"]);
}
/// No pulse result is larger than the bound its plan gave before it ran
/// (`catalog::plan_bounded`, through the pulse method's own), as RFC 8785
/// writes it with its provenance: one capture; 3,000 bits declared and one
/// captured (every key of the distribution and the counts as wide as the
/// program's bits, and each bit named); nothing captured (the populations as
/// the distribution); and two qubits captured at once. A qubit is captured
/// once at most (`J040`), so what grows is the bits. Each is bounded, and
/// within a few times what it gives.
#[test]
#[cfg(feature = "quantum_receipt")]
fn no_pulse_result_passes_the_bound_its_plan_gave() {
let cal = "OPENQASM 3.0;\ndefcalgrammar \"openpulse\";\ncal {\n extern port d0;\n extern port m0;\n extern port a0;\n extern port d1;\n extern port a1;\n frame q0 = newframe(d0, 5.0e9, 0);\n frame r0 = newframe(m0, 7.1e9, 0);\n frame c0 = newframe(a0, 7.1e9, 0);\n frame q1 = newframe(d1, 5.2e9, 0);\n frame c1 = newframe(a1, 7.2e9, 0);\n}\ndefcal x $0 { play(q0, gaussian(0.4, 64dt, 16dt)); }\ndefcal x $1 { play(q1, gaussian(0.4, 64dt, 16dt)); }\ndefcal measure $0 -> bit { return capture_v2(c0, 4dt); }\ndefcal measure $1 -> bit { return capture_v2(c1, 4dt); }\n";
// Registers of 64 bits at most (Q460), as many as `n` bits take, and the
// name of bit `i`.
let regs = |n: usize| (0..n.div_ceil(64)).map(|r| format!("bit[{}] b{r};\n", 64.min(n - 64 * r))).collect::<String>();
let bit = |i: usize| format!("b{}[{}]", i / 64, i % 64);
let jobs = [
("one capture", XM.to_string(), 500),
("one bit of 3,000 captured", format!("{cal}{}x $0;\n{} = measure $0;\n", regs(3000), bit(2999)), 2000),
("nothing captured", format!("{cal}x $0;\n"), 500),
("two qubits captured", format!("{cal}bit[2] c;\nx $0;\nx $1;\nc[0] = measure $0;\nc[1] = measure $1;\n"), 4000),
];
for (name, qasm, shots) in jobs {
let job = json!({ "qasm": qasm, "method": "pulse", "shots": shots, "seed": 3 });
let (p, bound) = match catalog::plan_bounded(&job, 1 << 30) {
Ok((p, Some(b))) => (p, b),
Ok((_, None)) => panic!("{name}: no bound"),
Err(e) => panic!("{name}: {e}"),
};
let result = crate::engine::run::run_with_provenance(&job, &p, "a host 1.0").unwrap_or_else(|e| panic!("{name}: {e}"));
let served = crate::engine::receipt::canonical(&result).len() as u64;
eprintln!("{name}: {served} bytes, bound {bound}");
assert!(served <= bound, "{name}: its result is {served} bytes, past its bound of {bound}");
assert!(bound <= 4 * served + (32 << 10), "{name}: {served} bytes, bound {bound}");
}
}

View file

@ -45,6 +45,87 @@ fn qerr(m: impl Into<String>) -> String {
tag(refusal::QEC, m.into())
}
thread_local! {
/// The time this thread's sampled decodes may take ([`within_time`]):
/// when the budget began, and how long it is.
static TIME: std::cell::Cell<Option<(std::time::Instant, std::time::Duration)>> = const { std::cell::Cell::new(None) };
}
/// Runs `f` with the shots it samples and decodes held to `limit` in all, on
/// this thread: a decode with `shots`, and a threshold of any kind, each
/// refused (`J031`, [`refusal::TOO_LARGE_HERE`]) at once when what it has
/// measured says it would take longer, and stopped if it runs a fifth past
/// that. What a shot costs depends on the noise, the distance and the decoder
/// as much as on the counts, so each point is measured before its shots are
/// run: its setup and sampling as they are, and a pilot of its own shots
/// (one in a hundred, at least 32, and on for [`PILOT_FLOOR`] where its shots
/// allow) for its rate. The rest is
/// projected from those, so setup is never multiplied by the shots, and a
/// call whose measured rate says it fits is run.
pub fn within_time<T>(limit: std::time::Duration, f: impl FnOnce() -> T) -> T {
struct Reset(Option<(std::time::Instant, std::time::Duration)>);
impl Drop for Reset {
fn drop(&mut self) {
TIME.with(|t| t.set(self.0));
}
}
let _reset = Reset(TIME.with(|t| t.replace(Some((std::time::Instant::now(), limit)))));
f()
}
/// The shots of a point decoded first to measure its rate: at most one in a
/// hundred (and at least 32), for at most half a second, and at least 8.
fn pilot_of(shots: u64) -> u64 {
shots.min((shots / 100).max(32))
}
/// Whether a pilot begun at `began` with `done` of its `most` shots decoded
/// goes on.
fn piloting(done: usize, most: usize, began: std::time::Instant) -> bool {
done < most && (done < 8 || began.elapsed() < std::time::Duration::from_millis(500))
}
/// The least time a circuit's pilot runs, its shots allowing. Its shots are
/// the call's own, decoded once and counted in its result, so measuring for
/// longer costs nothing; one in a hundred of a light decode is 20 ms, and
/// one pause of a loaded host's scheduler in so short a time read the rate
/// three times slow and refused a call that fit (a decode that took 2.7 s
/// alone, projected at 7 s against 6, at load 40 on the M3 Ultra).
const PILOT_FLOOR: std::time::Duration = std::time::Duration::from_millis(200);
/// [`piloting`] for a pilot of a call's own shots, `total` in all: past its
/// one in a hundred it goes on until [`PILOT_FLOOR`]. With no time set
/// (`most` 0) there is no pilot.
fn piloting_own(done: usize, most: usize, total: usize, began: std::time::Instant) -> bool {
most > 0 && done < total && (piloting(done, most, began) || (done >= most && began.elapsed() < PILOT_FLOOR))
}
/// The refusal of a call whose shots would take `secs`.
fn too_slow(secs: f64, limit: std::time::Duration, op: &str) -> String {
tag(refusal::TOO_LARGE_HERE, format!("its shots would take about {secs:.0} s to sample and decode here, more than the {} s a call here takes; run `wai-quantum qec {op}` on your machine", limit.as_secs()))
}
/// Whether the call, `rest` seconds of measured work from done, fits this
/// thread's time ([`within_time`]): the refusal where it does not.
fn fits(rest: f64, op: &str) -> Result<(), String> {
let Some((start, limit)) = TIME.with(std::cell::Cell::get) else { return Ok(()) };
let projected = start.elapsed().as_secs_f64() + rest;
if projected > limit.as_secs_f64() { Err(too_slow(projected, limit, op)) } else { Ok(()) }
}
/// Whether the call has run a fifth past this thread's time: the refusal
/// where it has, its projection having been short.
fn still_on_time(op: &str) -> Result<(), String> {
let Some((start, limit)) = TIME.with(std::cell::Cell::get) else { return Ok(()) };
let spent = start.elapsed();
if spent > limit + limit / 5 { Err(too_slow(spent.as_secs_f64(), limit, op)) } else { Ok(()) }
}
/// Whether a time is set on this thread: only then is a pilot measured.
fn timed() -> bool {
TIME.with(std::cell::Cell::get).is_some()
}
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
@ -360,7 +441,23 @@ pub fn decode(request: &Value) -> Result<Value, String> {
let seed = request.get("seed").and_then(Value::as_u64).unwrap_or(1);
let smp = frame::sample(&c, shots as usize, seed);
let mut failures = 0u64;
for i in 0..smp.shots {
// The pilot's shots measure the rate; the rest are projected from it.
let most = if timed() { pilot_of(shots) as usize } else { 0 };
let began = std::time::Instant::now();
let mut i = 0;
while piloting_own(i, most, smp.shots, began) {
if dec.run(&smp.fired(i), &mut ws)?.0 != smp.flips(i) {
failures += 1;
}
i += 1;
}
if i > 0 {
fits(began.elapsed().as_secs_f64() / i as f64 * (smp.shots - i) as f64, "decode")?;
}
for i in i..smp.shots {
if i % 64 == 0 {
still_on_time("decode")?;
}
if dec.run(&smp.fired(i), &mut ws)?.0 != smp.flips(i) {
failures += 1;
}
@ -523,6 +620,22 @@ pub fn threshold(request: &Value) -> Result<Value, String> {
"surface" => {
decoder = request.get("decoder").and_then(Value::as_str).unwrap_or("matching");
noise = "circuit level: depolarizing noise after every gate and on idle data, flips after resets and before measurements, all at p";
// Each point set up, sampled and its pilot decoded first, so the
// whole call is projected from what every point costs before the
// rest of any is run; then each point's other shots, in order.
struct Point {
d: u32,
rounds: u32,
p: f64,
smp: frame::Detections,
dec: Dec,
ws: Option<crate::quantum_uf::Workspace>,
failures: u64,
piloted: usize,
}
let most = if timed() { pilot_of(shots) as usize } else { 0 };
let mut run: Vec<Point> = Vec::with_capacity(points);
let mut rest = 0f64;
for &d in &dists {
let d = d as u32;
if !(3..=MAX_CIRCUIT_DISTANCE).contains(&d) || d.is_multiple_of(2) {
@ -534,36 +647,88 @@ pub fn threshold(request: &Value) -> Result<Value, String> {
let model = frame::error_model(&c).map_err(|e| qerr(e.to_string()))?;
let dec = decoder_for(decoder, &model, request)?;
let smp = frame::sample(&c, shots as usize, seed.wrapping_add(u64::from(d) * 1000 + (p * 1e6) as u64));
let mut ws = None;
let mut failures = 0u64;
for i in 0..smp.shots {
if dec.run(&smp.fired(i), &mut ws)?.0 != smp.flips(i) {
failures += 1;
let mut pt = Point { d, rounds, p, smp, dec, ws: None, failures: 0, piloted: 0 };
let began = std::time::Instant::now();
while piloting_own(pt.piloted, most, pt.smp.shots, began) {
let i = pt.piloted;
if pt.dec.run(&pt.smp.fired(i), &mut pt.ws)?.0 != pt.smp.flips(i) {
pt.failures += 1;
}
pt.piloted += 1;
}
let pt = MemoryPoint { distance: d, rounds, p, shots, failures };
memory.push(pt);
let rate = failures as f64 / shots as f64;
out_points.push(json!({ "distance": d, "p": p, "rounds": rounds, "shots": shots, "failures": failures, "rate": rate, "stderr": (rate * (1.0 - rate) / shots as f64).sqrt(), "per_round": pt.per_round() }));
if pt.piloted > 0 {
rest += began.elapsed().as_secs_f64() / pt.piloted as f64 * (pt.smp.shots - pt.piloted) as f64;
// Refused as soon as what is measured already passes it.
fits(rest, "threshold")?;
}
run.push(pt);
}
}
for mut pt in run {
for i in pt.piloted..pt.smp.shots {
if i % 64 == 0 {
still_on_time("threshold")?;
}
if pt.dec.run(&pt.smp.fired(i), &mut pt.ws)?.0 != pt.smp.flips(i) {
pt.failures += 1;
}
}
let (d, rounds, p, failures) = (pt.d, pt.rounds, pt.p, pt.failures);
let pt = MemoryPoint { distance: d, rounds, p, shots, failures };
memory.push(pt);
let rate = failures as f64 / shots as f64;
out_points.push(json!({ "distance": d, "p": p, "rounds": rounds, "shots": shots, "failures": failures, "rate": rate, "stderr": (rate * (1.0 - rate) / shots as f64).sqrt(), "per_round": pt.per_round() }));
}
}
"color" | "toric" | "surface_capacity" => {
decoder = if family == "color" { "color" } else { "union_find" };
noise = "code capacity: independent X flips on the data at p, perfect syndromes";
let mut codes = Vec::with_capacity(dists.len());
for &d in &dists {
let code_name = match family {
"color" => format!("color:{d}"),
"toric" => format!("toric:{d}"),
_ => format!("surface:{d}"),
};
let c = read_code(&json!(code_name))?;
codes.push((d, read_code(&json!(code_name))?));
}
let bench = |c: &Code, p: f64, trials: u32, s: u64| match &c.color {
Some(cc) => crate::quantum_color::benchmark(cc, p, trials, s),
None => crate::quantum_qec::benchmark_uf(&c.css, p, trials, s),
};
// A point runs whole, so each is measured on a pilot of its own
// trials first, and the call projected from them all. The pilot
// runs in batches, each twice the one before, held as a circuit's
// is to half a second ([`piloting`]): run whole, a point's one in
// a hundred of 200,000 trials of a large code took 11 s in a debug
// build before the call could be refused. A batch has no setup of
// its own (each trial is drawn and decoded alone), so its time
// over its trials is the point's rate.
if timed() {
let pilot = pilot_of(shots) as usize;
let mut rest = 0f64;
for (d, c) in &codes {
for &p in &ps {
let s = seed.wrapping_add(d * 1000 + (p * 1e6) as u64);
let began = std::time::Instant::now();
let (mut done, mut batch) = (0usize, 8usize);
while piloting(done, pilot, began) {
let n = batch.min(pilot - done);
bench(c, p, n as u32, s.wrapping_add(done as u64));
done += n;
batch *= 2;
}
rest += began.elapsed().as_secs_f64() / done.max(1) as f64 * shots as f64;
fits(rest, "threshold")?;
}
}
}
for (d, c) in &codes {
let d = *d;
for &p in &ps {
still_on_time("threshold")?;
let s = seed.wrapping_add(d * 1000 + (p * 1e6) as u64);
let (trials, failures) = match &c.color {
Some(cc) => crate::quantum_color::benchmark(cc, p, shots as u32, s),
None => crate::quantum_qec::benchmark_uf(&c.css, p, shots as u32, s),
};
let (trials, failures) = bench(c, p, shots as u32, s);
let rate = f64::from(failures) / f64::from(trials);
out_points.push(json!({ "distance": d, "p": p, "shots": trials, "failures": failures, "rate": rate, "stderr": (rate * (1.0 - rate) / f64::from(trials)).sqrt() }));
}
@ -693,6 +858,77 @@ pub fn call(op: &str, request: &Value) -> Result<Value, String> {
mod tests {
use super::*;
/// A circuit's pilot goes on past its one in a hundred until
/// [`PILOT_FLOOR`], so its rate is not read from 20 ms of a light decode;
/// it never passes its shots, nor half a second once one in a hundred is
/// not done, and there is none with no time set.
#[test]
fn a_circuit_s_pilot_runs_for_its_floor_within_its_shots() {
let now = std::time::Instant::now();
let ago = |ms: u64| now.checked_sub(std::time::Duration::from_millis(ms)).expect("an instant that long ago");
assert!(piloting_own(200, 200, 20_000, now), "past one in a hundred, within the floor");
assert!(!piloting_own(200, 200, 20_000, ago(PILOT_FLOOR.as_millis() as u64 + 1)), "past the floor");
assert!(!piloting_own(20_000, 200, 20_000, now), "every shot done");
assert!(!piloting_own(0, 0, 20_000, now), "no time set");
assert!(piloting_own(100, 200, 20_000, ago(300)), "short of one in a hundred, within half a second");
assert!(!piloting_own(100, 200, 20_000, ago(600)), "past half a second");
assert!(piloting_own(3, 200, 20_000, ago(600)), "fewer than 8 done");
}
/// A call whose setup and sampling are most of its time, and its shots
/// cheap, is run within a time it fits: what is measured before the first
/// shot is not multiplied by the shots. A code-capacity threshold, whose
/// points run whole, is measured on a pilot of each point and refused at
/// once where it would not fit. A documented default answers, or is
/// refused within a few seconds, not at the end of its time.
#[test]
fn a_time_is_measured_apart_from_setup_and_holds_every_threshold() {
// Its time measured here first, with no time set; then, given twice
// that and a second, it is run, not refused: a time here is a build's
// and a host's (a debug build beside the library's other tests took
// past 15 s, where a release build alone takes a second or two).
let circuit = crate::quantum_frame::surface_code_memory(11, 50, 1e-5);
let req = json!({ "circuit": circuit, "shots": 20_000 });
let t = std::time::Instant::now();
call("decode", &req).unwrap();
let took = t.elapsed();
let t = std::time::Instant::now();
let r = within_time(took * 2 + std::time::Duration::from_secs(1), || call("decode", &req));
assert!(r.is_ok(), "{r:?} after {:?}, where it took {took:?} with no time set", t.elapsed());
// Refused on its pilot, which is held to about half a second, not on
// its one in a hundred run whole (11 s in a debug build on macOS):
// within 5 s, or twice the decode's time where the host makes that
// longer (8.2 s in a release build beside the library's other tests,
// the Framework at load 77, where 5 s was asked).
let t = std::time::Instant::now();
let e = within_time(std::time::Duration::from_secs(3), || call("threshold", &json!({ "family": "color", "distances": [15], "p": [0.08], "shots": 200_000 }))).unwrap_err();
let most = std::time::Duration::from_secs(5).max(took * 2);
assert!(e.ends_with("[J031]") && t.elapsed() < most, "{e} after {:?}, where {most:?} was asked (the decode took {took:?})", t.elapsed());
let t = std::time::Instant::now();
let r = within_time(std::time::Duration::from_secs(25), || call("threshold", &json!({ "family": "surface", "decoder": "correlated" })));
assert!(r.is_ok() || t.elapsed() < std::time::Duration::from_secs(12), "{r:?} after {:?}", t.elapsed());
}
/// Within a time, a decode and a threshold whose shots would take longer
/// are refused as soon as their first shots say so, not at the end of the
/// time; one that fits answers as it did; with no time set, none is.
#[test]
fn a_time_holds_the_shots_sampled_and_decoded() {
let heavy = [
("threshold", json!({ "family": "surface", "distances": [7, 7, 7, 7], "p": [0.04, 0.045, 0.05, 0.055], "shots": 12_500 })),
("decode", json!({ "memory": { "distance": 7, "rounds": 7, "p": 0.05 }, "shots": 200_000 })),
];
for (op, req) in heavy {
let t = std::time::Instant::now();
let e = within_time(std::time::Duration::from_secs(3), || call(op, &req)).unwrap_err();
assert!(e.ends_with("[J031]") && e.contains("wai-quantum qec"), "{op}: {e}");
assert!(t.elapsed() < std::time::Duration::from_secs(10), "{op}: {:?}", t.elapsed());
}
let small = json!({ "family": "surface", "distances": [3], "p": [0.001], "shots": 200 });
assert!(within_time(std::time::Duration::from_secs(30), || call("threshold", &small)).is_ok());
assert!(TIME.with(std::cell::Cell::get).is_none());
}
#[test]
fn every_family_reads_and_certifies_its_distance() {
for (name, n, k, d) in [("surface:5", 25, 1, 5), ("toric:4", 32, 2, 4), ("steane", 7, 1, 3), ("color:7", 37, 1, 7), ("bb:72", 72, 12, 6), ("gross", 144, 12, 12)] {

View file

@ -188,6 +188,54 @@ fn write(v: &Value, o: &mut Vec<u8>) {
}
}
/// The length of `v` as RFC 8785 text, bytes, written nowhere: its length as
/// [`canonical`] writes it, or as a host writes it with each whole number
/// exactly as read and every other number as [`canonical`] does (a job
/// service's served text), whichever is longer. A number's length is its own
/// form's, never serde's (`1e20` is 21 characters here, 4 there), so this is
/// what a result that copies a request takes as served.
pub fn canonical_len(v: &Value) -> u64 {
let mut seen: std::collections::HashMap<u64, u64> = std::collections::HashMap::new();
let mut number = |n: &serde_json::Number| -> u64 {
let x = n.as_f64().unwrap_or(f64::NAN);
let es = *seen.entry(x.to_bits()).or_insert_with(|| es_number(x).len() as u64);
// A whole number written as read takes its own digits.
let exact = if n.is_f64() { 0 } else { n.to_string().len() as u64 };
es.max(exact)
};
let mut total = 0u64;
let mut stack = vec![v];
while let Some(v) = stack.pop() {
total += match v {
Value::Null => 4,
Value::Bool(b) => if *b { 4 } else { 5 },
Value::Number(n) => number(n),
Value::String(s) => str_len(s),
Value::Array(a) => {
stack.extend(a.iter());
2 + a.len().saturating_sub(1) as u64
}
Value::Object(m) => {
stack.extend(m.values());
2 + m.len().saturating_sub(1) as u64 + m.keys().map(|k| str_len(k) + 1).sum::<u64>()
}
};
}
total
}
/// A string's length as [`write_str`] writes it, bytes.
fn str_len(s: &str) -> u64 {
2 + s
.chars()
.map(|c| match c {
'"' | '\\' | '\u{8}' | '\t' | '\n' | '\u{c}' | '\r' => 2,
c if (c as u32) < 0x20 => 6,
c => c.len_utf8() as u64,
})
.sum::<u64>()
}
/// A string as RFC 8785 writes it: `"` and `\\` escaped, the five short
/// control escapes, every other control character as `\u00xx`, and the rest
/// as it is.

View file

@ -90,14 +90,14 @@ table! {
FABRIC = "J013", Refused, "A placement on the fabric (`fabric`) this host cannot honour: no member by that name, no certified accelerator member here, a state or a partition larger than a member holds, a partition count that is not a power of two, or a method or program the fabric's members do not run (the message says which, and what is here).";
// Intents and repairs.
BAD_INTENT = "J020", Refused, "An intent that does not read: not one of `distribution`, `equivalent` or `prepares`, a distribution whose outcomes are not the program's bits or whose probabilities do not sum to 1, or a reference program that does not read.";
BAD_INTENT = "J020", Refused, "An intent that does not read: not one of `distribution`, `equivalent` or `prepares`, a distribution whose outcomes are not the program's bits or whose probabilities do not sum to 1, or a reference program that does not read, or more intents than the host takes.";
REPAIR_REJECTED = "J021", Refused, "A proposed repair that changes a line no diagnostic flags, or adds lines away from one: rejected, and the program left as it was.";
BAD_REPAIR = "J022", Refused, "A repair's choices that do not read (`max_iterations`, `dyadic_bits`, `sizes`), or a proposal that is not a program's text.";
// The host.
RATE = "J030", Retry, "The host takes no more from this client for now (submissions in a window, or jobs at once): send it again shortly.";
TOO_LARGE_HERE = "J031", Refused, "A job or a check larger than this host takes: run it on your own machine, with the command line or its MCP server.";
BUSY = "J032", Retry, "The host is full (its queue, or the checks running at once): send it again later.";
TOO_LARGE_HERE = "J031", Refused, "A job or a check larger than this host takes, its reading included: run it on your own machine, with the command line or its MCP server.";
BUSY = "J032", Retry, "The host is full (its queue, the checks running at once, or the requests being read, yours or all of them): send it again later.";
KEY_REQUIRED = "J033", Refused, "A host that takes requests only with one of its API keys: give yours as `Authorization: Bearer <key>`; a key it does not hold is refused the same way.";
IDEMPOTENCY_KEY = "J034", Refused, "An idempotency key (`Idempotency-Key`, or a tool's `idempotency_key`) that does not read, or that this client sent before with another request: a key names one job, so another request needs another key. A key is 1 to 255 visible ASCII characters, and should be unguessable (a UUID, or 128 random bits).";

View file

@ -147,6 +147,9 @@ impl Method for PulseMethod {
fn plan(&self, job: &Job<'_>) -> Result<Plan, String> {
crate::engine::pulse::plan(job.json(), job.job_bytes())
}
fn plan_bounded(&self, job: &Job<'_>) -> Result<(Plan, Option<u64>), String> {
crate::engine::pulse::plan_bounded(job.json(), job.job_bytes()).map(|(p, b)| (p, Some(b)))
}
fn run(&self, job: &Job<'_>, plan: &Plan, out: &mut Value) -> Result<(), String> {
*out = crate::engine::pulse::run(job.json(), plan)?;
Ok(())

View file

@ -259,6 +259,14 @@ pub trait Method: Send + Sync {
}
/// The job admitted, with the memory and work it will take, or why not.
fn plan(&self, job: &Job<'_>) -> Result<Plan, String>;
/// [`Method::plan`], with the most its result takes as JSON text, bytes,
/// known before it runs, for a method that reads its own program
/// ([`Method::reads_program`]): the engine bounds every other method's
/// result itself. `None`, the default, where it does not say; a host
/// then holds the result to its cap only as it ends.
fn plan_bounded(&self, job: &Job<'_>) -> Result<(Plan, Option<u64>), String> {
Ok((self.plan(job)?, None))
}
/// The plan carried out: the method writes its answers into `out`, which
/// holds the method, the qubits and the gates (and a compile's or a
/// repair's account) already. It must write `accuracy`, with its

View file

@ -14,6 +14,28 @@ fn hex(b: &[u8]) -> String {
/// The most characters of bit strings a counts map lists.
const MAX_COUNTS_CHARS: usize = 16 << 20;
/// The most a counts map ([`counts`]) of `shots` samples of `width`-bit
/// outcomes, at most `distinct` of them possible, takes as JSON text, bytes:
/// as many outcomes as it may list (no more than the shots, `distinct`,
/// [`MAX_OUTCOMES`] and what [`MAX_COUNTS_CHARS`] of bit strings hold), each
/// its bit string, two quotes, a colon and a comma, and the map's own
/// fields; with `value`, that many bytes for each outcome's value, and
/// without, its count's digits. `k` counts that sum to `shots` have at most
/// `k·(log10(shots/k) + 1)` digits in all, the most where they are equal.
pub(crate) fn counts_bound_among(width: u32, shots: u64, distinct: u64, value: Option<u64>) -> u64 {
if shots == 0 || distinct == 0 {
return 0;
}
let w = u64::from(width).max(1);
let keep = (MAX_OUTCOMES as u64).min((MAX_COUNTS_CHARS as u64 / w).max(1));
let k = shots.min(distinct).min(keep);
let values = match value {
Some(v) => k * v,
None => ((k as f64) * ((shots as f64 / k as f64).log10() + 1.0)).ceil() as u64 + 16,
};
k * (u64::from(width) + 4) + values + 128
}
/// A counts map: the most frequent outcomes (at most [`MAX_OUTCOMES`], and
/// at most [`MAX_COUNTS_CHARS`] characters of bit strings), and how many
/// shots the omitted ones held.
@ -1068,6 +1090,65 @@ mod tests {
run(&job, &p).unwrap()
}
/// No result is larger than the bound its plan gave before it ran
/// (`catalog::plan_bounded`), as RFC 8785 writes it with its provenance:
/// the worked examples with their verdicts, and jobs that ask for each
/// part a result grows with (counts at the most a map lists, wide bit
/// strings, measurements in the middle, observables, amplitudes, a
/// critique's remarks on every gate, a repair, a compile, noise and a
/// mitigation, a device target). Where counts make the result, the bound
/// is within a quarter of it.
#[test]
#[cfg(all(feature = "quantum_qasm", feature = "quantum_noise", feature = "quantum_receipt"))]
fn no_result_passes_the_bound_its_plan_gave() {
let wide = format!("OPENQASM 3.0;\nqubit[16] q;\nbit[16] c;\nh q;\nc = measure q;\n");
let observables: Vec<String> = (0..256).map(|i| format!("{}.5 Z{} X{} Y{}", i % 7, i % 16, (i + 1) % 16, (i + 2) % 16)).collect();
let amplitudes: Vec<String> = (0..64).map(|i| format!("{:010b}", i * 13)).collect();
let mut jobs: Vec<(String, Value)> = crate::engine::guide::examples()
.iter()
.map(|e| {
let mut intent = e.intent.to_string();
if let Some(r) = e.reference {
intent = intent.replace("\"reference.qasm\"", &json!(r).to_string());
}
let mut j = json!({ "qasm": e.program, "verdict": true, "intent": serde_json::from_str::<Value>(&intent).unwrap() });
j.as_object_mut().unwrap().extend(e.run.as_object().cloned().unwrap_or_default());
(format!("example {}", e.name), j)
})
.collect();
jobs.extend([
("65,536 outcomes of 16 bits".to_string(), json!({ "qasm": wide, "shots": 1_000_000, "seed": 3 })),
("10,000 outcomes of 16 bits".to_string(), json!({ "qasm": wide, "shots": 10_000, "seed": 3 })),
("outcomes of 300 bits".to_string(), json!({ "qasm": "OPENQASM 3.0;\nqubit[12] q;\nbit[300] c;\nh q;\nfor uint i in [0:11] { c[i * 25] = measure q[i]; }\n", "shots": 20000 })),
("measured in the middle".to_string(), json!({ "qasm": "OPENQASM 3.0;\nqubit[3] q;\nbit[6] c;\nh q;\nc[0] = measure q[0];\nc[1] = measure q[1];\nc[2] = measure q[2];\nreset q;\nh q;\nc[3] = measure q[0];\nc[4] = measure q[1];\nc[5] = measure q[2];\n", "shots": 5000 })),
("256 observables".to_string(), json!({ "qasm": "OPENQASM 3.0;\nqubit[16] q;\nh q;\nrx(0.3) q[2];\ncx q[0], q[1];\n", "observables": observables })),
("64 amplitudes".to_string(), json!({ "qasm": "OPENQASM 3.0;\nqubit[10] q;\nh q;\nrz(0.7) q[3];\n", "amplitudes": amplitudes })),
("a remark on every gate".to_string(), json!({ "qasm": "OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nfor int i in [1:4000] { x q[0]; h q[1]; h q[1]; }\nc = measure q;\n", "shots": 100, "verdict": true })),
("repaired".to_string(), json!({ "qasm": "OPENQASM 3.0;\nqubit q;\nbit c;\nh q;\nh q;\nx q;\nc = measure q;\n", "shots": 100, "repair": true, "verdict": true })),
("compiled".to_string(), json!({ "qasm": "OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nrz(0.1234) q[0];\nrx(0.77) q[1];\ncx q[0], q[1];\nc = measure q;\n", "shots": 500, "compile": { "epsilon": 1e-3, "level": 2 } })),
("noisy".to_string(), json!({ "qasm": "OPENQASM 3.0;\nqubit[8] q;\nbit[8] c;\nh q;\ncx q[0], q[1];\nc = measure q;\n", "shots": 30000, "noise": { "depolarizing_1q": 0.01, "readout": 0.02 } })),
("mitigated".to_string(), json!({ "qasm": "OPENQASM 3.0;\nqubit[3] q;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\n", "observables": ["Z0 Z1", "Z1 Z2"], "noise": { "depolarizing_2q": 0.02, "depolarizing_1q": 0.004 }, "mitigate": { "method": "zne", "shots": 4000 } })),
("on a device target".to_string(), json!({ "qasm": "OPENQASM 3.0; qubit[3] q; bit[3] c; h q[0]; cx q[0], q[1]; cx q[1], q[2]; c = measure q;", "target": "synthetic-ring-8", "shots": 3000, "seed": 2 })),
]);
let mut checked = 0;
for (name, job) in &jobs {
let (p, bound) = match crate::engine::catalog::plan_bounded(job, CAP) {
Ok((p, Some(b))) => (p, b),
Ok((_, None)) => panic!("{name}: no bound"),
Err(e) => panic!("{name}: {e}"),
};
let result = run_with_provenance(job, &p, "a host 1.0").unwrap_or_else(|e| panic!("{name}: {e}"));
let served = crate::engine::receipt::canonical(&result).len() as u64;
eprintln!("{name}: {served} bytes, bound {bound}");
assert!(served <= bound, "{name}: its result is {served} bytes, past its bound of {bound}");
if name.contains("outcomes of 16 bits") {
assert!(bound <= served + served / 4 + (64 << 10), "{name}: {served} bytes, bound {bound}");
}
checked += 1;
}
assert_eq!(checked, jobs.len());
}
#[test]
#[cfg(feature = "quantum_toolchain")]
fn grover_as_the_ide_exports_it_finds_the_marked_item() {

View file

@ -213,6 +213,12 @@ fn sweep_of(request: &Value, sweep: &Value) -> Result<Vec<Item>, String> {
/// Each item planned within `job_bytes`, or the first that does not plan,
/// with its index.
pub fn plan(items: &[Item], job_bytes: u64) -> Result<Vec<catalog::Plan>, String> {
plan_bounded(items, job_bytes).map(|planned| planned.into_iter().map(|(p, _)| p).collect())
}
/// [`plan`], each item's plan with the most its result may take, by part
/// ([`catalog::plan_bound`]).
pub fn plan_bounded(items: &[Item], job_bytes: u64) -> Result<Vec<(catalog::Plan, Option<catalog::Bound>)>, String> {
items
.iter()
.enumerate()
@ -220,7 +226,7 @@ pub fn plan(items: &[Item], job_bytes: u64) -> Result<Vec<catalog::Plan>, String
if let Some(at) = receipt::inexact_number(&it.request) {
return Err(super::refusal::tag(super::refusal::LIMIT, format!("item {i}: `{at}`: a whole number here must be within ±9007199254740991 (2^53 − 1)")));
}
catalog::plan(&it.request, job_bytes).map_err(|e| format!("item {i}: {e}"))
catalog::plan_bound(&it.request, job_bytes).map_err(|e| format!("item {i}: {e}"))
})
.collect()
}
@ -410,8 +416,9 @@ pub fn verify(job: &Value) -> Value {
// The seal alone; the rules on its numbers, by its form, are §8.6's (`job::check`).
let signed = receipt::verify(request, &head, set_receipt);
let items = result["items"].as_array().cloned().unwrap_or_default();
let derived = match expand(request) {
Ok(want) => {
// Writing a sweep out reads its programs: for a seal that holds, and no other.
let derived = match signed.then(|| expand(request).ok()).flatten() {
Some(want) => {
want.len() == items.len()
&& want.iter().zip(&items).enumerate().all(|(i, (w, got))| {
got["index"].as_u64() == Some(i as u64)
@ -419,7 +426,7 @@ pub fn verify(job: &Value) -> Value {
&& w.point.as_ref().is_none_or(|p| receipt::canonical(p) == receipt::canonical(&got["point"]))
})
}
Err(_) => false,
None => false,
};
let count_ok = result["count"].as_u64() == Some(items.len() as u64);
let each = items.iter().all(|it| verify_item(request, &head, set_receipt, it));
@ -455,6 +462,15 @@ mod tests {
SigningKey::from_bytes(&[5; 32])
}
/// Writing a sweep out reads it: a seal that does not hold is told so
/// without that, so forging one buys no reading.
#[test]
fn a_seal_that_does_not_hold_is_not_written_out() {
let forged = json!({ "request": { "qasm": "input int n;\nqubit q;\nfor int i in [1:n] { x q; }\n", "sweep": { "n": [1000] } }, "result": { "format": FORMAT, "count": 0, "items": [], "receipt": {} } });
let (v, left) = crate::qasm::within_steps(1_000_000, || (verify(&forged), crate::qasm::steps_left()));
assert_eq!((v["signed"].clone(), v["derived"].clone(), left), (json!(false), json!(false), Some(1_000_000)));
}
#[test]
fn a_list_of_programs_is_one_job_each_the_set_s_fields_shared() {
let set = json!({ "programs": [BELL, { "qasm": "OPENQASM 3.0; qubit q; rx(0.3) q;", "observables": ["Z0"], "shots": 0 }], "shots": 100, "seed": 7, "dry_run": false });

View file

@ -103,11 +103,19 @@ pub fn chi2_tail(x: f64, k: u64) -> f64 {
gamma_q(k as f64 / 2.0, x / 2.0)
}
/// The most outcomes of probability 0 an answer lists by name: past them it
/// gives how many there were ([`goodness_of_fit`]), so what it says stays
/// within a bound however many outcomes a run gave.
pub const MAX_IMPOSSIBLE_LISTED: usize = 64;
/// Whether `observed` counts are consistent with `expected` probabilities,
/// by `test` (`chi_squared` or `g_test`) at significance `alpha`:
/// `{"test", "statistic", "dof", "p_value", "alpha", "shots", "rejected",
/// "smallest_expected", "impossible", "note"}`. Rejected when the p-value is
/// below `alpha`, or when an outcome of probability 0 was sampled.
/// "smallest_expected", "impossible", "impossible_outcomes", "note"}`.
/// Rejected when the p-value is below `alpha`, or when an outcome of
/// probability 0 was sampled: `impossible` lists the first
/// [`MAX_IMPOSSIBLE_LISTED`] of those, in order, and `impossible_outcomes`
/// says how many there were.
pub fn goodness_of_fit(test: &str, observed: &BTreeMap<String, u64>, expected: &BTreeMap<String, f64>, alpha: f64) -> Result<Value, String> {
if !TESTS.contains(&test) {
return Err(format!("{test}: the tests are {}", TESTS.join(" and ")));
@ -145,10 +153,15 @@ pub fn goodness_of_fit(test: &str, observed: &BTreeMap<String, u64>, expected: &
String::new()
};
let name = if test == "chi_squared" { "Pearson's chi-squared test" } else { "the G-test" };
let note = if impossible.is_empty() {
let all = impossible.len();
let mut impossible = impossible;
impossible.truncate(MAX_IMPOSSIBLE_LISTED);
let note = if all == 0 {
format!("{name} of {shots} samples against the stated distribution, {dof} degrees of freedom{law}")
} else {
format!("{} sampled, which the stated distribution gives probability 0", impossible.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", "))
let listed = impossible.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", ");
let more = if all > impossible.len() { format!(" and {} more", all - impossible.len()) } else { String::new() };
format!("{listed}{more} sampled, which the stated distribution gives probability 0")
};
Ok(json!({
"test": test,
@ -160,6 +173,7 @@ pub fn goodness_of_fit(test: &str, observed: &BTreeMap<String, u64>, expected: &
"rejected": rejected,
"smallest_expected": if smallest.is_finite() { json!(smallest) } else { Value::Null },
"impossible": impossible,
"impossible_outcomes": all,
"note": note,
}))
}

View file

@ -387,8 +387,10 @@ pub(crate) fn check_intent_for_in(reg: &super::registry::Registry, intent: &Valu
let note = g["note"].as_str().unwrap_or("").to_string();
let on = format!("{shots} samples (seed {seed}, on {})", plan.method);
let (outcome, strength, why) = if g["rejected"] == true {
let reason = if g["impossible"].as_array().is_some_and(|a| !a.is_empty()) { note.clone() } else { format!("p = {p:.3e} < α = {alpha}") };
("fails", "evidence", format!("{on} are not consistent with the intent's distribution: {reason}; {note}"))
// An outcome of probability 0 sampled is the reason, and the note says
// it; a p-value under α is the reason beside the note's test.
let why = if g["impossible_outcomes"].as_u64().is_some_and(|n| n > 0) { format!("{on} are not consistent with the intent's distribution: {note}") } else { format!("{on} are not consistent with the intent's distribution: p = {p:.3e} < α = {alpha}; {note}") };
("fails", "evidence", why)
} else if proof_only {
("unchecked", "none", format!("{on} are consistent with the intent's distribution (p = {p:.4} ≥ α = {alpha}), but a statistical test is evidence and the intent requires a proof"))
} else {
@ -722,6 +724,18 @@ pub fn of_refusal(request: &Value, error: &str) -> Value {
v
}
/// The verdict of a job a service admitted, given without reading it again
/// (a client's every poll would otherwise read it): admission read it, so it
/// reads; `error` is why it failed since, if it did.
pub fn of_admitted(error: Option<&str>) -> Value {
let mut v = base(json!({ "ok": true, "diagnostics": [] }));
if let Some(e) = error {
v["refusal"] = refusal::refusal_json(e);
}
conclude(&mut v);
v
}
/// The verdict of a pulse run ([`super::pulse`]): what the pulse reader
/// said of the program (the result's diagnostics), and the run's method and
/// accuracy. A pulse run checks no intent.

View file

@ -217,13 +217,21 @@ fn program_of<'a>(tool: &str, args: &'a Value, answer: &'a Value) -> Option<&'a
/// view, the tool, the program, its layout and description (or why it is not
/// drawn), and for a target, its map. `None` for a tool with no view.
pub fn data(tool: &str, args: &Value, answer: &Value) -> Option<Value> {
data_of(tool, args, answer, true)
}
/// [`data`], without reading anything where `draw` is false: the view, the
/// tool and the program, for an answer that cannot pay to read it again (a
/// poll, which a view makes every few seconds). The view shows the answer,
/// and no drawing.
pub fn data_of(tool: &str, args: &Value, answer: &Value, draw: bool) -> Option<Value> {
let view = for_tool(tool)?;
let mut d = json!({ "view": view.name(), "tool": tool, "protocol": PROTOCOL });
match view {
View::Result | View::Circuit | View::Critique => {
let src = program_of(tool, args, answer);
d["program"] = json!(src);
if let (Some(o), Some(x)) = (d.as_object_mut(), drawing(src).as_object()) {
if draw && let (Some(o), Some(x)) = (d.as_object_mut(), drawing(src).as_object()) {
o.extend(x.clone());
}
}

View file

@ -58,6 +58,7 @@ mod unroll;
pub mod pulse;
pub use diag::{Diagnostic, Outcome, Span};
pub use elab::MAX_STEPS;
#[cfg(feature = "quantum_qasm")]
use crate::gates::{C, Gate as Matrix, complex};
@ -69,14 +70,6 @@ use crate::quantum_tableau::Clifford;
/// The most qubits a program declares.
pub const MAX_QUBITS: u32 = 65_536;
/// The deepest a program nests ([`diag::NESTING`]): a statement at the top level is at
/// depth 1, and each statement, expression or type one deeper than the statement,
/// expression, type or bracket that holds it, operators grouped as they bind and each
/// call's body read in its place; past it the program is refused before it could use up
/// a reader's stack, so that every host, a small one among them, reads it the same way.
pub const MAX_NESTING: u32 = 128;
/// The most `else if` arms one `if` holds: each at the depth of its `if`.
pub const MAX_ELSE_IF: usize = 1024;
/// The most gates a program expands to.
pub const MAX_OPS: usize = 2_000_000;
@ -402,10 +395,98 @@ pub fn read(src: &str) -> Result<Reading, Diagnostic> {
/// [`read`] for a generator, at `inputs`.
pub fn read_with(src: &str, inputs: &Inputs) -> Result<Reading, Diagnostic> {
let toks = lex::lex(src)?;
let (stmts, version, depths) = parse::parse_depths(src, &toks)?;
let (program, lines, diagnostics) = elab::elaborate(src, &toks, &stmts, &depths, version, inputs)?;
Ok(Reading { program, lines, diagnostics })
// Within a budget of reading, a program read before (with no inputs) is not read again.
if inputs.is_empty()
&& let Some(seen) = READS.with_borrow(|r| r.as_ref().and_then(|r| r.read.get(src).cloned()))
{
return seen;
}
let reading = (|| {
let toks = lex::lex(src)?;
let (stmts, version) = parse::parse(src, &toks)?;
let (program, lines, diagnostics) = elab::elaborate(src, &toks, &stmts, version, inputs)?;
Ok(Reading { program, lines, diagnostics })
})();
if inputs.is_empty() {
READS.with_borrow_mut(|r| {
if let Some(r) = r {
r.read.insert(src.to_string(), reading.clone());
}
});
}
reading
}
/// A budget of reading on one thread ([`within_steps`]): the steps left
/// (statements elaborated, gates written, a generator's loops written out),
/// and each program read under it.
struct Reads {
left: u64,
read: std::collections::HashMap<String, Result<Reading, Diagnostic>>,
}
thread_local! {
static READS: std::cell::RefCell<Option<Reads>> = const { std::cell::RefCell::new(None) };
}
/// One step of reading (a statement elaborated, a gate written, a pass of a
/// generator's loop written out), spent from this thread's budget of reading
/// when one is set: whether the budget was already spent.
pub(crate) fn spend_step() -> bool {
READS.with_borrow_mut(|r| match r {
Some(r) if r.left == 0 => true,
Some(r) => {
r.left -= 1;
false
}
None => false,
})
}
/// `n` steps of reading spent at once (values made or copied, counted by
/// their bytes), from this thread's budget when one is set: whether the
/// budget ran out. What is left is spent, and the budget then holds no more.
pub(crate) fn spend_steps(n: u64) -> bool {
READS.with_borrow_mut(|r| match r {
Some(r) if r.left < n => {
r.left = 0;
true
}
Some(r) => {
r.left -= n;
false
}
None => false,
})
}
/// Runs `f` with every program it reads on this thread read once (a program
/// read again with no inputs is answered from its first reading) and every
/// reading within one budget of `steps` together: each statement the reader
/// elaborates, each gate it writes, and each pass of a loop a generator is
/// written out with ([`instantiate`]) spending one; past it, a reading is
/// refused ([`diag::UNROLL_LIMIT`]), and [`steps_left`] is `Some(0)`. What a
/// service holds the programs of one request to, so its reading is bounded
/// however many programs the request carries and however often it reads
/// each. A budget set inside another runs within the outer one.
pub fn within_steps<T>(steps: u64, f: impl FnOnce() -> T) -> T {
struct Scope(bool);
impl Drop for Scope {
fn drop(&mut self) {
if self.0 {
READS.with_borrow_mut(|r| *r = None);
}
}
}
let _scope = Scope(READS.with_borrow_mut(|r| r.is_none().then(|| *r = Some(Reads { left: steps, read: Default::default() })).is_some()));
f()
}
/// The steps left of this thread's budget of reading ([`within_steps`]), or
/// `None` outside one: `Some(0)` once a reading was refused for it, so a
/// caller tells that refusal from the program's own.
pub fn steps_left() -> Option<u64> {
READS.with_borrow(|r| r.as_ref().map(|r| r.left))
}
/// [`read`], each gate whose standard form would double with its controls
@ -419,8 +500,8 @@ pub fn read_whole(src: &str) -> Result<Reading, Diagnostic> {
/// [`read_whole`] for a generator, at `inputs`.
pub fn read_whole_with(src: &str, inputs: &Inputs) -> Result<Reading, Diagnostic> {
let toks = lex::lex(src)?;
let (stmts, version, depths) = parse::parse_depths(src, &toks)?;
let (program, lines, diagnostics) = elab::elaborate_whole(src, &toks, &stmts, &depths, version, inputs)?;
let (stmts, version) = parse::parse(src, &toks)?;
let (program, lines, diagnostics) = elab::elaborate_whole(src, &toks, &stmts, version, inputs)?;
Ok(Reading { program, lines, diagnostics })
}
@ -461,9 +542,9 @@ pub fn bind(src: &str, values: &std::collections::BTreeMap<String, f64>) -> Resu
/// [`diag::NOT_DIFFERENTIABLE`] (`Q129`), on its line.
pub fn tangents(src: &str, names: &[String]) -> Result<(Reading, Vec<Vec<Vec<f64>>>), Diagnostic> {
let toks = lex::lex(src)?;
let (stmts, version, depths) = parse::parse_depths(src, &toks)?;
let (stmts, version) = parse::parse(src, &toks)?;
let none = Inputs::new();
let (program, lines, diagnostics, t) = elab::elaborate_with(src, &toks, &stmts, &depths, version, &none, Some(elab::Tan::new(names)))?;
let (program, lines, diagnostics, t) = elab::elaborate_with(src, &toks, &stmts, version, &none, Some(elab::Tan::new(names)))?;
Ok((Reading { program, lines, diagnostics }, t))
}
@ -1871,6 +1952,9 @@ mod lang_tests;
#[cfg(all(test, feature = "quantum_qasm"))]
mod whole_tests;
#[cfg(all(test, feature = "quantum_qasm"))]
mod depth_tests;
#[cfg(all(test, feature = "quantum_qasm"))]
mod tests {
use super::*;

View file

@ -54,6 +54,12 @@ pub enum E {
Index(Box<Expr>, Vec<Expr>),
Unary(&'static str, Box<Expr>),
Binary(&'static str, Box<Expr>, Box<Expr>),
/// A run of left-associative operators of one precedence, `a + b - c`:
/// its first operand and each operator with the operand after it, applied
/// left to right. Written flat, so that a run of any length holds no
/// recursion: reading it, evaluating it and dropping it are each a loop.
/// A single operator is a [`E::Binary`].
Chain(Box<Expr>, Vec<(&'static str, Expr)>),
/// A built-in function, a subroutine or an `extern`.
Call(Name, Vec<Expr>),
Cast(Type, Box<Expr>),
@ -176,7 +182,10 @@ pub enum S {
/// `x = e;`, `x += e;`, `c[0] = measure q[0];`.
Assign { target: Expr, op: &'static str, value: Expr },
Expr(Expr),
If { cond: Expr, then: Vec<Stmt>, other: Option<Vec<Stmt>> },
/// `if (c) … else if (d) … else …`: each condition with its branch, in
/// order, and the last `else`. An `else if` chain is flat, as a run of
/// operators is.
If { arms: Vec<(Expr, Vec<Stmt>)>, other: Option<Vec<Stmt>> },
For { ty: Option<Type>, var: Name, over: Expr, body: Vec<Stmt> },
While { cond: Expr, body: Vec<Stmt> },
Switch { on: Expr, cases: Vec<Case>, default: Option<Vec<Stmt>> },

View file

@ -0,0 +1,456 @@
//! Nesting: every recursive construct the reader takes, nested thousands
//! deep, refused with `Q006`; nested to just inside the limits, read. Each
//! runs on a thread of 2 MiB, a runtime worker's stack (and the default for a
//! test's), in whatever profile the tests are built in.
use super::*;
use crate::qasm::parse::MAX_NESTING;
/// A runtime worker's stack, and a test thread's.
const STACK: usize = 2 << 20;
/// `f` on a thread with a stack of `bytes`. A stack overflow aborts the whole
/// test binary, which is what these tests guard against.
fn on_stack<T: Send + 'static>(bytes: usize, f: impl FnOnce() -> T + Send + 'static) -> T {
std::thread::Builder::new().stack_size(bytes).spawn(f).expect("a thread").join().expect("no panic")
}
/// The code `src` is read with: `Ok` and its gates, or the refusal's code.
fn read_on_stack(src: String) -> Result<usize, &'static str> {
on_stack(STACK, move || read(&src).map(|r| r.program.ops.len()).map_err(|d| d.code))
}
const HEAD: &str = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit q;\nbit c;\n";
/// A program whose one statement nests `n` deep in the construct `kind`.
fn nested(kind: &str, n: usize) -> String {
let r = |s: &str| s.repeat(n);
let body = match kind {
"paren" => format!("rz({}0.5{}) q;\n", r("("), r(")")),
"minus" => format!("rz({}0.5) q;\n", r("- ")),
"not" => format!("bool b = {}true;\nx q;\n", r("!")),
"tilde" => format!("const uint k = {}1;\nx q;\n", r("~")),
"sum" => format!("rz(0.5{}) q;\n", r(" + 0")),
"power" => format!("rz(1{}) q;\n", r(" ** 1")),
"call" => format!("rz({}0.5{}) q;\n", r("sin("), r(")")),
"cast" => format!("rz({}0.5{}) q;\n", r("float("), r(")")),
"set" => format!("for int i in {}1{} x q;\n", r("{"), r("}")),
"index" => format!("if (c{} == 1) x q;\n", r("[0]")),
"concat" => format!("let a = q{};\nx q;\n", r(" ++ q")),
"block" => format!("{}x q;{}\n", r("{ "), r(" }")),
"if" => format!("{}x q;\n", r("if (true) ")),
"if_block" => format!("{}x q;{}\n", r("if (true) { "), r(" }")),
"else_if" => format!("{}x q;\n", r("if (false) x q; else ")),
"for" => format!("{}x q;{}\n", r("for int i in [0:0] { "), r(" }")),
"while" => format!("int k = 0;\n{}x q;\n", r("while (k > 0) ")),
"switch" => format!("int k = 0;\n{}x q;{}\n", r("switch (k) { case 0 { "), r(" } }")),
"box" => format!("{}x q;{}\n", r("box { "), r(" }")),
"gate_body" => format!("gate g a {{ {}x a;{} }}\ng q;\n", r("{ "), r(" }")),
"def_body" => format!("def f(qubit a) {{ {}x a;{} }}\nf(q);\n", r("{ "), r(" }")),
"complex" => format!("input {}float{} z;\nx q;\n", r("complex["), r("]")),
"array" => format!("def f(readonly {}int{} a) {{ }}\nx q;\n", r("array["), r(", 1]")),
"durationof" => format!("rz({}0.5{}) q;\n", r("durationof({ "), r(" })")),
"measure" => format!("c = {}measure q{};\n", r("("), r(")")),
other => panic!("no construct {other}"),
};
format!("{HEAD}{body}")
}
/// Every construct that nests, each read by a recursion of the parser, the
/// elaborator, or both.
const NESTING: [&str; 23] = [
"paren", "minus", "not", "tilde", "power", "call", "cast", "set", "index", "concat", "block", "if", "if_block", "for", "while", "switch", "box", "gate_body", "def_body", "complex", "array", "durationof", "measure",
];
/// Nested thousands deep, as a request of a few kilobytes can be, each is
/// refused, `Q006`, on a 2 MiB stack: an overflow would end the process.
#[test]
fn every_construct_nested_past_the_limit_is_refused_not_overflowed() {
for kind in NESTING {
for n in [MAX_NESTING + 1, 5_000] {
assert_eq!(read_on_stack(nested(kind, n)), Err(diag::NESTING), "{kind} nested {n} deep");
}
}
// Chains whose tree is as deep as they are long: a power, an index or a
// `++` of a hundred thousand is refused too.
for kind in ["power", "index", "concat", "minus", "not"] {
assert_eq!(read_on_stack(nested(kind, 100_000)), Err(diag::NESTING), "{kind} 100000 long");
}
}
/// What people write nests a few levels; to just inside the limit, each
/// construct still reads, and the reader's answer is the one it gave before.
#[test]
fn every_construct_nested_inside_the_limit_reads() {
// The statement and the call's arguments take two levels; the rest is
// the construct's.
let inside = MAX_NESTING - 8;
for kind in NESTING {
// `durationof({ … })` nests a block and a statement in each level.
let n = if kind == "durationof" { inside / 3 } else { inside };
let r = read_on_stack(nested(kind, n));
match kind {
// Read, or refused for what they are (a set of sets, a bit's
// bit), never for their depth.
"set" | "index" | "concat" | "tilde" | "complex" | "array" | "durationof" | "measure" => assert!(r.is_ok() || r.is_err_and(|c| c != diag::NESTING), "{kind}: {r:?}"),
_ => assert!(r.is_ok(), "{kind} nested {n} deep: {r:?}"),
}
}
// The angle comes through every level: the statement, the call's
// arguments and the number take three levels, the parentheses the rest.
let p = on_stack(STACK, || parse(&nested("paren", MAX_NESTING - 3)).unwrap());
assert_eq!(p.ops[0].params, vec![0.5]);
let p = on_stack(STACK, || parse(&nested("minus", MAX_NESTING - 3)).unwrap());
assert_eq!(p.ops[0].params, vec![if (MAX_NESTING - 3) % 2 == 0 { 0.5 } else { -0.5 }]);
}
/// A run of operators of one precedence, and an `else if` chain, are flat:
/// read, run and dropped in loops, so they read at any length, on a stack of
/// 1 MiB, and only what nests counts. Each reads to what the same text in
/// parentheses gives, left to right.
#[test]
fn a_run_of_operators_and_an_else_if_chain_read_at_any_length() {
const SMALL: usize = 1 << 20;
let reads = |src: String| on_stack(SMALL, move || read(&src).map(|r| r.program.ops.len()).map_err(|d| d.code));
for n in [MAX_NESTING + 1, 100_000] {
let run = |op: &str, term: &str| format!("{term}{}", format!(" {op} {term}").repeat(n));
for (what, src) in [
("sum", format!("{HEAD}rz({}) q;\n", run("+", "0.5"))),
("product", format!("{HEAD}rz({}) q;\n", run("*", "1.0"))),
("declared", format!("{HEAD}float x = {};\nx q;\n", run("-", "0.5"))),
("or", format!("{HEAD}bool b = {};\nx q;\n", run("||", "false"))),
("and", format!("{HEAD}bool b = {};\nx q;\n", run("&&", "true"))),
("known bits", format!("{HEAD}if ({}) x q;\n", run("&&", "true"))),
("measured bits", format!("{HEAD}c = measure q;\nif ({}) x q;\n", run("&&", "c"))),
("measured bits or", format!("{HEAD}c = measure q;\nif ({}) x q;\n", run("||", "c"))),
("else if known", format!("{HEAD}{}x q;\n", "if (false) y q; else ".repeat(n))),
("else if measured", format!("{HEAD}c = measure q;\n{}x q;\n", "if (c == 1) y q; else ".repeat(n))),
] {
assert!(reads(src).is_ok_and(|ops| ops >= 1), "{what} of {n}");
}
}
// Left to right, in the double's own order: not rebalanced.
let terms = [0.1, 0.7, 1e-17, 0.3, 1e17, -1e17, 0.2];
let flat: String = terms.iter().map(|t| format!("{t:e}")).collect::<Vec<_>>().join(" + ");
let want = terms[1..].iter().fold(terms[0], |a, t| a + t);
let p = parse(&format!("{HEAD}rz({flat}) q;\n")).unwrap();
assert_eq!(p.ops[0].params, vec![want]);
for (run, tree) in [("7 - 2 + 3 - 4", "((7 - 2) + 3) - 4"), ("2 * 7 / 4 % 3", "((2 * 7) / 4) % 3"), ("1 << 3 >> 1 << 2", "((1 << 3) >> 1) << 2"), ("1 < 2 == true != false", "((1 < 2) == true) != false")] {
let v = |e: &str| parse(&format!("{HEAD}const int k = {e};\nqubit[32] r;\nx r[k];\n")).map(|p| p.ops[0].qubits.clone());
if run.contains('<') && run.contains("==") {
let v = |e: &str| parse(&format!("{HEAD}if ({e}) x q;\n")).map(|p| p.ops.len());
assert_eq!(v(run), v(tree), "{run}");
} else {
assert_eq!(v(run), v(tree), "{run}");
}
}
// A derivative runs along a run too, as it does along the tree.
let names = ["a".to_string()];
for (run, tree) in [("a + 2.0 - a * 3.0 + 1.5", "((a + 2.0) - (a * 3.0)) + 1.5"), ("a * 2.0 * a / 3.0", "((a * 2.0) * a) / 3.0")] {
let derivative = |e: &str| tangents(&format!("{HEAD}const float a = 0.7;\nrz({e}) q;\n"), &names).map(|(_, t)| t).map_err(|d| d.code);
assert_eq!(derivative(run), derivative(tree), "{run}");
}
// An `else if` chain's steps are the ones the same branches nested write,
// and what its branches assign differently is no longer known after it.
let chain = "if (c == 1) x q; else if (c == 0) y q; else if (true) z q; else h q;";
let nest = "if (c == 1) x q; else { if (c == 0) y q; else { if (true) z q; else h q; } }";
let steps = |body: &str| read(&format!("{HEAD}c = measure q;\n{body}\n")).unwrap().program;
assert_eq!(steps(chain), steps(nest));
let after = |a: u32, b: u32, c: u32| read_on_stack(format!("{HEAD}qubit[3] r;\nc = measure q;\nint k = 0;\nif (c == 1) k = {a}; else if (c == 0) k = {b}; else k = {c};\nx r[k];\n"));
assert_eq!(after(1, 1, 1), Ok(1));
assert_eq!(after(1, 2, 1), Err(diag::NOT_KNOWN));
assert_eq!(after(1, 1, 2), Err(diag::NOT_KNOWN));
}
/// A switch's case list on measured bits makes one condition of every value
/// listed: held flat, however many there are, on a stack of 1 MiB.
#[test]
fn a_switch_on_measured_bits_lists_any_number_of_values() {
let src = format!("OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nc = measure q;\nswitch (c) {{ case {}1 {{ x q[0]; }} }}\n", "1, ".repeat(600_000));
let r = on_stack(1 << 20, move || read(&src).map(|r| r.program.ops.len()).map_err(|d| d.code));
assert!(r.is_ok_and(|ops| ops >= 1), "{r:?}");
}
/// An array's dimensions cost the text no level, as they are a comma apart,
/// but each is a level of its value, which what copies, measures and writes
/// it recurses on: so they are held, where the type is read.
#[test]
fn an_array_past_its_dimensions_is_refused_where_its_type_is_read() {
let array = |n: usize| format!("{HEAD}array[int[8]{}] a;\nx q;\n", ", 1".repeat(n));
assert_eq!(read_on_stack(array(crate::qasm::parse::MAX_ARRAY_DIMS)), Ok(1));
for n in [crate::qasm::parse::MAX_ARRAY_DIMS + 1, 100_000] {
assert_eq!(read_on_stack(array(n)), Err(diag::NESTING), "{n} dimensions");
}
}
/// What a generator's loops cost is spent from the budget as they are written
/// out: a pass of a loop that writes nothing, and what a loop writes, by its
/// size, so that a sweep's points are held to the request's one budget.
#[test]
fn a_generator_s_passes_and_what_they_write_spend_the_budget() {
let (r, left) = within_steps(1_000, || (instantiate("input int n;\nqubit q;\nfor int i in [1:n] { }\nx q;\n", &[("n".to_string(), 999_999)].into_iter().collect()), steps_left()));
assert!(r.is_err_and(|e| e.contains("Q312")));
assert_eq!(left, Some(0));
let body = "x q;\n".repeat(1_000);
let fat = format!("input int n;\nqubit q;\nfor int i in [1:n] {{\n{body}}}\n");
let (r, left) = within_steps(1_000, || (instantiate(&fat, &[("n".to_string(), 3_000)].into_iter().collect()), steps_left()));
assert!(r.is_err_and(|e| e.contains("Q312")));
assert_eq!(left, Some(0));
}
/// A generator's integer `const`s fold in the writer, and both readers of a
/// generator agree on a flat sum, and an `else if` chain, of any length.
#[test]
fn both_readers_of_a_generator_take_a_flat_sum() {
let src = format!("input int nn;\nqubit[1] q;\nconst int s = nn{};\nfor int i in [0:s % 3] {{ x q[0]; }}\n", " + nn".repeat(999));
let inputs: Inputs = [("nn".to_string(), 2)].into_iter().collect();
let direct = read_with(&src, &inputs).unwrap();
let written = read(&instantiate(&src, &inputs).unwrap()).unwrap();
assert_eq!(direct.program.ops.len(), written.program.ops.len());
// An `else if` chain as well, decided on writing or left to the reader.
for arm in ["if (nn == 0) x q[0]; else ", "if (true) x q[0]; else "] {
let src = format!("input int nn;\nqubit[1] q;\n{}x q[0];\n", arm.repeat(5_000));
let (a, b) = on_stack(1 << 20, move || {
let inputs: Inputs = [("nn".to_string(), 2)].into_iter().collect();
(read_with(&src, &inputs).map(|r| r.program.ops.len()), instantiate(&src, &inputs).and_then(|w| read(&w).map(|r| r.program.ops.len()).map_err(|d| d.to_string())))
});
assert_eq!(a.map_err(|d| d.to_string()), b, "{arm}");
}
}
/// A modifier chain is a list, and an `include` a statement of its own:
/// neither nests, so either reads at any length (the gates the chain makes
/// are bounded by the reader's gate limit).
#[test]
fn modifier_chains_and_includes_read_at_any_length() {
let inv = format!("{HEAD}{}x q;\n", "inv @ ".repeat(100_001));
assert_eq!(read_on_stack(inv), Ok(1));
let includes = format!("{HEAD}{}x q;\n", "include \"other.inc\";\n".repeat(2_000));
let r = on_stack(STACK, move || read(&includes).map(|r| (r.program.ops.len(), r.diagnostics.iter().all(|d| d.code == diag::UNKNOWN_INCLUDE))));
assert_eq!(r, Ok((1, true)));
}
/// Calls stack their bodies: within one body the text's limit holds, and
/// past what calls stack up to, in all, the elaborator refuses (`Q006`)
/// before the stack runs out. Each body here nests within the text's limit,
/// and thirty of them, each called from the one before, nest past it in all.
#[test]
fn calls_that_stack_past_the_limit_in_all_are_refused() {
let chain = |levels: usize, calls: usize| {
let mut s = format!("{HEAD}def f0(float t, qubit a) {{ rz(t) a; }}\n");
for i in 1..=calls {
s += &format!("def f{i}(float t, qubit a) {{ {}f{}(t, a); }}\n", "if (true) ".repeat(levels), i - 1);
}
s + &format!("f{calls}(0.5, q);\n")
};
assert_eq!(read_on_stack(chain(60, 31)), Err(diag::NESTING));
// Shallower bodies read, through as many calls.
assert_eq!(read_on_stack(chain(4, 31)), Ok(1));
// Calls deeper than the call limit keep their own code.
assert_eq!(read_on_stack(chain(0, 40)), Err(diag::TOO_DEEP));
}
/// A generator written out (`instantiate`) folds its integers and unrolls
/// its loops by a reader of its own: nested past the limit, it refuses too.
#[test]
fn a_generator_nested_past_the_limit_is_refused_when_written_out() {
let inst = |src: String| on_stack(STACK, move || instantiate(&src, &Inputs::new()).map(|_| ()).map_err(|e| e[e.len() - 5..e.len() - 1].to_string()));
for (what, src) in [
("parentheses", format!("{HEAD}const int a = {}1{};\nx q;\n", "(".repeat(5_000), ")".repeat(5_000))),
("minus", format!("{HEAD}const int a = {}1;\nx q;\n", "- ".repeat(10_000))),
("power", format!("{HEAD}const int a = 1{};\nx q;\n", " ** 1".repeat(10_000))),
("loops", format!("{HEAD}{}x q;{}\n", "for int i in [0:0] { ".repeat(5_000), " }".repeat(5_000))),
("conditions", format!("{HEAD}const int k = 1;\n{}x q;\n", "if (k == 1) ".repeat(5_000))),
] {
assert_eq!(inst(src), Err("Q006".into()), "{what}");
}
// Inside the limit, written out as before.
let ok = format!("{HEAD}const int a = {}1{};\nx q;\n", "(".repeat(40), ")".repeat(40));
assert!(on_stack(STACK, move || instantiate(&ok, &Inputs::new())).is_ok());
}
/// A budget of reading ([`within_steps`]) holds every program read under it
/// together, statements, gates and a generator's loop passes alike; each
/// program read once however often it is read; and says when it ran out.
#[test]
fn a_budget_holds_every_reading_under_it_together_and_each_program_is_read_once() {
let heavy = |k: usize| format!("OPENQASM 3.0;\nqubit q;\nint a = 0;\nfor int i in [0:99999] {{ a += 1; }}\n// {k}\nx q;\n");
// One heavy program reads within a budget its size; read again it costs nothing.
let (first, again, left) = within_steps(500_000, || {
let first = read(&heavy(0)).is_ok();
let spent = steps_left();
let again = read(&heavy(0)).is_ok();
(first, again, (spent, steps_left()))
});
assert!(first && again);
assert!(left.0 == left.1 && left.0.is_some_and(|l| l < 400_000), "{left:?}");
// Ten different ones do not: the budget refuses the reading that runs it out.
let (refused, left) = within_steps(500_000, || {
let r: Vec<Result<Reading, Diagnostic>> = (0..10).map(|k| read(&heavy(k))).collect();
(r.iter().position(Result::is_err), steps_left())
});
assert!(refused.is_some_and(|k| (1..10).contains(&k)), "{refused:?}");
assert_eq!(left, Some(0));
// Gates count: a power that writes a million gates is past a budget of a thousand.
let gates = "OPENQASM 3.0;\ngate g a { x a; h a; }\nqubit q;\npow(5000) @ g q;\n".to_string();
let r = within_steps(1_000, || read(&gates).map(|_| ()).map_err(|d| d.code));
assert_eq!(r, Err(diag::UNROLL_LIMIT));
// Outside a budget, nothing is counted.
assert_eq!(steps_left(), None);
assert!(read(&heavy(0)).is_ok());
}
/// A small property test, with a seeded generator rather than a crate: any
/// mix of nesting constructs, at any depth, reads or is refused, and never
/// runs a 2 MiB stack out; inside the limit it reads, past it the refusal is
/// `Q006`. What the limit holds is the parser's recursion and each tree's
/// height together: a construct that nests by recursion (parentheses), one
/// that nests by height (a prefix `-`), or both (a call, a cast, a product
/// in parentheses). Either alone is at least the nesting; their sum, at most.
#[test]
fn any_mix_of_nesting_reads_or_is_refused_never_overflows() {
on_stack(STACK, || {
let mut x: u64 = 0x9e37_79b9_7f4a_7c15;
let mut next = move |n: u64| {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
x % n
};
for _ in 0..300 {
let depth = next(400) as usize;
let statements = next(depth as u64 + 1) as usize;
let (mut open, mut close) = (String::new(), String::new());
for _ in 0..statements {
match next(3) {
0 => {
open += "{ ";
close = format!(" }}{close}");
}
// With its own braces: `if (true) { … }` is one level,
// its body the braces' statements.
1 => {
open += "if (true) { ";
close = format!(" }}{close}");
}
_ => {
open += "for int i in [0:0] { ";
close = format!(" }}{close}");
}
}
}
let (mut pre, mut post, mut recursion, mut height) = (String::new(), String::new(), 0, 0);
for _ in statements..depth {
let (p, q, r, h) = [("(", ")", 1, 0), ("- ", "", 0, 1), ("sin(", ")", 1, 1), ("float(", ")", 1, 1), ("(", " * 1)", 1, 1)][next(5) as usize];
pre += p;
post = format!("{q}{post}");
recursion += r;
height += h;
}
let src = format!("{HEAD}{open}rz({pre}0.5{post}) q;{close}\n");
let r = read(&src).map(|_| ()).map_err(|d| d.code);
// The statements, the call's arguments and the number take their
// levels; a product's right operand, one more at most.
let at_least = statements + 2 + recursion.max(height) + 1;
let at_most = statements + 2 + recursion + height + 2;
if at_most <= MAX_NESTING {
assert_eq!(r, Ok(()), "{depth} deep:\n{src}");
} else if at_least > MAX_NESTING {
assert_eq!(r, Err(diag::NESTING), "{depth} deep:\n{src}");
}
}
});
}
/// The table's meaning for `Q006` states the limits the reader holds.
#[test]
fn the_code_states_the_limits() {
let m = diag::lookup(diag::NESTING).expect("Q006 is in the table").meaning;
assert!(m.contains(&format!("more than {MAX_NESTING} levels")), "{m}");
assert!(m.contains(&format!("more than {} levels in all", super::elab::MAX_NEST)), "{m}");
}
/// Memory: a value as large as its declaration asks for, a register's steps
/// grown in a loop, a bit string wider than an integer: each refused, on a
/// 2 MiB thread, before what it would hold is made. A declaration of a
/// hundred bytes asked for 137 GB here, which no refusal stopped: an
/// allocation that fails ends the process.
#[test]
fn what_a_program_would_hold_past_the_limit_is_refused_before_it_is_made() {
for (what, body) in [
("an array of 2^32 − 1 integers", "array[int[8], 4294967295] a;\n"),
("an array of 10^10 booleans", "array[bool, 100000, 100000] a;\n"),
("an array of wide bit strings", "array[bit[1048576], 1000] a;\n"),
("a bit register of 4·10^9 bits", "bit[4000000000] c;\n"),
("resets of a wide register, repeated", "qubit[60000] r;\nfor int i in [0:999999] reset r;\n"),
("measurements of a wide register, repeated", "qubit[60000] r;\nbit[60000] m;\nfor int i in [0:999999] m = measure r;\n"),
("barriers on a wide register, repeated", "qubit[60000] r;\nfor int i in [0:999999] barrier r;\n"),
] {
let t = std::time::Instant::now();
assert_eq!(read_on_stack(format!("{HEAD}{body}x q;\n")), Err(diag::TOO_LARGE), "{what}");
assert!(t.elapsed() < std::time::Duration::from_secs(60), "{what}: {:?}", t.elapsed());
eprintln!("{what}: refused in {:?}", t.elapsed());
}
// Within the limits, as programs hold them, each reads.
for body in ["array[int[8], 1000] a;\n", "array[float, 64, 64] m;\n", "bit[1048576] wide;\n", "qubit[64] r;\nfor int i in [0:99] reset r;\n"] {
assert!(read_on_stack(format!("{HEAD}{body}x q;\n")).is_ok(), "{body}");
}
// Reading an element of a large array copies the element, not the array;
// copying the whole array is counted as reading time.
let elements = format!("{HEAD}array[int[32], 100000] a;\nint s = 0;\nfor int i in [0:99999] {{ s += a[i]; }}\nx q;\n");
assert!(read_on_stack(elements).is_ok());
// Within a budget of a million steps, a loop that copies a 3.2 MB array
// runs out of it after some forty copies, not after a hundred thousand.
let copies = format!("{HEAD}array[int[32], 100000] a;\narray[int[32], 100000] b;\nfor int i in [0:99999] {{ b = a; }}\nx q;\n");
let r = on_stack(STACK, move || within_steps(1_000_000, || read(&copies).map(|_| ()).map_err(|d| d.code)));
assert_eq!(r, Err(diag::UNROLL_LIMIT));
}
/// A statement on a register of tens of thousands of qubits costs its
/// length: each qubit is checked once, where each was compared with every one
/// before it (some 1.8·10⁹ comparisons a statement, so a loop of resets ran
/// for minutes within a few hundred steps).
#[test]
fn a_statement_on_a_wide_register_costs_its_length() {
let t = std::time::Instant::now();
for body in ["for int i in [0:49] reset r;\n", "for int i in [0:49] barrier r;\n", "bit[60000] m;\nfor int i in [0:49] m = measure r;\n", "for int i in [0:9] x r;\n"] {
assert!(read_on_stack(format!("{HEAD}qubit[60000] r;\n{body}x q;\n")).is_ok(), "{body}");
}
assert!(t.elapsed() < std::time::Duration::from_secs(30), "{:?}", t.elapsed());
assert_eq!(read_on_stack(format!("{HEAD}qubit[4] r;\nlet a = r[0:3] ++ r[1:2];\nreset a;\n")), Err(diag::QUBIT_TWICE));
}
/// A bit string wider than an integer holds, given to an integer, is refused
/// with its code; the reader once panicked on it.
#[test]
fn a_bit_string_wider_than_an_integer_is_refused_not_a_panic() {
for body in [format!("int[32] x = \"{}\";\n", "1".repeat(200)), "bit[200] b;\nint[32] x = b;\n".to_string(), format!("uint y = \"{}\";\n", "0".repeat(121))] {
assert_eq!(read_on_stack(format!("{HEAD}{body}x q;\n")), Err(diag::INTEGER_RANGE), "{body}");
}
assert!(read_on_stack(format!("{HEAD}int[32] x = \"{}\";\nx q;\n", "1".repeat(20))).is_ok());
}
/// A loop over a range makes each value as it reaches it: a range of twenty
/// million that breaks at once holds no list of twenty million values (608
/// MB, before its first pass). Measured in a process of its own, by its peak
/// resident memory, where the system says it (Linux).
#[cfg(target_os = "linux")]
#[test]
fn a_range_is_not_made_all_at_once() {
const CHILD: &str = "WAI_QUANTUM_DEPTH_RANGE_CHILD";
let peak = || std::fs::read_to_string("/proc/self/status").ok().and_then(|s| s.lines().find(|l| l.starts_with("VmHWM:")).and_then(|l| l.split_whitespace().nth(1)?.parse::<u64>().ok())).unwrap_or(0);
if std::env::var_os(CHILD).is_some() {
let src = format!("{HEAD}for int i in [0:19999999] {{ break; }}\nx q;\n");
assert!(read(&src).is_ok());
println!("peak-kib {}", peak());
return;
}
let out = std::process::Command::new(std::env::current_exe().unwrap())
.args(["--exact", "qasm::depth_tests::a_range_is_not_made_all_at_once", "--nocapture", "--test-threads", "1"])
.env(CHILD, "1")
.output()
.unwrap();
let text = String::from_utf8_lossy(&out.stdout);
let kib: u64 = text.split("peak-kib ").nth(1).and_then(|v| v.split_whitespace().next()?.parse().ok()).unwrap_or_else(|| panic!("no peak: {text}"));
assert!(out.status.success() && kib < 300 << 10, "peak {kib} KiB:\n{text}");
}

View file

@ -163,6 +163,7 @@ table! {
UNCLOSED = "Q003", Refused, "A string, a block comment, a bracket or a calibration block that is not closed.";
EXPECTED = "Q004", Refused, "Something else was expected here: a `;`, a name, an expression or a statement (the message says which).";
MISPLACED = "Q005", Refused, "A statement in a place the language does not allow it: `else` with no `if`, `case` outside `switch`, a declaration inside a gate.";
NESTING = "Q006", Refused, "A program nested deeper than the reader takes: statements, parentheses, operators, brackets and types more than 64 levels deep in its text (a run of one precedence, `a + b + c`, and an `else if` chain each count one, however long), an array of more than 7 dimensions, or more than 256 levels in all through the gates and subroutines it calls; refused, never read by a recursion that could run out of stack.";
// Versions, includes, pragmas and annotations.
UNKNOWN_VERSION = "Q010", Warned, "An `OPENQASM` version other than 2.x or 3.x: the program is read as OpenQASM 3.";
@ -201,7 +202,7 @@ table! {
ASSIGN_CONSTANT = "Q127", Refused, "An assignment to a constant, an input or a loop variable.";
MEASURE_PLACE = "Q128", Refused, "A measurement where a value is needed that is not a bit (measurement gives bits, into a bit register).";
NOT_DIFFERENTIABLE = "Q129", Refused, "A parameter (`input float`, `input angle`) a gradient cannot follow to the gates: under a modifier, in a classical variable, a condition, an index, a size, a loop's range, a subroutine or `gphase`, or one whose type rounds it (`angle[n]`, `float[32]`); a gradient follows a parameter through arithmetic, the built-in functions and gate definitions into a gate's arguments.";
NESTING = "Q130", Refused, "A program nested deeper than 128, before a reader's stack could run out: a statement at the top level is at depth 1, and each statement in a block or a body, each expression a statement holds, each operand of an operator, what a bracket holds (of an expression or a type) and each index's indexed value, one deeper than what holds it; operators group as they bind (`a + b + c` is `(a + b) + c`), an `else if` is at the depth of its `if` (one `if` holds at most 1024 of them), and a call's body is read in its place, its statements one deeper than the call.";
TOO_LARGE = "Q130", Refused, "A program whose reading would hold more memory than the reader takes: a classical value (an array, a bit register) of more than 64 MiB, each element counted at its width, or more than 512 MiB in all of its values, gates, steps, measurements and timing; refused before it is made.";
// Gates and modifiers.
GATE_INLINED = "Q200", Lowered, "A `gate` the program defines, inlined at each call with its parameters bound.";
@ -231,7 +232,7 @@ table! {
WHILE_UNROLLED = "Q309", Lowered, "`while` whose condition is known on reading: unrolled until it fails.";
UNBOUNDED_WHILE = "Q310", Refused, "A `while` whose condition reads measured bits and that declares no bound: add `@wai.max_iterations N` before it.";
WHILE_CUT = "Q311", Warned, "A `while` on measured bits run at most its declared bound (`@wai.max_iterations N`): a run that still meets its condition after N iterations stops looping there.";
UNROLL_LIMIT = "Q312", Refused, "A program that does not finish unrolling within the reader's bound (20 000 000 statements and loop iterations).";
UNROLL_LIMIT = "Q312", Refused, "A program that does not finish unrolling within the reader's bound (20 000 000 statements and loop iterations), or that runs out a caller's budget of reading (`qasm::within_steps`: statements, gates and a generator's loop passes, shared by every program read under it).";
BAD_RANGE = "Q313", Refused, "A range with a zero step, or that does not read as `[start:end]` or `[start:step:end]`.";
OUTSIDE_LOOP = "Q314", Refused, "`break` or `continue` outside a loop, or `return` outside a subroutine.";
RUNTIME_RETURN = "Q315", Refused, "A `return` with a value under a condition on measured bits.";

File diff suppressed because it is too large Load diff

View file

@ -646,30 +646,39 @@ pub(crate) fn chain(kind: &str, d: usize, k: usize, inner: usize) -> String {
s
}
/// The nesting bound is the text's (Q130): each shape is read at 128 and refused at 129,
/// whatever the shape, an operator chain or a type among them; an `else if` is at its
/// `if`'s depth, up to 1024 of them; and a call's body counts from the call, so nesting
/// through calls is bounded as nesting in one body is.
/// The shapes the reader's nesting bound is counted over, each refused past it
/// (`Q006`, at [`crate::qasm::parse::MAX_NESTING`] levels in the text) and read inside
/// it; a run of one operator and an `else if` ladder are flat, read at any length; and a
/// call's body counts toward the bound through calls, so nesting through calls is bounded
/// as nesting in one body is. (`depth_tests` holds every construct to the limits.)
#[test]
fn nesting_is_bounded_as_the_text_counts_it() {
for shape in ["parens", "unary", "sum", "product", "index", "concat", "type", "blocks", "ifs", "fors"] {
let past = read(&nested(shape, 127)).map(|_| ()).unwrap_err();
assert_eq!(past.code, "Q130", "{shape} at 129: {past}");
if let Err(d) = read(&nested(shape, 126)) {
assert_ne!(d.code, "Q130", "{shape} at 128 is within the bound: {d}");
use crate::qasm::parse::MAX_NESTING;
for shape in ["parens", "unary", "index", "concat", "type", "blocks", "ifs", "fors"] {
let past = read(&nested(shape, 2 * MAX_NESTING)).map(|_| ()).unwrap_err();
assert_eq!(past.code, diag::NESTING, "{shape} at {}: {past}", 2 * MAX_NESTING);
if let Err(d) = read(&nested(shape, MAX_NESTING - 8)) {
assert_ne!(d.code, diag::NESTING, "{shape} inside the bound: {d}");
}
}
for shape in ["parens", "unary", "sum", "product", "blocks", "ifs", "fors"] {
assert!(read(&nested(shape, 126)).is_ok(), "{shape} at 128 reads");
for shape in ["parens", "unary", "blocks", "ifs", "fors"] {
assert!(read(&nested(shape, MAX_NESTING - 8)).is_ok(), "{shape} inside the bound reads");
}
// A run of one operator is one level, at any length.
for shape in ["sum", "product"] {
assert!(read(&nested(shape, 10_000)).is_ok(), "{shape} of 10,000 terms reads");
}
let ladder = |n: usize| format!("OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit q;\nint s = 3;\nif (s == 0) x q;\n{}else id q;\n", (1..=n).map(|i| format!("else if (s == {i}) {}\n", if i == 3 { "x q;" } else { "id q;" })).collect::<String>());
let read_ladder = read(&ladder(1024)).unwrap_or_else(|d| panic!("1024 arms: {d}"));
assert_eq!(read_ladder.program.ops.len(), 1, "the arm that holds is the one read");
assert_eq!(read(&ladder(1025)).map(|_| ()).unwrap_err().code, "Q130", "1025 arms");
// Through calls: 25 gates of 4 loops each reach 128, and one block more 129.
for arms in [1024, 5000] {
let read_ladder = read(&ladder(arms)).unwrap_or_else(|d| panic!("{arms} arms: {d}"));
assert_eq!(read_ladder.program.ops.len(), 1, "{arms} arms: the arm that holds is the one read");
}
// Through calls: 25 gates of 4 loops each read; 31 of 8 pass the bound in all.
for kind in ["gates", "defs"] {
assert!(read(&chain(kind, 25, 4, 0)).is_ok(), "{kind} at 128: {:?}", read(&chain(kind, 25, 4, 0)).err());
let past = read(&chain(kind, 25, 4, 1)).map(|_| ()).unwrap_err();
assert_eq!(past.code, "Q130", "{kind} at 129: {past}");
for inner in [0, 1] {
assert!(read(&chain(kind, 25, 4, inner)).is_ok(), "{kind}, 25 of 4 loops: {:?}", read(&chain(kind, 25, 4, inner)).err());
}
let past = read(&chain(kind, 31, 8, 0)).map(|_| ()).unwrap_err();
assert_eq!(past.code, diag::NESTING, "{kind}, 31 of 8 loops: {past}");
}
}

File diff suppressed because it is too large Load diff

View file

@ -1469,6 +1469,16 @@ impl<'a> Lower<'a> {
let (x, y) = (self.eval(a, locals)?, self.eval(b, locals)?);
self.arith(op, x, y, at)
}
E::Chain(first, rest) => {
let mut x = self.eval(first, locals)?;
let mut whole = first.at;
for (op, e) in rest {
let y = self.eval(e, locals)?;
whole = whole.to(e.at);
x = self.arith(op, x, y, whole)?;
}
Ok(x)
}
E::Call(name, args) => self.call_value(&name.text, args, locals, at),
E::Set(items) => {
let mut s = Vec::with_capacity(items.len());

View file

@ -211,3 +211,21 @@ fn the_schedule_writes_as_json_with_every_instruction() {
assert_eq!(j["instructions"][2]["bit"], "c");
assert_eq!(j["calls"][0]["name"], "measure");
}
/// An expression in a calibration nested past the reader's limit (the pulse
/// reader reads a calibration's expressions with the program's parser) is
/// refused, `Q006`, on a runtime worker's 2 MiB stack, as deep as a request
/// of a few kilobytes nests it.
#[test]
fn a_calibration_s_expression_nested_past_the_limit_is_refused() {
let deep = format!("{CAL}defcal x $0 {{ play(q0, gaussian({}0.2{}, 160dt, 40dt)); }}\nx $0;\n", "(".repeat(5_000), ")".repeat(5_000));
#[cfg(not(target_family = "wasm"))]
let d = std::thread::Builder::new().stack_size(2 << 20).spawn(move || refused(&deep)).unwrap().join().unwrap();
// No threads under WASI: the module's own stack.
#[cfg(target_family = "wasm")]
let d = refused(&deep);
assert_eq!(d.code, crate::qasm::diag::NESTING, "{d}");
let inside = format!("{CAL}defcal x $0 {{ play(q0, gaussian({}0.2{}, 160dt, 40dt)); }}\nx $0;\n", "(".repeat(40), ")".repeat(40));
assert_eq!(plays(&sched(&inside)).len(), 1);
}

View file

@ -9,12 +9,14 @@
//! as it was, its integer names still replaced by their values; the reader
//! reads that too.
use std::cell::Cell;
use std::collections::{BTreeSet, HashMap};
use super::Inputs;
use super::diag::{self, Diagnostic};
use super::elab::{MAX_STEPS, RESERVED};
use super::elab::{MAX_HELD, MAX_STEPS, RESERVED};
use super::lex::{Tok, Token, lex, text};
use super::parse::MAX_NESTING;
type R<T> = Result<T, Diagnostic>;
@ -210,7 +212,7 @@ fn unroll(src: &str, toks: Vec<Token>, inputs: &Inputs) -> R<Vec<Token>> {
}
return Ok(toks);
}
let mut u = Unroller { src, toks: &toks, inputs, env: HashMap::new(), shadow: BTreeSet::new(), taken: taken_names(&toks), out: Vec::with_capacity(toks.len()), used: Vec::new(), iterations: 0 };
let mut u = Unroller { src, toks: &toks, inputs, env: HashMap::new(), shadow: BTreeSet::new(), taken: taken_names(&toks), out: Vec::with_capacity(toks.len()), used: Vec::new(), iterations: 0, depth: Cell::new(0) };
u.seq(0, toks.len())?;
if let Some(unused) = inputs.keys().find(|k| !u.used.contains(k)) {
return err(diag::INPUT_VALUE, None, src, format!("the program declares no `input {unused}`"));
@ -266,9 +268,36 @@ struct Unroller<'a> {
out: Vec<Token>,
used: Vec<&'a String>,
iterations: u64,
/// How deep the unroller's recursion stands: statements in statements,
/// held to the reader's [`MAX_NESTING`].
depth: Cell<usize>,
}
/// The refusal of what nests past [`MAX_NESTING`], at `t`.
fn too_deep<T>(t: Option<&Token>, src: &str) -> R<T> {
err(
diag::NESTING,
t,
src,
format!("this nests deeper than {MAX_NESTING} levels (statements, parentheses and operators together); write it in steps, with a `const` for a part, or a long `else if` chain as a `switch`"),
)
}
impl<'a> Unroller<'a> {
/// One level deeper, at the token `i`, or the refusal past [`MAX_NESTING`].
fn enter(&self, i: usize) -> R<()> {
let d = self.depth.get();
if d >= MAX_NESTING {
return too_deep(self.tok(i.min(self.toks.len().saturating_sub(1))), self.src);
}
self.depth.set(d + 1);
Ok(())
}
fn leave(&self) {
self.depth.set(self.depth.get() - 1);
}
fn tok(&self, i: usize) -> Option<&'a Token> {
self.toks.get(i).or(self.toks.last())
}
@ -281,6 +310,14 @@ impl<'a> Unroller<'a> {
if self.out.len() >= MAX_TOKENS {
return err(diag::UNROLL_LIMIT, Some(&t), self.src, format!("the program unrolls past {MAX_TOKENS} tokens"));
}
// The tokens written are held as the reader holds a program's lists
// (`elab::MAX_HELD`), and every sixteen count as a step of reading.
if (self.out.len() as u64 + 1) * std::mem::size_of::<Token>() as u64 > MAX_HELD {
return err(diag::TOO_LARGE, Some(&t), self.src, format!("the program written out would hold more than {} MiB of tokens; write it out on your own machine, or at smaller inputs", MAX_HELD >> 20));
}
if self.out.len() % 16 == 15 && super::spend_step() {
return err(diag::UNROLL_LIMIT, Some(&t), self.src, "the programs read for one request unroll past its budget of steps together (wai_quantum::qasm::within_steps)");
}
self.out.push(t);
Ok(())
}
@ -347,6 +384,13 @@ impl<'a> Unroller<'a> {
/// The index just past the statement at `i` (a loop or `if` included).
fn statement_end(&self, i: usize) -> R<usize> {
self.enter(i)?;
let r = self.statement_end_here(i);
self.leave();
r
}
fn statement_end_here(&self, i: usize) -> R<usize> {
match self.ident(i) {
Some("if") => self.if_extent(i),
Some("for") => {
@ -372,17 +416,23 @@ impl<'a> Unroller<'a> {
}
}
/// The index just past the `if` statement at `i`, its `else` included.
fn if_extent(&self, i: usize) -> R<usize> {
if !self.sym(i + 1, "(") {
return self.fail(diag::EXPECTED, i, "expected `(` after `if`");
/// The index just past the `if` statement at `i`, its `else` included:
/// an `else if` chain followed in a loop, at any length.
fn if_extent(&self, mut i: usize) -> R<usize> {
loop {
if !self.sym(i + 1, "(") {
return self.fail(diag::EXPECTED, i, "expected `(` after `if`");
}
let close = self.close(i + 1, ("(", ")"))?;
let (_, next) = self.body(close)?;
if self.ident(next) != Some("else") {
return Ok(next);
}
if self.ident(next + 1) != Some("if") {
return Ok(self.body(next + 1)?.1);
}
i = next + 1;
}
let close = self.close(i + 1, ("(", ")"))?;
let (_, mut next) = self.body(close)?;
if self.ident(next) == Some("else") {
next = if self.ident(next + 1) == Some("if") { self.if_extent(next + 1)? } else { self.body(next + 1)?.1 };
}
Ok(next)
}
/// Whether the next token written starts a statement.
@ -438,7 +488,7 @@ impl<'a> Unroller<'a> {
/// The integer the tokens `lo..hi` evaluate to.
fn value(&self, lo: usize, hi: usize) -> R<i64> {
let mut e = IntExpr { src: self.src, toks: &self.toks[lo..hi], pos: 0, env: &self.env, at: self.tok(lo) };
let mut e = IntExpr { src: self.src, toks: &self.toks[lo..hi], pos: 0, env: &self.env, at: self.tok(lo), depth: 0 };
let v = e.or()?;
if e.pos != e.toks.len() {
return err(diag::EXPECTED, e.toks.get(e.pos), self.src, format!("unexpected `{}` in an integer expression", text(&e.toks[e.pos].tok)));
@ -505,6 +555,13 @@ impl<'a> Unroller<'a> {
}
fn seq(&mut self, lo: usize, hi: usize) -> R<()> {
self.enter(lo)?;
let r = self.seq_here(lo, hi);
self.leave();
r
}
fn seq_here(&mut self, lo: usize, hi: usize) -> R<()> {
let mut i = lo;
while i < hi {
let statement = self.at_statement_start();
@ -587,17 +644,20 @@ impl<'a> Unroller<'a> {
// A condition on integers alone is decided here.
let keep = self.value(i + 2, close - 1)? != 0;
let ((tlo, thi), mut next) = self.body(close)?;
let else_if = self.ident(next) == Some("else") && self.ident(next + 1) == Some("if");
if !keep && else_if {
// The rest of the chain is read here, in this loop,
// so a chain of any length holds no recursion.
i = next + 1;
continue;
}
let mut other = None;
if self.ident(next) == Some("else") {
if self.ident(next + 1) == Some("if") {
let end = self.if_extent(next + 1)?;
other = Some((next + 1, end));
next = end;
} else {
let (span, after) = self.body(next + 1)?;
other = Some(span);
next = after;
}
if else_if {
next = self.if_extent(next + 1)?;
} else if self.ident(next) == Some("else") {
let (span, after) = self.body(next + 1)?;
other = Some(span);
next = after;
}
if keep {
self.seq(tlo, thi)?;
@ -658,7 +718,8 @@ impl<'a> Unroller<'a> {
return self.fail(diag::EXPECTED, at, format!("expected `in` after `for {name}`"));
}
j += 1;
let values: Option<Vec<i64>> = if self.sym(j, "[") {
// A range's values are made one at a time, as the loop reaches each.
let values: Option<Box<dyn Iterator<Item = i64>>> = if self.sym(j, "[") {
let close = self.close(j, ("[", "]"))?;
let parts = self.split(j + 1, close - 1, ":");
if !parts.iter().all(|p| self.foldable(p.0, p.1)) {
@ -678,15 +739,15 @@ impl<'a> Unroller<'a> {
if count as u64 > MAX_STEPS {
return self.fail(diag::UNROLL_LIMIT, at, format!("the loop runs {count} times, so the program unrolls past {MAX_STEPS} statements"));
}
Some((0..count).map(|k| a + k * step).collect())
Some(Box::new((0..count).map(move |k| a + k * step)))
}
} else if self.sym(j, "{") {
let close = self.close(j, ("{", "}"))?;
let parts = self.split(j + 1, close - 1, ",");
if parts.iter().all(|p| self.foldable(p.0, p.1)) {
let vs = parts.into_iter().map(|(lo, hi)| self.value(lo, hi)).collect::<R<_>>()?;
let vs: Vec<i64> = parts.into_iter().map(|(lo, hi)| self.value(lo, hi)).collect::<R<_>>()?;
j = close;
Some(vs)
Some(Box::new(vs.into_iter()))
} else {
None
}
@ -704,6 +765,9 @@ impl<'a> Unroller<'a> {
if self.iterations > MAX_STEPS {
return self.fail(diag::UNROLL_LIMIT, at, format!("the program unrolls past {MAX_STEPS} statements"));
}
if super::spend_step() {
return self.fail(diag::UNROLL_LIMIT, at, "the programs read for one request unroll past its budget of steps together (wai_quantum::qasm::within_steps)");
}
let old = self.bind(name, v, at)?;
self.seq(blo, bhi)?;
self.restore(name, old);
@ -721,6 +785,8 @@ struct IntExpr<'a> {
pos: usize,
env: &'a HashMap<String, i64>,
at: Option<&'a Token>,
/// How deep the reading stands: every level passes through `unary`.
depth: usize,
}
impl IntExpr<'_> {
@ -812,6 +878,16 @@ impl IntExpr<'_> {
}
fn unary(&mut self) -> R<i64> {
if self.depth >= MAX_NESTING {
return too_deep(self.toks.get(self.pos).or(self.at), self.src);
}
self.depth += 1;
let r = self.unary_here();
self.depth -= 1;
r
}
fn unary_here(&mut self) -> R<i64> {
if self.eat("-") {
let v = self.unary()?;
return self.checked(v.checked_neg());

View file

@ -137,6 +137,14 @@ impl Val {
/// The value as an integer, if it is one: an integer, a bool, known bits
/// (unsigned, the lowest first), or a float with no fraction.
/// How many bits a bit string holds (0 for anything else).
pub fn bits_len(&self) -> usize {
match self {
Val::Bits(b) => b.len(),
_ => 0,
}
}
pub fn integer(&self) -> Option<i128> {
match self {
Val::Int(v) => Some(*v),

View file

@ -187,7 +187,7 @@ pub fn parse(text: &str) -> Result<Circuit, FrameError> {
pub fn lower(prog: &crate::quantum_qec_text::CircuitText) -> Result<Circuit, FrameError> {
let mut c = Circuit::default();
let mut shift: Vec<f64> = Vec::new();
lower_items(&prog.items, &mut c, &mut shift)?;
lower_items(&prog.items, &mut c, &mut shift, &mut Unrolling { depth: 0, passes: 0, checking: false })?;
let carrier = c.qubits;
let mut carried = false;
for op in &mut c.ops {
@ -205,9 +205,27 @@ pub fn lower(prog: &crate::quantum_qec_text::CircuitText) -> Result<Circuit, Fra
message: format!("a correlated error is carried on a qubit past the text's last, and qubit {} leaves none", CARRIER - 1),
})?;
}
// The carrier is one qubit past the text's last: the cap the text's
// indices are held to holds the circuit's width, carrier and all.
if c.qubits > crate::quantum_qec_text::MAX_QUBITS {
return Err(FrameError::Parse {
line: first_line(&prog.items, &["E", "CORRELATED_ERROR"]).unwrap_or(0),
message: format!("{} qubits with the correlated errors' carrier, past the {} a circuit here holds", c.qubits, crate::quantum_qec_text::MAX_QUBITS),
});
}
Ok(c)
}
/// Where unrolling stands: how deep in blocks, how many passes through a
/// block's body it has made (each counted with the operations against
/// [`MAX_OPS`], so a block that adds nothing still ends), and whether it is
/// checking the body of a block repeated no times.
struct Unrolling {
depth: usize,
passes: u64,
checking: bool,
}
/// The line of the first instruction named one of `names`, in a block too.
fn first_line(items: &[crate::quantum_qec_text::Item], names: &[&str]) -> Option<usize> {
use crate::quantum_qec_text::Item;
@ -256,23 +274,38 @@ fn pauli_channel_rates(px: f64, py: f64, pz: f64) -> Option<[f64; 3]> {
Some(u.map(|x| ((1.0 - x) / 2.0).clamp(0.0, 1.0)))
}
fn lower_items(items: &[crate::quantum_qec_text::Item], c: &mut Circuit, shift: &mut Vec<f64>) -> Result<(), FrameError> {
use crate::quantum_qec_text::Item;
fn lower_items(items: &[crate::quantum_qec_text::Item], c: &mut Circuit, shift: &mut Vec<f64>, u: &mut Unrolling) -> Result<(), FrameError> {
use crate::quantum_qec_text::{Item, MAX_BLOCK_DEPTH};
for it in items {
match it {
Item::Op(i) => lower_instruction(i, c, shift)?,
Item::Repeat { count, body, line, .. } => {
let past = |m: String| FrameError::Parse { line: *line, message: m };
if u.depth >= MAX_BLOCK_DEPTH {
return Err(past(format!("blocks nested deeper than {MAX_BLOCK_DEPTH}")));
}
u.depth += 1;
// A block repeated no times is still read, so what it holds
// is refused if it would be.
if *count == 0 {
lower_items(body, &mut c.clone(), &mut shift.clone())?;
}
for _ in 0..*count {
lower_items(body, c, shift)?;
if c.ops.len() > MAX_OPS {
return Err(FrameError::Parse { line: *line, message: format!("the circuit unrolls to more than {MAX_OPS} operations") });
}
}
// is refused if it would be: once, on a copy, and a block
// inside one being checked is read once on that copy too.
let r = if *count == 0 && !u.checking {
u.checking = true;
let r = lower_items(body, &mut c.clone(), &mut shift.clone(), u);
u.checking = false;
r
} else {
let times = if u.checking { 1 } else { *count };
(0..times).try_for_each(|_| {
u.passes += 1;
lower_items(body, c, shift, u)?;
if c.ops.len() as u64 + u.passes > MAX_OPS as u64 {
return Err(past(format!("the circuit unrolls to more than {MAX_OPS} operations and passes through its blocks")));
}
Ok(())
})
};
u.depth -= 1;
r?;
}
}
}
@ -1620,14 +1653,25 @@ mod tests {
}
}
/// A text whose qubits reach 4294967294 leaves the carrier no index: a
/// circuit with a correlated error is refused at the error's line.
/// A text whose qubits reach 4294967294 is refused where it names one, as
/// past what a circuit here holds. One that names the last qubit a circuit
/// holds leaves its correlated errors' carrier no room within the cap: the
/// circuit is refused at the error's line, as the carrier would make it a
/// qubit wider than its text may name.
#[test]
fn a_correlated_error_with_no_index_left_for_its_carrier_is_refused() {
for (text, line) in [("R 0\nE(0.1) X1\nX_ERROR(0.1) 4294967294\nM 0\nDETECTOR rec[-1]\n", 2), ("R 0\nX 4294967294\nREPEAT 2 {\n CORRELATED_ERROR(0.1) Z0\n}\nM 0\n", 4)] {
let e = parse(text).map(|c| c.qubits).unwrap_err();
assert!(e.to_string().starts_with(&format!("line {line}:")) && e.to_string().contains("4294967294"), "{text}: {e}");
let last = crate::quantum_qec_text::MAX_QUBITS - 1;
for (text, line, names) in [
("R 0\nE(0.1) X1\nX_ERROR(0.1) 4294967294\nM 0\nDETECTOR rec[-1]\n".to_string(), 3, "4294967294".to_string()),
(format!("R 0\nE(0.1) X1\nX_ERROR(0.1) {last}\nM 0\nDETECTOR rec[-1]\n"), 2, (last + 2).to_string()),
(format!("R 0\nX {last}\nREPEAT 2 {{\n CORRELATED_ERROR(0.1) Z0\n}}\nM 0\n"), 4, (last + 2).to_string()),
] {
let e = parse(&text).map(|c| c.qubits).unwrap_err();
assert!(e.to_string().starts_with(&format!("line {line}:")) && e.to_string().contains(&names) && e.to_string().contains("past the"), "{text}: {e}");
}
// Without a correlated error the last qubit is the circuit's own.
assert_eq!(parse(&format!("R 0\nX_ERROR(0.1) {last}\nM 0\nDETECTOR rec[-1]\n")).unwrap().qubits, last + 1);
assert_eq!(parse(&format!("R 0\nE(0.1) X1\nX_ERROR(0.1) {}\nM 0\nDETECTOR rec[-1]\n", last - 1)).unwrap().qubits, last + 1);
}
/// The instructions this reader added are in the page that lists the text
@ -1792,4 +1836,25 @@ mod tests {
assert_eq!(m.hyperedges(), 0, "d = {d}");
}
}
/// A block repeated without end that writes no operation (`TICK`,
/// coordinates, nothing) counts its passes with the operations, so it
/// ends at the bound, refused; blocks repeated no times, nested, are
/// each checked once on one copy, not on a copy each.
#[test]
fn blocks_that_write_nothing_end_and_unrun_ones_are_checked_once() {
crate::quantum_qec_text::ends_within(120, || {
for body in ["TICK\n", "SHIFT_COORDS(1)\n", ""] {
let text = format!("H 0\nREPEAT 18446744073709551615 {{\n{body}}}\n");
let e = parse(&text).unwrap_err().to_string();
assert!(e.contains("unrolls to more than"), "{e}");
}
});
let n = crate::quantum_qec_text::MAX_BLOCK_DEPTH;
let unrun = format!("{}H 0\n{}{}M 0\n", "REPEAT 0 {\n".repeat(n), "}\n".repeat(n), "H 0\n".repeat(1000));
let c = parse(&unrun).unwrap();
assert_eq!(c.ops.len(), 1001);
// What a block repeated no times holds is still refused.
assert!(parse("REPEAT 0 {\nREPEAT 0 {\nNOT_AN_INSTRUCTION 0\n}\n}\n").is_err());
}
}

View file

@ -32,6 +32,12 @@
//! What a circuit means is [`crate::quantum_frame`]'s question: an
//! instruction this module reads but that module does not implement is
//! refused there, with its line, not skipped.
//!
//! Qubits are named below [`MAX_QUBITS`] and detectors below
//! [`MAX_DETECTORS`], each refused on its line: what is made for an index is
//! as long as the index. Blocks nest at most [`MAX_BLOCK_DEPTH`] deep; a deeper one is refused on
//! its line, so reading, unrolling and writing a text never runs out of
//! stack.
use std::fmt::Write as _;
@ -233,10 +239,26 @@ fn circuit_target(line: usize, tok: &str) -> Result<Target, TextError> {
};
let first = body.chars().next().ok_or_else(|| err(line, "an empty target"))?;
if matches!(first, 'X' | 'Y' | 'Z' | 'x' | 'y' | 'z') {
let q: u32 = number(line, &body[1..], "a qubit")?;
let q = qubit(line, number(line, &body[1..], "a qubit")?)?;
return Ok(Target::Pauli { pauli: first.to_ascii_uppercase(), q, inverted });
}
Ok(Target::Qubit { q: number(line, body, "a qubit target")?, inverted })
Ok(Target::Qubit { q: qubit(line, number(line, body, "a qubit target")?)?, inverted })
}
/// `q`, a qubit's index a reader of the text may take ([`MAX_QUBITS`]).
fn qubit(line: usize, q: u32) -> Result<u32, TextError> {
if q >= MAX_QUBITS {
return Err(err(line, format!("qubit {q} is past the {MAX_QUBITS} a circuit here holds")));
}
Ok(q)
}
/// `d`, a detector's index a reader of the text may take ([`MAX_DETECTORS`]).
fn detector(line: usize, d: u64) -> Result<u64, TextError> {
if d >= MAX_DETECTORS {
return Err(err(line, format!("detector {d} is past the {MAX_DETECTORS} a model here holds")));
}
Ok(d)
}
/// The lines of a text, numbered from 1, comments and blank lines dropped.
@ -244,15 +266,37 @@ fn lines(text: &str) -> Vec<(usize, &str)> {
text.lines().enumerate().map(|(i, l)| (i + 1, l.split('#').next().unwrap_or("").trim())).filter(|(_, l)| !l.is_empty()).collect()
}
/// How deep `REPEAT` and `repeat` blocks may nest: far past any circuit's
/// or model's own (one or two levels), and shallow enough that each reader
/// of the blocks, which recurses into them, stays well inside its stack.
pub const MAX_BLOCK_DEPTH: usize = 32;
/// The most qubits a circuit's text may name, and so the widest circuit it
/// makes. An index costs a few bytes of text and what is made for it is as
/// long as the index, so it is held where it is read, with its line.
pub const MAX_QUBITS: u32 = 1 << 24;
/// The most detectors a model's text may name, offsets counted: what is made
/// for them (their coordinates, every decoder's tables) is as long as the
/// highest index, whichever instruction names it.
pub const MAX_DETECTORS: u64 = 1 << 24;
fn too_deep(line: usize) -> TextError {
err(line, format!("blocks nested deeper than {MAX_BLOCK_DEPTH}: write the inner ones out, or fold them into one with the product of their counts"))
}
/// Read a circuit's text.
pub fn parse_circuit(text: &str) -> Result<CircuitText, TextError> {
let ls = lines(text);
let mut pos = 0;
let items = circuit_block(&ls, &mut pos, None)?;
let items = circuit_block(&ls, &mut pos, None, 0)?;
Ok(CircuitText { items })
}
fn circuit_block(ls: &[(usize, &str)], pos: &mut usize, open: Option<usize>) -> Result<Vec<Item>, TextError> {
fn circuit_block(ls: &[(usize, &str)], pos: &mut usize, open: Option<usize>, depth: usize) -> Result<Vec<Item>, TextError> {
if let (Some(line), true) = (open, depth > MAX_BLOCK_DEPTH) {
return Err(too_deep(line));
}
let mut items = Vec::new();
while *pos < ls.len() {
let (line, text) = ls[*pos];
@ -269,7 +313,7 @@ fn circuit_block(ls: &[(usize, &str)], pos: &mut usize, open: Option<usize>) ->
if !h.args.is_empty() {
return Err(err(line, "REPEAT takes no arguments"));
}
let body = circuit_block(ls, pos, Some(line))?;
let body = circuit_block(ls, pos, Some(line), depth + 1)?;
items.push(Item::Repeat { count, tag: h.tag, body, line });
continue;
}
@ -353,11 +397,14 @@ fn write_items(out: &mut String, items: &[Item], depth: usize) {
pub fn parse_dem(text: &str) -> Result<DemText, TextError> {
let ls = lines(text);
let mut pos = 0;
let items = dem_block(&ls, &mut pos, None)?;
let items = dem_block(&ls, &mut pos, None, 0)?;
Ok(DemText { items })
}
fn dem_block(ls: &[(usize, &str)], pos: &mut usize, open: Option<usize>) -> Result<Vec<DemItem>, TextError> {
fn dem_block(ls: &[(usize, &str)], pos: &mut usize, open: Option<usize>, depth: usize) -> Result<Vec<DemItem>, TextError> {
if let (Some(line), true) = (open, depth > MAX_BLOCK_DEPTH) {
return Err(too_deep(line));
}
let mut items = Vec::new();
while *pos < ls.len() {
let (line, text) = ls[*pos];
@ -372,7 +419,7 @@ fn dem_block(ls: &[(usize, &str)], pos: &mut usize, open: Option<usize>) -> Resu
match h.name {
"repeat" => {
let count = h.rest.strip_suffix('{').map(str::trim).and_then(|n| n.parse::<u64>().ok()).ok_or_else(|| err(line, "expected 'repeat <n> {'"))?;
let body = dem_block(ls, pos, Some(line))?;
let body = dem_block(ls, pos, Some(line), depth + 1)?;
items.push(DemItem::Repeat { count, tag: h.tag, body, line });
}
"error" | "detector" | "logical_observable" | "shift_detectors" => {
@ -383,7 +430,8 @@ fn dem_block(ls: &[(usize, &str)], pos: &mut usize, open: Option<usize>) -> Resu
if t == "^" {
Ok(DemTarget::Separator)
} else if let Some(k) = t.strip_prefix('D') {
number(line, k, "a detector").map(DemTarget::Detector)
let d: u32 = number(line, k, "a detector")?;
detector(line, u64::from(d)).map(|_| DemTarget::Detector(d))
} else if let Some(k) = t.strip_prefix('L') {
number(line, k, "an observable").map(DemTarget::Observable)
} else {
@ -415,7 +463,7 @@ impl DemText {
/// mechanisms with one tag are combined as independent events.
pub fn to_model(&self) -> Result<crate::quantum_frame::ErrorModel, TextError> {
let mut walk = DemWalk::default();
walk.items(&self.items)?;
walk.items(&self.items, 0)?;
Ok(walk.finish())
}
}
@ -468,12 +516,18 @@ struct DemWalk {
}
impl DemWalk {
fn items(&mut self, items: &[DemItem]) -> Result<(), TextError> {
/// The items unrolled, `depth` blocks deep. A block's every pass counts
/// as work, so one that holds nothing still ends within the bound.
fn items(&mut self, items: &[DemItem], depth: usize) -> Result<(), TextError> {
for it in items {
match it {
DemItem::Repeat { count, body, line, .. } => {
if depth >= MAX_BLOCK_DEPTH {
return Err(too_deep(*line));
}
for _ in 0..*count {
self.items(body)?;
self.work += 1;
self.items(body, depth + 1)?;
if self.work > MAX_UNROLLED {
return Err(err(*line, format!("the blocks unroll to more than {MAX_UNROLLED} instructions")));
}
@ -502,10 +556,7 @@ impl DemWalk {
let cur = parts.last_mut().expect("a part");
match *t {
DemTarget::Detector(k) => {
let d = self.offset + k as u64;
if d >= u32::MAX as u64 {
return Err(err(line, "a detector index beyond 2³²"));
}
let d = detector(line, self.offset + k as u64)?;
self.detectors = self.detectors.max(d + 1);
let d = d as u32;
match cur.0.binary_search(&d) {
@ -551,7 +602,7 @@ impl DemWalk {
for t in &i.targets {
match *t {
DemTarget::Detector(k) => {
let d = self.offset + k as u64;
let d = detector(line, self.offset + k as u64)?;
self.detectors = self.detectors.max(d + 1);
// A declaration without coordinates only names the detector.
if !i.args.is_empty() {
@ -580,7 +631,8 @@ impl DemWalk {
[DemTarget::Number(n)] => *n,
_ => return Err(err(line, "shift_detectors takes one count")),
};
self.offset += n;
// Held here, so that an offset never overflows the sum above.
self.offset = detector(line, self.offset.saturating_add(n))?;
for (j, a) in i.args.iter().enumerate() {
if self.coord_shift.len() <= j {
self.coord_shift.resize(j + 1, 0.0);
@ -617,6 +669,20 @@ impl DemWalk {
}
}
/// `work` run within `secs` seconds, or the test fails then: what a bound
/// keeps from running forever fails here, by name, if the bound goes, rather
/// than holding the job until its own limit ends it.
#[cfg(test)]
pub(crate) fn ends_within<T: Send + 'static>(secs: u64, work: impl FnOnce() -> T + Send + 'static) -> T {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::Builder::new().stack_size(8 << 20).spawn(move || tx.send(std::panic::catch_unwind(std::panic::AssertUnwindSafe(work)))).unwrap();
match rx.recv_timeout(std::time::Duration::from_secs(secs)) {
Ok(Ok(v)) => v,
Ok(Err(p)) => std::panic::resume_unwind(p),
Err(_) => panic!("not ended within {secs} s"),
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -701,4 +767,73 @@ logical_observable L1
assert!(parse_dem("error(0.1) D0 ^ ^ D1\n").unwrap().to_model().is_err());
assert!(parse_dem("error(1.5) D0\n").unwrap().to_model().is_err());
}
/// `f` on a thread of 2 MiB, a runtime worker's stack.
fn on_stack<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> T {
std::thread::Builder::new().stack_size(2 << 20).spawn(f).unwrap().join().unwrap()
}
/// Blocks nested past [`MAX_BLOCK_DEPTH`], as deep as a megabyte of
/// `REPEAT 1 {` lines makes them, are refused on the line that opens one
/// too deep; nested to the limit they read, unroll and write back.
#[test]
fn blocks_nested_past_the_limit_are_refused_on_their_line() {
let circuit = |n: usize| format!("{}H 0\n{}", "REPEAT 1 {\n".repeat(n), "}\n".repeat(n));
let model = |n: usize| format!("{}error(0.1) D0\n{}", "repeat 1 {\n".repeat(n), "}\n".repeat(n));
for n in [MAX_BLOCK_DEPTH + 1, 100_000] {
let e = on_stack(move || parse_circuit(&circuit(n)).unwrap_err());
assert_eq!(e.line, MAX_BLOCK_DEPTH + 1, "{}", e.message);
assert!(e.message.contains("nested deeper"), "{}", e.message);
let e = on_stack(move || parse_dem(&model(n)).unwrap_err());
assert_eq!(e.line, MAX_BLOCK_DEPTH + 1, "{}", e.message);
}
let c = on_stack(move || parse_circuit(&circuit(MAX_BLOCK_DEPTH)).unwrap());
assert_eq!(parse_circuit(&c.to_text()).unwrap(), c);
assert_eq!(on_stack(move || crate::quantum_frame::lower(&c).map(|c| c.ops.len())).unwrap(), 1);
let m = on_stack(move || parse_dem(&model(MAX_BLOCK_DEPTH)).unwrap().to_model().map(|m| m.mechanisms.len()));
assert_eq!(m.unwrap(), 1);
}
/// A block that holds no mechanism still counts each pass toward the
/// bound, so one repeated 2^64 − 1 times ends at once, refused.
#[test]
fn an_empty_block_repeated_without_end_is_refused_not_run() {
ends_within(60, || {
let e = parse_dem("repeat 18446744073709551615 {\n}\nerror(0.1) D0\n").unwrap().to_model().unwrap_err();
assert!(e.message.contains("unroll"), "{}", e.message);
let e = parse_dem("repeat 18446744073709551615 {\nrepeat 18446744073709551615 {\n}\n}\n").unwrap().to_model().unwrap_err();
assert!(e.message.contains("unroll"), "{}", e.message);
});
}
/// An index in a few bytes of text would make what is as long as the
/// index: every way of naming one is refused where it is read, with its
/// line, before anything is made. The highest that reads still makes its
/// model, and the model counts it exactly.
#[test]
fn an_index_past_what_a_text_holds_is_refused_on_its_line() {
let past = MAX_DETECTORS;
let d = past - 1;
for model in [
format!("shift_detectors {past}\ndetector(1) D0\n"),
format!("shift_detectors {past}\ndetector D0\n"),
format!("shift_detectors 100000000000000\ndetector(1) D0\n"),
format!("shift_detectors 18446744073709551615\nshift_detectors 18446744073709551615\ndetector D0\n"),
format!("shift_detectors {d}\nerror(0.1) D1\n"),
format!("shift_detectors {d}\nshift_detectors 1\n"),
] {
let e = parse_dem(&model).unwrap().to_model().unwrap_err();
assert!(e.message.contains("past the"), "{model}: {}", e.message);
assert!(e.line > 0, "{model}: {}", e.message);
}
for model in ["detector(1) D4294967294\n", "detector D4294967294\n", "error(0.1) D4294967294\n"] {
assert!(parse_dem(model).unwrap_err().message.contains("past the"), "{model}");
}
let m = parse_dem(&format!("shift_detectors {d}\ndetector D0\n")).unwrap().to_model().unwrap();
assert_eq!(m.detectors as u64, MAX_DETECTORS);
for circuit in ["M 4000000000\n".to_string(), format!("H {MAX_QUBITS}\n"), format!("MPP X{MAX_QUBITS}\n")] {
assert!(parse_circuit(&circuit).unwrap_err().message.contains("past the"), "{circuit}");
}
assert!(parse_circuit(&format!("H {}\n", MAX_QUBITS - 1)).is_ok());
}
}

View file

@ -71,8 +71,9 @@ fn read(src: String) -> Result<wai_quantum::qasm::Reading, wai_quantum::qasm::di
#[test]
fn a_nest_of_loops_that_each_end_early_holds_one_value_a_level() {
// The widest range the reader runs, each level's; a deep nest of the same.
for depth in [1, 2, 8, 100] {
// The widest range the reader runs, each level's; a deep nest of the same, as deep as
// the reader nests loops (`parse::MAX_NESTING` less the levels around a statement).
for depth in [1, 2, 8, wai_quantum::qasm::parse::MAX_NESTING - 8] {
let src = nest(depth, 19_999_999);
let bytes = src.len();
let before = HELD.load(Relaxed);

View file

@ -1,18 +1,19 @@
//! What a program within the reader's bounds asks of its stack: every program inside the
//! nesting bound (Q130, counted through calls) and the call bound (Q209) reads on a 1 MiB
//! thread, natively, and on wasm32, whose stack is fixed when the reader is linked. Each
//! is at the bound in its own way: a shape nested 128 deep, a chain of calls whose bodies
//! read in place reach 128, a ladder of 1024 `else if` arms, and the same in a body of the
//! deepest chain.
//! nesting bound (Q006: `qasm::parse::MAX_NESTING` levels in the text, 256 through the
//! bodies calls read) and the call bound (Q209) reads on a 1 MiB thread, natively, and on
//! wasm32, whose stack is fixed when the reader is linked; and every program past them is
//! refused (Q006) on the same stacks, never overflowing them. Each is at the bound in its
//! own way: a shape nested to just inside the text's bound, a chain of calls whose bodies
//! read in place, a ladder of 1024 `else if` arms and a run of thousands of one operator
//! (both flat, at any length), and the same nested thousands deep.
#![cfg(feature = "quantum_qasm")]
/// Programs at the bounds, each read and its verdict on Q130 known: (what, the program,
/// whether it reads).
/// Programs at and past the bounds, each read and its verdict known: (what, the program,
/// whether it reads; one that does not is refused for its nesting, Q006).
fn at_the_bounds() -> Vec<(String, String, bool)> {
let head = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\n";
let mut out = Vec::new();
let nested = |shape: &str| -> String {
let k = 126;
let nested = |shape: &str, k: usize| -> String {
match shape {
"parens" => format!("{head}int x = {}1{};\n", "(".repeat(k), ")".repeat(k)),
"unary" => format!("{head}int x = {}1;\n", "-".repeat(k)),
@ -26,8 +27,14 @@ fn at_the_bounds() -> Vec<(String, String, bool)> {
other => panic!("no shape {other}"),
}
};
for shape in ["parens", "unary", "sum", "product", "blocks", "ifs", "runtime ifs", "fors", "angle"] {
out.push((shape.to_string(), nested(shape), true));
let inside = wai_quantum::qasm::parse::MAX_NESTING - 8;
for shape in ["parens", "unary", "blocks", "ifs", "runtime ifs", "fors", "angle"] {
out.push((format!("{shape}, {inside} deep"), nested(shape, inside), true));
out.push((format!("{shape}, 5000 deep"), nested(shape, 5000), false));
}
// A run of one operator is one level, however long.
for shape in ["sum", "product"] {
out.push((format!("{shape} of 5000 terms"), nested(shape, 5000), true));
}
let chain = |kind: &str, d: usize, k: usize, inner: &str| -> String {
let mut s = String::from("OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit q;\n");
@ -40,11 +47,13 @@ fn at_the_bounds() -> Vec<(String, String, bool)> {
s
};
for kind in ["gates", "defs"] {
// 25 bodies of 4 loops: 128 through the calls; 32 calls of 2: the most calls nested.
// 25 bodies of 4 loops, and 31 of 2, the most calls nested, read; 31 of 8 pass
// the bound through the calls.
out.push((format!("{kind}, 25 of 4 loops"), chain(kind, 25, 4, "x a;"), true));
out.push((format!("{kind}, 31 of 2 loops"), chain(kind, 31, 2, "x a;"), true));
// The innermost body's own expression as deep as is left: 31 × 3 + 3 + 32.
out.push((format!("{kind}, 31 of 2 loops and a deep angle"), chain(kind, 31, 2, &format!("rz({}0.5{}) a;", "(".repeat(32), ")".repeat(32))), true));
out.push((format!("{kind}, 31 of 8 loops"), chain(kind, 31, 8, "x a;"), false));
}
let ladder = |runtime: bool| {
let s = if runtime { "c[0] = measure q[0];\nif (c[0]) x q;\n".to_string() } else { "int s = 3;\nif (s == 0) x q;\n".to_string() };
@ -84,6 +93,7 @@ fn every_program_inside_the_bounds_reads_on_a_small_stack() {
eprintln!("reading {what} on {} KiB", stack >> 10);
match read_on(src.clone(), stack) {
Ok(Ok(())) if reads => {}
Ok(Err(d)) if !reads && d.contains("[Q006]") => {}
Ok(r) => wrong.push(format!("{what} on {} KiB: {r:?}", stack >> 10)),
Err(()) => wrong.push(format!("{what} on {} KiB: the reading did not come back", stack >> 10)),
}

View file

@ -20,7 +20,7 @@ cut at the lead's piece starts, each piece one object:
| component | capability | source | presented as |
|---|---|---|---|
| `v` | `wai.video.int_motion` bases and a `wai.video.av1` floor for every piece | the fixture `compose-01`, a 320 × 180 test pattern at 25 frames a second | the picture, and the clock |
| `v` | `wai.video.int_motion` bases and a `wai.video.av1` floor for every piece | the fixture `compose-01`, a test pattern of 320 × 180 at 25 frames a second | the picture, and the clock |
| `g` | `wai.graphics.vector` | `vector-conformance/vectors/motion/stroke-titles.wvg1`, a title card and a credit roll at 1280 × 720 | an overlay rasterised in WebAssembly |
| `t` | `wai.text.timed` | `sources/t.vtt`, three captions | text, in the page's text layer |
| `s` | `wai.audio.score`, and a `wai.audio.flac` floor of each piece's mixdown | `sources/s.wscr`, `score-conformance/vectors/scale.wscr`'s notes up and down | samples synthesised in the tab, on Web Audio |

View file

@ -1680,4 +1680,38 @@ mod wasm_tests {
assert_eq!(body["status"], "done", "{req}: {body}");
}
}
/// A program nested thousands deep, or one that asks for more memory
/// than the reader holds, is refused with the reader's code, never a
/// trap, through every door that reads one: a run, the lint, the
/// critique, a generator written out; an error-correction circuit's
/// blocks are refused on their line, and an answer's chain of seals past
/// its depth; and the engine answers the next request as before.
#[wasm_bindgen_test]
fn a_program_nested_past_the_reader_s_limit_is_refused_not_trapped() {
let e = QuantumEngine::new(&[9u8; 32], 512).unwrap();
let deep = format!("OPENQASM 3.0;\nqubit q;\nrz({}0.5{}) q;\n", "(".repeat(5_000), ")".repeat(5_000));
let blocks = format!("OPENQASM 3.0;\nqubit q;\n{}x q;{}\n", "{ ".repeat(5_000), " }".repeat(5_000));
for src in [&deep, &blocks] {
assert!(run(&e, json!({ "qasm": src, "shots": 10 })).to_string().contains("Q006"));
assert!(engine_lint(src).contains("Q006"));
assert!(engine_critique(src).contains("Q006"));
}
let generator = format!("OPENQASM 3.0;\ninput int n;\nconst int a = {}n{};\nqubit q;\nx q;\n", "(".repeat(5_000), ")".repeat(5_000));
assert!(engine_instantiate(&generator, "{\"n\": 1}").contains("Q006"));
let circuit = format!("{}H 0\n{}", "REPEAT 1 {\n".repeat(50_000), "}\n".repeat(50_000));
assert!(e.qec("decode", &json!({ "circuit": circuit, "shots": 10 }).to_string()).contains("nested deeper"));
let mut chain = String::from("```qasm p\nOPENQASM 3.0;\nqubit[1] q;\nbit[1] c;\nh q[0];\nc = measure q;\n```\n");
for k in 0..2_000 {
chain += &format!("s{k} = @Seal([s{}])\n", k + 1);
}
chain += "s2000 = @Sample(p, 10)\n";
assert!(e.answer_value(&chain, "s0", true).contains("more than 32 statements"));
// A declaration that asks for more than the page holds is refused, not a trap.
let huge = "OPENQASM 3.0;\nqubit q;\narray[int[8], 4294967295] a;\nx q;\n";
assert!(run(&e, json!({ "qasm": huge, "shots": 10 })).to_string().contains("Q130"));
assert!(engine_critique(huge).contains("Q130"));
let body = run(&e, json!({ "qasm": "OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];", "shots": 20 }));
assert_eq!(body["status"], "done", "{body}");
}
}