From dc3120548c39be266a8cc7de174aa1d02da64bca Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 2 Oct 2023 13:17:34 +0100 Subject: [PATCH 1/3] Add representation of storage-owning `Var` nodes This adds the representation of `expr.Var` nodes that own their own storage locations, and consequently are not backed by existing Qiskit objects (`Clbit` or `ClassicalRegister`). This is the base of the ability for Qiskit to represent manual classical-value storage in `QuantumCircuit`, and the base for how manual storage will be implemented. --- qiskit/circuit/classical/expr/__init__.py | 14 ++++-- qiskit/circuit/classical/expr/expr.py | 43 ++++++++++++++++--- .../circuit/classical/test_expr_properties.py | 42 ++++++++++++++++++ 3 files changed, 91 insertions(+), 8 deletions(-) diff --git a/qiskit/circuit/classical/expr/__init__.py b/qiskit/circuit/classical/expr/__init__.py index d2cd4bc5044e..23fec472202b 100644 --- a/qiskit/circuit/classical/expr/__init__.py +++ b/qiskit/circuit/classical/expr/__init__.py @@ -39,8 +39,8 @@ 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 an old-style :class:`.Clbit` or :class:`.ClassicalRegister`. .. autoclass:: Var @@ -86,10 +86,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 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 initialised. 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)) From 3d2f8dde866b7a33c72e5d1e8a49364b3018a6d6 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 2 Oct 2023 15:16:04 +0100 Subject: [PATCH 2/3] 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. --- qiskit/circuit/__init__.py | 2 + qiskit/circuit/classical/expr/__init__.py | 8 +- qiskit/circuit/classical/expr/constructors.py | 52 ++--------- qiskit/circuit/classical/expr/visitors.py | 63 ++++++++++++++ qiskit/circuit/classical/types/__init__.py | 27 +++++- qiskit/circuit/classical/types/ordering.py | 59 +++++++++++++ qiskit/circuit/store.py | 87 +++++++++++++++++++ .../circuit/classical/test_expr_helpers.py | 27 ++++++ .../circuit/classical/test_types_ordering.py | 10 +++ test/python/circuit/test_store.py | 62 +++++++++++++ 10 files changed, 350 insertions(+), 47 deletions(-) create mode 100644 qiskit/circuit/store.py create mode 100644 test/python/circuit/test_store.py diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index 1a41c299d53e..f214eb5c72dd 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -279,6 +279,7 @@ InstructionSet Operation EquivalenceLibrary + Store Control Flow Operations ----------------------- @@ -375,6 +376,7 @@ from .delay import Delay from .measure import Measure from .reset import Reset +from .store import Store from .parameter import Parameter from .parametervector import ParameterVector from .parameterexpression import ParameterExpression diff --git a/qiskit/circuit/classical/expr/__init__.py b/qiskit/circuit/classical/expr/__init__.py index 23fec472202b..f82ed0862377 100644 --- a/qiskit/circuit/classical/expr/__init__.py +++ b/qiskit/circuit/classical/expr/__init__.py @@ -159,6 +159,11 @@ suitable "key" functions to do the comparison. .. autofunction:: structurally_equivalent + +Some expressions have associated memory locations with them, and some may be purely temporaries. +You can use :func:`is_lvalue` to determine whether an expression has such a memory backing. + +.. autofunction:: is_lvalue """ __all__ = [ @@ -171,6 +176,7 @@ "ExprVisitor", "iter_vars", "structurally_equivalent", + "is_lvalue", "lift", "cast", "bit_not", @@ -190,7 +196,7 @@ ] from .expr import Expr, Var, Value, Cast, Unary, Binary -from .visitors import ExprVisitor, iter_vars, structurally_equivalent +from .visitors import ExprVisitor, iter_vars, structurally_equivalent, is_lvalue from .constructors import ( lift, cast, diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index 1406a86237c5..64a19a2aee2a 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -35,65 +35,27 @@ "lift_legacy_condition", ] -import enum import typing from .expr import Expr, Var, Value, Unary, Binary, Cast +from ..types import CastKind, cast_kind from .. import types if typing.TYPE_CHECKING: import qiskit -class _CastKind(enum.Enum): - EQUAL = enum.auto() - """The two types are equal; no cast node is required at all.""" - IMPLICIT = enum.auto() - """The 'from' type can be cast to the 'to' type implicitly. A ``Cast(implicit=True)`` node is - the minimum required to specify this.""" - LOSSLESS = enum.auto() - """The 'from' type can be cast to the 'to' type explicitly, and the cast will be lossless. This - requires a ``Cast(implicit=False)`` node, but there's no danger from inserting one.""" - DANGEROUS = enum.auto() - """The 'from' type has a defined cast to the 'to' type, but depending on the value, it may lose - data. A user would need to manually specify casts.""" - NONE = enum.auto() - """There is no casting permitted from the 'from' type to the 'to' type.""" - - -def _uint_cast(from_: types.Uint, to_: types.Uint, /) -> _CastKind: - if from_.width == to_.width: - return _CastKind.EQUAL - if from_.width < to_.width: - return _CastKind.LOSSLESS - return _CastKind.DANGEROUS - - -_ALLOWED_CASTS = { - (types.Bool, types.Bool): lambda _a, _b, /: _CastKind.EQUAL, - (types.Bool, types.Uint): lambda _a, _b, /: _CastKind.LOSSLESS, - (types.Uint, types.Bool): lambda _a, _b, /: _CastKind.IMPLICIT, - (types.Uint, types.Uint): _uint_cast, -} - - -def _cast_kind(from_: types.Type, to_: types.Type, /) -> _CastKind: - if (coercer := _ALLOWED_CASTS.get((from_.kind, to_.kind))) is None: - return _CastKind.NONE - return coercer(from_, to_) - - def _coerce_lossless(expr: Expr, type: types.Type) -> Expr: """Coerce ``expr`` to ``type`` by inserting a suitable :class:`Cast` node, if the cast is lossless. Otherwise, raise a ``TypeError``.""" - kind = _cast_kind(expr.type, type) - if kind is _CastKind.EQUAL: + kind = cast_kind(expr.type, type) + if kind is CastKind.EQUAL: return expr - if kind is _CastKind.IMPLICIT: + if kind is CastKind.IMPLICIT: return Cast(expr, type, implicit=True) - if kind is _CastKind.LOSSLESS: + if kind is CastKind.LOSSLESS: return Cast(expr, type, implicit=False) - if kind is _CastKind.DANGEROUS: + if kind is CastKind.DANGEROUS: raise TypeError(f"cannot cast '{expr}' to '{type}' without loss of precision") raise TypeError(f"no cast is defined to take '{expr}' to '{type}'") @@ -198,7 +160,7 @@ def cast(operand: typing.Any, type: types.Type, /) -> Expr: Cast(Value(5, types.Uint(32)), types.Uint(8), implicit=False) """ operand = lift(operand) - if _cast_kind(operand.type, type) is _CastKind.NONE: + if cast_kind(operand.type, type) is CastKind.NONE: raise TypeError(f"cannot cast '{operand}' to '{type}'") return Cast(operand, type) diff --git a/qiskit/circuit/classical/expr/visitors.py b/qiskit/circuit/classical/expr/visitors.py index 07ad36a8e0e4..c0c1a5894af6 100644 --- a/qiskit/circuit/classical/expr/visitors.py +++ b/qiskit/circuit/classical/expr/visitors.py @@ -215,3 +215,66 @@ def structurally_equivalent( True """ return left.accept(_StructuralEquivalenceImpl(right, left_var_key, right_var_key)) + + +class _IsLValueImpl(ExprVisitor[bool]): + __slots__ = () + + def visit_var(self, node, /): + return True + + def visit_value(self, node, /): + return False + + def visit_unary(self, node, /): + return False + + def visit_binary(self, node, /): + return False + + def visit_cast(self, node, /): + return False + + +_IS_LVALUE = _IsLValueImpl() + + +def is_lvalue(node: expr.Expr, /) -> bool: + """Return whether this expression can be used in l-value positions, that is, whether it has a + well-defined location in memory, such as one that might be writeable. + + Being an l-value is a necessary but not sufficient for this location to be writeable; it is + permissible that a larger object containing this memory location may not allow writing from + the scope that attempts to write to it. This would be an access property of the containing + program, however, and not an inherent property of the expression system. + + Examples: + Literal values are never l-values; there's no memory location associated with (for example) + the constant ``1``:: + + >>> from qiskit.circuit.classical import expr + >>> expr.is_lvalue(expr.lift(2)) + False + + :class:`~.expr.Var` nodes are always l-values, because they always have some associated + memory location:: + + >>> from qiskit.circuit.classical import types + >>> from qiskit.circuit import Clbit + >>> expr.is_lvalue(expr.Var.new("a", types.Bool())) + True + >>> expr.is_lvalue(expr.lift(Clbit())) + True + + Currently there are no unary or binary operations on variables that can produce an l-value + expression, but it is likely in the future that some sort of "indexing" operation will be + added, which could produce l-values:: + + >>> a = expr.Var.new("a", types.Uint(8)) + >>> b = expr.Var.new("b", types.Uint(8)) + >>> expr.is_lvalue(a) and expr.is_lvalue(b) + True + >>> expr.is_lvalue(expr.bit_and(a, b)) + False + """ + return node.accept(_IS_LVALUE) diff --git a/qiskit/circuit/classical/types/__init__.py b/qiskit/circuit/classical/types/__init__.py index c55c724315cc..93ab90e32166 100644 --- a/qiskit/circuit/classical/types/__init__.py +++ b/qiskit/circuit/classical/types/__init__.py @@ -15,6 +15,8 @@ Typing (:mod:`qiskit.circuit.classical.types`) ============================================== +Representation +============== The type system of the expression tree is exposed through this module. This is inherently linked to the expression system in the :mod:`~.classical.expr` module, as most expressions can only be @@ -41,11 +43,18 @@ Note that :class:`Uint` defines a family of types parametrised by their width; it is not one single type, which may be slightly different to the 'classical' programming languages you are used to. + +Working with types +================== + There are some functions on these types exposed here as well. These are mostly expected to be used only in manipulations of the expression tree; users who are building expressions using the :ref:`user-facing construction interface ` should not need to use these. +Partial ordering of types +------------------------- + The type system is equipped with a partial ordering, where :math:`a < b` is interpreted as ":math:`a` is a strict subtype of :math:`b`". Note that the partial ordering is a subset of the directed graph that describes the allowed explicit casting operations between types. The partial @@ -66,6 +75,20 @@ .. autofunction:: is_subtype .. autofunction:: is_supertype .. autofunction:: greater + + +Casting between types +--------------------- + +It is common to need to cast values of one type to another type. The casting rules for this are +embedded into the :mod:`types` module. You can query the casting kinds using :func:`cast_kind`: + +.. autofunction:: cast_kind + +The return values from this function are an enumeration explaining the types of cast that are +allowed from the left type to the right type. + +.. autoclass:: CastKind """ __all__ = [ @@ -77,7 +100,9 @@ "is_subtype", "is_supertype", "greater", + "CastKind", + "cast_kind", ] from .types import Type, Bool, Uint -from .ordering import Ordering, order, is_subtype, is_supertype, greater +from .ordering import Ordering, order, is_subtype, is_supertype, greater, CastKind, cast_kind diff --git a/qiskit/circuit/classical/types/ordering.py b/qiskit/circuit/classical/types/ordering.py index aceb9aeefbcf..b000e91cf5ed 100644 --- a/qiskit/circuit/classical/types/ordering.py +++ b/qiskit/circuit/classical/types/ordering.py @@ -20,6 +20,8 @@ "is_supertype", "order", "greater", + "CastKind", + "cast_kind", ] import enum @@ -161,3 +163,60 @@ def greater(left: Type, right: Type, /) -> Type: if order_ is Ordering.NONE: raise TypeError(f"no ordering exists between '{left}' and '{right}'") return left if order_ is Ordering.GREATER else right + + +class CastKind(enum.Enum): + """A return value indicating the type of cast that can occur from one type to another.""" + + EQUAL = enum.auto() + """The two types are equal; no cast node is required at all.""" + IMPLICIT = enum.auto() + """The 'from' type can be cast to the 'to' type implicitly. A :class:`~.expr.Cast` node with + ``implicit==True`` is the minimum required to specify this.""" + LOSSLESS = enum.auto() + """The 'from' type can be cast to the 'to' type explicitly, and the cast will be lossless. This + requires a :class:`~.expr.Cast`` node with ``implicit=False``, but there's no danger from + inserting one.""" + DANGEROUS = enum.auto() + """The 'from' type has a defined cast to the 'to' type, but depending on the value, it may lose + data. A user would need to manually specify casts.""" + NONE = enum.auto() + """There is no casting permitted from the 'from' type to the 'to' type.""" + + +def _uint_cast(from_: Uint, to_: Uint, /) -> CastKind: + if from_.width == to_.width: + return CastKind.EQUAL + if from_.width < to_.width: + return CastKind.LOSSLESS + return CastKind.DANGEROUS + + +_ALLOWED_CASTS = { + (Bool, Bool): lambda _a, _b, /: CastKind.EQUAL, + (Bool, Uint): lambda _a, _b, /: CastKind.LOSSLESS, + (Uint, Bool): lambda _a, _b, /: CastKind.IMPLICIT, + (Uint, Uint): _uint_cast, +} + + +def cast_kind(from_: Type, to_: Type, /) -> CastKind: + """Determine the sort of cast that is required to move from the left type to the right type. + + Examples: + + .. code-block:: python + + >>> from qiskit.circuit.classical import types + >>> types.cast_kind(types.Bool(), types.Bool()) + + >>> types.cast_kind(types.Uint(8), types.Bool()) + + >>> types.cast_kind(types.Bool(), types.Uint(8)) + + >>> types.cast_kind(types.Uint(16), types.Uint(8)) + + """ + if (coercer := _ALLOWED_CASTS.get((from_.kind, to_.kind))) is None: + return CastKind.NONE + return coercer(from_, to_) diff --git a/qiskit/circuit/store.py b/qiskit/circuit/store.py new file mode 100644 index 000000000000..100fe0e629b9 --- /dev/null +++ b/qiskit/circuit/store.py @@ -0,0 +1,87 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""The 'Store' operation.""" + +from __future__ import annotations + +import typing + +from .exceptions import CircuitError +from .classical import expr, types +from .instruction import Instruction + + +def _handle_equal_types(lvalue: expr.Expr, rvalue: expr.Expr, /) -> tuple[expr.Expr, expr.Expr]: + return lvalue, rvalue + + +def _handle_implicit_cast(lvalue: expr.Expr, rvalue: expr.Expr, /) -> tuple[expr.Expr, expr.Expr]: + return lvalue, expr.Cast(rvalue, lvalue.type, implicit=True) + + +def _requires_lossless_cast(lvalue: expr.Expr, rvalue: expr.Expr, /) -> typing.NoReturn: + raise CircuitError(f"an explicit cast is required from '{rvalue.type}' to '{lvalue.type}'") + + +def _requires_dangerous_cast(lvalue: expr.Expr, rvalue: expr.Expr, /) -> typing.NoReturn: + raise CircuitError( + f"an explicit cast is required from '{rvalue.type}' to '{lvalue.type}', which may be lossy" + ) + + +def _no_cast_possible(lvalue: expr.Expr, rvalue: expr.Expr) -> typing.NoReturn: + raise CircuitError(f"no cast is possible from '{rvalue.type}' to '{lvalue.type}'") + + +_HANDLE_CAST = { + types.CastKind.EQUAL: _handle_equal_types, + types.CastKind.IMPLICIT: _handle_implicit_cast, + types.CastKind.LOSSLESS: _requires_lossless_cast, + types.CastKind.DANGEROUS: _requires_dangerous_cast, + types.CastKind.NONE: _no_cast_possible, +} + + +class Store(Instruction): + """A manual storage of some classical value to a classical memory location. + + This is a low-level primitive of the classical-expression handling (similar to how + :class:`~.circuit.Measure` is a primitive for quantum measurement), and is not safe for + subclassing. It is likely to become a special-case instruction in later versions of Qiskit + circuit and compiler internal representations.""" + + def __init__(self, lvalue: expr.Expr, rvalue: expr.Expr): + if not expr.is_lvalue(lvalue): + raise CircuitError(f"'{lvalue}' is not an l-value") + + cast_kind = types.cast_kind(rvalue.type, lvalue.type) + if (handler := _HANDLE_CAST.get(cast_kind)) is None: + raise RuntimeError(f"unhandled cast kind required: {cast_kind}") + lvalue, rvalue = handler(lvalue, rvalue) + + super().__init__("store", 0, 0, [lvalue, rvalue]) + + @property + def lvalue(self): + """Get the l-value :class:`~.expr.Expr` node that is being stored to.""" + return self.params[0] + + @property + def rvalue(self): + """Get the r-value :class:`~.expr.Expr` node that is being written into the l-value.""" + return self.params[1] + + def c_if(self, classical, val): + raise NotImplementedError( + "stores cannot be conditioned with `c_if`; use a full `if_test` context instead" + ) diff --git a/test/python/circuit/classical/test_expr_helpers.py b/test/python/circuit/classical/test_expr_helpers.py index f7b420c07144..31b4d7028a8b 100644 --- a/test/python/circuit/classical/test_expr_helpers.py +++ b/test/python/circuit/classical/test_expr_helpers.py @@ -115,3 +115,30 @@ def always_equal(_): # ``True`` instead. self.assertFalse(expr.structurally_equivalent(left, right, not_handled, not_handled)) self.assertTrue(expr.structurally_equivalent(left, right, always_equal, always_equal)) + + +@ddt.ddt +class TestIsLValue(QiskitTestCase): + @ddt.data( + expr.Var.new("a", types.Bool()), + expr.Var.new("b", types.Uint(8)), + expr.Var(Clbit(), types.Bool()), + expr.Var(ClassicalRegister(8, "cr"), types.Uint(8)), + ) + def test_happy_cases(self, lvalue): + self.assertTrue(expr.is_lvalue(lvalue)) + + @ddt.data( + expr.Value(3, types.Uint(2)), + expr.Value(False, types.Bool()), + expr.Cast(expr.Var.new("a", types.Uint(2)), types.Uint(8)), + expr.Unary(expr.Unary.Op.LOGIC_NOT, expr.Var.new("a", types.Bool()), types.Bool()), + expr.Binary( + expr.Binary.Op.LOGIC_AND, + expr.Var.new("a", types.Bool()), + expr.Var.new("b", types.Bool()), + types.Bool(), + ), + ) + def test_bad_cases(self, not_an_lvalue): + self.assertFalse(expr.is_lvalue(not_an_lvalue)) diff --git a/test/python/circuit/classical/test_types_ordering.py b/test/python/circuit/classical/test_types_ordering.py index 374e1ecff1b1..58417fb17c03 100644 --- a/test/python/circuit/classical/test_types_ordering.py +++ b/test/python/circuit/classical/test_types_ordering.py @@ -58,3 +58,13 @@ def test_greater(self): self.assertEqual(types.greater(types.Bool(), types.Bool()), types.Bool()) with self.assertRaisesRegex(TypeError, "no ordering"): types.greater(types.Bool(), types.Uint(8)) + + +class TestTypesCastKind(QiskitTestCase): + def test_basic_examples(self): + """This is used extensively throughout the expression construction functions, but since it + is public API, it should have some direct unit tests as well.""" + self.assertIs(types.cast_kind(types.Bool(), types.Bool()), types.CastKind.EQUAL) + self.assertIs(types.cast_kind(types.Uint(8), types.Bool()), types.CastKind.IMPLICIT) + self.assertIs(types.cast_kind(types.Bool(), types.Uint(8)), types.CastKind.LOSSLESS) + self.assertIs(types.cast_kind(types.Uint(16), types.Uint(8)), types.CastKind.DANGEROUS) diff --git a/test/python/circuit/test_store.py b/test/python/circuit/test_store.py new file mode 100644 index 000000000000..6d5c4707cbed --- /dev/null +++ b/test/python/circuit/test_store.py @@ -0,0 +1,62 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring + +from qiskit.test import QiskitTestCase +from qiskit.circuit import Store, Clbit, CircuitError +from qiskit.circuit.classical import expr, types + + +class TestStoreInstruction(QiskitTestCase): + """Tests of the properties of the ``Store`` instruction itself.""" + + def test_happy_path_construction(self): + lvalue = expr.Var.new("a", types.Bool()) + rvalue = expr.lift(Clbit()) + constructed = Store(lvalue, rvalue) + self.assertIsInstance(constructed, Store) + self.assertEqual(constructed.lvalue, lvalue) + self.assertEqual(constructed.rvalue, rvalue) + + def test_implicit_cast(self): + lvalue = expr.Var.new("a", types.Bool()) + rvalue = expr.Var.new("b", types.Uint(8)) + constructed = Store(lvalue, rvalue) + self.assertIsInstance(constructed, Store) + self.assertEqual(constructed.lvalue, lvalue) + self.assertEqual(constructed.rvalue, expr.Cast(rvalue, types.Bool(), implicit=True)) + + def test_rejects_non_lvalue(self): + not_an_lvalue = expr.logic_and( + expr.Var.new("a", types.Bool()), expr.Var.new("b", types.Bool()) + ) + rvalue = expr.lift(False) + with self.assertRaisesRegex(CircuitError, "not an l-value"): + Store(not_an_lvalue, rvalue) + + def test_rejects_explicit_cast(self): + lvalue = expr.Var.new("a", types.Uint(16)) + rvalue = expr.Var.new("b", types.Uint(8)) + with self.assertRaisesRegex(CircuitError, "an explicit cast is required"): + Store(lvalue, rvalue) + + def test_rejects_dangerous_cast(self): + lvalue = expr.Var.new("a", types.Uint(8)) + rvalue = expr.Var.new("b", types.Uint(16)) + with self.assertRaisesRegex(CircuitError, "an explicit cast is required.*may be lossy"): + Store(lvalue, rvalue) + + def test_rejects_c_if(self): + instruction = Store(expr.Var.new("a", types.Bool()), expr.Var.new("b", types.Bool())) + with self.assertRaises(NotImplementedError): + instruction.c_if(Clbit(), False) From 424ec6696bc251361b6e5fcf9fe82fa44b2bb800 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 3 Oct 2023 23:08:24 +0100 Subject: [PATCH 3/3] 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]: https://github.com/Qiskit/RFCs/pull/50 --- qiskit/circuit/classical/expr/expr.py | 62 ++- qiskit/circuit/classical/types/types.py | 6 + qiskit/circuit/quantumcircuit.py | 338 +++++++++++++++- ...r-hashable-var-types-7cf2aaa00b201ae6.yaml | 5 + .../expr-var-standalone-2c1116583a2be9fd.yaml | 6 + .../circuit/classical/test_expr_properties.py | 36 +- test/python/circuit/test_circuit_vars.py | 366 ++++++++++++++++++ test/python/circuit/test_store.py | 139 ++++++- 8 files changed, 939 insertions(+), 19 deletions(-) create mode 100644 releasenotes/notes/expr-hashable-var-types-7cf2aaa00b201ae6.yaml create mode 100644 releasenotes/notes/expr-var-standalone-2c1116583a2be9fd.yaml create mode 100644 test/python/circuit/test_circuit_vars.py diff --git a/qiskit/circuit/classical/expr/expr.py b/qiskit/circuit/classical/expr/expr.py index 3adbacfd6926..d15a64988b83 100644 --- a/qiskit/circuit/classical/expr/expr.py +++ b/qiskit/circuit/classical/expr/expr.py @@ -115,10 +115,24 @@ class Var(Expr): 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`.""" + :meth:`.QuantumCircuit.add_var`. + + Variables are immutable after construction, so they can be used as dictionary keys.""" __slots__ = ("var", "name") + var: qiskit.circuit.Clbit | qiskit.circuit.ClassicalRegister | uuid.UUID + """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.""" + name: str | None + """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.""" + def __init__( self, var: qiskit.circuit.Clbit | qiskit.circuit.ClassicalRegister | uuid.UUID, @@ -126,27 +140,32 @@ def __init__( *, 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.""" + super().__setattr__("type", type) + super().__setattr__("var", var) + super().__setattr__("name", name) @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) + @property + def standalone(self) -> bool: + """Whether this :class:`Var` is a standalone variable that owns its storage location. If + false, this is a wrapper :class:`Var` around some pre-existing circuit object.""" + return isinstance(self.var, uuid.UUID) + def accept(self, visitor, /): return visitor.visit_var(self) + def __setattr__(self, key, value): + if hasattr(self, key): + raise AttributeError(f"'Var' object attribute '{key}' is read-only") + raise AttributeError(f"'Var' object has no attribute '{key}'") + + def __hash__(self): + return hash((self.type, self.var, self.name)) + def __eq__(self, other): return ( isinstance(other, Var) @@ -160,6 +179,23 @@ def __repr__(self): return f"Var({self.var}, {self.type})" return f"Var({self.var}, {self.type}, name='{self.name}')" + def __getstate__(self): + return (self.var, self.type, self.name) + + def __setstate__(self, state): + var, type, name = state + super().__setattr__("type", type) + super().__setattr__("var", var) + super().__setattr__("name", name) + + def __copy__(self): + # I am immutable... + return self + + def __deepcopy__(self, memo): + # ... as are all my consituent parts. + return self + @typing.final class Value(Expr): diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py index 711f82db5fc0..04266aefd410 100644 --- a/qiskit/circuit/classical/types/types.py +++ b/qiskit/circuit/classical/types/types.py @@ -89,6 +89,9 @@ class Bool(Type, metaclass=_Singleton): def __repr__(self): return "Bool()" + def __hash__(self): + return hash(self.__class__) + def __eq__(self, other): return isinstance(other, Bool) @@ -107,5 +110,8 @@ def __init__(self, width: int): def __repr__(self): return f"Uint({self.width})" + def __hash__(self): + return hash((self.__class__, self.width)) + def __eq__(self, other): return isinstance(other, Uint) and self.width == other.width diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 4d406daf8189..c9b8cf21d581 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -16,6 +16,7 @@ from __future__ import annotations import copy +import itertools import multiprocessing as mp import warnings import typing @@ -48,7 +49,7 @@ from qiskit.utils.deprecation import deprecate_func from . import _classical_resource_map from ._utils import sort_parameters -from .classical import expr +from .classical import expr, types from .parameterexpression import ParameterExpression, ParameterValueType from .quantumregister import QuantumRegister, Qubit, AncillaRegister, AncillaQubit from .classicalregister import ClassicalRegister, Clbit @@ -60,6 +61,7 @@ from .bit import Bit from .quantumcircuitdata import QuantumCircuitData, CircuitInstruction from .delay import Delay +from .store import Store if typing.TYPE_CHECKING: import qiskit # pylint: disable=cyclic-import @@ -137,6 +139,23 @@ class QuantumCircuit: circuit. This gets stored as free-form data in a dict in the :attr:`~qiskit.circuit.QuantumCircuit.metadata` attribute. It will not be directly used in the circuit. + inputs: any variables to declare as ``input`` runtime variables for this circuit. These + should already be existing :class:`.expr.Var` nodes that you build from somewhere else; + if you need to create the inputs as well, use :meth:`QuantumCircuit.add_input`. The + variables given in this argument will be passed directly to :meth:`add_input`. A + circuit cannot have both ``inputs`` and ``captures``. + captures: any variables that that this circuit scope should capture from a containing scope. + The variables given here will be passed directly to :meth:`add_capture`. A circuit + cannot have both ``inputs`` and ``captures``. + declarations: any variables that this circuit should declare and initialize immediately. + You can order this input so that later declarations depend on earlier ones (including + inputs or captures). If you need to depend on values that will be computed later at + runtime, use :meth:`add_var` at an appropriate point in the circuit execution. + + This argument is intended for convenient circuit initialization when you already have a + set of created variables. The variables used here will be directly passed to + :meth:`add_var`, which you can use directly if this is the first time you are creating + the variable. Raises: CircuitError: if the circuit name, if given, is not valid. @@ -199,6 +218,9 @@ def __init__( name: str | None = None, global_phase: ParameterValueType = 0, metadata: dict | None = None, + inputs: Iterable[expr.Var] = (), + captures: Iterable[expr.Var] = (), + declarations: Mapping[expr.Var, expr.Expr] | Iterable[Tuple[expr.Var, expr.Expr]] = (), ): if any(not isinstance(reg, (list, QuantumRegister, ClassicalRegister)) for reg in regs): # check if inputs are integers, but also allow e.g. 2.0 @@ -268,6 +290,20 @@ def __init__( self._global_phase: ParameterValueType = 0 self.global_phase = global_phase + # Add classical variables. Resolve inputs and captures first because they can't depend on + # anything, but declarations might depend on them. + self._vars_input: dict[str, expr.Var] = {} + self._vars_capture: dict[str, expr.Var] = {} + self._vars_local: dict[str, expr.Var] = {} + for input_ in inputs: + self.add_input(input_) + for capture in captures: + self.add_capture(capture) + if isinstance(declarations, Mapping): + declarations = declarations.items() + for var, initial in declarations: + self.add_var(var, initial) + self.duration = None self.unit = "dt" self.metadata = {} if metadata is None else metadata @@ -1111,6 +1147,33 @@ def ancillas(self) -> list[AncillaQubit]: """ return self._ancillas + def iter_vars(self) -> typing.Iterable[expr.Var]: + """Get an iterable over all runtime classical variables in scope within this circuit. + + This method will iterate over all variables in scope. For more fine-grained iterators, see + :meth:`iter_declared_vars`, :meth:`iter_input_vars` and :meth:`iter_captured_vars`.""" + return itertools.chain( + self._vars_input.values(), self._vars_capture.values(), self._vars_local.values() + ) + + def iter_declared_vars(self) -> typing.Iterable[expr.Var]: + """Get an iterable over all runtime classical variables that are declared with automatic + storage duration in this scope. This excludes input variables (see :meth:`iter_input_vars`) + and captured variables (see :meth:`iter_captured_vars`).""" + return self._vars_local.values() + + def iter_input_vars(self) -> typing.Iterable[expr.Var]: + """Get an iterable over all runtime classical variables that are declared as inputs to this + circuit scope. This excludes locally declared variables (see :meth:`iter_declared_vars`) + and captured variables (see :meth:`iter_captured_vars`).""" + return self._vars_input.values() + + def iter_captured_vars(self) -> typing.Iterable[expr.Var]: + """Get an iterable over all runtime classical variables that are captured by this circuit + scope from a containing scope. This excludes input variables (see :meth:`iter_input_vars`) + and locally declared variables (see :meth:`iter_declared_vars`).""" + return self._vars_capture.values() + def __and__(self, rhs: "QuantumCircuit") -> "QuantumCircuit": """Overload & to implement self.compose.""" return self.compose(rhs) @@ -1225,12 +1288,17 @@ def _resolve_classical_resource(self, specifier): def _validate_expr(self, node: expr.Expr) -> expr.Expr: for var in expr.iter_vars(node): - if isinstance(var.var, Clbit): + if var.standalone: + if not self.has_var(var): + raise CircuitError(f"Variable '{var}' is not present in this circuit.") + elif isinstance(var.var, Clbit): if var.var not in self._clbit_indices: raise CircuitError(f"Clbit {var.var} is not present in this circuit.") elif isinstance(var.var, ClassicalRegister): if var.var not in self.cregs: raise CircuitError(f"Register {var.var} is not present in this circuit.") + else: + raise RuntimeError(f"unhandled Var inner type in '{var}'") return node def append( @@ -1288,8 +1356,12 @@ def append( ) # Make copy of parameterized gate instances - if hasattr(operation, "params"): - is_parameter = any(isinstance(param, Parameter) for param in operation.params) + if params := getattr(operation, "params", ()): + is_parameter = False + for param in params: + is_parameter = is_parameter or isinstance(param, Parameter) + if isinstance(param, expr.Expr): + self._validate_expr(param) if is_parameter: operation = copy.deepcopy(operation) @@ -1407,6 +1479,243 @@ def _update_parameter_table(self, instruction: CircuitInstruction): # clear cache if new parameter is added self._parameters = None + @typing.overload + def get_var(self, name: str, default: T) -> Union[expr.Var, T]: + ... + + # The builtin `types` module has `EllipsisType`, but only from 3.10+! + @typing.overload + def get_var(self, name: str, default: type(...) = ...) -> expr.Var: + ... + + # We use a _literal_ `Ellipsis` as the marker value to leave `None` available as a default. + def get_var(self, name: str, default: typing.Any = ...): + """Retrieve a variable that is accessible in this circuit scope by name. + + Args: + name: the name of the variable to retrieve. + default: if given, this value will be returned if the variable is not present. If it + is not given, a :exc:`KeyError` is raised instead. + + Returns: + The corresponding variable. + + Raises: + KeyError: if no default is given, but the variable does not exist. + + Examples: + Retrieve a variable by name from a circuit:: + + from qiskit.circuit import QuantumCircuit + + # Create a circuit and create a variable in it. + qc = QuantumCircuit() + my_var = qc.add_var("my_var", False) + + # We can use 'my_var' as a variable, but let's say we've lost the Python object and + # need to retrieve it. + my_var_again = qc.get_var("my_var") + + assert my_var is my_var_again + + Get a variable from a circuit by name, returning some default if it is not present:: + + assert qc.get_var("my_var", None) is my_var + assert qc.get_var("unknown_variable", None) is None + """ + + if (out := self._vars_local.get(name)) is not None: + return out + if (out := self._vars_capture.get(name)) is not None: + return out + if (out := self._vars_input.get(name)) is not None: + return out + if default is Ellipsis: + raise KeyError(f"no variable named '{name}' is present") + return default + + def has_var(self, name_or_var: str | expr.Var, /) -> bool: + """Check whether a variable is defined in this scope. + + Args: + name_or_var: the variable, or name of a variable to check. If this is a + :class:`.expr.Var` node, the variable must be exactly the given one for this + function to return ``True``. + + Returns: + whether a matching variable is present. + + See also: + :meth:`QuantumCircuit.get_var`: retrieve a named variable from a circuit. + """ + if isinstance(name_or_var, str): + return self.get_var(name_or_var, None) is not None + return self.get_var(name_or_var.name, None) == name_or_var + + def _prepare_new_var( + self, name_or_var: str | expr.Var, type_: types.Type | None, / + ) -> expr.Var: + """The common logic for preparing and validating a new :class:`~.expr.Var` for the circuit. + + The given ``type_`` can be ``None`` if the variable specifier is already a :class:`.Var`, + and must be a :class:`~.types.Type` if it is a string. The argument is ignored if the given + first argument is a :class:`.Var` already. + + Returns the validated variable, which is guaranteed to be safe to add to the circuit.""" + if isinstance(name_or_var, str): + var = expr.Var.new(name_or_var, type_) + else: + var = name_or_var + if not var.standalone: + raise CircuitError( + "cannot add variables that wrap `Clbit` or `ClassicalRegister` instances." + " Use `add_bits` or `add_register` as appropriate." + ) + + # The `var` is guaranteed to have a name because we already excluded the cases where it's + # wrapping a bit/register. + if (previous := self.get_var(var.name, default=None)) is not None: + if previous == var: + raise CircuitError(f"'{var}' is already present in the circuit") + raise CircuitError(f"cannot add '{var}' as its name shadows the existing '{previous}'") + return var + + def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.Var: + """Add a classical variable with automatic storage and scope to this circuit. + + The variable is considered to have been "declared" at the beginning of the circuit, but it + only becomes initialized at the point of the circuit that you call this method, so it can + depend on variables defined before it. + + Args: + name_or_var: either a string of the variable name, or an existing instance of + :class:`~.expr.Var` to re-use. Variables cannot shadow names that are already in + use within the circuit. + initial: the value to initialize this variable with. If the first argument was given + as a string name, the type of the resulting variable is inferred from the initial + expression; to control this more manually, either use :meth:`.Var.new` to manually + construct a new variable with the desired type, or use :func:`.expr.cast` to cast + the initializer to the desired type. + + This must be either a :class:`~.expr.Expr` node, or a value that can be lifted to + one using :class:`.expr.lift`. + + Returns: + The created variable. If a :class:`~.expr.Var` instance was given, the exact same + object will be returned. + + Raises: + CircuitError: if the variable cannot be created due to shadowing an existing variable. + + Examples: + Define a new variable given just a name and an initializer expression:: + + from qiskit.circuit import QuantumCircuit + + qc = QuantumCircuit(2) + my_var = qc.add_var("my_var", False) + + Reuse a variable that may have been taking from a related circuit, or otherwise + constructed manually, and initialize it to some more complicated expression:: + + from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister + from qiskit.circuit.classical import expr, types + + my_var = expr.Var.new("my_var", types.Uint(8)) + + cr1 = ClassicalRegister(8, "cr1") + cr2 = ClassicalRegister(8, "cr2") + qc = QuantumCircuit(QuantumRegister(8), cr1, cr2) + + # Get some measurement results into each register. + qc.h(0) + for i in range(1, 8): + qc.cx(0, i) + qc.measure(range(8), cr1) + + qc.reset(range(8)) + qc.h(0) + for i in range(1, 8): + qc.cx(0, i) + qc.measure(range(8), cr2) + + # Now when we add the variable, it is initialized using the runtime state of the two + # classical registers we measured into above. + qc.add_var(my_var, expr.bit_and(cr1, cr2)) + """ + # Validate the initialiser first to catch cases where the variable to be declared is being + # used in the initialiser. + initial = self._validate_expr(expr.lift(initial)) + var = self._prepare_new_var(name_or_var, initial.type) + # Store is responsible for ensuring the type safety of the initialisation. We build this + # before actually modifying any of our own state, so we don't get into an inconsistent state + # if an exception is raised later. + store = Store(var, initial) + + self._vars_local[var.name] = var + self._append(CircuitInstruction(store, (), ())) + return var + + def add_capture(self, var: expr.Var): + """Add a variable to the circuit that it should capture from a scope it will be contained + within. + + This method requires a :class:`~.expr.Var` node to enforce that you've got a handle to one, + because you will need to declare the same variable using the same object into the outer + circuit. + + This is a low-level method. You typically will not need to call this method, assuming you + are using the builder interface for control-flow scopes (``with`` context-manager statements + for :meth:`if_test` and the other scoping constructs). The builder interface will + automatically make the inner scopes closures on your behalf by capturing any variables that + are used within them. + + Args: + var: the variable to capture from an enclosing scope. + + Raises: + CircuitError: if the variable cannot be created due to shadowing an existing variable. + """ + if self._vars_input: + raise CircuitError( + "circuits with input variables cannot be enclosed, so cannot be closures" + ) + self._vars_capture[var.name] = self._prepare_new_var(var, None) + + @typing.overload + def add_input(self, name_or_var: str, type_: types.Type, /) -> expr.Var: + ... + + @typing.overload + def add_input(self, name_or_var: expr.Var, type_: None = None, /) -> expr.Var: + ... + + def add_input( # pylint: disable=missing-raises-doc + self, name_or_var: str | expr.Var, type_: types.Type | None = None, / + ) -> expr.Var: + """Register a variable as an input to the circuit. + + Args: + name_or_var: either a string name, or an existing :class:`~.expr.Var` node to use as the + input variable. + type_: if the name is given as a string, then this must be a :class:`~.types.Type` to + use for the variable. If the variable is given as an existing :class:`~.expr.Var`, + then this must not be given, and will instead be read from the object itself. + + Returns: + the variable created, or the same variable as was passed in. + + Raises: + CircuitError: if the variable cannot be created due to shadowing an existing variable. + """ + if self._vars_capture: + raise CircuitError("circuits to be enclosed with captures cannot have input variables") + if isinstance(name_or_var, expr.Var) and type_ is not None: + raise ValueError("cannot give an explicit type with an existing Var") + var = self._prepare_new_var(name_or_var, type_) + self._vars_input[var.name] = var + return var + def add_register(self, *regs: Register | int | Sequence[Bit]) -> None: """Add registers.""" if not regs: @@ -2208,6 +2517,27 @@ def reset(self, qubit: QubitSpecifier) -> InstructionSet: return self.append(Reset(), [qubit], []) + def store(self, lvalue: typing.Any, rvalue: typing.Any, /) -> InstructionSet: + """Store the result of the given runtime classical expression ``rvalue`` in the memory + location defined by ``lvalue``. + + Typically ``lvalue`` will be a :class:`~.expr.Var` node and ``rvalue`` will be some + :class:`~.expr.Expr` to write into it, but anything that :func:`.expr.lift` can raise to an + :class:`~.expr.Expr` is permissible in both places, and it will be called on them. + + Args: + lvalue: a valid specifier for a memory location in the circuit. This will typically be + a :class:`~.expr.Var` node, but you can also write to :class:`.Clbit` or + :class:`.ClassicalRegister` memory locations if your hardware supports it. + rvalue: a runtime classical expression whose result should be written into the given + memory location. + + .. seealso:: + :class:`~.circuit.Store` + the backing :class:`~.circuit.Instruction` class that represents this operation. + """ + return self.append(Store(expr.lift(lvalue), expr.lift(rvalue)), (), ()) + def measure(self, qubit: QubitSpecifier, cbit: ClbitSpecifier) -> InstructionSet: r"""Measure a quantum bit (``qubit``) in the Z basis into a classical bit (``cbit``). diff --git a/releasenotes/notes/expr-hashable-var-types-7cf2aaa00b201ae6.yaml b/releasenotes/notes/expr-hashable-var-types-7cf2aaa00b201ae6.yaml new file mode 100644 index 000000000000..70a1cf81d061 --- /dev/null +++ b/releasenotes/notes/expr-hashable-var-types-7cf2aaa00b201ae6.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Classical types (subclasses of :class:`~classical.types.Type`) and variables (:class:`~.expr.Var`) + are now hashable. diff --git a/releasenotes/notes/expr-var-standalone-2c1116583a2be9fd.yaml b/releasenotes/notes/expr-var-standalone-2c1116583a2be9fd.yaml new file mode 100644 index 000000000000..71ec0320032e --- /dev/null +++ b/releasenotes/notes/expr-var-standalone-2c1116583a2be9fd.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + :class:`~.expr.Var` nodes now have a :attr:`.Var.standalone` property to quickly query whether + they are new-style memory-owning variables, or whether they wrap old-style classical memory in + the form of a :class:`.Clbit` or :class:`.ClassicalRegister`. diff --git a/test/python/circuit/classical/test_expr_properties.py b/test/python/circuit/classical/test_expr_properties.py index efda6ba37758..f8c1277cd0f4 100644 --- a/test/python/circuit/classical/test_expr_properties.py +++ b/test/python/circuit/classical/test_expr_properties.py @@ -14,11 +14,12 @@ import copy import pickle +import uuid import ddt from qiskit.test import QiskitTestCase -from qiskit.circuit import ClassicalRegister +from qiskit.circuit import ClassicalRegister, Clbit from qiskit.circuit.classical import expr, types @@ -98,3 +99,36 @@ def test_var_uuid_clone(self): 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)) + + def test_var_standalone(self): + """Test that the ``Var.standalone`` property is set correctly.""" + self.assertTrue(expr.Var.new("a", types.Bool()).standalone) + self.assertTrue(expr.Var.new("a", types.Uint(8)).standalone) + self.assertFalse(expr.Var(Clbit(), types.Bool()).standalone) + self.assertFalse(expr.Var(ClassicalRegister(8, "cr"), types.Uint(8)).standalone) + + def test_var_hashable(self): + clbits = [Clbit(), Clbit()] + cregs = [ClassicalRegister(2, "cr1"), ClassicalRegister(2, "cr2")] + + vars_ = [ + expr.Var.new("a", types.Bool()), + expr.Var.new("b", types.Uint(16)), + expr.Var(clbits[0], types.Bool()), + expr.Var(clbits[1], types.Bool()), + expr.Var(cregs[0], types.Uint(2)), + expr.Var(cregs[1], types.Uint(2)), + ] + duplicates = [ + expr.Var(uuid.UUID(bytes=vars_[0].var.bytes), types.Bool(), name=vars_[0].name), + expr.Var(uuid.UUID(bytes=vars_[1].var.bytes), types.Uint(16), name=vars_[1].name), + expr.Var(clbits[0], types.Bool()), + expr.Var(clbits[1], types.Bool()), + expr.Var(cregs[0], types.Uint(2)), + expr.Var(cregs[1], types.Uint(2)), + ] + + # Smoke test. + self.assertEqual(vars_, duplicates) + # Actual test of hashability properties. + self.assertEqual(set(vars_ + duplicates), set(vars_)) diff --git a/test/python/circuit/test_circuit_vars.py b/test/python/circuit/test_circuit_vars.py new file mode 100644 index 000000000000..fa7138df9be3 --- /dev/null +++ b/test/python/circuit/test_circuit_vars.py @@ -0,0 +1,366 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring + +from qiskit.test import QiskitTestCase +from qiskit.circuit import QuantumCircuit, CircuitError, Clbit, ClassicalRegister +from qiskit.circuit.classical import expr, types + + +class TestCircuitVars(QiskitTestCase): + """Tests for variable-manipulation routines on circuits. More specific functionality is likely + tested in the suites of the specific methods.""" + + def test_initialise_inputs(self): + vars_ = [expr.Var.new("a", types.Bool()), expr.Var.new("b", types.Uint(16))] + qc = QuantumCircuit(inputs=vars_) + self.assertEqual(set(vars_), set(qc.iter_vars())) + + def test_initialise_captures(self): + vars_ = [expr.Var.new("a", types.Bool()), expr.Var.new("b", types.Uint(16))] + qc = QuantumCircuit(captures=vars_) + self.assertEqual(set(vars_), set(qc.iter_vars())) + + def test_initialise_declarations_iterable(self): + vars_ = [ + (expr.Var.new("a", types.Bool()), expr.lift(True)), + (expr.Var.new("b", types.Uint(16)), expr.lift(0xFFFF)), + ] + qc = QuantumCircuit(declarations=vars_) + + self.assertEqual({var for var, _initialiser in vars_}, set(qc.iter_vars())) + operations = [ + (instruction.operation.name, instruction.operation.lvalue, instruction.operation.rvalue) + for instruction in qc.data + ] + self.assertEqual(operations, [("store", lvalue, rvalue) for lvalue, rvalue in vars_]) + + def test_initialise_declarations_mapping(self): + # Dictionary iteration order is guaranteed to be insertion order. + vars_ = { + expr.Var.new("a", types.Bool()): expr.lift(True), + expr.Var.new("b", types.Uint(16)): expr.lift(0xFFFF), + } + qc = QuantumCircuit(declarations=vars_) + + self.assertEqual(set(vars_), set(qc.iter_vars())) + operations = [ + (instruction.operation.name, instruction.operation.lvalue, instruction.operation.rvalue) + for instruction in qc.data + ] + self.assertEqual( + operations, [("store", lvalue, rvalue) for lvalue, rvalue in vars_.items()] + ) + + def test_initialise_declarations_dependencies(self): + """Test that the cirucit initialiser can take in declarations with dependencies between + them, provided they're specified in a suitable order.""" + a = expr.Var.new("a", types.Bool()) + vars_ = [ + (a, expr.lift(True)), + (expr.Var.new("b", types.Bool()), a), + ] + qc = QuantumCircuit(declarations=vars_) + + self.assertEqual({var for var, _initialiser in vars_}, set(qc.iter_vars())) + operations = [ + (instruction.operation.name, instruction.operation.lvalue, instruction.operation.rvalue) + for instruction in qc.data + ] + self.assertEqual(operations, [("store", lvalue, rvalue) for lvalue, rvalue in vars_]) + + def test_initialise_inputs_declarations(self): + a = expr.Var.new("a", types.Uint(16)) + b = expr.Var.new("b", types.Uint(16)) + b_init = expr.bit_and(a, 0xFFFF) + qc = QuantumCircuit(inputs=[a], declarations={b: b_init}) + + self.assertEqual({a}, set(qc.iter_input_vars())) + self.assertEqual({b}, set(qc.iter_declared_vars())) + self.assertEqual({a, b}, set(qc.iter_vars())) + operations = [ + (instruction.operation.name, instruction.operation.lvalue, instruction.operation.rvalue) + for instruction in qc.data + ] + self.assertEqual(operations, [("store", b, b_init)]) + + def test_initialise_captures_declarations(self): + a = expr.Var.new("a", types.Uint(16)) + b = expr.Var.new("b", types.Uint(16)) + b_init = expr.bit_and(a, 0xFFFF) + qc = QuantumCircuit(captures=[a], declarations={b: b_init}) + + self.assertEqual({a}, set(qc.iter_captured_vars())) + self.assertEqual({b}, set(qc.iter_declared_vars())) + self.assertEqual({a, b}, set(qc.iter_vars())) + operations = [ + (instruction.operation.name, instruction.operation.lvalue, instruction.operation.rvalue) + for instruction in qc.data + ] + self.assertEqual(operations, [("store", b, b_init)]) + + def test_add_var_returns_good_var(self): + qc = QuantumCircuit() + a = qc.add_var("a", expr.lift(True)) + self.assertEqual(a.name, "a") + self.assertEqual(a.type, types.Bool()) + + b = qc.add_var("b", expr.Value(0xFF, types.Uint(8))) + self.assertEqual(b.name, "b") + self.assertEqual(b.type, types.Uint(8)) + + def test_add_var_returns_input(self): + """Test that the `Var` returned by `add_var` is the same as the input if `Var`.""" + a = expr.Var.new("a", types.Bool()) + qc = QuantumCircuit() + a_other = qc.add_var(a, expr.lift(True)) + self.assertIs(a, a_other) + + def test_add_input_returns_good_var(self): + qc = QuantumCircuit() + a = qc.add_input("a", types.Bool()) + self.assertEqual(a.name, "a") + self.assertEqual(a.type, types.Bool()) + + b = qc.add_input("b", types.Uint(8)) + self.assertEqual(b.name, "b") + self.assertEqual(b.type, types.Uint(8)) + + def test_add_input_returns_input(self): + """Test that the `Var` returned by `add_input` is the same as the input if `Var`.""" + a = expr.Var.new("a", types.Bool()) + qc = QuantumCircuit() + a_other = qc.add_input(a) + self.assertIs(a, a_other) + + def test_cannot_have_both_inputs_and_captures(self): + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + + with self.assertRaisesRegex(CircuitError, "circuits with input.*cannot be closures"): + QuantumCircuit(inputs=[a], captures=[b]) + + qc = QuantumCircuit(inputs=[a]) + with self.assertRaisesRegex(CircuitError, "circuits with input.*cannot be closures"): + qc.add_capture(b) + + qc = QuantumCircuit(captures=[a]) + with self.assertRaisesRegex(CircuitError, "circuits to be enclosed.*cannot have input"): + qc.add_input(b) + + def test_cannot_add_cyclic_declaration(self): + a = expr.Var.new("a", types.Bool()) + with self.assertRaisesRegex(CircuitError, "not present in this circuit"): + QuantumCircuit(declarations=[(a, a)]) + + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "not present in this circuit"): + qc.add_var(a, a) + + def test_initialise_inputs_equal_to_add_input(self): + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(16)) + + qc_init = QuantumCircuit(inputs=[a, b]) + qc_manual = QuantumCircuit() + qc_manual.add_input(a) + qc_manual.add_input(b) + self.assertEqual(list(qc_init.iter_vars()), list(qc_manual.iter_vars())) + + qc_manual = QuantumCircuit() + a = qc_manual.add_input("a", types.Bool()) + b = qc_manual.add_input("b", types.Uint(16)) + qc_init = QuantumCircuit(inputs=[a, b]) + self.assertEqual(list(qc_init.iter_vars()), list(qc_manual.iter_vars())) + + def test_initialise_captures_equal_to_add_capture(self): + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(16)) + + qc_init = QuantumCircuit(captures=[a, b]) + qc_manual = QuantumCircuit() + qc_manual.add_capture(a) + qc_manual.add_capture(b) + self.assertEqual(list(qc_init.iter_vars()), list(qc_manual.iter_vars())) + + def test_initialise_declarations_equal_to_add_var(self): + a = expr.Var.new("a", types.Bool()) + a_init = expr.lift(False) + b = expr.Var.new("b", types.Uint(16)) + b_init = expr.lift(0xFFFF) + + qc_init = QuantumCircuit(declarations=[(a, a_init), (b, b_init)]) + qc_manual = QuantumCircuit() + qc_manual.add_var(a, a_init) + qc_manual.add_var(b, b_init) + self.assertEqual(list(qc_init.iter_vars()), list(qc_manual.iter_vars())) + self.assertEqual(qc_init.data, qc_manual.data) + + qc_manual = QuantumCircuit() + a = qc_manual.add_var("a", a_init) + b = qc_manual.add_var("b", b_init) + qc_init = QuantumCircuit(declarations=[(a, a_init), (b, b_init)]) + self.assertEqual(list(qc_init.iter_vars()), list(qc_manual.iter_vars())) + self.assertEqual(qc_init.data, qc_manual.data) + + def test_cannot_shadow_vars(self): + """Test that exact duplicate ``Var`` nodes within different combinations of the inputs are + detected and rejected.""" + a = expr.Var.new("a", types.Bool()) + a_init = expr.lift(True) + with self.assertRaisesRegex(CircuitError, "already present"): + QuantumCircuit(inputs=[a, a]) + with self.assertRaisesRegex(CircuitError, "already present"): + QuantumCircuit(captures=[a, a]) + with self.assertRaisesRegex(CircuitError, "already present"): + QuantumCircuit(declarations=[(a, a_init), (a, a_init)]) + with self.assertRaisesRegex(CircuitError, "already present"): + QuantumCircuit(inputs=[a], declarations=[(a, a_init)]) + with self.assertRaisesRegex(CircuitError, "already present"): + QuantumCircuit(captures=[a], declarations=[(a, a_init)]) + + def test_cannot_shadow_names(self): + """Test that exact duplicate ``Var`` nodes within different combinations of the inputs are + detected and rejected.""" + a_bool1 = expr.Var.new("a", types.Bool()) + a_bool2 = expr.Var.new("a", types.Bool()) + a_uint = expr.Var.new("a", types.Uint(16)) + a_bool_init = expr.lift(True) + a_uint_init = expr.lift(0xFFFF) + + tests = [ + ((a_bool1, a_bool_init), (a_bool2, a_bool_init)), + ((a_bool1, a_bool_init), (a_uint, a_uint_init)), + ] + for (left, left_init), (right, right_init) in tests: + with self.assertRaisesRegex(CircuitError, "its name shadows"): + QuantumCircuit(inputs=(left, right)) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + QuantumCircuit(captures=(left, right)) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + QuantumCircuit(declarations=[(left, left_init), (right, right_init)]) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + QuantumCircuit(inputs=[left], declarations=[(right, right_init)]) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + QuantumCircuit(captures=[left], declarations=[(right, right_init)]) + + qc = QuantumCircuit(inputs=[left]) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + qc.add_input(right) + qc = QuantumCircuit(inputs=[left]) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + qc.add_var(right, right_init) + + qc = QuantumCircuit(captures=[left]) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + qc.add_capture(right) + qc = QuantumCircuit(captures=[left]) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + qc.add_var(right, right_init) + + qc = QuantumCircuit(inputs=[left]) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + qc.add_var(right, right_init) + + qc = QuantumCircuit() + qc.add_var("a", expr.lift(True)) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + qc.add_var("a", expr.lift(True)) + with self.assertRaisesRegex(CircuitError, "its name shadows"): + qc.add_var("a", expr.lift(0xFF)) + + def test_cannot_add_vars_wrapping_clbits(self): + a = expr.Var(Clbit(), types.Bool()) + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + QuantumCircuit(inputs=[a]) + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + qc.add_input(a) + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + QuantumCircuit(captures=[a]) + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + qc.add_capture(a) + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + QuantumCircuit(declarations=[(a, expr.lift(True))]) + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + qc.add_var(a, expr.lift(True)) + + def test_cannot_add_vars_wrapping_cregs(self): + a = expr.Var(ClassicalRegister(8, "cr"), types.Uint(8)) + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + QuantumCircuit(inputs=[a]) + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + qc.add_input(a) + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + QuantumCircuit(captures=[a]) + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + qc.add_capture(a) + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + QuantumCircuit(declarations=[(a, expr.lift(0xFF))]) + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): + qc.add_var(a, expr.lift(0xFF)) + + def test_get_var_success(self): + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + + qc = QuantumCircuit(inputs=[a], declarations={b: expr.Value(0xFF, types.Uint(8))}) + self.assertIs(qc.get_var("a"), a) + self.assertIs(qc.get_var("b"), b) + + qc = QuantumCircuit(captures=[a, b]) + self.assertIs(qc.get_var("a"), a) + self.assertIs(qc.get_var("b"), b) + + qc = QuantumCircuit(declarations={a: expr.lift(True), b: expr.Value(0xFF, types.Uint(8))}) + self.assertIs(qc.get_var("a"), a) + self.assertIs(qc.get_var("b"), b) + + def test_get_var_missing(self): + qc = QuantumCircuit() + with self.assertRaises(KeyError): + qc.get_var("a") + + a = expr.Var.new("a", types.Bool()) + qc.add_input(a) + with self.assertRaises(KeyError): + qc.get_var("b") + + def test_get_var_default(self): + qc = QuantumCircuit() + self.assertIs(qc.get_var("a", None), None) + + missing = "default" + a = expr.Var.new("a", types.Bool()) + qc.add_input(a) + self.assertIs(qc.get_var("b", missing), missing) + self.assertIs(qc.get_var("b", a), a) + + def test_has_var(self): + a = expr.Var.new("a", types.Bool()) + self.assertFalse(QuantumCircuit().has_var("a")) + self.assertTrue(QuantumCircuit(inputs=[a]).has_var("a")) + self.assertTrue(QuantumCircuit(captures=[a]).has_var("a")) + self.assertTrue(QuantumCircuit(declarations={a: expr.lift(True)}).has_var("a")) + self.assertTrue(QuantumCircuit(inputs=[a]).has_var(a)) + self.assertTrue(QuantumCircuit(captures=[a]).has_var(a)) + self.assertTrue(QuantumCircuit(declarations={a: expr.lift(True)}).has_var(a)) + + # When giving an `Var`, the match must be exact, not just the name. + self.assertFalse(QuantumCircuit(inputs=[a]).has_var(expr.Var.new("a", types.Uint(8)))) + self.assertFalse(QuantumCircuit(inputs=[a]).has_var(expr.Var.new("a", types.Bool()))) diff --git a/test/python/circuit/test_store.py b/test/python/circuit/test_store.py index 6d5c4707cbed..7977765d8e45 100644 --- a/test/python/circuit/test_store.py +++ b/test/python/circuit/test_store.py @@ -13,7 +13,7 @@ # pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring from qiskit.test import QiskitTestCase -from qiskit.circuit import Store, Clbit, CircuitError +from qiskit.circuit import Store, Clbit, CircuitError, QuantumCircuit, ClassicalRegister from qiskit.circuit.classical import expr, types @@ -60,3 +60,140 @@ def test_rejects_c_if(self): instruction = Store(expr.Var.new("a", types.Bool()), expr.Var.new("b", types.Bool())) with self.assertRaises(NotImplementedError): instruction.c_if(Clbit(), False) + + +class TestStoreCircuit(QiskitTestCase): + """Tests of the `QuantumCircuit.store` method and appends of `Store`.""" + + def test_produces_expected_operation(self): + a = expr.Var.new("a", types.Bool()) + value = expr.Value(True, types.Bool()) + + qc = QuantumCircuit(inputs=[a]) + qc.store(a, value) + self.assertEqual(qc.data[-1].operation, Store(a, value)) + + qc = QuantumCircuit(captures=[a]) + qc.store(a, value) + self.assertEqual(qc.data[-1].operation, Store(a, value)) + + qc = QuantumCircuit(declarations=[(a, expr.lift(False))]) + qc.store(a, value) + self.assertEqual(qc.data[-1].operation, Store(a, value)) + + def test_allows_stores_with_clbits(self): + clbits = [Clbit(), Clbit()] + a = expr.Var.new("a", types.Bool()) + qc = QuantumCircuit(clbits, inputs=[a]) + qc.store(clbits[0], True) + qc.store(expr.Var(clbits[1], types.Bool()), a) + qc.store(clbits[0], clbits[1]) + qc.store(expr.lift(clbits[0]), expr.lift(clbits[1])) + qc.store(a, expr.lift(clbits[1])) + + expected = [ + Store(expr.lift(clbits[0]), expr.lift(True)), + Store(expr.lift(clbits[1]), a), + Store(expr.lift(clbits[0]), expr.lift(clbits[1])), + Store(expr.lift(clbits[0]), expr.lift(clbits[1])), + Store(a, expr.lift(clbits[1])), + ] + actual = [instruction.operation for instruction in qc.data] + self.assertEqual(actual, expected) + + def test_allows_stores_with_cregs(self): + cregs = [ClassicalRegister(8, "cr1"), ClassicalRegister(8, "cr2")] + a = expr.Var.new("a", types.Uint(8)) + qc = QuantumCircuit(*cregs, captures=[a]) + qc.store(cregs[0], 0xFF) + qc.store(expr.Var(cregs[1], types.Uint(8)), a) + qc.store(cregs[0], cregs[1]) + qc.store(expr.lift(cregs[0]), expr.lift(cregs[1])) + qc.store(a, cregs[1]) + + expected = [ + Store(expr.lift(cregs[0]), expr.lift(0xFF)), + Store(expr.lift(cregs[1]), a), + Store(expr.lift(cregs[0]), expr.lift(cregs[1])), + Store(expr.lift(cregs[0]), expr.lift(cregs[1])), + Store(a, expr.lift(cregs[1])), + ] + actual = [instruction.operation for instruction in qc.data] + self.assertEqual(actual, expected) + + def test_lifts_values(self): + a = expr.Var.new("a", types.Bool()) + qc = QuantumCircuit(captures=[a]) + qc.store(a, True) + self.assertEqual(qc.data[-1].operation, Store(a, expr.lift(True))) + + b = expr.Var.new("b", types.Uint(16)) + qc.add_capture(b) + qc.store(b, 0xFFFF) + self.assertEqual(qc.data[-1].operation, Store(b, expr.lift(0xFFFF))) + + def test_rejects_vars_not_in_circuit(self): + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "'a'.*not present"): + qc.store(expr.Var.new("a", types.Bool()), True) + + # Not the same 'a' + qc.add_input(a) + with self.assertRaisesRegex(CircuitError, "'a'.*not present"): + qc.store(expr.Var.new("a", types.Bool()), True) + with self.assertRaisesRegex(CircuitError, "'b'.*not present"): + qc.store(a, b) + + def test_rejects_bits_not_in_circuit(self): + a = expr.Var.new("a", types.Bool()) + clbit = Clbit() + qc = QuantumCircuit(captures=[a]) + with self.assertRaisesRegex(CircuitError, "not present"): + qc.store(clbit, False) + with self.assertRaisesRegex(CircuitError, "not present"): + qc.store(clbit, a) + with self.assertRaisesRegex(CircuitError, "not present"): + qc.store(a, clbit) + + def test_rejects_cregs_not_in_circuit(self): + a = expr.Var.new("a", types.Uint(8)) + creg = ClassicalRegister(8, "cr1") + qc = QuantumCircuit(captures=[a]) + with self.assertRaisesRegex(CircuitError, "not present"): + qc.store(creg, 0xFF) + with self.assertRaisesRegex(CircuitError, "not present"): + qc.store(creg, a) + with self.assertRaisesRegex(CircuitError, "not present"): + qc.store(a, creg) + + def test_rejects_non_lvalue(self): + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + qc = QuantumCircuit(inputs=[a, b]) + not_an_lvalue = expr.logic_and(a, b) + with self.assertRaisesRegex(CircuitError, "not an l-value"): + qc.store(not_an_lvalue, expr.lift(False)) + + def test_rejects_explicit_cast(self): + lvalue = expr.Var.new("a", types.Uint(16)) + rvalue = expr.Var.new("b", types.Uint(8)) + qc = QuantumCircuit(inputs=[lvalue, rvalue]) + with self.assertRaisesRegex(CircuitError, "an explicit cast is required"): + qc.store(lvalue, rvalue) + + def test_rejects_dangerous_cast(self): + lvalue = expr.Var.new("a", types.Uint(8)) + rvalue = expr.Var.new("b", types.Uint(16)) + qc = QuantumCircuit(inputs=[lvalue, rvalue]) + with self.assertRaisesRegex(CircuitError, "an explicit cast is required.*may be lossy"): + qc.store(lvalue, rvalue) + + def test_rejects_c_if(self): + a = expr.Var.new("a", types.Bool()) + qc = QuantumCircuit([Clbit()], inputs=[a]) + instruction_set = qc.store(a, True) + with self.assertRaises(NotImplementedError): + instruction_set.c_if(qc.clbits[0], False)