Skip to content

Nested external custom gate counts the depth of the decomposition it skips #367

Description

@ryanhill1

Description

A nested external custom gate counts the depth of the decomposition it skipped. This is the same symptom #352 fixed for external basic gates, in the one shape that fix did not reach.

Raised by @TheGupta2012 while reviewing #359, where the changelog wording implied the custom-gate path was already correct in general. It is correct for a single level of nesting, which is why the existing tests pass.

Reproduction

from pyqasm import loads

qasm = """OPENQASM 3.0;
include "stdgates.inc";
gate inner a, b { crz(0.5) a, b; }
gate outer a, b { inner a, b; h a; }
qubit[2] q;
outer q[0], q[1];
"""

module = loads(qasm)
module.unroll(external_gates=["outer"])
print(module.depth())  # 13, but exactly one statement is emitted

Measured on 7c31308:

external_gates statements emitted depth() expected
["outer"] 1 13 1
["inner", "outer"] 1 2 1
["g"] (single level, for contrast) 1 1 1

13 is the fully-decomposed depth — the crz decomposition plus the h. So an external gate that emits a single statement reports the cost of the body it never emitted.

Cause

_visit_custom_gate_operation assigns _recording_ext_gate_depth unconditionally at visitor.py:1309 and clears it unconditionally at visitor.py:1341-1342, with no save-restore:

self._recording_ext_gate_depth = self._in_verbatim_box or gate_name in self._external_gates
...
if self._recording_ext_gate_depth:
    self._recording_ext_gate_depth = False

With external_gates=["outer"], visiting outer sets the flag to True. Descending into inner overwrites it with False, because inner is not itself external — so the crz decomposition inside records its depth, and when outer finishes the flag is already False, so outer never records its own. The inner gate clobbers the outer gate's state in both directions.

Suggested fix

Two changes, both in _visit_custom_gate_operation. #359 already applies the save-restore half of this pattern in _visit_external_gate_operation.

  1. Save the flag and restore it on exit, rather than clearing it to False.
  2. Stay suppressed inside an enclosing external gate — prev or is_external rather than a bare assignment — and record only from the outermost external gate.
prev_recording = self._recording_ext_gate_depth
is_external = self._in_verbatim_box or gate_name in self._external_gates
self._recording_ext_gate_depth = prev_recording or is_external
...
self._recording_ext_gate_depth = prev_recording
if is_external and not prev_recording:
    ...

I prototyped this locally: all three rows above report 1, and the full suite passes (713 passed, 3 skipped) with no expectation changes. Note that the save-restore alone is not sufficient — it fixes ["inner", "outer"] but leaves ["outer"] at 13, because the nested gate still re-enables recording for the body its parent is skipping.

Test coverage

tests/qasm3/test_depth.py covers single-level external custom gates only. Worth adding the nested shape alongside test_external_basic_gate_counts_own_depth, parametrized over ["outer"] and ["inner", "outer"], since the two failed differently.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions