Skip to content

QCDL registers: report a register name that is allocated twice - #73

Open
qci-amos wants to merge 3 commits into
mainfrom
qcdl/report-duplicate-register-allocation
Open

QCDL registers: report a register name that is allocated twice#73
qci-amos wants to merge 3 commits into
mainfrom
qcdl/report-duplicate-register-allocation

Conversation

@qci-amos

@qci-amos qci-amos commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Part of splitting #71 into reviewable pieces. Independent of the other four.

The problem

The compiler keeps the first allocation of a register 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 was silent, and it is almost always a mistake.

The signal example in the user guide was itself relying on it:

send_register = sc.Register(name=name)          # allocates on q0 and q1
measure(q0, register=q0.Register(name=name))    # silently discarded

The change

Allocated register names are tracked on the circuit and a re-declaration
raises QCDLUserError, naming the dtype it already has, the procedure that
allocated it first, and the ways to say the reuse was deliberate.

Scope. Names are global to the circuit, not local to a procedure — a name
allocated in one procedure is the same qubit memory as that name in another —
so the record lives on QCDLCircuit.allocated_registers rather than on
Procedure. It is still keyed per module, so q0 and q1 may each hold a
register called "r0".

Procedure re-runs. Calling a procedure re-runs its body while the program
is being built, even though the procedure is emitted once, so every call would
otherwise look like a re-declaration of whatever registers it declares. Each
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.

The two opt-outs stay available, and differ in scope:

  • alias=True is always an alias and never allocates, so an
    initial_value is rejected whatever the state of the name — already
    allocated or not.
  • ignore_reallocation=True is a no-op only once the name is allocated.
    An initial_value is rejected only in that case; a name that is not yet
    allocated is allocated as usual and may carry one.

Where a value is rejected the reasoning is the same: opting in to the
re-declaration says the existing memory is what you want, whereas giving a
value says the opposite, and the compiler would ignore it either way.

To tell "no initial value given" apart from an explicit 0, initial_value
now defaults to None and resolves to 0 or 0.0 according to the dtype.
Register(0, name="dup", ignore_reallocation=True) is therefore an error when
dup already exists, while Register(name="dup", ignore_reallocation=True) is
not — passing the default explicitly is still saying something.

The guide's signal example and the equivalent example in the measure
docstring now say alias=True where they mean to reuse the scope register.

Compatibility

This is the one PR in the split with a real chance of breaking existing
programs, by design: any code that declared a name twice was silently losing
the second initial value and now gets an error that says so. That includes a
procedure that reuses a name its caller already took, which the global scoping
newly catches. The fix at each site is one of alias=True,
ignore_reallocation=True, a different name, or reusing the handle that
already exists.

Testing

pytest tests/ passes. tests/test_registers.py covers each opt-out on both a
fresh and an already-allocated name, the explicit-zero case, Array (which
always carries contents and so can never opt in), per-module scoping, the
global-across-procedures cases (clash, which procedure gets named, a procedure
called twice, a duplicate inside one body, and the same name on two qubits),
and the narrowing-with-an-alias pattern the guide now teaches.

🤖 Generated with Claude Code

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) <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.50249% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.57%. Comparing base (206a2d6) to head (eeed807).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
tests/test_registers.py 99.42% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #73      +/-   ##
==========================================
+ Coverage   90.18%   90.57%   +0.39%     
==========================================
  Files          31       31              
  Lines        5317     5516     +199     
==========================================
+ Hits         4795     4996     +201     
+ Misses        522      520       -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

qci-amos and others added 2 commits August 19, 2026 14:56
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>


class RegisterAllocation(NamedTuple):
"""One entry in the register names a circuit has allocated.

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
"""One entry in the register names a circuit has allocated.
"""Registers by name an allocated circuit.

This is my guess, I am having difficulty parsing the current description

"""One entry in the register names a circuit has allocated.

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.

Comment on lines +84 to +87
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.

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
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: The allocating procedure. If its name is then reused,
reruns of the procedure are distinguished from redeclarations.


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

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.

This statement need fixing: "its initial value never reaches the qubit"
The paragraph is talking about name so this is currently saying the value of name does not reach the qubit. My guess is that the initial value set for the register is not executed?

initial_value_specified: bool = False,
) -> None:
"""Record a register allocation, rejecting a silent re-declaration.

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
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.

Moved up so only relevant users read

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

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
ignore_reallocation: If True, and the name is already allocated, the
ignore_reallocation: If True, and the ``name`` is already allocated, the

Comment on lines +698 to +700
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.

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
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.

I think it's better to keep this info in the relevant args

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

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
allocation of a name takes effect, so a value given for a name that
allocation of a ``name`` takes effect, so a value given for a name that

Comment on lines +810 to +812
is already allocated is rejected rather than silently discarded;
see ``ignore_reallocation``. An ``alias`` never allocates, so it
never takes a value at all.

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
is already allocated is rejected rather than silently discarded;
see ``ignore_reallocation``. An ``alias`` never allocates, so it
never takes a value at all.
is already allocated is rejected; see ``ignore_reallocation``

Comment on lines +823 to +825
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.

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
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.

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