QCDL registers: report a register name that is allocated twice - #73
Open
qci-amos wants to merge 19 commits into
Open
QCDL registers: report a register name that is allocated twice#73qci-amos wants to merge 19 commits into
qci-amos wants to merge 19 commits into
Conversation
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 Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
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>
JoelPasvolsky
approved these changes
Aug 20, 2026
| """One entry in the register names a circuit has allocated. | ||
|
|
||
| Args: | ||
| dtype: ``"int"`` or ``"float"``. |
Collaborator
There was a problem hiding this comment.
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.
Collaborator
Author
There was a problem hiding this comment.
There's also Array... I think it's ok to leave it non-specific as "register"?
Co-authored-by: Joel Pasvolsky <34041130+JoelPasvolsky@users.noreply.github.com>
Co-authored-by: Joel Pasvolsky <34041130+JoelPasvolsky@users.noreply.github.com>
Co-authored-by: Joel Pasvolsky <34041130+JoelPasvolsky@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
The change
Allocated register names are tracked on the circuit and a re-declaration
raises
QCDLUserError, naming the dtype it already has, the procedure thatallocated 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_registersrather than onProcedure. It is still keyed per module, soq0andq1may each hold aregister 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
Procedureinstance sharing oneproc_name, andproc_nameis 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=Trueis always an alias and never allocates, so aninitial_valueis rejected whatever the state of the name — alreadyallocated or not.
ignore_reallocation=Trueis a no-op only once the name is allocated.An
initial_valueis rejected only in that case; a name that is not yetallocated 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_valuenow defaults to
Noneand resolves to0or0.0according to the dtype.Register(0, name="dup", ignore_reallocation=True)is therefore an error whendupalready exists, whileRegister(name="dup", ignore_reallocation=True)isnot — passing the default explicitly is still saying something.
The guide's signal example and the equivalent example in the
measuredocstring now say
alias=Truewhere 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 thatalready exists.
Testing
pytest tests/passes.tests/test_registers.pycovers each opt-out on both afresh and an already-allocated name, the explicit-zero case,
Array(whichalways 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