# 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 { 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 { let parse_result = parse_source_string(&source.0, Some("circuit.qasm"), None::<&[&str]>); let mut diagnostics: Vec = 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 { // 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 { // 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.