fix: nested external gate depth, and gate definition cycles of any length - #375
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 |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
Included review availability: Your plan includes up to 1 review per rolling hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe change updates custom-gate expansion to detect indirect recursive definitions and preserve nested external-gate depth state. Regression tests cover recursive cycles, shared gate expansion, repeated calls, and nested external-gate depth. ChangesGate expansion behavior
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to This PR corrects depth reporting for nested external custom gates and rejects cyclic gate definitions, with focused regression coverage; no actionable merge-blocking risk remains beyond normal checks and review. Sequence Diagram(s)sequenceDiagram
participant QASMInput
participant QasmVisitor
participant ExpandedCircuit
QASMInput->>QasmVisitor: visit custom-gate operation
QasmVisitor->>QasmVisitor: track active gate chain
QasmVisitor->>ExpandedCircuit: emit external gate or decomposed operations
QasmVisitor-->>QASMInput: report recursive cycle path when detected
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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! |
An external custom gate whose body calls another custom gate reported the depth of the decomposition it never emitted: depth() == 13 for a single emitted statement with external_gates=["outer"], and 2 with both gates named external. _visit_custom_gate_operation assigned _recording_ext_gate_depth without saving it and cleared it to False on exit. Descending into a non-external inner gate therefore re-enabled recording for the body the outer gate was skipping, and the clear on the inner gate's exit left the outer gate unable to record its own single depth. Save the flag, restore it in a finally block, keep an inner gate suppressed inside an enclosing external gate, and record the depth once from the outermost external gate only. Fixes #367 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
81e4018 to
41069c6
Compare
…#376) A cycle of two or more gate definitions recursed until the interpreter's stack limit and surfaced as a bare RecursionError naming nothing. The guard in _visit_custom_gate_operation compared the body's gate name against the single name being expanded, so it matched only a gate calling itself. Track the chain of gates currently being expanded and test membership of that chain instead. A cycle of any length now raises a ValidationError at the call that closes it, naming the path, e.g. (a -> b -> a). Membership of the chain, rather than of every gate seen, keeps a diamond -- one gate reached twice down separate paths -- expanding normally. Fixes #369 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The helper had a single caller, so the indirection cost a method and a docstring without earning anything. The message and span are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
coderabbit generated no comments, merging |
Fixes #367 and #369.
The bug
An external custom gate whose body calls another custom gate reported the depth of the decomposition it never emitted. This is the shape the #352 fix did not reach — one level of nesting was already correct, which is why the existing tests passed.
external_gatesdepth()before["outer"]["inner", "outer"]None(nothing external)Cause
_visit_custom_gate_operationassigned_recording_ext_gate_depthunconditionally and cleared it toFalseon exit, with no save-restore. The inner gate clobbered the outer gate's state in both directions:inneroverwrote the flag withFalse, so thecrzdecomposition inside the bodyouterwas skipping recorded its depth → 13.innerreturned it left the flagFalse, soouternever recorded its own single depth → the["inner", "outer"]row reported 2 instead of 1.That is why save-restore alone is not sufficient: it fixes
["inner", "outer"]and leaves["outer"]at 13.The fix
Two changes in
_visit_custom_gate_operation:finallyblock, matching the pattern fix: record an external gate's own depth, not its skipped decomposition's #359 introduced in_visit_external_gate_operation.prev_recording or is_externalrather than assigning, so an inner gate stays suppressed inside an enclosing external gate, and record the depth underis_external and not prev_recording— from the outermost external gate only.Verification
tox -e format-check: pylint 10.00/10, isort, black, mypy and headers all clean.["outer"]at 13,["inner", "outer"]at 2 — which is the point: they pin two distinct halves of the bug.Tests added
In
tests/qasm3/test_depth.py, besidetest_external_basic_gate_counts_own_depth:test_nested_external_custom_gate_counts_own_depth, parametrized over["outer"]and["inner", "outer"], asserting one emitted statement anddepth() == 1.test_nested_custom_gate_depth_unchanged_without_external_gates— the same program still decomposes to 13 when nothing is external, so the flag handling cannot silently suppress ordinary counting.test_inner_external_gate_records_depth_inside_plain_custom_gate—external_gates=["inner"]still records the inner gate's own depth when the enclosing gate is not external (2, not 1).The last two pass on
main; they are regression guards, not bug pins.Summary by CodeRabbit
Bug Fixes
Tests