QCDL registers: report a register name that is allocated twice - #73
QCDL registers: report a register name that is allocated twice#73qci-amos wants to merge 3 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 5516 +199
==========================================
+ Hits 4795 4996 +201
+ 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>
|
|
||
|
|
||
| class RegisterAllocation(NamedTuple): | ||
| """One entry in the register names a circuit has allocated. |
There was a problem hiding this comment.
| """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"``. |
There was a problem hiding this comment.
| 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.
| 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. |
There was a problem hiding this comment.
| 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 |
There was a problem hiding this comment.
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. | ||
|
|
There was a problem hiding this comment.
| 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 |
There was a problem hiding this comment.
| ignore_reallocation: If True, and the name is already allocated, the | |
| ignore_reallocation: If True, and the ``name`` is already allocated, the |
| 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. |
There was a problem hiding this comment.
| 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 |
There was a problem hiding this comment.
| 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 |
| is already allocated is rejected rather than silently discarded; | ||
| see ``ignore_reallocation``. An ``alias`` never allocates, so it | ||
| never takes a value at all. |
There was a problem hiding this comment.
| 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`` |
| 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. |
There was a problem hiding this comment.
| 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. |
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