wai-quantum: OpenQASM 3 generators, one program for a family of circuits #466

Merged
dcharlot merged 1 commit from wai/quantum-qasm-generators into main 2026-10-25 17:54:00 -04:00
6 changed files with 897 additions and 10 deletions

View file

@ -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,

View file

@ -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:

View file

@ -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<PathBuf>,
},
/// 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<wai_quantum::qasm::Inputs> = 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<String, String> {
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<String, String> {
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<String, String> {
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<Value, String> {
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(|| "<stdin>".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 },

View file

@ -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

View file

@ -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 |

View file

@ -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<Vec<(Tok, usize)>, String> {
"**" => Some("**"),
"==" => Some("=="),
"!=" => Some("!="),
"<=" => Some("<="),
">=" => Some(">="),
"&&" => Some("&&"),
"||" => Some("||"),
_ => None,
};
if let Some(s) = sym {
@ -322,6 +336,9 @@ fn lex(src: &str) -> Result<Vec<(Tok, usize)>, String> {
'-' => "-",
'*' => "*",
'/' => "/",
'%' => "%",
'<' => "<",
'>' => ">",
'^' => "^",
'@' => "@",
':' => ":",
@ -335,6 +352,670 @@ fn lex(src: &str) -> Result<Vec<(Tok, usize)>, 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<String, i64>;
/// 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<Vec<String>, 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<String, String> {
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<Vec<(Tok, usize)>, 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<String, &'static str> {
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<String, i64>,
taken: HashMap<String, &'static str>,
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<usize, String> {
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<usize, String> {
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<Option<i64>, 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<i64>) {
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<usize, String> {
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<i64, String> {
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<usize, String> {
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<i64> = 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::<Result<_, _>>()?;
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<String, i64>,
line: usize,
}
impl IntExpr<'_> {
fn err<T>(&self, msg: impl std::fmt::Display) -> Result<T, String> {
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<i64>) -> Result<i64, String> {
v.ok_or_else(|| format!("line {}: an integer expression overflows", self.line))
}
fn or(&mut self) -> Result<i64, String> {
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<i64, String> {
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<i64, String> {
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<i64, String> {
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<i64, String> {
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<i64, String> {
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<i64, String> {
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<Program, String> {
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<Program, String> {
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<usize>), 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<usize>), 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<u32> = 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<u32> = 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"),