feat: support OpenQASM 2 opaque declarations as black-box gates - #378
feat: support OpenQASM 2 opaque declarations as black-box gates#378TheGupta2012 wants to merge 4 commits into
Conversation
Argus reviewAuto-review is off for this repo. Tick the box below to run a review on this PR.
Estimated cost
Tip: you can also comment |
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
9ce4a97 to
2d636ef
Compare
2d636ef to
d831f3d
Compare
d831f3d to
a26ca6b
Compare
cb49446 to
896ca19
Compare
9cf45a3 to
c895a7f
Compare
c895a7f to
a9676b2
Compare
Any program containing an opaque declaration failed to parse, so no compiled program from Quantinuum H-series hardware could be loaded: the primitives in hqslib1.inc are all declared opaque. opaque is OpenQASM 2 syntax that OpenQASM 3 removed, and pyqasm routes qasm2 through the openqasm3 parser, which has no grammar production for it. The failure therefore happens before any visitor code runs, and a fix in the visitor cannot reach it. Rewrite the declaration in source preprocessing instead, gated on the OPENQASM 2 header so that opaque in a qasm3 program keeps failing to parse. An opaque gate has no decomposition by definition, so it is carried as a gate definition with an empty body -- a marker for its name and arity -- and its name is recorded on the module. A call to it is routed to the external-gate path and emitted as written, counting as one layer of depth. The names live on the module rather than in unroll()'s kwargs, because an opaque gate is a property of the program and has no decomposition for unroll() to fall back on when it flushes external_gates. to_qasm3() now raises for such a program. A body-less gate means the identity in OpenQASM 3, so converting one would silently turn a hardware primitive into a no-op. Fixes #370 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The rewrite ran over raw source, so 'opaque foo q;' inside a /* */ block comment was recorded in _opaque_gates. A real gate of that name would then be emitted as written instead of unrolled. Blank comments to spaces before matching, preserving length so the match spans still index the original text. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… back The blanked copy was only used to locate matches, which meant carrying offsets and splicing each rewrite into the original. Its one consumer is openqasm3.parse on the next line, and the parser discards comments, so the blanked text can be returned directly and the splice loop dropped. Blanking rather than deleting still matters: the parser reports spans against this text, so removing a comment would shift every line and column after it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaces the hand-written character scanner. Differential-tested against it over 20k inputs, including hqslib1.inc and adversarial cases -- unterminated block comments, '//' beating '/*', nested-looking markers -- with no mismatches. Also fixes a case the scanner shared and that mattered once the blanked text started going to the parser: a '//' or '/*' inside an include path was blanked, truncating the statement. Matching string literals first consumes them before either comment alternative can. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
a9676b2 to
efac162
Compare
Fixes #370. Supersedes #54.
The problem
pyqasm could not parse any OpenQASM 2 program containing an
opaquedeclaration:This blocked vendor include files — notably Quantinuum's
hqslib1.inc, whose six hardware primitives are all declaredopaque— so no compiled program from H-series hardware could be loaded at all.The failure is at parse time, not in the visitor.
opaqueis OpenQASM 2 syntax that OpenQASM 3 removed, and pyqasm routes qasm2 through theopenqasm3parser, which has no production for it — the stringopaqueappears nowhere in the installedopenqasm3package. This is why #54's suggestion to handle it in the visitor could not work.The change
The rewrite lives in source preprocessing, where include inlining already happens:
plus the name recorded on the module in
_opaque_gates. An opaque gate has no decomposition by definition, so the empty body is a carrier for its name and arity only; the recorded name is what makes it a black box._visit_generic_gate_operationsends an opaque call to_visit_external_gate_operation, so it is emitted as written and counts as one layer of depth — exactly the treatment an external gate gets. A new_is_black_box_gate()helper collects the three cases that share this treatment (external, verbatim-box, opaque).unroll()resetsexternal_gateson every call. Opaque gates are stored on the module instead, because they are a property of the program and have no decomposition to fall back on. Parametrized test overNone/[]/["h"]/["ZZ"].OPENQASM 2header matches, soopaquein a qasm3 program keeps failing to parse.opaqueinside a vendor include file is reached by bothload()andloads(..., include_dir=...).What works
The issue's bell state, with an
hqslib1-shaped include, loads and unrolls:validate(),depth(),has_measurements(),num_qubits,remove_idle_qubits(),reverse_qubit_order()andunroll(consolidate_qubits=True)all handle opaque calls, and arity is validated (ZZ q[0];→Qubit count mismatch for gate 'ZZ'. Expected 2 qubits, but got 1).Better than the issue predicted: it also handles
hqslib1'sUandCX, which are defined in terms of the opaque primitives. The issue author had to drop those; they now unroll down to the primitives and stop there.Three limits, all deliberate, all tested
gatedefinition unrolling already drops. Consequence: the output has calls to a gate it does not declare, so it does not load back into pyqasm on its own. A printer hook for re-emittingopaqueis deliberately not built in v1.rebase()raisesRebaseError: Gate 'ZZ' is not supported in the '<basis>' setto_qasm3()now raises for a program declaring onegatemeans the identity in OpenQASM 3. Emitting the carrier definition would silently turn every hardware primitive into a no-op. OpenQASM 3 removedopaqueand has no equivalent, so there is no correct translation and refusing is the loud option. Nothing insidesrc/callsto_qasm3, so this is confined to the public API, and these programs could not be loaded before this PR — no workflow regresses. Easily relaxed if someone proposes a representation.All three are documented in a new Opaque gates section in
src/README.md, alongside the existing Pragmas notes.Verification
tox -e format-check: pylint 10.00/10, isort, black, mypy and headers all clean.test_opaque_is_not_qasm3_syntax— correctly, since it pins behaviour this PR must not change.hqslib1-like fixture is written out in the test file rather than vendored, so nothing depends on pytket being installed.Merge order
This is stacked on #377 (
include_dir), because both touchloads()andpreprocess.py. Merge #377 first.The #375 conflict is already resolved. #375 (issues #367 and #369) has merged, and this branch is rebased on top of it. Both PRs rewrote the
_recording_ext_gate_depthassignment at the top of_visit_custom_gate_operation; the resolution keeps #375's save/restore shape with this PR's helper:Resolving it the other way — keeping
self._in_verbatim_box or gate_name in self._external_gates— would make opaque gates record zero depth, becauseis_externalalso feeds the single depth-recording block below.test_opaque_gate_counts_as_one_towards_depthcovers it.Related
hqslib1.incbe supplied as a string, this PR lets itsopaquelines parse. Both are needed; each is independent.hqslib1'sUandCX. That is now reported by name instead of blowing the stack. This PR removes the need for that workaround entirely.