diff --git a/qiskit/circuit/classical/expr/__init__.py b/qiskit/circuit/classical/expr/__init__.py index d2cd4bc5044e..b2b4d138fca7 100644 --- a/qiskit/circuit/classical/expr/__init__.py +++ b/qiskit/circuit/classical/expr/__init__.py @@ -39,10 +39,11 @@ These objects are mutable and should not be reused in a different location without a copy. -The entry point from general circuit objects to the expression system is by wrapping the object -in a :class:`Var` node and associating a :class:`~.types.Type` with it. +The base for dynamic variables is the :class:`Var`, which can be either an arbitrarily typed runtime +variable, or a wrapper around a :class:`.Clbit` or :class:`.ClassicalRegister`. .. autoclass:: Var + :members: var, name Similarly, literals used in comparison (such as integers) should be lifted to :class:`Value` nodes with associated types. @@ -86,10 +87,18 @@ The functions and methods described in this section are a more user-friendly way to build the expression tree, while staying close to the internal representation. All these functions will automatically lift valid Python scalar values into corresponding :class:`Var` or :class:`Value` -objects, and will resolve any required implicit casts on your behalf. +objects, and will resolve any required implicit casts on your behalf. If you want to directly use +some scalar value as an :class:`Expr` node, you can manually :func:`lift` it yourself. .. autofunction:: lift +Typically you should create memory-owning :class:`Var` instances by using the +:meth:`.QuantumCircuit.add_var` method to declare them in some circuit context, since a +:class:`.QuantumCircuit` will not accept an :class:`Expr` that contains variables that are not +already declared in it, since it needs to know how to allocate the storage and how the variable will +be initialized. However, should you want to do this manually, you should use the low-level +:meth:`Var.new` call to safely generate a named variable for usage. + You can manually specify casts in cases where the cast is allowed in explicit form, but may be lossy (such as the cast of a higher precision :class:`~.types.Uint` to a lower precision one). diff --git a/qiskit/circuit/classical/expr/expr.py b/qiskit/circuit/classical/expr/expr.py index b9e9aad4a2b7..3adbacfd6926 100644 --- a/qiskit/circuit/classical/expr/expr.py +++ b/qiskit/circuit/classical/expr/expr.py @@ -31,6 +31,7 @@ import abc import enum import typing +import uuid from .. import types @@ -108,24 +109,56 @@ def __repr__(self): @typing.final class Var(Expr): - """A classical variable.""" + """A classical variable. - __slots__ = ("var",) + These variables take two forms: a new-style variable that owns its storage location and has an + associated name; and an old-style variable that wraps a :class:`.Clbit` or + :class:`.ClassicalRegister` instance that is owned by some containing circuit. In general, + construction of variables for use in programs should use :meth:`Var.new` or + :meth:`.QuantumCircuit.add_var`.""" + + __slots__ = ("var", "name") def __init__( - self, var: qiskit.circuit.Clbit | qiskit.circuit.ClassicalRegister, type: types.Type + self, + var: qiskit.circuit.Clbit | qiskit.circuit.ClassicalRegister | uuid.UUID, + type: types.Type, + *, + name: str | None = None, ): self.type = type self.var = var + """A reference to the backing data storage of the :class:`Var` instance. When lifting + old-style :class:`.Clbit` or :class:`.ClassicalRegister` instances into a :class:`Var`, + this is exactly the :class:`.Clbit` or :class:`.ClassicalRegister`. If the variable is a + new-style classical variable (one that owns its own storage separate to the old + :class:`.Clbit`/:class:`.ClassicalRegister` model), this field will be a :class:`~uuid.UUID` + to uniquely identify it.""" + self.name = name + """The name of the variable. This is required to exist if the backing :attr:`var` attribute + is a :class:`~uuid.UUID`, i.e. if it is a new-style variable, and must be ``None`` if it is + an old-style variable.""" + + @classmethod + def new(cls, name: str, type: types.Type) -> typing.Self: + """Generate a new named variable that owns its own backing storage.""" + return cls(uuid.uuid4(), type, name=name) def accept(self, visitor, /): return visitor.visit_var(self) def __eq__(self, other): - return isinstance(other, Var) and self.type == other.type and self.var == other.var + return ( + isinstance(other, Var) + and self.type == other.type + and self.var == other.var + and self.name == other.name + ) def __repr__(self): - return f"Var({self.var}, {self.type})" + if self.name is None: + return f"Var({self.var}, {self.type})" + return f"Var({self.var}, {self.type}, name='{self.name}')" @typing.final diff --git a/test/python/circuit/classical/test_expr_properties.py b/test/python/circuit/classical/test_expr_properties.py index a6873153c0eb..efda6ba37758 100644 --- a/test/python/circuit/classical/test_expr_properties.py +++ b/test/python/circuit/classical/test_expr_properties.py @@ -56,3 +56,45 @@ def test_expr_can_be_cloned(self, obj): self.assertEqual(obj, copy.copy(obj)) self.assertEqual(obj, copy.deepcopy(obj)) self.assertEqual(obj, pickle.loads(pickle.dumps(obj))) + + def test_var_equality(self): + """Test that various types of :class:`.expr.Var` equality work as expected both in equal and + unequal cases.""" + var_a_bool = expr.Var.new("a", types.Bool()) + self.assertEqual(var_a_bool, var_a_bool) + + # Allocating a new variable should not compare equal, despite the name match. A semantic + # equality checker can choose to key these variables on only their names and types, if it + # knows that that check is valid within the semantic context. + self.assertNotEqual(var_a_bool, expr.Var.new("a", types.Bool())) + + # Manually constructing the same object with the same UUID should cause it compare equal, + # though, for serialisation ease. + self.assertEqual(var_a_bool, expr.Var(var_a_bool.var, types.Bool(), name="a")) + + # This is a badly constructed variable because it's using a different type to refer to the + # same storage location (the UUID) as another variable. It is an IR error to generate this + # sort of thing, but we can't fully be responsible for that and a pass would need to go out + # of its way to do this incorrectly, but we can still ensure that the direct equality check + # would spot the error. + self.assertNotEqual( + var_a_bool, expr.Var(var_a_bool.var, types.Uint(8), name=var_a_bool.name) + ) + + # This is also badly constructed because it uses a different name to refer to the "same" + # storage location. + self.assertNotEqual(var_a_bool, expr.Var(var_a_bool.var, types.Bool(), name="b")) + + # Obviously, two variables of different types and names should compare unequal. + self.assertNotEqual(expr.Var.new("a", types.Bool()), expr.Var.new("b", types.Uint(8))) + # As should two variables of the same name but different storage locations and types. + self.assertNotEqual(expr.Var.new("a", types.Bool()), expr.Var.new("a", types.Uint(8))) + + def test_var_uuid_clone(self): + """Test that :class:`.expr.Var` instances that have an associated UUID and name roundtrip + through pickle and copy operations to produce values that compare equal.""" + var_a_u8 = expr.Var.new("a", types.Uint(8)) + + self.assertEqual(var_a_u8, pickle.loads(pickle.dumps(var_a_u8))) + self.assertEqual(var_a_u8, copy.copy(var_a_u8)) + self.assertEqual(var_a_u8, copy.deepcopy(var_a_u8))