From 6283e756cf0fc793c358bfce5a7c7f97f7ee1492 Mon Sep 17 00:00:00 2001 From: Allan Scott Date: Tue, 14 Apr 2026 05:17:49 -0400 Subject: [PATCH 1/6] Add mid-circuit measurement and classical feedforward to Q-Orca MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the mid-circuit-measurement OpenSpec change: - AST: QEffectMeasure, QEffectConditional dataclasses; new optional fields on QActionSignature (mid_circuit_measure, conditional_gate) - Parser: recognize measure(qs[N]) -> bits[M] and if bits[M] == val: Gate(qs[K]) effect syntax; list context fields parsed as QTypeList(element_type="bit") - Qiskit compiler: QuantumCircuit(n_qubits, n_bits) for classical registers; emit qc.measure() mid-circuit and with qc.if_test() feedforward blocks; BFS no longer halts at mid-circuit measurement events - QASM compiler: emit c[M] = measure q[N]; inline and if(c==val) gate q[K]; terminal measurement block suppressed when mid-circuit measurements are present - Verifier: check_mid_circuit_coherence (errors on unitary gate after measured qubit) and check_feedforward_completeness (warns on unused measurement result); _has_rule now matches custom_name for custom rules - Examples: active-teleportation.q.orca.md, bit-flip-syndrome.q.orca.md - Tests: 21 new tests covering parser, Qiskit, QASM, and verifier paths (331 passed, 1 skipped — no regressions) Co-Authored-By: Claude Sonnet 4.6 --- examples/active-teleportation.q.orca.md | 70 ++++ examples/bit-flip-syndrome.q.orca.md | 70 ++++ .../changes/mid-circuit-measurement/design.md | 44 +++ .../changes/mid-circuit-measurement/tasks.md | 65 ++++ q_orca/ast.py | 17 + q_orca/compiler/qasm.py | 43 ++- q_orca/compiler/qiskit.py | 54 ++- q_orca/parser/markdown_parser.py | 48 ++- q_orca/verifier/quantum.py | 108 +++++- tests/test_mid_circuit_measurement.py | 349 ++++++++++++++++++ 10 files changed, 852 insertions(+), 16 deletions(-) create mode 100644 examples/active-teleportation.q.orca.md create mode 100644 examples/bit-flip-syndrome.q.orca.md create mode 100644 openspec/changes/mid-circuit-measurement/design.md create mode 100644 openspec/changes/mid-circuit-measurement/tasks.md create mode 100644 tests/test_mid_circuit_measurement.py diff --git a/examples/active-teleportation.q.orca.md b/examples/active-teleportation.q.orca.md new file mode 100644 index 0000000..e209e23 --- /dev/null +++ b/examples/active-teleportation.q.orca.md @@ -0,0 +1,70 @@ +# machine ActiveTeleportation + +> Standard 3-qubit active (deterministic) quantum teleportation. +> Alice holds q0 (state to teleport) and q1 (her Bell pair qubit). +> Bob holds q2 (his Bell pair qubit). +> Mid-circuit Bell measurement on q0+q1 feeds forward X and Z corrections to q2. + +## context + +| Field | Type | Default | +|--------|-------------|---------------| +| qubits | list | [q0, q1, q2] | +| bits | list | [b0, b1] | + +## events + +- create_bell_pair +- encode_alice +- measure_alice_x +- measure_alice_z +- correct_x +- correct_z +- done + +## state |init> [initial] + +> q0 in arbitrary state |ψ⟩; q1 and q2 in |00⟩ + +## state |bell_ready> + +> Bell pair prepared between q1 and q2: (|00⟩ + |11⟩)/√2 + +## state |alice_encoded> + +> Alice applied CNOT(q0,q1) and H(q0) to entangle her qubit with the channel + +## state |measured> + +> Mid-circuit Bell measurement complete: b0 = measure(q0), b1 = measure(q1) + +## state |teleported> [final] + +> Bob's qubit q2 holds |ψ⟩ after X and Z feedforward corrections + +## transitions + +| Source | Event | Guard | Target | Action | +|-----------------|-------------------|-------|------------------|---------------| +| |init> | create_bell_pair | | |bell_ready> | make_bell | +| |bell_ready> | encode_alice | | |alice_encoded> | encode_alice | +| |alice_encoded> | measure_alice_x | | |measured> | meas_q0 | +| |measured> | measure_alice_z | | |measured> | meas_q1 | +| |measured> | correct_x | | |measured> | feedfwd_x | +| |measured> | correct_z | | |teleported> | feedfwd_z | + +## actions + +| Name | Signature | Effect | +|--------------|----------------|------------------------------------| +| make_bell | (qs) -> qs | Hadamard(qs[1]); CNOT(qs[1], qs[2]) | +| encode_alice | (qs) -> qs | CNOT(qs[0], qs[1]); Hadamard(qs[0]) | +| meas_q0 | (qs) -> qs | measure(qs[0]) -> bits[0] | +| meas_q1 | (qs) -> qs | measure(qs[1]) -> bits[1] | +| feedfwd_x | (qs) -> qs | if bits[1] == 1: X(qs[2]) | +| feedfwd_z | (qs) -> qs | if bits[0] == 1: Z(qs[2]) | + +## verification rules + +- mid_circuit_coherence: q0 and q1 are not reused after mid-circuit measurement +- feedforward_completeness: both measured bits drive correction gates on Bob's qubit diff --git a/examples/bit-flip-syndrome.q.orca.md b/examples/bit-flip-syndrome.q.orca.md new file mode 100644 index 0000000..c981965 --- /dev/null +++ b/examples/bit-flip-syndrome.q.orca.md @@ -0,0 +1,70 @@ +# machine BitFlipSyndrome + +> 5-qubit bit-flip syndrome circuit: 3 data qubits (q0–q2) + 2 ancilla (q3, q4). +> Measure two syndrome bits mid-circuit; apply X corrections conditioned on +> the syndrome results. This demonstrates mid-circuit measurement and classical +> feedforward in Q-Orca. + +## context + +| Field | Type | Default | +|--------|-------------|-----------------------| +| qubits | list | [q0, q1, q2, q3, q4] | +| bits | list | [b0, b1] | + +## events + +- entangle +- measure_s0 +- measure_s1 +- correct_q0 +- correct_q2 + +## state |init> [initial] + +> Data qubits in |000⟩, ancilla in |00⟩ + +## state |entangled> + +> Ancilla qubits entangled with data for syndrome extraction + +## state |s0_measured> + +> First syndrome bit captured: bits[0] = measure(q3) + +## state |s1_measured> + +> Second syndrome bit captured: bits[1] = measure(q4) + +## state |q0_corrected> + +> X correction applied to q0 if bits[0] == 1 + +## state |corrected> [final] + +> Both corrections applied; logical qubit restored + +## transitions + +| Source | Event | Guard | Target | Action | +|-----------------|------------|-------|-----------------|---------------| +| |init> | entangle | | |entangled> | entangle_data | +| |entangled> | measure_s0 | | |s0_measured> | measure_s0 | +| |s0_measured> | measure_s1 | | |s1_measured> | measure_s1 | +| |s1_measured> | correct_q0 | | |q0_corrected> | correct_q0 | +| |q0_corrected> | correct_q2 | | |corrected> | correct_q2 | + +## actions + +| Name | Signature | Effect | +|---------------|----------------|--------------------------------------------------------------| +| entangle_data | (qs) -> qs | CNOT(qs[0], qs[3]); CNOT(qs[1], qs[3]); CNOT(qs[1], qs[4]); CNOT(qs[2], qs[4]) | +| measure_s0 | (qs) -> qs | measure(qs[3]) -> bits[0] | +| measure_s1 | (qs) -> qs | measure(qs[4]) -> bits[1] | +| correct_q0 | (qs) -> qs | if bits[0] == 1: X(qs[0]) | +| correct_q2 | (qs) -> qs | if bits[1] == 1: X(qs[2]) | + +## verification rules + +- mid_circuit_coherence: ancilla qubits q3 and q4 are not reused after measurement +- feedforward_completeness: every syndrome measurement drives a correction gate diff --git a/openspec/changes/mid-circuit-measurement/design.md b/openspec/changes/mid-circuit-measurement/design.md new file mode 100644 index 0000000..477cdaa --- /dev/null +++ b/openspec/changes/mid-circuit-measurement/design.md @@ -0,0 +1,44 @@ +## Context + +End-of-circuit measurement already exists (`Measurement` AST node, parsed from `measure(qs[N])`). Mid-circuit measurement requires measuring a qubit into a classical bit register *during* the circuit, then using that classical bit to conditionally apply a gate. The existing `QActionSignature` stores at most one `gate` and one `measurement` per action — this design adds two new optional fields for the two new effect kinds. + +## Goals / Non-Goals + +**Goals:** +- Add `list` as a recognised context field type (parse `list` → `QTypeList(element_type="bit")`) +- Parse `measure(qs[N]) -> bits[M]` effect → `QEffectMeasure(qubit_idx=N, bit_idx=M)` +- Parse `if bits[M] == val: Gate(qs[K])` effect → `QEffectConditional(bit_idx=M, value=val, gate=...)` +- Store both on `QActionSignature` as optional fields +- Qiskit compiler: use `QuantumCircuit(n_qubits, n_bits)` and emit `qc.measure()` + `with qc.if_test(...):` +- QASM compiler: emit `c[M] = measure q[N];` inline and `if(c==val) gate q[K];` +- Verifier: `MidCircuitCoherenceRule` (no unitary after unmeasured mid-circuit qubit) and `FeedforwardCompletenessRule` (every measure result is used) +- Examples: `bit-flip-syndrome.q.orca.md`, `active-teleportation.q.orca.md` + +**Non-Goals:** +- Multi-qubit classical register operations or arithmetic on bit results +- Reset gates (`reset q[N]`) — deferred to a follow-on +- Dynamic repetition / while-measure loops +- Noise model changes for mid-circuit measurement + +## Decisions + +**Decision: Two new fields on `QActionSignature` rather than a polymorphic effect list** +Adding `mid_circuit_measure: Optional[QEffectMeasure]` and `conditional_gate: Optional[QEffectConditional]` follows the same pattern as the existing `gate` and `measurement` fields. It avoids a larger refactor of the action/effect pipeline while keeping the new semantics visible at the AST level. + +**Decision: `measure(qs[N]) -> bits[M]` is parsed separately from the terminal `measure(qs[N])`** +The existing `_parse_measurement_from_effect` matches `measure(qs[N])` without an arrow. The new `_parse_mid_circuit_measure_from_effect` matches only the arrow form, so neither parser conflicts with the other. + +**Decision: Qiskit dynamic circuits via `qc.if_test` (OpenQASM 3 style)** +IBM's `qc.if_test((clbit, val))` context manager is the current Qiskit idiom for classical feedforward. It maps cleanly to the `with qc.if_test(...):` pattern and works without importing extra packages beyond `qiskit`. + +**Decision: QASM uses OpenQASM 2 `if()` syntax** +The proposal mentions OpenQASM 3 `measure q[N] -> c[M];` style but `stdgates.inc` targets QASM 3. To keep the QASM output runnable on the widest set of simulators, we emit `c[M] = measure q[N];` (QASM 3 assignment style) and `if(c==val) gate q[K];` (QASM 2 if-clause, which is also accepted by many QASM 3 parsers). + +**Decision: Bit count inferred from `list` context fields** +`_infer_bit_count` inspects context fields whose `QTypeList.element_type == "bit"` and counts them from the default value (e.g. `[b0, b1]` → 2). If no `list` field exists, bit count defaults to 0 (no classical register). + +## Risks / Trade-offs + +- **`qc.if_test` requires Qiskit ≥ 0.45** — accepted; older Qiskit already fails on other features +- **QASM `if()` clause targets QASM 2 semantics** — a future QASM 3 upgrade pass can switch to `if (c[M] == val) { ... }` style +- **Verifier checks are conservative** — `MidCircuitCoherenceRule` only fires if it can statically prove a qubit is used after measurement; it will miss dynamic paths involving guards diff --git a/openspec/changes/mid-circuit-measurement/tasks.md b/openspec/changes/mid-circuit-measurement/tasks.md new file mode 100644 index 0000000..227039a --- /dev/null +++ b/openspec/changes/mid-circuit-measurement/tasks.md @@ -0,0 +1,65 @@ +## 1. AST + +- [x] 1.1 Add `QEffectMeasure` and `QEffectConditional` dataclasses to `q_orca/ast.py`; + extend `QActionSignature` with `mid_circuit_measure: Optional[QEffectMeasure] = None` + and `conditional_gate: Optional[QEffectConditional] = None` + +## 2. Parser + +- [x] 2.1 Add `_parse_mid_circuit_measure_from_effect(effect_str)` in + `q_orca/parser/markdown_parser.py` — matches `measure(qs[N]) -> bits[M]`, + returns `QEffectMeasure(qubit_idx=N, bit_idx=M)`; also ensure `list` + is parsed as `QTypeList(element_type="bit")` (add `"bit"` as recognized element type + in `_parse_q_type_string`) +- [x] 2.2 Add `_parse_conditional_gate_from_effect(effect_str)` — matches + `if bits[M] == val: Gate(qs[K])`, returns + `QEffectConditional(bit_idx=M, value=val, gate=QuantumGate(...))` +- [x] 2.3 Call both helpers in `_parse_actions_table` and store results on + `QActionSignature.mid_circuit_measure` and `QActionSignature.conditional_gate` + +## 3. Qiskit compiler + +- [x] 3.1 Add `_infer_bit_count(machine)` helper in `q_orca/compiler/qiskit.py` that + returns the classical bit count from `list` context fields (count items in + default value like `[b0, b1]`) +- [x] 3.2 In `compile_to_qiskit`, switch from `QuantumCircuit({n})` to + `QuantumCircuit({n_qubits}, {n_bits})` when `n_bits > 0`; in the gate emission + loop, emit `qc.measure(N, M)` for `mid_circuit_measure` actions and + `with qc.if_test((qc.clbits[M], val)):\n qc.(K)` for + `conditional_gate` actions + +## 4. QASM compiler + +- [x] 4.1 In `compile_to_qasm` (`q_orca/compiler/qasm.py`), emit + `c[M] = measure q[N];` inline (in the gate sequence loop) for + `mid_circuit_measure` actions, and `if(c==val) q[K];` for + `conditional_gate` actions; also declare `bit[n_bits] c;` when the machine has + mid-circuit measurements + +## 5. Verifier + +- [x] 5.1 Add `check_mid_circuit_coherence(machine)` in `q_orca/verifier/quantum.py` — + activated by `"mid_circuit_coherence"` rule; walk the BFS gate sequence and + error if any action applies a unitary gate to a qubit that a prior action + already measured mid-circuit (uses `mid_circuit_measure`) +- [x] 5.2 Add `check_feedforward_completeness(machine)` — activated by + `"feedforward_completeness"` rule; warn if the machine has actions with + `mid_circuit_measure` but no action with `conditional_gate` referencing that + bit index +- [x] 5.3 Register both checks in `verify_quantum` so they run automatically + +## 6. Examples and tests + +- [x] 6.1 Add `examples/bit-flip-syndrome.q.orca.md` — 5-qubit bit-flip syndrome + circuit: 3 data qubits + 2 ancilla; prepare |000>, measure two syndromes + mid-circuit into `bits`, apply corrections conditioned on syndrome values +- [x] 6.2 Add `examples/active-teleportation.q.orca.md` — 3-qubit active teleportation: + prepare Bell pair, mid-circuit Bell measurement on data + Alice qubit, feedforward + X and Z corrections on Bob's qubit +- [x] 6.3 Create `tests/test_mid_circuit_measurement.py` covering: + parser round-trip for `QEffectMeasure` and `QEffectConditional`, + Qiskit emission with `qc.measure` and `qc.if_test`, + QASM emission with `c[M] = measure` and `if(c==val)`, + and verifier acceptance of well-formed machines +- [x] 6.4 Run `pytest tests/test_mid_circuit_measurement.py` and confirm all tests pass; + run full `pytest` suite and confirm no regressions diff --git a/q_orca/ast.py b/q_orca/ast.py index be8b6f3..9fb4b32 100644 --- a/q_orca/ast.py +++ b/q_orca/ast.py @@ -194,6 +194,21 @@ class ValueRef: value: any = None +@dataclass +class QEffectMeasure: + """Mid-circuit measurement: measure qubit N into classical bit M.""" + qubit_idx: int + bit_idx: int + + +@dataclass +class QEffectConditional: + """Classical feedforward: if bits[M] == val, apply gate to qubit K.""" + bit_idx: int + value: int # 0 or 1 + gate: QuantumGate + + @dataclass class QActionSignature: name: str @@ -204,6 +219,8 @@ class QActionSignature: effect_type: Optional[str] = None gate: Optional[QuantumGate] = None measurement: Optional[Measurement] = None + mid_circuit_measure: Optional[QEffectMeasure] = None + conditional_gate: Optional[QEffectConditional] = None @dataclass diff --git a/q_orca/compiler/qasm.py b/q_orca/compiler/qasm.py index 9a37f93..a82aa80 100644 --- a/q_orca/compiler/qasm.py +++ b/q_orca/compiler/qasm.py @@ -3,7 +3,7 @@ import re from q_orca.ast import QMachineDef, QuantumGate, QTypeScalar, QTypeList, QTypeQubit -from q_orca.compiler.qiskit import _parse_effect_string +from q_orca.compiler.qiskit import _parse_effect_string, _infer_bit_count def compile_to_qasm(machine: QMachineDef) -> str: @@ -16,14 +16,19 @@ def compile_to_qasm(machine: QMachineDef) -> str: lines.append("") qubit_count = _infer_qubit_count(machine) + bit_count = _infer_bit_count(machine) lines.append(f"qubit[{qubit_count}] q;") has_measurement = any(a.measurement for a in machine.actions) + has_mid_circuit = any(a.mid_circuit_measure is not None for a in machine.actions) has_measure_event = any( "measure" in e.name.lower() or "collapse" in e.name.lower() for e in machine.events ) - if has_measurement or has_measure_event: + if has_mid_circuit and bit_count > 0: + lines.append(f"bit[{bit_count}] c;") + lines.append("") + elif has_measurement or has_measure_event: lines.append(f"bit[{qubit_count}] c;") lines.append("") @@ -34,15 +39,27 @@ def compile_to_qasm(machine: QMachineDef) -> str: if any(hasattr(f.type, "kind") and f.type.kind == "int" for f in machine.context): lines.append("") + action_map = {a.name: a for a in machine.actions} gate_sequence = _extract_gate_sequence(machine) lines.append("// Gate sequence derived from state machine transitions") for action_name, gates, comment in gate_sequence: if comment: lines.append(f"// {comment}") - for gate in gates: - lines.append(_gate_to_qasm(gate, qubit_count)) - - if has_measurement or has_measure_event: + action = action_map.get(action_name) + if action and action.mid_circuit_measure is not None: + mcm = action.mid_circuit_measure + lines.append(f"c[{mcm.bit_idx}] = measure q[{mcm.qubit_idx}];") + elif action and action.conditional_gate is not None: + cg = action.conditional_gate + gate_str = _gate_to_qasm(cg.gate, qubit_count).rstrip(";") + lines.append(f"if(c=={cg.value}) {gate_str};") + else: + for gate in gates: + lines.append(_gate_to_qasm(gate, qubit_count)) + + # Emit terminal measurement block only when there are no mid-circuit + # measurements (which are emitted inline in the gate sequence above). + if not has_mid_circuit and (has_measurement or has_measure_event): lines.append("") lines.append("// Measurement") for i in range(qubit_count): @@ -82,8 +99,18 @@ def _extract_gate_sequence(machine: QMachineDef) -> list: gates = [action.gate] steps.append((t.action, gates, f"{t.source} -> {t.target} via {t.event}")) - is_measure = "measure" in t.event.lower() or "collapse" in t.event.lower() - if not is_measure and t.target not in visited: + # Continue BFS past mid-circuit measurement events; stop only at + # terminal (end-of-circuit) measurement events. + transition_action = action_map.get(t.action) if t.action else None + is_mid_circuit = ( + transition_action is not None + and transition_action.mid_circuit_measure is not None + ) + is_terminal_measure = ( + ("measure" in t.event.lower() or "collapse" in t.event.lower()) + and not is_mid_circuit + ) + if not is_terminal_measure and t.target not in visited: queue.append(t.target) return steps diff --git a/q_orca/compiler/qiskit.py b/q_orca/compiler/qiskit.py index f13a460..39cb177 100644 --- a/q_orca/compiler/qiskit.py +++ b/q_orca/compiler/qiskit.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from q_orca.angle import evaluate_angle -from q_orca.ast import QMachineDef, QuantumGate, QTypeQubit, QTypeScalar, QTypeList, NoiseModel +from q_orca.ast import QMachineDef, QuantumGate, QTypeQubit, QTypeScalar, QTypeList, NoiseModel, QEffectMeasure, QEffectConditional def _parse_effect_string(effect_str: str) -> list[QuantumGate]: @@ -234,8 +234,13 @@ def compile_to_qiskit(machine: QMachineDef, options: QSimulationOptions) -> str: lines.append("") qubit_count = _infer_qubit_count(machine) + bit_count = _infer_bit_count(machine) lines.append(f"qubit_count = {qubit_count}") - lines.append(f"qc = QuantumCircuit({qubit_count})") + if bit_count > 0: + lines.append(f"bit_count = {bit_count}") + lines.append(f"qc = QuantumCircuit({qubit_count}, {bit_count})") + else: + lines.append(f"qc = QuantumCircuit({qubit_count})") lines.append("") # Noise model from context @@ -257,11 +262,20 @@ def compile_to_qiskit(machine: QMachineDef, options: QSimulationOptions) -> str: gate_sequence = _extract_gate_sequence(machine) + action_map = {a.name: a for a in machine.actions} lines.append("# Gate sequence from state machine") for action_name, gates, comment in gate_sequence: if comment: lines.append(f"# {comment}") - if gates: + action = action_map.get(action_name) + if action and action.mid_circuit_measure is not None: + mcm = action.mid_circuit_measure + lines.append(f"qc.measure({mcm.qubit_idx}, {mcm.bit_idx})") + elif action and action.conditional_gate is not None: + cg = action.conditional_gate + lines.append(f"with qc.if_test((qc.clbits[{cg.bit_idx}], {cg.value})):") + lines.append(f" {_gate_to_qiskit(cg.gate)}") + elif gates: for gate in gates: lines.append(_gate_to_qiskit(gate)) @@ -398,8 +412,18 @@ def _extract_gate_sequence(machine: QMachineDef) -> list: # Append (action_name, [gates], comment) for each action steps.append((t.action, gates, comment)) - is_measure = "measure" in t.event.lower() or "collapse" in t.event.lower() - if not is_measure and t.target not in visited: + # Stop BFS at terminal (end-of-circuit) measurement events, but + # continue past mid-circuit measurement events. + transition_action = action_map.get(t.action) if t.action else None + is_mid_circuit = ( + transition_action is not None + and transition_action.mid_circuit_measure is not None + ) + is_terminal_measure = ( + ("measure" in t.event.lower() or "collapse" in t.event.lower()) + and not is_mid_circuit + ) + if not is_terminal_measure and t.target not in visited: queue.append(t.target) return steps @@ -460,6 +484,26 @@ def _gate_to_qiskit(gate: QuantumGate) -> str: return f"# unknown gate: {gate.kind}" +def _infer_bit_count(machine: QMachineDef) -> int: + """Count classical bits from list context fields.""" + for field in machine.context: + if isinstance(field.type, QTypeList) and field.type.element_type == "bit": + if field.default_value: + items = re.findall(r"b\d+", field.default_value) + if items: + return len(items) + # No default — count from mid_circuit_measure actions + break + # Infer bit count from max bit_idx in mid_circuit_measure actions + max_bit = -1 + for action in machine.actions: + if action.mid_circuit_measure is not None: + max_bit = max(max_bit, action.mid_circuit_measure.bit_idx) + if action.conditional_gate is not None: + max_bit = max(max_bit, action.conditional_gate.bit_idx) + return max_bit + 1 if max_bit >= 0 else 0 + + def _infer_qubit_count(machine: QMachineDef) -> int: # First, try to infer from context fields n_value = None diff --git a/q_orca/parser/markdown_parser.py b/q_orca/parser/markdown_parser.py index 1e784ef..dd7b44b 100644 --- a/q_orca/parser/markdown_parser.py +++ b/q_orca/parser/markdown_parser.py @@ -13,7 +13,7 @@ QType, QTypeQubit, QTypeList, QTypeScalar, QTypeOptional, QTypeCustom, QGuardRef, QuantumGate, Measurement, CollapseOutcome, QGuardTrue, QGuardFalse, QGuardCompare, QGuardProbability, QGuardFidelity, - VariableRef, ValueRef, + VariableRef, ValueRef, QEffectMeasure, QEffectConditional, ) @@ -421,6 +421,8 @@ def _parse_actions_table(table: MdTable, errors: list[str] | None = None) -> lis params, return_type = _parse_signature(sig_str) gate = _parse_gate_from_effect(effect_str, errors, action_name=name) measurement = _parse_measurement_from_effect(effect_str) + mid_circuit_measure = _parse_mid_circuit_measure_from_effect(effect_str) + conditional_gate = _parse_conditional_gate_from_effect(effect_str, errors, action_name=name) actions.append(QActionSignature( name=name, @@ -430,6 +432,8 @@ def _parse_actions_table(table: MdTable, errors: list[str] | None = None) -> lis has_effect=bool(effect_str), gate=gate, measurement=measurement, + mid_circuit_measure=mid_circuit_measure, + conditional_gate=conditional_gate, )) return actions @@ -548,7 +552,8 @@ def _parse_q_type_string(text: str) -> QType: return QTypeOptional(inner_type=text[:-1]) list_match = re.match(r"^list<\s*(.+)\s*>$", text) if list_match: - return QTypeList(element_type=list_match.group(1)) + element = list_match.group(1).strip() + return QTypeList(element_type=element) if text == "qubit": return QTypeQubit() scalar_map = {"int": "int", "float": "float", "decimal": "float", @@ -723,9 +728,48 @@ def _parse_measurement_from_effect(effect_str: str) -> Optional[Measurement]: if not effect_str: return None # Match 'measure(q[i])' (standard) OR 'M(q[i])' (custom quantum notation). + # Skip mid-circuit form 'measure(qs[N]) -> bits[M]' — that is handled separately. + if re.search(r"measure\s*\(.*\)\s*->", effect_str, re.IGNORECASE): + return None # 'M' is the conventional single-letter symbol for measurement in quantum circuits. m = re.search(r"(?:measure|M)\(\s*\w+\[([^\]]+)\]\s*\)", effect_str, re.IGNORECASE) if m: indices = [int(x.strip()) for x in m.group(1).split(",")] return Measurement(qubits=indices, basis="computational") return None + + +def _parse_mid_circuit_measure_from_effect(effect_str: str) -> Optional[QEffectMeasure]: + """Parse 'measure(qs[N]) -> bits[M]' into QEffectMeasure(qubit_idx=N, bit_idx=M).""" + if not effect_str: + return None + m = re.search( + r"measure\s*\(\s*\w+\[(\d+)\]\s*\)\s*->\s*bits\[(\d+)\]", + effect_str, re.IGNORECASE, + ) + if m: + return QEffectMeasure(qubit_idx=int(m.group(1)), bit_idx=int(m.group(2))) + return None + + +def _parse_conditional_gate_from_effect( + effect_str: str, + errors: list[str] | None = None, + action_name: str = "", +) -> Optional[QEffectConditional]: + """Parse 'if bits[M] == val: Gate(qs[K])' into QEffectConditional.""" + if not effect_str: + return None + m = re.match( + r"if\s+bits\[(\d+)\]\s*==\s*(\d+)\s*:\s*(.+)$", + effect_str.strip(), re.IGNORECASE, + ) + if not m: + return None + bit_idx = int(m.group(1)) + value = int(m.group(2)) + gate_str = m.group(3).strip() + gate = _parse_gate_from_effect(gate_str, errors=errors, action_name=action_name) + if gate is None: + return None + return QEffectConditional(bit_idx=bit_idx, value=value, gate=gate) diff --git a/q_orca/verifier/quantum.py b/q_orca/verifier/quantum.py index e260bd4..af62f48 100644 --- a/q_orca/verifier/quantum.py +++ b/q_orca/verifier/quantum.py @@ -22,7 +22,10 @@ def _has_rule(machine: QMachineDef, kind: str) -> bool: return any( - r.kind == kind or r.kind == kind.replace("_", "-") + r.kind == kind + or r.kind == kind.replace("_", "-") + or r.custom_name == kind + or r.custom_name == kind.replace("_", "-") for r in machine.verification_rules ) @@ -243,12 +246,115 @@ def check_collapse_completeness(machine: QMachineDef) -> QVerificationResult: ) +def check_mid_circuit_coherence(machine: QMachineDef) -> QVerificationResult: + """Activated by 'mid_circuit_coherence' rule. + + Errors if any action applies a unitary gate to a qubit that was already + measured mid-circuit by a prior action in the BFS gate sequence (without a + reset in between). + """ + errors: list[QVerificationError] = [] + + if not _has_rule(machine, "mid_circuit_coherence"): + return QVerificationResult(valid=True, errors=errors) + + action_map = {a.name: a for a in machine.actions} + measured_qubits: set[int] = set() + + initial = next((s for s in machine.states if s.is_initial), None) + if not initial: + return QVerificationResult(valid=True, errors=errors) + + visited: set[str] = set() + queue = [initial.name] + + while queue: + current = queue.pop(0) + if current in visited: + continue + visited.add(current) + + for t in machine.transitions: + if t.source != current: + continue + if t.action: + action = action_map.get(t.action) + if action: + if action.mid_circuit_measure is not None: + measured_qubits.add(action.mid_circuit_measure.qubit_idx) + elif action.gate is not None: + for idx in action.gate.targets: + if idx in measured_qubits: + errors.append(QVerificationError( + code="MID_CIRCUIT_COHERENCE_VIOLATION", + message=( + f"Action '{action.name}' applies gate " + f"'{action.gate.kind}' to qubit {idx} which " + "was already measured mid-circuit without a reset" + ), + severity="error", + location={"action": action.name}, + suggestion="Add a reset gate before reusing a measured qubit, or use a fresh qubit", + )) + + is_measure = "measure" in t.event.lower() or "collapse" in t.event.lower() + if not is_measure and t.target not in visited: + queue.append(t.target) + + return QVerificationResult( + valid=not any(e.severity == "error" for e in errors), + errors=errors, + ) + + +def check_feedforward_completeness(machine: QMachineDef) -> QVerificationResult: + """Activated by 'feedforward_completeness' rule. + + Warns if the machine has mid-circuit measurements but no conditional gate + (feedforward) action uses the measured bit — i.e. the measurement result + is discarded. + """ + errors: list[QVerificationError] = [] + + if not _has_rule(machine, "feedforward_completeness"): + return QVerificationResult(valid=True, errors=errors) + + measured_bits: set[int] = set() + feedforward_bits: set[int] = set() + + for action in machine.actions: + if action.mid_circuit_measure is not None: + measured_bits.add(action.mid_circuit_measure.bit_idx) + if action.conditional_gate is not None: + feedforward_bits.add(action.conditional_gate.bit_idx) + + unused = measured_bits - feedforward_bits + for bit_idx in sorted(unused): + errors.append(QVerificationError( + code="FEEDFORWARD_UNUSED", + message=( + f"Classical bit {bit_idx} is written by a mid-circuit measurement " + "but never read by a conditional gate (feedforward unused)" + ), + severity="warning", + location={"bit_idx": bit_idx}, + suggestion="Add an 'if bits[M] == val: Gate(qs[K])' action that consumes this measurement result", + )) + + return QVerificationResult( + valid=not any(e.severity == "error" for e in errors), + errors=errors, + ) + + def verify_quantum(machine: QMachineDef) -> QVerificationResult: results = [ check_unitarity(machine), check_no_cloning(machine), check_entanglement(machine), check_collapse_completeness(machine), + check_mid_circuit_coherence(machine), + check_feedforward_completeness(machine), ] all_errors = [e for r in results for e in r.errors] return QVerificationResult( diff --git a/tests/test_mid_circuit_measurement.py b/tests/test_mid_circuit_measurement.py new file mode 100644 index 0000000..b76eb4f --- /dev/null +++ b/tests/test_mid_circuit_measurement.py @@ -0,0 +1,349 @@ +"""Tests for mid-circuit measurement and classical feedforward (mid-circuit-measurement change).""" + +import pytest + +from q_orca.ast import QEffectMeasure, QEffectConditional, QTypeList +from q_orca.parser.markdown_parser import parse_q_orca_markdown +from q_orca.compiler.qiskit import compile_to_qiskit, QSimulationOptions, _infer_bit_count +from q_orca.compiler.qasm import compile_to_qasm +from q_orca.verifier.quantum import ( + check_mid_circuit_coherence, + check_feedforward_completeness, +) + + +# --------------------------------------------------------------------------- +# Minimal machine fixture used across multiple tests +# --------------------------------------------------------------------------- + +MINIMAL_MCM_MACHINE = """\ +# machine MinimalMCM + +## context + +| Field | Type | Default | +|--------|------------|--------------| +| qubits | list | [q0, q1] | +| bits | list | [b0] | + +## events + +- prepare +- measure_mid +- correct +- done + +## state |ready> [initial] + +## state |measured> + +## state |done> [final] + +## transitions + +| Source | Event | Guard | Target | Action | +|------------|-------------|-------|-------------|-------------| +| |ready> | prepare | | |ready> | apply_h | +| |ready> | measure_mid | | |measured> | meas_q0 | +| |measured> | correct | | |done> | corr_q1 | + +## actions + +| Name | Signature | Effect | +|---------|------------|-----------------------------| +| apply_h | (qs) -> qs | Hadamard(qs[0]) | +| meas_q0 | (qs) -> qs | measure(qs[0]) -> bits[0] | +| corr_q1 | (qs) -> qs | if bits[0] == 1: X(qs[1]) | + +## verification rules + +- mid_circuit_coherence: q0 is not reused after measurement +- feedforward_completeness: bit 0 drives an X correction on q1 +""" + + +# --------------------------------------------------------------------------- +# Parser tests +# --------------------------------------------------------------------------- + +class TestParser: + def _parse(self, source: str): + result = parse_q_orca_markdown(source) + assert not result.errors, f"Parse errors: {result.errors}" + return result.file.machines[0] + + def test_list_bit_type_parsed(self): + machine = self._parse(MINIMAL_MCM_MACHINE) + bit_field = next((f for f in machine.context if f.name == "bits"), None) + assert bit_field is not None + assert isinstance(bit_field.type, QTypeList) + assert bit_field.type.element_type == "bit" + + def test_mid_circuit_measure_parsed(self): + machine = self._parse(MINIMAL_MCM_MACHINE) + action = next((a for a in machine.actions if a.name == "meas_q0"), None) + assert action is not None + assert action.mid_circuit_measure is not None + mcm = action.mid_circuit_measure + assert isinstance(mcm, QEffectMeasure) + assert mcm.qubit_idx == 0 + assert mcm.bit_idx == 0 + + def test_mid_circuit_measure_does_not_also_set_terminal_measurement(self): + """measure(qs[N]) -> bits[M] should not create a terminal Measurement node.""" + machine = self._parse(MINIMAL_MCM_MACHINE) + action = next((a for a in machine.actions if a.name == "meas_q0"), None) + assert action is not None + assert action.measurement is None + + def test_conditional_gate_parsed(self): + machine = self._parse(MINIMAL_MCM_MACHINE) + action = next((a for a in machine.actions if a.name == "corr_q1"), None) + assert action is not None + assert action.conditional_gate is not None + cg = action.conditional_gate + assert isinstance(cg, QEffectConditional) + assert cg.bit_idx == 0 + assert cg.value == 1 + assert cg.gate.kind == "X" + assert cg.gate.targets == [1] + + def test_regular_actions_unaffected(self): + machine = self._parse(MINIMAL_MCM_MACHINE) + action = next((a for a in machine.actions if a.name == "apply_h"), None) + assert action is not None + assert action.mid_circuit_measure is None + assert action.conditional_gate is None + assert action.gate is not None + assert action.gate.kind == "H" + + def test_active_teleportation_example_parses(self): + with open("examples/active-teleportation.q.orca.md") as f: + src = f.read() + result = parse_q_orca_markdown(src) + assert not result.errors, f"Parse errors: {result.errors}" + machine = result.file.machines[0] + # Should have two mid-circuit measurements and two conditional gates + mcm_actions = [a for a in machine.actions if a.mid_circuit_measure is not None] + cg_actions = [a for a in machine.actions if a.conditional_gate is not None] + assert len(mcm_actions) == 2 + assert len(cg_actions) == 2 + + def test_bit_flip_syndrome_example_parses(self): + with open("examples/bit-flip-syndrome.q.orca.md") as f: + src = f.read() + result = parse_q_orca_markdown(src) + assert not result.errors, f"Parse errors: {result.errors}" + machine = result.file.machines[0] + mcm_actions = [a for a in machine.actions if a.mid_circuit_measure is not None] + cg_actions = [a for a in machine.actions if a.conditional_gate is not None] + assert len(mcm_actions) == 2 + assert len(cg_actions) == 2 + + +# --------------------------------------------------------------------------- +# Qiskit compiler tests +# --------------------------------------------------------------------------- + +class TestQiskitCompiler: + def _machine(self): + result = parse_q_orca_markdown(MINIMAL_MCM_MACHINE) + return result.file.machines[0] + + def test_infer_bit_count(self): + machine = self._machine() + assert _infer_bit_count(machine) == 1 + + def test_circuit_has_classical_register(self): + machine = self._machine() + code = compile_to_qiskit(machine, QSimulationOptions(analytic=True, skip_qutip=True)) + assert "QuantumCircuit(2, 1)" in code + + def test_mid_circuit_measure_emitted(self): + machine = self._machine() + code = compile_to_qiskit(machine, QSimulationOptions(analytic=True, skip_qutip=True)) + assert "qc.measure(0, 0)" in code + + def test_if_test_emitted(self): + machine = self._machine() + code = compile_to_qiskit(machine, QSimulationOptions(analytic=True, skip_qutip=True)) + assert "qc.if_test(" in code + assert "qc.clbits[0]" in code + + def test_no_classical_register_without_bits(self): + source = """\ +# machine NoBits + +## state |0> [initial] +## state |1> [final] + +## transitions + +| Source | Event | Guard | Target | Action | +|--------|---------|-------|--------|--------| +| |0> | prepare | | |1> | apply_h | + +## actions + +| Name | Signature | Effect | +|---------|------------|-----------------| +| apply_h | (qs) -> qs | Hadamard(qs[0]) | +""" + result = parse_q_orca_markdown(source) + machine = result.file.machines[0] + code = compile_to_qiskit(machine, QSimulationOptions(analytic=True, skip_qutip=True)) + # Should be QuantumCircuit(N) not QuantumCircuit(N, M) + assert "QuantumCircuit(1, " not in code + + +# --------------------------------------------------------------------------- +# QASM compiler tests +# --------------------------------------------------------------------------- + +class TestQasmCompiler: + def _machine(self): + result = parse_q_orca_markdown(MINIMAL_MCM_MACHINE) + return result.file.machines[0] + + def test_bit_register_declared(self): + machine = self._machine() + code = compile_to_qasm(machine) + assert "bit[1] c;" in code + + def test_mid_circuit_measure_emitted(self): + machine = self._machine() + code = compile_to_qasm(machine) + assert "c[0] = measure q[0];" in code + + def test_conditional_gate_emitted(self): + machine = self._machine() + code = compile_to_qasm(machine) + assert "if(c==1)" in code + assert "x q[1]" in code + + def test_active_teleportation_qasm(self): + with open("examples/active-teleportation.q.orca.md") as f: + src = f.read() + result = parse_q_orca_markdown(src) + machine = result.file.machines[0] + code = compile_to_qasm(machine) + # Two mid-circuit measurements + assert "c[0] = measure q[0];" in code + assert "c[1] = measure q[1];" in code + # Two conditional corrections + assert "if(c==1)" in code + + +# --------------------------------------------------------------------------- +# Verifier tests +# --------------------------------------------------------------------------- + +class TestVerifier: + def _machine(self): + result = parse_q_orca_markdown(MINIMAL_MCM_MACHINE) + return result.file.machines[0] + + def test_coherence_passes_valid_machine(self): + machine = self._machine() + result = check_mid_circuit_coherence(machine) + assert result.valid + assert not any(e.severity == "error" for e in result.errors) + + def test_feedforward_completeness_passes_valid_machine(self): + machine = self._machine() + result = check_feedforward_completeness(machine) + assert result.valid + + def test_coherence_inactive_without_rule(self): + """Without the mid_circuit_coherence rule, the check is skipped.""" + source = """\ +# machine NoCohRule + +## state |0> [initial] +## state |1> [final] + +## transitions + +| Source | Event | Guard | Target | Action | +|--------|-------|-------|--------|--------| +| |0> | go | | |1> | meas | + +## actions + +| Name | Signature | Effect | +|------|------------|----------------------------| +| meas | (qs) -> qs | measure(qs[0]) -> bits[0] | +""" + result = parse_q_orca_markdown(source) + machine = result.file.machines[0] + vr = check_mid_circuit_coherence(machine) + assert vr.valid # rule not declared → skipped + + def test_feedforward_completeness_warns_on_unused_bit(self): + source = """\ +# machine UnusedBit + +## context + +| Field | Type | Default | +|-------|-----------|---------| +| bits | list | [b0] | + +## state |0> [initial] +## state |1> [final] + +## transitions + +| Source | Event | Guard | Target | Action | +|--------|-------|-------|--------|--------| +| |0> | go | | |1> | meas | + +## actions + +| Name | Signature | Effect | +|------|------------|----------------------------| +| meas | (qs) -> qs | measure(qs[0]) -> bits[0] | + +## verification rules + +- feedforward_completeness: bit 0 should be used +""" + result = parse_q_orca_markdown(source) + machine = result.file.machines[0] + vr = check_feedforward_completeness(machine) + # Should warn (warning, not error) + assert any(e.code == "FEEDFORWARD_UNUSED" for e in vr.errors) + assert all(e.severity == "warning" for e in vr.errors) + assert vr.valid # warnings don't fail validation + + def test_coherence_errors_on_reuse_after_measurement(self): + source = """\ +# machine ReusedQubit + +## state |0> [initial] +## state |1> +## state |2> [final] + +## transitions + +| Source | Event | Guard | Target | Action | +|--------|-------|-------|--------|--------| +| |0> | step1 | | |1> | meas | +| |1> | step2 | | |2> | reuse | + +## actions + +| Name | Signature | Effect | +|-------|------------|----------------------------| +| meas | (qs) -> qs | measure(qs[0]) -> bits[0] | +| reuse | (qs) -> qs | Hadamard(qs[0]) | + +## verification rules + +- mid_circuit_coherence: q0 reused after measurement +""" + result = parse_q_orca_markdown(source) + machine = result.file.machines[0] + vr = check_mid_circuit_coherence(machine) + assert not vr.valid + assert any(e.code == "MID_CIRCUIT_COHERENCE_VIOLATION" for e in vr.errors) From de1f4049befff50013e72f9e290947ae44f6f675 Mon Sep 17 00:00:00 2001 From: Allan Scott Date: Tue, 14 Apr 2026 06:03:23 -0400 Subject: [PATCH 2/6] Fix QASM conditional emission to use OpenQASM 3.0 per-bit syntax Replace if(c==val) (OpenQASM 2, whole-register comparison) with if (c[M] == val) { gate; } (OpenQASM 3.0, per-bit), consistent with the OPENQASM 3.0; declaration at the top of every emitted file. The old form gave wrong results for multi-bit classical registers: if(c==1) on a 2-bit register tests c==01 in binary, not bit 0 alone. Co-Authored-By: Claude Sonnet 4.6 --- q_orca/compiler/qasm.py | 3 ++- tests/test_mid_circuit_measurement.py | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/q_orca/compiler/qasm.py b/q_orca/compiler/qasm.py index a82aa80..36294b8 100644 --- a/q_orca/compiler/qasm.py +++ b/q_orca/compiler/qasm.py @@ -52,7 +52,8 @@ def compile_to_qasm(machine: QMachineDef) -> str: elif action and action.conditional_gate is not None: cg = action.conditional_gate gate_str = _gate_to_qasm(cg.gate, qubit_count).rstrip(";") - lines.append(f"if(c=={cg.value}) {gate_str};") + # OpenQASM 3.0 per-bit conditional: if (c[M] == val) { gate; } + lines.append(f"if (c[{cg.bit_idx}] == {cg.value}) {{ {gate_str}; }}") else: for gate in gates: lines.append(_gate_to_qasm(gate, qubit_count)) diff --git a/tests/test_mid_circuit_measurement.py b/tests/test_mid_circuit_measurement.py index b76eb4f..15bbd5d 100644 --- a/tests/test_mid_circuit_measurement.py +++ b/tests/test_mid_circuit_measurement.py @@ -218,7 +218,8 @@ def test_mid_circuit_measure_emitted(self): def test_conditional_gate_emitted(self): machine = self._machine() code = compile_to_qasm(machine) - assert "if(c==1)" in code + # OpenQASM 3.0 per-bit conditional syntax + assert "if (c[0] == 1)" in code assert "x q[1]" in code def test_active_teleportation_qasm(self): @@ -230,8 +231,9 @@ def test_active_teleportation_qasm(self): # Two mid-circuit measurements assert "c[0] = measure q[0];" in code assert "c[1] = measure q[1];" in code - # Two conditional corrections - assert "if(c==1)" in code + # Two conditional corrections using per-bit OpenQASM 3.0 syntax + assert "if (c[0] == 1)" in code + assert "if (c[1] == 1)" in code # --------------------------------------------------------------------------- From b982e3291715aa132d3ac6a315edbb1805cf4162 Mon Sep 17 00:00:00 2001 From: Allan Scott Date: Tue, 14 Apr 2026 06:07:08 -0400 Subject: [PATCH 3/6] Fix ruff lint: remove unused imports in qiskit.py and test file Co-Authored-By: Claude Sonnet 4.6 --- q_orca/compiler/qiskit.py | 2 +- tests/test_mid_circuit_measurement.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/q_orca/compiler/qiskit.py b/q_orca/compiler/qiskit.py index 39cb177..db5688b 100644 --- a/q_orca/compiler/qiskit.py +++ b/q_orca/compiler/qiskit.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from q_orca.angle import evaluate_angle -from q_orca.ast import QMachineDef, QuantumGate, QTypeQubit, QTypeScalar, QTypeList, NoiseModel, QEffectMeasure, QEffectConditional +from q_orca.ast import QMachineDef, QuantumGate, QTypeQubit, QTypeScalar, QTypeList, NoiseModel def _parse_effect_string(effect_str: str) -> list[QuantumGate]: diff --git a/tests/test_mid_circuit_measurement.py b/tests/test_mid_circuit_measurement.py index 15bbd5d..8529f59 100644 --- a/tests/test_mid_circuit_measurement.py +++ b/tests/test_mid_circuit_measurement.py @@ -1,6 +1,5 @@ """Tests for mid-circuit measurement and classical feedforward (mid-circuit-measurement change).""" -import pytest from q_orca.ast import QEffectMeasure, QEffectConditional, QTypeList from q_orca.parser.markdown_parser import parse_q_orca_markdown From 64f6f19c4ff6ffc460e7a5ece80783860d6f98b4 Mon Sep 17 00:00:00 2001 From: Allan Scott Date: Tue, 14 Apr 2026 06:16:01 -0400 Subject: [PATCH 4/6] Fix verifier false positives on mid-circuit measurement transitions SUPERPOSITION_LEAK was firing for measure_alice_x/measure_s0 events because the check only looked at event names, not whether the action was a mid-circuit measurement (intentional, coherence-preserving) vs a terminal collapse. Add an action-level guard: skip the leak check when the transition's action has mid_circuit_measure set. Also fix check_mid_circuit_coherence BFS to continue past mid-circuit measurement transitions rather than stopping at them (terminal-only halt), enabling correct qubit reuse detection across the full circuit. Co-Authored-By: Claude Sonnet 4.6 --- q_orca/verifier/quantum.py | 11 +++++++++-- q_orca/verifier/superposition.py | 9 +++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/q_orca/verifier/quantum.py b/q_orca/verifier/quantum.py index af62f48..f397bce 100644 --- a/q_orca/verifier/quantum.py +++ b/q_orca/verifier/quantum.py @@ -297,8 +297,15 @@ def check_mid_circuit_coherence(machine: QMachineDef) -> QVerificationResult: suggestion="Add a reset gate before reusing a measured qubit, or use a fresh qubit", )) - is_measure = "measure" in t.event.lower() or "collapse" in t.event.lower() - if not is_measure and t.target not in visited: + # Continue BFS through mid-circuit measurement transitions; stop only + # at terminal (end-of-circuit) measurement events. + t_action = action_map.get(t.action) if t.action else None + is_mid_circuit = t_action is not None and t_action.mid_circuit_measure is not None + is_terminal_measure = ( + ("measure" in t.event.lower() or "collapse" in t.event.lower()) + and not is_mid_circuit + ) + if not is_terminal_measure and t.target not in visited: queue.append(t.target) return QVerificationResult( diff --git a/q_orca/verifier/superposition.py b/q_orca/verifier/superposition.py index a83f2a2..d4397e9 100644 --- a/q_orca/verifier/superposition.py +++ b/q_orca/verifier/superposition.py @@ -69,6 +69,7 @@ def check_superposition_leaks(machine: QMachineDef) -> QVerificationResult: return QVerificationResult(valid=True, errors=[]) analysis = analyze_machine(machine) + action_map = {a.name: a for a in machine.actions} for state in machine.states: if state.name not in superposition_states: @@ -80,6 +81,10 @@ def check_superposition_leaks(machine: QMachineDef) -> QVerificationResult: for t in state_info.outgoing: is_measure = "measure" in t.event.lower() or "collapse" in t.event.lower() + # Mid-circuit measurements are intentional — not a superposition leak + transition_action = action_map.get(t.action) if t.action else None + if transition_action and transition_action.mid_circuit_measure is not None: + continue if is_measure: if not t.guard: @@ -126,6 +131,10 @@ def check_superposition_leaks(machine: QMachineDef) -> QVerificationResult: for t in machine.transitions: if t.event != event.name: continue + # Skip mid-circuit measurement transitions — they are intentional + action = action_map.get(t.action) if t.action else None + if action and action.mid_circuit_measure is not None: + continue if t.source not in transitions_by_source: transitions_by_source[t.source] = [] transitions_by_source[t.source].append(t) From c05a773c48f37ff426c37027e74cac41e807888d Mon Sep 17 00:00:00 2001 From: Allan Scott Date: Tue, 14 Apr 2026 06:18:38 -0400 Subject: [PATCH 5/6] Remove unused 'done' event from active-teleportation example 'done' was declared in the events list but had no transitions, causing an ORPHAN_EVENT strict verify failure in CI. Co-Authored-By: Claude Sonnet 4.6 --- examples/active-teleportation.q.orca.md | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/active-teleportation.q.orca.md b/examples/active-teleportation.q.orca.md index e209e23..7f38bc7 100644 --- a/examples/active-teleportation.q.orca.md +++ b/examples/active-teleportation.q.orca.md @@ -20,7 +20,6 @@ - measure_alice_z - correct_x - correct_z -- done ## state |init> [initial] From ffeb0b00612a61e016cff9ed354a6fc79ecd629f Mon Sep 17 00:00:00 2001 From: Allan Scott Date: Tue, 14 Apr 2026 06:27:58 -0400 Subject: [PATCH 6/6] Fix QASM conditional syntax: use bare bit instead of int comparison Qiskit's QASM 3.0 importer requires single-bit conditions to be 'bit == bool' not 'bit == int'. Switch from: if (c[M] == 1) { gate; } to: if (c[M]) { gate; } (value == 1) if (!c[M]) { gate; } (value == 0) Verified round-trip: both examples parse successfully through qiskit.qasm3.loads() with depth and clbit counts intact. Co-Authored-By: Claude Sonnet 4.6 --- q_orca/compiler/qasm.py | 5 +++-- tests/test_mid_circuit_measurement.py | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/q_orca/compiler/qasm.py b/q_orca/compiler/qasm.py index 36294b8..752962e 100644 --- a/q_orca/compiler/qasm.py +++ b/q_orca/compiler/qasm.py @@ -52,8 +52,9 @@ def compile_to_qasm(machine: QMachineDef) -> str: elif action and action.conditional_gate is not None: cg = action.conditional_gate gate_str = _gate_to_qasm(cg.gate, qubit_count).rstrip(";") - # OpenQASM 3.0 per-bit conditional: if (c[M] == val) { gate; } - lines.append(f"if (c[{cg.bit_idx}] == {cg.value}) {{ {gate_str}; }}") + # OpenQASM 3.0 per-bit conditional: bare bit for 1, negated for 0 + cond = f"c[{cg.bit_idx}]" if cg.value else f"!c[{cg.bit_idx}]" + lines.append(f"if ({cond}) {{ {gate_str}; }}") else: for gate in gates: lines.append(_gate_to_qasm(gate, qubit_count)) diff --git a/tests/test_mid_circuit_measurement.py b/tests/test_mid_circuit_measurement.py index 8529f59..ca57335 100644 --- a/tests/test_mid_circuit_measurement.py +++ b/tests/test_mid_circuit_measurement.py @@ -217,8 +217,8 @@ def test_mid_circuit_measure_emitted(self): def test_conditional_gate_emitted(self): machine = self._machine() code = compile_to_qasm(machine) - # OpenQASM 3.0 per-bit conditional syntax - assert "if (c[0] == 1)" in code + # OpenQASM 3.0 per-bit conditional: bare bit for value==1 + assert "if (c[0])" in code assert "x q[1]" in code def test_active_teleportation_qasm(self): @@ -230,9 +230,9 @@ def test_active_teleportation_qasm(self): # Two mid-circuit measurements assert "c[0] = measure q[0];" in code assert "c[1] = measure q[1];" in code - # Two conditional corrections using per-bit OpenQASM 3.0 syntax - assert "if (c[0] == 1)" in code - assert "if (c[1] == 1)" in code + # Two conditional corrections using per-bit OpenQASM 3.0 bare-bit syntax + assert "if (c[0])" in code + assert "if (c[1])" in code # ---------------------------------------------------------------------------