11 KiB
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.tomlwith all depssrc/main.rsdeclaresmod error; mod executor; mod types;src/error.rs—BridgeErrorsrc/types.rs—CircuitSource,ValidationResult,ValidationDiagnostic,DiagnosticSeveritysrc/executor.rs—SUPPORTED_GATES,MAX_LOCAL_QUBITS, trait defs onlycargo testpasses
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(addmod 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
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:
mod validator;
- Step 3: Run exploration tests and read AST output
cargo test exploration -- --nocapture 2>&1 | head -300
Read the output carefully. Identify:
- How
Programexposes statements — look forstmts(),statements(), oriter() - The
Stmtenum variant name for gate calls (e.g.GateCall,Gate) - The
Stmtvariant name for qubit register declarations (e.g.QubitDeclaration,QubitDecl) - How to extract the gate name string from a gate call node
- How to extract the register size (width) from a qubit declaration node
- 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!()
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:
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:
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
cargo test validator::tests 2>&1
Common issues:
-
If
program.stmts()doesn't exist: usecargo doc --openor hover in rust-analyzer to find the correct method onProgram -
If gate name extraction returns wrong case: add
.to_lowercase() -
If
too_many_qubitstest fails:extract_qubit_countmay be returning 0 — verify the qubit declaration variant name -
Step 8: Run full test suite
cargo test 2>&1
Expected: all tests pass (exploration + validator::tests + executor::tests from sub-plan 1).
- Step 9: Commit
git add src/validator.rs src/main.rs
git commit -m "feat: implement CircuitValidator with oq3_semantics"
Final verification
cargo fmt --check && cargo clippy -- -D warnings && cargo test
Expected: all green. Sub-plan 2 complete — hand off to sub-plan 3.