diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 43e3310cf..558295845 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -1116,6 +1116,28 @@ wire format may change and such changes are recorded here. - **`tools/ops-gpu-bench`** times each capability's decode with its graphs on the CPU and on each adapter, and reads the energy counters the host has. - **CI:** `wai-ops-gpu.yml` runs the Metal and browser legs on the `macos` runner, and requires on a runner with no GPU that the device tests skip without `WAI_OPS_GPU_REQUIRE` and fail with it. +### wai-quantum: OpenQASM 3 generators — one program for a family of circuits + +- **The reader takes generators** (`qasm::parse_with`, `qasm::inputs`). `input int n;` and `input + uint n;` take an integer given when the program is read. `const int k = …;` names one. `for i + in [a:b] { … }` (also `[a:step:b]`, both ends included as OpenQASM 3 reads a range, and `{a, b, + c}`, nested) is unrolled once per value. Indices and sizes take integer arithmetic (`q[2 * i + + 1]`, `qubit[n + 1] q;`): `+ - * / %` with C's truncating division, `**`, comparisons, `&& || + !`. An `if` whose condition reads only integers is decided on reading; one that reads bits + stays classical control. A gate a loop makes keeps the line of the statement in the loop's + body, so a critique still points into the generator. +- **An instance written out** (`qasm::instantiate`): the circuit at given inputs as plain + OpenQASM, with no `input`, `const` or loop left, for any surface that takes a program as + text. Reading it gives the same program, gate for gate. +- **Bounded.** A program unrolls to at most 20 million tokens, every integer operation is + checked for overflow, and a name an integer would shadow (a register, a gate's parameter or + qubit, a word of the language) is refused with its line. A loop variable may still share a + gate's name, such as the identity `i`. `while`, `inv` and `pow` are still refused. +- **The command line** takes `--input NAME=VALUE` on every command, so `run`, `plan`, + `critique`, `equivalent`, `shortcuts` and the rest each read one circuit of the family. + `wai-quantum instance gen.qasm --input n=5` prints that circuit. +- A program with no `input`, `const` or `for` reads exactly as before. + ### wai-quantum-service: sandboxed workers, and limits no client can multiply - **Workers run sandboxed** (macOS). Each job's worker runs under `sandbox-exec`: no network, diff --git a/wai/wai-quantum-cli/README.md b/wai/wai-quantum-cli/README.md index b2ade0b08..616a6409f 100644 --- a/wai/wai-quantum-cli/README.md +++ b/wai/wai-quantum-cli/README.md @@ -35,9 +35,24 @@ A program is OpenQASM 3, or the OpenQASM 2 forms in common use. The reader accep - the standard gates; - `ctrl` and `negctrl` modifiers; - gate definitions; -- measurement and `reset` anywhere, and `if` on measured bits. +- measurement and `reset` anywhere, and `if` on measured bits; +- generators: integer `input`s, `const`s and `for` loops, so one program describes a family of circuits. -It refuses loops, `inv` and `pow`, giving the line of each. +It refuses `while`, `inv` and `pow`, giving the line of each. + +A generator takes its inputs with `--input`, which every command accepts, and `instance` writes the circuit out: + +```sh +cat > ghz.qasm <<'QASM' +OPENQASM 3.0; +input uint n; +qubit[n] q; +h q[0]; +for uint i in [0:n-2] { cx q[i], q[i + 1]; } +QASM +wai-quantum run ghz.qasm --input n=40 --shots 100 +wai-quantum instance ghz.qasm --input n=3 +``` `--method auto` (the default) picks the strongest method the program admits: diff --git a/wai/wai-quantum-cli/src/main.rs b/wai/wai-quantum-cli/src/main.rs index f82086c85..e94ff8c34 100644 --- a/wai/wai-quantum-cli/src/main.rs +++ b/wai/wai-quantum-cli/src/main.rs @@ -12,6 +12,8 @@ //! wai-quantum equivalent a.qasm b.qasm # the same circuit? and how the engine knows //! wai-quantum improve prog.qasm > better.qasm # its smells fixed, checked the same circuit //! wai-quantum shortcuts prog.qasm # each exact classical route and its cost here +//! wai-quantum run ghz.qasm --input n=40 # a generator, read at n = 40 +//! wai-quantum instance qft.qasm --input n=5 # a generator written out as plain OpenQASM //! wai-quantum submit prog.qasm # on the job service //! wai-quantum verify result.json # a result's receipt, or a lab receipt //! wai-quantum key # the key this user signs with @@ -25,6 +27,11 @@ //! ([`wai_quantum::engine`]), so a job gives here the result it gives there. //! `run --json` prints the job as the service returns it: the request, the //! plan, and the result with its receipt, which `verify` checks. +//! +//! A generator is a program with integer `input`s, `const`s and `for` loops +//! ([`wai_quantum::qasm::parse_with`]): one program for a whole family of +//! circuits. Every command reads one circuit of the family, at the values +//! given with `--input`, and `instance` prints that circuit. mod lab; mod mcp; @@ -43,6 +50,10 @@ use wai_quantum_cli::keys; propagate_version = true )] struct Cli { + /// A value for a generator's integer `input`, as n=5 (repeat for each + /// input): the program is read as its family's circuit at those values + #[arg(long = "input", value_name = "NAME=VALUE", global = true, value_parser = input_arg)] + inputs: Vec<(String, i64)>, #[command(subcommand)] command: Command, } @@ -66,6 +77,11 @@ enum Command { #[arg(long)] json: bool, }, + /// Write a generator out at its `--input` values, as plain OpenQASM + Instance { + /// The program; `-` or nothing reads standard input + file: Option, + }, /// Check a program and print each problem with its line Lint { /// The program; `-` or nothing reads standard input @@ -229,6 +245,15 @@ fn noise_arg(s: &str) -> Result<(String, f64), String> { Ok((k.to_string(), p)) } +fn input_arg(s: &str) -> Result<(String, i64), String> { + let (k, v) = s.split_once('=').ok_or("give an input and its value, as n=5")?; + let v: i64 = v.trim().parse().map_err(|_| format!("{v}: an input takes a whole number"))?; + Ok((k.trim().to_string(), v)) +} + +/// The values given with `--input`, for every program the command reads. +static INPUTS: std::sync::OnceLock = std::sync::OnceLock::new(); + fn option_arg(s: &str) -> Result<(String, Value), String> { let (k, v) = s.split_once('=').ok_or("give an option and its value, as max_bond=128")?; // A number or true/false reads as JSON; anything else is a string. @@ -241,8 +266,24 @@ fn die(msg: &str) -> ! { std::process::exit(1) } -/// The text of `file`, or standard input for `-` or none. +/// The program in `file`, or on standard input for `-` or none. A generator +/// (a program with integer `input`s) is written out at the `--input` values, +/// so every command reads one circuit of its family. fn read_input(file: Option<&Path>) -> Result { + at_inputs(read_text(file)?, &INPUTS.get().cloned().unwrap_or_default()) +} + +/// `text` as one circuit: a generator written out at `inputs`, any other +/// program as it is (and refused if it was given inputs it does not take). +fn at_inputs(text: String, inputs: &wai_quantum::qasm::Inputs) -> Result { + if inputs.is_empty() && wai_quantum::qasm::inputs(&text).map(|v| v.is_empty()).unwrap_or(true) { + return Ok(text); + } + wai_quantum::qasm::instantiate(&text, inputs) +} + +/// The text of `file`, or standard input for `-` or none. +fn read_text(file: Option<&Path>) -> Result { match file { Some(p) if p != Path::new("-") => std::fs::read_to_string(p).map_err(|e| format!("{}: {e}", p.display())), _ => { @@ -419,6 +460,7 @@ fn submit(req: &Value, service: &str) -> Result { fn main() { let cli = Cli::parse(); + let _ = INPUTS.set(cli.inputs.iter().cloned().collect()); match cli.command { Command::Run { job, out, key } => { let qasm = read_input(job.file.as_deref()).unwrap_or_else(|e| die(&e)); @@ -444,6 +486,9 @@ fn main() { println!("work {}", p.work); } } + Command::Instance { file } => { + print!("{}", read_input(file.as_deref()).unwrap_or_else(|e| die(&e))); + } Command::Lint { file, json } => { let name = file.as_deref().filter(|p| *p != Path::new("-")).map(|p| p.display().to_string()).unwrap_or_else(|| "".into()); let src = read_input(file.as_deref()).unwrap_or_else(|e| die(&e)); @@ -595,6 +640,26 @@ fn main() { mod tests { use super::*; + #[test] + fn a_generator_is_read_at_its_inputs_and_a_plain_program_as_it_is() { + let ghz = "OPENQASM 3.0;\ninput uint n;\nqubit[n] q;\nh q[0];\nfor uint i in [0:n-2] { cx q[i], q[i + 1]; }\n"; + let at = |text: &str, pairs: &[(&str, i64)]| at_inputs(text.to_string(), &pairs.iter().map(|(k, v)| (k.to_string(), *v)).collect()); + let three = at(ghz, &[("n", 3)]).unwrap(); + assert_eq!(three, "OPENQASM 3.0;\nqubit[3] q;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\n"); + // The instance runs as the service would run it: twelve qubits, one + // GHZ state, every shot all zeros or all ones. + let (mut j, _) = job(""); + j.shots = Some(200); + let key = wai_quantum::ed25519_dalek::SigningKey::from_bytes(&[3; 32]); + let body = run_here(&request(&j, at(ghz, &[("n", 12)]).unwrap()), 1, &key).unwrap(); + let counts = body["result"]["counts"].as_object().unwrap(); + assert!(counts.keys().all(|k| k == &"0".repeat(12) || k == &"1".repeat(12)), "{counts:?}"); + let plain = "OPENQASM 3.0;\nqubit q;\nh q;\n"; + assert_eq!(at(plain, &[]).unwrap(), plain); + assert!(at(ghz, &[]).unwrap_err().contains("give n a value")); + assert!(at(plain, &[("n", 2)]).unwrap_err().contains("no `input n`")); + } + fn job(qasm: &str) -> (Job, String) { ( Job { file: None, shots: None, seed: None, method: None, observables: vec![], amplitudes: vec![], noise: vec![], options: vec![], memory_gb: 1 }, diff --git a/wai/wai-quantum-service/README.md b/wai/wai-quantum-service/README.md index 63cc04a62..a4fafe835 100644 --- a/wai/wai-quantum-service/README.md +++ b/wai/wai-quantum-service/README.md @@ -28,7 +28,7 @@ A job is JSON: Counts are keyed by the program's classical bits, the highest bit first. With no measurement in the program, every qubit is measured into the bit of its own index. -The program may use the standard gates, user `gate` definitions, and the control modifiers `ctrl @`, `ctrl(n) @`, `negctrl @` and `negctrl(n) @` (on `x y z h s sdg t tdg p` and `cx cy cz ch cp ccx ccz`). Measurement and `reset` may come anywhere, and `if` acts on measured bits (`if (c[0]) x q[1];`, `if (c == 3) { … } else { … }`, OpenQASM 2's `if(c==1) x q;`). Loops, `inv @` and `pow @` are refused, each with its line and what to write instead (`src/qasm.rs` lists everything read). +The program may use the standard gates, user `gate` definitions, and the control modifiers `ctrl @`, `ctrl(n) @`, `negctrl @` and `negctrl(n) @` (on `x y z h s sdg t tdg p` and `cx cy cz ch cp ccx ccz`). Measurement and `reset` may come anywhere, and `if` acts on measured bits (`if (c[0]) x q[1];`, `if (c == 3) { … } else { … }`, OpenQASM 2's `if(c==1) x q;`). `for` loops over ranges and sets, and integer `const`s, are unrolled on reading. A program that declares an integer `input` is a generator: the service reads one circuit, so send it written out (`wai-quantum instance gen.qasm --input n=5`). `while`, `inv @` and `pow @` are refused, each with its line and what to write instead (`src/qasm.rs` lists everything read). ## Methods diff --git a/wai/wai-quantum/README.md b/wai/wai-quantum/README.md index 1d7a1da4f..0edfc93dc 100644 --- a/wai/wai-quantum/README.md +++ b/wai/wai-quantum/README.md @@ -67,7 +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_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. Generators: integer `input`s, `const`s and `for` loops (ranges, steps, sets, nested), with integer arithmetic in indices and sizes and `if` on integers decided on reading, so one program describes a family of circuits; `qasm::parse_with` reads it at given inputs and `qasm::instantiate` writes that circuit out as plain OpenQASM. `while`, `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_engine` | **the quantum job service's engine, for every host**: a job (an OpenQASM program and what is asked of it: shots, Pauli expectation values, amplitudes, noise) planned onto the strongest method it admits, with the reason and the memory and work it will take; run; answered in the service's JSON with its accuracy class; and sealed over the request and the result in their canonical JSON (`engine::job`, `engine::receipt`), so a result from the service, the command line or a browser tab checks the same way. Also a program's circuit drawn as text (`engine::draw`). It builds for `wasm32-unknown-unknown`, where it runs on one thread and claims no time | | `quantum_stabilizer` | stabilizer (CHP) tableau — `O(n²)`, scales far past statevector | | `quantum_mps` | tensor-network (matrix-product-state) backend | diff --git a/wai/wai-quantum/src/qasm.rs b/wai/wai-quantum/src/qasm.rs index 81bffc000..fd9ea5ce4 100644 --- a/wai/wai-quantum/src/qasm.rs +++ b/wai/wai-quantum/src/qasm.rs @@ -23,7 +23,17 @@ //! ccz`: a gate the standard names is read as that gate, and any other as //! standard gates, exactly (see [`Parser::phase_n`]). //! -//! Refused, each with its line: loops, the `inv` and `pow` modifiers, +//! Generators ([`parse_with`]): `input int n;` and `input uint n;` take an +//! integer given when the program is read, `const int k = …;` and `const uint` +//! name one, and `for i in [a:b] { … }` (also `[a:step:b]`, both ends +//! included, and `{a, b, c}`) is unrolled once per value. Indices and sizes +//! take integer arithmetic (`q[2 * i + 1]`, `qubit[n + 1] q;`): `+ - * / %` +//! with C's truncating division, `**`, comparisons, `&& || !`. An `if` whose +//! condition reads only integers is decided on reading; one that reads bits +//! stays classical control. One program so describes a whole family of +//! circuits, one for each value of its inputs. +//! +//! Refused, each with its line: `while`, the `inv` and `pow` modifiers, //! declarations inside `if`, and anything else not listed. //! //! The standard gates: `id x y z h s sdg t tdg sx sxdg rx ry rz p phase u1 u2 @@ -301,6 +311,10 @@ fn lex(src: &str) -> Result, String> { "**" => Some("**"), "==" => Some("=="), "!=" => Some("!="), + "<=" => Some("<="), + ">=" => Some(">="), + "&&" => Some("&&"), + "||" => Some("||"), _ => None, }; if let Some(s) = sym { @@ -322,6 +336,9 @@ fn lex(src: &str) -> Result, String> { '-' => "-", '*' => "*", '/' => "/", + '%' => "%", + '<' => "<", + '>' => ">", '^' => "^", '@' => "@", ':' => ":", @@ -335,6 +352,670 @@ fn lex(src: &str) -> Result, String> { Ok(out) } +// --------------------------------------------------------------------------- +// Generators: integer inputs, constants and loops, unrolled on reading +// --------------------------------------------------------------------------- + +/// Values for a program's integer `input`s, by name. +pub type Inputs = std::collections::BTreeMap; + +/// The most tokens a program may unroll to: a loop is unrolled whole before +/// the program is read, so this bounds the work and memory a short program +/// can ask for. +const MAX_TOKENS: usize = 20_000_000; + +/// Words an integer may not be named, since the reader gives them their own +/// meaning. +const RESERVED: &[&str] = &[ + "OPENQASM", "include", "qubit", "bit", "qreg", "creg", "gate", "measure", "reset", "barrier", "if", "else", "for", "in", "while", "input", "output", + "const", "int", "uint", "float", "angle", "bool", "ctrl", "negctrl", "inv", "pow", "pi", "tau", "euler", "π", "τ", "ℯ", "sin", "cos", "tan", "exp", + "ln", "sqrt", "arcsin", "arccos", "arctan", +]; + +/// The integer `input`s a program declares (`input uint n;`, `input int k;`), +/// in the order it declares them. A program that declares one is a +/// generator: [`parse_with`] reads it for each value, and [`parse`] refuses it, +/// saying which value is missing. +pub fn inputs(src: &str) -> Result, String> { + let toks = lex(src)?; + let mut names = Vec::new(); + let mut i = 0; + while i < toks.len() { + if matches!(&toks[i].0, Tok::Ident(w) if w == "input") { + let mut j = i + 1; + if matches!(toks.get(j).map(|t| &t.0), Some(Tok::Ident(t)) if t == "int" || t == "uint") { + j += 1; + if matches!(toks.get(j).map(|t| &t.0), Some(Tok::Sym("["))) { + while j < toks.len() && toks[j].0 != Tok::Sym("]") { + j += 1; + } + j += 1; + } + if let Some((Tok::Ident(name), _)) = toks.get(j) { + names.push(name.clone()); + } + } + i = j; + } + i += 1; + } + Ok(names) +} + +/// A generator written out for one set of `inputs`, as plain OpenQASM with no +/// `input`, `const` or loop left: the circuit of the family at those values, +/// for any reader of OpenQASM, and for every surface that takes a program as +/// text. Reading it gives the program [`parse_with`] gives, gate for gate. +pub fn instantiate(src: &str, inputs: &Inputs) -> Result { + let toks = unroll(lex(src)?, inputs)?; + let mut out = String::new(); + let mut line_start = true; + let mut depth = 0usize; + let mut prev: Option<&Tok> = None; + for (t, _) in &toks { + if matches!(t, Tok::Sym("}")) { + depth = depth.saturating_sub(1); + } + if line_start { + out.push_str(&" ".repeat(depth)); + } else if needs_space(prev, t) { + out.push(' '); + } + match t { + Tok::Ident(w) => out.push_str(w), + // The version keeps its decimal; any other number is the shortest + // text that reads back as the same number. + Tok::Num(v) if matches!(prev, Some(Tok::Ident(w)) if w == "OPENQASM") => out.push_str(&format!("{v:?}")), + Tok::Num(v) => out.push_str(format!("{v:?}").trim_end_matches(".0")), + Tok::Str(s) => { + out.push('"'); + out.push_str(s); + out.push('"'); + } + Tok::Sym(s) => out.push_str(s), + } + line_start = matches!(t, Tok::Sym(";" | "{" | "}")); + if line_start { + out.push('\n'); + } + if matches!(t, Tok::Sym("{")) { + depth += 1; + } + prev = Some(t); + } + Ok(out) +} + +/// Whether two tokens written in a row need a space between them. +fn needs_space(prev: Option<&Tok>, t: &Tok) -> bool { + let word = |t: &Tok| matches!(t, Tok::Ident(_) | Tok::Num(_) | Tok::Str(_)); + match (prev, t) { + (None, _) => false, + (_, Tok::Sym(";" | "," | ")" | "]" | "[")) => false, + (Some(Tok::Sym("(" | "[" | "!")), _) => false, + (Some(Tok::Sym(",")), _) => true, + (Some(Tok::Ident(w)), Tok::Sym("(")) if w == "if" => true, + (Some(p), Tok::Sym("(")) => !word(p), + (Some(p), _) if word(p) && word(t) => true, + (Some(Tok::Sym(_)), _) | (_, Tok::Sym(_)) => true, + _ => false, + } +} + +/// The tokens a program unrolls to for `inputs`: each `input` bound to its +/// value, each `const` folded, each `for` loop written out once per value, each +/// `if` on integers alone decided, every integer name replaced by its value, +/// and every index or size (`q[i + 1]`, `qubit[2 * n] q;`) folded with +/// integer arithmetic. A program with no `input`, `const` or `for` is returned +/// as it came, so it reads exactly as before. +fn unroll(toks: Vec<(Tok, usize)>, inputs: &Inputs) -> Result, String> { + let generator = toks.iter().any(|(t, _)| matches!(t, Tok::Ident(w) if w == "input" || w == "const" || w == "for")); + if !generator { + if let Some(name) = inputs.keys().next() { + return Err(format!("the program declares no `input {name}`")); + } + return Ok(toks); + } + let mut u = Unroller { toks: &toks, inputs, env: HashMap::new(), taken: taken_names(&toks), out: Vec::with_capacity(toks.len()), used: Vec::new() }; + u.seq(0, toks.len())?; + if let Some(unused) = inputs.keys().find(|k| !u.used.contains(k)) { + return Err(format!("the program declares no `input {unused}`")); + } + Ok(u.out) +} + +/// The names registers, gates, gate parameters and gate qubits take: an +/// integer may not shadow one of them. +fn taken_names(toks: &[(Tok, usize)]) -> HashMap { + let mut taken = HashMap::new(); + let mut i = 0; + while i < toks.len() { + if let Tok::Ident(w) = &toks[i].0 { + match w.as_str() { + "qubit" | "bit" => { + let mut j = i + 1; + if toks.get(j).is_some_and(|t| t.0 == Tok::Sym("[")) { + while j < toks.len() && toks[j].0 != Tok::Sym("]") { + j += 1; + } + j += 1; + } + if let Some((Tok::Ident(name), _)) = toks.get(j) { + taken.insert(name.clone(), "a register"); + } + } + "qreg" | "creg" => { + if let Some((Tok::Ident(name), _)) = toks.get(i + 1) { + taken.insert(name.clone(), "a register"); + } + } + "gate" => { + let mut j = i + 1; + if let Some((Tok::Ident(name), _)) = toks.get(j) { + taken.insert(name.clone(), "a gate"); + } + j += 1; + while j < toks.len() && toks[j].0 != Tok::Sym("{") { + if let Tok::Ident(name) = &toks[j].0 { + taken.insert(name.clone(), "a gate's parameter or qubit"); + } + j += 1; + } + } + _ => {} + } + } + i += 1; + } + taken +} + +struct Unroller<'a> { + toks: &'a [(Tok, usize)], + inputs: &'a Inputs, + env: HashMap, + taken: HashMap, + out: Vec<(Tok, usize)>, + used: Vec<&'a String>, +} + +impl<'a> Unroller<'a> { + fn emit(&mut self, t: Tok, line: usize) -> Result<(), String> { + if self.out.len() >= MAX_TOKENS { + return Err(format!("line {line}: the program unrolls past {MAX_TOKENS} tokens")); + } + self.out.push((t, line)); + Ok(()) + } + + fn ident(&self, i: usize) -> Option<&'a str> { + match self.toks.get(i) { + Some((Tok::Ident(w), _)) => Some(w.as_str()), + _ => None, + } + } + + fn sym(&self, i: usize, s: &str) -> bool { + matches!(self.toks.get(i), Some((Tok::Sym(x), _)) if *x == s) + } + + fn line(&self, i: usize) -> usize { + self.toks.get(i).or(self.toks.last()).map(|t| t.1).unwrap_or(0) + } + + /// The index just past the bracket that closes the one at `open`. + fn close(&self, open: usize, (l, r): (&str, &str)) -> Result { + let mut depth = 0usize; + for j in open..self.toks.len() { + if self.sym(j, l) { + depth += 1; + } else if self.sym(j, r) { + depth -= 1; + if depth == 0 { + return Ok(j + 1); + } + } + } + Err(format!("line {}: `{l}` is not closed", self.line(open))) + } + + /// The statement or `{ block }` starting at `i`: the span to unroll, and + /// the index after it. + fn body(&self, i: usize) -> Result<((usize, usize), usize), String> { + if self.sym(i, "{") { + let end = self.close(i, ("{", "}"))?; + return Ok(((i + 1, end - 1), end)); + } + if matches!(self.ident(i), Some("for" | "if")) { + return Err(format!("line {}: put a loop or an `if` that is itself a body in braces", self.line(i))); + } + let mut depth = 0usize; + for j in i..self.toks.len() { + if self.sym(j, "{") || self.sym(j, "(") || self.sym(j, "[") { + depth += 1; + } else if self.sym(j, "}") || self.sym(j, ")") || self.sym(j, "]") { + depth = depth.saturating_sub(1); + } else if depth == 0 && self.sym(j, ";") { + return Ok(((i, j + 1), j + 1)); + } + } + Err(format!("line {}: expected a statement or a `{{ block }}`", self.line(i))) + } + + /// The index just past the `if` statement at `i`, its `else` and any + /// `else if` after it included. + fn if_extent(&self, i: usize) -> Result { + if !self.sym(i + 1, "(") { + return Err(format!("line {}: expected `(` after `if`", self.line(i))); + } + 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. + fn at_statement_start(&self) -> bool { + match self.out.last() { + None => true, + Some((Tok::Sym(s), _)) => matches!(*s, ";" | "{" | "}" | ")"), + Some((Tok::Ident(w), _)) => w == "else", + _ => false, + } + } + + fn bind(&mut self, name: &str, value: i64, line: usize) -> Result, String> { + if RESERVED.contains(&name) { + return Err(format!("line {line}: `{name}` is a word of the language; give the integer another name")); + } + if let Some(what) = self.taken.get(name) { + return Err(format!("line {line}: `{name}` is also {what}; give the integer another name")); + } + Ok(self.env.insert(name.to_string(), value)) + } + + fn restore(&mut self, name: &str, old: Option) { + match old { + Some(v) => self.env.insert(name.to_string(), v), + None => self.env.remove(name), + }; + } + + /// An integer type (`int`, `uint`, `int[32]`) at `i`: whether it is + /// unsigned, and the index after it. + fn int_type(&self, i: usize) -> Result<(bool, usize), String> { + let unsigned = match self.ident(i) { + Some("uint") => true, + Some("int") => false, + Some(other) => return Err(format!("line {}: only `int` and `uint` are read here, not `{other}`", self.line(i))), + None => return Err(format!("line {}: expected `int` or `uint`", self.line(i))), + }; + let mut j = i + 1; + if self.sym(j, "[") { + j = self.close(j, ("[", "]"))?; + } + Ok((unsigned, j)) + } + + fn name_at(&self, i: usize) -> Result<&'a str, String> { + self.ident(i).ok_or_else(|| format!("line {}: expected a name", self.line(i))) + } + + fn expect(&self, i: usize, s: &str) -> Result { + if self.sym(i, s) { Ok(i + 1) } else { Err(format!("line {}: expected `{s}`", self.line(i))) } + } + + /// The integer the tokens `lo..hi` evaluate to. + fn value(&self, lo: usize, hi: usize) -> Result { + let mut e = IntExpr { toks: &self.toks[lo..hi], pos: 0, env: &self.env, line: self.line(lo) }; + let v = e.or()?; + if e.pos != e.toks.len() { + return Err(format!("line {}: unexpected `{}` in an integer expression", e.line, tok_text(&e.toks[e.pos].0))); + } + Ok(v) + } + + /// The tokens `lo..hi` split at the top-level `sep`. + fn split(&self, lo: usize, hi: usize, sep: &str) -> Vec<(usize, usize)> { + let (mut parts, mut start, mut depth) = (Vec::new(), lo, 0usize); + for j in lo..hi { + if self.sym(j, "(") || self.sym(j, "[") || self.sym(j, "{") { + depth += 1; + } else if self.sym(j, ")") || self.sym(j, "]") || self.sym(j, "}") { + depth = depth.saturating_sub(1); + } else if depth == 0 && self.sym(j, sep) { + parts.push((start, j)); + start = j + 1; + } + } + parts.push((start, hi)); + parts + } + + /// Whether every name in `lo..hi` is an integer known here, so that the + /// tokens can be folded to a number. + fn foldable(&self, lo: usize, hi: usize) -> bool { + hi > lo + && self.toks[lo..hi].iter().all(|(t, _)| match t { + Tok::Num(_) => true, + Tok::Ident(w) => self.env.contains_key(w), + Tok::Sym(s) => matches!(*s, "+" | "-" | "*" | "/" | "%" | "**" | "(" | ")" | "==" | "!=" | "<" | "<=" | ">" | ">=" | "&&" | "||" | "!"), + Tok::Str(_) => false, + }) + } + + fn seq(&mut self, lo: usize, hi: usize) -> Result<(), String> { + let mut i = lo; + while i < hi { + let line = self.line(i); + match &self.toks[i].0 { + Tok::Ident(w) if w == "input" => { + let (unsigned, j) = self.int_type(i + 1)?; + let name = self.name_at(j)?; + let next = self.expect(j + 1, ";")?; + let Some((key, &v)) = self.inputs.get_key_value(name) else { + return Err(format!("line {line}: the program takes `input {} {name}`; give {name} a value", if unsigned { "uint" } else { "int" })); + }; + if unsigned && v < 0 { + return Err(format!("line {line}: {name} is a `uint`, and {v} is negative")); + } + if self.bind(name, v, line)?.is_some() { + return Err(format!("line {line}: {name} is declared twice")); + } + self.used.push(key); + i = next; + } + Tok::Ident(w) if w == "const" => { + let (unsigned, j) = self.int_type(i + 1)?; + let name = self.name_at(j)?; + let start = self.expect(j + 1, "=")?; + let end = self.split(start, hi, ";").first().map(|p| p.1).unwrap_or(hi); + if end >= hi { + return Err(format!("line {line}: expected `;`")); + } + let v = self.value(start, end)?; + if unsigned && v < 0 { + return Err(format!("line {line}: {name} is a `uint`, and {v} is negative")); + } + if self.bind(name, v, line)?.is_some() { + return Err(format!("line {line}: {name} is declared twice")); + } + i = end + 1; + } + Tok::Ident(w) if w == "for" => i = self.for_loop(i)?, + Tok::Ident(w) if w == "if" && self.sym(i + 1, "(") => { + let close = self.close(i + 1, ("(", ")"))?; + if self.foldable(i + 2, close - 1) { + // 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 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 keep { + self.seq(tlo, thi)?; + } else if let Some((elo, ehi)) = other { + self.seq(elo, ehi)?; + } + i = next; + } else { + self.emit(Tok::Ident("if".into()), line)?; + i += 1; + } + } + Tok::Sym("[") => { + let close = self.close(i, ("[", "]"))?; + if self.foldable(i + 1, close - 1) { + let v = self.value(i + 1, close - 1)?; + self.emit(Tok::Sym("["), line)?; + self.emit(Tok::Num(v as f64), line)?; + self.emit(Tok::Sym("]"), line)?; + i = close; + } else { + self.emit(Tok::Sym("["), line)?; + i += 1; + } + } + // An integer's name stands for its value, except where it + // starts a statement: there it is a gate's name (`i` is the + // identity), so `for i …` may still call `i`. + Tok::Ident(w) if self.env.contains_key(w) && !self.at_statement_start() => { + let v = self.env[w]; + self.emit(Tok::Num(v as f64), line)?; + i += 1; + } + t => { + self.emit(t.clone(), line)?; + i += 1; + } + } + } + Ok(()) + } + + /// `for [int|uint] i in [a:b] body`, `[a:step:b]` (both ends included, + /// as OpenQASM 3 reads a range) or `{v, w, …}`: the body once per value. + fn for_loop(&mut self, at: usize) -> Result { + let line = self.line(at); + let mut j = at + 1; + if matches!(self.ident(j), Some("int" | "uint")) { + j = self.int_type(j)?.1; + } + let name = self.name_at(j)?; + j += 1; + if self.ident(j) != Some("in") { + return Err(format!("line {line}: expected `in` after `for {name}`")); + } + j += 1; + let values: Vec = if self.sym(j, "[") { + let close = self.close(j, ("[", "]"))?; + let parts = self.split(j + 1, close - 1, ":"); + let (a, step, b) = match parts.as_slice() { + [a, b] => (self.value(a.0, a.1)?, 1, self.value(b.0, b.1)?), + [a, s, b] => (self.value(a.0, a.1)?, self.value(s.0, s.1)?, self.value(b.0, b.1)?), + _ => return Err(format!("line {line}: a range is `[start:end]` or `[start:step:end]`")), + }; + if step == 0 { + return Err(format!("line {line}: a range's step is not 0")); + } + j = close; + let span = b.checked_sub(a).ok_or_else(|| format!("line {line}: the range overflows"))?; + let count = if (step > 0 && a > b) || (step < 0 && a < b) { 0 } else { span / step + 1 }; + if count as u64 > MAX_TOKENS as u64 { + return Err(format!("line {line}: the loop runs {count} times, so the program unrolls past {MAX_TOKENS} tokens")); + } + (0..count).map(|k| a + k * step).collect() + } else if self.sym(j, "{") { + let close = self.close(j, ("{", "}"))?; + let vs = self.split(j + 1, close - 1, ",").into_iter().map(|(lo, hi)| self.value(lo, hi)).collect::>()?; + j = close; + vs + } else { + return Err(format!("line {line}: a loop runs over a range `[a:b]` or a set `{{a, b}}`")); + }; + let ((blo, bhi), next) = self.body(j)?; + for v in values { + let old = self.bind(name, v, line)?; + self.seq(blo, bhi)?; + self.restore(name, old); + } + Ok(next) + } +} + +fn tok_text(t: &Tok) -> String { + match t { + Tok::Ident(w) => w.clone(), + Tok::Num(v) => v.to_string(), + Tok::Str(s) => format!("\"{s}\""), + Tok::Sym(s) => s.to_string(), + } +} + +/// An integer expression, as OpenQASM 3 reads one: `+ - * / %` with C's +/// truncating division, `**` for powers, comparisons, `&& || !`, every +/// operation checked for overflow. +struct IntExpr<'a> { + toks: &'a [(Tok, usize)], + pos: usize, + env: &'a HashMap, + line: usize, +} + +impl IntExpr<'_> { + fn err(&self, msg: impl std::fmt::Display) -> Result { + Err(format!("line {}: {msg}", self.line)) + } + + fn eat(&mut self, s: &str) -> bool { + if matches!(self.toks.get(self.pos), Some((Tok::Sym(x), _)) if *x == s) { + self.pos += 1; + true + } else { + false + } + } + + fn checked(&self, v: Option) -> Result { + v.ok_or_else(|| format!("line {}: an integer expression overflows", self.line)) + } + + fn or(&mut self) -> Result { + let mut a = self.and()?; + while self.eat("||") { + let b = self.and()?; + a = i64::from(a != 0 || b != 0); + } + Ok(a) + } + + fn and(&mut self) -> Result { + let mut a = self.cmp()?; + while self.eat("&&") { + let b = self.cmp()?; + a = i64::from(a != 0 && b != 0); + } + Ok(a) + } + + fn cmp(&mut self) -> Result { + let a = self.sum()?; + for op in ["==", "!=", "<=", ">=", "<", ">"] { + if self.eat(op) { + let b = self.sum()?; + return Ok(i64::from(match op { + "==" => a == b, + "!=" => a != b, + "<=" => a <= b, + ">=" => a >= b, + "<" => a < b, + _ => a > b, + })); + } + } + Ok(a) + } + + fn sum(&mut self) -> Result { + let mut a = self.prod()?; + loop { + if self.eat("+") { + let b = self.prod()?; + a = self.checked(a.checked_add(b))?; + } else if self.eat("-") { + let b = self.prod()?; + a = self.checked(a.checked_sub(b))?; + } else { + return Ok(a); + } + } + } + + fn prod(&mut self) -> Result { + let mut a = self.unary()?; + loop { + if self.eat("*") { + let b = self.unary()?; + a = self.checked(a.checked_mul(b))?; + } else if self.eat("/") || self.eat("%") { + let div = matches!(self.toks[self.pos - 1].0, Tok::Sym("/")); + let b = self.unary()?; + if b == 0 { + return self.err("division by zero"); + } + a = self.checked(if div { a.checked_div(b) } else { a.checked_rem(b) })?; + } else { + return Ok(a); + } + } + } + + fn unary(&mut self) -> Result { + if self.eat("-") { + let v = self.unary()?; + return self.checked(v.checked_neg()); + } + if self.eat("+") { + return self.unary(); + } + if self.eat("!") { + return Ok(i64::from(self.unary()? == 0)); + } + let base = self.atom()?; + if self.eat("**") { + let e = self.unary()?; + let Ok(e) = u32::try_from(e) else { return self.err(format!("{base} ** {e}: the exponent is not a small non-negative integer")) }; + return self.checked(base.checked_pow(e)); + } + if self.eat("^") { + return self.err("in an integer expression, write `**` for a power"); + } + Ok(base) + } + + fn atom(&mut self) -> Result { + match self.toks.get(self.pos).map(|t| &t.0) { + Some(Tok::Num(v)) => { + let v = *v; + self.pos += 1; + if v.fract() != 0.0 || v.abs() > 9.007_199_254_740_991e15 { + return self.err(format!("{v} is not an integer")); + } + Ok(v as i64) + } + Some(Tok::Ident(w)) => { + let w = w.clone(); + self.pos += 1; + match self.env.get(&w) { + Some(v) => Ok(*v), + None => self.err(format!("`{w}` is not an integer known here")), + } + } + Some(Tok::Sym("(")) => { + self.pos += 1; + let v = self.or()?; + if !self.eat(")") { + return self.err("expected `)`"); + } + Ok(v) + } + Some(t) => self.err(format!("unexpected `{}` in an integer expression", tok_text(t))), + None => self.err("an integer expression ends early"), + } + } +} + // --------------------------------------------------------------------------- // Expressions // --------------------------------------------------------------------------- @@ -709,7 +1390,7 @@ impl Parser { } "while" | "for" | "def" | "defcal" | "cal" | "box" | "let" | "const" | "input" | "output" | "inv" | "pow" | "delay" | "gphase" => { let hint = match word.as_str() { - "while" | "for" => "; unroll the loop into statements", + "while" => "; write it as a `for` over a range, or unroll it", "inv" | "pow" => "; write the inverse or powered gate out, or define it with `gate`", _ => "", }; @@ -1146,11 +1827,24 @@ pub fn parse(src: &str) -> Result { parse_lines(src).map(|(p, _)| p) } +/// A generator read for one set of `inputs`: the program its `input`s, +/// `const`s, loops and integer conditions unroll to ([`inputs`] lists the +/// names it takes). A program with none of them reads as with [`parse`]. +pub fn parse_with(src: &str, inputs: &Inputs) -> Result { + parse_lines_with(src, inputs).map(|(p, _)| p) +} + /// [`parse`], with the line of the statement each of the program's ops came /// from (`lines[i]` for `ops[i]`): a broadcast's gates, and the gates a gate /// definition expands to, share the line of the statement that made them. pub fn parse_lines(src: &str) -> Result<(Program, Vec), String> { - let toks = lex(src)?; + parse_lines_with(src, &Inputs::new()) +} + +/// [`parse_lines`] for a generator read with `inputs`: a gate a loop makes +/// has the line of the statement in the loop's body. +pub fn parse_lines_with(src: &str, inputs: &Inputs) -> Result<(Program, Vec), String> { + let toks = unroll(lex(src)?, inputs)?; let mut p = Parser { toks, pos: 0, @@ -1999,6 +2693,97 @@ impl Program { mod tests { use super::*; + fn with(pairs: &[(&str, i64)]) -> Inputs { + pairs.iter().map(|(k, v)| (k.to_string(), *v)).collect() + } + + #[test] + fn a_generator_unrolls_to_the_circuit_written_out() { + let ghz = "OPENQASM 3.0;\ninput uint n;\nqubit[n] q;\nh q[0];\nfor uint i in [0:n-2] {\n cx q[i], q[i + 1];\n}\n"; + assert_eq!(inputs(ghz).unwrap(), vec!["n".to_string()]); + let five = parse_with(ghz, &with(&[("n", 5)])).unwrap(); + let out = parse("OPENQASM 3.0;\nqubit[5] q;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\ncx q[2], q[3];\ncx q[3], q[4];\n").unwrap(); + assert_eq!(five, out); + // n = 1 makes an empty range: the loop runs no times. + assert_eq!(parse_with(ghz, &with(&[("n", 1)])).unwrap().ops.len(), 1); + // A gate the loop makes has the line of the statement in its body. + let (_, lines) = parse_lines_with(ghz, &with(&[("n", 3)])).unwrap(); + assert_eq!(lines, vec![4, 6, 6]); + } + + #[test] + fn nested_loops_angles_and_integer_division_make_the_qft() { + let family = "OPENQASM 3.0;\ninput uint n;\nqubit[n] q;\nfor uint i in [0:n-1] {\n h q[i];\n for uint j in [i+1:n-1] {\n cp(pi / 2**(j - i)) q[j], q[i];\n }\n}\nfor uint k in [0:n/2-1] { swap q[k], q[n-1-k]; }\n"; + let four = parse_with(family, &with(&[("n", 4)])).unwrap(); + let out = parse( + "OPENQASM 3.0;\nqubit[4] q;\nh q[0];\ncp(pi/2) q[1], q[0];\ncp(pi/4) q[2], q[0];\ncp(pi/8) q[3], q[0];\nh q[1];\ncp(pi/2) q[2], q[1];\ncp(pi/4) q[3], q[1];\nh q[2];\ncp(pi/2) q[3], q[2];\nh q[3];\nswap q[0], q[3];\nswap q[1], q[2];\n", + ) + .unwrap(); + assert_eq!(four, out); + // Five qubits: 5 / 2 is 2, as in C, so two swaps. + let five = parse_with(family, &with(&[("n", 5)])).unwrap(); + assert_eq!(five.ops.iter().filter(|o| o.kind == Kind::Swap).count(), 2); + } + + #[test] + fn an_instance_written_out_reads_back_as_the_same_program() { + let qft = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\ninput uint n;\nqubit[n] q;\nbit[n] c;\nfor uint i in [0:n-1] {\n h q[i];\n for uint j in [i+1:n-1] { cp(pi / 2**(j - i)) q[j], q[i]; }\n}\nc = measure q;\n"; + for n in [1, 3, 6] { + let ins = with(&[("n", n)]); + let text = instantiate(qft, &ins).unwrap(); + assert!(!text.contains("for") && !text.contains("input"), "{text}"); + assert_eq!(parse(&text).unwrap(), parse_with(qft, &ins).unwrap(), "n = {n}:\n{text}"); + } + let ghz = instantiate("OPENQASM 3.0;\ninput uint n;\nqubit[n] q;\nh q[0];\nfor uint i in [0:n-2] { cx q[i], q[i+1]; }\n", &with(&[("n", 3)])).unwrap(); + assert_eq!(ghz, "OPENQASM 3.0;\nqubit[3] q;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\n"); + } + + #[test] + fn steps_sets_constants_and_sizes_fold() { + let family = "OPENQASM 3.0;\ninput uint n;\nconst uint m = 2 * n;\nqubit[m + 1] q;\nfor uint i in [m:-2:0] x q[i];\nfor int k in {1, 3} h q[k];\n"; + let p = parse_with(family, &with(&[("n", 2)])).unwrap(); + assert_eq!(p.n_qubits, 5); + let xs: Vec = p.ops.iter().filter(|o| o.kind == Kind::X).map(|o| o.qubits[0]).collect(); + assert_eq!(xs, vec![4, 2, 0]); + let hs: Vec = p.ops.iter().filter(|o| o.kind == Kind::H).map(|o| o.qubits[0]).collect(); + assert_eq!(hs, vec![1, 3]); + } + + #[test] + fn an_if_on_integers_is_decided_and_one_on_bits_stays_control() { + let family = "OPENQASM 3.0;\ninput uint n;\nqubit[3] q;\nbit[3] c;\nif (n % 2 == 0) { x q[0]; } else if (n > 4 && n != 7) { x q[1]; } else { x q[2]; }\nc[0] = measure q[0];\nfor uint i in [1:2] {\n if (c[0]) x q[i];\n}\n"; + let target = |n: i64| { + let p = parse_with(family, &with(&[("n", n)])).unwrap(); + (p.ops.iter().find(|o| o.kind == Kind::X).unwrap().qubits[0], p.dynamic.is_some()) + }; + assert_eq!(target(4), (0, true)); + assert_eq!(target(5), (1, true)); + assert_eq!(target(7), (2, true)); + let p = parse_with(family, &with(&[("n", 3)])).unwrap(); + let ifs = p.dynamic.as_ref().unwrap().iter().filter(|s| matches!(s, Instr::JumpUnless(..))).count(); + assert_eq!(ifs, 2, "each unrolled `if (c[0])` stays a condition on the measured bit"); + } + + #[test] + fn a_generator_refuses_what_it_cannot_unroll_and_says_why() { + let ghz = "OPENQASM 3.0;\ninput uint n;\nqubit[n] q;\nfor uint i in [0:n-2] cx q[i], q[i+1];\n"; + let err = |src: &str, ins: &[(&str, i64)]| parse_with(src, &with(ins)).unwrap_err(); + assert!(parse(ghz).unwrap_err().contains("give n a value")); + assert!(err(ghz, &[("n", -1)]).contains("negative")); + assert!(err(ghz, &[("n", 3), ("m", 1)]).contains("no `input m`")); + assert!(err("OPENQASM 3.0;\nqubit[2] q;\nh q;\n", &[("n", 3)]).contains("no `input n`")); + assert!(err("OPENQASM 3.0;\ninput float x;\nqubit q;\n", &[]).contains("only `int` and `uint`")); + assert!(err("OPENQASM 3.0;\ninput uint q;\nqubit[q] q;\n", &[("q", 2)]).contains("also a register")); + assert!(err("OPENQASM 3.0;\ngate g(t) a { rz(t) a; }\nqubit q;\nfor int t in [0:1] g(t) q;\n", &[]).contains("gate's parameter")); + assert!(err("OPENQASM 3.0;\nqubit q;\nfor int pi in [0:1] h q;\n", &[]).contains("word of the language")); + assert!(err("OPENQASM 3.0;\nconst int k = 2 ^ 3;\nqubit q;\n", &[]).contains("**")); + assert!(err("OPENQASM 3.0;\nconst int k = 1 / 0;\nqubit q;\n", &[]).contains("division by zero")); + assert!(err("OPENQASM 3.0;\nqubit[2] q;\nfor int i in [0:1] for int j in [0:1] h q[j];\n", &[]).contains("braces")); + assert!(err("OPENQASM 3.0;\nqubit q;\nfor int i in [0:0:3] h q;\n", &[]).contains("step")); + assert!(err("OPENQASM 3.0;\nqubit q;\nfor int i in [0:100000000] h q;\n", &[]).contains("unrolls past")); + assert!(err("OPENQASM 3.0;\nqubit q;\nwhile (1) h q;\n", &[]).contains("`for`")); + } + #[test] fn registers_broadcast_and_measurement_forms_parse() { let p = parse( @@ -2034,8 +2819,8 @@ mod tests { fn what_is_not_supported_is_refused_with_its_line() { for (src, needle) in [ ("qubit[1] q;\nif (c) x q;", "not a bit register"), - ("qubit[1] q;\nfor uint i in [0:3] { x q; }", "unroll the loop"), - ("qubit[1] q;\nbit c;\nwhile (c) { x q; }", "unroll the loop"), + ("qubit[1] q;\nfor uint i in [0:3] x q", "expected a statement"), + ("qubit[1] q;\nbit c;\nwhile (c) { x q; }", "write it as a `for`"), ("qubit[1] q;\nbit c;\nc = measure q;\nif (c) { qubit r; }", "outside `if` blocks"), ("qubit[2] q;\nbit[2] c;\nc = measure q;\nif (c) x q[0];", "compare the register"), ("qubit[2] q;\nbit[2] c;\nc = measure q;\nif (c == 4) x q[0];", "does not fit in 2 bits"),