wai-quantum: OpenQASM 3 generators, one program for a family of circuits #466
6 changed files with 897 additions and 10 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
Loading…
Reference in a new issue