Skip to content

fix: detect gate definition cycles of any length, not just self-calls - #376

Merged
TheGupta2012 merged 1 commit into
fix-nested-external-gate-depthfrom
fix-indirect-gate-recursion
Aug 18, 2026
Merged

TheGupta2012 merged 1 commit into
fix-nested-external-gate-depthfrom
fix-indirect-gate-recursion

Conversation

@TheGupta2012

Copy link
Copy Markdown
Member

Fixes #369.

The bug

pyqasm reported a gate that calls itself cleanly, but a cycle between two definitions exhausted the Python stack:

# direct — already correct
pyqasm.loads('gate a(t) q { a(t) q; } ...').validate()
# ValidationError: Recursive definitions not allowed for gate 'a'

# indirect — stack blowout
pyqasm.loads('gate a(t) q { b(t) q; } gate b(t) q { a(t) q; } ...').validate()
# RecursionError: maximum recursion depth exceeded

The RecursionError named nothing, so there was no indication of which gates were cycling.

Cause

The guard in _visit_custom_gate_operation compared the body's gate name against one name — the gate currently being expanded:

if isinstance(gate_op, qasm3_ast.QuantumGate) and gate_op.name.name == gate_name:

A cycle of length one matches. A cycle of length two or more passes the check and recurses through gate expansion until the interpreter's stack limit.

The fix

Track the chain of gates currently being expanded (_gate_expansion_chain, pushed on entry and popped in a finally) and test membership of that chain instead of equality with a single name. One mechanism now covers both cases, so the direct-recursion check is replaced rather than duplicated.

The error names the path it closes, as the issue requested:

Recursive definitions not allowed for gate 'a' (a -> b -> a)
Recursive definitions not allowed for gate 'a' (a -> b -> c -> a)
Recursive definitions not allowed for gate 'a' (a -> a)          # direct, unchanged shape

The check stays at the same point in the body scan as the old one, so the reported span is still the call inside the definition that closes the cycle.

Chain, not a seen-set. Membership of the chain is what distinguishes a cycle from a diamond. With a set of every gate seen, a calling both b and c, where both call d, would be misreported as recursion. Popping on return keeps that legal.

Behaviour that does not change

Program Before After
gate a q { a q; } ValidationError same, plus (a -> a)
gate b never defined Unsupported / undeclared QASM operation: b unchanged
diamond a → {b, c} → d expands expands
same gate called twice in one body expands twice expands twice

Verification

  • Full suite: 789 passed, 4 skipped.
  • tox -e format-check: pylint 10.00/10, isort, black, mypy and headers all clean.
  • Mutation-tested. With the source fix reverted, all three new cycle tests fail with RecursionError; the two diamond/repeat guards pass on main, as they should.

Tests added

  • CUSTOM_GATE_INCORRECT_TESTS gains indirect_recursive_definition (a → b → a) and three_gate_recursive_definition (a → b → c → a). Both pin the full message including the path, and both inherit the existing line/column assertions.
  • test_shared_gate_definition_is_not_recursion — the diamond must expand, not raise.
  • test_repeated_gate_call_in_one_body_is_not_recursion — the chain entry must be popped when an expansion returns.
  • test_indirect_recursion_does_not_exhaust_the_stack — pins that the failure is a ValidationError and never a RecursionError, which was the reported symptom.

Note on the base branch

This is stacked on #375, because both PRs edit _visit_custom_gate_operation — reviewing them independently would hand you a conflict. Merge #375 first; this diff then reduces to its own four files. The commits are separate and each stands on its own.

Follow-up not taken

The issue notes this is likely to be hit by anyone fixing #54 / #370 (opaque declarations), since giving Quantinuum's opaque primitives real bodies creates a mutual cycle with hqslib1's U and CX. That case now reports the cycle by name instead of blowing the stack, which is the whole of what this issue asked for.

@argus-eye

argus-eye Bot commented Aug 18, 2026

Copy link
Copy Markdown

Argus review

Auto-review is off for this repo. Tick the box below to run a review on this PR.

  • Trigger Argus review

Estimated cost

  • Files changed: 4
  • Diff lines (±): 147
  • Historical avg: ~243.6k tokens · ~$0.95 · across last 10 review(s)

Tip: you can also comment @argus-eye review at any time.

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a9becccb-7d62-4d50-b571-a538ff1c95f0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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>
@TheGupta2012
TheGupta2012 force-pushed the fix-indirect-gate-recursion branch from 6dfffb0 to 4da00a9 Compare August 18, 2026 07:41
@TheGupta2012

Copy link
Copy Markdown
Member Author

Looks good, the error was simple

@TheGupta2012
TheGupta2012 merged commit 82fc04b into fix-nested-external-gate-depth Aug 18, 2026
23 checks passed
TheGupta2012 added a commit that referenced this pull request Aug 18, 2026
…ngth (#375)

* fix: record a nested external gate's own depth, not its skipped body's

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>

* fix: detect gate definition cycles of any length, not just self-calls (#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>

* refactor: inline the cyclic-gate error at its one call site

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>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants