Skip to content

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

Open
qci-amos wants to merge 19 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 19 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.50739% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.57%. Comparing base (2b82b20) to head (dbd7f52).

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     5517     +200     
==========================================
+ Hits         4795     4997     +202     
+ 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>
Comment thread dwave/gate/qcdl/components.py Outdated
"""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.

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"?

Comment thread dwave/gate/qcdl/components.py Outdated
Comment thread dwave/gate/qcdl/components.py Outdated
Comment thread dwave/gate/qcdl/components.py
Comment thread dwave/gate/qcdl/registers.py Outdated
Comment thread dwave/gate/qcdl/registers.py Outdated
Comment thread dwave/gate/qcdl/registers.py Outdated
Comment thread dwave/gate/qcdl/registers.py Outdated
Comment thread dwave/gate/qcdl/registers.py Outdated
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