Files
quantum-bridge-mcp/docs/superpowers/plans/sub2-validator.md
T
Vincent Bourdon 9af114e391 Initial import
2026-06-09 16:14:55 +02:00

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.toml with all deps
  • src/main.rs declares mod error; mod executor; mod types;
  • src/error.rsBridgeError
  • src/types.rsCircuitSource, ValidationResult, ValidationDiagnostic, DiagnosticSeverity
  • src/executor.rsSUPPORTED_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
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:

  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!()
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: 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

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.