From c619ef999d8db3933f4f6dc69d80e100f46262d6 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 2 Oct 2023 15:16:04 +0100 Subject: [PATCH] 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 | 85 +++++++++++++++++++ .../circuit/classical/test_expr_helpers.py | 27 ++++++ .../circuit/classical/test_types_ordering.py | 10 +++ test/python/circuit/test_store.py | 62 ++++++++++++++ 10 files changed, 348 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 e2f7936acbcc..7a2459dedb88 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -280,6 +280,7 @@ Operation EquivalenceLibrary SingletonGate + 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..8750ff52ec96 --- /dev/null +++ b/qiskit/circuit/store.py @@ -0,0 +1,85 @@ +# 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. + +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)