Skip to content

Commit ba161e9

Browse files
Ensure QuantumCircuit.append validates captures in control-flow (#10974)
* Add definition of `Store` instruction This does not yet add the implementation of `QuantumCircuit.store`, which will come later as part of expanding the full API of `QuantumCircuit` to be able to support these runtime variables. The `is_lvalue` helper is added generally to the `classical.expr` module because it's generally useful, while `types.cast_kind` is moved from being a private method in `expr` to a public-API function so `Store` can use it. These now come with associated unit tests. * Add variable-handling methods to `QuantumCircuit` This adds all the new `QuantumCircuit` methods discussed in the variable-declaration RFC[^1], and threads the support for them through the methods that are called in turn, such as `QuantumCircuit.append`. It does yet not add support to methods such as `copy` or `compose`, which will be done in a follow-up. The APIs discussed in the RFC necessitated making `Var` nodes hashable. This is done in this commit, as it is logically connected. These nodes now have enforced immutability, which is technically a minor breaking change, but in practice required for the properties of such expressions to be tracked correctly through circuits. A helper attribute `Var.standalone` is added to unify the handling of whether a variable is an old-style existing-memory wrapper, or a new "proper" variable with its own memory. [^1]: Qiskit/RFCs#50 * Support manual variables `QuantumCircuit` copy methods This commit adds support to the `QuantumCircuit` methods `copy` and `copy_empty_like` for manual variables. This involves the non-trivial extension to the original RFC[^1] that variables can now be uninitialised; this is somewhat required for the logic of how the `Store` instruction works and the existence of `QuantumCircuit.copy_empty_like`; a variable could be initialised with the result of a `measure` that no longer exists, therefore it must be possible for variables to be uninitialised. This was not originally intended to be possible in the design document, but is somewhat required for logical consistency. A method `add_uninitialized_var` is added, so that the behaviour of `copy_empty_like` is not an awkward special case only possible through that method, but instead a complete part of the data model that must be reasoned about. The method however is deliberately a bit less ergononmic to type and to use, because really users _should_ use `add_var` in almost all circumstances. [^1]: Qiskit/RFCs#50 * Ensure `QuantumCircuit.append` validates captures in control-flow This adds an inner check to the control-flow operations that their blocks do not contain input variables, and to `QuantumCircuit.append` that any captures within blocks are validate (in the sense of the variables existing in the outer circuit). In order to avoid an `import` on every call to `QuantumCircuit.append` (especially since we're already eating the cost of an extra `isinstance` check), this reorganises the import structure of `qiskit.circuit.controlflow` to sit strictly _before_ `qiskit.circuit.quantumcircuit` in the import tree. Since those are key parts of the circuit data structure, that does make sense, although by their nature the structures are of course recursive at runtime. * Update documentation Co-authored-by: Matthew Treinish <mtreinish@kortar.org> * Catch simple error case in '_prepare_new_var' Co-authored-by: Matthew Treinish <mtreinish@kortar.org> * Add partial release note --------- Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
1 parent 4c9cdee commit ba161e9

27 files changed

+1802
-107
lines changed

qiskit/circuit/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@
279279
InstructionSet
280280
Operation
281281
EquivalenceLibrary
282+
Store
282283
283284
Control Flow Operations
284285
-----------------------
@@ -375,6 +376,7 @@
375376
from .delay import Delay
376377
from .measure import Measure
377378
from .reset import Reset
379+
from .store import Store
378380
from .parameter import Parameter
379381
from .parametervector import ParameterVector
380382
from .parameterexpression import ParameterExpression

