Initial import

This commit is contained in:
Vincent Bourdon
2026-06-09 16:14:55 +02:00
commit 9af114e391
87 changed files with 20848 additions and 0 deletions
+358
View File
@@ -0,0 +1,358 @@
# 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.