Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c329fb6
report a register name that is allocated twice
qci-amos Aug 19, 2026
588b900
say precisely when ignore_reallocation forbids an initial value
qci-amos Aug 19, 2026
eeed807
track register names per circuit rather than per procedure
qci-amos Aug 19, 2026
57c46de
Merge branch 'main' into qcdl/report-duplicate-register-allocation
qci-amos Aug 25, 2026
51c1299
revert qcdl.rst
qci-amos Aug 25, 2026
145a0b6
manually fixed QCDLModule capitalization
qci-amos Aug 25, 2026
cbb8f2c
fix grammar
qci-amos Aug 25, 2026
e594c9c
Apply suggestions from code review
qci-amos Aug 25, 2026
b8b8adc
improve the wording around register allocation and its initial value
qci-amos Aug 25, 2026
66eb3e5
tweak the wording for clarity
qci-amos Aug 25, 2026
e8aaa5c
simplify error message
qci-amos Aug 25, 2026
04a4f44
improve wording in docstring
qci-amos Aug 25, 2026
15e1b97
improve wording in example
qci-amos Aug 25, 2026
a31a4f9
add a word
qci-amos Aug 25, 2026
27bfabd
simplify doc
qci-amos Aug 25, 2026
b555b98
simplify wording further
qci-amos Aug 25, 2026
5d6364c
Apply suggestions from code review
qci-amos Aug 25, 2026
a05bac7
Apply suggestions from code review
qci-amos Aug 25, 2026
dbd7f52
reno note
qci-amos Aug 25, 2026
719ab26
Merge branch 'main' into qcdl/report-duplicate-register-allocation
qci-amos Sep 10, 2026
b6058b2
Merge branch 'main' into qcdl/report-duplicate-register-allocation
qci-amos Sep 10, 2026
be2ce1e
Merge branch 'main' into qcdl/report-duplicate-register-allocation
qci-amos Sep 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/workflow.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1085,8 +1085,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.
Expand Down
19 changes: 18 additions & 1 deletion dwave/gate/qcdl/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,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 .models import QCDLProgram, QCDLModuleName, QCDLProcedureDef
from .transformer import transform_program_to_qcdl_str, transform_qcdl
Expand Down Expand Up @@ -117,6 +117,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
)
Expand Down Expand Up @@ -482,6 +487,18 @@ 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 raises an exception on a second allocation.
"""
return self._allocated_registers

def get_procedure(self, procedure_name: str) -> QCDLProcedureDef | None:
return self.procedures.get(procedure_name)

Expand Down
112 changes: 109 additions & 3 deletions dwave/gate/qcdl/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -76,6 +76,21 @@ def default(self, obj: Any) -> Any:
return str(obj)


class RegisterAllocation(NamedTuple):
"""One register a circuit has allocated, held under its module and name.

Args:
dtype: ``"int"`` or ``"float"``.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
dtype: ``"int"`` or ``"float"``.
dtype: ``"int"`` for a :class:`~dwave.gate.qcdl.registers.Register` or
``"float"`` for a
:class:`~dwave.gate.qcdl.registers.FixedPointRegister`.

This is my guess, the intention is to let the user know what each of the dtypes are meant for.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's also Array... I think it's ok to leave it non-specific as "register"?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

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
procedure: Procedure


class Procedure(IndexerMixin):
"""A QCDL procedure.

Expand Down Expand Up @@ -241,6 +256,93 @@ 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.

Comment thread
qci-amos marked this conversation as resolved.
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.

Register allocation and initialization happens at compile time, not run
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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
modules: Modules the register is allocated on.
modules: Modules, typically qubits, the register is allocated on.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to keep it abstract as "modules" here because it could be couplers and we want to hide these details in the abstraction.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have been using the phrase with the "typically" for exactly that purpose for this first release. The idea being to help a new user understand what a module might be (we never replace "module" with just "qubit" in such places).
Up to you

name: Name of the register.
dtype: ``"int"`` or ``"float"``.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
dtype: ``"int"`` or ``"float"``.
dtype: ``"int"`` for a :class:`~dwave.gate.qcdl.registers.Register` or
``"float"`` for a
:class:`~dwave.gate.qcdl.registers.FixedPointRegister`.

This is my guess, the intention is to let the user know what each of the dtypes are meant for.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's also used for Array (and other contexts like arbitrary function also have a dtype). Are you comfortable just leaving it abstract with "register" here?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

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.
Comment on lines +299 to +302

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.
allow_existing: If True, allow a register declaration that reuses
``name`` if no initial value is specified. You set this by specifying
the ``alias`` and ``ignore_reallocation`` arguments in a
:class:`~dwave.gate.qcdl.registers.Register` or
:class:`~dwave.gate.qcdl.registers.FixedPointRegister`
instantiation. Ignored when ``name`` is not already allocated.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.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
):
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"{where}, so this initial value would never reach the"
f" qubit; drop the initial value"
)
raise QCDLUserError(
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)

def _is_rerun_of(self, other: Procedure) -> bool:
"""Whether ``other`` is an earlier run of the user's same @procedure
decorated Python code.

This logic is useful for tracking register allocations.
"""
return other is not self and other.proc_name == self.proc_name

@property
def expression_queue(self) -> list | None:
"""Create an expression queue.
Expand Down Expand Up @@ -1234,7 +1336,11 @@ 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 this
# 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):
x(q1)
Expand Down Expand Up @@ -1820,7 +1926,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.

Expand Down
51 changes: 41 additions & 10 deletions dwave/gate/qcdl/registers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -257,6 +264,22 @@ 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 this initial value would"
f" never reach the qubit; drop the initial value"
)

# 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
Expand Down Expand Up @@ -655,16 +678,21 @@ 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. 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
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.
ignore_reallocation: If True, the compiler does not reallocate if
already allocated (and does not raise an exception).
Aliased registers are not reinitialized, so you may not give an
``initial_value``.
ignore_reallocation: If True, and the ``name`` is already allocated, the
compiler does not reallocate it (and does not raise an exception).
scope_id: Identity of the :class:`~dwave.gate.qcdl.Scope` this register
is derived from. The
:meth:`~dwave.gate.qcdl.QCDLModuleContainer.Register` method sets
Expand Down Expand Up @@ -727,7 +755,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,
Expand Down Expand Up @@ -772,16 +800,19 @@ 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. Only the first
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
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.
ignore_reallocation: If True, the compiler does not reallocate if
already allocated (and does not raise an exception).
Aliased registers are not reinitialized, so you may not give an
``initial_value``.
ignore_reallocation: If True, and the name is already allocated, the
compiler does not reallocate it (and does not raise an exception).
scope_id: Identity of the :class:`~dwave.gate.qcdl.Scope` this register
is derived from. The
:meth:`~dwave.gate.qcdl.QCDLModuleContainer.FixedPointRegister`
Expand Down Expand Up @@ -825,7 +856,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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Loading