qiskit/circuit/classical/expr/__init__.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,11 @@
160160
suitable "key" functions to do the comparison.
161161
162162
.. autofunction:: structurally_equivalent
163+
164+
Some expressions have associated memory locations, and others may be purely temporary.
165+
You can use :func:`is_lvalue` to determine whether an expression has an associated memory location.
166+
167+
.. autofunction:: is_lvalue
163168
"""
164169

165170
__all__ = [
@@ -172,6 +177,7 @@
172177
"ExprVisitor",
173178
"iter_vars",
174179
"structurally_equivalent",
180+
"is_lvalue",
175181
"lift",
176182
"cast",
177183
"bit_not",
@@ -191,7 +197,7 @@
191197
]
192198

193199
from .expr import Expr, Var, Value, Cast, Unary, Binary
194-
from .visitors import ExprVisitor, iter_vars, structurally_equivalent
200+
from .visitors import ExprVisitor, iter_vars, structurally_equivalent, is_lvalue
195201
from .constructors import (
196202
lift,
197203
cast,

qiskit/circuit/classical/expr/constructors.py

+7-45
Original file line numberDiff line numberDiff line change
@@ -35,65 +35,27 @@
3535
"lift_legacy_condition",
3636
]
3737

38-
import enum
3938
import typing
4039

4140
from .expr import Expr, Var, Value, Unary, Binary, Cast
41+
from ..types import CastKind, cast_kind
4242
from .. import types
4343

4444
if typing.TYPE_CHECKING:
4545
import qiskit
4646

4747

48-
class _CastKind(enum.Enum):
49-
EQUAL = enum.auto()
50-
"""The two types are equal; no cast node is required at all."""
51-
IMPLICIT = enum.auto()
52-
"""The 'from' type can be cast to the 'to' type implicitly. A ``Cast(implicit=True)`` node is
53-
the minimum required to specify this."""
54-
LOSSLESS = enum.auto()
55-
"""The 'from' type can be cast to the 'to' type explicitly, and the cast will be lossless. This
56-
requires a ``Cast(implicit=False)`` node, but there's no danger from inserting one."""
57-
DANGEROUS = enum.auto()
58-
"""The 'from' type has a defined cast to the 'to' type, but depending on the value, it may lose
59-
data. A user would need to manually specify casts."""
60-
NONE = enum.auto()
61-
"""There is no casting permitted from the 'from' type to the 'to' type."""
62-
63-
64-
def _uint_cast(from_: types.Uint, to_: types.Uint, /) -> _CastKind:
65-
if from_.width == to_.width:
66-
return _CastKind.EQUAL
67-
if from_.width < to_.width:
68-
return _CastKind.LOSSLESS
69-
return _CastKind.DANGEROUS
70-
71-
72-
_ALLOWED_CASTS = {
73-
(types.Bool, types.Bool): lambda _a, _b, /: _CastKind.EQUAL,
74-
(types.Bool, types.Uint): lambda _a, _b, /: _CastKind.LOSSLESS,
75-
(types.Uint, types.Bool): lambda _a, _b, /: _CastKind.IMPLICIT,
76-
(types.Uint, types.Uint): _uint_cast,
77-
}
78-
79-
80-
def _cast_kind(from_: types.Type, to_: types.Type, /) -> _CastKind:
81-
if (coercer := _ALLOWED_CASTS.get((from_.kind, to_.kind))) is None:
82-
return _CastKind.NONE
83-
return coercer(from_, to_)
84-
85-
8648
def _coerce_lossless(expr: Expr, type: types.Type) -> Expr:
8749
"""Coerce ``expr`` to ``type`` by inserting a suitable :class:`Cast` node, if the cast is
8850
lossless. Otherwise, raise a ``TypeError``."""
89-
kind = _cast_kind(expr.type, type)
90-
if kind is _CastKind.EQUAL:
51+
kind = cast_kind(expr.type, type)
52+
if kind is CastKind.EQUAL:
9153
return expr
92-
if kind is _CastKind.IMPLICIT:
54+
if kind is CastKind.IMPLICIT:
9355
return Cast(expr, type, implicit=True)
94-
if kind is _CastKind.LOSSLESS:
56+
if kind is CastKind.LOSSLESS:
9557
return Cast(expr, type, implicit=False)
96-
if kind is _CastKind.DANGEROUS:
58+
if kind is CastKind.DANGEROUS:
9759
raise TypeError(f"cannot cast '{expr}' to '{type}' without loss of precision")
9860
raise TypeError(f"no cast is defined to take '{expr}' to '{type}'")
9961

