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.
- Save the flag and restore it on exit, rather than clearing it to
False.
- 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.
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
Measured on
7c31308:external_gatesdepth()["outer"]["inner", "outer"]["g"](single level, for contrast)13 is the fully-decomposed depth — the
crzdecomposition plus theh. So an external gate that emits a single statement reports the cost of the body it never emitted.Cause
_visit_custom_gate_operationassigns_recording_ext_gate_depthunconditionally atvisitor.py:1309and clears it unconditionally atvisitor.py:1341-1342, with no save-restore:With
external_gates=["outer"], visitingoutersets the flag toTrue. Descending intoinneroverwrites it withFalse, becauseinneris not itself external — so thecrzdecomposition inside records its depth, and whenouterfinishes the flag is alreadyFalse, soouternever 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.False.prev or is_externalrather than a bare assignment — and record only from the outermost external gate.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.pycovers single-level external custom gates only. Worth adding the nested shape alongsidetest_external_basic_gate_counts_own_depth, parametrized over["outer"]and["inner", "outer"], since the two failed differently.