359 lines
11 KiB
Markdown
359 lines
11 KiB
Markdown
# Sub-plan 2 — CircuitValidator (Task 4)
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans.
|
|
|
|
**Goal:** Implement `CircuitValidator` wrapping `oq3_semantics`. Validates OpenQASM 3.0 syntax, qubit bounds, and our supported gate subset. Returns structured `ValidationResult` with line/column diagnostics.
|
|
|
|
**Starting state (after sub-plan 1):**
|
|
- `Cargo.toml` with all deps
|
|
- `src/main.rs` declares `mod error; mod executor; mod types;`
|
|
- `src/error.rs` — `BridgeError`
|
|
- `src/types.rs` — `CircuitSource`, `ValidationResult`, `ValidationDiagnostic`, `DiagnosticSeverity`
|
|
- `src/executor.rs` — `SUPPORTED_GATES`, `MAX_LOCAL_QUBITS`, trait defs only
|
|
- `cargo test` passes
|
|
|
|
**Deliverable:** `cargo test` passes with 5 validator unit tests green. One commit added.
|
|
|
|
**Main plan reference:** `docs/superpowers/plans/2026-04-28-quantum-bridge-mcp-v1.md` Task 4.
|
|
|
|
---
|
|
|
|
## Task 4: CircuitValidator — oq3_semantics integration
|
|
|
|
**Files:**
|
|
- Create: `src/validator.rs`
|
|
- Modify: `src/main.rs` (add `mod validator;`)
|
|
|
|
### 4a — AST exploration
|
|
|
|
The `oq3_semantics` AST is lightly documented (6.9% rustdoc coverage). You MUST run the exploration test first to discover the real type names before implementing.
|
|
|
|
- [ ] **Step 1: Create src/validator.rs with exploration tests**
|
|
|
|
```rust
|
|
use oq3_semantics::syntax_to_semantics::parse_source_string;
|
|
|
|
use crate::error::BridgeError;
|
|
use crate::executor::SUPPORTED_GATES;
|
|
use crate::types::{CircuitSource, DiagnosticSeverity, ValidationDiagnostic, ValidationResult};
|
|
|
|
pub struct CircuitValidator {
|
|
max_qubits: usize,
|
|
}
|
|
|
|
impl CircuitValidator {
|
|
pub fn new(max_qubits: usize) -> Self {
|
|
Self { max_qubits }
|
|
}
|
|
|
|
pub fn validate(&self, source: &CircuitSource) -> Result<ValidationResult, BridgeError> {
|
|
todo!("implement after AST exploration")
|
|
}
|
|
}
|
|
|
|
fn byte_offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
|
|
let safe_offset = offset.min(source.len());
|
|
let prefix = &source[..safe_offset];
|
|
let line = prefix.bytes().filter(|&b| b == b'\n').count() + 1;
|
|
let col = prefix.rfind('\n').map(|p| safe_offset - p - 1).unwrap_or(safe_offset) + 1;
|
|
(line, col)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod exploration {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn print_bell_circuit_ast() {
|
|
let qasm = r#"OPENQASM 3.0;
|
|
include "stdgates.inc";
|
|
qubit[2] q;
|
|
bit[2] c;
|
|
h q[0];
|
|
cx q[0], q[1];
|
|
c = measure q;"#;
|
|
let result = parse_source_string(qasm, Some("bell.qasm"), None::<&[&str]>);
|
|
println!("any_errors: {}", result.any_errors());
|
|
println!("program: {:#?}", result.program());
|
|
for err in result.semantic_errors().iter() {
|
|
println!("semantic_error: {:?}", err);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn print_unsupported_gate_ast() {
|
|
let qasm = r#"OPENQASM 3.0;
|
|
include "stdgates.inc";
|
|
qubit[1] q;
|
|
u3(0.1, 0.2, 0.3) q[0];"#;
|
|
let result = parse_source_string(qasm, Some("bad.qasm"), None::<&[&str]>);
|
|
println!("any_errors: {}", result.any_errors());
|
|
println!("program: {:#?}", result.program());
|
|
}
|
|
|
|
#[test]
|
|
fn print_qubit_declaration_ast() {
|
|
let qasm = "OPENQASM 3.0;\nqubit[3] myq;\n";
|
|
let result = parse_source_string(qasm, Some("q.qasm"), None::<&[&str]>);
|
|
println!("program: {:#?}", result.program());
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::executor::MAX_LOCAL_QUBITS;
|
|
|
|
fn validator() -> CircuitValidator {
|
|
CircuitValidator::new(MAX_LOCAL_QUBITS)
|
|
}
|
|
|
|
const BELL_CIRCUIT: &str = r#"OPENQASM 3.0;
|
|
include "stdgates.inc";
|
|
qubit[2] q;
|
|
bit[2] c;
|
|
h q[0];
|
|
cx q[0], q[1];
|
|
c = measure q;"#;
|
|
|
|
#[test]
|
|
fn valid_bell_circuit_is_accepted() {
|
|
let result = validator()
|
|
.validate(&CircuitSource(BELL_CIRCUIT.to_string()))
|
|
.unwrap();
|
|
assert!(result.is_valid, "diagnostics: {:?}", result.diagnostics);
|
|
assert!(result.diagnostics.is_empty());
|
|
assert_eq!(result.num_qubits, Some(2));
|
|
}
|
|
|
|
#[test]
|
|
fn unsupported_gate_produces_error_diagnostic() {
|
|
let circuit = r#"OPENQASM 3.0;
|
|
include "stdgates.inc";
|
|
qubit[1] q;
|
|
u3(0.1, 0.2, 0.3) q[0];"#;
|
|
let result = validator()
|
|
.validate(&CircuitSource(circuit.to_string()))
|
|
.unwrap();
|
|
assert!(!result.is_valid);
|
|
assert!(!result.diagnostics.is_empty());
|
|
let msg = &result.diagnostics[0].message;
|
|
assert!(msg.to_lowercase().contains("u3"), "expected 'u3' in: {msg}");
|
|
}
|
|
|
|
#[test]
|
|
fn too_many_qubits_produces_error_diagnostic() {
|
|
let n = MAX_LOCAL_QUBITS + 1;
|
|
let circuit = format!("OPENQASM 3.0;\nqubit[{n}] q;\n");
|
|
let result = validator().validate(&CircuitSource(circuit)).unwrap();
|
|
assert!(!result.is_valid);
|
|
let msg = &result.diagnostics[0].message;
|
|
assert!(msg.contains(&n.to_string()), "expected {n} in: {msg}");
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_qasm_syntax_produces_error_diagnostic() {
|
|
let circuit = "OPENQASM 3.0;\nthis is not valid qasm;\n";
|
|
let result = validator()
|
|
.validate(&CircuitSource(circuit.to_string()))
|
|
.unwrap();
|
|
assert!(!result.is_valid);
|
|
assert!(!result.diagnostics.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn undeclared_qubit_produces_error_diagnostic() {
|
|
let circuit = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nh q[5];\n";
|
|
let result = validator()
|
|
.validate(&CircuitSource(circuit.to_string()))
|
|
.unwrap();
|
|
assert!(!result.is_valid);
|
|
assert!(!result.diagnostics.is_empty());
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Declare the module**
|
|
|
|
Add to `src/main.rs`:
|
|
```rust
|
|
mod validator;
|
|
```
|
|
|
|
- [ ] **Step 3: Run exploration tests and read AST output**
|
|
|
|
```bash
|
|
cargo test exploration -- --nocapture 2>&1 | head -300
|
|
```
|
|
|
|
**Read the output carefully. Identify:**
|
|
1. How `Program` exposes statements — look for `stmts()`, `statements()`, or `iter()`
|
|
2. The `Stmt` enum variant name for gate calls (e.g. `GateCall`, `Gate`)
|
|
3. The `Stmt` variant name for qubit register declarations (e.g. `QubitDeclaration`, `QubitDecl`)
|
|
4. How to extract the gate name string from a gate call node
|
|
5. How to extract the register size (width) from a qubit declaration node
|
|
6. What `semantic_errors().iter()` yields — specifically the `.range()` method's return type
|
|
|
|
Write down the exact variant and method names before continuing.
|
|
|
|
- [ ] **Step 4: Confirm tests fail at todo!()**
|
|
|
|
```bash
|
|
cargo test validator::tests 2>&1 | tail -10
|
|
```
|
|
|
|
Expected: all 5 panic at `todo!()`.
|
|
|
|
### 4b — Implementation
|
|
|
|
- [ ] **Step 5: Implement CircuitValidator::validate**
|
|
|
|
Replace the `todo!()` with the full implementation. The skeleton below uses placeholder helper names — replace variant patterns with what you observed in Step 3:
|
|
|
|
```rust
|
|
pub fn validate(&self, source: &CircuitSource) -> Result<ValidationResult, BridgeError> {
|
|
let parse_result = parse_source_string(&source.0, Some("circuit.qasm"), None::<&[&str]>);
|
|
let mut diagnostics: Vec<ValidationDiagnostic> = Vec::new();
|
|
|
|
// Collect oq3_semantics errors (syntax + undeclared symbols)
|
|
for error in parse_result.semantic_errors().iter() {
|
|
let range = error.range();
|
|
// TextRange::start() → rowan::TextSize → u32 → usize
|
|
let offset: usize = u32::from(range.start()) as usize;
|
|
let (line, col) = byte_offset_to_line_col(&source.0, offset);
|
|
diagnostics.push(ValidationDiagnostic {
|
|
line,
|
|
column: col,
|
|
message: error.message(),
|
|
severity: DiagnosticSeverity::Error,
|
|
});
|
|
}
|
|
|
|
if parse_result.any_errors() {
|
|
return Ok(ValidationResult {
|
|
is_valid: false,
|
|
diagnostics,
|
|
num_qubits: None,
|
|
num_gates: None,
|
|
});
|
|
}
|
|
|
|
let program = parse_result.program();
|
|
let mut total_qubits: usize = 0;
|
|
let mut gate_count: usize = 0;
|
|
|
|
for stmt in program.stmts() { // ← adjust method name from exploration
|
|
// Qubit declarations — adjust variant name from exploration output
|
|
if let Some(n) = extract_qubit_count(stmt) {
|
|
total_qubits += n;
|
|
}
|
|
|
|
// Gate calls — adjust variant name from exploration output
|
|
if let Some(gate_name) = extract_gate_name(stmt) {
|
|
if gate_name == "measure" {
|
|
continue;
|
|
}
|
|
gate_count += 1;
|
|
if !SUPPORTED_GATES.contains(&gate_name.as_str()) {
|
|
let line = extract_stmt_line(stmt, &source.0);
|
|
let supported = SUPPORTED_GATES.join(", ");
|
|
diagnostics.push(ValidationDiagnostic {
|
|
line,
|
|
column: 1,
|
|
message: format!(
|
|
"gate '{gate_name}' is not supported (supported: {supported})"
|
|
),
|
|
severity: DiagnosticSeverity::Error,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if total_qubits > self.max_qubits {
|
|
diagnostics.push(ValidationDiagnostic {
|
|
line: 1,
|
|
column: 1,
|
|
message: format!(
|
|
"circuit requires {total_qubits} qubits, exceeds local simulator limit of {}",
|
|
self.max_qubits
|
|
),
|
|
severity: DiagnosticSeverity::Error,
|
|
});
|
|
}
|
|
|
|
Ok(ValidationResult {
|
|
is_valid: diagnostics.is_empty(),
|
|
diagnostics,
|
|
num_qubits: if total_qubits > 0 { Some(total_qubits) } else { None },
|
|
num_gates: if gate_count > 0 { Some(gate_count) } else { None },
|
|
})
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 6: Implement the three AST helper functions**
|
|
|
|
Add below `validate`. Fill in match arms using the variant names from Step 3:
|
|
|
|
```rust
|
|
use oq3_semantics::asg::Stmt;
|
|
|
|
fn extract_qubit_count(stmt: &Stmt) -> Option<usize> {
|
|
// Fill in from exploration output, e.g.:
|
|
// if let Stmt::QubitDeclaration(qd) = stmt {
|
|
// return Some(qd.width().unwrap_or(1));
|
|
// }
|
|
todo!("fill in from AST exploration")
|
|
}
|
|
|
|
fn extract_gate_name(stmt: &Stmt) -> Option<String> {
|
|
// Fill in from exploration output, e.g.:
|
|
// if let Stmt::GateCall(gc) = stmt {
|
|
// return Some(gc.name().to_string().to_lowercase());
|
|
// }
|
|
todo!("fill in from AST exploration")
|
|
}
|
|
|
|
fn extract_stmt_line(stmt: &Stmt, source: &str) -> usize {
|
|
// Fill in from exploration output, e.g.:
|
|
// let offset = u32::from(stmt.span().start()) as usize;
|
|
// byte_offset_to_line_col(source, offset).0
|
|
todo!("fill in from AST exploration")
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 7: Run tests and iterate until all 5 pass**
|
|
|
|
```bash
|
|
cargo test validator::tests 2>&1
|
|
```
|
|
|
|
Common issues:
|
|
- If `program.stmts()` doesn't exist: use `cargo doc --open` or hover in rust-analyzer to find the correct method on `Program`
|
|
- If gate name extraction returns wrong case: add `.to_lowercase()`
|
|
- If `too_many_qubits` test fails: `extract_qubit_count` may be returning 0 — verify the qubit declaration variant name
|
|
|
|
- [ ] **Step 8: Run full test suite**
|
|
|
|
```bash
|
|
cargo test 2>&1
|
|
```
|
|
|
|
Expected: all tests pass (exploration + validator::tests + executor::tests from sub-plan 1).
|
|
|
|
- [ ] **Step 9: Commit**
|
|
|
|
```bash
|
|
git add src/validator.rs src/main.rs
|
|
git commit -m "feat: implement CircuitValidator with oq3_semantics"
|
|
```
|
|
|
|
---
|
|
|
|
## Final verification
|
|
|
|
```bash
|
|
cargo fmt --check && cargo clippy -- -D warnings && cargo test
|
|
```
|
|
|
|
Expected: all green. Sub-plan 2 complete — hand off to sub-plan 3.
|