@@ -198,7 +160,7 @@ def cast(operand: typing.Any, type: types.Type, /) -> Expr:
198160
Cast(Value(5, types.Uint(32)), types.Uint(8), implicit=False)
199161
"""
200162
operand = lift(operand)
201-
if _cast_kind(operand.type, type) is _CastKind.NONE:
163+
if cast_kind(operand.type, type) is CastKind.NONE:
202164
raise TypeError(f"cannot cast '{operand}' to '{type}'")
203165
return Cast(operand, type)
204166

qiskit/circuit/classical/expr/expr.py

+49-13
Original file line numberDiff line numberDiff line change
@@ -115,38 +115,57 @@ class Var(Expr):
115115
associated name; and an old-style variable that wraps a :class:`.Clbit` or
116116
:class:`.ClassicalRegister` instance that is owned by some containing circuit. In general,
117117
construction of variables for use in programs should use :meth:`Var.new` or
118-
:meth:`.QuantumCircuit.add_var`."""
118+
:meth:`.QuantumCircuit.add_var`.
119+
120+
Variables are immutable after construction, so they can be used as dictionary keys."""
119121

120122
__slots__ = ("var", "name")
121123

124+
var: qiskit.circuit.Clbit | qiskit.circuit.ClassicalRegister | uuid.UUID
125+
"""A reference to the backing data storage of the :class:`Var` instance. When lifting
126+
old-style :class:`.Clbit` or :class:`.ClassicalRegister` instances into a :class:`Var`,
127+
this is exactly the :class:`.Clbit` or :class:`.ClassicalRegister`. If the variable is a
128+
new-style classical variable (one that owns its own storage separate to the old
129+
:class:`.Clbit`/:class:`.ClassicalRegister` model), this field will be a :class:`~uuid.UUID`
130+
to uniquely identify it."""
131+
name: str | None
132+
"""The name of the variable. This is required to exist if the backing :attr:`var` attribute
133+
is a :class:`~uuid.UUID`, i.e. if it is a new-style variable, and must be ``None`` if it is
134+
an old-style variable."""
135+
122136
def __init__(
123137
self,
124138
var: qiskit.circuit.Clbit | qiskit.circuit.ClassicalRegister | uuid.UUID,
125139
type: types.Type,
126140
*,
127141
name: str | None = None,
128142
):
129-
self.type = type
130-
self.var = var
131-
"""A reference to the backing data storage of the :class:`Var` instance. When lifting
132-
old-style :class:`.Clbit` or :class:`.ClassicalRegister` instances into a :class:`Var`,
133-
this is exactly the :class:`.Clbit` or :class:`.ClassicalRegister`. If the variable is a
134-
new-style classical variable (one that owns its own storage separate to the old
135-
:class:`.Clbit`/:class:`.ClassicalRegister` model), this field will be a :class:`~uuid.UUID`
136-
to uniquely identify it."""
137-
self.name = name
138-
"""The name of the variable. This is required to exist if the backing :attr:`var` attribute
139-
is a :class:`~uuid.UUID`, i.e. if it is a new-style variable, and must be ``None`` if it is
140-
an old-style variable."""
143+
super().__setattr__("type", type)
144+
super().__setattr__("var", var)
145+
super().__setattr__("name", name)
141146

142147
@classmethod
143148
def new(cls, name: str, type: types.Type) -> typing.Self:
144149
"""Generate a new named variable that owns its own backing storage."""
145150
return cls(uuid.uuid4(), type, name=name)
146151

152+
@property
153+
def standalone(self) -> bool:
154+
"""Whether this :class:`Var` is a standalone variable that owns its storage location. If
155+
false, this is a wrapper :class:`Var` around a pre-existing circuit object."""
156+
return isinstance(self.var, uuid.UUID)
157+
147158
def accept(self, visitor, /):
148159
return visitor.visit_var(self)
149160

161+
def __setattr__(self, key, value):
162+
if hasattr(self, key):
163+
raise AttributeError(f"'Var' object attribute '{key}' is read-only")
164+
raise AttributeError(f"'Var' object has no attribute '{key}'")
165+
166+
def __hash__(self):
167+
return hash((self.type, self.var, self.name))
168+
150169
def __eq__(self, other):
151170
return (
152171
isinstance(other, Var)
@@ -160,6 +179,23 @@ def __repr__(self):
160179
return f"Var({self.var}, {self.type})"
161180
return f"Var({self.var}, {self.type}, name='{self.name}')"
162181

182+
def __getstate__(self):
183+
return (self.var, self.type, self.name)
184+
185+
def __setstate__(self, state):
186+
var, type, name = state
187+
super().__setattr__("type", type)
188+
super().__setattr__("var", var)
189+
super().__setattr__("name", name)
190+
191+
def __copy__(self):
192+
# I am immutable...
193+
return self
194+
195+
def __deepcopy__(self, memo):
196+
# ... as are all my consituent parts.
197+
return self
198+
163199

164200
@typing.final
165201
class Value(Expr):

qiskit/circuit/classical/expr/visitors.py

+63
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,66 @@ def structurally_equivalent(
215215
True
216216
"""
217217
return left.accept(_StructuralEquivalenceImpl(right, left_var_key, right_var_key))
218+
219+
220+
class _IsLValueImpl(ExprVisitor[bool]):
221+
__slots__ = ()
222+
223+
def visit_var(self, node, /):
224+
return True
225+
226+
def visit_value(self, node, /):
227+
return False
228+
229+
def visit_unary(self, node, /):
230+
return False
231+
232+
def visit_binary(self, node, /):
233+
return False
234+
235+
def visit_cast(self, node, /):
236+
return False
237+
238+
239+
_IS_LVALUE = _IsLValueImpl()
240+
241+
242+
def is_lvalue(node: expr.Expr, /) -> bool:
243+
"""Return whether this expression can be used in l-value positions, that is, whether it has a
244+
well-defined location in memory, such as one that might be writeable.
245+
246+
Being an l-value is a necessary but not sufficient for this location to be writeable; it is
247+
permissible that a larger object containing this memory location may not allow writing from
248+
the scope that attempts to write to it. This would be an access property of the containing
249+
program, however, and not an inherent property of the expression system.
250+
251+
Examples:
252+
Literal values are never l-values; there's no memory location associated with (for example)
253+
the constant ``1``::
254+
255+
>>> from qiskit.circuit.classical import expr
256+
>>> expr.is_lvalue(expr.lift(2))
257+
False
258+
259+
:class:`~.expr.Var` nodes are always l-values, because they always have some associated
260+
memory location::
261+
262+
>>> from qiskit.circuit.classical import types
263+
>>> from qiskit.circuit import Clbit
264+
>>> expr.is_lvalue(expr.Var.new("a", types.Bool()))
265+
True
266+
>>> expr.is_lvalue(expr.lift(Clbit()))
267+
True
268+
269+
Currently there are no unary or binary operations on variables that can produce an l-value
270+
expression, but it is likely in the future that some sort of "indexing" operation will be
271+
added, which could produce l-values::
272+
273+
>>> a = expr.Var.new("a", types.Uint(8))
274+
>>> b = expr.Var.new("b", types.Uint(8))
275+
>>> expr.is_lvalue(a) and expr.is_lvalue(b)
276+
True
277+
>>> expr.is_lvalue(expr.bit_and(a, b))
278+
False
279+
"""
280+
return node.accept(_IS_LVALUE)

