From c329fb6b509c5f5430a8cbb2b8a31150b074a454 Mon Sep 17 00:00:00 2001 From: Amos Anderson Date: Wed, 19 Aug 2026 12:50:57 -0400 Subject: [PATCH 01/18] report a register name that is allocated twice The compiler keeps the first allocation of a name, so a second declaration of the same name on the same module is a no-op and its initial value never reaches the qubit. That was silent. Procedure now tracks the names it has allocated per module and raises QCDLUserError, pointing at the ways to say the reuse was deliberate. alias=True and ignore_reallocation=True stay allowed, but only without an initial value: opting in says the existing memory is wanted, whereas giving a value says the opposite and the compiler would ignore it. To tell "no value given" apart from an explicit 0, initial_value now defaults to None and resolves to 0 or 0.0 per dtype. The signal example in the user guide relied on the silent behaviour, so it now says alias=True where it means to reuse the scope register. Co-Authored-By: Claude Opus 5 (1M context) --- docs/workflow.rst | 5 +- dwave/gate/qcdl/components.py | 77 ++++++++++++- dwave/gate/qcdl/registers.py | 53 +++++++-- tests/test_registers.py | 205 ++++++++++++++++++++++++++++++++++ 4 files changed, 329 insertions(+), 11 deletions(-) diff --git a/docs/workflow.rst b/docs/workflow.rst index f9501e2..a12e1e7 100644 --- a/docs/workflow.rst +++ b/docs/workflow.rst @@ -986,8 +986,9 @@ condition value used in the ``If`` statement here and in subsequent examples. # all qubits have a copy of the same register: send_register = sc.Register(name=name) - # set the register on q0 to 0 or 1 - measure(q0, register=q0.Register(name=name)) + # set the register on q0 to 0 or 1; alias=True reuses the memory + # send_register already allocated instead of redeclaring it + measure(q0, register=q0.Register(name=name, alias=True)) # if any of the copies of the register are equal to 1, then all # will receive a condition of True. diff --git a/dwave/gate/qcdl/components.py b/dwave/gate/qcdl/components.py index a777cc5..4645e2c 100644 --- a/dwave/gate/qcdl/components.py +++ b/dwave/gate/qcdl/components.py @@ -134,6 +134,10 @@ def __init__( # qubits in the statement are not listed in the last item in this list. self._exclusive_modules: list[set[str]] = [] + # register names allocated in this procedure, per module, so that + # re-declaring one can be reported rather than silently discarded + self._allocated_registers: dict[str, dict[str, str]] = {} + self._procedure_ended = False def to_model(self) -> QCDLProcedureDef: @@ -241,6 +245,75 @@ def register_module_used(self, module_name: str | None) -> None: if module not in self.modules_used: self.modules_used.append(module) + def register_memory_allocation( + self, + modules: Sequence[QCDLModule], + name: str, + dtype: str, + allow_existing: bool = False, + initial_value_specified: bool = False, + ) -> None: + """Record a register allocation, rejecting a silent re-declaration. + + The compiler keeps the *first* allocation of a name, so a second + declaration of the same name on the same module is a no-op: its initial + value never reaches the qubit. That is almost always a mistake, so it is + reported here instead. + + Re-declaring the name is allowed when the caller asked for it, but an + explicit initial value is still rejected: opting in to the + re-declaration says the existing memory is wanted, whereas giving a + value says the opposite, and the compiler would ignore it. + + This method is mostly intended for use by developers of QCDL; the + :class:`~dwave.gate.qcdl.registers.Register` and + :class:`~dwave.gate.qcdl.registers.FixedPointRegister` classes call it + for you. + + Args: + modules: Modules the register is allocated on. + name: Name of the register. + dtype: ``"int"`` or ``"float"``. + allow_existing: If True, an existing allocation of ``name`` is + accepted as long as no initial value was given. Set by the + ``alias`` and ``ignore_reallocation`` arguments of a register. + initial_value_specified: Whether the caller gave an initial value + for this register. + + Raises: + :exception:`~dwave.gate.qcdl.exceptions.QCDLUserError`: If ``name`` + is already allocated on one of ``modules`` and either + ``allow_existing`` is False or an initial value was given. + """ + for module in modules: + allocated = self._allocated_registers.setdefault( + module.qcdl_module_name, {} + ) + previous = allocated.get(name) + if previous is not None and not ( + allow_existing and not initial_value_specified + ): + if allow_existing: + raise QCDLUserError( + f"register {name!r} is already allocated on" + f" {module.qcdl_module_name} with dtype {previous} in" + f" procedure {self.name}, and the compiler keeps the" + f" first allocation, so the initial value given here" + f" would never reach the qubit. Re-declaring the name is" + f" allowed, but giving it a value is not: drop the" + f" initial value." + ) + raise QCDLUserError( + f"register {name!r} is already allocated on" + f" {module.qcdl_module_name} with dtype {previous} in" + f" procedure {self.name}; the compiler keeps the first" + f" allocation, so this one would be discarded. Reuse the" + f" existing register, pick another name, or redeclare it" + f" deliberately with alias=True or ignore_reallocation=True" + f" and no initial value." + ) + allocated[name] = dtype + @property def expression_queue(self) -> list | None: """Create an expression queue. @@ -1230,7 +1303,9 @@ def all_to_all_use(q0, q1): sc = Scope(q0, q1) r1 = sc.Register(name="r1") h(q0) - measure(q0, register=q0.Register(name="r1")) + # alias=True reuses the memory r1 already allocated, so the + # outcome is stored on q0 only rather than mirrored + measure(q0, register=q0.Register(name="r1", alias=True)) sc.all_to_all(send=r1==1, reduce_op="&") with sc.If(None): x(q1) diff --git a/dwave/gate/qcdl/registers.py b/dwave/gate/qcdl/registers.py index d3878c0..872ae4b 100644 --- a/dwave/gate/qcdl/registers.py +++ b/dwave/gate/qcdl/registers.py @@ -244,6 +244,13 @@ def _register_initialization( if isinstance(initial_value, np.ndarray): initial_value = initial_value.tolist() + # None means the caller gave no initial value. That distinction matters + # because a value that would never reach the qubit has to be reported + # rather than silently dropped. + initial_value_specified = initial_value is not None + if not initial_value_specified: + initial_value = 0.0 if str(dtype) == "float" else 0 + if length is None: length = len(initial_value) if isinstance(initial_value, Sequence) else 1 @@ -257,6 +264,24 @@ def _register_initialization( name, initial_value=initial_value, length=length, dtype=dtype, signed=signed ) + if alias is True and initial_value_specified: + raise QCDLUserError( + f"register {name!r} is an alias, so no memory is allocated for" + f" it and the initial value given here would never reach the" + f" qubit; drop the initial value, or drop alias=True to" + f" allocate new memory" + ) + + # An alias deliberately names memory that already exists, and + # ignore_reallocation is the documented opt out. + modules[0].procedure.register_memory_allocation( + modules, + name, + dtype, + allow_existing=alias is True or ignore_reallocation, + initial_value_specified=initial_value_specified, + ) + if alias is not True: # use alias=True if some other code called allocate_memory for this # register @@ -655,16 +680,22 @@ class Register(IntegerOpsMixin, AssignmentOpsMixin, RegisterInitializerMixin, Ta one or more qubits. Typically, you create a register from a :class:`~dwave.gate.qcdl.Scope` object, which handles this parameter for you. - initial_value: Initial value. Defaults to 0. + initial_value: Initial value. Defaults to 0. Omit it when reusing a name + that is already allocated (see ``alias`` and + ``ignore_reallocation``): only the first allocation of a name takes + effect, so a value given for a later one is rejected rather than + silently discarded. name: Name for this register; useful for troubleshooting. If None, a name is generated. See the ``alias`` parameter for type punning. master_kwargs: Propagate this to the master instruction. This parameter is intended for use by developers of QCDL. alias: Set to True if you are aliasing an existing register, and for type punning reuse that register's name in the ``name`` parameter. - Aliased registers are not reinitialized. + Aliased registers are not reinitialized, so you may not give an + ``initial_value``. ignore_reallocation: If True, the compiler does not reallocate if - already allocated (and does not raise an exception). + already allocated (and does not raise an exception). You may not + give an ``initial_value`` as well. scope_id: Identity of the :class:`~dwave.gate.qcdl.Scope` this register is derived from. The :meth:`~dwave.gate.qcdl.QCDLModuleContainer.Register` method sets @@ -727,7 +758,7 @@ def direct(q0, q1): def __init__( self, modules: Sequence[QCDLModule], - initial_value: int = 0, + initial_value: int | None = None, name: str | None = None, master_kwargs: dict[str, Any] | None = None, alias: bool | str = False, @@ -772,16 +803,22 @@ class FixedPointRegister( one or more qubits. Typically, you create a register from a :class:`~dwave.gate.qcdl.Scope` object, which handles this parameter for you. - initial_value: Initial value. Defaults to 0.0. + initial_value: Initial value. Defaults to 0.0. Omit it when reusing a + name that is already allocated (see ``alias`` and + ``ignore_reallocation``): only the first allocation of a name takes + effect, so a value given for a later one is rejected rather than + silently discarded. name: Name for this register; useful for troubleshooting. If None, a name is generated. See the ``alias`` parameter for type punning. master_kwargs: Propagate this to the master instruction. This parameter is intended for use by developers of QCDL. alias: Set to True if you are aliasing an existing register, and for type punning reuse that register's name in the ``name`` parameter. - Aliased registers are not reinitialized. + Aliased registers are not reinitialized, so you may not give an + ``initial_value``. ignore_reallocation: If True, the compiler does not reallocate if - already allocated (and does not raise an exception). + already allocated (and does not raise an exception). You may not + give an ``initial_value`` as well. scope_id: Identity of the :class:`~dwave.gate.qcdl.Scope` this register is derived from. The :meth:`~dwave.gate.qcdl.QCDLModuleContainer.FixedPointRegister` @@ -825,7 +862,7 @@ def create_fixed_reg(q0, q1): def __init__( self, modules: Sequence[QCDLModule], - initial_value: float = 0.0, + initial_value: float | None = None, name: str | None = None, master_kwargs: dict[str, Any] | None = None, alias: bool | str = False, diff --git a/tests/test_registers.py b/tests/test_registers.py index dfca204..7db4680 100644 --- a/tests/test_registers.py +++ b/tests/test_registers.py @@ -25,6 +25,7 @@ QCDLModule, QCDLUserError, Register, + Scope, arbitrary_function, procedure, qcdl, @@ -563,6 +564,210 @@ def main(q0, q1, **kwargs): assert main() +def test_duplicate_register_name_raises(): + """The compiler keeps the first allocation, so the second value is lost.""" + + @qcdl(1) + def main(q0): + q0.Register(1, name="dup") + q0.Register(2, name="dup") + + with pytest.raises(QCDLUserError) as cm: + main() + assert "'dup' is already allocated on q0" in str(cm.value) + + +def test_duplicate_register_name_reports_the_dtype_it_has(): + @qcdl(1) + def main(q0): + q0.Register(name="clash") + q0.FixedPointRegister(name="clash") + + with pytest.raises(QCDLUserError, match="with dtype int"): + main() + + +def test_duplicate_register_name_allowed_by_ignore_reallocation(): + """Opting in gets you a handle on the register that is already there.""" + + @qcdl(1) + def main(q0): + q0.Register(1, name="dup") + q0.Register(name="dup", ignore_reallocation=True) + q0.measure() + + allocations = [ + s for s in main().program.statements if s.op == "allocate_memory" + ] + assert len(allocations) == 2 + assert allocations[0].kwargs["initial_value"] == 1 + assert allocations[1].kwargs["ignore_reallocation"] is True + + +def test_ignore_reallocation_still_rejects_an_initial_value(): + """The second value is explicit information the compiler would discard.""" + + @qcdl(1) + def main(q0): + q0.Register(1, name="dup") + q0.Register(2, name="dup", ignore_reallocation=True) + + with pytest.raises(QCDLUserError) as cm: + main() + message = str(cm.value) + assert "would never reach the qubit" in message + assert "drop the initial value" in message + + +def test_an_explicit_zero_counts_as_an_initial_value(): + """0 is the default, but passing it is still saying something.""" + + @qcdl(1) + def main(q0): + q0.Register(name="dup") + q0.Register(0, name="dup", ignore_reallocation=True) + + with pytest.raises(QCDLUserError, match="would never reach the qubit"): + main() + + +def test_ignore_reallocation_on_a_fresh_name_may_carry_a_value(): + """Nothing is discarded when the name is not already allocated.""" + + @qcdl(1) + def main(q0): + q0.Register(7, name="fresh", ignore_reallocation=True) + + allocations = [ + s for s in main().program.statements if s.op == "allocate_memory" + ] + assert [a.kwargs["initial_value"] for a in allocations] == [7] + + +def test_alias_rejects_an_initial_value(): + """An alias is never initialized, so its value is dead on arrival.""" + + @qcdl(1) + def main(q0): + q0.FixedPointRegister(1.0, name="fr") + q0.Register(3, name="fr", alias=True) + + with pytest.raises(QCDLUserError, match="is an alias"): + main() + + +def test_duplicate_array_name_with_ignore_reallocation_still_raises(): + """An Array always carries contents, so it can never opt in.""" + + @qcdl(1) + def main(q0): + Array(q0, [1, 2], name="arr") + Array(q0, [3, 4], name="arr", ignore_reallocation=True) + + with pytest.raises(QCDLUserError, match="would never reach the qubit"): + main() + + +@pytest.mark.parametrize( + "register_type,expected", [("Register", 0), ("FixedPointRegister", 0.0)] +) +def test_omitted_initial_value_still_allocates_zero(register_type, expected): + """The sentinel default has to resolve per dtype, not leak out as None.""" + + @qcdl(1) + def main(q0): + getattr(q0, register_type)(name="r") + + allocation = next( + s for s in main().program.statements if s.op == "allocate_memory" + ) + value = allocation.kwargs["initial_value"] + assert value == expected + assert isinstance(value, type(expected)) + + +def test_aliasing_an_allocated_register_is_not_a_duplicate(): + """alias=True deliberately names memory that already exists.""" + + @qcdl(1) + def main(q0): + q0.FixedPointRegister(initial_value=1, name="fr") + integer_view = q0.Register(name="fr", alias=True) + integer_view += integer_view & 4 + + allocations = [ + s for s in main().program.statements if s.op == "allocate_memory" + ] + assert len(allocations) == 1 + + +def test_alias_of_another_register_is_still_tracked(): + """A string alias allocates a new name, so redeclaring it is a duplicate.""" + + @qcdl(1) + def main(q0): + q0.Register(name="original") + q0.Register(name="punned", alias="original") + q0.Register(name="punned") + + with pytest.raises(QCDLUserError, match="'punned' is already allocated"): + main() + + +def test_a_scope_register_may_be_narrowed_with_an_alias(): + """The documented way to write to one qubit's copy of a shared register.""" + + @qcdl(2) + def main(q0, q1): + scope = Scope(q0, q1) + scope.Register(name="bit") + q0.measure(register=q0.Register(name="bit", alias=True)) + + allocations = [ + s for s in main().program.statements if s.op == "allocate_memory" + ] + assert len(allocations) == 1 + assert [str(q) for q in allocations[0].modules] == ["q0", "q1"] + + +def test_same_register_name_on_different_qubits_is_fine(): + @qcdl(2) + def main(q0, q1): + q0.Register(1, name="r0") + q1.Register(2, name="r0") + + allocations = [ + s for s in main().program.statements if s.op == "allocate_memory" + ] + assert [a.kwargs["initial_value"] for a in allocations] == [1, 2] + + +def test_duplicate_array_name_raises(): + @qcdl(1) + def main(q0): + Array(q0, [1, 2], name="arr") + Array(q0, [3, 4], name="arr") + + with pytest.raises(QCDLUserError, match="'arr' is already allocated"): + main() + + +def test_register_names_are_tracked_per_procedure(): + """A procedure has its own statements, so it may reuse a name.""" + + @procedure + def inner(qa): + qa.Register(name="local") + qa.rx(0.123) + + @qcdl(1) + def main(q0): + q0.Register(name="local") + inner(q0) + + assert main() + + def test_parent_proc_not_altered(): class ModuleHider(object): # this is not a QCDLModuleContainer and thus From 588b9002ae067fadf7b624467c00131eafb1ca17 Mon Sep 17 00:00:00 2001 From: Amos Anderson Date: Wed, 19 Aug 2026 14:56:53 -0400 Subject: [PATCH 02/18] say precisely when ignore_reallocation forbids an initial value The validation already only rejects a value for a name that is already allocated -- a first allocation takes its value whatever the caller opted in to -- but the docstrings stated the rule unconditionally, as if ignore_reallocation could never carry one. That is the distinction between the two opt outs: an alias is always an alias and never allocates, so its value is discarded whatever the state of the name, whereas ignore_reallocation is a no-op only once the name is allocated. A test now pins the alias half on a fresh name. Co-Authored-By: Claude Opus 5 (1M context) --- dwave/gate/qcdl/components.py | 11 +++++++---- dwave/gate/qcdl/registers.py | 36 +++++++++++++++++++---------------- tests/test_registers.py | 16 ++++++++++++++++ 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/dwave/gate/qcdl/components.py b/dwave/gate/qcdl/components.py index 4645e2c..edcb7f2 100644 --- a/dwave/gate/qcdl/components.py +++ b/dwave/gate/qcdl/components.py @@ -260,10 +260,12 @@ def register_memory_allocation( value never reaches the qubit. That is almost always a mistake, so it is reported here instead. - Re-declaring the name is allowed when the caller asked for it, but an - explicit initial value is still rejected: opting in to the - re-declaration says the existing memory is wanted, whereas giving a - value says the opposite, and the compiler would ignore it. + Re-declaring the name is allowed when the caller asked for it, but only + without an initial value: opting in to the re-declaration says the + existing memory is wanted, whereas giving a value says the opposite, + and the compiler would ignore it. This applies only once the name is + allocated; a first allocation always takes its value, whatever the + caller opted in to. This method is mostly intended for use by developers of QCDL; the :class:`~dwave.gate.qcdl.registers.Register` and @@ -277,6 +279,7 @@ def register_memory_allocation( allow_existing: If True, an existing allocation of ``name`` is accepted as long as no initial value was given. Set by the ``alias`` and ``ignore_reallocation`` arguments of a register. + It has no effect when ``name`` is not already allocated. initial_value_specified: Whether the caller gave an initial value for this register. diff --git a/dwave/gate/qcdl/registers.py b/dwave/gate/qcdl/registers.py index 872ae4b..160f826 100644 --- a/dwave/gate/qcdl/registers.py +++ b/dwave/gate/qcdl/registers.py @@ -680,11 +680,11 @@ class Register(IntegerOpsMixin, AssignmentOpsMixin, RegisterInitializerMixin, Ta one or more qubits. Typically, you create a register from a :class:`~dwave.gate.qcdl.Scope` object, which handles this parameter for you. - initial_value: Initial value. Defaults to 0. Omit it when reusing a name - that is already allocated (see ``alias`` and - ``ignore_reallocation``): only the first allocation of a name takes - effect, so a value given for a later one is rejected rather than - silently discarded. + initial_value: Initial value. Defaults to 0. Only the first allocation + of a name takes effect, so a value given for a name that is already + allocated is rejected rather than silently discarded; see + ``ignore_reallocation``. An ``alias`` never allocates, so it never + takes a value at all. name: Name for this register; useful for troubleshooting. If None, a name is generated. See the ``alias`` parameter for type punning. master_kwargs: Propagate this to the master instruction. This parameter @@ -693,9 +693,11 @@ class Register(IntegerOpsMixin, AssignmentOpsMixin, RegisterInitializerMixin, Ta type punning reuse that register's name in the ``name`` parameter. Aliased registers are not reinitialized, so you may not give an ``initial_value``. - ignore_reallocation: If True, the compiler does not reallocate if - already allocated (and does not raise an exception). You may not - give an ``initial_value`` as well. + ignore_reallocation: If True, and the name is already allocated, the + compiler does not reallocate it (and does not raise an exception). + In that case you may not give an ``initial_value``, since it would + never reach the qubit. A name that is not yet allocated is + allocated as usual and may carry a value. scope_id: Identity of the :class:`~dwave.gate.qcdl.Scope` this register is derived from. The :meth:`~dwave.gate.qcdl.QCDLModuleContainer.Register` method sets @@ -803,11 +805,11 @@ class FixedPointRegister( one or more qubits. Typically, you create a register from a :class:`~dwave.gate.qcdl.Scope` object, which handles this parameter for you. - initial_value: Initial value. Defaults to 0.0. Omit it when reusing a - name that is already allocated (see ``alias`` and - ``ignore_reallocation``): only the first allocation of a name takes - effect, so a value given for a later one is rejected rather than - silently discarded. + initial_value: Initial value. Defaults to 0.0. Only the first + allocation of a name takes effect, so a value given for a name that + is already allocated is rejected rather than silently discarded; + see ``ignore_reallocation``. An ``alias`` never allocates, so it + never takes a value at all. name: Name for this register; useful for troubleshooting. If None, a name is generated. See the ``alias`` parameter for type punning. master_kwargs: Propagate this to the master instruction. This parameter @@ -816,9 +818,11 @@ class FixedPointRegister( type punning reuse that register's name in the ``name`` parameter. Aliased registers are not reinitialized, so you may not give an ``initial_value``. - ignore_reallocation: If True, the compiler does not reallocate if - already allocated (and does not raise an exception). You may not - give an ``initial_value`` as well. + ignore_reallocation: If True, and the name is already allocated, the + compiler does not reallocate it (and does not raise an exception). + In that case you may not give an ``initial_value``, since it would + never reach the qubit. A name that is not yet allocated is + allocated as usual and may carry a value. scope_id: Identity of the :class:`~dwave.gate.qcdl.Scope` this register is derived from. The :meth:`~dwave.gate.qcdl.QCDLModuleContainer.FixedPointRegister` diff --git a/tests/test_registers.py b/tests/test_registers.py index 7db4680..6eb01d6 100644 --- a/tests/test_registers.py +++ b/tests/test_registers.py @@ -656,6 +656,22 @@ def main(q0): main() +def test_alias_rejects_an_initial_value_on_a_fresh_name_too(): + """This is where alias and ignore_reallocation part ways. + + An alias never allocates, so its value is discarded whether or not the + name is already taken, whereas ignore_reallocation is a no-op only for a + name that is already allocated and so may carry a value otherwise. + """ + + @qcdl(1) + def main(q0): + q0.Register(3, name="fresh", alias=True) + + with pytest.raises(QCDLUserError, match="is an alias"): + main() + + def test_duplicate_array_name_with_ignore_reallocation_still_raises(): """An Array always carries contents, so it can never opt in.""" From eeed80702d8e40566eb775b8d5fcc97a6ee14b99 Mon Sep 17 00:00:00 2001 From: Amos Anderson Date: Wed, 19 Aug 2026 15:11:55 -0400 Subject: [PATCH 03/18] track register names per circuit rather than per procedure Register names are global: a name allocated in one procedure is the same qubit memory as that name in another, so the tracking belongs on the QCDLCircuit state, not on Procedure. A clash now reports the procedure that allocated first, which is the useful half of the information once the name can have come from anywhere. Calling a procedure re-runs its body, though, while the procedure itself is emitted once, so each call would otherwise look like a re-declaration. Every run is a distinct Procedure instance sharing one proc_name, and proc_name is what the circuit already deduplicates procedures on, so an allocation reached through a different run of the same procedure is not treated as a clash. A duplicate within one body still is. Co-Authored-By: Claude Opus 5 (1M context) --- docs/qcdl.rst | 2 +- dwave/gate/qcdl/components.py | 67 +++++++++++++++++++++------- dwave/gate/qcdl/qcdl_circuit.py | 20 ++++++++- tests/test_registers.py | 78 ++++++++++++++++++++++++++++++++- 4 files changed, 148 insertions(+), 19 deletions(-) diff --git a/docs/qcdl.rst b/docs/qcdl.rst index c6ccc32..1ae20b2 100644 --- a/docs/qcdl.rst +++ b/docs/qcdl.rst @@ -64,7 +64,7 @@ These classes are of interest mostly to developers of QCDL. .. automodule:: dwave.gate.qcdl.components :show-inheritance: - :members: Procedure, QCDLModuleName + :members: Procedure, QCDLModuleName, RegisterAllocation .. automodule:: dwave.gate.qcdl.qcdl_models :show-inheritance: diff --git a/dwave/gate/qcdl/components.py b/dwave/gate/qcdl/components.py index edcb7f2..6b8377b 100644 --- a/dwave/gate/qcdl/components.py +++ b/dwave/gate/qcdl/components.py @@ -28,7 +28,7 @@ import types from collections.abc import Mapping, Sequence, Set from contextlib import contextmanager -from typing import TYPE_CHECKING, Any, Callable, Iterator +from typing import TYPE_CHECKING, Any, Callable, Iterator, NamedTuple import numpy as np @@ -76,6 +76,21 @@ def default(self, obj: Any) -> Any: return str(obj) +class RegisterAllocation(NamedTuple): + """One entry in the register names a circuit has allocated. + + Args: + dtype: ``"int"`` or ``"float"``. + procedure: Procedure that made the allocation. The name it was made + under is reported when a later declaration clashes, and the + identity tells a re-run of that same procedure apart from a + genuine re-declaration. + """ + + dtype: str + procedure: Procedure + + class Procedure(IndexerMixin): """A QCDL procedure. @@ -134,10 +149,6 @@ def __init__( # qubits in the statement are not listed in the last item in this list. self._exclusive_modules: list[set[str]] = [] - # register names allocated in this procedure, per module, so that - # re-declaring one can be reported rather than silently discarded - self._allocated_registers: dict[str, dict[str, str]] = {} - self._procedure_ended = False def to_model(self) -> QCDLProcedureDef: @@ -260,6 +271,17 @@ def register_memory_allocation( value never reaches the qubit. That is almost always a mistake, so it is reported here instead. + Register names are global to the circuit rather than local to a + procedure, so the record lives on the + :attr:`~dwave.gate.qcdl.qcdl_circuit.QCDLCircuit.allocated_registers` + attribute of the state, and a name taken in one procedure clashes with + the same name in another. + + A procedure body is re-executed on every call while the program is + being built, but is emitted once, so a declaration reached through a + later run of the *same* procedure is not a re-declaration and is not + reported. + Re-declaring the name is allowed when the caller asked for it, but only without an initial value: opting in to the re-declaration says the existing memory is wanted, whereas giving a value says the opposite, @@ -289,33 +311,48 @@ def register_memory_allocation( ``allow_existing`` is False or an initial value was given. """ for module in modules: - allocated = self._allocated_registers.setdefault( + allocated = self.state.allocated_registers.setdefault( module.qcdl_module_name, {} ) previous = allocated.get(name) + if previous is not None and self._is_rerun_of(previous.procedure): + previous = None if previous is not None and not ( allow_existing and not initial_value_specified ): if allow_existing: raise QCDLUserError( f"register {name!r} is already allocated on" - f" {module.qcdl_module_name} with dtype {previous} in" - f" procedure {self.name}, and the compiler keeps the" - f" first allocation, so the initial value given here" - f" would never reach the qubit. Re-declaring the name is" - f" allowed, but giving it a value is not: drop the" - f" initial value." + f" {module.qcdl_module_name} with dtype" + f" {previous.dtype} in procedure" + f" {previous.procedure.name}, and the compiler keeps" + f" the first allocation, so the initial value given" + f" here would never reach the qubit. Re-declaring the" + f" name is allowed, but giving it a value is not: drop" + f" the initial value." ) raise QCDLUserError( f"register {name!r} is already allocated on" - f" {module.qcdl_module_name} with dtype {previous} in" - f" procedure {self.name}; the compiler keeps the first" + f" {module.qcdl_module_name} with dtype {previous.dtype} in" + f" procedure {previous.procedure.name}; register names are" + f" global to the circuit and the compiler keeps the first" f" allocation, so this one would be discarded. Reuse the" f" existing register, pick another name, or redeclare it" f" deliberately with alias=True or ignore_reallocation=True" f" and no initial value." ) - allocated[name] = dtype + allocated[name] = RegisterAllocation(dtype, self) + + def _is_rerun_of(self, other: Procedure) -> bool: + """Whether ``other`` is an earlier run of the procedure ``self`` is. + + Calling a procedure runs its body again, so a register it declares is + seen once per call even though the procedure is emitted once. Those + runs are separate :class:`.Procedure` instances sharing a name, and the + name is what the rest of the circuit deduplicates on, so matching on it + here agrees with what ends up in the program. + """ + return other is not self and other.proc_name == self.proc_name @property def expression_queue(self) -> list | None: diff --git a/dwave/gate/qcdl/qcdl_circuit.py b/dwave/gate/qcdl/qcdl_circuit.py index 7df00fc..80b4ee5 100644 --- a/dwave/gate/qcdl/qcdl_circuit.py +++ b/dwave/gate/qcdl/qcdl_circuit.py @@ -26,7 +26,7 @@ import numpy as np from .base import IndexerMixin -from .components import Procedure, QCDLModule +from .components import Procedure, QCDLModule, RegisterAllocation from .exceptions import QCDLInternalError, QCDLUserError from .qcdl_models import QCDLProgram, QCDLModuleName, QCDLProcedureDef from .transformer import print_qcdl @@ -120,6 +120,11 @@ def __init__( self._main: Procedure | None = None self._program: QCDLProcedureDef | None = None self._procedures: dict[str, QCDLProcedureDef] = {} + + # register names are global to a circuit rather than local to a + # procedure, so they are tracked here rather than on Procedure + self._allocated_registers: dict[str, dict[str, RegisterAllocation]] = {} + self._validate_non_deterministic_qubits_mid = ( validate_non_deterministic_qubits_mid ) @@ -485,6 +490,19 @@ def set_or_check_nondeterministic_modules( def procedures(self) -> dict[str, QCDLProcedureDef]: return self._procedures + @property + def allocated_registers(self) -> dict[str, dict[str, RegisterAllocation]]: + """Register names allocated so far, by module name then register name. + + Register names are global to a circuit: a name allocated in one + procedure is the same memory as that name in another, and only the + first allocation of it takes effect. The + :meth:`~dwave.gate.qcdl.components.Procedure.register_memory_allocation` + method maintains this, and reports a second allocation rather than + letting it be discarded. + """ + return self._allocated_registers + def get_procedure(self, procedure_name: str) -> QCDLProcedureDef | None: return self.procedures.get(procedure_name) diff --git a/tests/test_registers.py b/tests/test_registers.py index 6eb01d6..81800a7 100644 --- a/tests/test_registers.py +++ b/tests/test_registers.py @@ -768,8 +768,8 @@ def main(q0): main() -def test_register_names_are_tracked_per_procedure(): - """A procedure has its own statements, so it may reuse a name.""" +def test_register_names_are_global_across_procedures(): + """A procedure does not get its own namespace; the memory is the qubit's.""" @procedure def inner(qa): @@ -781,6 +781,80 @@ def main(q0): q0.Register(name="local") inner(q0) + with pytest.raises(QCDLUserError) as cm: + main() + message = str(cm.value) + assert "'local' is already allocated on q0" in message + # the clash is with a register declared in another procedure, so the + # message has to say which one rather than naming the current procedure + assert "in procedure main" in message + + +def test_a_clash_reports_the_procedure_that_allocated_first(): + @procedure + def inner(qa): + qa.Register(name="shared") + qa.rx(0.123) + + @qcdl(1) + def main(q0): + inner(q0) + q0.Register(name="shared") + + with pytest.raises(QCDLUserError, match="in procedure inner_q0"): + main() + + +def test_calling_a_procedure_twice_is_not_a_duplicate(): + """Each call re-runs the body, but the procedure is emitted once.""" + + @procedure + def inner(qa): + qa.Register(3, name="local") + qa.rx(0.123) + + @qcdl(1) + def main(q0): + inner(q0) + inner(q0) + + program = main() + allocations = [ + s for s in program.procedures["inner_q0"].statements + if s.op == "allocate_memory" + ] + assert [a.kwargs["initial_value"] for a in allocations] == [3] + + +def test_a_procedure_still_may_not_declare_a_name_twice_itself(): + """The re-run allowance must not blind the check inside one body.""" + + @procedure + def inner(qa): + qa.Register(1, name="local") + qa.Register(2, name="local") + + @qcdl(1) + def main(q0): + inner(q0) + + with pytest.raises(QCDLUserError, match="'local' is already allocated"): + main() + + +def test_two_procedures_may_use_a_name_on_different_qubits(): + """Names are global to the circuit but still per qubit.""" + + @procedure + def inner(qa): + qa.Register(name="local") + qa.rx(0.123) + + @qcdl(2) + def main(q0, q1): + inner(q0) + inner(q1) + assert main() From 51c12997a315464f146afc80b5c0d766c1e352f2 Mon Sep 17 00:00:00 2001 From: Amos Anderson Date: Tue, 25 Aug 2026 10:33:14 -0400 Subject: [PATCH 04/18] revert qcdl.rst --- docs/qcdl.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/qcdl.rst b/docs/qcdl.rst index 1ae20b2..c6ccc32 100644 --- a/docs/qcdl.rst +++ b/docs/qcdl.rst @@ -64,7 +64,7 @@ These classes are of interest mostly to developers of QCDL. .. automodule:: dwave.gate.qcdl.components :show-inheritance: - :members: Procedure, QCDLModuleName, RegisterAllocation + :members: Procedure, QCDLModuleName .. automodule:: dwave.gate.qcdl.qcdl_models :show-inheritance: From 145a0b6e4c46265792572d543a7da6895c065414 Mon Sep 17 00:00:00 2001 From: Amos Anderson Date: Tue, 25 Aug 2026 10:36:27 -0400 Subject: [PATCH 05/18] manually fixed QCDLModule capitalization --- dwave/gate/qcdl/components.py | 10 ++++++---- dwave/gate/qcdl/qcdl_circuit.py | 14 +++++--------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/dwave/gate/qcdl/components.py b/dwave/gate/qcdl/components.py index 6b8377b..f591a66 100644 --- a/dwave/gate/qcdl/components.py +++ b/dwave/gate/qcdl/components.py @@ -447,9 +447,11 @@ def add_statement( qubit=QCDLModuleName.model_validate(qubit) if qubit is not None else None, args=list(args) if args else [], kwargs=dict(kwargs) if kwargs else {}, - caller_qubits=[QCDLModuleName.model_validate(q) for q in caller_qubits] - if caller_qubits - else [], + caller_qubits=( + [QCDLModuleName.model_validate(q) for q in caller_qubits] + if caller_qubits + else [] + ), ) if not stmt.qubits: @@ -1881,7 +1883,7 @@ def from_rewrapping(m: QCDLModule, new_proc: Procedure) -> QCDLModule: return QCDLModule(m.qcdl_module_name, proc) @property - def qcdl_modules(self) -> tuple[QcdlModule]: + def qcdl_modules(self) -> tuple[QCDLModule]: """The :class:`~dwave.gate.qcdl.QCDLModule` this container holds. diff --git a/dwave/gate/qcdl/qcdl_circuit.py b/dwave/gate/qcdl/qcdl_circuit.py index 80b4ee5..b44783d 100644 --- a/dwave/gate/qcdl/qcdl_circuit.py +++ b/dwave/gate/qcdl/qcdl_circuit.py @@ -38,8 +38,7 @@ class Environment(Protocol): """Structural type for environments.""" - def get_modules(self, include_couplers: bool) -> Iterable[Any]: - ... + def get_modules(self, include_couplers: bool) -> Iterable[Any]: ... class Machine(Protocol): @@ -47,14 +46,11 @@ class Machine(Protocol): environment: Environment - def get_system(self, name: str) -> QCDLModule: - ... + def get_system(self, name: str) -> QCDLModule: ... - def set_up_systems(self, systems: dict[str, Any], procedure: Procedure) -> None: - ... + def set_up_systems(self, systems: dict[str, Any], procedure: Procedure) -> None: ... - def clean_up_systems(self, systems: dict[str, Any]) -> None: - ... + def clean_up_systems(self, systems: dict[str, Any]) -> None: ... class QCDLCircuit(IndexerMixin): @@ -194,7 +190,7 @@ def all_modules(self) -> dict[str, QCDLModule] | None: These objects are not necessarily ready to be used as-is in a circuit, needing to be rewrapped based on the procedure. Use the - :meth:`~dwave.gate.qcdl.QcdlModule.get_other_qcdl_module` instead of + :meth:`~dwave.gate.qcdl.QCDLModule.get_other_qcdl_module` instead of accessing this property directly. Returns: From cbb8f2cd501559482fa5335e3c281d45d02badda Mon Sep 17 00:00:00 2001 From: Amos Anderson Date: Tue, 25 Aug 2026 10:43:21 -0400 Subject: [PATCH 06/18] fix grammar --- dwave/gate/qcdl/components.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dwave/gate/qcdl/components.py b/dwave/gate/qcdl/components.py index f591a66..070c761 100644 --- a/dwave/gate/qcdl/components.py +++ b/dwave/gate/qcdl/components.py @@ -77,14 +77,14 @@ def default(self, obj: Any) -> Any: class RegisterAllocation(NamedTuple): - """One entry in the register names a circuit has allocated. + """One register a circuit has allocated, held under its module and name. Args: dtype: ``"int"`` or ``"float"``. - procedure: Procedure that made the allocation. The name it was made - under is reported when a later declaration clashes, and the - identity tells a re-run of that same procedure apart from a - genuine re-declaration. + procedure: Procedure that made the allocation. Its name is reported + when a later declaration of the same register name clashes, and + comparing it against the declaring procedure separates a re-run of + that same procedure from a genuine re-declaration. """ dtype: str @@ -344,7 +344,7 @@ def register_memory_allocation( allocated[name] = RegisterAllocation(dtype, self) def _is_rerun_of(self, other: Procedure) -> bool: - """Whether ``other`` is an earlier run of the procedure ``self`` is. + """Whether ``other`` is an earlier run of the procedure that ``self`` is. Calling a procedure runs its body again, so a register it declares is seen once per call even though the procedure is emitted once. Those From e594c9c246cabd92981dfffff641cf130c227c8d Mon Sep 17 00:00:00 2001 From: Amos Anderson <45039789+qci-amos@users.noreply.github.com> Date: Tue, 25 Aug 2026 10:47:21 -0400 Subject: [PATCH 07/18] Apply suggestions from code review Co-authored-by: Joel Pasvolsky <34041130+JoelPasvolsky@users.noreply.github.com> --- dwave/gate/qcdl/components.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dwave/gate/qcdl/components.py b/dwave/gate/qcdl/components.py index 070c761..d1a2583 100644 --- a/dwave/gate/qcdl/components.py +++ b/dwave/gate/qcdl/components.py @@ -266,6 +266,11 @@ def register_memory_allocation( ) -> None: """Record a register allocation, rejecting a silent re-declaration. + This method is mostly intended for use by developers of QCDL; the + :class:`~dwave.gate.qcdl.registers.Register` and + :class:`~dwave.gate.qcdl.registers.FixedPointRegister` classes call it + for you. + The compiler keeps the *first* allocation of a name, so a second declaration of the same name on the same module is a no-op: its initial value never reaches the qubit. That is almost always a mistake, so it is @@ -289,11 +294,6 @@ def register_memory_allocation( allocated; a first allocation always takes its value, whatever the caller opted in to. - This method is mostly intended for use by developers of QCDL; the - :class:`~dwave.gate.qcdl.registers.Register` and - :class:`~dwave.gate.qcdl.registers.FixedPointRegister` classes call it - for you. - Args: modules: Modules the register is allocated on. name: Name of the register. From b8b8adc1bd23e869ea7f7b224f9cca09e8d710f2 Mon Sep 17 00:00:00 2001 From: Amos Anderson Date: Tue, 25 Aug 2026 10:52:27 -0400 Subject: [PATCH 08/18] improve the wording around register allocation and its initial value --- dwave/gate/qcdl/components.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dwave/gate/qcdl/components.py b/dwave/gate/qcdl/components.py index d1a2583..6a9b644 100644 --- a/dwave/gate/qcdl/components.py +++ b/dwave/gate/qcdl/components.py @@ -271,9 +271,10 @@ def register_memory_allocation( :class:`~dwave.gate.qcdl.registers.FixedPointRegister` classes call it for you. - The compiler keeps the *first* allocation of a name, so a second + Register allocation and initialization happens at compile time, not run + time. The compiler keeps the *first* allocation of a name, so a second declaration of the same name on the same module is a no-op: its initial - value never reaches the qubit. That is almost always a mistake, so it is + value would not be used. That is almost always a mistake, so it is reported here instead. Register names are global to the circuit rather than local to a From 66eb3e52cda574f3062eac94ee7f4b30052c9449 Mon Sep 17 00:00:00 2001 From: Amos Anderson Date: Tue, 25 Aug 2026 11:03:01 -0400 Subject: [PATCH 09/18] tweak the wording for clarity --- dwave/gate/qcdl/components.py | 41 ++++++++++++++++------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/dwave/gate/qcdl/components.py b/dwave/gate/qcdl/components.py index 6a9b644..f519253 100644 --- a/dwave/gate/qcdl/components.py +++ b/dwave/gate/qcdl/components.py @@ -272,28 +272,25 @@ def register_memory_allocation( for you. Register allocation and initialization happens at compile time, not run - time. The compiler keeps the *first* allocation of a name, so a second - declaration of the same name on the same module is a no-op: its initial - value would not be used. That is almost always a mistake, so it is - reported here instead. - - Register names are global to the circuit rather than local to a - procedure, so the record lives on the - :attr:`~dwave.gate.qcdl.qcdl_circuit.QCDLCircuit.allocated_registers` - attribute of the state, and a name taken in one procedure clashes with - the same name in another. - - A procedure body is re-executed on every call while the program is - being built, but is emitted once, so a declaration reached through a - later run of the *same* procedure is not a re-declaration and is not - reported. - - Re-declaring the name is allowed when the caller asked for it, but only - without an initial value: opting in to the re-declaration says the - existing memory is wanted, whereas giving a value says the opposite, - and the compiler would ignore it. This applies only once the name is - allocated; a first allocation always takes its value, whatever the - caller opted in to. + time, and hence are global to the circuit rather than local to a + procedure. Consequently, the compiler keeps the *first* allocation of a + name it finds when traversing the procedures, so a second declaration of + the same name on the same module is a no-op: its initial value would not + be used. That is almost always a mistake, so it is reported here + instead. + + A procedure body is re-executed on every call while the program is being + built, but is emitted once in the QCDLProgram, so a declaration reached + through a later run of the *same* procedure is not a re-declaration and + is not reported. + + Re-declaring the name is allowed when the caller asked for it. This can + be used to obtain new :class:`~dwave.gate.qcdl.registers.Register` or + :class:`~dwave.gate.qcdl.registers.FixedPointRegister` instances which + are useful for creating additional expressions on the previously + allocated memory. To avoid re-declarations that would attempt to + reallocate memory, this use case is opt-in using allow_existing and if + initial_value_specified is False. Args: modules: Modules the register is allocated on. From e8aaa5ccd392399d3d87cf415e117b7e32e23648 Mon Sep 17 00:00:00 2001 From: Amos Anderson Date: Tue, 25 Aug 2026 11:10:14 -0400 Subject: [PATCH 10/18] simplify error message --- dwave/gate/qcdl/components.py | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/dwave/gate/qcdl/components.py b/dwave/gate/qcdl/components.py index f519253..6875aca 100644 --- a/dwave/gate/qcdl/components.py +++ b/dwave/gate/qcdl/components.py @@ -318,26 +318,20 @@ def register_memory_allocation( if previous is not None and not ( allow_existing and not initial_value_specified ): + where = ( + f"register {name!r} is already allocated on" + f" {module.qcdl_module_name} with dtype {previous.dtype}" + f" in procedure {previous.procedure.name}" + ) if allow_existing: raise QCDLUserError( - f"register {name!r} is already allocated on" - f" {module.qcdl_module_name} with dtype" - f" {previous.dtype} in procedure" - f" {previous.procedure.name}, and the compiler keeps" - f" the first allocation, so the initial value given" - f" here would never reach the qubit. Re-declaring the" - f" name is allowed, but giving it a value is not: drop" - f" the initial value." + f"{where}, so this initial value would never reach the" + f" qubit; drop the initial value" ) raise QCDLUserError( - f"register {name!r} is already allocated on" - f" {module.qcdl_module_name} with dtype {previous.dtype} in" - f" procedure {previous.procedure.name}; register names are" - f" global to the circuit and the compiler keeps the first" - f" allocation, so this one would be discarded. Reuse the" - f" existing register, pick another name, or redeclare it" - f" deliberately with alias=True or ignore_reallocation=True" - f" and no initial value." + f"{where}, so this declaration would be discarded; reuse" + f" that register, pick another name, or pass alias=True or" + f" ignore_reallocation=True with no initial value" ) allocated[name] = RegisterAllocation(dtype, self) From 04a4f440845bda923c1eb6a92d0ea409a7d6679e Mon Sep 17 00:00:00 2001 From: Amos Anderson Date: Tue, 25 Aug 2026 11:20:51 -0400 Subject: [PATCH 11/18] improve wording in docstring --- dwave/gate/qcdl/components.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/dwave/gate/qcdl/components.py b/dwave/gate/qcdl/components.py index 6875aca..30f1e24 100644 --- a/dwave/gate/qcdl/components.py +++ b/dwave/gate/qcdl/components.py @@ -336,13 +336,10 @@ def register_memory_allocation( allocated[name] = RegisterAllocation(dtype, self) def _is_rerun_of(self, other: Procedure) -> bool: - """Whether ``other`` is an earlier run of the procedure that ``self`` is. + """Whether ``other`` is an earlier run of the user's same @procedure + decorated Python code. - Calling a procedure runs its body again, so a register it declares is - seen once per call even though the procedure is emitted once. Those - runs are separate :class:`.Procedure` instances sharing a name, and the - name is what the rest of the circuit deduplicates on, so matching on it - here agrees with what ends up in the program. + This logic is useful for tracking register allocations. """ return other is not self and other.proc_name == self.proc_name From 15e1b975045a5e48ae725f1db0e5a2bbca8a8081 Mon Sep 17 00:00:00 2001 From: Amos Anderson Date: Tue, 25 Aug 2026 11:26:32 -0400 Subject: [PATCH 12/18] improve wording in example --- dwave/gate/qcdl/components.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dwave/gate/qcdl/components.py b/dwave/gate/qcdl/components.py index 30f1e24..5592286 100644 --- a/dwave/gate/qcdl/components.py +++ b/dwave/gate/qcdl/components.py @@ -1334,8 +1334,10 @@ def all_to_all_use(q0, q1): sc = Scope(q0, q1) r1 = sc.Register(name="r1") h(q0) - # alias=True reuses the memory r1 already allocated, so the - # outcome is stored on q0 only rather than mirrored + # alias=True reuses the memory r1 already allocated, so this + # illustrates a way to store this outcome only on q0 rather + # than mirrored to all the qubits in the Scope where r1 was + # originally allocated. measure(q0, register=q0.Register(name="r1", alias=True)) sc.all_to_all(send=r1==1, reduce_op="&") with sc.If(None): From a31a4f9530bcc09704b85c848447b079990e412a Mon Sep 17 00:00:00 2001 From: Amos Anderson Date: Tue, 25 Aug 2026 11:28:13 -0400 Subject: [PATCH 13/18] add a word --- dwave/gate/qcdl/components.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dwave/gate/qcdl/components.py b/dwave/gate/qcdl/components.py index 5592286..3997ceb 100644 --- a/dwave/gate/qcdl/components.py +++ b/dwave/gate/qcdl/components.py @@ -1335,9 +1335,9 @@ def all_to_all_use(q0, q1): r1 = sc.Register(name="r1") h(q0) # alias=True reuses the memory r1 already allocated, so this - # illustrates a way to store this outcome only on q0 rather - # than mirrored to all the qubits in the Scope where r1 was - # originally allocated. + # example illustrates a way to store this outcome only on q0 + # rather than mirrored to all the qubits in the Scope where + # r1 was originally allocated. measure(q0, register=q0.Register(name="r1", alias=True)) sc.all_to_all(send=r1==1, reduce_op="&") with sc.If(None): From 27bfabd74acd82089885b478bc104b2c7d66349d Mon Sep 17 00:00:00 2001 From: Amos Anderson Date: Tue, 25 Aug 2026 11:30:10 -0400 Subject: [PATCH 14/18] simplify doc --- dwave/gate/qcdl/qcdl_circuit.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dwave/gate/qcdl/qcdl_circuit.py b/dwave/gate/qcdl/qcdl_circuit.py index b44783d..67e2a14 100644 --- a/dwave/gate/qcdl/qcdl_circuit.py +++ b/dwave/gate/qcdl/qcdl_circuit.py @@ -494,8 +494,7 @@ def allocated_registers(self) -> dict[str, dict[str, RegisterAllocation]]: procedure is the same memory as that name in another, and only the first allocation of it takes effect. The :meth:`~dwave.gate.qcdl.components.Procedure.register_memory_allocation` - method maintains this, and reports a second allocation rather than - letting it be discarded. + method maintains this, and raises an exception on a second allocation. """ return self._allocated_registers From b555b989071564e74d1704dd9a20f4d2e3bbf7ba Mon Sep 17 00:00:00 2001 From: Amos Anderson Date: Tue, 25 Aug 2026 11:34:40 -0400 Subject: [PATCH 15/18] simplify wording further --- dwave/gate/qcdl/registers.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dwave/gate/qcdl/registers.py b/dwave/gate/qcdl/registers.py index 160f826..29b1674 100644 --- a/dwave/gate/qcdl/registers.py +++ b/dwave/gate/qcdl/registers.py @@ -266,10 +266,8 @@ def _register_initialization( if alias is True and initial_value_specified: raise QCDLUserError( - f"register {name!r} is an alias, so no memory is allocated for" - f" it and the initial value given here would never reach the" - f" qubit; drop the initial value, or drop alias=True to" - f" allocate new memory" + f"register {name!r} is an alias, so this initial value would" + f" never reach the qubit; drop the initial value" ) # An alias deliberately names memory that already exists, and From 5d6364c8d34811931935e24bc21f89900431b2f8 Mon Sep 17 00:00:00 2001 From: Amos Anderson <45039789+qci-amos@users.noreply.github.com> Date: Tue, 25 Aug 2026 11:35:31 -0400 Subject: [PATCH 16/18] Apply suggestions from code review Co-authored-by: Joel Pasvolsky <34041130+JoelPasvolsky@users.noreply.github.com> --- dwave/gate/qcdl/registers.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/dwave/gate/qcdl/registers.py b/dwave/gate/qcdl/registers.py index 29b1674..842d250 100644 --- a/dwave/gate/qcdl/registers.py +++ b/dwave/gate/qcdl/registers.py @@ -691,11 +691,8 @@ class Register(IntegerOpsMixin, AssignmentOpsMixin, RegisterInitializerMixin, Ta type punning reuse that register's name in the ``name`` parameter. Aliased registers are not reinitialized, so you may not give an ``initial_value``. - ignore_reallocation: If True, and the name is already allocated, the + ignore_reallocation: If True, and the ``name`` is already allocated, the compiler does not reallocate it (and does not raise an exception). - In that case you may not give an ``initial_value``, since it would - never reach the qubit. A name that is not yet allocated is - allocated as usual and may carry a value. scope_id: Identity of the :class:`~dwave.gate.qcdl.Scope` this register is derived from. The :meth:`~dwave.gate.qcdl.QCDLModuleContainer.Register` method sets @@ -804,10 +801,8 @@ class FixedPointRegister( :class:`~dwave.gate.qcdl.Scope` object, which handles this parameter for you. initial_value: Initial value. Defaults to 0.0. Only the first - allocation of a name takes effect, so a value given for a name that - is already allocated is rejected rather than silently discarded; - see ``ignore_reallocation``. An ``alias`` never allocates, so it - never takes a value at all. + allocation of a ``name`` takes effect, so a value given for a name that + is already allocated is rejected; see ``ignore_reallocation`` name: Name for this register; useful for troubleshooting. If None, a name is generated. See the ``alias`` parameter for type punning. master_kwargs: Propagate this to the master instruction. This parameter @@ -818,9 +813,6 @@ class FixedPointRegister( ``initial_value``. ignore_reallocation: If True, and the name is already allocated, the compiler does not reallocate it (and does not raise an exception). - In that case you may not give an ``initial_value``, since it would - never reach the qubit. A name that is not yet allocated is - allocated as usual and may carry a value. scope_id: Identity of the :class:`~dwave.gate.qcdl.Scope` this register is derived from. The :meth:`~dwave.gate.qcdl.QCDLModuleContainer.FixedPointRegister` From a05bac727b87f0eaee63c3203dc406a868fda2ef Mon Sep 17 00:00:00 2001 From: Amos Anderson <45039789+qci-amos@users.noreply.github.com> Date: Tue, 25 Aug 2026 11:35:51 -0400 Subject: [PATCH 17/18] Apply suggestions from code review Co-authored-by: Joel Pasvolsky <34041130+JoelPasvolsky@users.noreply.github.com> --- dwave/gate/qcdl/registers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dwave/gate/qcdl/registers.py b/dwave/gate/qcdl/registers.py index 842d250..9af882e 100644 --- a/dwave/gate/qcdl/registers.py +++ b/dwave/gate/qcdl/registers.py @@ -679,7 +679,7 @@ class Register(IntegerOpsMixin, AssignmentOpsMixin, RegisterInitializerMixin, Ta :class:`~dwave.gate.qcdl.Scope` object, which handles this parameter for you. initial_value: Initial value. Defaults to 0. Only the first allocation - of a name takes effect, so a value given for a name that is already + of a name takes effect, so a value given for a ``name`` that is already allocated is rejected rather than silently discarded; see ``ignore_reallocation``. An ``alias`` never allocates, so it never takes a value at all. From dbd7f527a786e979cad0e5aca2e05d1289625371 Mon Sep 17 00:00:00 2001 From: Amos Anderson Date: Tue, 25 Aug 2026 11:38:42 -0400 Subject: [PATCH 18/18] reno note --- ...-register-allocation-2aafa0fdc9ac4506.yaml | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 releasenotes/notes/report-duplicate-register-allocation-2aafa0fdc9ac4506.yaml diff --git a/releasenotes/notes/report-duplicate-register-allocation-2aafa0fdc9ac4506.yaml b/releasenotes/notes/report-duplicate-register-allocation-2aafa0fdc9ac4506.yaml new file mode 100644 index 0000000..c7a90e0 --- /dev/null +++ b/releasenotes/notes/report-duplicate-register-allocation-2aafa0fdc9ac4506.yaml @@ -0,0 +1,30 @@ +--- +upgrade: + - | + Allocating a register name twice on the same module now raises + ``QCDLUserError``. Register names are global to a circuit rather than local + to a procedure, and the compiler keeps only the first allocation of a name, + so a second declaration was previously discarded without a word. A program + that re-declared a name to get another handle on the same memory now has to + say so: pass ``alias=True`` to reuse the memory the name already has, or + ``ignore_reallocation=True`` with no initial value. A declaration reached + through a later run of the *same* procedure is not a re-declaration and is + still accepted. + - | + The ``initial_value`` argument of ``Register`` and ``FixedPointRegister`` + now defaults to ``None`` rather than to ``0`` and ``0.0``, so that a value + the caller gave can be told from one that was omitted. Omitting it still + allocates zero. Passing ``0`` or ``0.0`` explicitly now counts as giving a + value, and so is rejected for a name that is already allocated. +fixes: + - | + An ``initial_value`` that could never reach the qubit is now reported rather + than silently dropped. An ``alias`` never allocates memory, so it never + takes a value at all, and ``ignore_reallocation`` accepts a name that is + already allocated only when no value is given. A name that is not yet + allocated is allocated as usual and may carry a value. +features: + - | + Add the ``QCDLCircuit.allocated_registers`` property, which reports the + register names allocated so far by module name and then register name, + together with each one's dtype and the procedure that allocated it.