Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Types of changes:
- Fixed `unroll(consolidate_qubits=True)` raising `AttributeError: 'str' object has no attribute 'name'` for any gate applied to a physical qubit, e.g. `h $1;`. Consolidation assumed every gate operand was an `IndexedIdentifier`, but a physical qubit survives unrolling as `Identifier("$1")`. Physical qubits are absolute hardware indices belonging to no declared register, so they are now left as written — matching how `measure`, `reset` and `barrier` already treat them. ([#344](https://github.com/qBraid/pyqasm/pull/344))
- Fixed `unroll()` and `rebase()` emitting statements that share operand AST nodes: gate decompositions passed the same `IndexedIdentifier` objects into every statement they emitted, so transformations that rewrite qubit indices in place mutated a shared node once per referencing statement. This crashed `reverse_qubit_order()` (`KeyError: -1`) and `remove_idle_qubits()` (`KeyError`, [#331](https://github.com/qBraid/pyqasm/issues/331)) on any decomposed gate (e.g. `crz`) whenever the remap was not the identity. Statement constructors in `maps/gates.py` and `Decomposer` now copy their qubit operands so every emitted statement owns its nodes. ([#333](https://github.com/qBraid/pyqasm/issues/333))
- Fixed statements that OpenQASM 2 cannot condition being accepted as the body of a classical conditional. The QASM 2 grammar admits only a `<qop>` — a gate application, a measurement or a reset — as the body of an `if`. `barrier` is a separate production, and `delay`/`box` have no QASM 2 syntax at all, yet `if(m==1) barrier q;`, `if(m==1) delay[10ns] q;` and `if(m==1) box {...}` all validated and were emitted unchanged, producing output that QASM 2 parsers reject. Conditional bodies are now filtered against the `<qop>` whitelist at any nesting depth, raising a `ValidationError` that names the offending keyword and its source span. ([#339](https://github.com/qBraid/pyqasm/pull/339))
- Fixed the OpenQASM 2 serializer emitting OpenQASM 3 syntax for classical conditionals while still declaring `OPENQASM 2.0`. `if(m==1) x q[1];` round-tripped as a braced `if (m == 1) { x q[1]; }` block, which QASM 2 parsers reject, so valid feed-forward programs were corrupted in-place. Unrolling made it worse: `unroll()` ravels a register comparison into a nested chain of per-bit tests (`if (m[0] == true) { if (m[1] == false) { ... } }`), which additionally uses creg indexing and a `true` literal that QASM 2 has no syntax for. Conditionals are now emitted as `if (creg == int) <statement>`, with the per-bit chain collapsed back into the whole-register comparison it came from, a register-level measurement reassembled from the per-bit statements unrolling expanded it into, and any remaining multi-statement body expanded to one guarded statement each. Branch shapes QASM 2 has no syntax for — `else` blocks, nested `if`s, conditions spanning registers or indexing a single bit, and non-equality operators — are now reported by `validate()` rather than only at serialization time, and the diagnostic names the operator the user wrote. ([#337](https://github.com/qBraid/pyqasm/issues/337))
- Fixed `remove_idle_qubits(in_place=False)` updating the qubit count on the wrong module: the original module's `num_qubits` was decremented while the returned copy kept the stale pre-removal count. The copy's AST was already correct; only the counters were swapped. ([#336](https://github.com/qBraid/pyqasm/pull/336))
- Fixed `remove_idle_qubits()` raising `KeyError` when the unrolled AST contains operand nodes shared across multiple statements (e.g. the `crz` decomposition) and an idle lower-indexed qubit shifts the register indices. `_remap_qubits` now remaps each operand node exactly once instead of once per statement that references it. ([#332](https://github.com/qBraid/pyqasm/pull/332))
- Fixed `box` duration validation summing `delay` durations across all qubits instead of tracking each qubit's timeline. Delays on disjoint qubits run in parallel, so `box[300ns] { delay[200ns] q[0]; delay[200ns] q[1]; }` was rejected while the identical schedule written as a broadcast delay (`delay[200ns] q;`) was accepted. Delays are now accumulated per qubit and the box is validated against the busiest single timeline; the error message names the offending qubit. Nested boxes now also contribute their declared duration to the enclosing box's timelines (previously the accumulator was reset when an inner box closed, dropping all inner delay accounting). ([#330](https://github.com/qBraid/pyqasm/pull/330))
Expand Down
Loading
Loading