qiskit/circuit/classical/types/__init__.py

+26-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
Typing (:mod:`qiskit.circuit.classical.types`)
1616
==============================================
1717
18+
Representation
19+
==============
1820
1921
The type system of the expression tree is exposed through this module. This is inherently linked to
2022
the expression system in the :mod:`~.classical.expr` module, as most expressions can only be
@@ -41,11 +43,18 @@
4143
Note that :class:`Uint` defines a family of types parametrised by their width; it is not one single
4244
type, which may be slightly different to the 'classical' programming languages you are used to.
4345
46+
47+
Working with types
48+
==================
49+
4450
There are some functions on these types exposed here as well. These are mostly expected to be used
4551
only in manipulations of the expression tree; users who are building expressions using the
4652
:ref:`user-facing construction interface <circuit-classical-expressions-expr-construction>` should
4753
not need to use these.
4854
55+
Partial ordering of types
56+
-------------------------
57+
4958
The type system is equipped with a partial ordering, where :math:`a < b` is interpreted as
5059
":math:`a` is a strict subtype of :math:`b`". Note that the partial ordering is a subset of the
5160
directed graph that describes the allowed explicit casting operations between types. The partial
@@ -66,6 +75,20 @@
6675
.. autofunction:: is_subtype
6776
.. autofunction:: is_supertype
6877
.. autofunction:: greater
78+
79+
80+
Casting between types
81+
---------------------
82+
83+
It is common to need to cast values of one type to another type. The casting rules for this are
84+
embedded into the :mod:`types` module. You can query the casting kinds using :func:`cast_kind`:
85+
86+
.. autofunction:: cast_kind
87+
88+
The return values from this function are an enumeration explaining the types of cast that are
89+
allowed from the left type to the right type.
90+
91+
.. autoclass:: CastKind
6992
"""
7093

7194
__all__ = [
@@ -77,7 +100,9 @@
77100
"is_subtype",
78101
"is_supertype",
79102
"greater",
103+
"CastKind",
104+
"cast_kind",
80105
]
81106

82107
from .types import Type, Bool, Uint
83-
from .ordering import Ordering, order, is_subtype, is_supertype, greater
108+
from .ordering import Ordering, order, is_subtype, is_supertype, greater, CastKind, cast_kind

0 commit comments

Comments
 (0)