diff --git a/.github/workflows/wai-web-wasm.yml b/.github/workflows/wai-web-wasm.yml index dfe99c844..067adf0a0 100644 --- a/.github/workflows/wai-web-wasm.yml +++ b/.github/workflows/wai-web-wasm.yml @@ -161,8 +161,10 @@ jobs: # both quantum corpora to their manifests' hashes and capability and refuses # every reject file with its listed class (quantum::tests), and the quantum IDE # builds its catalog circuits in its gateset to the same manifests, stores no - # parameter on a gate other than P, and shows no hash for an invalid circuit - # (quantum_toolchain::tests). The OTT demo seals a figure the page supplies as + # parameter on a gate other than P, and shows no hash for an invalid circuit; + # its import reads a program as the job service does and refuses one that + # measures and then acts, its replies stay valid JSON whatever an error says, + # and it signs with the page's own key once given one (quantum_toolchain::tests). The OTT demo seals a figure the page supplies as # an estimate, never a reading (video_ott::tests). The count check turns a test that # stopped being compiled into a failure instead of a silent "0 passed". - name: wai-web native unit tests @@ -171,7 +173,7 @@ jobs: run: | set -euo pipefail cargo test --locked --lib 2>&1 | tee "$RUNNER_TEMP/wai-web-native.log" - grep -q "test result: ok. 32 passed; 0 failed" "$RUNNER_TEMP/wai-web-native.log" + grep -q "test result: ok. 35 passed; 0 failed" "$RUNNER_TEMP/wai-web-native.log" - name: wasm32 int_synth decode equals the frozen native corpus shell: bash working-directory: wai/wai-web diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 2b36338ac..371ccabcf 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -6,6 +6,34 @@ wire format may change and such changes are recorded here. ## v0.4.0 — current +### wai-quantum 0.6.10: one OpenQASM reader for the service, the command line and the browser IDE + +Additive, apart from the old line reader's refusals below. Every pinned digest is unchanged. + +- **`wai_quantum::qasm`** (feature `quantum_qasm`) is the job service's OpenQASM reader, + moved into the library unchanged. The service, `wai-quantum run --qasm` and the browser + IDE's import now read a program the same way. Before, the IDE read two registers as + one, skipped a gate on a whole register, and read past what it did not know. +- **`qasm::dyadic_circuit`** gives the exact state vector's circuit for a program whose + measurements all come at the end, and refuses one that measures and then acts, resets + after a gate or uses `if`, with the reason. +- **`quantum_toolchain::from_qasm`**, the older line reader, refuses what it cannot take + instead of dropping the line: more than one register, `reset`, gate definitions, + `if`/`while`/`for`, a gate after a measurement, two statements on one line. Each + refusal names `wai_quantum::qasm`. +- **The IDE binding** (`wai-web`) builds its JSON with `serde_json`, so a quote or a + control character in a program or an error no longer breaks a reply. + `set_signing_key`, `public_key_hex`, `signer_did` and `key_is_demo` let a page sign + receipts with a key of its own; without one it signs with the published demo key and + says so. +- **The command line** signs with `--key FILE`, `$WAI_QUANTUM_KEY`, or this user's key at + `$XDG_CONFIG_HOME/wai/quantum-key` (or `~/.config/wai/quantum-key`). That key is made + from the operating system's random source on first use, with mode 0600. + `--demo-key` keeps the published key, with a note that its receipts show a result is + unaltered, not who signed it. +- **A receipt from the command line or the IDE names its signer by its key's `did:key`** + (`wai::signer_id::did_key`), where it used to carry a label naming no key. + ### wai-quantum 0.6.9: cut-rank amplitudes with the circuit's own global phase Additive. Every pinned digest is unchanged. diff --git a/wai/tools/quantum-meter/Cargo.lock b/wai/tools/quantum-meter/Cargo.lock index 985e84317..75358a064 100644 --- a/wai/tools/quantum-meter/Cargo.lock +++ b/wai/tools/quantum-meter/Cargo.lock @@ -1017,7 +1017,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wai-quantum" -version = "0.6.9" +version = "0.6.10" dependencies = [ "blake3", "ed25519-dalek", diff --git a/wai/tools/quantum-wgsl-check/Cargo.lock b/wai/tools/quantum-wgsl-check/Cargo.lock index f4b0bcb14..07f8b0b28 100644 --- a/wai/tools/quantum-wgsl-check/Cargo.lock +++ b/wai/tools/quantum-wgsl-check/Cargo.lock @@ -599,7 +599,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wai-quantum" -version = "0.6.9" +version = "0.6.10" dependencies = [ "blake3", "ed25519-dalek", diff --git a/wai/wai-pack/Cargo.lock b/wai/wai-pack/Cargo.lock index 0572fe23e..0095c1963 100644 --- a/wai/wai-pack/Cargo.lock +++ b/wai/wai-pack/Cargo.lock @@ -713,7 +713,7 @@ dependencies = [ [[package]] name = "wai-quantum" -version = "0.6.9" +version = "0.6.10" dependencies = [ "blake3", "ed25519-dalek", diff --git a/wai/wai-quantum-qpu/Cargo.lock b/wai/wai-quantum-qpu/Cargo.lock index 0b9cf7c2a..c83505a33 100644 --- a/wai/wai-quantum-qpu/Cargo.lock +++ b/wai/wai-quantum-qpu/Cargo.lock @@ -422,7 +422,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wai-quantum" -version = "0.6.9" +version = "0.6.10" dependencies = [ "blake3", "ed25519-dalek", diff --git a/wai/wai-quantum-serve/Cargo.lock b/wai/wai-quantum-serve/Cargo.lock index f9a3beec5..5faa436cf 100644 --- a/wai/wai-quantum-serve/Cargo.lock +++ b/wai/wai-quantum-serve/Cargo.lock @@ -599,7 +599,7 @@ dependencies = [ [[package]] name = "wai-quantum" -version = "0.6.9" +version = "0.6.10" dependencies = [ "blake3", "ed25519-dalek", diff --git a/wai/wai-quantum-service/Cargo.lock b/wai/wai-quantum-service/Cargo.lock index 50058c247..ef64ec4ee 100644 --- a/wai/wai-quantum-service/Cargo.lock +++ b/wai/wai-quantum-service/Cargo.lock @@ -857,7 +857,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wai-quantum" -version = "0.6.9" +version = "0.6.10" dependencies = [ "blake3", "ed25519-dalek", diff --git a/wai/wai-quantum-service/Cargo.toml b/wai/wai-quantum-service/Cargo.toml index 18b304ea2..14a92fb65 100644 --- a/wai/wai-quantum-service/Cargo.toml +++ b/wai/wai-quantum-service/Cargo.toml @@ -17,7 +17,7 @@ serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["float_roundtrip"] } blake3 = "1" getrandom = "0.4" -wai-quantum = { path = "../wai-quantum", default-features = false, features = ["quantum", "quantum_toolchain", "quantum_receipt", "quantum_sv", "quantum_tn", "quantum_tableau", "quantum_cutrank", "quantum_mpsim", "quantum_spd", "quantum_noisy", "quantum_sv_metal"] } +wai-quantum = { path = "../wai-quantum", default-features = false, features = ["quantum", "quantum_toolchain", "quantum_receipt", "quantum_sv", "quantum_tn", "quantum_tableau", "quantum_cutrank", "quantum_qasm", "quantum_mpsim", "quantum_spd", "quantum_noisy", "quantum_sv_metal"] } # proc_pid_rusage and waitid, for each job's energy (src/energy.rs). [target.'cfg(target_os = "macos")'.dependencies] diff --git a/wai/wai-quantum-service/src/main.rs b/wai/wai-quantum-service/src/main.rs index 42608b028..ff66b6b42 100644 --- a/wai/wai-quantum-service/src/main.rs +++ b/wai/wai-quantum-service/src/main.rs @@ -20,7 +20,7 @@ mod energy; mod http; mod jobs; mod mcp; -mod qasm; +use wai_quantum::qasm; mod receipt; mod run; diff --git a/wai/wai-quantum/Cargo.lock b/wai/wai-quantum/Cargo.lock index 47a7b5bcb..fe3c47e0c 100644 --- a/wai/wai-quantum/Cargo.lock +++ b/wai/wai-quantum/Cargo.lock @@ -1079,7 +1079,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wai-quantum" -version = "0.6.9" +version = "0.6.10" dependencies = [ "blake3", "block2", diff --git a/wai/wai-quantum/Cargo.toml b/wai/wai-quantum/Cargo.toml index a86503a1b..c9cd4a6b9 100644 --- a/wai/wai-quantum/Cargo.toml +++ b/wai/wai-quantum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wai-quantum" -version = "0.6.9" +version = "0.6.10" edition = "2024" rust-version = "1.97" license = "Apache-2.0" @@ -80,6 +80,9 @@ quantum_tableau = [] # Exact amplitudes, probabilities and samples of a Clifford circuit measured in a # product basis, at a cost set by its cut-rank (src/quantum_cutrank.rs). quantum_cutrank = ["quantum_tableau", "quantum_sv"] +# OpenQASM 3 (and the OpenQASM 2 forms in common use) read to one program and +# lowered to each method's input (src/qasm.rs). +quantum_qasm = ["quantum", "quantum_sv", "quantum_spd", "quantum_tableau"] quantum_mpsim = ["quantum_sv", "quantum_mps"] quantum_pauli = ["quantum"] # Sparse Pauli dynamics at utility scale: arbitrary-angle Pauli rotations on up @@ -206,7 +209,7 @@ quantum_phasor_meter = ["_signing"] # Everything. full = [ - "quantum", "quantum_toolchain", "quantum_mps", "quantum_stabilizer", "quantum_tableau", "quantum_cutrank", "quantum_mpsim", "quantum_pauli", "quantum_spd", "quantum_spd_receipt", "quantum_bptn", "quantum_resource", "quantum_resource_receipt", "quantum_frame", "quantum_tn", "quantum_gbs", "quantum_sv", "quantum_arch", "quantum_ldpc", "quantum_qio", "quantum_uf", "quantum_noisy", "quantum_dmrg", "quantum_tdvp", "quantum_chem", "quantum_nqs", "quantum_qir", "quantum_info", "quantum_qudit", "quantum_comm", "quantum_source", "quantum_channel", + "quantum", "quantum_toolchain", "quantum_mps", "quantum_stabilizer", "quantum_tableau", "quantum_cutrank", "quantum_qasm", "quantum_mpsim", "quantum_pauli", "quantum_spd", "quantum_spd_receipt", "quantum_bptn", "quantum_resource", "quantum_resource_receipt", "quantum_frame", "quantum_tn", "quantum_gbs", "quantum_sv", "quantum_arch", "quantum_ldpc", "quantum_qio", "quantum_uf", "quantum_noisy", "quantum_dmrg", "quantum_tdvp", "quantum_chem", "quantum_nqs", "quantum_qir", "quantum_info", "quantum_qudit", "quantum_comm", "quantum_source", "quantum_channel", "quantum_receipt", "quantum_ops", "quantum_cal", "quantum_mitigate", "quantum_qec", "quantum_control", "quantum_noise", "quantum_compile", "quantum_atom", "quantum_qbom", "quantum_fabric", "quantum_vml", "quantum_phasor", "quantum_qfhrr", "quantum_kernel", "quantum_qdata", diff --git a/wai/wai-quantum/README.md b/wai/wai-quantum/README.md index dfba3c2ed..035bd2824 100644 --- a/wai/wai-quantum/README.md +++ b/wai/wai-quantum/README.md @@ -67,6 +67,7 @@ never creates information. | `quantum` | byte-exact statevector simulation (dyadic Clifford+T+P(k)), as `wai.quantum.circuit2` by default or as the frozen `wai.quantum.circuit` | | `quantum_toolchain` | algorithm library, backend recommendation, **OpenQASM 2/3 ingest of what the mainstream toolchains actually export** — both register and both measurement spellings, `rx`/`ry`/`rz`/`sx`/`u`/`u2`/`u3`/`crz`, and exact rational-multiple-of-pi angles (non-dyadic angles are refused, never rounded); **device-ready OpenQASM 2/3** (`device::device_qasm`): every gate lowered exactly to its dialect's standard library, multi-controlled gates included, every qubit measured into `c[i]`, and the service's counts read back onto the crate's basis; **native programs** (`native::compile`): a processor's own basis (`rz`/`sx`/`x`/`cz`; `prx`/`cz`; or `gpi`/`gpi2`/`zz` with angles in turns, written exactly; the Z rotations carried in the phases of the last two) on its own qubits, routed over its coupling map, and checked against the circuit by simulation before it is sent — Bloch / entanglement / purity analysis | +| `quantum_qasm` | **the OpenQASM reader the job service runs**, for every surface that takes a program: OpenQASM 3 and the OpenQASM 2 forms in common use, every register, a gate on a whole register, gate definitions with parameters, `ctrl`/`negctrl` modifiers, angle expressions, comments, and measurement in every spelling. A program that measures and then acts, resets after a gate or uses `if` is kept as dynamic steps; loops, `inv`, `pow` and anything else not listed are refused with their line. Lowered to each method's input exactly up to a global phase. `qasm::dyadic_circuit` gives the exact state vector's circuit (dyadic angles) for a program whose measurements all come at the end, and refuses any other with the reason | | `quantum_stabilizer` | stabilizer (CHP) tableau — `O(n²)`, scales far past statevector | | `quantum_mps` | tensor-network (matrix-product-state) backend | | `quantum_pauli` | sparse-Pauli / Heisenberg observable propagation over the dyadic gate set (up to 64 qubits) | diff --git a/wai/wai-quantum/src/lib.rs b/wai/wai-quantum/src/lib.rs index 876f07274..f22defbd7 100644 --- a/wai/wai-quantum/src/lib.rs +++ b/wai/wai-quantum/src/lib.rs @@ -47,6 +47,11 @@ mod linalg; #[cfg(any(feature = "quantum_tn", feature = "quantum_sv"))] pub mod gates; +// OpenQASM 3 (and the OpenQASM 2 forms in common use): one front end for every +// surface, lowered to each method's input. +#[cfg(feature = "quantum_qasm")] +pub mod qasm; + #[cfg(any( feature = "quantum_spd", feature = "quantum_pauli", diff --git a/wai/wai-quantum-service/src/qasm.rs b/wai/wai-quantum/src/qasm.rs similarity index 98% rename from wai/wai-quantum-service/src/qasm.rs rename to wai/wai-quantum/src/qasm.rs index b79f1730c..37bfe5a3f 100644 --- a/wai/wai-quantum-service/src/qasm.rs +++ b/wai/wai-quantum/src/qasm.rs @@ -16,7 +16,7 @@ //! OpenQASM 2's `if(c==n) gate;`. A program that measures a qubit and then //! acts on it, resets one after its first gate, or uses `if` is dynamic: it //! is kept as steps ([`Instr`]) as well as gates, and runs by -//! [`crate::dynamic`]. Every other program is exactly what it was before. +//! the service's `dynamic` module. Every other program is exactly what it was before. //! //! Control modifiers, `ctrl @`, `ctrl(n) @`, `negctrl @` and `negctrl(n) @`, //! on `x y z h s sdg t tdg p` and on the controlled names `cx cy cz ch cp ccx @@ -35,10 +35,10 @@ //! lowering to the others. use std::collections::HashMap; -use wai_quantum::gates::{C, Gate as Matrix, complex}; -use wai_quantum::quantum::{BaseGate, Circuit as Dyadic, Gate as DyadicGate}; -use wai_quantum::quantum_spd::{Op as Rot, Pauli, RotCircuit}; -use wai_quantum::quantum_tableau::Clifford; +use crate::gates::{C, Gate as Matrix, complex}; +use crate::quantum::{BaseGate, Circuit as Dyadic, Gate as DyadicGate}; +use crate::quantum_spd::{Op as Rot, Pauli, RotCircuit}; +use crate::quantum_tableau::Clifford; /// The most qubits a program declares. pub const MAX_QUBITS: u32 = 65_536; @@ -1119,6 +1119,20 @@ impl Parser { } /// Parses an OpenQASM program. +/// A program whose measurements all come at the end, as the dyadic circuit: +/// the reader of the bit-exact state vector, the in-browser IDE and the CLI. +/// Every register, broadcast, gate definition and modifier [`parse`] reads is +/// read the same way here. A program that measures a qubit and then acts on +/// it, resets a qubit after its first gate, or acts on measured bits is +/// refused, never rewritten: its meaning needs the measurements. +pub fn dyadic_circuit(src: &str) -> Result { + let p = parse(src)?; + if p.dynamic.is_some() { + return Err("this program measures a qubit and then acts on it, resets a qubit after its first gate, or acts on measured bits; the exact state vector here takes programs whose measurements all come at the end".into()); + } + p.dyadic() +} + pub fn parse(src: &str) -> Result { let toks = lex(src)?; let mut p = Parser { @@ -1292,7 +1306,7 @@ fn lowering_phase(op: &Op, gates: &[Clifford]) -> C { let want = op.matrix().matrix; let mut best = (0.0f64, complex(1.0, 0.0)); for col in 0..d { - let mut sv = wai_quantum::quantum_sv::StateVector::zero(k).expect("at most three qubits"); + let mut sv = crate::quantum_sv::StateVector::zero(k).expect("at most three qubits"); sv.re[0] = 0.0; sv.re[col] = 1.0; for g in &local_gates { @@ -2119,8 +2133,8 @@ mod tests { Program { n_qubits: n, ops, measured: (0..n).map(|q| (q, q)).collect(), n_bits: n, dynamic: None } } - fn dense(p: &Program) -> wai_quantum::quantum_sv::StateVector { - let mut sv = wai_quantum::quantum_sv::StateVector::zero(p.n_qubits).unwrap(); + fn dense(p: &Program) -> crate::quantum_sv::StateVector { + let mut sv = crate::quantum_sv::StateVector::zero(p.n_qubits).unwrap(); sv.run(&p.matrices(), 1).unwrap(); sv } @@ -2276,7 +2290,7 @@ mod tests { #[test] fn the_clifford_lowering_gives_the_matrices_expectation_values() { - use wai_quantum::quantum_tableau::Tableau; + use crate::quantum_tableau::Tableau; for seed in 0..12 { let p = clifford_kinds(5, seed); let sv = dense(&p); @@ -2304,7 +2318,7 @@ mod tests { for seed in 0..6 { let p = every_kind(4, seed + 300); let want = dense(&p); - let mut got = wai_quantum::quantum_sv::StateVector::zero(p.n_qubits).unwrap(); + let mut got = crate::quantum_sv::StateVector::zero(p.n_qubits).unwrap(); let local = p.local_matrices(); assert!(local.iter().all(|g| g.qubits.len() <= 2)); got.run(&local, 1).unwrap(); @@ -2319,7 +2333,7 @@ mod tests { #[test] fn the_rotation_lowering_gives_the_matrices_expectation_values() { - use wai_quantum::quantum_spd::{SpdConfig, expectation}; + use crate::quantum_spd::{SpdConfig, expectation}; for seed in 0..6 { let p = every_kind(4, seed + 100); let sv = dense(&p); diff --git a/wai/wai-quantum/src/quantum_toolchain.rs b/wai/wai-quantum/src/quantum_toolchain.rs index 978c8021a..6064818ff 100644 --- a/wai/wai-quantum/src/quantum_toolchain.rs +++ b/wai/wai-quantum/src/quantum_toolchain.rs @@ -674,7 +674,15 @@ pub fn to_qasm(c: &Circuit) -> String { /// if its gateset is then changed. pub fn from_qasm(src: &str) -> Result { let mut n: Option = None; + let mut registers = 0; // first pass: qubit count + // Registers counted statement by statement, however lines are broken. + for stmt in src.lines().map(|l| l.split("//").next().unwrap_or("")).flat_map(|l| l.split(';')) { + let stmt = stmt.trim(); + if stmt.starts_with("qubit") || stmt.starts_with("qreg ") { + registers += 1; + } + } for raw in src.lines() { let line = strip(raw); if let Some(rest) = line.strip_prefix("qubit[") @@ -688,20 +696,42 @@ pub fn from_qasm(src: &str) -> Result { } } let n = n.ok_or("no qubit register declaration (qubit[n] q; or qreg q[n];)")?; + // What this reader does not take is refused, never dropped: the program + // would mean something else. `wai_quantum::qasm` reads all of it. + let fuller = "; the full reader (wai_quantum::qasm, the IDE's and the service's) takes it"; + if registers > 1 { + return Err(format!("{registers} qubit registers; this reader takes one{fuller}")); + } let mut c = Circuit::new(n); + let mut measured = false; for raw in src.lines() { let line = strip(raw); if line.is_empty() { continue; } + if line.contains(';') && !line.starts_with("OPENQASM") && !line.starts_with("include") { + return Err(format!("several statements on one line (`{line}`); this reader takes one a line{fuller}")); + } + let first: String = line.chars().take_while(|c| c.is_ascii_alphanumeric() || *c == '_').collect(); + match first.as_str() { + "reset" => return Err(format!("`reset` changes the state; this reader does not take it{fuller}")), + "gate" => return Err(format!("gate definitions are not read here{fuller}")), + "if" | "while" | "for" => return Err(format!("`{first}` acts on measured bits; this reader does not take it{fuller}")), + _ => {} + } // Measurement is recorded, not simulated here, and the exporters disagree // about how to spell it: OpenQASM 2 writes `measure q[0] -> c[0];` while // Qiskit's OpenQASM 3 exporter writes `c[0] = measure q[0];`, whose first // token is the *classical* register. Skip either shape before parsing a // gate name out of the line. if line.contains("measure") { + measured = true; continue; } + let gate_line = !matches!(first.as_str(), "OPENQASM" | "include" | "qubit" | "qreg" | "creg" | "bit" | "barrier"); + if measured && gate_line { + return Err(format!("a gate after a measurement (`{line}`): this reader takes measurements only at the end{fuller}")); + } // Gate modifiers: `ctrl @`, `ctrl(n) @`, `negctrl @`, `negctrl(n) @`. if let Some((polarity, call)) = modifiers(&line)? { push_modified(&mut c, &polarity, &call)?; @@ -1322,6 +1352,24 @@ mod tests { assert_eq!(recommend_backend(&Circuit::qft(6)).0, Backend::Statevector); } + /// What this reader cannot take is refused, never dropped. + #[test] + fn what_the_reader_cannot_take_is_refused() { + for src in [ + "OPENQASM 3.0;\nqubit[1] a;\nqubit[1] b;\nx a[0];\nx b[0];\n", + "OPENQASM 3.0;\nqubit[1] q;\nx q[0];\nreset q[0];\n", + "OPENQASM 3.0;\nqubit[1] q;\nbit[1] c;\nh q[0];\nc[0] = measure q[0];\nh q[0];\n", + "OPENQASM 3.0;\ngate flip a { x a; }\nqubit[1] q;\nflip q[0];\n", + "OPENQASM 3.0;\nqubit[2] q;\nh q[0]; cx q[0], q[1];\n", + ] { + let e = from_qasm(src).unwrap_err(); + assert!(e.contains("wai_quantum::qasm"), "{src}: {e}"); + } + // Measurements at the end are still read (and dropped, as they change nothing). + let c = from_qasm("OPENQASM 3.0;\nqubit[1] q;\nbit[1] c;\nx q[0];\nc[0] = measure q[0];\n").unwrap(); + assert_eq!(c.ops.len(), 1); + } + #[test] fn qasm_round_trip() { // Grover's search past three qubits uses a Z with three or more diff --git a/wai/wai-rs/Cargo.lock b/wai/wai-rs/Cargo.lock index 890a68780..4d07fe7e5 100644 --- a/wai/wai-rs/Cargo.lock +++ b/wai/wai-rs/Cargo.lock @@ -4280,6 +4280,7 @@ dependencies = [ "dav1d", "ed25519-dalek", "flac-bound", + "getrandom 0.2.17", "image", "jpeg-encoder", "jpegxl-rs", @@ -4310,7 +4311,7 @@ version = "0.1.0" [[package]] name = "wai-quantum" -version = "0.6.9" +version = "0.6.10" dependencies = [ "blake3", "ed25519-dalek", diff --git a/wai/wai-rs/Cargo.toml b/wai/wai-rs/Cargo.toml index 4f7b8b82a..cc0a07abf 100644 --- a/wai/wai-rs/Cargo.toml +++ b/wai/wai-rs/Cargo.toml @@ -90,6 +90,8 @@ c2pa = { version = "0.90", optional = true, default-features = false, features = # deterministic fixed-point simulation, replay-equivalence conformance. blake3 = { version = "1.8", optional = true } # state hashes, node minting, op/seal commitments; `hazmat` (1.8) for the HTTP head proof ed25519-dalek = { version = "2", optional = true } # op + seal signatures +# The CLI's own signing key, made from the operating system's random source. +getrandom = { version = "0.2", optional = true } num-bigint = { version = "0.4", optional = true } # oer: exact decimal arithmetic in the composition checks # The curve ed25519-dalek is built on, named directly only by `receipt_claims`: its # conformance corpus builds the signatures jwp-receipts §3.1 step 3 must refuse (keys of @@ -177,6 +179,8 @@ semantic = ["codecs", "dep:ort", "dep:ndarray"] # the dependency on its own, so the one verifier builds exactly when ed25519-dalek does # and asks for no feature its caller does not need. Every signing feature enables it. ed25519 = ["dep:ed25519-dalek"] +# The wai-quantum CLI's per-user signing key (src/bin/wai_quantum.rs). +cli_keys = ["dep:getrandom"] # Interactive worlds (extensions/interactive-worlds): media that consumes # input. Pure-Rust, no ML dep chain — the sim is closed-form fixed-point. world = ["dep:blake3", "ed25519"] @@ -444,6 +448,8 @@ quantum_atom = ["quantum_ops", "wai-quantum/quantum_atom"] # selection (Clifford → statevector → tensor-network cascade) + OpenQASM interop, # all over the byte-exact fixed-point simulator. Framework-neutral, no cloud. quantum_toolchain = ["quantum", "wai-quantum/quantum_toolchain"] +# OpenQASM 3 read one way on every surface (wai_quantum::qasm). +quantum_qasm = ["quantum", "wai-quantum/quantum_qasm"] # Matrix Product State backend (extensions/quantum-sim § MPS, `wai.quantum.mps`): # the tensor-network "classical referee" — evolve a bounded-bond MPS gate-by-gate # with a deterministic complex Jacobi SVD truncation, reproducible on every machine @@ -617,6 +623,8 @@ name = "wai-quantum" path = "src/bin/wai_quantum.rs" required-features = [ "quantum_toolchain", + "quantum_qasm", + "cli_keys", "quantum_receipt", "quantum_qir", "quantum_channel", diff --git a/wai/wai-rs/src/bin/wai_quantum.rs b/wai/wai-rs/src/bin/wai_quantum.rs index c00f05f71..535f1574c 100644 --- a/wai/wai-rs/src/bin/wai_quantum.rs +++ b/wai/wai-rs/src/bin/wai_quantum.rs @@ -81,6 +81,7 @@ use wai::quantum_info::{chsh, DensityMatrix}; use wai::quantum_qudit::QuditState; use wai::quantum_energy::{EnergyClass, MaybeLabelled}; use wai::quantum_receipt::QuantumReceipt; +use wai::signer_id::did_key; use wai::quantum_resource::{self as qr, EstimateConfig, LogicalCounts, PhysicalQubit, QecModel}; use wai::quantum_resource_receipt::{Program, ResourceJob, ResourceReceipt}; use wai::quantum_spd_receipt::{SpdJob, SpdReceipt}; @@ -149,7 +150,7 @@ fn usage() -> ! { wai-quantum run --qir [--top K] [--json] [--gateset v1|v2] (QIR on stdin)\n \ wai-quantum qasm [-n QUBITS] [-p PARAM] [--gateset v1|v2]\n \ wai-quantum qir [-n QUBITS] [-p PARAM] [--no-measure] [--gateset v1|v2]\n \ - wai-quantum receipt [-n QUBITS] [-p PARAM] [--shots N] [--seed S] [--gateset v1|v2]\n \ + wai-quantum receipt [-n QUBITS] [-p PARAM] [--shots N] [--seed S] [--gateset v1|v2] [--key FILE | --demo-key]\n \ wai-quantum verify (receipt JSON on stdin)\n \ --gateset v2 (the default) is wai.quantum.circuit2; v1 is the frozen wai.quantum.circuit.\n\n\ INFORMATION AND TRANSPORT:\n \ @@ -223,6 +224,61 @@ fn read_stdin() -> String { } /// Pull `--flag VALUE` out of the argument list. +/// The key a receipt is signed with: +/// - `--demo-key`: the published demo key, for receipts that repeat byte for +/// byte everywhere. Anyone can sign with it, so it proves nothing about who +/// signed, and a note on stderr says so; +/// - `--key FILE`, or `$WAI_QUANTUM_KEY` naming a file: that key; +/// - otherwise this user's key, `$XDG_CONFIG_HOME/wai/quantum-key` (or +/// `~/.config/wai/quantum-key`), made from the operating system's random +/// source on first use and readable by its owner only. +/// +/// A key file holds the 32-byte Ed25519 seed as 64 hex digits. +fn signing_key(args: &[String]) -> SigningKey { + if flag(args, "--demo-key") { + eprintln!("note: signed with the published demo key; the receipt shows the result is unaltered, not who signed it"); + return SigningKey::from_bytes(&[42u8; 32]); + } + let path = opt(args, "--key", "").or_else(|| std::env::var("WAI_QUANTUM_KEY").ok()).map(std::path::PathBuf::from).unwrap_or_else(|| { + let base = std::env::var_os("XDG_CONFIG_HOME").map(std::path::PathBuf::from).or_else(|| std::env::var_os("HOME").map(|h| std::path::Path::new(&h).join(".config"))).unwrap_or_else(|| die("no HOME to keep a key in: pass --key FILE")); + base.join("wai").join("quantum-key") + }); + let parse = |text: &str| -> Option<[u8; 32]> { + let t = text.trim(); + if t.len() != 64 { + return None; + } + let mut out = [0u8; 32]; + for (i, b) in out.iter_mut().enumerate() { + *b = u8::from_str_radix(&t[2 * i..2 * i + 2], 16).ok()?; + } + Some(out) + }; + match std::fs::read_to_string(&path) { + Ok(text) => SigningKey::from_bytes(&parse(&text).unwrap_or_else(|| die(&format!("{}: a key file holds 64 hex digits", path.display())))), + Err(e) if e.kind() == std::io::ErrorKind::NotFound && opt(args, "--key", "").is_none() && std::env::var_os("WAI_QUANTUM_KEY").is_none() => { + let mut seed = [0u8; 32]; + getrandom::getrandom(&mut seed).unwrap_or_else(|e| die(&format!("no random source for a new key: {e}"))); + if let Some(dir) = path.parent() { + std::fs::create_dir_all(dir).unwrap_or_else(|e| die(&format!("{}: {e}", dir.display()))); + } + let text: String = seed.iter().map(|b| format!("{b:02x}")).collect(); + let mut f = std::fs::OpenOptions::new(); + f.write(true).create_new(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + f.mode(0o600); + } + use std::io::Write as _; + f.open(&path).and_then(|mut file| writeln!(file, "{text}")).unwrap_or_else(|e| die(&format!("{}: {e}", path.display()))); + eprintln!("note: made a signing key at {}", path.display()); + SigningKey::from_bytes(&seed) + } + Err(e) => die(&format!("{}: {e}", path.display())), + } +} + fn opt(args: &[String], long: &str, short: &str) -> Option { args.iter() .position(|a| a == long || (!short.is_empty() && a == short)) @@ -259,7 +315,7 @@ fn gateset_from(args: &[String]) -> Gateset { fn circuit_from(args: &[String], algo: Option<&String>) -> Circuit { let gs = gateset_from(args); let mut c = if flag(args, "--qasm") { - qt::from_qasm(&read_stdin()).unwrap_or_else(|e| die(&format!("QASM parse: {e}"))) + wai::qasm::dyadic_circuit(&read_stdin()).unwrap_or_else(|e| die(&format!("QASM parse: {e}"))) } else if flag(args, "--qir") { wai::quantum_qir::from_qir(&read_stdin()).unwrap_or_else(|e| die(&format!("QIR parse: {e}"))) } else { @@ -509,10 +565,8 @@ fn main() { let shots: u64 = num(args, "--shots", "", 0); let seed: u64 = num(args, "--seed", "", 0); let measure = (shots > 0).then_some(Measure { seed, shots }); - // Deterministic demo key: the receipt is reproducible byte-for-byte, so - // native and cloud-WASM runs sign identically. Supply real keys in prod. - let key = SigningKey::from_bytes(&[42u8; 32]); - let r = QuantumReceipt::seal(&key, "did:key:wai-quantum-cli", &c, measure, 0, None) + let key = signing_key(args); + let r = QuantumReceipt::seal(&key, did_key(key.verifying_key().as_bytes()), &c, measure, 0, None) .unwrap_or_else(|e| die(&format!("seal: {e:?}"))); println!("{}", r.to_json()); } @@ -633,10 +687,8 @@ fn main() { config: SpdConfig { threshold, max_weight: None }, }; if flag(args, "--receipt") { - // The same deterministic demo key as `receipt`: the run is - // bit-identical everywhere, so is its receipt. - let key = SigningKey::from_bytes(&[42u8; 32]); - let r = SpdReceipt::seal(&key, "did:key:wai-quantum-cli", &job, None, |run| { + let key = signing_key(args); + let r = SpdReceipt::seal(&key, did_key(key.verifying_key().as_bytes()), &job, None, |run| { run(); 0 }) @@ -698,10 +750,8 @@ fn main() { die("--receipt seals one estimate: pick a --qubit"); } let job = ResourceJob { program: Program::Logical(counts), qubit: qubits[0].clone(), code, config: cfg }; - // The same deterministic demo key as `receipt`: the estimate is - // bit-identical everywhere, so is its receipt. - let key = SigningKey::from_bytes(&[42u8; 32]); - let r = ResourceReceipt::seal(&key, "did:key:wai-quantum-cli", &job, None, |run| { + let key = signing_key(args); + let r = ResourceReceipt::seal(&key, did_key(key.verifying_key().as_bytes()), &job, None, |run| { run(); 0 }) diff --git a/wai/wai-rs/src/lib.rs b/wai/wai-rs/src/lib.rs index 0ddd880ed..5f8598aaf 100644 --- a/wai/wai-rs/src/lib.rs +++ b/wai/wai-rs/src/lib.rs @@ -775,6 +775,8 @@ pub use wai_quantum::quantum_atom; // byte-exact fixed-point simulator. Framework-neutral and offline. #[cfg(feature = "quantum_toolchain")] pub use wai_quantum::quantum_toolchain; +#[cfg(feature = "quantum_qasm")] +pub use wai_quantum::qasm; // Matrix Product State backend (`wai.quantum.mps`) — the tensor-network classical // referee: a bounded-bond MPS evolved with a deterministic complex Jacobi SVD diff --git a/wai/wai-web/Cargo.lock b/wai/wai-web/Cargo.lock index 2bc7cea66..526a2a58b 100644 --- a/wai/wai-web/Cargo.lock +++ b/wai/wai-web/Cargo.lock @@ -1241,7 +1241,7 @@ version = "0.1.0" [[package]] name = "wai-quantum" -version = "0.6.9" +version = "0.6.10" dependencies = [ "blake3", "ed25519-dalek", diff --git a/wai/wai-web/Cargo.toml b/wai/wai-web/Cargo.toml index 30b4bdc43..23b58c8fc 100644 --- a/wai/wai-web/Cargo.toml +++ b/wai/wai-web/Cargo.toml @@ -19,7 +19,7 @@ js-sys = "0.3" # with the codec chain off. No second implementation: replay-equivalence # in the browser is the identical Rust that generated the conformance # corpus hashes. -wai = { path = "../wai-rs", default-features = false, features = ["world", "audio_scene", "haptic", "splat4d", "provenance", "score", "film", "neural_int", "generate", "video_int", "audio_int", "world_model", "world_int", "splat_int", "quantum", "quantum_ops", "quantum_cal", "quantum_mitigate", "quantum_qec", "quantum_control", "quantum_noise", "quantum_compile", "quantum_atom", "quantum_qbom", "quantum_receipt", "quantum_toolchain", "quantum_mps", "quantum_stabilizer", "quantum_pauli", "quantum_vml", "quantum_phasor", "quantum_qfhrr", "quantum_kernel", "quantum_phasor_meter", "quantum_qdata", "video_workflow", "staged", "stage_measure", "receipt_claims"] } +wai = { path = "../wai-rs", default-features = false, features = ["world", "audio_scene", "haptic", "splat4d", "provenance", "score", "film", "neural_int", "generate", "video_int", "audio_int", "world_model", "world_int", "splat_int", "quantum", "quantum_ops", "quantum_cal", "quantum_mitigate", "quantum_qec", "quantum_control", "quantum_noise", "quantum_compile", "quantum_atom", "quantum_qbom", "quantum_receipt", "quantum_toolchain", "quantum_qasm", "quantum_mps", "quantum_stabilizer", "quantum_pauli", "quantum_vml", "quantum_phasor", "quantum_qfhrr", "quantum_kernel", "quantum_phasor_meter", "quantum_qdata", "video_workflow", "staged", "stage_measure", "receipt_claims"] } ed25519-dalek = "2" # actor keys for browser-side op authoring (same version as wai-rs) blake3 = "1" # in-page receipt check: hash a decoded image against a pinned digest # Awaits the WebGPU member's futures behind the promises of src/quantum_webgpu.rs; diff --git a/wai/wai-web/src/quantum_toolchain.rs b/wai/wai-web/src/quantum_toolchain.rs index 8756a19b5..2663ec2e8 100644 --- a/wai/wai-web/src/quantum_toolchain.rs +++ b/wai/wai-web/src/quantum_toolchain.rs @@ -14,15 +14,24 @@ use wai::quantum::{circuit_hash, BaseGate, Circuit, Gate, Gateset, Measure, Stat use wai::quantum_mps; use wai::quantum_pauli; use wai::quantum_receipt::QuantumReceipt; +use wai::signer_id::did_key; use wai::quantum_stabilizer; use wai::quantum_toolchain as tc; use wasm_bindgen::prelude::*; const SCALE: f64 = (1u64 << FRAC) as f64; -fn signer() -> SigningKey { +/// The key a receipt is signed with before the page gives the IDE its own +/// ([`QuantumIde::set_signing_key`]). It is public, so a receipt it signs +/// proves nothing about who made it; [`QuantumIde::key_is_demo`] says so. +fn demo_signer() -> SigningKey { SigningKey::from_bytes(&[0x51; 32]) } +/// `{"error": message}`, the message escaped as JSON requires. +fn error_json(message: impl std::fmt::Display) -> String { + serde_json::json!({ "error": message.to_string() }).to_string() +} + fn hex(b: &[u8]) -> String { b.iter().map(|x| format!("{x:02x}")).collect() } @@ -98,6 +107,8 @@ pub struct QuantumIde { sv: Option, /// Whether the console note has been printed for the circuit as it stands. noted: bool, + /// The key receipts are signed with: this browser's, once the page sets it. + key: Option, } #[wasm_bindgen] @@ -105,7 +116,30 @@ impl QuantumIde { /// An empty circuit on `n_qubits` (1 to 12), in circuit2. #[wasm_bindgen(constructor)] pub fn new(n_qubits: u8) -> QuantumIde { - QuantumIde { circuit: Circuit::with_gateset(n_qubits.clamp(1, 12), Gateset::V2), sv: None, noted: false } + QuantumIde { circuit: Circuit::with_gateset(n_qubits.clamp(1, 12), Gateset::V2), sv: None, noted: false, key: None } + } + + // ---- signing key ---- + /// Sign receipts with the Ed25519 key whose 32-byte seed is `seed`: the + /// page makes one at random and keeps it in this browser. Refuses any + /// other length. + pub fn set_signing_key(&mut self, seed: &[u8]) -> Result<(), String> { + let bytes: [u8; 32] = seed.try_into().map_err(|_| format!("a signing key's seed is 32 bytes, not {}", seed.len()))?; + self.key = Some(SigningKey::from_bytes(&bytes)); + Ok(()) + } + /// The public key receipts are signed with, as hex. + pub fn public_key_hex(&self) -> String { + hex(self.signer().verifying_key().as_bytes()) + } + /// The signer a receipt names: that public key as a `did:key`. + pub fn signer_did(&self) -> String { + did_key(self.signer().verifying_key().as_bytes()) + } + /// Whether receipts are still signed with the published demo key, which + /// proves nothing about who made them. + pub fn key_is_demo(&self) -> bool { + self.key.is_none() } // ---- gateset ---- @@ -180,10 +214,20 @@ impl QuantumIde { } } /// Replace the circuit from an OpenQASM source, built in the IDE's gateset. - /// Returns an error string on failure (unsupported gate, non-dyadic phase, - /// bad register, a gate that names one qubit twice). + /// The program is read as the job service reads it ([`wai::qasm`]): every + /// register, broadcast, gate definition and modifier. Returns an error + /// string on failure (an unsupported construct with its line, a + /// non-dyadic angle, more qubits than the page holds). A program that + /// measures in the middle, resets after a gate or acts on measured bits is + /// refused, never rewritten, with a pointer to the console that runs it. pub fn load_qasm(&mut self, src: &str) -> Result<(), String> { - let c = tc::from_qasm(src)?; + let c = wai::qasm::dyadic_circuit(src).map_err(|e| { + if e.starts_with("this program measures") { + format!("{e}. The console at /quantum-run/ runs it.") + } else { + e + } + })?; if c.n_qubits > 12 { return Err(format!("{} qubits exceeds the in-browser limit (12)", c.n_qubits)); } @@ -192,22 +236,13 @@ impl QuantumIde { } /// The op-log as JSON, for the editor to render. pub fn gates_json(&self) -> String { - let items: Vec = self + let items: Vec = self .circuit .ops .iter() - .map(|g| { - let ctrls: Vec = g.controls.iter().map(|c| c.to_string()).collect(); - format!( - "{{\"name\":\"{}\",\"controls\":[{}],\"target\":{},\"param\":{}}}", - name_of(g.base), - ctrls.join(","), - g.target, - g.param - ) - }) + .map(|g| serde_json::json!({ "name": name_of(g.base), "controls": g.controls, "target": g.target, "param": g.param })) .collect(); - format!("[{}]", items.join(",")) + serde_json::Value::Array(items).to_string() } // ---- run ---- @@ -314,13 +349,7 @@ impl QuantumIde { pub fn transpile_line(&self) -> String { let r = tc::route_line(&self.circuit); let eq = tc::routing_equivalent(&self.circuit, &r); - format!( - "{{\"source_2q\":{},\"nonlocal\":{},\"routed_gates\":{},\"swaps\":{},\"equivalent\":{eq}}}", - r.source_2q, - r.source_nonlocal, - r.circuit.ops.len(), - r.swaps - ) + serde_json::json!({ "source_2q": r.source_2q, "nonlocal": r.source_nonlocal, "routed_gates": r.circuit.ops.len(), "swaps": r.swaps, "equivalent": eq }).to_string() } /// Run the circuit through the tensor-network (MPS) backend at bond cap /// `chi_max` and report `{bond, retained, fidelity}` against the exact dense @@ -328,17 +357,14 @@ impl QuantumIde { pub fn run_mps(&self, chi_max: u32) -> String { let dense = match self.circuit.simulate() { Ok(d) => d, - Err(e) => return format!("{{\"error\":\"{e}\"}}"), + Err(e) => return error_json(e), }; match quantum_mps::run(&self.circuit, chi_max.max(1) as usize) { Ok((mps, r)) => { let f = quantum_mps::fidelity(&mps, &dense); - format!( - "{{\"bond\":{},\"retained\":{:.6},\"fidelity\":{:.6}}}", - r.max_bond, r.retained_weight, f - ) + serde_json::json!({ "bond": r.max_bond, "retained": r.retained_weight, "fidelity": f }).to_string() } - Err(e) => format!("{{\"error\":\"{e:?}\"}}"), + Err(e) => error_json(format!("{e:?}")), } } /// Sample the current circuit on the O(n²) stabilizer backend (if Clifford): @@ -347,9 +373,9 @@ impl QuantumIde { match quantum_stabilizer::sample(&self.circuit, seed as u64, shots.max(1) as u64) { Ok(v) => { let (o, c) = v.first().copied().unwrap_or((0, 0)); - format!("{{\"distinct\":{},\"top_index\":{o},\"top_count\":{c}}}", v.len()) + serde_json::json!({ "distinct": v.len(), "top_index": o, "top_count": c }).to_string() } - Err(e) => format!("{{\"error\":\"{}\"}}", e.0), + Err(e) => error_json(e.0), } } /// Scale demonstration: build an `n`-qubit GHZ and sample it on the stabilizer @@ -364,16 +390,16 @@ impl QuantumIde { c.cx(i - 1, i); } match quantum_stabilizer::sample(&c, seed as u64, shots.max(1) as u64) { - Ok(v) => format!("{{\"n\":{n},\"distinct\":{},\"gates\":{}}}", v.len(), c.ops.len()), - Err(e) => format!("{{\"error\":\"{}\"}}", e.0), + Ok(v) => serde_json::json!({ "n": n, "distinct": v.len(), "gates": c.ops.len() }).to_string(), + Err(e) => error_json(e.0), } } /// Compute ⟨Z_q⟩ on the current circuit via sparse-Pauli / Heisenberg /// propagation: `{value, peak}` (peak Pauli-sum size) or `{error}`. pub fn expect_z_pauli(&self, q: u8, threshold: f64) -> String { match quantum_pauli::expect_z(&self.circuit, q, threshold.max(0.0)) { - Ok((v, peak)) => format!("{{\"value\":{v:.6},\"peak\":{peak}}}"), - Err(e) => format!("{{\"error\":\"{}\"}}", e.0), + Ok((v, peak)) => serde_json::json!({ "value": v, "peak": peak }).to_string(), + Err(e) => error_json(e.0), } } /// Scale demonstration: ⟨Z_0⟩ on an `n`-qubit low-magic circuit — a single @@ -401,8 +427,8 @@ impl QuantumIde { } } match quantum_pauli::expect_z(&c, 0, threshold.max(0.0)) { - Ok((v, peak)) => format!("{{\"n\":{n},\"value\":{v:.6},\"peak\":{peak}}}"), - Err(e) => format!("{{\"error\":\"{}\"}}", e.0), + Ok((v, peak)) => serde_json::json!({ "n": n, "value": v, "peak": peak }).to_string(), + Err(e) => error_json(e.0), } } /// Seal a signed run receipt over the byte-exact reconstruction (and a shot @@ -445,10 +471,14 @@ impl QuantumIde { } } + fn signer(&self) -> SigningKey { + self.key.clone().unwrap_or_else(demo_signer) + } + fn seal(&self, seed: u64, shots: u64) -> Option { QuantumReceipt::seal( - &signer(), - "did:key:quantum-ide", + &self.signer(), + did_key(self.signer().verifying_key().as_bytes()), &self.circuit, Some(Measure { seed, shots }), 0, @@ -461,11 +491,8 @@ impl QuantumIde { /// The algorithm library catalog as JSON `[{id,name,desc}]`, for the IDE dropdown. #[wasm_bindgen] pub fn ide_catalog() -> String { - let items: Vec = tc::catalog() - .into_iter() - .map(|(id, name, desc)| format!("{{\"id\":\"{id}\",\"name\":\"{name}\",\"desc\":\"{desc}\"}}")) - .collect(); - format!("[{}]", items.join(",")) + let items: Vec = tc::catalog().into_iter().map(|(id, name, desc)| serde_json::json!({ "id": id, "name": name, "desc": desc })).collect(); + serde_json::Value::Array(items).to_string() } #[cfg(test)] @@ -600,4 +627,82 @@ mod tests { assert!(ide.set_gateset(2)); assert_eq!(ide.note(), ""); } + + fn loaded(src: &str) -> QuantumIde { + let mut ide = QuantumIde::new(2); + ide.load_qasm(src).unwrap_or_else(|e| panic!("{src}: {e}")); + ide.run().unwrap(); + ide + } + + /// The outcome with probability 1, or None. + fn certain(ide: &QuantumIde) -> Option { + let p = ide.probabilities(); + p.iter().position(|&x| (x - 1.0).abs() < 1e-9) + } + + /// OpenQASM means what it says: registers keep their own qubits, broadcast, + /// comments, single-qubit declarations and gate definitions are read, and a + /// program whose meaning needs its measurements is refused, not rewritten. + #[test] + fn imported_programs_mean_what_they_say() { + // Two registers are two qubits: |11⟩. + let ide = loaded("OPENQASM 3.0; qubit[1] a; qubit[1] b; x a[0]; x b[0];"); + assert_eq!(ide.circuit.n_qubits, 2); + assert_eq!(certain(&ide), Some(3)); + // OpenQASM 2's registers too: three qubits, one of them flipped. + let ide = loaded("OPENQASM 2.0; include \"qelib1.inc\"; qreg a[2]; qreg b[1]; x b[0];"); + assert_eq!(ide.circuit.n_qubits, 3); + assert_eq!(certain(&ide).map(|i| i.count_ones()), Some(1)); + // Broadcast, a single-qubit declaration, a block comment, a definition. + let ide = loaded("OPENQASM 3.0; qubit[2] q; /* both */ h q;"); + assert!(ide.probabilities().iter().all(|&p| (p - 0.25).abs() < 1e-9), "{:?}", ide.probabilities()); + let ide = loaded("OPENQASM 3.0; qubit q; x q;"); + assert_eq!((ide.circuit.n_qubits, certain(&ide)), (1, Some(1))); + let ide = loaded("OPENQASM 3.0; gate flip a { x a; } qubit[1] q; flip q[0];"); + assert_eq!(certain(&ide), Some(1)); + // A final measurement leaves the state as it was. + let ide = loaded("OPENQASM 3.0; qubit[1] q; bit[1] c; x q[0]; c[0] = measure q[0];"); + assert_eq!(certain(&ide), Some(1)); + // A measurement acted on afterwards, or a reset after a gate, is refused. + let mut ide = QuantumIde::new(2); + for src in ["OPENQASM 3.0; qubit[1] q; bit[1] c; h q[0]; c[0] = measure q[0]; h q[0];", "OPENQASM 3.0; qubit[1] q; x q[0]; reset q[0];"] { + let e = ide.load_qasm(src).unwrap_err(); + assert!(e.contains("measures a qubit and then acts on it") && e.contains("/quantum-run/"), "{src}: {e}"); + } + // A missing semicolon is an error with its line, as in the service. + assert!(ide.load_qasm("OPENQASM 3.0;\nqubit[2] q\nh q[0];").is_err()); + } + + /// Every JSON the panels read parses, whatever an error message holds. + #[test] + fn the_panels_get_json_that_parses() { + let mut ide = QuantumIde::new(8); + assert!(ide.load_algorithm("grover", 8, 0)); + ide.run().unwrap(); + for text in [ide.run_mps(8), ide.transpile_line(), ide.sample_stabilizer(1.0, 16), ide.stabilizer_scale(20, 1.0, 16), ide.expect_z_pauli(0, 1e-6), ide.pauli_scale(8, 1e-4), ide.gates_json(), super::ide_catalog()] { + serde_json::from_str::(&text).unwrap_or_else(|e| panic!("{text}: {e}")); + } + assert_eq!(super::error_json("say \"no\" \\ here"), r#"{"error":"say \"no\" \\ here"}"#); + } + + /// Receipts are signed with the key the page gives, and say when they are + /// not. + #[test] + fn receipts_use_the_browsers_key() { + let mut ide = QuantumIde::new(2); + assert!(ide.load_algorithm("bell", 2, 0)); + ide.run().unwrap(); + assert!(ide.key_is_demo()); + let demo = ide.public_key_hex(); + ide.set_signing_key(&[7u8; 32]).unwrap(); + assert!(!ide.key_is_demo()); + assert_ne!(ide.public_key_hex(), demo); + assert!(ide.receipt_verifies(1.0, 64)); + let r: serde_json::Value = serde_json::from_str(&ide.receipt_json(1.0, 64)).unwrap(); + assert_eq!(r["signer_pubkey"], ide.public_key_hex()); + assert_eq!(r["signer_id"], ide.signer_did()); + assert!(ide.signer_did().starts_with("did:key:z6Mk")); + assert!(ide.set_signing_key(&[1u8; 31]).is_err()); + } }