From af4ba52fb275fdb7e582a30816b0d2acf4dd67c2 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Fri, 7 Feb 2025 10:09:56 -0500 Subject: [PATCH 01/53] WIP --- qiskit/circuit/classical/expr/constructors.py | 75 ++++++-- qiskit/circuit/classical/expr/visitors.py | 8 +- qiskit/circuit/classical/types/ordering.py | 27 ++- qiskit/circuit/classical/types/types.py | 64 +++++- .../classical/test_expr_constructors.py | 182 +++++++++++++++--- 5 files changed, 289 insertions(+), 67 deletions(-) diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index de3875eef90..91013ef115f 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -124,16 +124,19 @@ def lift(value: typing.Any, /, type: types.Type | None = None) -> Expr: from qiskit.circuit import Clbit, ClassicalRegister # pylint: disable=cyclic-import inferred: types.Type - if value is True or value is False or isinstance(value, Clbit): + if value is True or value is False: + inferred = types.Bool(const=True) + constructor = Value + elif isinstance(value, Clbit): inferred = types.Bool() - constructor = Value if value is True or value is False else Var + constructor = Var elif isinstance(value, ClassicalRegister): inferred = types.Uint(width=value.size) constructor = Var elif isinstance(value, int): if value < 0: raise ValueError("cannot represent a negative value") - inferred = types.Uint(width=value.bit_length() or 1) + inferred = types.Uint(width=value.bit_length() or 1, const=True) constructor = Value else: raise TypeError(f"failed to infer a type for '{value}'") @@ -198,41 +201,73 @@ def logic_not(operand: typing.Any, /) -> Expr: Cast(Var(ClassicalRegister(3, 'c'), Uint(3)), Bool(), implicit=True), \ Bool()) """ - operand = _coerce_lossless(lift(operand), types.Bool()) + var_or_value = lift(operand) + operand = _coerce_lossless(var_or_value, types.Bool(const=var_or_value.type.const)) return Unary(Unary.Op.LOGIC_NOT, operand, operand.type) def _lift_binary_operands(left: typing.Any, right: typing.Any) -> tuple[Expr, Expr]: """Lift two binary operands simultaneously, inferring the widths of integer literals in either - position to match the other operand.""" - left_int = isinstance(left, int) and not isinstance(left, bool) - right_int = isinstance(right, int) and not isinstance(right, bool) + position to match the other operand. + + Const-ness is handled as follows: + * If neither operand is an expression, both are lifted to share the same const-ness. + Both will be const, if possible. Else, neither will be. + * If only one operand is an expression, the other is lifted with the same const-ness, if possible. + Otherwise, the returned operands will have different const-ness, and thus require a cast node. + * If both operands are expressions, they are returned as-is and may require a cast node. + """ + left_bool = isinstance(left, bool) + left_int = isinstance(left, int) and not left_bool + right_bool = isinstance(right, bool) + right_int = isinstance(right, int) and not right_bool if not (left_int or right_int): - left = lift(left) - right = lift(right) + if left_bool == right_bool: + # If they're both bool, lifting them will produce const Bool. + # If neither are bool, they're a mix of bits/registers (which are always + # non-const) and Expr, which we can't modify the const-ness of without + # a cast node. + left = lift(left) + right = lift(right) + elif not right_bool: + # Left is a bool + right = lift(right) + # TODO: if right.type isn't Bool, there's a type mismatch so we _should_ + # raise here. But, _binary_bitwise will error for us with a better msg. + left = lift(left, right.type if right.type.kind is types.Bool else None) + elif not left_bool: + # Right is a bool. + left = lift(left) + # TODO: if left.type isn't Bool, there's a type mismatch so we _should_ + # raise here. But, _binary_bitwise will error for us with a better msg. + right = lift(right, left.type if left.type.kind is types.Bool else None) elif not right_int: + # Left is an int. right = lift(right) if right.type.kind is types.Uint: if left.bit_length() > right.type.width: raise TypeError( f"integer literal '{left}' is wider than the other operand '{right}'" ) + # Left will share const-ness of right. left = Value(left, right.type) else: left = lift(left) elif not left_int: + # Right is an int. left = lift(left) if left.type.kind is types.Uint: if right.bit_length() > left.type.width: raise TypeError( f"integer literal '{right}' is wider than the other operand '{left}'" ) + # Right will share const-ness of left. right = Value(right, left.type) else: right = lift(right) else: # Both are `int`, so we take our best case to make things work. - uint = types.Uint(max(left.bit_length(), right.bit_length(), 1)) + uint = types.Uint(max(left.bit_length(), right.bit_length(), 1), const=True) left = Value(left, uint) right = Value(right, uint) return left, right @@ -242,14 +277,14 @@ def _binary_bitwise(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: left, right = _lift_binary_operands(left, right) type: types.Type if left.type.kind is right.type.kind is types.Bool: - type = types.Bool() + type = types.Bool(const=(left.type.const and right.type.const)) elif left.type.kind is types.Uint and right.type.kind is types.Uint: if left.type != right.type: raise TypeError( "binary bitwise operations are defined between unsigned integers of the same width," f" but got {left.type.width} and {right.type.width}." ) - type = left.type + type = types.Uint(width=left.type.width, const=(left.type.const and right.type.const)) else: raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") return Binary(op, left, right, type) @@ -313,10 +348,10 @@ def bit_xor(left: typing.Any, right: typing.Any, /) -> Expr: def _binary_logical(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: - bool_ = types.Bool() - left = _coerce_lossless(lift(left), bool_) - right = _coerce_lossless(lift(right), bool_) - return Binary(op, left, right, bool_) + left, right = _lift_binary_operands(left, right) + left = _coerce_lossless(left, types.Bool(const=left.type.const)) + right = _coerce_lossless(right, types.Bool(const=right.type.const)) + return Binary(op, left, right, types.Bool(const=(left.type.const and right.type.const))) def logic_and(left: typing.Any, right: typing.Any, /) -> Expr: @@ -354,7 +389,7 @@ def _equal_like(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: if left.type.kind is not right.type.kind: raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") type = types.greater(left.type, right.type) - return Binary(op, _coerce_lossless(left, type), _coerce_lossless(right, type), types.Bool()) + return Binary(op, _coerce_lossless(left, type), _coerce_lossless(right, type), types.Bool(const=type.const)) def equal(left: typing.Any, right: typing.Any, /) -> Expr: @@ -398,7 +433,7 @@ def _binary_relation(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr if left.type.kind is not right.type.kind or left.type.kind is types.Bool: raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") type = types.greater(left.type, right.type) - return Binary(op, _coerce_lossless(left, type), _coerce_lossless(right, type), types.Bool()) + return Binary(op, _coerce_lossless(left, type), _coerce_lossless(right, type), types.Bool(const=type.const)) def less(left: typing.Any, right: typing.Any, /) -> Expr: @@ -485,7 +520,7 @@ def _shift_like( right = lift(right) if left.type.kind != types.Uint or right.type.kind != types.Uint: raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") - return Binary(op, left, right, left.type) + return Binary(op, left, right, types.Uint(width=left.type.width, const=(left.type.const and right.type.const))) def shift_left(left: typing.Any, right: typing.Any, /, type: types.Type | None = None) -> Expr: @@ -553,4 +588,4 @@ def index(target: typing.Any, index: typing.Any, /) -> Expr: target, index = lift(target), lift(index) if target.type.kind is not types.Uint or index.type.kind is not types.Uint: raise TypeError(f"invalid types for indexing: '{target.type}' and '{index.type}'") - return Index(target, index, types.Bool()) + return Index(target, index, types.Bool(const=target.type.const)) diff --git a/qiskit/circuit/classical/expr/visitors.py b/qiskit/circuit/classical/expr/visitors.py index be7e9311c37..60783380144 100644 --- a/qiskit/circuit/classical/expr/visitors.py +++ b/qiskit/circuit/classical/expr/visitors.py @@ -276,8 +276,8 @@ def is_lvalue(node: expr.Expr, /) -> bool: >>> expr.is_lvalue(expr.lift(2)) False - :class:`~.expr.Var` nodes are always l-values, because they always have some associated - memory location:: + :class:`~.expr.Var` nodes are l-values (unless their resolution type is `const`!), because + they have some associated memory location:: >>> from qiskit.circuit.classical import types >>> from qiskit.circuit import Clbit @@ -297,4 +297,8 @@ def is_lvalue(node: expr.Expr, /) -> bool: >>> expr.is_lvalue(expr.bit_and(a, b)) False """ + if node.type.const: + # If the expression's resolution type is const, then this can never be + # an l-value (even if the expression is a Var). + return False return node.accept(_IS_LVALUE) diff --git a/qiskit/circuit/classical/types/ordering.py b/qiskit/circuit/classical/types/ordering.py index 5a4365b8e14..f0ed723d992 100644 --- a/qiskit/circuit/classical/types/ordering.py +++ b/qiskit/circuit/classical/types/ordering.py @@ -26,7 +26,7 @@ import enum -from .types import Type, Bool, Uint +from .types import Type, Bool, Uint, Duration, Stretch # While the type system is simple, it's overkill to represent the complete partial ordering graph of @@ -55,7 +55,7 @@ def __repr__(self): return str(self) -def _order_bool_bool(_a: Bool, _b: Bool, /) -> Ordering: +def _order_identical(_a: Type, _b: Type, /) -> Ordering: return Ordering.EQUAL @@ -68,8 +68,10 @@ def _order_uint_uint(left: Uint, right: Uint, /) -> Ordering: _ORDERERS = { - (Bool, Bool): _order_bool_bool, + (Bool, Bool): _order_identical, (Uint, Uint): _order_uint_uint, + (Duration, Duration): _order_identical, + (Stretch, Stretch): _order_identical, } @@ -90,7 +92,13 @@ def order(left: Type, right: Type, /) -> Ordering: """ if (orderer := _ORDERERS.get((left.kind, right.kind))) is None: return Ordering.NONE - return orderer(left, right) + order_ = orderer(left, right) + if order_ is Ordering.EQUAL: + if left.const is True and right.const is False: + return Ordering.LESS + if right.const is True and left.const is False: + return Ordering.GREATER + return order_ def is_subtype(left: Type, right: Type, /, strict: bool = False) -> bool: @@ -213,13 +221,20 @@ def cast_kind(from_: Type, to_: Type, /) -> CastKind: >>> 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.Uint(8, const=True), types.Bool()) >>> types.cast_kind(types.Bool(), types.Uint(8)) >>> types.cast_kind(types.Uint(16), types.Uint(8)) """ + if to_.const is True and from_.const is False: + # we can't cast to a const type + return CastKind.NONE if (coercer := _ALLOWED_CASTS.get((from_.kind, to_.kind))) is None: return CastKind.NONE - return coercer(from_, to_) + cast_kind_ = coercer(from_, to_) + if cast_kind_ is CastKind.EQUAL and to_.const != from_.const: + # we need an implicit cast to drop const + return CastKind.IMPLICIT + return cast_kind_ diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py index d20e7b5fd74..8a80537a3f2 100644 --- a/qiskit/circuit/classical/types/types.py +++ b/qiskit/circuit/classical/types/types.py @@ -23,6 +23,8 @@ "Type", "Bool", "Uint", + "Duration", + "Stretch" ] import typing @@ -81,37 +83,79 @@ def __setstate__(self, state): @typing.final -class Bool(Type, metaclass=_Singleton): +class Bool(Type): """The Boolean type. This has exactly two values: ``True`` and ``False``.""" - __slots__ = () + __slots__ = ("const",) + + def __init__(self, *, const: bool = False): + super(Type, self).__setattr__("const", const) def __repr__(self): - return "Bool()" + return f"Bool(const={self.const})" def __hash__(self): - return hash(self.__class__) + return hash((self.__class__, self.const)) def __eq__(self, other): - return isinstance(other, Bool) + return isinstance(other, Bool) and self.const == other.const @typing.final class Uint(Type): """An unsigned integer of fixed bit width.""" - __slots__ = ("width",) + __slots__ = ("const", "width",) - def __init__(self, width: int): + def __init__(self, width: int, *, const: bool = False): if isinstance(width, int) and width <= 0: raise ValueError("uint width must be greater than zero") + super(Type, self).__setattr__("const", const) super(Type, self).__setattr__("width", width) def __repr__(self): - return f"Uint({self.width})" + return f"Uint({self.width}, const={self.const})" + + def __hash__(self): + return hash((self.__class__, self.const, self.width)) + + def __eq__(self, other): + return isinstance(other, Uint) and self.const == other.const and self.width == other.width + + +@typing.final +class Duration(Type): + """A length of time, possibly negative.""" + + __slots__ = ("const",) + + def __init__(self, *, const: bool = False): + super(Type, self).__setattr__("const", const) + + def __repr__(self): + return f"Duration(const={self.const})" + + def __hash__(self): + return hash((self.__class__, self.const)) + + def __eq__(self, other): + return isinstance(other, Duration) and self.const == other.const + + +@typing.final +class Stretch(Type): + """A special type that denotes some not-yet-known non-negative duration.""" + + __slots__ = ("const",) + + def __init__(self, *, const: bool = False): + super(Type, self).__setattr__("const", const) + + def __repr__(self): + return f"Stretch(const={self.const})" def __hash__(self): - return hash((self.__class__, self.width)) + return hash((self.__class__, self.const)) def __eq__(self, other): - return isinstance(other, Uint) and self.width == other.width + return isinstance(other, Stretch) and self.const == other.const diff --git a/test/python/circuit/classical/test_expr_constructors.py b/test/python/circuit/classical/test_expr_constructors.py index 6500e5593ac..c8251f3df0a 100644 --- a/test/python/circuit/classical/test_expr_constructors.py +++ b/test/python/circuit/classical/test_expr_constructors.py @@ -83,15 +83,15 @@ def test_value_lifts_qiskit_scalars(self): self.assertEqual(expr.lift(clbit), expr.Var(clbit, types.Bool())) def test_value_lifts_python_builtins(self): - self.assertEqual(expr.lift(True), expr.Value(True, types.Bool())) - self.assertEqual(expr.lift(False), expr.Value(False, types.Bool())) - self.assertEqual(expr.lift(7), expr.Value(7, types.Uint(3))) + self.assertEqual(expr.lift(True), expr.Value(True, types.Bool(const=True))) + self.assertEqual(expr.lift(False), expr.Value(False, types.Bool(const=True))) + self.assertEqual(expr.lift(7), expr.Value(7, types.Uint(3, const=True))) def test_value_ensures_nonzero_width(self): - self.assertEqual(expr.lift(0), expr.Value(0, types.Uint(1))) + self.assertEqual(expr.lift(0), expr.Value(0, types.Uint(1, const=True))) def test_value_type_representation(self): - self.assertEqual(expr.lift(5), expr.Value(5, types.Uint((5).bit_length()))) + self.assertEqual(expr.lift(5), expr.Value(5, types.Uint((5).bit_length(), const=True))) self.assertEqual(expr.lift(5, types.Uint(8)), expr.Value(5, types.Uint(8))) cr = ClassicalRegister(3, "c") @@ -165,24 +165,140 @@ def test_logic_not_explicit(self): expr.Unary(expr.Unary.Op.LOGIC_NOT, expr.Var(clbit, types.Bool()), types.Bool()), ) + # TODO: test equal like separately with bools @ddt.data( - (expr.bit_and, ClassicalRegister(3), ClassicalRegister(3)), - (expr.bit_or, ClassicalRegister(3), ClassicalRegister(3)), - (expr.bit_xor, ClassicalRegister(3), ClassicalRegister(3)), - (expr.logic_and, Clbit(), True), - (expr.logic_or, False, ClassicalRegister(3)), - (expr.equal, ClassicalRegister(8), 255), - (expr.not_equal, ClassicalRegister(8), 255), - (expr.less, ClassicalRegister(3), 6), - (expr.less_equal, ClassicalRegister(3), 5), - (expr.greater, 4, ClassicalRegister(3)), - (expr.greater_equal, ClassicalRegister(3), 5), + (expr.equal, expr.Binary.Op.EQUAL), + (expr.not_equal, expr.Binary.Op.NOT_EQUAL), + (expr.less, expr.Binary.Op.LESS), + (expr.less_equal, expr.Binary.Op.LESS_EQUAL), + (expr.greater, expr.Binary.Op.GREATER), + (expr.greater_equal, expr.Binary.Op.GREATER_EQUAL), + ) + @ddt.unpack + def test_binary_relations_lift_scalars(self, function, op): + cr_3 = ClassicalRegister(3, "c") + cr_8 = ClassicalRegister(8, "c") + self.assertEqual( + function(cr_3, cr_3), + expr.Binary( + op, + expr.Var(cr_3, types.Uint(width=3)), + expr.Var(cr_3, types.Uint(width=3)), + types.Bool(), + ) + ) + self.assertEqual( + function(cr_8, 255), + expr.Binary( + op, + expr.Var(cr_8, types.Uint(width=8)), + expr.Value(255, types.Uint(width=8)), + types.Bool(), + ) + ) + self.assertEqual( + function(cr_3, 6), + expr.Binary( + op, + expr.Var(cr_3, types.Uint(width=3)), + expr.Value(6, types.Uint(width=3)), + types.Bool(), + ) + ) + self.assertEqual( + function(255, 6), + expr.Binary( + op, + expr.Value(255, types.Uint(width=8, const=True)), + expr.Value(6, types.Uint(width=8, const=True)), + types.Bool(const=True), + ) + ) + + @ddt.data( + (expr.logic_and, expr.Binary.Op.LOGIC_AND), + (expr.logic_or, expr.Binary.Op.LOGIC_OR), + ) + @ddt.unpack + def test_binary_logical_lift_scalars(self, function, op): + cr_3 = ClassicalRegister(3, "c") + clbit = Clbit() + + self.assertEqual( + function(cr_3, cr_3), + expr.Binary( + op, + expr.Cast(expr.Var(cr_3, types.Uint(width=3)), types.Bool(), implicit=True), + expr.Cast(expr.Var(cr_3, types.Uint(width=3)), types.Bool(), implicit=True), + types.Bool(), + ) + ) + self.assertEqual( + function(clbit, True), + expr.Binary( + op, + expr.Var(clbit, types.Bool()), + expr.Value(True, types.Bool()), + types.Bool(), + ) + ) + print(f"HI: {function(cr_3, True)}") + self.assertEqual( + function(cr_3, True), + expr.Binary( + op, + expr.Cast(expr.Var(cr_3, types.Uint(width=3)), types.Bool(), implicit=True), + expr.Value(True, types.Bool()), + types.Bool(), + ) + ) + + @ddt.data( + (expr.bit_and, expr.Binary.Op.BIT_AND), + (expr.bit_or, expr.Binary.Op.BIT_OR), + (expr.bit_xor, expr.Binary.Op.BIT_XOR), ) @ddt.unpack - def test_binary_functions_lift_scalars(self, function, left, right): - self.assertEqual(function(left, right), function(expr.lift(left), right)) - self.assertEqual(function(left, right), function(left, expr.lift(right))) - self.assertEqual(function(left, right), function(expr.lift(left), expr.lift(right))) + def test_binary_bitwise_lift_scalars(self, function, op): + cr_3 = ClassicalRegister(3, "c") + cr_8 = ClassicalRegister(8, "c") + clbit = Clbit() + self.assertEqual( + function(cr_3, cr_3), + expr.Binary( + op, + expr.Var(cr_3, types.Uint(width=3)), + expr.Var(cr_3, types.Uint(width=3)), + types.Uint(width=3), + ) + ) + self.assertEqual( + function(clbit, True), + expr.Binary( + op, + expr.Var(clbit, types.Bool()), + expr.Value(True, types.Bool()), + types.Bool(), + ) + ) + self.assertEqual( + function(cr_8, 255), + expr.Binary( + op, + expr.Var(cr_8, types.Uint(width=8)), + expr.Value(255, types.Uint(width=8)), + types.Uint(width=8), + ) + ) + self.assertEqual( + function(cr_3, 6), + expr.Binary( + op, + expr.Var(cr_3, types.Uint(width=3)), + expr.Value(6, types.Uint(width=3)), + types.Uint(width=3), + ) + ) @ddt.data( (expr.bit_and, expr.Binary.Op.BIT_AND), @@ -261,9 +377,9 @@ def test_binary_bitwise_uint_inference(self, function, opcode): function(5, 255), expr.Binary( opcode, - expr.Value(5, types.Uint(8)), - expr.Value(255, types.Uint(8)), - types.Uint(8), + expr.Value(5, types.Uint(8, const=True)), + expr.Value(255, types.Uint(8, const=True)), + types.Uint(8, const=True), ), ) @@ -300,7 +416,7 @@ def test_binary_logical_explicit(self, function, opcode): expr.Binary( opcode, expr.Cast(expr.Var(cr, types.Uint(cr.size)), types.Bool(), implicit=True), - expr.Cast(expr.Value(3, types.Uint(2)), types.Bool(), implicit=True), + expr.Cast(expr.Value(3, types.Uint(2, const=True)), types.Bool(const=True), implicit=True), types.Bool(), ), ) @@ -309,7 +425,7 @@ def test_binary_logical_explicit(self, function, opcode): function(False, clbit), expr.Binary( opcode, - expr.Value(False, types.Bool()), + expr.Value(False, types.Bool(const=True)), expr.Var(clbit, types.Bool()), types.Bool(), ), @@ -402,7 +518,7 @@ def test_index_explicit(self): self.assertEqual( expr.index(cr, 3), - expr.Index(expr.Var(cr, types.Uint(4)), expr.Value(3, types.Uint(2)), types.Bool()), + expr.Index(expr.Var(cr, types.Uint(4)), expr.Value(3, types.Uint(2, const=True)), types.Bool()), ) self.assertEqual( expr.index(a, cr), @@ -423,21 +539,29 @@ def test_index_forbidden(self): def test_shift_explicit(self, function, opcode): cr = ClassicalRegister(8, "c") a = expr.Var.new("a", types.Uint(4)) - self.assertEqual( function(cr, 5), expr.Binary( - opcode, expr.Var(cr, types.Uint(8)), expr.Value(5, types.Uint(3)), types.Uint(8) + opcode, expr.Var(cr, types.Uint(8)), expr.Value(5, types.Uint(3, const=True)), types.Uint(8) ), ) self.assertEqual( function(a, cr), expr.Binary(opcode, a, expr.Var(cr, types.Uint(8)), types.Uint(4)), ) + # TODO: as it is, the LHS of a shift expression always becomes exactly the explicit + # type passed to `shift_*`. The RHS is inferred with lift, and stays const if it's + # const. Is this the best behavior? self.assertEqual( function(3, 5, types.Uint(8)), expr.Binary( - opcode, expr.Value(3, types.Uint(8)), expr.Value(5, types.Uint(3)), types.Uint(8) + opcode, expr.Value(3, types.Uint(8)), expr.Value(5, types.Uint(3, const=True)), types.Uint(8) + ), + ) + self.assertEqual( + function(3, 5, types.Uint(8, const=True)), + expr.Binary( + opcode, expr.Value(3, types.Uint(8, const=True)), expr.Value(5, types.Uint(3, const=True)), types.Uint(8, const=True) ), ) From e423bf9f2d47cd5dcd37560fb74d0148450afd1a Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Fri, 7 Feb 2025 12:14:08 -0500 Subject: [PATCH 02/53] Add try_const to lift. --- qiskit/circuit/classical/expr/constructors.py | 29 ++++++++----------- qiskit/circuit/classical/types/ordering.py | 4 +-- qiskit/circuit/classical/types/types.py | 5 ++++ .../classical/test_expr_constructors.py | 15 ++++++++-- 4 files changed, 30 insertions(+), 23 deletions(-) diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index 91013ef115f..daaf6209496 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -91,7 +91,7 @@ def lift_legacy_condition( return Binary(Binary.Op.EQUAL, left, right, types.Bool()) -def lift(value: typing.Any, /, type: types.Type | None = None) -> Expr: +def lift(value: typing.Any, /, type: types.Type | None = None, *, try_const: bool = True) -> Expr: """Lift the given Python ``value`` to a :class:`~.expr.Value` or :class:`~.expr.Var`. If an explicit ``type`` is given, the typing in the output will reflect that. @@ -125,7 +125,7 @@ def lift(value: typing.Any, /, type: types.Type | None = None) -> Expr: inferred: types.Type if value is True or value is False: - inferred = types.Bool(const=True) + inferred = types.Bool(const=try_const) constructor = Value elif isinstance(value, Clbit): inferred = types.Bool() @@ -136,7 +136,7 @@ def lift(value: typing.Any, /, type: types.Type | None = None) -> Expr: elif isinstance(value, int): if value < 0: raise ValueError("cannot represent a negative value") - inferred = types.Uint(width=value.bit_length() or 1, const=True) + inferred = types.Uint(width=value.bit_length() or 1, const=try_const) constructor = Value else: raise TypeError(f"failed to infer a type for '{value}'") @@ -223,24 +223,19 @@ def _lift_binary_operands(left: typing.Any, right: typing.Any) -> tuple[Expr, Ex right_int = isinstance(right, int) and not right_bool if not (left_int or right_int): if left_bool == right_bool: - # If they're both bool, lifting them will produce const Bool. - # If neither are bool, they're a mix of bits/registers (which are always - # non-const) and Expr, which we can't modify the const-ness of without - # a cast node. - left = lift(left) - right = lift(right) + # If they're both bool, they'll lift as const here. + # If neither are, we've already checked for int, so they must be bits, + # registers, or expressions, none of which we can't lift to be const. + left = lift(left, try_const=True) + right = lift(right, try_const=True) elif not right_bool: - # Left is a bool + # Left is a bool, which should only be const if right is const. right = lift(right) - # TODO: if right.type isn't Bool, there's a type mismatch so we _should_ - # raise here. But, _binary_bitwise will error for us with a better msg. - left = lift(left, right.type if right.type.kind is types.Bool else None) + left = lift(left, try_const=right.type.const) elif not left_bool: - # Right is a bool. + # Right is a bool, which should only be const if left is const. left = lift(left) - # TODO: if left.type isn't Bool, there's a type mismatch so we _should_ - # raise here. But, _binary_bitwise will error for us with a better msg. - right = lift(right, left.type if left.type.kind is types.Bool else None) + right = lift(right, try_const=left.type.const) elif not right_int: # Left is an int. right = lift(right) diff --git a/qiskit/circuit/classical/types/ordering.py b/qiskit/circuit/classical/types/ordering.py index f0ed723d992..730fdba573c 100644 --- a/qiskit/circuit/classical/types/ordering.py +++ b/qiskit/circuit/classical/types/ordering.py @@ -26,7 +26,7 @@ import enum -from .types import Type, Bool, Uint, Duration, Stretch +from .types import Type, Bool, Uint # While the type system is simple, it's overkill to represent the complete partial ordering graph of @@ -70,8 +70,6 @@ def _order_uint_uint(left: Uint, right: Uint, /) -> Ordering: _ORDERERS = { (Bool, Bool): _order_identical, (Uint, Uint): _order_uint_uint, - (Duration, Duration): _order_identical, - (Stretch, Stretch): _order_identical, } diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py index 8a80537a3f2..34731c1ffb6 100644 --- a/qiskit/circuit/classical/types/types.py +++ b/qiskit/circuit/classical/types/types.py @@ -64,6 +64,11 @@ def kind(self): this a hashable enum-like discriminator you can rely on.""" return self.__class__ + @property + def const(self): + """Get the const-ness of this type.""" + raise NotImplementedError("types must implement the 'const' attribute") + # Enforcement of immutability. The constructor methods need to manually skip this. def __setattr__(self, _key, _value): diff --git a/test/python/circuit/classical/test_expr_constructors.py b/test/python/circuit/classical/test_expr_constructors.py index c8251f3df0a..3f33a8a8105 100644 --- a/test/python/circuit/classical/test_expr_constructors.py +++ b/test/python/circuit/classical/test_expr_constructors.py @@ -242,7 +242,6 @@ def test_binary_logical_lift_scalars(self, function, op): types.Bool(), ) ) - print(f"HI: {function(cr_3, True)}") self.assertEqual( function(cr_3, True), expr.Binary( @@ -416,7 +415,7 @@ def test_binary_logical_explicit(self, function, opcode): expr.Binary( opcode, expr.Cast(expr.Var(cr, types.Uint(cr.size)), types.Bool(), implicit=True), - expr.Cast(expr.Value(3, types.Uint(2, const=True)), types.Bool(const=True), implicit=True), + expr.Cast(expr.Value(3, types.Uint(cr.size)), types.Bool(), implicit=True), types.Bool(), ), ) @@ -425,12 +424,22 @@ def test_binary_logical_explicit(self, function, opcode): function(False, clbit), expr.Binary( opcode, - expr.Value(False, types.Bool(const=True)), + expr.Value(False, types.Bool()), expr.Var(clbit, types.Bool()), types.Bool(), ), ) + self.assertEqual( + function(False, 3), + expr.Binary( + opcode, + expr.Value(False, types.Bool(const=True)), + expr.Cast(expr.Value(3, types.Uint(2, const=True)), types.Bool(const=True), implicit=True), + types.Bool(const=True) + ) + ) + @ddt.data( (expr.equal, expr.Binary.Op.EQUAL), (expr.not_equal, expr.Binary.Op.NOT_EQUAL), From 0a5917b3aeb8b6a436c7ca58948a91c97d224cfb Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Fri, 7 Feb 2025 12:15:18 -0500 Subject: [PATCH 03/53] Try multiple singletons, new one for const. --- qiskit/circuit/classical/types/types.py | 20 ++++++++++++------- .../circuit/classical/test_expr_properties.py | 15 ++++++++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py index 34731c1ffb6..a05a7b06c36 100644 --- a/qiskit/circuit/classical/types/types.py +++ b/qiskit/circuit/classical/types/types.py @@ -31,11 +31,11 @@ class _Singleton(type): - """Metaclass to make the child, which should take zero initialization arguments, a singleton + """Metaclass to make the child, which should take a single "const" argument, a singleton object.""" - def _get_singleton_instance(cls): - return cls._INSTANCE + def _get_singleton_instance(cls, const=False): + return cls._CONST_INSTANCE if const else cls._INSTANCE @classmethod def __prepare__(mcs, name, bases): # pylint: disable=unused-argument @@ -45,6 +45,7 @@ def __prepare__(mcs, name, bases): # pylint: disable=unused-argument def __new__(cls, name, bases, namespace): out = super().__new__(cls, name, bases, namespace) out._INSTANCE = object.__new__(out) # pylint: disable=invalid-name + out._CONST_INSTANCE = object.__new__(out) # pylint: disable=invalid-name return out @@ -88,13 +89,18 @@ def __setstate__(self, state): @typing.final -class Bool(Type): +class Bool(Type, metaclass=_Singleton): """The Boolean type. This has exactly two values: ``True`` and ``False``.""" - __slots__ = ("const",) + __slots__ = () - def __init__(self, *, const: bool = False): - super(Type, self).__setattr__("const", const) + def __new__(cls, *, const=False): + return cls._get_singleton_instance(const) + + @property + def const(self): + # Check if this instance is the const singleton. + return self is self.__class__._CONST_INSTANCE def __repr__(self): return f"Bool(const={self.const})" diff --git a/test/python/circuit/classical/test_expr_properties.py b/test/python/circuit/classical/test_expr_properties.py index 625db22cc12..39bf7874932 100644 --- a/test/python/circuit/classical/test_expr_properties.py +++ b/test/python/circuit/classical/test_expr_properties.py @@ -33,6 +33,21 @@ def test_bool_type_is_singleton(self): self.assertIs(types.Bool(), copy.deepcopy(types.Bool())) self.assertIs(types.Bool(), pickle.loads(pickle.dumps(types.Bool()))) + def test_const_bool_type_is_singleton(self): + """The `Bool` type is meant (and used) as a Python singleton object for efficiency. It must + always be referentially equal to all other references to it.""" + self.assertIs(types.Bool(const=True), types.Bool(const=True)) + self.assertIs(types.Bool(const=True), copy.copy(types.Bool(const=True))) + self.assertIs(types.Bool(const=True), copy.deepcopy(types.Bool(const=True))) + self.assertIs(types.Bool(const=True), pickle.loads(pickle.dumps(types.Bool(const=True)))) + + def test_bool_type_singleton_is_not_const_bool_type_singleton(self): + """The const `Bool` and non-const `Bool` should not share the same singleton.""" + self.assertIsNot(types.Bool(const=True), types.Bool()) + self.assertIsNot(types.Bool(const=True), copy.copy(types.Bool())) + self.assertIsNot(types.Bool(const=True), copy.deepcopy(types.Bool())) + self.assertIsNot(types.Bool(const=True), pickle.loads(pickle.dumps(types.Bool()))) + @ddt.data(types.Bool(), types.Uint(8)) def test_types_can_be_cloned(self, obj): """Test that various ways of cloning a `Type` object are valid and produce equal output.""" From a97434d1763a3147ff6ff4586ff96d2868c6a54e Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Fri, 7 Feb 2025 12:15:23 -0500 Subject: [PATCH 04/53] Revert "Try multiple singletons, new one for const." This reverts commit e2b32212533a52b30fecb700ed36a7f9e93a19a6. --- qiskit/circuit/classical/types/types.py | 20 +++++++------------ .../circuit/classical/test_expr_properties.py | 15 -------------- 2 files changed, 7 insertions(+), 28 deletions(-) diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py index a05a7b06c36..34731c1ffb6 100644 --- a/qiskit/circuit/classical/types/types.py +++ b/qiskit/circuit/classical/types/types.py @@ -31,11 +31,11 @@ class _Singleton(type): - """Metaclass to make the child, which should take a single "const" argument, a singleton + """Metaclass to make the child, which should take zero initialization arguments, a singleton object.""" - def _get_singleton_instance(cls, const=False): - return cls._CONST_INSTANCE if const else cls._INSTANCE + def _get_singleton_instance(cls): + return cls._INSTANCE @classmethod def __prepare__(mcs, name, bases): # pylint: disable=unused-argument @@ -45,7 +45,6 @@ def __prepare__(mcs, name, bases): # pylint: disable=unused-argument def __new__(cls, name, bases, namespace): out = super().__new__(cls, name, bases, namespace) out._INSTANCE = object.__new__(out) # pylint: disable=invalid-name - out._CONST_INSTANCE = object.__new__(out) # pylint: disable=invalid-name return out @@ -89,18 +88,13 @@ def __setstate__(self, state): @typing.final -class Bool(Type, metaclass=_Singleton): +class Bool(Type): """The Boolean type. This has exactly two values: ``True`` and ``False``.""" - __slots__ = () - - def __new__(cls, *, const=False): - return cls._get_singleton_instance(const) + __slots__ = ("const",) - @property - def const(self): - # Check if this instance is the const singleton. - return self is self.__class__._CONST_INSTANCE + def __init__(self, *, const: bool = False): + super(Type, self).__setattr__("const", const) def __repr__(self): return f"Bool(const={self.const})" diff --git a/test/python/circuit/classical/test_expr_properties.py b/test/python/circuit/classical/test_expr_properties.py index 39bf7874932..625db22cc12 100644 --- a/test/python/circuit/classical/test_expr_properties.py +++ b/test/python/circuit/classical/test_expr_properties.py @@ -33,21 +33,6 @@ def test_bool_type_is_singleton(self): self.assertIs(types.Bool(), copy.deepcopy(types.Bool())) self.assertIs(types.Bool(), pickle.loads(pickle.dumps(types.Bool()))) - def test_const_bool_type_is_singleton(self): - """The `Bool` type is meant (and used) as a Python singleton object for efficiency. It must - always be referentially equal to all other references to it.""" - self.assertIs(types.Bool(const=True), types.Bool(const=True)) - self.assertIs(types.Bool(const=True), copy.copy(types.Bool(const=True))) - self.assertIs(types.Bool(const=True), copy.deepcopy(types.Bool(const=True))) - self.assertIs(types.Bool(const=True), pickle.loads(pickle.dumps(types.Bool(const=True)))) - - def test_bool_type_singleton_is_not_const_bool_type_singleton(self): - """The const `Bool` and non-const `Bool` should not share the same singleton.""" - self.assertIsNot(types.Bool(const=True), types.Bool()) - self.assertIsNot(types.Bool(const=True), copy.copy(types.Bool())) - self.assertIsNot(types.Bool(const=True), copy.deepcopy(types.Bool())) - self.assertIsNot(types.Bool(const=True), pickle.loads(pickle.dumps(types.Bool()))) - @ddt.data(types.Bool(), types.Uint(8)) def test_types_can_be_cloned(self, obj): """Test that various ways of cloning a `Type` object are valid and produce equal output.""" From 1afc9654fb77f10cb66f1fa8458649575fc3b654 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Fri, 7 Feb 2025 12:15:55 -0500 Subject: [PATCH 05/53] Remove Bool singleton test. --- test/python/circuit/classical/test_expr_properties.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/python/circuit/classical/test_expr_properties.py b/test/python/circuit/classical/test_expr_properties.py index 625db22cc12..60a4b1f9080 100644 --- a/test/python/circuit/classical/test_expr_properties.py +++ b/test/python/circuit/classical/test_expr_properties.py @@ -25,14 +25,6 @@ @ddt.ddt class TestExprProperties(QiskitTestCase): - def test_bool_type_is_singleton(self): - """The `Bool` type is meant (and used) as a Python singleton object for efficiency. It must - always be referentially equal to all other references to it.""" - self.assertIs(types.Bool(), types.Bool()) - self.assertIs(types.Bool(), copy.copy(types.Bool())) - self.assertIs(types.Bool(), copy.deepcopy(types.Bool())) - self.assertIs(types.Bool(), pickle.loads(pickle.dumps(types.Bool()))) - @ddt.data(types.Bool(), types.Uint(8)) def test_types_can_be_cloned(self, obj): """Test that various ways of cloning a `Type` object are valid and produce equal output.""" From 86655f1ec19acff1bce7825956cc337c234fe7cd Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Fri, 7 Feb 2025 16:12:39 -0500 Subject: [PATCH 06/53] Add const handling for stores, fix test bugs. --- qiskit/circuit/quantumcircuit.py | 6 +-- .../circuit/test_circuit_load_from_qpy.py | 2 +- test/python/circuit/test_circuit_vars.py | 26 ++++++------- test/python/circuit/test_control_flow.py | 2 +- .../circuit/test_control_flow_builders.py | 6 +-- test/python/circuit/test_store.py | 39 +++++++++++++++---- 6 files changed, 53 insertions(+), 28 deletions(-) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 7ab1bae5d8c..06e3ec38bfc 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -2911,7 +2911,7 @@ def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.V coerce_type = name_or_var.type else: coerce_type = None - initial = _validate_expr(circuit_scope, expr.lift(initial, coerce_type)) + initial = _validate_expr(circuit_scope, expr.lift(initial, coerce_type, try_const=False)) if isinstance(name_or_var, str): var = expr.Var.new(name_or_var, initial.type) elif not name_or_var.standalone: @@ -3836,11 +3836,11 @@ def store(self, lvalue: typing.Any, rvalue: typing.Any, /) -> InstructionSet: Create a new variable in the circuit that can be written to with this method. """ # As a convenience, lift integer-literal rvalues to the matching width. - lvalue = expr.lift(lvalue) + lvalue = expr.lift(lvalue, try_const=False) rvalue_type = ( lvalue.type if isinstance(rvalue, int) and not isinstance(rvalue, bool) else None ) - rvalue = expr.lift(rvalue, rvalue_type) + rvalue = expr.lift(rvalue, rvalue_type, try_const=False) return self.append(Store(lvalue, rvalue), (), (), copy=False) def measure(self, qubit: QubitSpecifier, cbit: ClbitSpecifier) -> InstructionSet: diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 962dd22ac79..2dcbdcbf1d8 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -1802,7 +1802,7 @@ def test_load_empty_vars(self): a = expr.Var.new("a", types.Bool()) b = expr.Var.new("b", types.Uint(8)) all_vars = { - a: expr.lift(False), + a: expr.lift(False, try_const=False), b: expr.lift(3, type=b.type), expr.Var.new("ΞΈΟˆΟ†", types.Bool()): expr.logic_not(a), expr.Var.new("🐍🐍🐍", types.Uint(8)): expr.bit_and(b, b), diff --git a/test/python/circuit/test_circuit_vars.py b/test/python/circuit/test_circuit_vars.py index f6916dcb72d..f910e4c6a8e 100644 --- a/test/python/circuit/test_circuit_vars.py +++ b/test/python/circuit/test_circuit_vars.py @@ -42,8 +42,8 @@ def test_initialise_captures(self): 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)), + (expr.Var.new("a", types.Bool()), expr.lift(True, try_const=False)), + (expr.Var.new("b", types.Uint(16)), expr.lift(0xFFFF, try_const=False)), ] qc = QuantumCircuit(declarations=vars_) @@ -61,8 +61,8 @@ def test_initialise_declarations_iterable(self): 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), + expr.Var.new("a", types.Bool()): expr.lift(True, try_const=False), + expr.Var.new("b", types.Uint(16)): expr.lift(0xFFFF, try_const=False), } qc = QuantumCircuit(declarations=vars_) @@ -80,7 +80,7 @@ def test_initialise_declarations_dependencies(self): them, provided they're specified in a suitable order.""" a = expr.Var.new("a", types.Bool()) vars_ = [ - (a, expr.lift(True)), + (a, expr.lift(True, try_const=False)), (expr.Var.new("b", types.Bool()), a), ] qc = QuantumCircuit(declarations=vars_) @@ -139,7 +139,7 @@ def test_add_uninitialized_var(self): def test_add_var_returns_good_var(self): qc = QuantumCircuit() - a = qc.add_var("a", expr.lift(True)) + a = qc.add_var("a", expr.lift(True, try_const=False)) self.assertEqual(a.name, "a") self.assertEqual(a.type, types.Bool()) @@ -223,9 +223,9 @@ def test_initialise_captures_equal_to_add_capture(self): def test_initialise_declarations_equal_to_add_var(self): a = expr.Var.new("a", types.Bool()) - a_init = expr.lift(False) + a_init = expr.lift(False, try_const=False) b = expr.Var.new("b", types.Uint(16)) - b_init = expr.lift(0xFFFF) + b_init = expr.lift(0xFFFF, try_const=False) qc_init = QuantumCircuit(declarations=[(a, a_init), (b, b_init)]) qc_manual = QuantumCircuit() @@ -287,8 +287,8 @@ def test_cannot_shadow_names(self): 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) + a_bool_init = expr.lift(True, try_const=False) + a_uint_init = expr.lift(0xFFFF, try_const=False) tests = [ ((a_bool1, a_bool_init), (a_bool2, a_bool_init)), @@ -325,11 +325,11 @@ def test_cannot_shadow_names(self): qc.add_var(right, right_init) qc = QuantumCircuit() - qc.add_var("a", expr.lift(True)) + qc.add_var("a", expr.lift(True, try_const=False)) with self.assertRaisesRegex(CircuitError, "its name shadows"): - qc.add_var("a", expr.lift(True)) + qc.add_var("a", expr.lift(True, try_const=False)) with self.assertRaisesRegex(CircuitError, "its name shadows"): - qc.add_var("a", expr.lift(0xFF)) + qc.add_var("a", expr.lift(0xFF, try_const=False)) def test_cannot_add_vars_wrapping_clbits(self): a = expr.Var(Clbit(), types.Bool()) diff --git a/test/python/circuit/test_control_flow.py b/test/python/circuit/test_control_flow.py index 51733a37ffc..2ef8b1a3e78 100644 --- a/test/python/circuit/test_control_flow.py +++ b/test/python/circuit/test_control_flow.py @@ -996,7 +996,7 @@ def test_can_add_op_with_captures_of_captures(self): def test_can_add_op_with_captures_of_locals(self): """Test circuit methods can capture declared variables.""" outer = QuantumCircuit(1, 1) - a = outer.add_var("a", expr.lift(True)) + a = outer.add_var("a", expr.lift(True, try_const=False)) inner = QuantumCircuit(1, 1, captures=[a]) diff --git a/test/python/circuit/test_control_flow_builders.py b/test/python/circuit/test_control_flow_builders.py index 3b699a16898..cf56a7208de 100644 --- a/test/python/circuit/test_control_flow_builders.py +++ b/test/python/circuit/test_control_flow_builders.py @@ -4192,15 +4192,15 @@ def test_exception_during_initialisation_does_not_add_variable(self): with self.assertRaises(CircuitError): Store(uint_var, bool_expr) base = QuantumCircuit() - with base.while_loop(expr.lift(False)): + with base.while_loop(expr.lift(False, try_const=False)): # Should succeed. - b = base.add_var("b", expr.lift(False)) + b = base.add_var("b", expr.lift(False, try_const=False)) try: base.add_var(uint_var, bool_expr) except CircuitError: pass # Should succeed. - c = base.add_var("c", expr.lift(False)) + c = base.add_var("c", expr.lift(False, try_const=False)) local_vars = set(base.iter_vars()) self.assertEqual(local_vars, {b, c}) diff --git a/test/python/circuit/test_store.py b/test/python/circuit/test_store.py index ecb98681bbd..6ccc06ec1c6 100644 --- a/test/python/circuit/test_store.py +++ b/test/python/circuit/test_store.py @@ -31,7 +31,7 @@ def test_happy_path_construction(self): def test_store_to_index(self): lvalue = expr.index(expr.Var.new("a", types.Uint(8)), 3) - rvalue = expr.lift(False) + rvalue = expr.lift(False, try_const=False) constructed = Store(lvalue, rvalue) self.assertIsInstance(constructed, Store) self.assertEqual(constructed.lvalue, lvalue) @@ -45,6 +45,14 @@ def test_implicit_cast(self): self.assertEqual(constructed.lvalue, lvalue) self.assertEqual(constructed.rvalue, expr.Cast(rvalue, types.Bool(), implicit=True)) + def test_implicit_const_cast(self): + lvalue = expr.Var.new("a", types.Bool()) + rvalue = expr.Value("b", types.Bool(const=True)) + 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()) @@ -107,7 +115,7 @@ def test_allows_stores_with_clbits(self): qc.store(a, expr.lift(clbits[1])) expected = [ - Store(expr.lift(clbits[0]), expr.lift(True)), + Store(expr.lift(clbits[0]), expr.lift(True, try_const=False)), Store(expr.lift(clbits[1]), a), Store(expr.lift(clbits[0]), expr.lift(clbits[1])), Store(expr.lift(clbits[0]), expr.lift(clbits[1])), @@ -127,7 +135,7 @@ def test_allows_stores_with_cregs(self): qc.store(a, cregs[1]) expected = [ - Store(expr.lift(cregs[0]), expr.lift(0xFF)), + Store(expr.lift(cregs[0]), expr.lift(0xFF, try_const=False)), Store(expr.lift(cregs[1]), a), Store(expr.lift(cregs[0]), expr.lift(cregs[1])), Store(expr.lift(cregs[0]), expr.lift(cregs[1])), @@ -144,8 +152,8 @@ def test_allows_stores_with_index(self): qc.store(expr.index(a, 3), True) qc.store(expr.index(cr, a), expr.index(cr, 0)) expected = [ - Store(expr.index(cr, 0), expr.lift(False)), - Store(expr.index(a, 3), expr.lift(True)), + Store(expr.index(cr, 0), expr.lift(False, try_const=False)), + Store(expr.index(a, 3), expr.lift(True, try_const=False)), Store(expr.index(cr, a), expr.index(cr, 0)), ] actual = [instruction.operation for instruction in qc.data] @@ -155,12 +163,29 @@ 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))) + self.assertEqual(qc.data[-1].operation, Store(a, expr.lift(True, try_const=False))) 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))) + self.assertEqual(qc.data[-1].operation, Store(b, expr.lift(0xFFFF, try_const=False))) + + def test_implicit_const_cast(self): + a = expr.Var.new("a", types.Bool()) + qc = QuantumCircuit(captures=[a]) + qc.store(a, expr.lift(False)) + self.assertEqual( + qc.data[-1].operation, + Store(a, expr.Cast(expr.Value(False, types.Bool(const=True)), types.Bool(), implicit=True)) + ) + + b = expr.Var.new("b", types.Uint(16)) + qc.add_capture(b) + qc.store(b, expr.lift(0xFFFF)) + self.assertEqual( + qc.data[-1].operation, + Store(b, expr.Cast(expr.Value(0xFFFF, types.Uint(width=16, const=True)), types.Uint(width=16), implicit=True)) + ) def test_lifts_integer_literals_to_full_width(self): a = expr.Var.new("a", types.Uint(8)) From aaeae9b8cd4506667c5c239544f79ba7825411c4 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Fri, 7 Feb 2025 16:13:46 -0500 Subject: [PATCH 07/53] Fix formatting. --- qiskit/circuit/classical/expr/constructors.py | 21 ++++++-- qiskit/circuit/classical/types/types.py | 13 ++--- .../classical/test_expr_constructors.py | 49 ++++++++++++------- test/python/circuit/test_store.py | 13 ++++- 4 files changed, 65 insertions(+), 31 deletions(-) diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index daaf6209496..021ec61c414 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -384,7 +384,12 @@ def _equal_like(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: if left.type.kind is not right.type.kind: raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") type = types.greater(left.type, right.type) - return Binary(op, _coerce_lossless(left, type), _coerce_lossless(right, type), types.Bool(const=type.const)) + return Binary( + op, + _coerce_lossless(left, type), + _coerce_lossless(right, type), + types.Bool(const=type.const), + ) def equal(left: typing.Any, right: typing.Any, /) -> Expr: @@ -428,7 +433,12 @@ def _binary_relation(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr if left.type.kind is not right.type.kind or left.type.kind is types.Bool: raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") type = types.greater(left.type, right.type) - return Binary(op, _coerce_lossless(left, type), _coerce_lossless(right, type), types.Bool(const=type.const)) + return Binary( + op, + _coerce_lossless(left, type), + _coerce_lossless(right, type), + types.Bool(const=type.const), + ) def less(left: typing.Any, right: typing.Any, /) -> Expr: @@ -515,7 +525,12 @@ def _shift_like( right = lift(right) if left.type.kind != types.Uint or right.type.kind != types.Uint: raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") - return Binary(op, left, right, types.Uint(width=left.type.width, const=(left.type.const and right.type.const))) + return Binary( + op, + left, + right, + types.Uint(width=left.type.width, const=(left.type.const and right.type.const)), + ) def shift_left(left: typing.Any, right: typing.Any, /, type: types.Type | None = None) -> Expr: diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py index 34731c1ffb6..53654be6626 100644 --- a/qiskit/circuit/classical/types/types.py +++ b/qiskit/circuit/classical/types/types.py @@ -19,13 +19,7 @@ from __future__ import annotations -__all__ = [ - "Type", - "Bool", - "Uint", - "Duration", - "Stretch" -] +__all__ = ["Type", "Bool", "Uint", "Duration", "Stretch"] import typing @@ -110,7 +104,10 @@ def __eq__(self, other): class Uint(Type): """An unsigned integer of fixed bit width.""" - __slots__ = ("const", "width",) + __slots__ = ( + "const", + "width", + ) def __init__(self, width: int, *, const: bool = False): if isinstance(width, int) and width <= 0: diff --git a/test/python/circuit/classical/test_expr_constructors.py b/test/python/circuit/classical/test_expr_constructors.py index 3f33a8a8105..0463c4b7ecc 100644 --- a/test/python/circuit/classical/test_expr_constructors.py +++ b/test/python/circuit/classical/test_expr_constructors.py @@ -185,7 +185,7 @@ def test_binary_relations_lift_scalars(self, function, op): expr.Var(cr_3, types.Uint(width=3)), expr.Var(cr_3, types.Uint(width=3)), types.Bool(), - ) + ), ) self.assertEqual( function(cr_8, 255), @@ -194,7 +194,7 @@ def test_binary_relations_lift_scalars(self, function, op): expr.Var(cr_8, types.Uint(width=8)), expr.Value(255, types.Uint(width=8)), types.Bool(), - ) + ), ) self.assertEqual( function(cr_3, 6), @@ -203,7 +203,7 @@ def test_binary_relations_lift_scalars(self, function, op): expr.Var(cr_3, types.Uint(width=3)), expr.Value(6, types.Uint(width=3)), types.Bool(), - ) + ), ) self.assertEqual( function(255, 6), @@ -212,7 +212,7 @@ def test_binary_relations_lift_scalars(self, function, op): expr.Value(255, types.Uint(width=8, const=True)), expr.Value(6, types.Uint(width=8, const=True)), types.Bool(const=True), - ) + ), ) @ddt.data( @@ -231,7 +231,7 @@ def test_binary_logical_lift_scalars(self, function, op): expr.Cast(expr.Var(cr_3, types.Uint(width=3)), types.Bool(), implicit=True), expr.Cast(expr.Var(cr_3, types.Uint(width=3)), types.Bool(), implicit=True), types.Bool(), - ) + ), ) self.assertEqual( function(clbit, True), @@ -240,7 +240,7 @@ def test_binary_logical_lift_scalars(self, function, op): expr.Var(clbit, types.Bool()), expr.Value(True, types.Bool()), types.Bool(), - ) + ), ) self.assertEqual( function(cr_3, True), @@ -249,7 +249,7 @@ def test_binary_logical_lift_scalars(self, function, op): expr.Cast(expr.Var(cr_3, types.Uint(width=3)), types.Bool(), implicit=True), expr.Value(True, types.Bool()), types.Bool(), - ) + ), ) @ddt.data( @@ -269,7 +269,7 @@ def test_binary_bitwise_lift_scalars(self, function, op): expr.Var(cr_3, types.Uint(width=3)), expr.Var(cr_3, types.Uint(width=3)), types.Uint(width=3), - ) + ), ) self.assertEqual( function(clbit, True), @@ -278,7 +278,7 @@ def test_binary_bitwise_lift_scalars(self, function, op): expr.Var(clbit, types.Bool()), expr.Value(True, types.Bool()), types.Bool(), - ) + ), ) self.assertEqual( function(cr_8, 255), @@ -287,7 +287,7 @@ def test_binary_bitwise_lift_scalars(self, function, op): expr.Var(cr_8, types.Uint(width=8)), expr.Value(255, types.Uint(width=8)), types.Uint(width=8), - ) + ), ) self.assertEqual( function(cr_3, 6), @@ -296,7 +296,7 @@ def test_binary_bitwise_lift_scalars(self, function, op): expr.Var(cr_3, types.Uint(width=3)), expr.Value(6, types.Uint(width=3)), types.Uint(width=3), - ) + ), ) @ddt.data( @@ -435,9 +435,11 @@ def test_binary_logical_explicit(self, function, opcode): expr.Binary( opcode, expr.Value(False, types.Bool(const=True)), - expr.Cast(expr.Value(3, types.Uint(2, const=True)), types.Bool(const=True), implicit=True), - types.Bool(const=True) - ) + expr.Cast( + expr.Value(3, types.Uint(2, const=True)), types.Bool(const=True), implicit=True + ), + types.Bool(const=True), + ), ) @ddt.data( @@ -527,7 +529,9 @@ def test_index_explicit(self): self.assertEqual( expr.index(cr, 3), - expr.Index(expr.Var(cr, types.Uint(4)), expr.Value(3, types.Uint(2, const=True)), types.Bool()), + expr.Index( + expr.Var(cr, types.Uint(4)), expr.Value(3, types.Uint(2, const=True)), types.Bool() + ), ) self.assertEqual( expr.index(a, cr), @@ -551,7 +555,10 @@ def test_shift_explicit(self, function, opcode): self.assertEqual( function(cr, 5), expr.Binary( - opcode, expr.Var(cr, types.Uint(8)), expr.Value(5, types.Uint(3, const=True)), types.Uint(8) + opcode, + expr.Var(cr, types.Uint(8)), + expr.Value(5, types.Uint(3, const=True)), + types.Uint(8), ), ) self.assertEqual( @@ -564,13 +571,19 @@ def test_shift_explicit(self, function, opcode): self.assertEqual( function(3, 5, types.Uint(8)), expr.Binary( - opcode, expr.Value(3, types.Uint(8)), expr.Value(5, types.Uint(3, const=True)), types.Uint(8) + opcode, + expr.Value(3, types.Uint(8)), + expr.Value(5, types.Uint(3, const=True)), + types.Uint(8), ), ) self.assertEqual( function(3, 5, types.Uint(8, const=True)), expr.Binary( - opcode, expr.Value(3, types.Uint(8, const=True)), expr.Value(5, types.Uint(3, const=True)), types.Uint(8, const=True) + opcode, + expr.Value(3, types.Uint(8, const=True)), + expr.Value(5, types.Uint(3, const=True)), + types.Uint(8, const=True), ), ) diff --git a/test/python/circuit/test_store.py b/test/python/circuit/test_store.py index 6ccc06ec1c6..ec60b87e644 100644 --- a/test/python/circuit/test_store.py +++ b/test/python/circuit/test_store.py @@ -176,7 +176,9 @@ def test_implicit_const_cast(self): qc.store(a, expr.lift(False)) self.assertEqual( qc.data[-1].operation, - Store(a, expr.Cast(expr.Value(False, types.Bool(const=True)), types.Bool(), implicit=True)) + Store( + a, expr.Cast(expr.Value(False, types.Bool(const=True)), types.Bool(), implicit=True) + ), ) b = expr.Var.new("b", types.Uint(16)) @@ -184,7 +186,14 @@ def test_implicit_const_cast(self): qc.store(b, expr.lift(0xFFFF)) self.assertEqual( qc.data[-1].operation, - Store(b, expr.Cast(expr.Value(0xFFFF, types.Uint(width=16, const=True)), types.Uint(width=16), implicit=True)) + Store( + b, + expr.Cast( + expr.Value(0xFFFF, types.Uint(width=16, const=True)), + types.Uint(width=16), + implicit=True, + ), + ), ) def test_lifts_integer_literals_to_full_width(self): From a2a444b4840e1c9d6185f4f18d9aa03e762bdb16 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Fri, 7 Feb 2025 16:18:37 -0500 Subject: [PATCH 08/53] Remove Duration and Stretch for now. --- qiskit/circuit/classical/types/types.py | 40 +------------------------ 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py index 53654be6626..9aafe5f1f09 100644 --- a/qiskit/circuit/classical/types/types.py +++ b/qiskit/circuit/classical/types/types.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__ = ["Type", "Bool", "Uint", "Duration", "Stretch"] +__all__ = ["Type", "Bool", "Uint"] import typing @@ -123,41 +123,3 @@ def __hash__(self): def __eq__(self, other): return isinstance(other, Uint) and self.const == other.const and self.width == other.width - - -@typing.final -class Duration(Type): - """A length of time, possibly negative.""" - - __slots__ = ("const",) - - def __init__(self, *, const: bool = False): - super(Type, self).__setattr__("const", const) - - def __repr__(self): - return f"Duration(const={self.const})" - - def __hash__(self): - return hash((self.__class__, self.const)) - - def __eq__(self, other): - return isinstance(other, Duration) and self.const == other.const - - -@typing.final -class Stretch(Type): - """A special type that denotes some not-yet-known non-negative duration.""" - - __slots__ = ("const",) - - def __init__(self, *, const: bool = False): - super(Type, self).__setattr__("const", const) - - def __repr__(self): - return f"Stretch(const={self.const})" - - def __hash__(self): - return hash((self.__class__, self.const)) - - def __eq__(self, other): - return isinstance(other, Stretch) and self.const == other.const From 8ac2dc343935ea36553b5dc6ef48a3aea68b1995 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Fri, 7 Feb 2025 16:54:32 -0500 Subject: [PATCH 09/53] Cleanup, fix const bug in index. --- qiskit/circuit/classical/expr/constructors.py | 127 ++++++++++-------- qiskit/circuit/classical/expr/expr.py | 4 +- qiskit/circuit/classical/expr/visitors.py | 2 + qiskit/circuit/classical/types/ordering.py | 22 ++- qiskit/circuit/classical/types/types.py | 18 --- 5 files changed, 95 insertions(+), 78 deletions(-) diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index 021ec61c414..7d26ecd063a 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -96,15 +96,18 @@ def lift(value: typing.Any, /, type: types.Type | None = None, *, try_const: boo If an explicit ``type`` is given, the typing in the output will reflect that. + By default, lifted scalars will be const if they aren't backed by a classical resource. + To lift scalars to a non-const-typed expression, specify `try_const` as `False`. + Examples: Lifting simple circuit objects to be :class:`~.expr.Var` instances:: >>> from qiskit.circuit import Clbit, ClassicalRegister >>> from qiskit.circuit.classical import expr >>> expr.lift(Clbit()) - Var(, Bool()) + Var(, Bool(const=False)) >>> expr.lift(ClassicalRegister(3, "c")) - Var(ClassicalRegister(3, "c"), Uint(3)) + Var(ClassicalRegister(3, "c"), Uint(3, const=False)) The type of the return value can be influenced, if the given value could be interpreted losslessly as the given type (use :func:`cast` to perform a full set of casting @@ -113,9 +116,17 @@ def lift(value: typing.Any, /, type: types.Type | None = None, *, try_const: boo >>> from qiskit.circuit import ClassicalRegister >>> from qiskit.circuit.classical import expr, types >>> expr.lift(ClassicalRegister(3, "c"), types.Uint(5)) - Var(ClassicalRegister(3, "c"), Uint(5)) + Var(ClassicalRegister(3, "c"), Uint(5, const=False)) >>> expr.lift(5, types.Uint(4)) - Value(5, Uint(4)) + Value(5, Uint(4, const=True)) + + Lifting non-classical resource scalars to non-const values:: + + >>> from qiskit.circuit.classical import expr, types + >>> expr.lift(7) + Value(7, Uint(3, const=True)) + >>> expr.lift(7, try_const=False) + Value(7, Uint(3, const=False)) """ if isinstance(value, Expr): if type is not None: @@ -153,14 +164,23 @@ def lift(value: typing.Any, /, type: types.Type | None = None, *, try_const: boo def cast(operand: typing.Any, type: types.Type, /) -> Expr: """Create an explicit cast from the given value to the given type. + This can also be used to cast-away const status. + Examples: Add an explicit cast node that explicitly casts a higher precision type to a lower precision one:: >>> from qiskit.circuit.classical import expr, types - >>> value = expr.value(5, types.Uint(32)) + >>> value = expr.Value(5, types.Uint(32)) >>> expr.cast(value, types.Uint(8)) - Cast(Value(5, types.Uint(32)), types.Uint(8), implicit=False) + Cast(Value(5, types.Uint(32, const=False)), types.Uint(8, const=False), implicit=False) + + Cast-away const status:: + + >>> from qiskit.circuit.classical import expr, types + >>> value = expr.Value(5, types.Uint(32, const=True)) + >>> expr.cast(value, types.Uint(32)) + Cast(Value(5, types.Uint(32, const=True)), types.Uint(32, const=False), implicit=False) """ operand = lift(operand) if cast_kind(operand.type, type) is CastKind.NONE: @@ -178,7 +198,7 @@ def bit_not(operand: typing.Any, /) -> Expr: >>> from qiskit.circuit import ClassicalRegister >>> from qiskit.circuit.classical import expr >>> expr.bit_not(ClassicalRegister(3, "c")) - Unary(Unary.Op.BIT_NOT, Var(ClassicalRegister(3, 'c'), Uint(3)), Uint(3)) + Unary(Unary.Op.BIT_NOT, Var(ClassicalRegister(3, 'c'), Uint(3, const=False)), Uint(3, const=False)) """ operand = lift(operand) if operand.type.kind not in (types.Bool, types.Uint): @@ -198,8 +218,8 @@ def logic_not(operand: typing.Any, /) -> Expr: >>> expr.logic_not(ClassicalRegister(3, "c")) Unary(\ Unary.Op.LOGIC_NOT, \ -Cast(Var(ClassicalRegister(3, 'c'), Uint(3)), Bool(), implicit=True), \ -Bool()) +Cast(Var(ClassicalRegister(3, 'c'), Uint(3, const=False)), Bool(const=False), implicit=True), \ +Bool(const=False)) """ var_or_value = lift(operand) operand = _coerce_lossless(var_or_value, types.Bool(const=var_or_value.type.const)) @@ -214,8 +234,9 @@ def _lift_binary_operands(left: typing.Any, right: typing.Any) -> tuple[Expr, Ex * If neither operand is an expression, both are lifted to share the same const-ness. Both will be const, if possible. Else, neither will be. * If only one operand is an expression, the other is lifted with the same const-ness, if possible. - Otherwise, the returned operands will have different const-ness, and thus require a cast node. - * If both operands are expressions, they are returned as-is and may require a cast node. + Otherwise, the returned operands will have different const-ness, and thus may require a cast node + to be interoperable. + * If both operands are expressions, they are returned as-is, and may require a cast node. """ left_bool = isinstance(left, bool) left_int = isinstance(left, int) and not left_bool @@ -225,7 +246,7 @@ def _lift_binary_operands(left: typing.Any, right: typing.Any) -> tuple[Expr, Ex if left_bool == right_bool: # If they're both bool, they'll lift as const here. # If neither are, we've already checked for int, so they must be bits, - # registers, or expressions, none of which we can't lift to be const. + # registers, or expressions, none of which will lift to be const. left = lift(left, try_const=True) right = lift(right, try_const=True) elif not right_bool: @@ -297,9 +318,9 @@ def bit_and(left: typing.Any, right: typing.Any, /) -> Expr: >>> expr.bit_and(ClassicalRegister(3, "c"), 0b111) Binary(\ Binary.Op.BIT_AND, \ -Var(ClassicalRegister(3, 'c'), Uint(3)), \ -Value(7, Uint(3)), \ -Uint(3)) +Var(ClassicalRegister(3, 'c'), Uint(3, const=False)), \ +Value(7, Uint(3, const=False)), \ +Uint(3, const=False)) """ return _binary_bitwise(Binary.Op.BIT_AND, left, right) @@ -316,9 +337,9 @@ def bit_or(left: typing.Any, right: typing.Any, /) -> Expr: >>> expr.bit_or(ClassicalRegister(3, "c"), 0b101) Binary(\ Binary.Op.BIT_OR, \ -Var(ClassicalRegister(3, 'c'), Uint(3)), \ -Value(5, Uint(3)), \ -Uint(3)) +Var(ClassicalRegister(3, 'c'), Uint(3, const=False)), \ +Value(5, Uint(3, const=False)), \ +Uint(3, const=False)) """ return _binary_bitwise(Binary.Op.BIT_OR, left, right) @@ -335,9 +356,9 @@ def bit_xor(left: typing.Any, right: typing.Any, /) -> Expr: >>> expr.bit_xor(ClassicalRegister(3, "c"), 0b101) Binary(\ Binary.Op.BIT_XOR, \ -Var(ClassicalRegister(3, 'c'), Uint(3)), \ -Value(5, Uint(3)), \ -Uint(3)) +Var(ClassicalRegister(3, 'c'), Uint(3, const=False)), \ +Value(5, Uint(3, const=False)), \ +Uint(3, const=False)) """ return _binary_bitwise(Binary.Op.BIT_XOR, left, right) @@ -358,8 +379,8 @@ def logic_and(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit import Clbit >>> from qiskit.circuit.classical import expr - >>> expr.logical_and(Clbit(), Clbit()) - Binary(Binary.Op.LOGIC_AND, Var(, Bool()), Var(, Bool()), Bool()) + >>> expr.logic_and(Clbit(), Clbit()) + Binary(Binary.Op.LOGIC_AND, Var(, Bool(const=False)), Var(, Bool(const=False)), Bool(const=False)) """ return _binary_logical(Binary.Op.LOGIC_AND, left, right) @@ -374,7 +395,7 @@ def logic_or(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit import Clbit >>> from qiskit.circuit.classical import expr >>> expr.logical_and(Clbit(), Clbit()) - Binary(Binary.Op.LOGIC_OR, Var(, Bool()), Var(, Bool()), Bool()) + Binary(Binary.Op.LOGIC_OR, Var(, Bool(const=False)), Var(, Bool(const=False)), Bool(const=False)) """ return _binary_logical(Binary.Op.LOGIC_OR, left, right) @@ -403,9 +424,9 @@ def equal(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit.classical import expr >>> expr.equal(ClassicalRegister(3, "c"), 7) Binary(Binary.Op.EQUAL, \ -Var(ClassicalRegister(3, "c"), Uint(3)), \ -Value(7, Uint(3)), \ -Uint(3)) +Var(ClassicalRegister(3, "c"), Uint(3, const=False)), \ +Value(7, Uint(3, const=False)), \ +Uint(3, const=False)) """ return _equal_like(Binary.Op.EQUAL, left, right) @@ -421,9 +442,9 @@ def not_equal(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit.classical import expr >>> expr.not_equal(ClassicalRegister(3, "c"), 7) Binary(Binary.Op.NOT_EQUAL, \ -Var(ClassicalRegister(3, "c"), Uint(3)), \ -Value(7, Uint(3)), \ -Uint(3)) +Var(ClassicalRegister(3, "c"), Uint(3, const=False)), \ +Value(7, Uint(3, const=False)), \ +Uint(3, const=False)) """ return _equal_like(Binary.Op.NOT_EQUAL, left, right) @@ -452,9 +473,9 @@ def less(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit.classical import expr >>> expr.less(ClassicalRegister(3, "c"), 5) Binary(Binary.Op.LESS, \ -Var(ClassicalRegister(3, "c"), Uint(3)), \ -Value(5, Uint(3)), \ -Uint(3)) +Var(ClassicalRegister(3, "c"), Uint(3, const=False)), \ +Value(5, Uint(3, const=False)), \ +Uint(3, const=False)) """ return _binary_relation(Binary.Op.LESS, left, right) @@ -470,9 +491,9 @@ def less_equal(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit.classical import expr >>> expr.less(ClassicalRegister(3, "a"), ClassicalRegister(3, "b")) Binary(Binary.Op.LESS_EQUAL, \ -Var(ClassicalRegister(3, "a"), Uint(3)), \ -Var(ClassicalRegister(3, "b"), Uint(3)), \ -Uint(3)) +Var(ClassicalRegister(3, "a"), Uint(3, const=False)), \ +Var(ClassicalRegister(3, "b"), Uint(3, const=False)), \ +Uint(3,const=False)) """ return _binary_relation(Binary.Op.LESS_EQUAL, left, right) @@ -488,9 +509,9 @@ def greater(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit.classical import expr >>> expr.less(ClassicalRegister(3, "c"), 5) Binary(Binary.Op.GREATER, \ -Var(ClassicalRegister(3, "c"), Uint(3)), \ -Value(5, Uint(3)), \ -Uint(3)) +Var(ClassicalRegister(3, "c"), Uint(3, const=False)), \ +Value(5, Uint(3, const=False)), \ +Uint(3, const=False)) """ return _binary_relation(Binary.Op.GREATER, left, right) @@ -506,9 +527,9 @@ def greater_equal(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit.classical import expr >>> expr.less(ClassicalRegister(3, "a"), ClassicalRegister(3, "b")) Binary(Binary.Op.GREATER_EQUAL, \ -Var(ClassicalRegister(3, "a"), Uint(3)), \ -Var(ClassicalRegister(3, "b"), Uint(3)), \ -Uint(3)) +Var(ClassicalRegister(3, "a"), Uint(3, const=False)), \ +Var(ClassicalRegister(3, "b"), Uint(3, const=False)), \ +Uint(3, const=False)) """ return _binary_relation(Binary.Op.GREATER_EQUAL, left, right) @@ -546,17 +567,17 @@ def shift_left(left: typing.Any, right: typing.Any, /, type: types.Type | None = >>> a = expr.Var.new("a", types.Uint(8)) >>> expr.shift_left(a, 4) Binary(Binary.Op.SHIFT_LEFT, \ -Var(, Uint(8), name='a'), \ -Value(4, Uint(3)), \ -Uint(8)) +Var(, Uint(8, const=False), name='a'), \ +Value(4, Uint(3, const=True)), \ +Uint(8, const=False)) Shift an integer literal by a variable amount, coercing the type of the literal:: >>> expr.shift_left(3, a, types.Uint(16)) Binary(Binary.Op.SHIFT_LEFT, \ -Value(3, Uint(16)), \ -Var(, Uint(8), name='a'), \ -Uint(16)) +Value(3, Uint(16, const=True)), \ +Var(, Uint(8, const=False), name='a'), \ +Uint(16, const=False)) """ return _shift_like(Binary.Op.SHIFT_LEFT, left, right, type) @@ -574,9 +595,9 @@ def shift_right(left: typing.Any, right: typing.Any, /, type: types.Type | None >>> from qiskit.circuit.classical import expr >>> expr.shift_right(ClassicalRegister(8, "a"), 4) Binary(Binary.Op.SHIFT_RIGHT, \ -Var(ClassicalRegister(8, "a"), Uint(8)), \ -Value(4, Uint(3)), \ -Uint(8)) +Var(ClassicalRegister(8, "a"), Uint(8, const=False)), \ +Value(4, Uint(3, const=True)), \ +Uint(8, const=False)) """ return _shift_like(Binary.Op.SHIFT_RIGHT, left, right, type) @@ -593,9 +614,9 @@ def index(target: typing.Any, index: typing.Any, /) -> Expr: >>> from qiskit.circuit import ClassicalRegister >>> from qiskit.circuit.classical import expr >>> expr.index(ClassicalRegister(8, "a"), 3) - Index(Var(ClassicalRegister(8, "a"), Uint(8)), Value(3, Uint(2)), Bool()) + Index(Var(ClassicalRegister(8, "a"), Uint(8, const=False)), Value(3, Uint(2, const=True)), Bool(const=False)) """ target, index = lift(target), lift(index) if target.type.kind is not types.Uint or index.type.kind is not types.Uint: raise TypeError(f"invalid types for indexing: '{target.type}' and '{index.type}'") - return Index(target, index, types.Bool(const=target.type.const)) + return Index(target, index, types.Bool(const=target.type.const and index.type.const)) diff --git a/qiskit/circuit/classical/expr/expr.py b/qiskit/circuit/classical/expr/expr.py index 586b06ec9db..0fc0c3480c6 100644 --- a/qiskit/circuit/classical/expr/expr.py +++ b/qiskit/circuit/classical/expr/expr.py @@ -151,8 +151,8 @@ def new(cls, name: str, type: types.Type) -> typing.Self: @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 a pre-existing circuit object.""" + """Whether this :class:`Var` is a standalone variable that owns its storage location, if applicable. + If false, this is a wrapper :class:`Var` around a pre-existing circuit object.""" return isinstance(self.var, uuid.UUID) def accept(self, visitor, /): diff --git a/qiskit/circuit/classical/expr/visitors.py b/qiskit/circuit/classical/expr/visitors.py index 60783380144..c229215f7b4 100644 --- a/qiskit/circuit/classical/expr/visitors.py +++ b/qiskit/circuit/classical/expr/visitors.py @@ -283,6 +283,8 @@ def is_lvalue(node: expr.Expr, /) -> bool: >>> from qiskit.circuit import Clbit >>> expr.is_lvalue(expr.Var.new("a", types.Bool())) True + >>> expr.is_lvalue(expr.Var.new("a", types.Bool(const=False))) + False >>> expr.is_lvalue(expr.lift(Clbit())) True diff --git a/qiskit/circuit/classical/types/ordering.py b/qiskit/circuit/classical/types/ordering.py index 730fdba573c..7615fd70212 100644 --- a/qiskit/circuit/classical/types/ordering.py +++ b/qiskit/circuit/classical/types/ordering.py @@ -55,7 +55,7 @@ def __repr__(self): return str(self) -def _order_identical(_a: Type, _b: Type, /) -> Ordering: +def _order_bool_bool(_a: Bool, _b: Bool, /) -> Ordering: return Ordering.EQUAL @@ -68,7 +68,7 @@ def _order_uint_uint(left: Uint, right: Uint, /) -> Ordering: _ORDERERS = { - (Bool, Bool): _order_identical, + (Bool, Bool): _order_bool_bool, (Uint, Uint): _order_uint_uint, } @@ -83,6 +83,12 @@ def order(left: Type, right: Type, /) -> Ordering: >>> types.order(types.Uint(8), types.Uint(16)) Ordering.LESS + Compare two :class:`Bool` types of differing const-ness:: + + >>> from qiskit.circuit.classical import types + >>> types.order(types.Bool(), types.Bool(const=True)) + Ordering.GREATER + Compare two types that have no ordering between them:: >>> types.order(types.Uint(8), types.Bool()) @@ -117,6 +123,8 @@ def is_subtype(left: Type, right: Type, /, strict: bool = False) -> bool: True >>> types.is_subtype(types.Bool(), types.Bool(), strict=True) False + >>> types.is_subtype(types.Bool(const=True), types.Bool(), strict=True) + True """ order_ = order(left, right) return order_ is Ordering.LESS or (not strict and order_ is Ordering.EQUAL) @@ -140,6 +148,8 @@ def is_supertype(left: Type, right: Type, /, strict: bool = False) -> bool: True >>> types.is_supertype(types.Bool(), types.Bool(), strict=True) False + >>> types.is_supertype(types.Bool(), types.Bool(const=True), strict=True) + True """ order_ = order(left, right) return order_ is Ordering.GREATER or (not strict and order_ is Ordering.EQUAL) @@ -219,7 +229,9 @@ def cast_kind(from_: Type, to_: Type, /) -> CastKind: >>> from qiskit.circuit.classical import types >>> types.cast_kind(types.Bool(), types.Bool()) - >>> types.cast_kind(types.Uint(8, const=True), types.Bool()) + >>> types.cast_kind(types.Uint(8), types.Bool()) + + >>> types.cast_kind(types.Uint(8, const=True), types.Uint(8)) >>> types.cast_kind(types.Bool(), types.Uint(8)) @@ -227,12 +239,12 @@ def cast_kind(from_: Type, to_: Type, /) -> CastKind: """ if to_.const is True and from_.const is False: - # we can't cast to a const type + # We can't cast to a const type. return CastKind.NONE if (coercer := _ALLOWED_CASTS.get((from_.kind, to_.kind))) is None: return CastKind.NONE cast_kind_ = coercer(from_, to_) if cast_kind_ is CastKind.EQUAL and to_.const != from_.const: - # we need an implicit cast to drop const + # We need an implicit cast to drop const. return CastKind.IMPLICIT return cast_kind_ diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py index 9aafe5f1f09..aec5d28b565 100644 --- a/qiskit/circuit/classical/types/types.py +++ b/qiskit/circuit/classical/types/types.py @@ -24,24 +24,6 @@ import typing -class _Singleton(type): - """Metaclass to make the child, which should take zero initialization arguments, a singleton - object.""" - - def _get_singleton_instance(cls): - return cls._INSTANCE - - @classmethod - def __prepare__(mcs, name, bases): # pylint: disable=unused-argument - return {"__new__": mcs._get_singleton_instance} - - @staticmethod - def __new__(cls, name, bases, namespace): - out = super().__new__(cls, name, bases, namespace) - out._INSTANCE = object.__new__(out) # pylint: disable=invalid-name - return out - - class Type: """Root base class of all nodes in the type tree. The base case should never be instantiated directly. From 9f8313cfc086f16e1a6b1d2b3d0c5c6464cbce1e Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Sun, 9 Feb 2025 12:15:40 -0500 Subject: [PATCH 10/53] Fix ordering issue for types with differing const-ness. Types that have some natural order no longer have an ordering when one of them is strictly greater but has an incompatible const-ness (i.e. when the greater type is const but the other type is not). --- qiskit/circuit/classical/types/ordering.py | 19 ++++++++++++--- .../circuit/classical/test_types_ordering.py | 24 +++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/qiskit/circuit/classical/types/ordering.py b/qiskit/circuit/classical/types/ordering.py index 7615fd70212..c42cfd23bec 100644 --- a/qiskit/circuit/classical/types/ordering.py +++ b/qiskit/circuit/classical/types/ordering.py @@ -93,15 +93,28 @@ def order(left: Type, right: Type, /) -> Ordering: >>> types.order(types.Uint(8), types.Bool()) Ordering.NONE + >>> types.order(types.Uint(8), types.Uint(16, const=True)) + Ordering.NONE """ if (orderer := _ORDERERS.get((left.kind, right.kind))) is None: return Ordering.NONE order_ = orderer(left, right) - if order_ is Ordering.EQUAL: - if left.const is True and right.const is False: + + # If the natural type ordering is equal (either one can represent both) + # but the types differ in const-ness, the non-const variant is greater. + # If one type is greater (and thus is the only type that can represent + # both) an ordering is only defined if that type is non-const or both + # types are const. + if left.const is True and right.const is False: + if order_ is Ordering.EQUAL: return Ordering.LESS - if right.const is True and left.const is False: + if order_ is Ordering.GREATER: + return Ordering.NONE + if right.const is True and left.const is False: + if order_ is Ordering.EQUAL: return Ordering.GREATER + if order_ is Ordering.LESS: + return Ordering.NONE return order_ diff --git a/test/python/circuit/classical/test_types_ordering.py b/test/python/circuit/classical/test_types_ordering.py index 16c3791f70f..7820499a828 100644 --- a/test/python/circuit/classical/test_types_ordering.py +++ b/test/python/circuit/classical/test_types_ordering.py @@ -19,13 +19,37 @@ class TestTypesOrdering(QiskitTestCase): def test_order(self): self.assertIs(types.order(types.Uint(8), types.Uint(16)), types.Ordering.LESS) + self.assertIs(types.order(types.Uint(8, const=True), types.Uint(16)), types.Ordering.LESS) + self.assertIs( + types.order(types.Uint(8, const=True), types.Uint(16, const=True)), types.Ordering.LESS + ) + self.assertIs(types.order(types.Uint(16), types.Uint(8)), types.Ordering.GREATER) + self.assertIs( + types.order(types.Uint(16), types.Uint(8, const=True)), types.Ordering.GREATER + ) + self.assertIs( + types.order(types.Uint(16, const=True), types.Uint(8, const=True)), + types.Ordering.GREATER, + ) + self.assertIs(types.order(types.Uint(8), types.Uint(8)), types.Ordering.EQUAL) + self.assertIs(types.order(types.Uint(8, const=True), types.Uint(8)), types.Ordering.LESS) + self.assertIs(types.order(types.Uint(8), types.Uint(8, const=True)), types.Ordering.GREATER) + self.assertIs( + types.order(types.Uint(8, const=True), types.Uint(8, const=True)), types.Ordering.EQUAL + ) self.assertIs(types.order(types.Bool(), types.Bool()), types.Ordering.EQUAL) + self.assertIs(types.order(types.Bool(const=True), types.Bool()), types.Ordering.LESS) + self.assertIs(types.order(types.Bool(), types.Bool(const=True)), types.Ordering.GREATER) + self.assertIs( + types.order(types.Bool(const=True), types.Bool(const=True)), types.Ordering.EQUAL + ) self.assertIs(types.order(types.Bool(), types.Uint(8)), types.Ordering.NONE) self.assertIs(types.order(types.Uint(8), types.Bool()), types.Ordering.NONE) + self.assertIs(types.order(types.Uint(8), types.Uint(16, const=True)), types.Ordering.NONE) def test_is_subtype(self): self.assertTrue(types.is_subtype(types.Uint(8), types.Uint(16))) From db9d9cbcbbe7ca3e88e4960d500ddcb6a703fd0e Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Mon, 10 Feb 2025 14:11:48 -0500 Subject: [PATCH 11/53] Fix QPY serialization. We need to reject types with const=True in QPY until it supports them. For now, I've also made the Index and shift operator constructors lift their RHS to the same const-ness as the target to make it less likely that existing users of expr run into issues when serializing to older QPY versions. --- qiskit/circuit/classical/expr/constructors.py | 5 +++-- qiskit/qpy/binary_io/value.py | 5 +++-- .../classical/test_expr_constructors.py | 20 +++++++++++-------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index 7d26ecd063a..6b8221acb0a 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -543,7 +543,7 @@ def _shift_like( left = _coerce_lossless(left, type) if type is not None else left else: left = lift(left, type) - right = lift(right) + right = lift(right, try_const=left.type.const) if left.type.kind != types.Uint or right.type.kind != types.Uint: raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") return Binary( @@ -616,7 +616,8 @@ def index(target: typing.Any, index: typing.Any, /) -> Expr: >>> expr.index(ClassicalRegister(8, "a"), 3) Index(Var(ClassicalRegister(8, "a"), Uint(8, const=False)), Value(3, Uint(2, const=True)), Bool(const=False)) """ - target, index = lift(target), lift(index) + target = lift(target) + index = lift(index, try_const=target.type.const) if target.type.kind is not types.Uint or index.type.kind is not types.Uint: raise TypeError(f"invalid types for indexing: '{target.type}' and '{index.type}'") return Index(target, index, types.Bool(const=target.type.const and index.type.const)) diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 9799fdf3f45..3bab629421d 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -367,9 +367,10 @@ def _write_expr( def _write_expr_type(file_obj, type_: types.Type): - if type_.kind is types.Bool: + # Currently, QPY doesn't support const types + if type_.kind is types.Bool and not type_.const: file_obj.write(type_keys.ExprType.BOOL) - elif type_.kind is types.Uint: + elif type_.kind is types.Uint and not type_.const: file_obj.write(type_keys.ExprType.UINT) file_obj.write( struct.pack(formats.EXPR_TYPE_UINT_PACK, *formats.EXPR_TYPE_UINT(type_.width)) diff --git a/test/python/circuit/classical/test_expr_constructors.py b/test/python/circuit/classical/test_expr_constructors.py index 0463c4b7ecc..66a7d7acd54 100644 --- a/test/python/circuit/classical/test_expr_constructors.py +++ b/test/python/circuit/classical/test_expr_constructors.py @@ -529,9 +529,7 @@ def test_index_explicit(self): self.assertEqual( expr.index(cr, 3), - expr.Index( - expr.Var(cr, types.Uint(4)), expr.Value(3, types.Uint(2, const=True)), types.Bool() - ), + expr.Index(expr.Var(cr, types.Uint(4)), expr.Value(3, types.Uint(2)), types.Bool()), ) self.assertEqual( expr.index(a, cr), @@ -557,7 +555,7 @@ def test_shift_explicit(self, function, opcode): expr.Binary( opcode, expr.Var(cr, types.Uint(8)), - expr.Value(5, types.Uint(3, const=True)), + expr.Value(5, types.Uint(3)), types.Uint(8), ), ) @@ -565,15 +563,21 @@ def test_shift_explicit(self, function, opcode): function(a, cr), expr.Binary(opcode, a, expr.Var(cr, types.Uint(8)), types.Uint(4)), ) - # TODO: as it is, the LHS of a shift expression always becomes exactly the explicit - # type passed to `shift_*`. The RHS is inferred with lift, and stays const if it's - # const. Is this the best behavior? + self.assertEqual( + function(3, 5), + expr.Binary( + opcode, + expr.Value(3, types.Uint(2, const=True)), + expr.Value(5, types.Uint(3, const=True)), + types.Uint(2, const=True), + ), + ) self.assertEqual( function(3, 5, types.Uint(8)), expr.Binary( opcode, expr.Value(3, types.Uint(8)), - expr.Value(5, types.Uint(3, const=True)), + expr.Value(5, types.Uint(3)), types.Uint(8), ), ) From 71b7e7a0f5a8858b710e41e99e4119902a502e61 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Tue, 11 Feb 2025 16:59:42 -0500 Subject: [PATCH 12/53] Make expr.Lift default to non-const. This is probably a better default in general, since we don't really have much use for const except for timing stuff. --- qiskit/circuit/classical/expr/constructors.py | 49 ++++++++++--------- qiskit/circuit/quantumcircuit.py | 2 +- .../classical/test_expr_constructors.py | 44 ++++++++--------- test/python/circuit/test_store.py | 26 ---------- 4 files changed, 48 insertions(+), 73 deletions(-) diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index 6b8221acb0a..21bd7d713db 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -91,13 +91,15 @@ def lift_legacy_condition( return Binary(Binary.Op.EQUAL, left, right, types.Bool()) -def lift(value: typing.Any, /, type: types.Type | None = None, *, try_const: bool = True) -> Expr: +def lift(value: typing.Any, /, type: types.Type | None = None, *, try_const: bool = False) -> Expr: """Lift the given Python ``value`` to a :class:`~.expr.Value` or :class:`~.expr.Var`. - If an explicit ``type`` is given, the typing in the output will reflect that. + By default, lifted scalars are not const. To lift supported scalars to const-typed + expressions, specify `try_const=True`. - By default, lifted scalars will be const if they aren't backed by a classical resource. - To lift scalars to a non-const-typed expression, specify `try_const` as `False`. + If an explicit ``type`` is given, the typing in the output will reflect that. + The ``type`` must be const if ``try_const`` is specified and ``value`` can lift to + a const expression. Examples: Lifting simple circuit objects to be :class:`~.expr.Var` instances:: @@ -118,15 +120,15 @@ def lift(value: typing.Any, /, type: types.Type | None = None, *, try_const: boo >>> expr.lift(ClassicalRegister(3, "c"), types.Uint(5)) Var(ClassicalRegister(3, "c"), Uint(5, const=False)) >>> expr.lift(5, types.Uint(4)) - Value(5, Uint(4, const=True)) + Value(5, Uint(4)) - Lifting non-classical resource scalars to non-const values:: + Lifting non-classical resource scalars to const values:: >>> from qiskit.circuit.classical import expr, types >>> expr.lift(7) - Value(7, Uint(3, const=True)) - >>> expr.lift(7, try_const=False) Value(7, Uint(3, const=False)) + >>> expr.lift(7, try_const=True) + Value(7, Uint(3, const=True)) """ if isinstance(value, Expr): if type is not None: @@ -239,16 +241,15 @@ def _lift_binary_operands(left: typing.Any, right: typing.Any) -> tuple[Expr, Ex * If both operands are expressions, they are returned as-is, and may require a cast node. """ left_bool = isinstance(left, bool) - left_int = isinstance(left, int) and not left_bool + left_int = isinstance(left, int) and not isinstance(left, bool) right_bool = isinstance(right, bool) right_int = isinstance(right, int) and not right_bool if not (left_int or right_int): if left_bool == right_bool: - # If they're both bool, they'll lift as const here. - # If neither are, we've already checked for int, so they must be bits, - # registers, or expressions, none of which will lift to be const. - left = lift(left, try_const=True) - right = lift(right, try_const=True) + # They're either both bool, or neither are, so we lift them + # independently. + left = lift(left) + right = lift(right) elif not right_bool: # Left is a bool, which should only be const if right is const. right = lift(right) @@ -268,7 +269,7 @@ def _lift_binary_operands(left: typing.Any, right: typing.Any) -> tuple[Expr, Ex # Left will share const-ness of right. left = Value(left, right.type) else: - left = lift(left) + left = lift(left, try_const=right.type.const) elif not left_int: # Right is an int. left = lift(left) @@ -280,10 +281,12 @@ def _lift_binary_operands(left: typing.Any, right: typing.Any) -> tuple[Expr, Ex # Right will share const-ness of left. right = Value(right, left.type) else: - right = lift(right) + right = lift(right, try_const=left.type.const) else: # Both are `int`, so we take our best case to make things work. - uint = types.Uint(max(left.bit_length(), right.bit_length(), 1), const=True) + # If the caller needs a const type, they should lift one side to + # a const type explicitly before calling this function. + uint = types.Uint(max(left.bit_length(), right.bit_length(), 1)) left = Value(left, uint) right = Value(right, uint) return left, right @@ -295,7 +298,7 @@ def _binary_bitwise(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: if left.type.kind is right.type.kind is types.Bool: type = types.Bool(const=(left.type.const and right.type.const)) elif left.type.kind is types.Uint and right.type.kind is types.Uint: - if left.type != right.type: + if left.type.width != right.type.width: raise TypeError( "binary bitwise operations are defined between unsigned integers of the same width," f" but got {left.type.width} and {right.type.width}." @@ -303,7 +306,7 @@ def _binary_bitwise(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: type = types.Uint(width=left.type.width, const=(left.type.const and right.type.const)) else: raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") - return Binary(op, left, right, type) + return Binary(op, _coerce_lossless(left, type), _coerce_lossless(right, type), type) def bit_and(left: typing.Any, right: typing.Any, /) -> Expr: @@ -568,14 +571,14 @@ def shift_left(left: typing.Any, right: typing.Any, /, type: types.Type | None = >>> expr.shift_left(a, 4) Binary(Binary.Op.SHIFT_LEFT, \ Var(, Uint(8, const=False), name='a'), \ -Value(4, Uint(3, const=True)), \ +Value(4, Uint(3, const=False)), \ Uint(8, const=False)) Shift an integer literal by a variable amount, coercing the type of the literal:: >>> expr.shift_left(3, a, types.Uint(16)) Binary(Binary.Op.SHIFT_LEFT, \ -Value(3, Uint(16, const=True)), \ +Value(3, Uint(16, const=False)), \ Var(, Uint(8, const=False), name='a'), \ Uint(16, const=False)) """ @@ -596,7 +599,7 @@ def shift_right(left: typing.Any, right: typing.Any, /, type: types.Type | None >>> expr.shift_right(ClassicalRegister(8, "a"), 4) Binary(Binary.Op.SHIFT_RIGHT, \ Var(ClassicalRegister(8, "a"), Uint(8, const=False)), \ -Value(4, Uint(3, const=True)), \ +Value(4, Uint(3, const=False)), \ Uint(8, const=False)) """ return _shift_like(Binary.Op.SHIFT_RIGHT, left, right, type) @@ -614,7 +617,7 @@ def index(target: typing.Any, index: typing.Any, /) -> Expr: >>> from qiskit.circuit import ClassicalRegister >>> from qiskit.circuit.classical import expr >>> expr.index(ClassicalRegister(8, "a"), 3) - Index(Var(ClassicalRegister(8, "a"), Uint(8, const=False)), Value(3, Uint(2, const=True)), Bool(const=False)) + Index(Var(ClassicalRegister(8, "a"), Uint(8, const=False)), Value(3, Uint(2, const=False)), Bool(const=False)) """ target = lift(target) index = lift(index, try_const=target.type.const) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 06e3ec38bfc..8638071f8ef 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -2911,7 +2911,7 @@ def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.V coerce_type = name_or_var.type else: coerce_type = None - initial = _validate_expr(circuit_scope, expr.lift(initial, coerce_type, try_const=False)) + initial = _validate_expr(circuit_scope, expr.lift(initial, coerce_type)) if isinstance(name_or_var, str): var = expr.Var.new(name_or_var, initial.type) elif not name_or_var.standalone: diff --git a/test/python/circuit/classical/test_expr_constructors.py b/test/python/circuit/classical/test_expr_constructors.py index 66a7d7acd54..80d751682ac 100644 --- a/test/python/circuit/classical/test_expr_constructors.py +++ b/test/python/circuit/classical/test_expr_constructors.py @@ -83,15 +83,15 @@ def test_value_lifts_qiskit_scalars(self): self.assertEqual(expr.lift(clbit), expr.Var(clbit, types.Bool())) def test_value_lifts_python_builtins(self): - self.assertEqual(expr.lift(True), expr.Value(True, types.Bool(const=True))) - self.assertEqual(expr.lift(False), expr.Value(False, types.Bool(const=True))) - self.assertEqual(expr.lift(7), expr.Value(7, types.Uint(3, const=True))) + self.assertEqual(expr.lift(True), expr.Value(True, types.Bool())) + self.assertEqual(expr.lift(False), expr.Value(False, types.Bool())) + self.assertEqual(expr.lift(7), expr.Value(7, types.Uint(3))) def test_value_ensures_nonzero_width(self): - self.assertEqual(expr.lift(0), expr.Value(0, types.Uint(1, const=True))) + self.assertEqual(expr.lift(0), expr.Value(0, types.Uint(1))) def test_value_type_representation(self): - self.assertEqual(expr.lift(5), expr.Value(5, types.Uint((5).bit_length(), const=True))) + self.assertEqual(expr.lift(5), expr.Value(5, types.Uint((5).bit_length()))) self.assertEqual(expr.lift(5, types.Uint(8)), expr.Value(5, types.Uint(8))) cr = ClassicalRegister(3, "c") @@ -209,9 +209,9 @@ def test_binary_relations_lift_scalars(self, function, op): function(255, 6), expr.Binary( op, - expr.Value(255, types.Uint(width=8, const=True)), - expr.Value(6, types.Uint(width=8, const=True)), - types.Bool(const=True), + expr.Value(255, types.Uint(width=8)), + expr.Value(6, types.Uint(width=8)), + types.Bool(), ), ) @@ -376,9 +376,9 @@ def test_binary_bitwise_uint_inference(self, function, opcode): function(5, 255), expr.Binary( opcode, - expr.Value(5, types.Uint(8, const=True)), - expr.Value(255, types.Uint(8, const=True)), - types.Uint(8, const=True), + expr.Value(5, types.Uint(8)), + expr.Value(255, types.Uint(8)), + types.Uint(8), ), ) @@ -434,11 +434,9 @@ def test_binary_logical_explicit(self, function, opcode): function(False, 3), expr.Binary( opcode, - expr.Value(False, types.Bool(const=True)), - expr.Cast( - expr.Value(3, types.Uint(2, const=True)), types.Bool(const=True), implicit=True - ), - types.Bool(const=True), + expr.Value(False, types.Bool()), + expr.Cast(expr.Value(3, types.Uint(2)), types.Bool(), implicit=True), + types.Bool(), ), ) @@ -567,9 +565,9 @@ def test_shift_explicit(self, function, opcode): function(3, 5), expr.Binary( opcode, - expr.Value(3, types.Uint(2, const=True)), - expr.Value(5, types.Uint(3, const=True)), - types.Uint(2, const=True), + expr.Value(3, types.Uint(2)), + expr.Value(5, types.Uint(3)), + types.Uint(2), ), ) self.assertEqual( @@ -582,12 +580,12 @@ def test_shift_explicit(self, function, opcode): ), ) self.assertEqual( - function(3, 5, types.Uint(8, const=True)), + function(3, 5, types.Uint(8)), expr.Binary( opcode, - expr.Value(3, types.Uint(8, const=True)), - expr.Value(5, types.Uint(3, const=True)), - types.Uint(8, const=True), + expr.Value(3, types.Uint(8)), + expr.Value(5, types.Uint(3)), + types.Uint(8), ), ) diff --git a/test/python/circuit/test_store.py b/test/python/circuit/test_store.py index ec60b87e644..cff7cfda0f7 100644 --- a/test/python/circuit/test_store.py +++ b/test/python/circuit/test_store.py @@ -170,32 +170,6 @@ def test_lifts_values(self): qc.store(b, 0xFFFF) self.assertEqual(qc.data[-1].operation, Store(b, expr.lift(0xFFFF, try_const=False))) - def test_implicit_const_cast(self): - a = expr.Var.new("a", types.Bool()) - qc = QuantumCircuit(captures=[a]) - qc.store(a, expr.lift(False)) - self.assertEqual( - qc.data[-1].operation, - Store( - a, expr.Cast(expr.Value(False, types.Bool(const=True)), types.Bool(), implicit=True) - ), - ) - - b = expr.Var.new("b", types.Uint(16)) - qc.add_capture(b) - qc.store(b, expr.lift(0xFFFF)) - self.assertEqual( - qc.data[-1].operation, - Store( - b, - expr.Cast( - expr.Value(0xFFFF, types.Uint(width=16, const=True)), - types.Uint(width=16), - implicit=True, - ), - ), - ) - def test_lifts_integer_literals_to_full_width(self): a = expr.Var.new("a", types.Uint(8)) qc = QuantumCircuit(inputs=[a]) From 2091557951c2aaab6a107d67aa6068491c283b0a Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Tue, 11 Feb 2025 17:01:33 -0500 Subject: [PATCH 13/53] Revert to old test_expr_constructors.py. --- .../classical/test_expr_constructors.py | 186 ++---------------- 1 file changed, 19 insertions(+), 167 deletions(-) diff --git a/test/python/circuit/classical/test_expr_constructors.py b/test/python/circuit/classical/test_expr_constructors.py index 80d751682ac..6500e5593ac 100644 --- a/test/python/circuit/classical/test_expr_constructors.py +++ b/test/python/circuit/classical/test_expr_constructors.py @@ -165,139 +165,24 @@ def test_logic_not_explicit(self): expr.Unary(expr.Unary.Op.LOGIC_NOT, expr.Var(clbit, types.Bool()), types.Bool()), ) - # TODO: test equal like separately with bools @ddt.data( - (expr.equal, expr.Binary.Op.EQUAL), - (expr.not_equal, expr.Binary.Op.NOT_EQUAL), - (expr.less, expr.Binary.Op.LESS), - (expr.less_equal, expr.Binary.Op.LESS_EQUAL), - (expr.greater, expr.Binary.Op.GREATER), - (expr.greater_equal, expr.Binary.Op.GREATER_EQUAL), + (expr.bit_and, ClassicalRegister(3), ClassicalRegister(3)), + (expr.bit_or, ClassicalRegister(3), ClassicalRegister(3)), + (expr.bit_xor, ClassicalRegister(3), ClassicalRegister(3)), + (expr.logic_and, Clbit(), True), + (expr.logic_or, False, ClassicalRegister(3)), + (expr.equal, ClassicalRegister(8), 255), + (expr.not_equal, ClassicalRegister(8), 255), + (expr.less, ClassicalRegister(3), 6), + (expr.less_equal, ClassicalRegister(3), 5), + (expr.greater, 4, ClassicalRegister(3)), + (expr.greater_equal, ClassicalRegister(3), 5), ) @ddt.unpack - def test_binary_relations_lift_scalars(self, function, op): - cr_3 = ClassicalRegister(3, "c") - cr_8 = ClassicalRegister(8, "c") - self.assertEqual( - function(cr_3, cr_3), - expr.Binary( - op, - expr.Var(cr_3, types.Uint(width=3)), - expr.Var(cr_3, types.Uint(width=3)), - types.Bool(), - ), - ) - self.assertEqual( - function(cr_8, 255), - expr.Binary( - op, - expr.Var(cr_8, types.Uint(width=8)), - expr.Value(255, types.Uint(width=8)), - types.Bool(), - ), - ) - self.assertEqual( - function(cr_3, 6), - expr.Binary( - op, - expr.Var(cr_3, types.Uint(width=3)), - expr.Value(6, types.Uint(width=3)), - types.Bool(), - ), - ) - self.assertEqual( - function(255, 6), - expr.Binary( - op, - expr.Value(255, types.Uint(width=8)), - expr.Value(6, types.Uint(width=8)), - types.Bool(), - ), - ) - - @ddt.data( - (expr.logic_and, expr.Binary.Op.LOGIC_AND), - (expr.logic_or, expr.Binary.Op.LOGIC_OR), - ) - @ddt.unpack - def test_binary_logical_lift_scalars(self, function, op): - cr_3 = ClassicalRegister(3, "c") - clbit = Clbit() - - self.assertEqual( - function(cr_3, cr_3), - expr.Binary( - op, - expr.Cast(expr.Var(cr_3, types.Uint(width=3)), types.Bool(), implicit=True), - expr.Cast(expr.Var(cr_3, types.Uint(width=3)), types.Bool(), implicit=True), - types.Bool(), - ), - ) - self.assertEqual( - function(clbit, True), - expr.Binary( - op, - expr.Var(clbit, types.Bool()), - expr.Value(True, types.Bool()), - types.Bool(), - ), - ) - self.assertEqual( - function(cr_3, True), - expr.Binary( - op, - expr.Cast(expr.Var(cr_3, types.Uint(width=3)), types.Bool(), implicit=True), - expr.Value(True, types.Bool()), - types.Bool(), - ), - ) - - @ddt.data( - (expr.bit_and, expr.Binary.Op.BIT_AND), - (expr.bit_or, expr.Binary.Op.BIT_OR), - (expr.bit_xor, expr.Binary.Op.BIT_XOR), - ) - @ddt.unpack - def test_binary_bitwise_lift_scalars(self, function, op): - cr_3 = ClassicalRegister(3, "c") - cr_8 = ClassicalRegister(8, "c") - clbit = Clbit() - self.assertEqual( - function(cr_3, cr_3), - expr.Binary( - op, - expr.Var(cr_3, types.Uint(width=3)), - expr.Var(cr_3, types.Uint(width=3)), - types.Uint(width=3), - ), - ) - self.assertEqual( - function(clbit, True), - expr.Binary( - op, - expr.Var(clbit, types.Bool()), - expr.Value(True, types.Bool()), - types.Bool(), - ), - ) - self.assertEqual( - function(cr_8, 255), - expr.Binary( - op, - expr.Var(cr_8, types.Uint(width=8)), - expr.Value(255, types.Uint(width=8)), - types.Uint(width=8), - ), - ) - self.assertEqual( - function(cr_3, 6), - expr.Binary( - op, - expr.Var(cr_3, types.Uint(width=3)), - expr.Value(6, types.Uint(width=3)), - types.Uint(width=3), - ), - ) + def test_binary_functions_lift_scalars(self, function, left, right): + self.assertEqual(function(left, right), function(expr.lift(left), right)) + self.assertEqual(function(left, right), function(left, expr.lift(right))) + self.assertEqual(function(left, right), function(expr.lift(left), expr.lift(right))) @ddt.data( (expr.bit_and, expr.Binary.Op.BIT_AND), @@ -415,7 +300,7 @@ def test_binary_logical_explicit(self, function, opcode): expr.Binary( opcode, expr.Cast(expr.Var(cr, types.Uint(cr.size)), types.Bool(), implicit=True), - expr.Cast(expr.Value(3, types.Uint(cr.size)), types.Bool(), implicit=True), + expr.Cast(expr.Value(3, types.Uint(2)), types.Bool(), implicit=True), types.Bool(), ), ) @@ -430,16 +315,6 @@ def test_binary_logical_explicit(self, function, opcode): ), ) - self.assertEqual( - function(False, 3), - expr.Binary( - opcode, - expr.Value(False, types.Bool()), - expr.Cast(expr.Value(3, types.Uint(2)), types.Bool(), implicit=True), - types.Bool(), - ), - ) - @ddt.data( (expr.equal, expr.Binary.Op.EQUAL), (expr.not_equal, expr.Binary.Op.NOT_EQUAL), @@ -548,44 +423,21 @@ def test_index_forbidden(self): def test_shift_explicit(self, function, opcode): cr = ClassicalRegister(8, "c") a = expr.Var.new("a", types.Uint(4)) + self.assertEqual( function(cr, 5), expr.Binary( - opcode, - expr.Var(cr, types.Uint(8)), - expr.Value(5, types.Uint(3)), - types.Uint(8), + opcode, expr.Var(cr, types.Uint(8)), expr.Value(5, types.Uint(3)), types.Uint(8) ), ) self.assertEqual( function(a, cr), expr.Binary(opcode, a, expr.Var(cr, types.Uint(8)), types.Uint(4)), ) - self.assertEqual( - function(3, 5), - expr.Binary( - opcode, - expr.Value(3, types.Uint(2)), - expr.Value(5, types.Uint(3)), - types.Uint(2), - ), - ) self.assertEqual( function(3, 5, types.Uint(8)), expr.Binary( - opcode, - expr.Value(3, types.Uint(8)), - expr.Value(5, types.Uint(3)), - types.Uint(8), - ), - ) - self.assertEqual( - function(3, 5, types.Uint(8)), - expr.Binary( - opcode, - expr.Value(3, types.Uint(8)), - expr.Value(5, types.Uint(3)), - types.Uint(8), + opcode, expr.Value(3, types.Uint(8)), expr.Value(5, types.Uint(3)), types.Uint(8) ), ) From 7307be98795e8608ac42c194af18447e53e1ae6e Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Tue, 11 Feb 2025 17:15:53 -0500 Subject: [PATCH 14/53] Make binary_logical lift independent again. Since we're going for using a Cast node when const-ness differs, this will be fine. --- qiskit/circuit/classical/expr/constructors.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index 21bd7d713db..5835e8a5abc 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -367,10 +367,12 @@ def bit_xor(left: typing.Any, right: typing.Any, /) -> Expr: def _binary_logical(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: - left, right = _lift_binary_operands(left, right) - left = _coerce_lossless(left, types.Bool(const=left.type.const)) - right = _coerce_lossless(right, types.Bool(const=right.type.const)) - return Binary(op, left, right, types.Bool(const=(left.type.const and right.type.const))) + left = lift(left) + right = lift(right) + type = types.Bool(const=(left.type.const and right.type.const)) + left = _coerce_lossless(left, type) + right = _coerce_lossless(right, type) + return Binary(op, left, right, type) def logic_and(left: typing.Any, right: typing.Any, /) -> Expr: From 9b30284ce718b2a35dc7c3a2ebb1b8045e241801 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Wed, 12 Feb 2025 01:18:20 -0500 Subject: [PATCH 15/53] Update tests, handle a few edge cases. --- qiskit/circuit/classical/expr/constructors.py | 20 +- .../classical/test_expr_constructors.py | 180 +++++++++++++++++- 2 files changed, 194 insertions(+), 6 deletions(-) diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index 5835e8a5abc..b4283f1b50d 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -97,9 +97,8 @@ def lift(value: typing.Any, /, type: types.Type | None = None, *, try_const: boo By default, lifted scalars are not const. To lift supported scalars to const-typed expressions, specify `try_const=True`. - If an explicit ``type`` is given, the typing in the output will reflect that. - The ``type`` must be const if ``try_const`` is specified and ``value`` can lift to - a const expression. + If an explicit ``type`` is given, the typing in the output will reflect that, + including its const-ness. The ``try_const`` parameter is ignored when this is specified. Examples: Lifting simple circuit objects to be :class:`~.expr.Var` instances:: @@ -136,6 +135,10 @@ def lift(value: typing.Any, /, type: types.Type | None = None, *, try_const: boo return value from qiskit.circuit import Clbit, ClassicalRegister # pylint: disable=cyclic-import + if type is not None: + # If a type was specified, the inferred type must be the same + # const-ness. + try_const = type.const inferred: types.Type if value is True or value is False: inferred = types.Bool(const=try_const) @@ -407,7 +410,10 @@ def logic_or(left: typing.Any, right: typing.Any, /) -> Expr: def _equal_like(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: left, right = _lift_binary_operands(left, right) - if left.type.kind is not right.type.kind: + if ( + left.type.kind is not right.type.kind + or types.order(left.type, right.type) is types.Ordering.NONE + ): raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") type = types.greater(left.type, right.type) return Binary( @@ -456,7 +462,11 @@ def not_equal(left: typing.Any, right: typing.Any, /) -> Expr: def _binary_relation(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: left, right = _lift_binary_operands(left, right) - if left.type.kind is not right.type.kind or left.type.kind is types.Bool: + if ( + left.type.kind is not right.type.kind + or left.type.kind is types.Bool + or types.order(left.type, right.type) is types.Ordering.NONE + ): raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") type = types.greater(left.type, right.type) return Binary( diff --git a/test/python/circuit/classical/test_expr_constructors.py b/test/python/circuit/classical/test_expr_constructors.py index 6500e5593ac..9ed90c14eec 100644 --- a/test/python/circuit/classical/test_expr_constructors.py +++ b/test/python/circuit/classical/test_expr_constructors.py @@ -84,15 +84,27 @@ def test_value_lifts_qiskit_scalars(self): def test_value_lifts_python_builtins(self): self.assertEqual(expr.lift(True), expr.Value(True, types.Bool())) + self.assertEqual(expr.lift(True, try_const=True), expr.Value(True, types.Bool(const=True))) self.assertEqual(expr.lift(False), expr.Value(False, types.Bool())) + self.assertEqual( + expr.lift(False, try_const=True), expr.Value(False, types.Bool(const=True)) + ) self.assertEqual(expr.lift(7), expr.Value(7, types.Uint(3))) + self.assertEqual(expr.lift(7, try_const=True), expr.Value(7, types.Uint(3, const=True))) def test_value_ensures_nonzero_width(self): self.assertEqual(expr.lift(0), expr.Value(0, types.Uint(1))) + self.assertEqual(expr.lift(0, try_const=True), expr.Value(0, types.Uint(1, const=True))) def test_value_type_representation(self): self.assertEqual(expr.lift(5), expr.Value(5, types.Uint((5).bit_length()))) + self.assertEqual( + expr.lift(5, try_const=True), expr.Value(5, types.Uint((5).bit_length(), const=True)) + ) self.assertEqual(expr.lift(5, types.Uint(8)), expr.Value(5, types.Uint(8))) + self.assertEqual( + expr.lift(5, types.Uint(8, const=True)), expr.Value(5, types.Uint(8, const=True)) + ) cr = ClassicalRegister(3, "c") self.assertEqual(expr.lift(cr, types.Uint(8)), expr.Var(cr, types.Uint(8))) @@ -115,6 +127,12 @@ def test_cast_adds_explicit_nodes(self): expr.cast(base, types.Uint(8)), expr.Cast(base, types.Uint(8), implicit=False) ) + def test_cast_adds_node_when_shedding_const(self): + base = expr.Value(5, types.Uint(8, const=True)) + self.assertEqual( + expr.cast(base, types.Uint(8)), expr.Cast(base, types.Uint(8), implicit=False) + ) + def test_cast_allows_lossy_downcasting(self): """An explicit 'cast' call should allow lossy casts to be performed.""" base = expr.Value(5, types.Uint(16)) @@ -148,6 +166,14 @@ def test_bit_not_explicit(self): expr.bit_not(clbit), expr.Unary(expr.Unary.Op.BIT_NOT, expr.Var(clbit, types.Bool()), types.Bool()), ) + self.assertEqual( + expr.bit_not(expr.Value(3, types.Uint(2, const=True))), + expr.Unary( + expr.Unary.Op.BIT_NOT, + expr.Value(3, types.Uint(2, const=True)), + types.Uint(2, const=True), + ), + ) def test_logic_not_explicit(self): cr = ClassicalRegister(3) @@ -164,6 +190,16 @@ def test_logic_not_explicit(self): expr.logic_not(clbit), expr.Unary(expr.Unary.Op.LOGIC_NOT, expr.Var(clbit, types.Bool()), types.Bool()), ) + self.assertEqual( + expr.logic_not(expr.Value(3, types.Uint(2, const=True))), + expr.Unary( + expr.Unary.Op.LOGIC_NOT, + expr.Cast( + expr.Value(3, types.Uint(2, const=True)), types.Bool(const=True), implicit=True + ), + types.Bool(const=True), + ), + ) @ddt.data( (expr.bit_and, ClassicalRegister(3), ClassicalRegister(3)), @@ -184,6 +220,31 @@ def test_binary_functions_lift_scalars(self, function, left, right): self.assertEqual(function(left, right), function(left, expr.lift(right))) self.assertEqual(function(left, right), function(expr.lift(left), expr.lift(right))) + @ddt.data( + (expr.bit_and, 6, 7), + (expr.bit_or, 5, 6), + (expr.bit_xor, 255, 254), + (expr.equal, 254, 255), + (expr.not_equal, 255, 255), + (expr.less, 5, 4), + (expr.less_equal, 3, 3), + (expr.greater, 254, 255), + (expr.greater_equal, 4, 5), + ) + @ddt.unpack + def test_binary_functions_lift_scalars_const(self, function, left, right): + """If one operand is an expr with a const type, the other scalar should be lifted as const. + Note that logical operators (e.g. logic_and, logic_or) are excluded since these lift operands + independently.""" + self.assertEqual( + function(expr.lift(left, try_const=True), right), + function(expr.lift(left, try_const=True), expr.lift(right, try_const=True)), + ) + self.assertEqual( + function(left, expr.lift(right, try_const=True)), + function(expr.lift(left, try_const=True), expr.lift(right, try_const=True)), + ) + @ddt.data( (expr.bit_and, expr.Binary.Op.BIT_AND), (expr.bit_or, expr.Binary.Op.BIT_OR), @@ -204,7 +265,15 @@ def test_binary_bitwise_explicit(self, function, opcode): opcode, expr.Value(255, types.Uint(8)), expr.Var(cr, types.Uint(8)), types.Uint(8) ), ) - + self.assertEqual( + function(expr.lift(255, try_const=True), cr), + expr.Binary( + opcode, + expr.Cast(expr.Value(255, types.Uint(8, const=True)), types.Uint(8), implicit=True), + expr.Var(cr, types.Uint(8)), + types.Uint(8), + ), + ) clbit = Clbit() self.assertEqual( function(True, clbit), @@ -224,6 +293,24 @@ def test_binary_bitwise_explicit(self, function, opcode): types.Bool(), ), ) + self.assertEqual( + function(255, 255), + expr.Binary( + opcode, + expr.Value(255, types.Uint(8)), + expr.Value(255, types.Uint(8)), + types.Uint(8), + ), + ) + self.assertEqual( + function(expr.lift(255, try_const=True), 255), + expr.Binary( + opcode, + expr.Value(255, types.Uint(8, const=True)), + expr.Value(255, types.Uint(8, const=True)), + types.Uint(8, const=True), + ), + ) @ddt.data( (expr.bit_and, expr.Binary.Op.BIT_AND), @@ -305,6 +392,16 @@ def test_binary_logical_explicit(self, function, opcode): ), ) + self.assertEqual( + function(cr, expr.lift(3, try_const=True)), + expr.Binary( + opcode, + expr.Cast(expr.Var(cr, types.Uint(cr.size)), types.Bool(), implicit=True), + expr.Cast(expr.Value(3, types.Uint(2, const=True)), types.Bool(), implicit=True), + types.Bool(), + ), + ) + self.assertEqual( function(False, clbit), expr.Binary( @@ -315,6 +412,17 @@ def test_binary_logical_explicit(self, function, opcode): ), ) + # Logical operations lift their operands independently. + self.assertEqual( + function(expr.lift(False, try_const=True), 1), + expr.Binary( + opcode, + expr.Cast(expr.Value(False, types.Bool(const=True)), types.Bool(), implicit=True), + expr.Cast(expr.Value(1, types.Uint(1)), types.Bool(), implicit=True), + types.Bool(), + ), + ) + @ddt.data( (expr.equal, expr.Binary.Op.EQUAL), (expr.not_equal, expr.Binary.Op.NOT_EQUAL), @@ -341,6 +449,17 @@ def test_binary_equal_explicit(self, function, opcode): ), ) + self.assertEqual( + function(expr.lift(7, try_const=True), cr), + expr.Binary( + opcode, + # Explicit cast required to get from Uint(3) to Uint(8) + expr.Cast(expr.Value(7, types.Uint(3, const=True)), types.Uint(8), implicit=False), + expr.Var(cr, types.Uint(8)), + types.Bool(), + ), + ) + self.assertEqual( function(clbit, True), expr.Binary( @@ -351,6 +470,16 @@ def test_binary_equal_explicit(self, function, opcode): ), ) + self.assertEqual( + function(expr.lift(False, try_const=True), True), + expr.Binary( + opcode, + expr.Value(False, types.Bool(const=True)), + expr.Value(True, types.Bool(const=True)), + types.Bool(const=True), + ), + ) + @ddt.data(expr.equal, expr.not_equal) def test_binary_equal_forbidden(self, function): with self.assertRaisesRegex(TypeError, "invalid types"): @@ -359,6 +488,9 @@ def test_binary_equal_forbidden(self, function): function(ClassicalRegister(3, "c"), False) with self.assertRaisesRegex(TypeError, "invalid types"): function(5, True) + with self.assertRaisesRegex(TypeError, "invalid types"): + # No order between a smaller non-const int and larger const. + function(expr.lift(0xFF, types.Uint(8)), expr.lift(0xFFFF, types.Uint(16, const=True))) @ddt.data( (expr.less, expr.Binary.Op.LESS), @@ -387,6 +519,31 @@ def test_binary_relation_explicit(self, function, opcode): ), ) + self.assertEqual( + function(expr.lift(12, try_const=True), cr), + expr.Binary( + opcode, + # Explicit cast required to get from Uint(4) to Uint(8) + expr.Cast(expr.Value(12, types.Uint(4, const=True)), types.Uint(8), implicit=False), + expr.Var(cr, types.Uint(8)), + types.Bool(), + ), + ) + + self.assertEqual( + function(expr.lift(12, types.Uint(8, const=True)), expr.lift(12, try_const=True)), + expr.Binary( + opcode, + expr.Value(12, types.Uint(8, const=True)), + expr.Cast( + expr.Value(12, types.Uint(4, const=True)), + types.Uint(8, const=True), + implicit=False, + ), + types.Bool(const=True), + ), + ) + @ddt.data(expr.less, expr.less_equal, expr.greater, expr.greater_equal) def test_binary_relation_forbidden(self, function): with self.assertRaisesRegex(TypeError, "invalid types"): @@ -395,6 +552,9 @@ def test_binary_relation_forbidden(self, function): function(ClassicalRegister(3, "c"), False) with self.assertRaisesRegex(TypeError, "invalid types"): function(Clbit(), Clbit()) + with self.assertRaisesRegex(TypeError, "invalid types"): + # No order between a smaller non-const int and larger const. + function(expr.lift(0xFF, types.Uint(8)), expr.lift(0xFFFF, types.Uint(16, const=True))) def test_index_explicit(self): cr = ClassicalRegister(4, "c") @@ -408,6 +568,24 @@ def test_index_explicit(self): expr.index(a, cr), expr.Index(a, expr.Var(cr, types.Uint(4)), types.Bool()), ) + # The index arg gets lifted to match the const-ness of the target. + self.assertEqual( + expr.index(expr.lift(0xFF, try_const=True), 2), + expr.Index( + expr.Value(0xFF, types.Uint(8, const=True)), + expr.Value(2, types.Uint(2, const=True)), + types.Bool(const=True), + ), + ) + # ...but not the other way around. + self.assertEqual( + expr.index(expr.lift(0xFF), expr.lift(2, try_const=True)), + expr.Index( + expr.Value(0xFF, types.Uint(8)), + expr.Value(2, types.Uint(2, const=True)), + types.Bool(), + ), + ) def test_index_forbidden(self): with self.assertRaisesRegex(TypeError, "invalid types"): From ce1faf158535c7cfa60c68c256bd33cc479eecdb Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Wed, 12 Feb 2025 01:37:49 -0500 Subject: [PATCH 16/53] Fix docstring. --- qiskit/circuit/classical/expr/constructors.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index b4283f1b50d..a4ef71fa5eb 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -236,8 +236,7 @@ def _lift_binary_operands(left: typing.Any, right: typing.Any) -> tuple[Expr, Ex position to match the other operand. Const-ness is handled as follows: - * If neither operand is an expression, both are lifted to share the same const-ness. - Both will be const, if possible. Else, neither will be. + * If neither operand is an expression, both are lifted as non-const. * If only one operand is an expression, the other is lifted with the same const-ness, if possible. Otherwise, the returned operands will have different const-ness, and thus may require a cast node to be interoperable. From 4fee48f565f736c0df20ededf1ac4310fce8b3ee Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Wed, 12 Feb 2025 01:49:35 -0500 Subject: [PATCH 17/53] Remove now redundant arg from tests. --- qiskit/circuit/quantumcircuit.py | 4 +-- .../circuit/test_circuit_load_from_qpy.py | 2 +- test/python/circuit/test_circuit_vars.py | 26 +++++++++---------- test/python/circuit/test_control_flow.py | 2 +- .../circuit/test_control_flow_builders.py | 6 ++--- test/python/circuit/test_store.py | 14 +++++----- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 8638071f8ef..7ab1bae5d8c 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -3836,11 +3836,11 @@ def store(self, lvalue: typing.Any, rvalue: typing.Any, /) -> InstructionSet: Create a new variable in the circuit that can be written to with this method. """ # As a convenience, lift integer-literal rvalues to the matching width. - lvalue = expr.lift(lvalue, try_const=False) + lvalue = expr.lift(lvalue) rvalue_type = ( lvalue.type if isinstance(rvalue, int) and not isinstance(rvalue, bool) else None ) - rvalue = expr.lift(rvalue, rvalue_type, try_const=False) + rvalue = expr.lift(rvalue, rvalue_type) return self.append(Store(lvalue, rvalue), (), (), copy=False) def measure(self, qubit: QubitSpecifier, cbit: ClbitSpecifier) -> InstructionSet: diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 2dcbdcbf1d8..962dd22ac79 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -1802,7 +1802,7 @@ def test_load_empty_vars(self): a = expr.Var.new("a", types.Bool()) b = expr.Var.new("b", types.Uint(8)) all_vars = { - a: expr.lift(False, try_const=False), + a: expr.lift(False), b: expr.lift(3, type=b.type), expr.Var.new("ΞΈΟˆΟ†", types.Bool()): expr.logic_not(a), expr.Var.new("🐍🐍🐍", types.Uint(8)): expr.bit_and(b, b), diff --git a/test/python/circuit/test_circuit_vars.py b/test/python/circuit/test_circuit_vars.py index f910e4c6a8e..f6916dcb72d 100644 --- a/test/python/circuit/test_circuit_vars.py +++ b/test/python/circuit/test_circuit_vars.py @@ -42,8 +42,8 @@ def test_initialise_captures(self): def test_initialise_declarations_iterable(self): vars_ = [ - (expr.Var.new("a", types.Bool()), expr.lift(True, try_const=False)), - (expr.Var.new("b", types.Uint(16)), expr.lift(0xFFFF, try_const=False)), + (expr.Var.new("a", types.Bool()), expr.lift(True)), + (expr.Var.new("b", types.Uint(16)), expr.lift(0xFFFF)), ] qc = QuantumCircuit(declarations=vars_) @@ -61,8 +61,8 @@ def test_initialise_declarations_iterable(self): 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, try_const=False), - expr.Var.new("b", types.Uint(16)): expr.lift(0xFFFF, try_const=False), + expr.Var.new("a", types.Bool()): expr.lift(True), + expr.Var.new("b", types.Uint(16)): expr.lift(0xFFFF), } qc = QuantumCircuit(declarations=vars_) @@ -80,7 +80,7 @@ def test_initialise_declarations_dependencies(self): them, provided they're specified in a suitable order.""" a = expr.Var.new("a", types.Bool()) vars_ = [ - (a, expr.lift(True, try_const=False)), + (a, expr.lift(True)), (expr.Var.new("b", types.Bool()), a), ] qc = QuantumCircuit(declarations=vars_) @@ -139,7 +139,7 @@ def test_add_uninitialized_var(self): def test_add_var_returns_good_var(self): qc = QuantumCircuit() - a = qc.add_var("a", expr.lift(True, try_const=False)) + a = qc.add_var("a", expr.lift(True)) self.assertEqual(a.name, "a") self.assertEqual(a.type, types.Bool()) @@ -223,9 +223,9 @@ def test_initialise_captures_equal_to_add_capture(self): def test_initialise_declarations_equal_to_add_var(self): a = expr.Var.new("a", types.Bool()) - a_init = expr.lift(False, try_const=False) + a_init = expr.lift(False) b = expr.Var.new("b", types.Uint(16)) - b_init = expr.lift(0xFFFF, try_const=False) + b_init = expr.lift(0xFFFF) qc_init = QuantumCircuit(declarations=[(a, a_init), (b, b_init)]) qc_manual = QuantumCircuit() @@ -287,8 +287,8 @@ def test_cannot_shadow_names(self): 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, try_const=False) - a_uint_init = expr.lift(0xFFFF, try_const=False) + a_bool_init = expr.lift(True) + a_uint_init = expr.lift(0xFFFF) tests = [ ((a_bool1, a_bool_init), (a_bool2, a_bool_init)), @@ -325,11 +325,11 @@ def test_cannot_shadow_names(self): qc.add_var(right, right_init) qc = QuantumCircuit() - qc.add_var("a", expr.lift(True, try_const=False)) + qc.add_var("a", expr.lift(True)) with self.assertRaisesRegex(CircuitError, "its name shadows"): - qc.add_var("a", expr.lift(True, try_const=False)) + qc.add_var("a", expr.lift(True)) with self.assertRaisesRegex(CircuitError, "its name shadows"): - qc.add_var("a", expr.lift(0xFF, try_const=False)) + qc.add_var("a", expr.lift(0xFF)) def test_cannot_add_vars_wrapping_clbits(self): a = expr.Var(Clbit(), types.Bool()) diff --git a/test/python/circuit/test_control_flow.py b/test/python/circuit/test_control_flow.py index 2ef8b1a3e78..51733a37ffc 100644 --- a/test/python/circuit/test_control_flow.py +++ b/test/python/circuit/test_control_flow.py @@ -996,7 +996,7 @@ def test_can_add_op_with_captures_of_captures(self): def test_can_add_op_with_captures_of_locals(self): """Test circuit methods can capture declared variables.""" outer = QuantumCircuit(1, 1) - a = outer.add_var("a", expr.lift(True, try_const=False)) + a = outer.add_var("a", expr.lift(True)) inner = QuantumCircuit(1, 1, captures=[a]) diff --git a/test/python/circuit/test_control_flow_builders.py b/test/python/circuit/test_control_flow_builders.py index cf56a7208de..3b699a16898 100644 --- a/test/python/circuit/test_control_flow_builders.py +++ b/test/python/circuit/test_control_flow_builders.py @@ -4192,15 +4192,15 @@ def test_exception_during_initialisation_does_not_add_variable(self): with self.assertRaises(CircuitError): Store(uint_var, bool_expr) base = QuantumCircuit() - with base.while_loop(expr.lift(False, try_const=False)): + with base.while_loop(expr.lift(False)): # Should succeed. - b = base.add_var("b", expr.lift(False, try_const=False)) + b = base.add_var("b", expr.lift(False)) try: base.add_var(uint_var, bool_expr) except CircuitError: pass # Should succeed. - c = base.add_var("c", expr.lift(False, try_const=False)) + c = base.add_var("c", expr.lift(False)) local_vars = set(base.iter_vars()) self.assertEqual(local_vars, {b, c}) diff --git a/test/python/circuit/test_store.py b/test/python/circuit/test_store.py index cff7cfda0f7..e52df728c98 100644 --- a/test/python/circuit/test_store.py +++ b/test/python/circuit/test_store.py @@ -31,7 +31,7 @@ def test_happy_path_construction(self): def test_store_to_index(self): lvalue = expr.index(expr.Var.new("a", types.Uint(8)), 3) - rvalue = expr.lift(False, try_const=False) + rvalue = expr.lift(False) constructed = Store(lvalue, rvalue) self.assertIsInstance(constructed, Store) self.assertEqual(constructed.lvalue, lvalue) @@ -115,7 +115,7 @@ def test_allows_stores_with_clbits(self): qc.store(a, expr.lift(clbits[1])) expected = [ - Store(expr.lift(clbits[0]), expr.lift(True, try_const=False)), + 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])), @@ -135,7 +135,7 @@ def test_allows_stores_with_cregs(self): qc.store(a, cregs[1]) expected = [ - Store(expr.lift(cregs[0]), expr.lift(0xFF, try_const=False)), + 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])), @@ -152,8 +152,8 @@ def test_allows_stores_with_index(self): qc.store(expr.index(a, 3), True) qc.store(expr.index(cr, a), expr.index(cr, 0)) expected = [ - Store(expr.index(cr, 0), expr.lift(False, try_const=False)), - Store(expr.index(a, 3), expr.lift(True, try_const=False)), + Store(expr.index(cr, 0), expr.lift(False)), + Store(expr.index(a, 3), expr.lift(True)), Store(expr.index(cr, a), expr.index(cr, 0)), ] actual = [instruction.operation for instruction in qc.data] @@ -163,12 +163,12 @@ 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, try_const=False))) + 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, try_const=False))) + self.assertEqual(qc.data[-1].operation, Store(b, expr.lift(0xFFFF))) def test_lifts_integer_literals_to_full_width(self): a = expr.Var.new("a", types.Uint(8)) From 88ab046d63ec87922304f92f8ac380447d761bb7 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Wed, 12 Feb 2025 10:36:36 -0500 Subject: [PATCH 18/53] Add const testing for ordering. --- .../circuit/classical/test_types_ordering.py | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/test/python/circuit/classical/test_types_ordering.py b/test/python/circuit/classical/test_types_ordering.py index 7820499a828..4179d3c11da 100644 --- a/test/python/circuit/classical/test_types_ordering.py +++ b/test/python/circuit/classical/test_types_ordering.py @@ -20,6 +20,7 @@ class TestTypesOrdering(QiskitTestCase): def test_order(self): self.assertIs(types.order(types.Uint(8), types.Uint(16)), types.Ordering.LESS) self.assertIs(types.order(types.Uint(8, const=True), types.Uint(16)), types.Ordering.LESS) + self.assertIs(types.order(types.Uint(8), types.Uint(16, const=True)), types.Ordering.NONE) self.assertIs( types.order(types.Uint(8, const=True), types.Uint(16, const=True)), types.Ordering.LESS ) @@ -28,6 +29,7 @@ def test_order(self): self.assertIs( types.order(types.Uint(16), types.Uint(8, const=True)), types.Ordering.GREATER ) + self.assertIs(types.order(types.Uint(16, const=True), types.Uint(8)), types.Ordering.NONE) self.assertIs( types.order(types.Uint(16, const=True), types.Uint(8, const=True)), types.Ordering.GREATER, @@ -49,39 +51,66 @@ def test_order(self): self.assertIs(types.order(types.Bool(), types.Uint(8)), types.Ordering.NONE) self.assertIs(types.order(types.Uint(8), types.Bool()), types.Ordering.NONE) - self.assertIs(types.order(types.Uint(8), types.Uint(16, const=True)), types.Ordering.NONE) def test_is_subtype(self): self.assertTrue(types.is_subtype(types.Uint(8), types.Uint(16))) + self.assertTrue(types.is_subtype(types.Uint(8, const=True), types.Uint(16))) self.assertFalse(types.is_subtype(types.Uint(16), types.Uint(8))) + self.assertFalse(types.is_subtype(types.Uint(16, const=True), types.Uint(8))) + self.assertFalse(types.is_subtype(types.Uint(16), types.Uint(8, const=True))) self.assertTrue(types.is_subtype(types.Uint(8), types.Uint(8))) self.assertFalse(types.is_subtype(types.Uint(8), types.Uint(8), strict=True)) + self.assertTrue(types.is_subtype(types.Uint(8, const=True), types.Uint(8), strict=True)) self.assertTrue(types.is_subtype(types.Bool(), types.Bool())) self.assertFalse(types.is_subtype(types.Bool(), types.Bool(), strict=True)) + self.assertTrue(types.is_subtype(types.Bool(const=True), types.Bool(), strict=True)) self.assertFalse(types.is_subtype(types.Bool(), types.Uint(8))) self.assertFalse(types.is_subtype(types.Uint(8), types.Bool())) def test_is_supertype(self): self.assertFalse(types.is_supertype(types.Uint(8), types.Uint(16))) + self.assertFalse(types.is_supertype(types.Uint(8, const=True), types.Uint(16))) self.assertTrue(types.is_supertype(types.Uint(16), types.Uint(8))) + self.assertTrue(types.is_supertype(types.Uint(16), types.Uint(8, const=True))) + self.assertTrue(types.is_supertype(types.Uint(16, const=True), types.Uint(8, const=True))) + self.assertFalse(types.is_supertype(types.Uint(16, const=True), types.Uint(8))) self.assertTrue(types.is_supertype(types.Uint(8), types.Uint(8))) self.assertFalse(types.is_supertype(types.Uint(8), types.Uint(8), strict=True)) + self.assertTrue(types.is_supertype(types.Uint(8), types.Uint(8, const=True), strict=True)) self.assertTrue(types.is_supertype(types.Bool(), types.Bool())) self.assertFalse(types.is_supertype(types.Bool(), types.Bool(), strict=True)) + self.assertTrue(types.is_supertype(types.Bool(), types.Bool(const=True), strict=True)) self.assertFalse(types.is_supertype(types.Bool(), types.Uint(8))) self.assertFalse(types.is_supertype(types.Uint(8), types.Bool())) def test_greater(self): self.assertEqual(types.greater(types.Uint(16), types.Uint(8)), types.Uint(16)) + self.assertEqual(types.greater(types.Uint(16), types.Uint(8, const=True)), types.Uint(16)) self.assertEqual(types.greater(types.Uint(8), types.Uint(16)), types.Uint(16)) + self.assertEqual(types.greater(types.Uint(8, const=True), types.Uint(16)), types.Uint(16)) self.assertEqual(types.greater(types.Uint(8), types.Uint(8)), types.Uint(8)) + self.assertEqual(types.greater(types.Uint(8), types.Uint(8, const=True)), types.Uint(8)) + self.assertEqual(types.greater(types.Uint(8, const=True), types.Uint(8)), types.Uint(8)) + self.assertEqual( + types.greater(types.Uint(8, const=True), types.Uint(8, const=True)), + types.Uint(8, const=True), + ) self.assertEqual(types.greater(types.Bool(), types.Bool()), types.Bool()) + self.assertEqual(types.greater(types.Bool(const=True), types.Bool()), types.Bool()) + self.assertEqual(types.greater(types.Bool(), types.Bool(const=True)), types.Bool()) + self.assertEqual( + types.greater(types.Bool(const=True), types.Bool(const=True)), types.Bool(const=True) + ) with self.assertRaisesRegex(TypeError, "no ordering"): types.greater(types.Bool(), types.Uint(8)) + with self.assertRaisesRegex(TypeError, "no ordering"): + types.greater(types.Uint(16, const=True), types.Uint(8)) + with self.assertRaisesRegex(TypeError, "no ordering"): + types.greater(types.Uint(8), types.Uint(16, const=True)) class TestTypesCastKind(QiskitTestCase): @@ -89,6 +118,39 @@ 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.Bool(const=True), types.Bool(const=True)), types.CastKind.EQUAL + ) + self.assertIs( + types.cast_kind(types.Bool(const=True), types.Bool()), types.CastKind.IMPLICIT + ) + self.assertIs(types.cast_kind(types.Bool(), types.Bool(const=True)), types.CastKind.NONE) self.assertIs(types.cast_kind(types.Uint(8), types.Bool()), types.CastKind.IMPLICIT) + self.assertIs( + types.cast_kind(types.Uint(8, const=True), types.Bool(const=True)), + types.CastKind.IMPLICIT, + ) + self.assertIs( + types.cast_kind(types.Uint(8, const=True), types.Bool()), types.CastKind.IMPLICIT + ) + self.assertIs(types.cast_kind(types.Uint(8), types.Bool(const=True)), types.CastKind.NONE) self.assertIs(types.cast_kind(types.Bool(), types.Uint(8)), types.CastKind.LOSSLESS) + self.assertIs( + types.cast_kind(types.Bool(const=True), types.Uint(8, const=True)), + types.CastKind.LOSSLESS, + ) + self.assertIs( + types.cast_kind(types.Bool(const=True), types.Uint(8)), types.CastKind.LOSSLESS + ) + self.assertIs(types.cast_kind(types.Bool(), types.Uint(8, const=True)), types.CastKind.NONE) self.assertIs(types.cast_kind(types.Uint(16), types.Uint(8)), types.CastKind.DANGEROUS) + self.assertIs( + types.cast_kind(types.Uint(16, const=True), types.Uint(8, const=True)), + types.CastKind.DANGEROUS, + ) + self.assertIs( + types.cast_kind(types.Uint(16, const=True), types.Uint(8)), types.CastKind.DANGEROUS + ) + self.assertIs( + types.cast_kind(types.Uint(16), types.Uint(8, const=True)), types.CastKind.NONE + ) From c5a230fe115ba0e626920ce69b716d3f669d4877 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Wed, 12 Feb 2025 10:59:16 -0500 Subject: [PATCH 19/53] Add const tests for shifts. --- .../classical/test_expr_constructors.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/python/circuit/classical/test_expr_constructors.py b/test/python/circuit/classical/test_expr_constructors.py index 9ed90c14eec..bfcfef45c1e 100644 --- a/test/python/circuit/classical/test_expr_constructors.py +++ b/test/python/circuit/classical/test_expr_constructors.py @@ -608,6 +608,15 @@ def test_shift_explicit(self, function, opcode): opcode, expr.Var(cr, types.Uint(8)), expr.Value(5, types.Uint(3)), types.Uint(8) ), ) + self.assertEqual( + function(cr, expr.lift(5, try_const=True)), + expr.Binary( + opcode, + expr.Var(cr, types.Uint(8)), + expr.Value(5, types.Uint(3, const=True)), + types.Uint(8), + ), + ) self.assertEqual( function(a, cr), expr.Binary(opcode, a, expr.Var(cr, types.Uint(8)), types.Uint(4)), @@ -618,6 +627,38 @@ def test_shift_explicit(self, function, opcode): opcode, expr.Value(3, types.Uint(8)), expr.Value(5, types.Uint(3)), types.Uint(8) ), ) + self.assertEqual( + function(3, 5, types.Uint(8, const=True)), + expr.Binary( + opcode, + expr.Value(3, types.Uint(8, const=True)), + expr.Value(5, types.Uint(3, const=True)), + types.Uint(8, const=True), + ), + ) + self.assertEqual( + function(expr.lift(3, try_const=True), 5, types.Uint(8, const=True)), + expr.Binary( + opcode, + expr.Cast( + expr.Value(3, types.Uint(2, const=True)), + types.Uint(8, const=True), + implicit=False, + ), + expr.Value(5, types.Uint(3, const=True)), + types.Uint(8, const=True), + ), + ) + self.assertEqual( + function(expr.lift(3, try_const=True), 5, types.Uint(8)), + expr.Binary( + opcode, + expr.Cast(expr.Value(3, types.Uint(2, const=True)), types.Uint(8), implicit=False), + # Lifts as non-const because target type types.Uint(8) is non-const. + expr.Value(5, types.Uint(3)), + types.Uint(8), + ), + ) @ddt.data(expr.shift_left, expr.shift_right) def test_shift_forbidden(self, function): From 7c88d88faabdf6b23fcd93fcc7caddbb56893fe9 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Wed, 12 Feb 2025 11:31:10 -0500 Subject: [PATCH 20/53] Add release note. --- .../notes/const-expr-397ff09042942b81.yaml | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 releasenotes/notes/const-expr-397ff09042942b81.yaml diff --git a/releasenotes/notes/const-expr-397ff09042942b81.yaml b/releasenotes/notes/const-expr-397ff09042942b81.yaml new file mode 100644 index 00000000000..ebbccd32d06 --- /dev/null +++ b/releasenotes/notes/const-expr-397ff09042942b81.yaml @@ -0,0 +1,21 @@ +--- +features_circuits: + - | + The classical realtime-expressions module :mod:`qiskit.circuit.classical` can now represent + constant types and operations on them. All :class:`~.types.Type` classes now have a bool + :attr:`~.types.Type.const` property which is used to mark const-ness and enforce const + invariants across the types system. + + To create a const value expression use :func:`~.expr.lift`, setting ``try_const=True``:: + + from qiskit.circuit.classical import expr + + expr.lift(5, try_const=True) + # >>> Value(5, Uint(3, const=True)) + + The result type of an operation applied to const types is also const:: + + from qiskit.circuit.classical import expr + + expr.bit_and(expr.lift(5, try_const=True), expr.lift(6, try_const=True)).type.const + # >>> True From c58a7b809bcde54ec09b0514f432c94517b6cbad Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Wed, 12 Feb 2025 11:51:55 -0500 Subject: [PATCH 21/53] Add const store tests. --- test/python/circuit/test_store.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/test/python/circuit/test_store.py b/test/python/circuit/test_store.py index e52df728c98..f79fb5c6926 100644 --- a/test/python/circuit/test_store.py +++ b/test/python/circuit/test_store.py @@ -178,6 +178,15 @@ def test_lifts_integer_literals_to_full_width(self): qc.store(a, 255) self.assertEqual(qc.data[-1].operation, Store(a, expr.Value(255, a.type))) + def test_implicitly_casts_const_scalars(self): + a = expr.Var.new("a", types.Uint(8)) + qc = QuantumCircuit(inputs=[a]) + qc.store(a, expr.lift(1, types.Uint(8, const=True))) + self.assertEqual( + qc.data[-1].operation, + Store(a, expr.Cast(expr.Value(1, types.Uint(8, const=True)), a.type, implicit=True)), + ) + def test_does_not_widen_bool_literal(self): # `bool` is a subclass of `int` in Python (except some arithmetic operations have different # semantics...). It's not in Qiskit's value type system, though. @@ -226,10 +235,13 @@ def test_rejects_cregs_not_in_circuit(self): 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]) + c = expr.Var.new("c", types.Bool(const=True)) + qc = QuantumCircuit(inputs=[a, b, c]) not_an_lvalue = expr.logic_and(a, b) with self.assertRaisesRegex(CircuitError, "not an l-value"): qc.store(not_an_lvalue, expr.lift(False)) + with self.assertRaisesRegex(CircuitError, "not an l-value"): + qc.store(c, expr.lift(False)) def test_rejects_explicit_cast(self): lvalue = expr.Var.new("a", types.Uint(16)) From d9e9a8c2cc8aea049ecdeda8e2d09d1c3183046e Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Wed, 12 Feb 2025 22:52:04 -0500 Subject: [PATCH 22/53] Address lint, minor cleanup. --- qiskit/circuit/classical/expr/constructors.py | 28 ++++++++++++++----- qiskit/circuit/classical/expr/expr.py | 5 ++-- qiskit/circuit/classical/expr/visitors.py | 2 +- qiskit/circuit/classical/types/types.py | 21 ++++++-------- 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index a4ef71fa5eb..df858c0e5a0 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -128,6 +128,9 @@ def lift(value: typing.Any, /, type: types.Type | None = None, *, try_const: boo Value(7, Uint(3, const=False)) >>> expr.lift(7, try_const=True) Value(7, Uint(3, const=True)) + >>> expr.lift(7, types.Uint(8, const=True)) + Value(7, Uint(8, const=True)) + """ if isinstance(value, Expr): if type is not None: @@ -203,7 +206,8 @@ def bit_not(operand: typing.Any, /) -> Expr: >>> from qiskit.circuit import ClassicalRegister >>> from qiskit.circuit.classical import expr >>> expr.bit_not(ClassicalRegister(3, "c")) - Unary(Unary.Op.BIT_NOT, Var(ClassicalRegister(3, 'c'), Uint(3, const=False)), Uint(3, const=False)) + Unary(Unary.Op.BIT_NOT, \ +Var(ClassicalRegister(3, 'c'), Uint(3, const=False)), Uint(3, const=False)) """ operand = lift(operand) if operand.type.kind not in (types.Bool, types.Uint): @@ -223,11 +227,12 @@ def logic_not(operand: typing.Any, /) -> Expr: >>> expr.logic_not(ClassicalRegister(3, "c")) Unary(\ Unary.Op.LOGIC_NOT, \ -Cast(Var(ClassicalRegister(3, 'c'), Uint(3, const=False)), Bool(const=False), implicit=True), \ +Cast(Var(ClassicalRegister(3, 'c'), Uint(3, const=False)), \ +Bool(const=False), implicit=True), \ Bool(const=False)) """ - var_or_value = lift(operand) - operand = _coerce_lossless(var_or_value, types.Bool(const=var_or_value.type.const)) + operand = lift(operand) + operand = _coerce_lossless(operand, types.Bool(const=operand.type.const)) return Unary(Unary.Op.LOGIC_NOT, operand, operand.type) @@ -387,7 +392,10 @@ def logic_and(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit import Clbit >>> from qiskit.circuit.classical import expr >>> expr.logic_and(Clbit(), Clbit()) - Binary(Binary.Op.LOGIC_AND, Var(, Bool(const=False)), Var(, Bool(const=False)), Bool(const=False)) + Binary(Binary.Op.LOGIC_AND, \ +Var(, Bool(const=False)), \ +Var(, Bool(const=False)), \ +Bool(const=False)) """ return _binary_logical(Binary.Op.LOGIC_AND, left, right) @@ -402,7 +410,10 @@ def logic_or(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit import Clbit >>> from qiskit.circuit.classical import expr >>> expr.logical_and(Clbit(), Clbit()) - Binary(Binary.Op.LOGIC_OR, Var(, Bool(const=False)), Var(, Bool(const=False)), Bool(const=False)) + Binary(Binary.Op.LOGIC_OR, \ +Var(, Bool(const=False)), \ +Var(, Bool(const=False)), \ +Bool(const=False)) """ return _binary_logical(Binary.Op.LOGIC_OR, left, right) @@ -628,7 +639,10 @@ def index(target: typing.Any, index: typing.Any, /) -> Expr: >>> from qiskit.circuit import ClassicalRegister >>> from qiskit.circuit.classical import expr >>> expr.index(ClassicalRegister(8, "a"), 3) - Index(Var(ClassicalRegister(8, "a"), Uint(8, const=False)), Value(3, Uint(2, const=False)), Bool(const=False)) + Index(\ +Var(ClassicalRegister(8, "a"), Uint(8, const=False)), \ +Value(3, Uint(2, const=False)), \ +Bool(const=False)) """ target = lift(target) index = lift(index, try_const=target.type.const) diff --git a/qiskit/circuit/classical/expr/expr.py b/qiskit/circuit/classical/expr/expr.py index 0fc0c3480c6..6f41fc0d219 100644 --- a/qiskit/circuit/classical/expr/expr.py +++ b/qiskit/circuit/classical/expr/expr.py @@ -151,8 +151,9 @@ def new(cls, name: str, type: types.Type) -> typing.Self: @property def standalone(self) -> bool: - """Whether this :class:`Var` is a standalone variable that owns its storage location, if applicable. - If false, this is a wrapper :class:`Var` around a pre-existing circuit object.""" + """Whether this :class:`Var` is a standalone variable that owns its storage + location, if applicable. If false, this is a wrapper :class:`Var` around a + pre-existing circuit object.""" return isinstance(self.var, uuid.UUID) def accept(self, visitor, /): diff --git a/qiskit/circuit/classical/expr/visitors.py b/qiskit/circuit/classical/expr/visitors.py index c229215f7b4..eeb1d775fa5 100644 --- a/qiskit/circuit/classical/expr/visitors.py +++ b/qiskit/circuit/classical/expr/visitors.py @@ -283,7 +283,7 @@ def is_lvalue(node: expr.Expr, /) -> bool: >>> from qiskit.circuit import Clbit >>> expr.is_lvalue(expr.Var.new("a", types.Bool())) True - >>> expr.is_lvalue(expr.Var.new("a", types.Bool(const=False))) + >>> expr.is_lvalue(expr.Var.new("a", types.Bool(const=True))) False >>> expr.is_lvalue(expr.lift(Clbit())) True diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py index aec5d28b565..81bd80bc33f 100644 --- a/qiskit/circuit/classical/types/types.py +++ b/qiskit/circuit/classical/types/types.py @@ -29,9 +29,14 @@ class Type: directly. This must not be subclassed by users; subclasses form the internal data of the representation of - expressions, and it does not make sense to add more outside of Qiskit library code.""" + expressions, and it does not make sense to add more outside of Qiskit library code. - __slots__ = () + All subclasses are responsible for setting the ``const`` attribute in their ``__init__``. + """ + + __slots__ = ("const",) + + const: bool @property def kind(self): @@ -40,11 +45,6 @@ def kind(self): this a hashable enum-like discriminator you can rely on.""" return self.__class__ - @property - def const(self): - """Get the const-ness of this type.""" - raise NotImplementedError("types must implement the 'const' attribute") - # Enforcement of immutability. The constructor methods need to manually skip this. def __setattr__(self, _key, _value): @@ -67,7 +67,7 @@ def __setstate__(self, state): class Bool(Type): """The Boolean type. This has exactly two values: ``True`` and ``False``.""" - __slots__ = ("const",) + __slots__ = () def __init__(self, *, const: bool = False): super(Type, self).__setattr__("const", const) @@ -86,10 +86,7 @@ def __eq__(self, other): class Uint(Type): """An unsigned integer of fixed bit width.""" - __slots__ = ( - "const", - "width", - ) + __slots__ = ("width",) def __init__(self, width: int, *, const: bool = False): if isinstance(width, int) and width <= 0: From 4a561502c069ad16a74256b2797783933d4e143a Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Wed, 12 Feb 2025 14:19:31 -0500 Subject: [PATCH 23/53] Add Float type to classical expressions. --- qiskit/circuit/classical/expr/constructors.py | 25 ++++++++++++------- qiskit/circuit/classical/types/__init__.py | 3 ++- qiskit/circuit/classical/types/ordering.py | 13 +++++----- qiskit/circuit/classical/types/types.py | 23 ++++++++++++++++- 4 files changed, 47 insertions(+), 17 deletions(-) diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index df858c0e5a0..5345a939f50 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -19,6 +19,7 @@ __all__ = [ "lift", + "cast", "bit_not", "logic_not", "bit_and", @@ -32,6 +33,9 @@ "less_equal", "greater", "greater_equal", + "shift_left", + "shift_right", + "index", "lift_legacy_condition", ] @@ -157,6 +161,9 @@ def lift(value: typing.Any, /, type: types.Type | None = None, *, try_const: boo raise ValueError("cannot represent a negative value") inferred = types.Uint(width=value.bit_length() or 1, const=try_const) constructor = Value + elif isinstance(value, float): + inferred = types.Float(const=try_const) + constructor = Value else: raise TypeError(f"failed to infer a type for '{value}'") if type is None: @@ -247,22 +254,22 @@ def _lift_binary_operands(left: typing.Any, right: typing.Any) -> tuple[Expr, Ex to be interoperable. * If both operands are expressions, they are returned as-is, and may require a cast node. """ - left_bool = isinstance(left, bool) + left_other_literal = isinstance(left, (bool, float)) left_int = isinstance(left, int) and not isinstance(left, bool) - right_bool = isinstance(right, bool) - right_int = isinstance(right, int) and not right_bool + right_other_literal = isinstance(right, (bool, float)) + right_int = isinstance(right, int) and not right_other_literal if not (left_int or right_int): - if left_bool == right_bool: - # They're either both bool, or neither are, so we lift them + if left_other_literal == right_other_literal: + # They're either both literals or neither are, so we lift them # independently. left = lift(left) right = lift(right) - elif not right_bool: - # Left is a bool, which should only be const if right is const. + elif not right_other_literal: + # Left is a literal, which should only be const if right is const. right = lift(right) left = lift(left, try_const=right.type.const) - elif not left_bool: - # Right is a bool, which should only be const if left is const. + elif not left_other_literal: + # Right is a literal, which should only be const if left is const. left = lift(left) right = lift(right, try_const=left.type.const) elif not right_int: diff --git a/qiskit/circuit/classical/types/__init__.py b/qiskit/circuit/classical/types/__init__.py index ae38a0d97fb..cd97c5cdc90 100644 --- a/qiskit/circuit/classical/types/__init__.py +++ b/qiskit/circuit/classical/types/__init__.py @@ -95,6 +95,7 @@ __all__ = [ "Type", "Bool", + "Float", "Uint", "Ordering", "order", @@ -105,5 +106,5 @@ "cast_kind", ] -from .types import Type, Bool, Uint +from .types import Type, Bool, Float, Uint 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 c42cfd23bec..4bebcf50e6e 100644 --- a/qiskit/circuit/classical/types/ordering.py +++ b/qiskit/circuit/classical/types/ordering.py @@ -26,7 +26,7 @@ import enum -from .types import Type, Bool, Uint +from .types import Type, Bool, Float, Uint # While the type system is simple, it's overkill to represent the complete partial ordering graph of @@ -55,10 +55,6 @@ def __repr__(self): return str(self) -def _order_bool_bool(_a: Bool, _b: Bool, /) -> Ordering: - return Ordering.EQUAL - - def _order_uint_uint(left: Uint, right: Uint, /) -> Ordering: if left.width < right.width: return Ordering.LESS @@ -68,8 +64,9 @@ def _order_uint_uint(left: Uint, right: Uint, /) -> Ordering: _ORDERERS = { - (Bool, Bool): _order_bool_bool, + (Bool, Bool): lambda _a, _b, /: Ordering.EQUAL, (Uint, Uint): _order_uint_uint, + (Float, Float): lambda _a, _b, /: Ordering.EQUAL, } @@ -224,8 +221,12 @@ def _uint_cast(from_: Uint, to_: Uint, /) -> CastKind: _ALLOWED_CASTS = { (Bool, Bool): lambda _a, _b, /: CastKind.EQUAL, (Bool, Uint): lambda _a, _b, /: CastKind.LOSSLESS, + (Bool, Float): lambda _a, _b, /: CastKind.LOSSLESS, (Uint, Bool): lambda _a, _b, /: CastKind.IMPLICIT, (Uint, Uint): _uint_cast, + (Uint, Float): lambda _a, _b, /: CastKind.DANGEROUS, + (Float, Float): lambda _a, _b, /: CastKind.EQUAL, + (Float, Uint): lambda _a, _b, /: CastKind.DANGEROUS, } diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py index 81bd80bc33f..49077246fa1 100644 --- a/qiskit/circuit/classical/types/types.py +++ b/qiskit/circuit/classical/types/types.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__ = ["Type", "Bool", "Uint"] +__all__ = ["Type", "Bool", "Float", "Uint"] import typing @@ -102,3 +102,24 @@ def __hash__(self): def __eq__(self, other): return isinstance(other, Uint) and self.const == other.const and self.width == other.width + + +@typing.final +class Float(Type): + """A floating point number of unspecified width. + In the future, this may also be used to represent a fixed-width float. + """ + + __slots__ = ("const",) + + def __init__(self, *, const: bool = False): + super(Type, self).__setattr__("const", const) + + def __repr__(self): + return f"Float(const={self.const})" + + def __hash__(self): + return hash((self.__class__, self.const)) + + def __eq__(self, other): + return isinstance(other, Float) and self.const == other.const From 23b5961ae91a967b6717fa26d3166672178b45e9 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Wed, 12 Feb 2025 14:47:22 -0500 Subject: [PATCH 24/53] Allow DANGEROUS conversion from Float to Bool. I wasn't going to have this, but since we have DANGEROUS Float => Int, and we have Int => Bool, I think this makes the most sense. --- qiskit/circuit/classical/types/ordering.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qiskit/circuit/classical/types/ordering.py b/qiskit/circuit/classical/types/ordering.py index 4bebcf50e6e..6314ca8f668 100644 --- a/qiskit/circuit/classical/types/ordering.py +++ b/qiskit/circuit/classical/types/ordering.py @@ -227,6 +227,7 @@ def _uint_cast(from_: Uint, to_: Uint, /) -> CastKind: (Uint, Float): lambda _a, _b, /: CastKind.DANGEROUS, (Float, Float): lambda _a, _b, /: CastKind.EQUAL, (Float, Uint): lambda _a, _b, /: CastKind.DANGEROUS, + (Float, Bool): lambda _a, _b, /: CastKind.DANGEROUS, } From 8bf2e4fd971ed0b69cc485f971a6dd883e01d39f Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Wed, 12 Feb 2025 14:48:45 -0500 Subject: [PATCH 25/53] Test Float ordering. --- .../circuit/classical/test_types_ordering.py | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/test/python/circuit/classical/test_types_ordering.py b/test/python/circuit/classical/test_types_ordering.py index 4179d3c11da..0bbf0ee41e4 100644 --- a/test/python/circuit/classical/test_types_ordering.py +++ b/test/python/circuit/classical/test_types_ordering.py @@ -49,8 +49,19 @@ def test_order(self): types.order(types.Bool(const=True), types.Bool(const=True)), types.Ordering.EQUAL ) + self.assertIs(types.order(types.Float(), types.Float()), types.Ordering.EQUAL) + self.assertIs(types.order(types.Float(const=True), types.Float()), types.Ordering.LESS) + self.assertIs(types.order(types.Float(), types.Float(const=True)), types.Ordering.GREATER) + self.assertIs( + types.order(types.Float(const=True), types.Float(const=True)), types.Ordering.EQUAL + ) + self.assertIs(types.order(types.Bool(), types.Uint(8)), types.Ordering.NONE) + self.assertIs(types.order(types.Bool(), types.Float()), types.Ordering.NONE) self.assertIs(types.order(types.Uint(8), types.Bool()), types.Ordering.NONE) + self.assertIs(types.order(types.Uint(8), types.Float()), types.Ordering.NONE) + self.assertIs(types.order(types.Float(), types.Uint(8)), types.Ordering.NONE) + self.assertIs(types.order(types.Float(), types.Bool()), types.Ordering.NONE) def test_is_subtype(self): self.assertTrue(types.is_subtype(types.Uint(8), types.Uint(16))) @@ -66,8 +77,16 @@ def test_is_subtype(self): self.assertFalse(types.is_subtype(types.Bool(), types.Bool(), strict=True)) self.assertTrue(types.is_subtype(types.Bool(const=True), types.Bool(), strict=True)) + self.assertTrue(types.is_subtype(types.Float(), types.Float())) + self.assertFalse(types.is_subtype(types.Float(), types.Float(), strict=True)) + self.assertTrue(types.is_subtype(types.Float(const=True), types.Float(), strict=True)) + self.assertFalse(types.is_subtype(types.Bool(), types.Uint(8))) + self.assertFalse(types.is_subtype(types.Bool(), types.Float())) self.assertFalse(types.is_subtype(types.Uint(8), types.Bool())) + self.assertFalse(types.is_subtype(types.Uint(8), types.Float())) + self.assertFalse(types.is_subtype(types.Float(), types.Uint(8))) + self.assertFalse(types.is_subtype(types.Float(), types.Bool())) def test_is_supertype(self): self.assertFalse(types.is_supertype(types.Uint(8), types.Uint(16))) @@ -84,8 +103,16 @@ def test_is_supertype(self): self.assertFalse(types.is_supertype(types.Bool(), types.Bool(), strict=True)) self.assertTrue(types.is_supertype(types.Bool(), types.Bool(const=True), strict=True)) + self.assertTrue(types.is_supertype(types.Float(), types.Float())) + self.assertFalse(types.is_supertype(types.Float(), types.Float(), strict=True)) + self.assertTrue(types.is_supertype(types.Float(), types.Float(const=True), strict=True)) + self.assertFalse(types.is_supertype(types.Bool(), types.Uint(8))) + self.assertFalse(types.is_supertype(types.Bool(), types.Float())) self.assertFalse(types.is_supertype(types.Uint(8), types.Bool())) + self.assertFalse(types.is_supertype(types.Uint(8), types.Float())) + self.assertFalse(types.is_supertype(types.Float(), types.Uint(8))) + self.assertFalse(types.is_supertype(types.Float(), types.Bool())) def test_greater(self): self.assertEqual(types.greater(types.Uint(16), types.Uint(8)), types.Uint(16)) @@ -105,6 +132,12 @@ def test_greater(self): self.assertEqual( types.greater(types.Bool(const=True), types.Bool(const=True)), types.Bool(const=True) ) + self.assertEqual(types.greater(types.Float(), types.Float()), types.Float()) + self.assertEqual(types.greater(types.Float(const=True), types.Float()), types.Float()) + self.assertEqual(types.greater(types.Float(), types.Float(const=True)), types.Float()) + self.assertEqual( + types.greater(types.Float(const=True), types.Float(const=True)), types.Float(const=True) + ) with self.assertRaisesRegex(TypeError, "no ordering"): types.greater(types.Bool(), types.Uint(8)) with self.assertRaisesRegex(TypeError, "no ordering"): @@ -125,6 +158,14 @@ def test_basic_examples(self): types.cast_kind(types.Bool(const=True), types.Bool()), types.CastKind.IMPLICIT ) self.assertIs(types.cast_kind(types.Bool(), types.Bool(const=True)), types.CastKind.NONE) + self.assertIs(types.cast_kind(types.Float(), types.Float()), types.CastKind.EQUAL) + self.assertIs( + types.cast_kind(types.Float(const=True), types.Float(const=True)), types.CastKind.EQUAL + ) + self.assertIs( + types.cast_kind(types.Float(const=True), types.Float()), types.CastKind.IMPLICIT + ) + self.assertIs(types.cast_kind(types.Float(), types.Float(const=True)), types.CastKind.NONE) self.assertIs(types.cast_kind(types.Uint(8), types.Bool()), types.CastKind.IMPLICIT) self.assertIs( types.cast_kind(types.Uint(8, const=True), types.Bool(const=True)), @@ -134,6 +175,16 @@ def test_basic_examples(self): types.cast_kind(types.Uint(8, const=True), types.Bool()), types.CastKind.IMPLICIT ) self.assertIs(types.cast_kind(types.Uint(8), types.Bool(const=True)), types.CastKind.NONE) + self.assertIs(types.cast_kind(types.Float(), types.Bool()), types.CastKind.DANGEROUS) + self.assertIs( + types.cast_kind(types.Float(const=True), types.Bool(const=True)), + types.CastKind.DANGEROUS, + ) + self.assertIs( + types.cast_kind(types.Float(const=True), types.Bool()), types.CastKind.DANGEROUS + ) + self.assertIs(types.cast_kind(types.Float(), types.Bool(const=True)), types.CastKind.NONE) + self.assertIs(types.cast_kind(types.Bool(), types.Uint(8)), types.CastKind.LOSSLESS) self.assertIs( types.cast_kind(types.Bool(const=True), types.Uint(8, const=True)), @@ -143,6 +194,21 @@ def test_basic_examples(self): types.cast_kind(types.Bool(const=True), types.Uint(8)), types.CastKind.LOSSLESS ) self.assertIs(types.cast_kind(types.Bool(), types.Uint(8, const=True)), types.CastKind.NONE) + self.assertIs(types.cast_kind(types.Uint(16), types.Uint(8)), types.CastKind.DANGEROUS) + self.assertIs( + types.cast_kind(types.Uint(16, const=True), types.Uint(8, const=True)), + types.CastKind.DANGEROUS, + ) + self.assertIs(types.cast_kind(types.Bool(), types.Float()), types.CastKind.LOSSLESS) + self.assertIs( + types.cast_kind(types.Bool(const=True), types.Float(const=True)), + types.CastKind.LOSSLESS, + ) + self.assertIs( + types.cast_kind(types.Bool(const=True), types.Float()), types.CastKind.LOSSLESS + ) + self.assertIs(types.cast_kind(types.Bool(), types.Float(const=True)), types.CastKind.NONE) + self.assertIs(types.cast_kind(types.Uint(16), types.Uint(8)), types.CastKind.DANGEROUS) self.assertIs( types.cast_kind(types.Uint(16, const=True), types.Uint(8, const=True)), @@ -154,3 +220,23 @@ def test_basic_examples(self): self.assertIs( types.cast_kind(types.Uint(16), types.Uint(8, const=True)), types.CastKind.NONE ) + self.assertIs(types.cast_kind(types.Uint(16), types.Float()), types.CastKind.DANGEROUS) + self.assertIs( + types.cast_kind(types.Uint(16, const=True), types.Float(const=True)), + types.CastKind.DANGEROUS, + ) + self.assertIs( + types.cast_kind(types.Uint(16, const=True), types.Float()), types.CastKind.DANGEROUS + ) + self.assertIs(types.cast_kind(types.Uint(16), types.Float(const=True)), types.CastKind.NONE) + self.assertIs(types.cast_kind(types.Float(), types.Uint(16)), types.CastKind.DANGEROUS) + self.assertIs( + types.cast_kind(types.Float(const=True), types.Uint(16, const=True)), + types.CastKind.DANGEROUS, + ) + self.assertIs( + types.cast_kind(types.Float(const=True), types.Uint(16)), types.CastKind.DANGEROUS + ) + self.assertIs( + types.cast_kind(types.Float(), types.Uint(16, const=True)), types.CastKind.NONE + ) From 111eb3250665197666b55dd0d3844d121536a249 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Wed, 12 Feb 2025 15:54:41 -0500 Subject: [PATCH 26/53] Improve error messages for using Float with logical operators. --- qiskit/circuit/classical/expr/constructors.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index 5345a939f50..12104c59834 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -239,8 +239,11 @@ def logic_not(operand: typing.Any, /) -> Expr: Bool(const=False)) """ operand = lift(operand) - operand = _coerce_lossless(operand, types.Bool(const=operand.type.const)) - return Unary(Unary.Op.LOGIC_NOT, operand, operand.type) + try: + operand = _coerce_lossless(operand, types.Bool(const=operand.type.const)) + return Unary(Unary.Op.LOGIC_NOT, operand, operand.type) + except TypeError: + raise TypeError(f"cannot apply '{Unary.Op.BIT_NOT}' to type '{operand.type}'") def _lift_binary_operands(left: typing.Any, right: typing.Any) -> tuple[Expr, Expr]: @@ -384,9 +387,12 @@ def _binary_logical(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: left = lift(left) right = lift(right) type = types.Bool(const=(left.type.const and right.type.const)) - left = _coerce_lossless(left, type) - right = _coerce_lossless(right, type) - return Binary(op, left, right, type) + try: + left = _coerce_lossless(left, type) + right = _coerce_lossless(right, type) + return Binary(op, left, right, type) + except TypeError: + raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") def logic_and(left: typing.Any, right: typing.Any, /) -> Expr: From a839d5157d498045ad5f83ea16292a6bca1f996d Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Wed, 12 Feb 2025 15:55:07 -0500 Subject: [PATCH 27/53] Float tests for constructors. --- .../classical/test_expr_constructors.py | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/test/python/circuit/classical/test_expr_constructors.py b/test/python/circuit/classical/test_expr_constructors.py index bfcfef45c1e..4d664bacae3 100644 --- a/test/python/circuit/classical/test_expr_constructors.py +++ b/test/python/circuit/classical/test_expr_constructors.py @@ -91,6 +91,8 @@ def test_value_lifts_python_builtins(self): ) self.assertEqual(expr.lift(7), expr.Value(7, types.Uint(3))) self.assertEqual(expr.lift(7, try_const=True), expr.Value(7, types.Uint(3, const=True))) + self.assertEqual(expr.lift(7.0), expr.Value(7.0, types.Float())) + self.assertEqual(expr.lift(7.0, try_const=True), expr.Value(7.0, types.Float(const=True))) def test_value_ensures_nonzero_width(self): self.assertEqual(expr.lift(0), expr.Value(0, types.Uint(1))) @@ -112,6 +114,8 @@ def test_value_type_representation(self): def test_value_does_not_allow_downcast(self): with self.assertRaisesRegex(TypeError, "the explicit type .* is not suitable"): expr.lift(0xFF, types.Uint(2)) + with self.assertRaisesRegex(TypeError, "the explicit type .* is not suitable"): + expr.lift(1.1, types.Uint(2)) def test_value_rejects_bad_values(self): with self.assertRaisesRegex(TypeError, "failed to infer a type"): @@ -142,6 +146,9 @@ def test_cast_allows_lossy_downcasting(self): self.assertEqual( expr.cast(base, types.Bool()), expr.Cast(base, types.Bool(), implicit=False) ) + self.assertEqual( + expr.cast(base, types.Float()), expr.Cast(base, types.Float(), implicit=False) + ) @ddt.data( (expr.bit_not, ClassicalRegister(3)), @@ -175,6 +182,11 @@ def test_bit_not_explicit(self): ), ) + @ddt.data(expr.bit_not) + def test_urnary_bitwise_forbidden(self, function): + with self.assertRaisesRegex(TypeError, "cannot apply"): + function(7.0) + def test_logic_not_explicit(self): cr = ClassicalRegister(3) self.assertEqual( @@ -201,6 +213,11 @@ def test_logic_not_explicit(self): ), ) + @ddt.data(expr.logic_not) + def test_urnary_logical_forbidden(self, function): + with self.assertRaisesRegex(TypeError, "cannot apply"): + function(7.0) + @ddt.data( (expr.bit_and, ClassicalRegister(3), ClassicalRegister(3)), (expr.bit_or, ClassicalRegister(3), ClassicalRegister(3)), @@ -213,6 +230,12 @@ def test_logic_not_explicit(self): (expr.less_equal, ClassicalRegister(3), 5), (expr.greater, 4, ClassicalRegister(3)), (expr.greater_equal, ClassicalRegister(3), 5), + (expr.equal, 8.0, 255.0), + (expr.not_equal, 8.0, 255.0), + (expr.less, 3.0, 6.0), + (expr.less_equal, 3.0, 5.0), + (expr.greater, 4.0, 3.0), + (expr.greater_equal, 3.0, 5.0), ) @ddt.unpack def test_binary_functions_lift_scalars(self, function, left, right): @@ -230,6 +253,12 @@ def test_binary_functions_lift_scalars(self, function, left, right): (expr.less_equal, 3, 3), (expr.greater, 254, 255), (expr.greater_equal, 4, 5), + (expr.equal, 254.0, 255.0), + (expr.not_equal, 255.0, 255.0), + (expr.less, 5.0, 4.0), + (expr.less_equal, 3.0, 3.0), + (expr.greater, 254.0, 255.0), + (expr.greater_equal, 4.0, 5.0), ) @ddt.unpack def test_binary_functions_lift_scalars_const(self, function, left, right): @@ -358,6 +387,12 @@ def test_binary_bitwise_uint_inference(self, function, opcode): def test_binary_bitwise_forbidden(self, function): with self.assertRaisesRegex(TypeError, "invalid types"): function(ClassicalRegister(3, "c"), Clbit()) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(3.0, 3.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(3, 3.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(3.0, 3) # Unlike most other functions, the bitwise functions should error if the two bit-like types # aren't of the same width, except for the special inference for integer literals. with self.assertRaisesRegex(TypeError, "binary bitwise operations .* same width"): @@ -423,6 +458,15 @@ def test_binary_logical_explicit(self, function, opcode): ), ) + @ddt.data(expr.logic_and, expr.logic_or) + def test_binary_logic_forbidden(self, function): + with self.assertRaisesRegex(TypeError, "invalid types"): + function(3.0, 3.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(3, 3.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(3.0, 3) + @ddt.data( (expr.equal, expr.Binary.Op.EQUAL), (expr.not_equal, expr.Binary.Op.NOT_EQUAL), @@ -480,6 +524,16 @@ def test_binary_equal_explicit(self, function, opcode): ), ) + self.assertEqual( + function(expr.lift(7.0, try_const=True), 7.0), + expr.Binary( + opcode, + expr.Value(7.0, types.Float(const=True)), + expr.Value(7.0, types.Float(const=True)), + types.Bool(const=True), + ), + ) + @ddt.data(expr.equal, expr.not_equal) def test_binary_equal_forbidden(self, function): with self.assertRaisesRegex(TypeError, "invalid types"): @@ -488,6 +542,12 @@ def test_binary_equal_forbidden(self, function): function(ClassicalRegister(3, "c"), False) with self.assertRaisesRegex(TypeError, "invalid types"): function(5, True) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(True, 5.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(5, 5.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(ClassicalRegister(3, "c"), 5.0) with self.assertRaisesRegex(TypeError, "invalid types"): # No order between a smaller non-const int and larger const. function(expr.lift(0xFF, types.Uint(8)), expr.lift(0xFFFF, types.Uint(16, const=True))) @@ -544,6 +604,16 @@ def test_binary_relation_explicit(self, function, opcode): ), ) + self.assertEqual( + function(expr.lift(12.0, types.Float(const=True)), expr.lift(12.0, try_const=True)), + expr.Binary( + opcode, + expr.Value(12.0, types.Float(const=True)), + expr.Value(12.0, types.Float(const=True)), + types.Bool(const=True), + ), + ) + @ddt.data(expr.less, expr.less_equal, expr.greater, expr.greater_equal) def test_binary_relation_forbidden(self, function): with self.assertRaisesRegex(TypeError, "invalid types"): @@ -552,6 +622,12 @@ def test_binary_relation_forbidden(self, function): function(ClassicalRegister(3, "c"), False) with self.assertRaisesRegex(TypeError, "invalid types"): function(Clbit(), Clbit()) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(True, 5.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(5, 5.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(ClassicalRegister(3, "c"), 5.0) with self.assertRaisesRegex(TypeError, "invalid types"): # No order between a smaller non-const int and larger const. function(expr.lift(0xFF, types.Uint(8)), expr.lift(0xFFFF, types.Uint(16, const=True))) @@ -592,6 +668,10 @@ def test_index_forbidden(self): expr.index(Clbit(), 3) with self.assertRaisesRegex(TypeError, "invalid types"): expr.index(ClassicalRegister(3, "a"), False) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.index(ClassicalRegister(3, "a"), 1.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.index(0xFFFF, 1.0) @ddt.data( (expr.shift_left, expr.Binary.Op.SHIFT_LEFT), @@ -668,3 +748,7 @@ def test_shift_forbidden(self, function): function(ClassicalRegister(3, "c"), False) with self.assertRaisesRegex(TypeError, "invalid types"): function(Clbit(), Clbit()) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(0xFFFF, 2.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(255.0, 1) From a19b39a56d687817121d8f4d723b80dd9f335968 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Wed, 12 Feb 2025 16:08:51 -0500 Subject: [PATCH 28/53] Add release note. --- .../notes/float-expr-02b01d9ea89ad47a.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml diff --git a/releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml b/releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml new file mode 100644 index 00000000000..23c1117e07f --- /dev/null +++ b/releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml @@ -0,0 +1,19 @@ +--- +features_circuits: + - | + The classical realtime-expressions module :mod:`qiskit.circuit.classical` can now represent + floating point values of unspecified width, using the new type :class:`~.types.Float`. + + The :func:`~.expr.lift` function can be used to create a value expression from a Python + float:: + + from qiskit.circuit.classical import expr + + expr.lift(5.0) + # >>> Value(5.0, Float(const=False)) + expr.lift(5.0, try_const=True) + # >>> Value(5.0, Float(const=True)) + + This type is intended primarily for use in timing-related (duration and stretch) + expressions. It is not compatible with bitwise or logical operations, though it + can be used (dangerously) with these if first explicitly cast to something else. From 25508bff2016b58408b590500779135a937df30b Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Mon, 17 Feb 2025 16:27:54 -0500 Subject: [PATCH 29/53] Reject const vars in add_var and add_input. Also removes the assumption that a const-type can never be an l-value in favor of just restricting l-values with const types from being added to circuits for now. We will (in a separate PR) add support for adding stretch variables to circuits, which are const. However, we may track those differently, or at least not report them as variable when users query the circuit for variables. --- qiskit/circuit/classical/expr/visitors.py | 4 -- qiskit/circuit/quantumcircuit.py | 45 +++++++++++-------- test/python/circuit/test_circuit_vars.py | 53 +++++++++++++++++++++++ test/python/circuit/test_store.py | 13 ++++-- 4 files changed, 90 insertions(+), 25 deletions(-) diff --git a/qiskit/circuit/classical/expr/visitors.py b/qiskit/circuit/classical/expr/visitors.py index eeb1d775fa5..4d348fb848a 100644 --- a/qiskit/circuit/classical/expr/visitors.py +++ b/qiskit/circuit/classical/expr/visitors.py @@ -299,8 +299,4 @@ def is_lvalue(node: expr.Expr, /) -> bool: >>> expr.is_lvalue(expr.bit_and(a, b)) False """ - if node.type.const: - # If the expression's resolution type is const, then this can never be - # an l-value (even if the expression is a Var). - return False return node.accept(_IS_LVALUE) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 7ab1bae5d8c..0e1a9c157d3 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -2843,13 +2843,14 @@ def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.V 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. + a non-const-typed :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. + the initializer to the desired type. If a const-typed expression is provided, it + will be automatically cast to its non-const counterpart. This must be either a :class:`~.expr.Expr` node, or a value that can be lifted to one using :class:`.expr.lift`. @@ -2859,7 +2860,8 @@ def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.V object will be returned. Raises: - CircuitError: if the variable cannot be created due to shadowing an existing variable. + CircuitError: if the variable cannot be created due to shadowing an existing variable + or a const variable was specified for ``name_or_var``. Examples: Define a new variable given just a name and an initializer expression:: @@ -2900,17 +2902,18 @@ def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.V # Validate the initializer first to catch cases where the variable to be declared is being # used in the initializer. circuit_scope = self._current_scope() - # Convenience method to widen Python integer literals to the right width during the initial - # lift, if the type is already known via the variable. - if ( - isinstance(name_or_var, expr.Var) - and name_or_var.type.kind is types.Uint - and isinstance(initial, int) - and not isinstance(initial, bool) - ): - coerce_type = name_or_var.type - else: - coerce_type = None + coerce_type = None + if isinstance(name_or_var, expr.Var): + if name_or_var.type.const: + raise CircuitError("const variables are not supported.") + if ( + name_or_var.type.kind is types.Uint + and isinstance(initial, int) + and not isinstance(initial, bool) + ): + # Convenience method to widen Python integer literals to the right width during the initial + # lift, if the type is already known via the variable. + coerce_type = name_or_var.type initial = _validate_expr(circuit_scope, expr.lift(initial, coerce_type)) if isinstance(name_or_var, str): var = expr.Var.new(name_or_var, initial.type) @@ -2961,6 +2964,8 @@ def add_uninitialized_var(self, var: expr.Var, /): raise CircuitError("cannot add an uninitialized variable in a control-flow scope") if not var.standalone: raise CircuitError("cannot add a variable wrapping a bit or register to a circuit") + if var.type.const: + raise CircuitError("const variables are not supported.") self._builder_api.add_uninitialized_var(var) def add_capture(self, var: expr.Var): @@ -3023,8 +3028,14 @@ def add_input( # pylint: disable=missing-raises-doc raise CircuitError("cannot add an input variable in a control-flow scope") 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") + if isinstance(name_or_var, expr.Var): + if type_ is not None: + raise ValueError("cannot give an explicit type with an existing Var") + if name_or_var.type.const: + raise CircuitError("const variables are not supported") + elif type_ is not None and type_.const: + raise CircuitError("const variables are not supported") + var = self._prepare_new_var(name_or_var, type_) self._vars_input[var.name] = var return var diff --git a/test/python/circuit/test_circuit_vars.py b/test/python/circuit/test_circuit_vars.py index f6916dcb72d..aa761098128 100644 --- a/test/python/circuit/test_circuit_vars.py +++ b/test/python/circuit/test_circuit_vars.py @@ -92,6 +92,12 @@ def test_initialise_declarations_dependencies(self): ] self.assertEqual(operations, [("store", lvalue, rvalue) for lvalue, rvalue in vars_]) + def test_initialise_declarations_rejects_const_vars(self): + a = expr.Var.new("a", types.Uint(16, const=True)) + a_init = expr.lift(12, try_const=True) + with self.assertRaisesRegex(CircuitError, "const variables.*not supported"): + QuantumCircuit(declarations=[(a, a_init)]) + def test_initialise_inputs_declarations(self): a = expr.Var.new("a", types.Uint(16)) b = expr.Var.new("b", types.Uint(16)) @@ -111,6 +117,11 @@ def test_initialise_inputs_declarations(self): ] self.assertEqual(operations, [("store", b, b_init)]) + def test_initialise_inputs_declarations_rejects_const_vars(self): + a = expr.Var.new("a", types.Uint(16, const=True)) + with self.assertRaisesRegex(CircuitError, "const variables.*not supported"): + QuantumCircuit(inputs=[a]) + def test_initialise_captures_declarations(self): a = expr.Var.new("a", types.Uint(16)) b = expr.Var.new("b", types.Uint(16)) @@ -137,6 +148,12 @@ def test_add_uninitialized_var(self): self.assertEqual({a}, set(qc.iter_vars())) self.assertEqual([], list(qc.data)) + def test_add_uninitialized_var_rejects_const_lvalue(self): + a = expr.Var.new("a", types.Bool(const=True)) + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "const variables.*not supported"): + qc.add_uninitialized_var(a) + def test_add_var_returns_good_var(self): qc = QuantumCircuit() a = qc.add_var("a", expr.lift(True)) @@ -154,6 +171,34 @@ def test_add_var_returns_input(self): a_other = qc.add_var(a, expr.lift(True)) self.assertIs(a, a_other) + def test_add_var_rejects_const_lvalue(self): + a = expr.Var.new("a", types.Bool(const=True)) + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "const variables.*not supported"): + qc.add_var(a, True) + + def test_add_var_implicitly_casts_const_rvalue(self): + a = expr.Var.new("a", types.Bool()) + qc = QuantumCircuit() + qc.add_var(a, expr.lift(True, try_const=True)) + self.assertEqual(qc.num_vars, 1) + operations = [ + (instruction.operation.name, instruction.operation.lvalue, instruction.operation.rvalue) + for instruction in qc.data + ] + self.assertEqual( + operations, + [ + ( + "store", + a, + expr.Cast( + expr.Value(True, types.Bool(const=True)), types.Bool(), implicit=True + ), + ) + ], + ) + def test_add_input_returns_good_var(self): qc = QuantumCircuit() a = qc.add_input("a", types.Bool()) @@ -171,6 +216,14 @@ def test_add_input_returns_input(self): a_other = qc.add_input(a) self.assertIs(a, a_other) + def test_add_input_rejects_const_var(self): + a = expr.Var.new("a", types.Bool(const=True)) + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "const variables.*not supported"): + qc.add_input(a) + with self.assertRaisesRegex(CircuitError, "const variables.*not supported"): + qc.add_input("a", types.Bool(const=True)) + def test_cannot_have_both_inputs_and_captures(self): a = expr.Var.new("a", types.Bool()) b = expr.Var.new("b", types.Bool()) diff --git a/test/python/circuit/test_store.py b/test/python/circuit/test_store.py index f79fb5c6926..2af618e0ddd 100644 --- a/test/python/circuit/test_store.py +++ b/test/python/circuit/test_store.py @@ -235,13 +235,18 @@ def test_rejects_cregs_not_in_circuit(self): def test_rejects_non_lvalue(self): a = expr.Var.new("a", types.Bool()) b = expr.Var.new("b", types.Bool()) - c = expr.Var.new("c", types.Bool(const=True)) - qc = QuantumCircuit(inputs=[a, b, c]) + 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)) - with self.assertRaisesRegex(CircuitError, "not an l-value"): - qc.store(c, expr.lift(False)) + + def test_rejects_const_lvalue(self): + """At least for now, lvalue expressions with a const type are not + permitted in calls to qc.store.""" + a = expr.Var.new("a", types.Bool(const=True)) + qc = QuantumCircuit() + with self.assertRaisesRegex(CircuitError, "const variables.*not supported"): + qc.store(a, expr.lift(False, try_const=True)) def test_rejects_explicit_cast(self): lvalue = expr.Var.new("a", types.Uint(16)) From ccf9441f425284bdfd7c10fa8881b7165fd90d58 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Mon, 17 Feb 2025 19:12:50 -0500 Subject: [PATCH 30/53] Implement QPY support for const-typed expressions. --- qiskit/qpy/__init__.py | 7 ++ qiskit/qpy/binary_io/circuits.py | 4 +- qiskit/qpy/binary_io/value.py | 112 ++++++++++++++++++++++++------- qiskit/qpy/common.py | 2 +- qiskit/qpy/formats.py | 9 ++- 5 files changed, 105 insertions(+), 29 deletions(-) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 60922f3d3ec..33fe69c35c5 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -370,6 +370,13 @@ def open(*args): by ``num_circuits`` in the file header). There is no padding between the circuits in the data. +.. _qpy_version_14: + +Version 14 +---------- + +Version 14 added support for constant types to classical expressions. + .. _qpy_version_13: Version 13 diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 174acceb59e..3561a181c9c 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -1257,7 +1257,7 @@ def write_circuit( file_obj.write(metadata_raw) # Write header payload file_obj.write(registers_raw) - standalone_var_indices = value.write_standalone_vars(file_obj, circuit) + standalone_var_indices = value.write_standalone_vars(file_obj, circuit, version) else: if circuit.num_vars: raise exceptions.UnsupportedFeatureForVersion( @@ -1425,7 +1425,7 @@ def read_circuit(file_obj, version, metadata_deserializer=None, use_symengine=Fa "q": [Qubit() for _ in out_bits["q"]], "c": [Clbit() for _ in out_bits["c"]], } - var_segments, standalone_var_indices = value.read_standalone_vars(file_obj, num_vars) + var_segments, standalone_var_indices = value.read_standalone_vars(file_obj, num_vars, version) circ = QuantumCircuit( out_bits["q"], out_bits["c"], diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 3bab629421d..44e0e83eba8 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -261,12 +261,18 @@ def __init__(self, file_obj, clbit_indices, standalone_var_indices, version): self.standalone_var_indices = standalone_var_indices self.version = version + def write_expr_type(self, type_: types.Type): + if self.version < 14: + _write_expr_type(self.file_obj, type_, self.version) + else: + _write_expr_type_v14(self.file_obj, type_) + def visit_generic(self, node, /): raise exceptions.QpyError(f"unhandled Expr object '{node}'") def visit_var(self, node, /): self.file_obj.write(type_keys.Expression.VAR) - _write_expr_type(self.file_obj, node.type) + self.write_expr_type(node.type) if node.standalone: self.file_obj.write(type_keys.ExprVar.UUID) self.file_obj.write( @@ -296,7 +302,7 @@ def visit_var(self, node, /): def visit_value(self, node, /): self.file_obj.write(type_keys.Expression.VALUE) - _write_expr_type(self.file_obj, node.type) + self.write_expr_type(node.type) if node.value is True or node.value is False: self.file_obj.write(type_keys.ExprValue.BOOL) self.file_obj.write( @@ -322,7 +328,7 @@ def visit_value(self, node, /): def visit_cast(self, node, /): self.file_obj.write(type_keys.Expression.CAST) - _write_expr_type(self.file_obj, node.type) + self.write_expr_type(node.type) self.file_obj.write( struct.pack(formats.EXPRESSION_CAST_PACK, *formats.EXPRESSION_CAST(node.implicit)) ) @@ -330,7 +336,7 @@ def visit_cast(self, node, /): def visit_unary(self, node, /): self.file_obj.write(type_keys.Expression.UNARY) - _write_expr_type(self.file_obj, node.type) + self.write_expr_type(node.type) self.file_obj.write( struct.pack(formats.EXPRESSION_UNARY_PACK, *formats.EXPRESSION_UNARY(node.op.value)) ) @@ -338,7 +344,7 @@ def visit_unary(self, node, /): def visit_binary(self, node, /): self.file_obj.write(type_keys.Expression.BINARY) - _write_expr_type(self.file_obj, node.type) + self.write_expr_type(node.type) self.file_obj.write( struct.pack(formats.EXPRESSION_BINARY_PACK, *formats.EXPRESSION_BINARY(node.op.value)) ) @@ -351,7 +357,7 @@ def visit_index(self, node, /): "the 'Index' expression", required=12, target=self.version ) self.file_obj.write(type_keys.Expression.INDEX) - _write_expr_type(self.file_obj, node.type) + self.write_expr_type(node.type) node.target.accept(self) node.index.accept(self) @@ -366,11 +372,14 @@ def _write_expr( node.accept(_ExprWriter(file_obj, clbit_indices, standalone_var_indices, version)) -def _write_expr_type(file_obj, type_: types.Type): - # Currently, QPY doesn't support const types - if type_.kind is types.Bool and not type_.const: +def _write_expr_type(file_obj, type_: types.Type, version): + if type_.const: + raise exceptions.UnsupportedFeatureForVersion( + "const-typed expressions", required=14, target=version + ) + if type_.kind is types.Bool: file_obj.write(type_keys.ExprType.BOOL) - elif type_.kind is types.Uint and not type_.const: + elif type_.kind is types.Uint: file_obj.write(type_keys.ExprType.UINT) file_obj.write( struct.pack(formats.EXPR_TYPE_UINT_PACK, *formats.EXPR_TYPE_UINT(type_.width)) @@ -379,6 +388,25 @@ def _write_expr_type(file_obj, type_: types.Type): raise exceptions.QpyError(f"unhandled Type object '{type_};") +def _write_expr_type_v14(file_obj, type_: types.Type): + if type_.kind is types.Bool: + file_obj.write(type_keys.ExprType.BOOL) + file_obj.write( + struct.pack(formats.EXPR_TYPE_BOOL_PACK_V14, *formats.EXPR_TYPE_BOOL_V14(type_.const)) + ) + elif type_.kind is types.Uint: + file_obj.write(type_keys.ExprType.UINT) + file_obj.write( + # TODO: make sure you're calling this correctly + struct.pack( + formats.EXPR_TYPE_UINT_PACK_V14, + *formats.EXPR_TYPE_UINT_V14(type_.width, type_.const), + ) + ) + else: + raise exceptions.QpyError(f"unhandled Type object '{type_};") + + def _read_parameter(file_obj): data = formats.PARAMETER( *struct.unpack(formats.PARAMETER_PACK, file_obj.read(formats.PARAMETER_SIZE)) @@ -636,10 +664,14 @@ def _read_expr( clbits: collections.abc.Sequence[Clbit], cregs: collections.abc.Mapping[str, ClassicalRegister], standalone_vars: collections.abc.Sequence[expr.Var], + version: int, ) -> expr.Expr: # pylint: disable=too-many-return-statements type_key = file_obj.read(formats.EXPRESSION_DISCRIMINATOR_SIZE) - type_ = _read_expr_type(file_obj) + if version < 14: + type_ = _read_expr_type(file_obj) + else: + type_ = _read_expr_type_v14(file_obj) if type_key == type_keys.Expression.VAR: var_type_key = file_obj.read(formats.EXPR_VAR_DISCRIMINATOR_SIZE) if var_type_key == type_keys.ExprVar.UUID: @@ -687,7 +719,9 @@ def _read_expr( struct.unpack(formats.EXPRESSION_CAST_PACK, file_obj.read(formats.EXPRESSION_CAST_SIZE)) ) return expr.Cast( - _read_expr(file_obj, clbits, cregs, standalone_vars), type_, implicit=payload.implicit + _read_expr(file_obj, clbits, cregs, standalone_vars, version), + type_, + implicit=payload.implicit, ) if type_key == type_keys.Expression.UNARY: payload = formats.EXPRESSION_UNARY._make( @@ -697,7 +731,7 @@ def _read_expr( ) return expr.Unary( expr.Unary.Op(payload.opcode), - _read_expr(file_obj, clbits, cregs, standalone_vars), + _read_expr(file_obj, clbits, cregs, standalone_vars, version), type_, ) if type_key == type_keys.Expression.BINARY: @@ -708,14 +742,14 @@ def _read_expr( ) return expr.Binary( expr.Binary.Op(payload.opcode), - _read_expr(file_obj, clbits, cregs, standalone_vars), - _read_expr(file_obj, clbits, cregs, standalone_vars), + _read_expr(file_obj, clbits, cregs, standalone_vars, version), + _read_expr(file_obj, clbits, cregs, standalone_vars, version), type_, ) if type_key == type_keys.Expression.INDEX: return expr.Index( - _read_expr(file_obj, clbits, cregs, standalone_vars), - _read_expr(file_obj, clbits, cregs, standalone_vars), + _read_expr(file_obj, clbits, cregs, standalone_vars, version), + _read_expr(file_obj, clbits, cregs, standalone_vars, version), type_, ) raise exceptions.QpyError(f"Invalid classical-expression Expr key '{type_key}'") @@ -733,12 +767,32 @@ def _read_expr_type(file_obj) -> types.Type: raise exceptions.QpyError(f"Invalid classical-expression Type key '{type_key}'") -def read_standalone_vars(file_obj, num_vars): +def _read_expr_type_v14(file_obj) -> types.Type: + type_key = file_obj.read(formats.EXPR_TYPE_DISCRIMINATOR_SIZE) + if type_key == type_keys.ExprType.BOOL: + elem = formats.EXPR_TYPE_BOOL_V14._make( + struct.unpack( + formats.EXPR_TYPE_BOOL_PACK_V14, file_obj.read(formats.EXPR_TYPE_BOOL_SIZE_V14) + ) + ) + return types.Bool(const=elem.const) + if type_key == type_keys.ExprType.UINT: + elem = formats.EXPR_TYPE_UINT_V14._make( + struct.unpack( + formats.EXPR_TYPE_UINT_PACK_V14, file_obj.read(formats.EXPR_TYPE_UINT_SIZE_V14) + ) + ) + return types.Uint(elem.width, const=elem.const) + raise exceptions.QpyError(f"Invalid classical-expression Type key '{type_key}'") + + +def read_standalone_vars(file_obj, num_vars, version): """Read the ``num_vars`` standalone variable declarations from the file. Args: file_obj (File): a file-like object to read from. num_vars (int): the number of variables to read. + version (int): the target QPY version. Returns: tuple[dict, list]: the first item is a mapping of the ``ExprVarDeclaration`` type keys to @@ -758,7 +812,10 @@ def read_standalone_vars(file_obj, num_vars): file_obj.read(formats.EXPR_VAR_DECLARATION_SIZE), ) ) - type_ = _read_expr_type(file_obj) + if version < 14: + type_ = _read_expr_type(file_obj) + else: + type_ = _read_expr_type_v14(file_obj) name = file_obj.read(data.name_size).decode(common.ENCODE) var = expr.Var(uuid.UUID(bytes=data.uuid_bytes), type_, name=name) read_vars[data.usage].append(var) @@ -766,7 +823,7 @@ def read_standalone_vars(file_obj, num_vars): return read_vars, var_order -def _write_standalone_var(file_obj, var, type_key): +def _write_standalone_var(file_obj, var, type_key, version): name = var.name.encode(common.ENCODE) file_obj.write( struct.pack( @@ -774,16 +831,20 @@ def _write_standalone_var(file_obj, var, type_key): *formats.EXPR_VAR_DECLARATION(var.var.bytes, type_key, len(name)), ) ) - _write_expr_type(file_obj, var.type) + if version < 14: + _write_expr_type(file_obj, var.type, version) + else: + _write_expr_type_v14(file_obj, var.type) file_obj.write(name) -def write_standalone_vars(file_obj, circuit): +def write_standalone_vars(file_obj, circuit, version): """Write the standalone variables out from a circuit. Args: file_obj (File): the file-like object to write to. circuit (QuantumCircuit): the circuit to take the variables from. + version (int): the target QPY version. Returns: dict[expr.Var, int]: a mapping of the variables written to the index that they were written @@ -792,15 +853,15 @@ def write_standalone_vars(file_obj, circuit): index = 0 out = {} for var in circuit.iter_input_vars(): - _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.INPUT) + _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.INPUT, version) out[var] = index index += 1 for var in circuit.iter_captured_vars(): - _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.CAPTURE) + _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.CAPTURE, version) out[var] = index index += 1 for var in circuit.iter_declared_vars(): - _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.LOCAL) + _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.LOCAL, version) out[var] = index index += 1 return out @@ -979,6 +1040,7 @@ def loads_value( clbits=clbits, cregs=cregs or {}, standalone_vars=standalone_vars, + version=version, ) raise exceptions.QpyError(f"Serialization for {type_key} is not implemented in value I/O.") diff --git a/qiskit/qpy/common.py b/qiskit/qpy/common.py index c2585d5be4d..0f30e90cc34 100644 --- a/qiskit/qpy/common.py +++ b/qiskit/qpy/common.py @@ -25,7 +25,7 @@ from qiskit.qpy import formats, exceptions -QPY_VERSION = 13 +QPY_VERSION = 14 QPY_COMPATIBILITY_VERSION = 10 ENCODE = "utf8" diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py index 7696cae94e2..ed44604e783 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -360,6 +360,14 @@ # EXPR_TYPE +EXPR_TYPE_BOOL_V14 = namedtuple("EXPR_TYPE_BOOL_V14", ["const"]) +EXPR_TYPE_BOOL_PACK_V14 = "!?" +EXPR_TYPE_BOOL_SIZE_V14 = struct.calcsize(EXPR_TYPE_BOOL_PACK_V14) + +EXPR_TYPE_UINT_V14 = namedtuple("EXPR_TYPE_UINT_V14", ["width", "const"]) +EXPR_TYPE_UINT_PACK_V14 = "!L?" +EXPR_TYPE_UINT_SIZE_V14 = struct.calcsize(EXPR_TYPE_UINT_PACK_V14) + EXPR_TYPE_DISCRIMINATOR_SIZE = 1 EXPR_TYPE_BOOL = namedtuple("EXPR_TYPE_BOOL", []) @@ -370,7 +378,6 @@ EXPR_TYPE_UINT_PACK = "!L" EXPR_TYPE_UINT_SIZE = struct.calcsize(EXPR_TYPE_UINT_PACK) - # EXPR_VAR EXPR_VAR_DISCRIMINATOR_SIZE = 1 From c6eab02b0203f20915d910056032cd9ab49643a7 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Mon, 17 Feb 2025 20:06:08 -0500 Subject: [PATCH 31/53] Remove invalid test. This one I'd added thinking I ought to block store from using a const var target. But since I figured it's better to just restrict adding vars to the circuit that are const (and leave the decision of whether or not a const var can be an l-value till later), this test no longer makes sense. --- test/python/circuit/test_store.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/python/circuit/test_store.py b/test/python/circuit/test_store.py index 2af618e0ddd..5f4d7de6702 100644 --- a/test/python/circuit/test_store.py +++ b/test/python/circuit/test_store.py @@ -240,14 +240,6 @@ def test_rejects_non_lvalue(self): with self.assertRaisesRegex(CircuitError, "not an l-value"): qc.store(not_an_lvalue, expr.lift(False)) - def test_rejects_const_lvalue(self): - """At least for now, lvalue expressions with a const type are not - permitted in calls to qc.store.""" - a = expr.Var.new("a", types.Bool(const=True)) - qc = QuantumCircuit() - with self.assertRaisesRegex(CircuitError, "const variables.*not supported"): - qc.store(a, expr.lift(False, try_const=True)) - def test_rejects_explicit_cast(self): lvalue = expr.Var.new("a", types.Uint(16)) rvalue = expr.Var.new("b", types.Uint(8)) From edd780659451a57032441494331b582d1e26f133 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Mon, 17 Feb 2025 20:06:56 -0500 Subject: [PATCH 32/53] Update QPY version 14 desc. --- qiskit/qpy/__init__.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 33fe69c35c5..15c2ffc7c8d 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -375,7 +375,24 @@ def open(*args): Version 14 ---------- -Version 14 added support for constant types to classical expressions. +Version 14 adds support for additional :class:`~.types.Type` classes, and adds support for +const-ness to existing types :class:`~.types.Bool` and :class:`~.types.Uint`. +The ``EXPR_TYPE_BOOL`` and ``EXPR_TYPE_UNIT`` structs are now replaced by ``EXPR_TYPE_BOOL_V14`` +and ``EXPR_TYPE_UINT_V14``, respectively. See the updated expression type table below. + +EXPR_TYPE +~~~~~~~~~ + +A :class:`~.types.Type` is encoded by a single-byte ASCII ``char`` that encodes the kind of type, +followed by a payload that varies depending on the type. The defined codes are: + +====================== ========= ================================================================= +Qiskit class Type code Payload +====================== ========= ================================================================= +:class:`~.types.Bool` ``b`` One `_Bool const`. + +:class:`~.types.Uint` ``u`` One ``uint32_t width``, followed by one ``_Bool const``. +====================== ========= ================================================================= .. _qpy_version_13: From 8afa92e17a391a342b014dc3f43de93a099b4527 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Tue, 18 Feb 2025 13:00:21 -0500 Subject: [PATCH 33/53] Fix lint. --- qiskit/circuit/quantumcircuit.py | 4 ++-- qiskit/qpy/binary_io/value.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 36af5f3acc0..1c918abd86b 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -2911,8 +2911,8 @@ def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.V and isinstance(initial, int) and not isinstance(initial, bool) ): - # Convenience method to widen Python integer literals to the right width during the initial - # lift, if the type is already known via the variable. + # Convenience method to widen Python integer literals to the right width during + # the initial lift, if the type is already known via the variable. coerce_type = name_or_var.type initial = _validate_expr(circuit_scope, expr.lift(initial, coerce_type)) if isinstance(name_or_var, str): diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 44e0e83eba8..15c7f32321c 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -262,6 +262,7 @@ def __init__(self, file_obj, clbit_indices, standalone_var_indices, version): self.version = version def write_expr_type(self, type_: types.Type): + """Write the expression's type using the appropriate QPY version.""" if self.version < 14: _write_expr_type(self.file_obj, type_, self.version) else: From 4e0f2dfb20465f17bccbd60a2166ae832f87375c Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Tue, 18 Feb 2025 13:09:53 -0500 Subject: [PATCH 34/53] Add serialization testing. --- test/python/compiler/test_transpiler.py | 3 +- test/python/qasm3/test_export.py | 43 +++++++++++++++++++++++++ test/qpy_compat/test_qpy.py | 20 ++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index a0ea0dc118d..8ce87c5ea2a 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -2240,7 +2240,8 @@ def _control_flow_expr_circuit(self): base.append(CustomCX(), [2, 4]) base.ry(a, 4) base.measure(4, 2) - with base.switch(expr.bit_and(base.cregs[0], 2)) as case_: + # Use a const Uint RHS to make sure we QPY can serialize it. + with base.switch(expr.bit_and(base.cregs[0], expr.lift(2, try_const=True))) as case_: with case_(0, 1): base.cz(3, 5) with case_(case_.DEFAULT): diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 1b9c24ca541..164104436e9 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -1908,6 +1908,49 @@ def test_no_unnecessary_cast(self): """ self.assertEqual(dumps(qc), expected) + def test_const_expr(self): + """Test that const-typed expressions are implicitly converted without a cast.""" + qubit = Qubit() + creg = ClassicalRegister(2, "c") + circuit = QuantumCircuit([qubit], creg) + + body = QuantumCircuit([qubit], creg) + body.x(0) + body.y(0) + + circuit.if_test(expr.lift(True, types.Bool(const=True)), body, [0], body.clbits) + circuit.if_test( + expr.equal(creg, expr.lift(1, types.Uint(2, const=True))), body, [0], body.clbits + ) + circuit.if_test( + expr.equal( + expr.lift(1, types.Uint(2, const=True)), expr.lift(2, types.Uint(2, const=True)) + ), + body, + [0], + body.clbits, + ) + test = dumps(circuit) + expected = """\ +OPENQASM 3.0; +include "stdgates.inc"; +bit[2] c; +qubit _qubit0; +if (true) { + x _qubit0; + y _qubit0; +} +if (c == 1) { + x _qubit0; + y _qubit0; +} +if (1 == 2) { + x _qubit0; + y _qubit0; +} +""" + self.assertEqual(test, expected) + def test_var_use(self): """Test that input and declared vars work in simple local scopes and can be set.""" qc = QuantumCircuit() diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index cc70cccf7d4..1878b9e6004 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -820,6 +820,24 @@ def generate_v12_expr(): return [index, shift] +def generate_v14_expr(): + """Circuits that contain const-typed expressions, new in QPY v14.""" + from qiskit.circuit.classical import expr, types + + cr = ClassicalRegister(4, "cr") + + qc = QuantumCircuit(cr, name="const_expr") + with qc.if_test( + expr.not_equal( + expr.equal(expr.lift(1, types.Uint(1, const=True)), 1), + expr.lift(False, types.Bool(const=True)), + ) + ): + pass + + return [qc] + + def generate_circuits(version_parts): """Generate reference circuits.""" output_circuits = { @@ -871,6 +889,8 @@ def generate_circuits(version_parts): if version_parts >= (1, 1, 0): output_circuits["standalone_vars.qpy"] = generate_standalone_var() output_circuits["v12_expr.qpy"] = generate_v12_expr() + if version_parts >= (2, 0, 0): + output_circuits["v14_expr.qpy"] = generate_v14_expr() return output_circuits From 50c31d32888424479ce7a2997e28f2a042b5226f Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Tue, 18 Feb 2025 14:03:09 -0500 Subject: [PATCH 35/53] Test pre-v14 QPY rejects const-typed exprs. --- .../circuit/test_circuit_load_from_qpy.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 962dd22ac79..256c77ac094 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -1985,6 +1985,26 @@ def test_pre_v12_rejects_index(self, version): ): dump(qc, fptr, version=version) + @ddt.idata(range(QPY_COMPATIBILITY_VERSION, 14)) + def test_pre_v14_rejects_const_typed_expr(self, version): + """Test that dumping to older QPY versions rejects const-typed expressions.""" + qc = QuantumCircuit() + with qc.if_test( + expr.not_equal( + expr.equal(expr.lift(1, types.Uint(1, const=True)), 1), + expr.lift(False, types.Bool(const=True)), + ) + ): + pass + + with ( + io.BytesIO() as fptr, + self.assertRaisesRegex( + UnsupportedFeatureForVersion, "version 14 is required.*const-typed expressions" + ), + ): + dump(qc, fptr, version=version) + class TestSymengineLoadFromQPY(QiskitTestCase): """Test use of symengine in qpy set of methods.""" From e55e189e4e6b32d1a998a6f04b6ab61fe47e3faa Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Tue, 18 Feb 2025 14:18:23 -0500 Subject: [PATCH 36/53] QASM export for floats. --- qiskit/qasm3/ast.py | 8 ++++++++ qiskit/qasm3/exporter.py | 4 ++++ qiskit/qasm3/printer.py | 9 +++++++-- test/python/qasm3/test_export.py | 14 +++++++++++--- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/qiskit/qasm3/ast.py b/qiskit/qasm3/ast.py index c723c443fef..1d15c177d4f 100644 --- a/qiskit/qasm3/ast.py +++ b/qiskit/qasm3/ast.py @@ -136,6 +136,7 @@ class ClassicalType(ASTNode): class FloatType(ClassicalType, enum.Enum): """Allowed values for the width of floating-point types.""" + UNSPECIFIED = 0 HALF = 16 SINGLE = 32 DOUBLE = 64 @@ -242,6 +243,13 @@ def __init__(self, value): self.value = value +class FloatLiteral(Expression): + __slots__ = ("value",) + + def __init__(self, value): + self.value = value + + class BooleanLiteral(Expression): __slots__ = ("value",) diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index b08b2a9719e..9c1ffae5a10 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -1262,6 +1262,8 @@ def _build_ast_type(type_: types.Type) -> ast.ClassicalType: return ast.BoolType() if type_.kind is types.Uint: return ast.UintType(type_.width) + if type_.kind is types.Float: + return ast.FloatType.UNSPECIFIED raise RuntimeError(f"unhandled expr type '{type_}'") @@ -1283,6 +1285,8 @@ def visit_value(self, node, /): return ast.BooleanLiteral(node.value) if node.type.kind is types.Uint: return ast.IntegerLiteral(node.value) + if node.type.kind is types.Float: + return ast.FloatLiteral(node.value) raise RuntimeError(f"unhandled Value type '{node}'") def visit_cast(self, node, /): diff --git a/qiskit/qasm3/printer.py b/qiskit/qasm3/printer.py index 221c99bc4f9..30c6ec20d1e 100644 --- a/qiskit/qasm3/printer.py +++ b/qiskit/qasm3/printer.py @@ -78,7 +78,9 @@ class BasicPrinter: ast.QuantumGateModifierName.POW: "pow", } - _FLOAT_WIDTH_LOOKUP = {type: str(type.value) for type in ast.FloatType} + _FLOAT_TYPE_LOOKUP = {ast.FloatType.UNSPECIFIED: "float"} | { + type: f"float[{type.value}]" for type in ast.FloatType if type.value > 0 + } # The visitor names include the class names, so they mix snake_case with PascalCase. # pylint: disable=invalid-name @@ -205,7 +207,7 @@ def _visit_CalibrationGrammarDeclaration(self, node: ast.CalibrationGrammarDecla self._write_statement(f'defcalgrammar "{node.name}"') def _visit_FloatType(self, node: ast.FloatType) -> None: - self.stream.write(f"float[{self._FLOAT_WIDTH_LOOKUP[node]}]") + self.stream.write(self._FLOAT_TYPE_LOOKUP[node]) def _visit_BoolType(self, _node: ast.BoolType) -> None: self.stream.write("bool") @@ -282,6 +284,9 @@ def _visit_QuantumDelay(self, node: ast.QuantumDelay) -> None: def _visit_IntegerLiteral(self, node: ast.IntegerLiteral) -> None: self.stream.write(str(node.value)) + def _visit_FloatLiteral(self, node: ast.FloatLiteral) -> None: + self.stream.write(str(node.value)) + def _visit_BooleanLiteral(self, node: ast.BooleanLiteral): self.stream.write("true" if node.value else "false") diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 164104436e9..296d8c7d37c 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -1923,9 +1923,13 @@ def test_const_expr(self): expr.equal(creg, expr.lift(1, types.Uint(2, const=True))), body, [0], body.clbits ) circuit.if_test( - expr.equal( - expr.lift(1, types.Uint(2, const=True)), expr.lift(2, types.Uint(2, const=True)) - ), + expr.equal(expr.lift(1, types.Uint(2)), expr.lift(2, types.Uint(2, const=True))), + body, + [0], + body.clbits, + ) + circuit.if_test( + expr.less(expr.lift(1.0, types.Float(const=True)), expr.lift(2.0, types.Float())), body, [0], body.clbits, @@ -1948,6 +1952,10 @@ def test_const_expr(self): x _qubit0; y _qubit0; } +if (1.0 < 2.0) { + x _qubit0; + y _qubit0; +} """ self.assertEqual(test, expected) From 1d51022008e882d35e6c7159141bba2e98dfa2a3 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Tue, 18 Feb 2025 15:11:56 -0500 Subject: [PATCH 37/53] QPY support for floats. --- qiskit/qpy/__init__.py | 31 ++++++++++++++----- qiskit/qpy/binary_io/value.py | 26 ++++++++++++++++ qiskit/qpy/formats.py | 8 +++++ qiskit/qpy/type_keys.py | 2 ++ .../circuit/test_circuit_load_from_qpy.py | 14 +++++++++ test/python/compiler/test_transpiler.py | 2 ++ test/qpy_compat/test_qpy.py | 15 +++++---- 7 files changed, 84 insertions(+), 14 deletions(-) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 15c2ffc7c8d..a909de8a6dc 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -376,24 +376,39 @@ def open(*args): ---------- Version 14 adds support for additional :class:`~.types.Type` classes, and adds support for -const-ness to existing types :class:`~.types.Bool` and :class:`~.types.Uint`. -The ``EXPR_TYPE_BOOL`` and ``EXPR_TYPE_UNIT`` structs are now replaced by ``EXPR_TYPE_BOOL_V14`` -and ``EXPR_TYPE_UINT_V14``, respectively. See the updated expression type table below. +const-ness to existing types :class:`~.types.Bool` and :class:`~.types.Uint` in classical +expressions. -EXPR_TYPE -~~~~~~~~~ +Changes to EXPR_TYPE +~~~~~~~~~~~~~~~~~~~~ -A :class:`~.types.Type` is encoded by a single-byte ASCII ``char`` that encodes the kind of type, -followed by a payload that varies depending on the type. The defined codes are: +The ``EXPR_TYPE_BOOL`` and ``EXPR_TYPE_UNIT`` structs are now replaced by ``EXPR_TYPE_BOOL_V14`` +and ``EXPR_TYPE_UINT_V14``, respectively. + +The updated expression type encodings are shown below: ====================== ========= ================================================================= Qiskit class Type code Payload ====================== ========= ================================================================= -:class:`~.types.Bool` ``b`` One `_Bool const`. +:class:`~.types.Bool` ``b`` One ``_Bool const``. :class:`~.types.Uint` ``u`` One ``uint32_t width``, followed by one ``_Bool const``. +:class:`~.types.Float` ``f`` One ``_Bool const``. ====================== ========= ================================================================= +Changes to EXPR_VALUE +~~~~~~~~~~~~~~~~~~~~~ + +The classical expression's type system now supports new encoding types for value literals, in +addition to the existing encodings for int and bool. The new value type encodings are below: + +=========== ========= ============================================================================ +Python type Type code Payload +=========== ========= ============================================================================ +``float`` ``f`` One ``double value``. + +=========== ========= ============================================================================ + .. _qpy_version_13: Version 13 diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 15c7f32321c..87b577e3a97 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -324,6 +324,11 @@ def visit_value(self, node, /): struct.pack(formats.EXPR_VALUE_INT_PACK, *formats.EXPR_VALUE_INT(num_bytes)) ) self.file_obj.write(buffer) + elif isinstance(node.value, float): + self.file_obj.write(type_keys.ExprValue.FLOAT) + self.file_obj.write( + struct.pack(formats.EXPR_VALUE_FLOAT_PACK, *formats.EXPR_VALUE_FLOAT(node.value)) + ) else: raise exceptions.QpyError(f"unhandled Value object '{node.value}'") @@ -378,6 +383,10 @@ def _write_expr_type(file_obj, type_: types.Type, version): raise exceptions.UnsupportedFeatureForVersion( "const-typed expressions", required=14, target=version ) + if type_.kind is types.Float: + raise exceptions.UnsupportedFeatureForVersion( + "float-typed expressions", required=14, target=version + ) if type_.kind is types.Bool: file_obj.write(type_keys.ExprType.BOOL) elif type_.kind is types.Uint: @@ -404,6 +413,11 @@ def _write_expr_type_v14(file_obj, type_: types.Type): *formats.EXPR_TYPE_UINT_V14(type_.width, type_.const), ) ) + elif type_.kind is types.Float: + file_obj.write(type_keys.ExprType.FLOAT) + file_obj.write( + struct.pack(formats.EXPR_TYPE_FLOAT_PACK, *formats.EXPR_TYPE_FLOAT(type_.const)) + ) else: raise exceptions.QpyError(f"unhandled Type object '{type_};") @@ -714,6 +728,13 @@ def _read_expr( return expr.Value( int.from_bytes(file_obj.read(payload.num_bytes), "big", signed=True), type_ ) + if value_type_key == type_keys.ExprValue.FLOAT: + payload = formats.EXPR_VALUE_FLOAT._make( + struct.unpack( + formats.EXPR_VALUE_FLOAT_PACK, file_obj.read(formats.EXPR_VALUE_FLOAT_SIZE) + ) + ) + return expr.Value(payload.value, type_) raise exceptions.QpyError("Invalid classical-expression Value key '{value_type_key}'") if type_key == type_keys.Expression.CAST: payload = formats.EXPRESSION_CAST._make( @@ -784,6 +805,11 @@ def _read_expr_type_v14(file_obj) -> types.Type: ) ) return types.Uint(elem.width, const=elem.const) + if type_key == type_keys.ExprType.FLOAT: + elem = formats.EXPR_TYPE_FLOAT._make( + struct.unpack(formats.EXPR_TYPE_FLOAT_PACK, file_obj.read(formats.EXPR_TYPE_FLOAT_SIZE)) + ) + return types.Float(const=elem.const) raise exceptions.QpyError(f"Invalid classical-expression Type key '{type_key}'") diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py index ed44604e783..e795c703a14 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -360,6 +360,10 @@ # EXPR_TYPE +EXPR_TYPE_FLOAT = namedtuple("EXPR_TYPE_FLOAT", ["const"]) +EXPR_TYPE_FLOAT_PACK = "!?" +EXPR_TYPE_FLOAT_SIZE = struct.calcsize(EXPR_TYPE_FLOAT_PACK) + EXPR_TYPE_BOOL_V14 = namedtuple("EXPR_TYPE_BOOL_V14", ["const"]) EXPR_TYPE_BOOL_PACK_V14 = "!?" EXPR_TYPE_BOOL_SIZE_V14 = struct.calcsize(EXPR_TYPE_BOOL_PACK_V14) @@ -399,6 +403,10 @@ EXPR_VALUE_DISCRIMINATOR_SIZE = 1 +EXPR_VALUE_FLOAT = namedtuple("EXPR_VALUE_FLOAT", ["value"]) +EXPR_VALUE_FLOAT_PACK = "!d" +EXPR_VALUE_FLOAT_SIZE = struct.calcsize(EXPR_VALUE_FLOAT_PACK) + EXPR_VALUE_BOOL = namedtuple("EXPR_VALUE_BOOL", ["value"]) EXPR_VALUE_BOOL_PACK = "!?" EXPR_VALUE_BOOL_SIZE = struct.calcsize(EXPR_VALUE_BOOL_PACK) diff --git a/qiskit/qpy/type_keys.py b/qiskit/qpy/type_keys.py index 60262440d03..caa97537886 100644 --- a/qiskit/qpy/type_keys.py +++ b/qiskit/qpy/type_keys.py @@ -494,6 +494,7 @@ class ExprType(TypeKeyBase): BOOL = b"b" UINT = b"u" + FLOAT = b"f" @classmethod def assign(cls, obj): @@ -538,6 +539,7 @@ class ExprValue(TypeKeyBase): BOOL = b"b" INT = b"i" + FLOAT = b"f" @classmethod def assign(cls, obj): diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 256c77ac094..d87cf1ad280 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -2005,6 +2005,20 @@ def test_pre_v14_rejects_const_typed_expr(self, version): ): dump(qc, fptr, version=version) + @ddt.idata(range(QPY_COMPATIBILITY_VERSION, 14)) + def test_pre_v14_rejects_float_typed_expr(self, version): + """Test that dumping to older QPY versions rejects float-typed expressions.""" + qc = QuantumCircuit() + with qc.if_test(expr.less(1.0, 2.0)): + pass + with ( + io.BytesIO() as fptr, + self.assertRaisesRegex( + UnsupportedFeatureForVersion, "version 14 is required.*float-typed expressions" + ), + ): + dump(qc, fptr, version=version) + class TestSymengineLoadFromQPY(QiskitTestCase): """Test use of symengine in qpy set of methods.""" diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 8bc4ebb8f30..4f2facc0a4e 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -2175,6 +2175,8 @@ def _control_flow_expr_circuit(self): base.cz(1, 4) base.append(CustomCX(), [2, 4]) base.append(CustomCX(), [3, 4]) + with base.if_test(expr.less(1.0, 2.0)): + base.cx(0, 1) return base def _standalone_var_circuit(self): diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index 1878b9e6004..72e03e701f7 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -821,13 +821,12 @@ def generate_v12_expr(): def generate_v14_expr(): - """Circuits that contain const-typed expressions, new in QPY v14.""" + """Circuits that contain expressions new in QPY v14, including constant types + and floats.""" from qiskit.circuit.classical import expr, types - cr = ClassicalRegister(4, "cr") - - qc = QuantumCircuit(cr, name="const_expr") - with qc.if_test( + const_expr = QuantumCircuit(name="const_expr") + with const_expr.if_test( expr.not_equal( expr.equal(expr.lift(1, types.Uint(1, const=True)), 1), expr.lift(False, types.Bool(const=True)), @@ -835,7 +834,11 @@ def generate_v14_expr(): ): pass - return [qc] + float_expr = QuantumCircuit(name="float_expr") + with float_expr.if_test(expr.less(1.0, 2.0)): + pass + + return [const_expr, float_expr] def generate_circuits(version_parts): From eb8f1505fa1066a862997738df77788096f3f4c4 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Tue, 18 Feb 2025 15:28:08 -0500 Subject: [PATCH 38/53] Fix lint. --- qiskit/circuit/classical/expr/constructors.py | 8 ++++---- qiskit/circuit/classical/types/types.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index 12104c59834..11917d13ed9 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -242,8 +242,8 @@ def logic_not(operand: typing.Any, /) -> Expr: try: operand = _coerce_lossless(operand, types.Bool(const=operand.type.const)) return Unary(Unary.Op.LOGIC_NOT, operand, operand.type) - except TypeError: - raise TypeError(f"cannot apply '{Unary.Op.BIT_NOT}' to type '{operand.type}'") + except TypeError as ex: + raise TypeError(f"cannot apply '{Unary.Op.BIT_NOT}' to type '{operand.type}'") from ex def _lift_binary_operands(left: typing.Any, right: typing.Any) -> tuple[Expr, Expr]: @@ -391,8 +391,8 @@ def _binary_logical(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: left = _coerce_lossless(left, type) right = _coerce_lossless(right, type) return Binary(op, left, right, type) - except TypeError: - raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") + except TypeError as ex: + raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") from ex def logic_and(left: typing.Any, right: typing.Any, /) -> Expr: diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py index 49077246fa1..62b6518e71d 100644 --- a/qiskit/circuit/classical/types/types.py +++ b/qiskit/circuit/classical/types/types.py @@ -110,7 +110,7 @@ class Float(Type): In the future, this may also be used to represent a fixed-width float. """ - __slots__ = ("const",) + __slots__ = () def __init__(self, *, const: bool = False): super(Type, self).__setattr__("const", const) From 50deb93505856268a722257f1b1b94d402b86064 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Mon, 24 Feb 2025 17:41:39 -0500 Subject: [PATCH 39/53] Revert visitors.py. --- qiskit/circuit/classical/expr/visitors.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/qiskit/circuit/classical/expr/visitors.py b/qiskit/circuit/classical/expr/visitors.py index 4d348fb848a..be7e9311c37 100644 --- a/qiskit/circuit/classical/expr/visitors.py +++ b/qiskit/circuit/classical/expr/visitors.py @@ -276,15 +276,13 @@ def is_lvalue(node: expr.Expr, /) -> bool: >>> expr.is_lvalue(expr.lift(2)) False - :class:`~.expr.Var` nodes are l-values (unless their resolution type is `const`!), because - they have some associated memory location:: + :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.Var.new("a", types.Bool(const=True))) - False >>> expr.is_lvalue(expr.lift(Clbit())) True From f1dc1a150ed8a1875bca3497cd225483461bd501 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Mon, 24 Feb 2025 18:10:56 -0500 Subject: [PATCH 40/53] Address review comments. --- qiskit/circuit/classical/types/__init__.py | 9 ++++++++- qiskit/circuit/classical/types/ordering.py | 6 +++--- qiskit/circuit/quantumcircuit.py | 4 ++-- qiskit/qpy/binary_io/value.py | 1 - releasenotes/notes/const-expr-397ff09042942b81.yaml | 4 ++-- test/python/circuit/test_circuit_vars.py | 6 +++--- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/qiskit/circuit/classical/types/__init__.py b/qiskit/circuit/classical/types/__init__.py index ae38a0d97fb..345750ab309 100644 --- a/qiskit/circuit/classical/types/__init__.py +++ b/qiskit/circuit/classical/types/__init__.py @@ -40,6 +40,10 @@ .. autoclass:: Bool .. autoclass:: Uint +All types have a :attr:`~Type.const` field to indicate their const-ness. When the result type of +an expression is constant, the expression is considered a constant expression. Constant +expressions can be used in certain contexts that aren't valid for runtime-initialized variables. + Note that :class:`Uint` defines a family of types parametrized by their width; it is not one single type, which may be slightly different to the 'classical' programming languages you are used to. @@ -59,7 +63,10 @@ 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 -ordering defines when one type may be lossless directly interpreted as another. +ordering defines when one type may be losslessly directly interpreted as another. + +When two types differ only in const-ness, the non-const version is considered to be the +"greater" of the two. The low-level interface to querying the subtyping relationship is the :func:`order` function. diff --git a/qiskit/circuit/classical/types/ordering.py b/qiskit/circuit/classical/types/ordering.py index c42cfd23bec..aa078fbf403 100644 --- a/qiskit/circuit/classical/types/ordering.py +++ b/qiskit/circuit/classical/types/ordering.py @@ -105,12 +105,12 @@ def order(left: Type, right: Type, /) -> Ordering: # If one type is greater (and thus is the only type that can represent # both) an ordering is only defined if that type is non-const or both # types are const. - if left.const is True and right.const is False: + if left.const and not right.const: if order_ is Ordering.EQUAL: return Ordering.LESS if order_ is Ordering.GREATER: return Ordering.NONE - if right.const is True and left.const is False: + if right.const and not left.const: if order_ is Ordering.EQUAL: return Ordering.GREATER if order_ is Ordering.LESS: @@ -251,7 +251,7 @@ def cast_kind(from_: Type, to_: Type, /) -> CastKind: >>> types.cast_kind(types.Uint(16), types.Uint(8)) """ - if to_.const is True and from_.const is False: + if to_.const and not from_.const: # We can't cast to a const type. return CastKind.NONE if (coercer := _ALLOWED_CASTS.get((from_.kind, to_.kind))) is None: diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 890e9732cb5..9429b7844e1 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -2940,9 +2940,9 @@ def add_input( # pylint: disable=missing-raises-doc if type_ is not None: raise ValueError("cannot give an explicit type with an existing Var") if name_or_var.type.const: - raise CircuitError("const variables are not supported") + raise CircuitError("const variables cannot be input variables") elif type_ is not None and type_.const: - raise CircuitError("const variables are not supported") + raise CircuitError("const variables cannot be input variables") var = self._prepare_new_var(name_or_var, type_) self._vars_input[var.name] = var diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 15c7f32321c..138c8e57154 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -398,7 +398,6 @@ def _write_expr_type_v14(file_obj, type_: types.Type): elif type_.kind is types.Uint: file_obj.write(type_keys.ExprType.UINT) file_obj.write( - # TODO: make sure you're calling this correctly struct.pack( formats.EXPR_TYPE_UINT_PACK_V14, *formats.EXPR_TYPE_UINT_V14(type_.width, type_.const), diff --git a/releasenotes/notes/const-expr-397ff09042942b81.yaml b/releasenotes/notes/const-expr-397ff09042942b81.yaml index ebbccd32d06..5c6f6352828 100644 --- a/releasenotes/notes/const-expr-397ff09042942b81.yaml +++ b/releasenotes/notes/const-expr-397ff09042942b81.yaml @@ -11,11 +11,11 @@ features_circuits: from qiskit.circuit.classical import expr expr.lift(5, try_const=True) - # >>> Value(5, Uint(3, const=True)) + # Value(5, Uint(3, const=True)) The result type of an operation applied to const types is also const:: from qiskit.circuit.classical import expr expr.bit_and(expr.lift(5, try_const=True), expr.lift(6, try_const=True)).type.const - # >>> True + # True diff --git a/test/python/circuit/test_circuit_vars.py b/test/python/circuit/test_circuit_vars.py index aa761098128..4bb8c262143 100644 --- a/test/python/circuit/test_circuit_vars.py +++ b/test/python/circuit/test_circuit_vars.py @@ -119,7 +119,7 @@ def test_initialise_inputs_declarations(self): def test_initialise_inputs_declarations_rejects_const_vars(self): a = expr.Var.new("a", types.Uint(16, const=True)) - with self.assertRaisesRegex(CircuitError, "const variables.*not supported"): + with self.assertRaisesRegex(CircuitError, "const variables cannot be input variables"): QuantumCircuit(inputs=[a]) def test_initialise_captures_declarations(self): @@ -219,9 +219,9 @@ def test_add_input_returns_input(self): def test_add_input_rejects_const_var(self): a = expr.Var.new("a", types.Bool(const=True)) qc = QuantumCircuit() - with self.assertRaisesRegex(CircuitError, "const variables.*not supported"): + with self.assertRaisesRegex(CircuitError, "const variables cannot be input variables"): qc.add_input(a) - with self.assertRaisesRegex(CircuitError, "const variables.*not supported"): + with self.assertRaisesRegex(CircuitError, "const variables cannot be input variables"): qc.add_input("a", types.Bool(const=True)) def test_cannot_have_both_inputs_and_captures(self): From 166d7b2833c6ed3b48d91772128a22f64054f55c Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Wed, 26 Feb 2025 11:27:53 -0500 Subject: [PATCH 41/53] Improve type docs. --- qiskit/circuit/classical/types/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qiskit/circuit/classical/types/__init__.py b/qiskit/circuit/classical/types/__init__.py index 345750ab309..d10a4769ca5 100644 --- a/qiskit/circuit/classical/types/__init__.py +++ b/qiskit/circuit/classical/types/__init__.py @@ -43,6 +43,9 @@ All types have a :attr:`~Type.const` field to indicate their const-ness. When the result type of an expression is constant, the expression is considered a constant expression. Constant expressions can be used in certain contexts that aren't valid for runtime-initialized variables. +This is not to be confused with the concept of a ``const`` variable in languages like C, where +the variable has a well-defined but immutable storage location. Qiskit's definition of const-ness +is more similar to C++'s ``constexpr``. Note that :class:`Uint` defines a family of types parametrized by their width; it is not one single type, which may be slightly different to the 'classical' programming languages you are used to. From 9d6cf39873e9bba35de256e53b870468fdb99bcd Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Wed, 26 Feb 2025 13:23:59 -0500 Subject: [PATCH 42/53] Revert QPY, since the old format can support constexprs. By making const-ness a property of expressions, we don't need any special serialization in QPY. That's because we assume that all `Var` expressions are non-const, and all `Value` expressions are const. And the const-ness of any expression is defined by the const-ness of its operands, e.g. when QPY reconstructs a binary operand, the constructed expression's `const` attribute gets set to `True` if both of the operands are `const`, which ultimately flows bottom-up from the `Var` and `Value` leaf nodes. --- qiskit/qpy/__init__.py | 24 ------- qiskit/qpy/binary_io/circuits.py | 4 +- qiskit/qpy/binary_io/value.py | 107 +++++++------------------------ qiskit/qpy/common.py | 2 +- qiskit/qpy/formats.py | 9 +-- 5 files changed, 26 insertions(+), 120 deletions(-) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 502207df2f5..208fa8e44c1 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -377,30 +377,6 @@ def open(*args): by ``num_circuits`` in the file header). There is no padding between the circuits in the data. -.. _qpy_version_14: - -Version 14 ----------- - -Version 14 adds support for additional :class:`~.types.Type` classes, and adds support for -const-ness to existing types :class:`~.types.Bool` and :class:`~.types.Uint`. -The ``EXPR_TYPE_BOOL`` and ``EXPR_TYPE_UNIT`` structs are now replaced by ``EXPR_TYPE_BOOL_V14`` -and ``EXPR_TYPE_UINT_V14``, respectively. See the updated expression type table below. - -EXPR_TYPE -~~~~~~~~~ - -A :class:`~.types.Type` is encoded by a single-byte ASCII ``char`` that encodes the kind of type, -followed by a payload that varies depending on the type. The defined codes are: - -====================== ========= ================================================================= -Qiskit class Type code Payload -====================== ========= ================================================================= -:class:`~.types.Bool` ``b`` One `_Bool const`. - -:class:`~.types.Uint` ``u`` One ``uint32_t width``, followed by one ``_Bool const``. -====================== ========= ================================================================= - .. _qpy_version_13: Version 13 diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 5484d831685..2297dbf6d34 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -1236,7 +1236,7 @@ def write_circuit( file_obj.write(metadata_raw) # Write header payload file_obj.write(registers_raw) - standalone_var_indices = value.write_standalone_vars(file_obj, circuit, version) + standalone_var_indices = value.write_standalone_vars(file_obj, circuit) else: if circuit.num_vars: raise exceptions.UnsupportedFeatureForVersion( @@ -1407,7 +1407,7 @@ def read_circuit(file_obj, version, metadata_deserializer=None, use_symengine=Fa "q": [Qubit() for _ in out_bits["q"]], "c": [Clbit() for _ in out_bits["c"]], } - var_segments, standalone_var_indices = value.read_standalone_vars(file_obj, num_vars, version) + var_segments, standalone_var_indices = value.read_standalone_vars(file_obj, num_vars) circ = QuantumCircuit( out_bits["q"], out_bits["c"], diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 138c8e57154..9799fdf3f45 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -261,19 +261,12 @@ def __init__(self, file_obj, clbit_indices, standalone_var_indices, version): self.standalone_var_indices = standalone_var_indices self.version = version - def write_expr_type(self, type_: types.Type): - """Write the expression's type using the appropriate QPY version.""" - if self.version < 14: - _write_expr_type(self.file_obj, type_, self.version) - else: - _write_expr_type_v14(self.file_obj, type_) - def visit_generic(self, node, /): raise exceptions.QpyError(f"unhandled Expr object '{node}'") def visit_var(self, node, /): self.file_obj.write(type_keys.Expression.VAR) - self.write_expr_type(node.type) + _write_expr_type(self.file_obj, node.type) if node.standalone: self.file_obj.write(type_keys.ExprVar.UUID) self.file_obj.write( @@ -303,7 +296,7 @@ def visit_var(self, node, /): def visit_value(self, node, /): self.file_obj.write(type_keys.Expression.VALUE) - self.write_expr_type(node.type) + _write_expr_type(self.file_obj, node.type) if node.value is True or node.value is False: self.file_obj.write(type_keys.ExprValue.BOOL) self.file_obj.write( @@ -329,7 +322,7 @@ def visit_value(self, node, /): def visit_cast(self, node, /): self.file_obj.write(type_keys.Expression.CAST) - self.write_expr_type(node.type) + _write_expr_type(self.file_obj, node.type) self.file_obj.write( struct.pack(formats.EXPRESSION_CAST_PACK, *formats.EXPRESSION_CAST(node.implicit)) ) @@ -337,7 +330,7 @@ def visit_cast(self, node, /): def visit_unary(self, node, /): self.file_obj.write(type_keys.Expression.UNARY) - self.write_expr_type(node.type) + _write_expr_type(self.file_obj, node.type) self.file_obj.write( struct.pack(formats.EXPRESSION_UNARY_PACK, *formats.EXPRESSION_UNARY(node.op.value)) ) @@ -345,7 +338,7 @@ def visit_unary(self, node, /): def visit_binary(self, node, /): self.file_obj.write(type_keys.Expression.BINARY) - self.write_expr_type(node.type) + _write_expr_type(self.file_obj, node.type) self.file_obj.write( struct.pack(formats.EXPRESSION_BINARY_PACK, *formats.EXPRESSION_BINARY(node.op.value)) ) @@ -358,7 +351,7 @@ def visit_index(self, node, /): "the 'Index' expression", required=12, target=self.version ) self.file_obj.write(type_keys.Expression.INDEX) - self.write_expr_type(node.type) + _write_expr_type(self.file_obj, node.type) node.target.accept(self) node.index.accept(self) @@ -373,11 +366,7 @@ def _write_expr( node.accept(_ExprWriter(file_obj, clbit_indices, standalone_var_indices, version)) -def _write_expr_type(file_obj, type_: types.Type, version): - if type_.const: - raise exceptions.UnsupportedFeatureForVersion( - "const-typed expressions", required=14, target=version - ) +def _write_expr_type(file_obj, type_: types.Type): if type_.kind is types.Bool: file_obj.write(type_keys.ExprType.BOOL) elif type_.kind is types.Uint: @@ -389,24 +378,6 @@ def _write_expr_type(file_obj, type_: types.Type, version): raise exceptions.QpyError(f"unhandled Type object '{type_};") -def _write_expr_type_v14(file_obj, type_: types.Type): - if type_.kind is types.Bool: - file_obj.write(type_keys.ExprType.BOOL) - file_obj.write( - struct.pack(formats.EXPR_TYPE_BOOL_PACK_V14, *formats.EXPR_TYPE_BOOL_V14(type_.const)) - ) - elif type_.kind is types.Uint: - file_obj.write(type_keys.ExprType.UINT) - file_obj.write( - struct.pack( - formats.EXPR_TYPE_UINT_PACK_V14, - *formats.EXPR_TYPE_UINT_V14(type_.width, type_.const), - ) - ) - else: - raise exceptions.QpyError(f"unhandled Type object '{type_};") - - def _read_parameter(file_obj): data = formats.PARAMETER( *struct.unpack(formats.PARAMETER_PACK, file_obj.read(formats.PARAMETER_SIZE)) @@ -664,14 +635,10 @@ def _read_expr( clbits: collections.abc.Sequence[Clbit], cregs: collections.abc.Mapping[str, ClassicalRegister], standalone_vars: collections.abc.Sequence[expr.Var], - version: int, ) -> expr.Expr: # pylint: disable=too-many-return-statements type_key = file_obj.read(formats.EXPRESSION_DISCRIMINATOR_SIZE) - if version < 14: - type_ = _read_expr_type(file_obj) - else: - type_ = _read_expr_type_v14(file_obj) + type_ = _read_expr_type(file_obj) if type_key == type_keys.Expression.VAR: var_type_key = file_obj.read(formats.EXPR_VAR_DISCRIMINATOR_SIZE) if var_type_key == type_keys.ExprVar.UUID: @@ -719,9 +686,7 @@ def _read_expr( struct.unpack(formats.EXPRESSION_CAST_PACK, file_obj.read(formats.EXPRESSION_CAST_SIZE)) ) return expr.Cast( - _read_expr(file_obj, clbits, cregs, standalone_vars, version), - type_, - implicit=payload.implicit, + _read_expr(file_obj, clbits, cregs, standalone_vars), type_, implicit=payload.implicit ) if type_key == type_keys.Expression.UNARY: payload = formats.EXPRESSION_UNARY._make( @@ -731,7 +696,7 @@ def _read_expr( ) return expr.Unary( expr.Unary.Op(payload.opcode), - _read_expr(file_obj, clbits, cregs, standalone_vars, version), + _read_expr(file_obj, clbits, cregs, standalone_vars), type_, ) if type_key == type_keys.Expression.BINARY: @@ -742,14 +707,14 @@ def _read_expr( ) return expr.Binary( expr.Binary.Op(payload.opcode), - _read_expr(file_obj, clbits, cregs, standalone_vars, version), - _read_expr(file_obj, clbits, cregs, standalone_vars, version), + _read_expr(file_obj, clbits, cregs, standalone_vars), + _read_expr(file_obj, clbits, cregs, standalone_vars), type_, ) if type_key == type_keys.Expression.INDEX: return expr.Index( - _read_expr(file_obj, clbits, cregs, standalone_vars, version), - _read_expr(file_obj, clbits, cregs, standalone_vars, version), + _read_expr(file_obj, clbits, cregs, standalone_vars), + _read_expr(file_obj, clbits, cregs, standalone_vars), type_, ) raise exceptions.QpyError(f"Invalid classical-expression Expr key '{type_key}'") @@ -767,32 +732,12 @@ def _read_expr_type(file_obj) -> types.Type: raise exceptions.QpyError(f"Invalid classical-expression Type key '{type_key}'") -def _read_expr_type_v14(file_obj) -> types.Type: - type_key = file_obj.read(formats.EXPR_TYPE_DISCRIMINATOR_SIZE) - if type_key == type_keys.ExprType.BOOL: - elem = formats.EXPR_TYPE_BOOL_V14._make( - struct.unpack( - formats.EXPR_TYPE_BOOL_PACK_V14, file_obj.read(formats.EXPR_TYPE_BOOL_SIZE_V14) - ) - ) - return types.Bool(const=elem.const) - if type_key == type_keys.ExprType.UINT: - elem = formats.EXPR_TYPE_UINT_V14._make( - struct.unpack( - formats.EXPR_TYPE_UINT_PACK_V14, file_obj.read(formats.EXPR_TYPE_UINT_SIZE_V14) - ) - ) - return types.Uint(elem.width, const=elem.const) - raise exceptions.QpyError(f"Invalid classical-expression Type key '{type_key}'") - - -def read_standalone_vars(file_obj, num_vars, version): +def read_standalone_vars(file_obj, num_vars): """Read the ``num_vars`` standalone variable declarations from the file. Args: file_obj (File): a file-like object to read from. num_vars (int): the number of variables to read. - version (int): the target QPY version. Returns: tuple[dict, list]: the first item is a mapping of the ``ExprVarDeclaration`` type keys to @@ -812,10 +757,7 @@ def read_standalone_vars(file_obj, num_vars, version): file_obj.read(formats.EXPR_VAR_DECLARATION_SIZE), ) ) - if version < 14: - type_ = _read_expr_type(file_obj) - else: - type_ = _read_expr_type_v14(file_obj) + type_ = _read_expr_type(file_obj) name = file_obj.read(data.name_size).decode(common.ENCODE) var = expr.Var(uuid.UUID(bytes=data.uuid_bytes), type_, name=name) read_vars[data.usage].append(var) @@ -823,7 +765,7 @@ def read_standalone_vars(file_obj, num_vars, version): return read_vars, var_order -def _write_standalone_var(file_obj, var, type_key, version): +def _write_standalone_var(file_obj, var, type_key): name = var.name.encode(common.ENCODE) file_obj.write( struct.pack( @@ -831,20 +773,16 @@ def _write_standalone_var(file_obj, var, type_key, version): *formats.EXPR_VAR_DECLARATION(var.var.bytes, type_key, len(name)), ) ) - if version < 14: - _write_expr_type(file_obj, var.type, version) - else: - _write_expr_type_v14(file_obj, var.type) + _write_expr_type(file_obj, var.type) file_obj.write(name) -def write_standalone_vars(file_obj, circuit, version): +def write_standalone_vars(file_obj, circuit): """Write the standalone variables out from a circuit. Args: file_obj (File): the file-like object to write to. circuit (QuantumCircuit): the circuit to take the variables from. - version (int): the target QPY version. Returns: dict[expr.Var, int]: a mapping of the variables written to the index that they were written @@ -853,15 +791,15 @@ def write_standalone_vars(file_obj, circuit, version): index = 0 out = {} for var in circuit.iter_input_vars(): - _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.INPUT, version) + _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.INPUT) out[var] = index index += 1 for var in circuit.iter_captured_vars(): - _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.CAPTURE, version) + _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.CAPTURE) out[var] = index index += 1 for var in circuit.iter_declared_vars(): - _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.LOCAL, version) + _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.LOCAL) out[var] = index index += 1 return out @@ -1040,7 +978,6 @@ def loads_value( clbits=clbits, cregs=cregs or {}, standalone_vars=standalone_vars, - version=version, ) raise exceptions.QpyError(f"Serialization for {type_key} is not implemented in value I/O.") diff --git a/qiskit/qpy/common.py b/qiskit/qpy/common.py index 0f30e90cc34..c2585d5be4d 100644 --- a/qiskit/qpy/common.py +++ b/qiskit/qpy/common.py @@ -25,7 +25,7 @@ from qiskit.qpy import formats, exceptions -QPY_VERSION = 14 +QPY_VERSION = 13 QPY_COMPATIBILITY_VERSION = 10 ENCODE = "utf8" diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py index ed44604e783..7696cae94e2 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -360,14 +360,6 @@ # EXPR_TYPE -EXPR_TYPE_BOOL_V14 = namedtuple("EXPR_TYPE_BOOL_V14", ["const"]) -EXPR_TYPE_BOOL_PACK_V14 = "!?" -EXPR_TYPE_BOOL_SIZE_V14 = struct.calcsize(EXPR_TYPE_BOOL_PACK_V14) - -EXPR_TYPE_UINT_V14 = namedtuple("EXPR_TYPE_UINT_V14", ["width", "const"]) -EXPR_TYPE_UINT_PACK_V14 = "!L?" -EXPR_TYPE_UINT_SIZE_V14 = struct.calcsize(EXPR_TYPE_UINT_PACK_V14) - EXPR_TYPE_DISCRIMINATOR_SIZE = 1 EXPR_TYPE_BOOL = namedtuple("EXPR_TYPE_BOOL", []) @@ -378,6 +370,7 @@ EXPR_TYPE_UINT_PACK = "!L" EXPR_TYPE_UINT_SIZE = struct.calcsize(EXPR_TYPE_UINT_PACK) + # EXPR_VAR EXPR_VAR_DISCRIMINATOR_SIZE = 1 From 8021e00850116efd16aa6b3688fac46f9d289afe Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Wed, 26 Feb 2025 13:29:03 -0500 Subject: [PATCH 43/53] Move const-ness from Type to Expr. --- qiskit/circuit/classical/expr/constructors.py | 66 ++----- qiskit/circuit/classical/expr/expr.py | 11 +- qiskit/circuit/classical/types/ordering.py | 25 +-- qiskit/circuit/classical/types/types.py | 23 +-- qiskit/circuit/quantumcircuit.py | 8 - .../classical/test_expr_constructors.py | 187 +----------------- .../circuit/classical/test_types_ordering.py | 86 -------- test/python/circuit/test_circuit_vars.py | 53 ----- test/python/circuit/test_store.py | 17 -- test/python/compiler/test_transpiler.py | 2 +- test/python/qasm3/test_export.py | 43 ---- 11 files changed, 41 insertions(+), 480 deletions(-) diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index ce99632aaf2..062795125ce 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -77,7 +77,7 @@ def lift_legacy_condition( return Binary(Binary.Op.EQUAL, left, right, types.Bool()) -def lift(value: typing.Any, /, type: types.Type | None = None, *, try_const: bool = False) -> Expr: +def lift(value: typing.Any, /, type: types.Type | None = None) -> Expr: """Lift the given Python ``value`` to a :class:`~.expr.Value` or :class:`~.expr.Var`. By default, lifted scalars are not const. To lift supported scalars to const-typed @@ -106,17 +106,6 @@ def lift(value: typing.Any, /, type: types.Type | None = None, *, try_const: boo Var(ClassicalRegister(3, "c"), Uint(5, const=False)) >>> expr.lift(5, types.Uint(4)) Value(5, Uint(4)) - - Lifting non-classical resource scalars to const values:: - - >>> from qiskit.circuit.classical import expr, types - >>> expr.lift(7) - Value(7, Uint(3, const=False)) - >>> expr.lift(7, try_const=True) - Value(7, Uint(3, const=True)) - >>> expr.lift(7, types.Uint(8, const=True)) - Value(7, Uint(8, const=True)) - """ if isinstance(value, Expr): if type is not None: @@ -124,13 +113,9 @@ def lift(value: typing.Any, /, type: types.Type | None = None, *, try_const: boo return value from qiskit.circuit import Clbit, ClassicalRegister # pylint: disable=cyclic-import - if type is not None: - # If a type was specified, the inferred type must be the same - # const-ness. - try_const = type.const inferred: types.Type if value is True or value is False: - inferred = types.Bool(const=try_const) + inferred = types.Bool() constructor = Value elif isinstance(value, Clbit): inferred = types.Bool() @@ -141,7 +126,7 @@ def lift(value: typing.Any, /, type: types.Type | None = None, *, try_const: boo elif isinstance(value, int): if value < 0: raise ValueError("cannot represent a negative value") - inferred = types.Uint(width=value.bit_length() or 1, const=try_const) + inferred = types.Uint(width=value.bit_length() or 1) constructor = Value else: raise TypeError(f"failed to infer a type for '{value}'") @@ -218,7 +203,7 @@ def logic_not(operand: typing.Any, /) -> Expr: Bool(const=False)) """ operand = lift(operand) - operand = _coerce_lossless(operand, types.Bool(const=operand.type.const)) + operand = _coerce_lossless(operand, types.Bool()) return Unary(Unary.Op.LOGIC_NOT, operand, operand.type) @@ -233,24 +218,13 @@ def _lift_binary_operands(left: typing.Any, right: typing.Any) -> tuple[Expr, Ex to be interoperable. * If both operands are expressions, they are returned as-is, and may require a cast node. """ - left_bool = isinstance(left, bool) left_int = isinstance(left, int) and not isinstance(left, bool) - right_bool = isinstance(right, bool) - right_int = isinstance(right, int) and not right_bool + right_int = isinstance(right, int) and not isinstance(right, bool) if not (left_int or right_int): - if left_bool == right_bool: - # They're either both bool, or neither are, so we lift them - # independently. - left = lift(left) - right = lift(right) - elif not right_bool: - # Left is a bool, which should only be const if right is const. - right = lift(right) - left = lift(left, try_const=right.type.const) - elif not left_bool: - # Right is a bool, which should only be const if left is const. - left = lift(left) - right = lift(right, try_const=left.type.const) + # They're either both bool, or neither are, so we lift them + # independently. + left = lift(left) + right = lift(right) elif not right_int: # Left is an int. right = lift(right) @@ -262,7 +236,7 @@ def _lift_binary_operands(left: typing.Any, right: typing.Any) -> tuple[Expr, Ex # Left will share const-ness of right. left = Value(left, right.type) else: - left = lift(left, try_const=right.type.const) + left = lift(left) elif not left_int: # Right is an int. left = lift(left) @@ -274,7 +248,7 @@ def _lift_binary_operands(left: typing.Any, right: typing.Any) -> tuple[Expr, Ex # Right will share const-ness of left. right = Value(right, left.type) else: - right = lift(right, try_const=left.type.const) + right = lift(right) else: # Both are `int`, so we take our best case to make things work. # If the caller needs a const type, they should lift one side to @@ -289,14 +263,14 @@ def _binary_bitwise(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: left, right = _lift_binary_operands(left, right) type: types.Type if left.type.kind is right.type.kind is types.Bool: - type = types.Bool(const=(left.type.const and right.type.const)) + type = types.Bool() elif left.type.kind is types.Uint and right.type.kind is types.Uint: if left.type.width != right.type.width: raise TypeError( "binary bitwise operations are defined between unsigned integers of the same width," f" but got {left.type.width} and {right.type.width}." ) - type = types.Uint(width=left.type.width, const=(left.type.const and right.type.const)) + type = types.Uint(width=left.type.width) else: raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") return Binary(op, _coerce_lossless(left, type), _coerce_lossless(right, type), type) @@ -362,7 +336,7 @@ def bit_xor(left: typing.Any, right: typing.Any, /) -> Expr: def _binary_logical(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: left = lift(left) right = lift(right) - type = types.Bool(const=(left.type.const and right.type.const)) + type = types.Bool() left = _coerce_lossless(left, type) right = _coerce_lossless(right, type) return Binary(op, left, right, type) @@ -416,7 +390,7 @@ def _equal_like(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: op, _coerce_lossless(left, type), _coerce_lossless(right, type), - types.Bool(const=type.const), + types.Bool(), ) @@ -469,7 +443,7 @@ def _binary_relation(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr op, _coerce_lossless(left, type), _coerce_lossless(right, type), - types.Bool(const=type.const), + types.Bool(), ) @@ -554,14 +528,14 @@ def _shift_like( left = _coerce_lossless(left, type) if type is not None else left else: left = lift(left, type) - right = lift(right, try_const=left.type.const) + right = lift(right) if left.type.kind != types.Uint or right.type.kind != types.Uint: raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") return Binary( op, left, right, - types.Uint(width=left.type.width, const=(left.type.const and right.type.const)), + types.Uint(width=left.type.width), ) @@ -631,7 +605,7 @@ def index(target: typing.Any, index: typing.Any, /) -> Expr: Bool(const=False)) """ target = lift(target) - index = lift(index, try_const=target.type.const) + index = lift(index) if target.type.kind is not types.Uint or index.type.kind is not types.Uint: raise TypeError(f"invalid types for indexing: '{target.type}' and '{index.type}'") - return Index(target, index, types.Bool(const=target.type.const and index.type.const)) + return Index(target, index, types.Bool()) diff --git a/qiskit/circuit/classical/expr/expr.py b/qiskit/circuit/classical/expr/expr.py index 6f41fc0d219..58fad02612e 100644 --- a/qiskit/circuit/classical/expr/expr.py +++ b/qiskit/circuit/classical/expr/expr.py @@ -55,9 +55,10 @@ class Expr(abc.ABC): All subclasses are responsible for setting their ``type`` attribute in their ``__init__``, and should not call the parent initializer.""" - __slots__ = ("type",) + __slots__ = ("type", "const") type: types.Type + const: bool # Sentinel to prevent instantiation of the base class. @abc.abstractmethod @@ -89,6 +90,7 @@ class Cast(Expr): def __init__(self, operand: Expr, type: types.Type, implicit: bool = False): self.type = type + self.const = operand.const self.operand = operand self.implicit = implicit @@ -99,6 +101,7 @@ def __eq__(self, other): return ( isinstance(other, Cast) and self.type == other.type + and self.const == other.const and self.operand == other.operand and self.implicit == other.implicit ) @@ -141,6 +144,7 @@ def __init__( name: str | None = None, ): super().__setattr__("type", type) + super().__setattr__("const", False) super().__setattr__("var", var) super().__setattr__("name", name) @@ -207,6 +211,7 @@ class Value(Expr): def __init__(self, value: typing.Any, type: types.Type): self.type = type self.value = value + self.const = True def accept(self, visitor, /): return visitor.visit_value(self) @@ -258,6 +263,7 @@ def __init__(self, op: Unary.Op, operand: Expr, type: types.Type): self.op = op self.operand = operand self.type = type + self.const = operand.const def accept(self, visitor, /): return visitor.visit_unary(self) @@ -266,6 +272,7 @@ def __eq__(self, other): return ( isinstance(other, Unary) and self.type == other.type + and self.const == other.const and self.op is other.op and self.operand == other.operand ) @@ -349,6 +356,7 @@ def __init__(self, op: Binary.Op, left: Expr, right: Expr, type: types.Type): self.left = left self.right = right self.type = type + self.const = left.const and right.const def accept(self, visitor, /): return visitor.visit_binary(self) @@ -382,6 +390,7 @@ def __init__(self, target: Expr, index: Expr, type: types.Type): self.target = target self.index = index self.type = type + self.const = target.const and index.const def accept(self, visitor, /): return visitor.visit_index(self) diff --git a/qiskit/circuit/classical/types/ordering.py b/qiskit/circuit/classical/types/ordering.py index aa078fbf403..7e232880d7f 100644 --- a/qiskit/circuit/classical/types/ordering.py +++ b/qiskit/circuit/classical/types/ordering.py @@ -98,24 +98,7 @@ def order(left: Type, right: Type, /) -> Ordering: """ if (orderer := _ORDERERS.get((left.kind, right.kind))) is None: return Ordering.NONE - order_ = orderer(left, right) - - # If the natural type ordering is equal (either one can represent both) - # but the types differ in const-ness, the non-const variant is greater. - # If one type is greater (and thus is the only type that can represent - # both) an ordering is only defined if that type is non-const or both - # types are const. - if left.const and not right.const: - if order_ is Ordering.EQUAL: - return Ordering.LESS - if order_ is Ordering.GREATER: - return Ordering.NONE - if right.const and not left.const: - if order_ is Ordering.EQUAL: - return Ordering.GREATER - if order_ is Ordering.LESS: - return Ordering.NONE - return order_ + return orderer(left, right) def is_subtype(left: Type, right: Type, /, strict: bool = False) -> bool: @@ -251,13 +234,7 @@ def cast_kind(from_: Type, to_: Type, /) -> CastKind: >>> types.cast_kind(types.Uint(16), types.Uint(8)) """ - if to_.const and not from_.const: - # We can't cast to a const type. - return CastKind.NONE if (coercer := _ALLOWED_CASTS.get((from_.kind, to_.kind))) is None: return CastKind.NONE cast_kind_ = coercer(from_, to_) - if cast_kind_ is CastKind.EQUAL and to_.const != from_.const: - # We need an implicit cast to drop const. - return CastKind.IMPLICIT return cast_kind_ diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py index 81bd80bc33f..c65e063f52e 100644 --- a/qiskit/circuit/classical/types/types.py +++ b/qiskit/circuit/classical/types/types.py @@ -30,13 +30,10 @@ class Type: This must not be subclassed by users; subclasses form the internal data of the representation of expressions, and it does not make sense to add more outside of Qiskit library code. - - All subclasses are responsible for setting the ``const`` attribute in their ``__init__``. """ - __slots__ = ("const",) + __slots__ = () - const: bool @property def kind(self): @@ -69,17 +66,14 @@ class Bool(Type): __slots__ = () - def __init__(self, *, const: bool = False): - super(Type, self).__setattr__("const", const) - def __repr__(self): - return f"Bool(const={self.const})" + return f"Bool()" def __hash__(self): - return hash((self.__class__, self.const)) + return hash(self.__class__) def __eq__(self, other): - return isinstance(other, Bool) and self.const == other.const + return isinstance(other, Bool) @typing.final @@ -88,17 +82,16 @@ class Uint(Type): __slots__ = ("width",) - def __init__(self, width: int, *, const: bool = False): + def __init__(self, width: int): if isinstance(width, int) and width <= 0: raise ValueError("uint width must be greater than zero") - super(Type, self).__setattr__("const", const) super(Type, self).__setattr__("width", width) def __repr__(self): - return f"Uint({self.width}, const={self.const})" + return f"Uint({self.width})" def __hash__(self): - return hash((self.__class__, self.const, self.width)) + return hash(self.__class__) def __eq__(self, other): - return isinstance(other, Uint) and self.const == other.const and self.width == other.width + return isinstance(other, Uint) and self.width == other.width diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 9429b7844e1..210b3a72213 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -2812,8 +2812,6 @@ def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.V circuit_scope = self._current_scope() coerce_type = None if isinstance(name_or_var, expr.Var): - if name_or_var.type.const: - raise CircuitError("const variables are not supported.") if ( name_or_var.type.kind is types.Uint and isinstance(initial, int) @@ -2872,8 +2870,6 @@ def add_uninitialized_var(self, var: expr.Var, /): raise CircuitError("cannot add an uninitialized variable in a control-flow scope") if not var.standalone: raise CircuitError("cannot add a variable wrapping a bit or register to a circuit") - if var.type.const: - raise CircuitError("const variables are not supported.") self._builder_api.add_uninitialized_var(var) def add_capture(self, var: expr.Var): @@ -2939,10 +2935,6 @@ def add_input( # pylint: disable=missing-raises-doc if isinstance(name_or_var, expr.Var): if type_ is not None: raise ValueError("cannot give an explicit type with an existing Var") - if name_or_var.type.const: - raise CircuitError("const variables cannot be input variables") - elif type_ is not None and type_.const: - raise CircuitError("const variables cannot be input variables") var = self._prepare_new_var(name_or_var, type_) self._vars_input[var.name] = var diff --git a/test/python/circuit/classical/test_expr_constructors.py b/test/python/circuit/classical/test_expr_constructors.py index bcfc703347a..80f997e0f98 100644 --- a/test/python/circuit/classical/test_expr_constructors.py +++ b/test/python/circuit/classical/test_expr_constructors.py @@ -68,27 +68,15 @@ def test_value_lifts_qiskit_scalars(self): def test_value_lifts_python_builtins(self): self.assertEqual(expr.lift(True), expr.Value(True, types.Bool())) - self.assertEqual(expr.lift(True, try_const=True), expr.Value(True, types.Bool(const=True))) self.assertEqual(expr.lift(False), expr.Value(False, types.Bool())) - self.assertEqual( - expr.lift(False, try_const=True), expr.Value(False, types.Bool(const=True)) - ) self.assertEqual(expr.lift(7), expr.Value(7, types.Uint(3))) - self.assertEqual(expr.lift(7, try_const=True), expr.Value(7, types.Uint(3, const=True))) def test_value_ensures_nonzero_width(self): self.assertEqual(expr.lift(0), expr.Value(0, types.Uint(1))) - self.assertEqual(expr.lift(0, try_const=True), expr.Value(0, types.Uint(1, const=True))) def test_value_type_representation(self): self.assertEqual(expr.lift(5), expr.Value(5, types.Uint((5).bit_length()))) - self.assertEqual( - expr.lift(5, try_const=True), expr.Value(5, types.Uint((5).bit_length(), const=True)) - ) self.assertEqual(expr.lift(5, types.Uint(8)), expr.Value(5, types.Uint(8))) - self.assertEqual( - expr.lift(5, types.Uint(8, const=True)), expr.Value(5, types.Uint(8, const=True)) - ) cr = ClassicalRegister(3, "c") self.assertEqual(expr.lift(cr, types.Uint(8)), expr.Var(cr, types.Uint(8))) @@ -111,12 +99,6 @@ def test_cast_adds_explicit_nodes(self): expr.cast(base, types.Uint(8)), expr.Cast(base, types.Uint(8), implicit=False) ) - def test_cast_adds_node_when_shedding_const(self): - base = expr.Value(5, types.Uint(8, const=True)) - self.assertEqual( - expr.cast(base, types.Uint(8)), expr.Cast(base, types.Uint(8), implicit=False) - ) - def test_cast_allows_lossy_downcasting(self): """An explicit 'cast' call should allow lossy casts to be performed.""" base = expr.Value(5, types.Uint(16)) @@ -150,14 +132,6 @@ def test_bit_not_explicit(self): expr.bit_not(clbit), expr.Unary(expr.Unary.Op.BIT_NOT, expr.Var(clbit, types.Bool()), types.Bool()), ) - self.assertEqual( - expr.bit_not(expr.Value(3, types.Uint(2, const=True))), - expr.Unary( - expr.Unary.Op.BIT_NOT, - expr.Value(3, types.Uint(2, const=True)), - types.Uint(2, const=True), - ), - ) def test_logic_not_explicit(self): cr = ClassicalRegister(3) @@ -174,16 +148,6 @@ def test_logic_not_explicit(self): expr.logic_not(clbit), expr.Unary(expr.Unary.Op.LOGIC_NOT, expr.Var(clbit, types.Bool()), types.Bool()), ) - self.assertEqual( - expr.logic_not(expr.Value(3, types.Uint(2, const=True))), - expr.Unary( - expr.Unary.Op.LOGIC_NOT, - expr.Cast( - expr.Value(3, types.Uint(2, const=True)), types.Bool(const=True), implicit=True - ), - types.Bool(const=True), - ), - ) @ddt.data( (expr.bit_and, ClassicalRegister(3), ClassicalRegister(3)), @@ -220,6 +184,7 @@ def test_binary_functions_lift_scalars_const(self, function, left, right): """If one operand is an expr with a const type, the other scalar should be lifted as const. Note that logical operators (e.g. logic_and, logic_or) are excluded since these lift operands independently.""" + a = expr.Var.new("a", types.Uint(8)) self.assertEqual( function(expr.lift(left, try_const=True), right), function(expr.lift(left, try_const=True), expr.lift(right, try_const=True)), @@ -249,15 +214,6 @@ def test_binary_bitwise_explicit(self, function, opcode): opcode, expr.Value(255, types.Uint(8)), expr.Var(cr, types.Uint(8)), types.Uint(8) ), ) - self.assertEqual( - function(expr.lift(255, try_const=True), cr), - expr.Binary( - opcode, - expr.Cast(expr.Value(255, types.Uint(8, const=True)), types.Uint(8), implicit=True), - expr.Var(cr, types.Uint(8)), - types.Uint(8), - ), - ) clbit = Clbit() self.assertEqual( function(True, clbit), @@ -286,15 +242,6 @@ def test_binary_bitwise_explicit(self, function, opcode): types.Uint(8), ), ) - self.assertEqual( - function(expr.lift(255, try_const=True), 255), - expr.Binary( - opcode, - expr.Value(255, types.Uint(8, const=True)), - expr.Value(255, types.Uint(8, const=True)), - types.Uint(8, const=True), - ), - ) @ddt.data( (expr.bit_and, expr.Binary.Op.BIT_AND), @@ -376,16 +323,6 @@ def test_binary_logical_explicit(self, function, opcode): ), ) - self.assertEqual( - function(cr, expr.lift(3, try_const=True)), - expr.Binary( - opcode, - expr.Cast(expr.Var(cr, types.Uint(cr.size)), types.Bool(), implicit=True), - expr.Cast(expr.Value(3, types.Uint(2, const=True)), types.Bool(), implicit=True), - types.Bool(), - ), - ) - self.assertEqual( function(False, clbit), expr.Binary( @@ -396,17 +333,6 @@ def test_binary_logical_explicit(self, function, opcode): ), ) - # Logical operations lift their operands independently. - self.assertEqual( - function(expr.lift(False, try_const=True), 1), - expr.Binary( - opcode, - expr.Cast(expr.Value(False, types.Bool(const=True)), types.Bool(), implicit=True), - expr.Cast(expr.Value(1, types.Uint(1)), types.Bool(), implicit=True), - types.Bool(), - ), - ) - @ddt.data( (expr.equal, expr.Binary.Op.EQUAL), (expr.not_equal, expr.Binary.Op.NOT_EQUAL), @@ -433,17 +359,6 @@ def test_binary_equal_explicit(self, function, opcode): ), ) - self.assertEqual( - function(expr.lift(7, try_const=True), cr), - expr.Binary( - opcode, - # Explicit cast required to get from Uint(3) to Uint(8) - expr.Cast(expr.Value(7, types.Uint(3, const=True)), types.Uint(8), implicit=False), - expr.Var(cr, types.Uint(8)), - types.Bool(), - ), - ) - self.assertEqual( function(clbit, True), expr.Binary( @@ -454,16 +369,6 @@ def test_binary_equal_explicit(self, function, opcode): ), ) - self.assertEqual( - function(expr.lift(False, try_const=True), True), - expr.Binary( - opcode, - expr.Value(False, types.Bool(const=True)), - expr.Value(True, types.Bool(const=True)), - types.Bool(const=True), - ), - ) - @ddt.data(expr.equal, expr.not_equal) def test_binary_equal_forbidden(self, function): with self.assertRaisesRegex(TypeError, "invalid types"): @@ -472,9 +377,6 @@ def test_binary_equal_forbidden(self, function): function(ClassicalRegister(3, "c"), False) with self.assertRaisesRegex(TypeError, "invalid types"): function(5, True) - with self.assertRaisesRegex(TypeError, "invalid types"): - # No order between a smaller non-const int and larger const. - function(expr.lift(0xFF, types.Uint(8)), expr.lift(0xFFFF, types.Uint(16, const=True))) @ddt.data( (expr.less, expr.Binary.Op.LESS), @@ -503,31 +405,6 @@ def test_binary_relation_explicit(self, function, opcode): ), ) - self.assertEqual( - function(expr.lift(12, try_const=True), cr), - expr.Binary( - opcode, - # Explicit cast required to get from Uint(4) to Uint(8) - expr.Cast(expr.Value(12, types.Uint(4, const=True)), types.Uint(8), implicit=False), - expr.Var(cr, types.Uint(8)), - types.Bool(), - ), - ) - - self.assertEqual( - function(expr.lift(12, types.Uint(8, const=True)), expr.lift(12, try_const=True)), - expr.Binary( - opcode, - expr.Value(12, types.Uint(8, const=True)), - expr.Cast( - expr.Value(12, types.Uint(4, const=True)), - types.Uint(8, const=True), - implicit=False, - ), - types.Bool(const=True), - ), - ) - @ddt.data(expr.less, expr.less_equal, expr.greater, expr.greater_equal) def test_binary_relation_forbidden(self, function): with self.assertRaisesRegex(TypeError, "invalid types"): @@ -536,9 +413,6 @@ def test_binary_relation_forbidden(self, function): function(ClassicalRegister(3, "c"), False) with self.assertRaisesRegex(TypeError, "invalid types"): function(Clbit(), Clbit()) - with self.assertRaisesRegex(TypeError, "invalid types"): - # No order between a smaller non-const int and larger const. - function(expr.lift(0xFF, types.Uint(8)), expr.lift(0xFFFF, types.Uint(16, const=True))) def test_index_explicit(self): cr = ClassicalRegister(4, "c") @@ -552,24 +426,6 @@ def test_index_explicit(self): expr.index(a, cr), expr.Index(a, expr.Var(cr, types.Uint(4)), types.Bool()), ) - # The index arg gets lifted to match the const-ness of the target. - self.assertEqual( - expr.index(expr.lift(0xFF, try_const=True), 2), - expr.Index( - expr.Value(0xFF, types.Uint(8, const=True)), - expr.Value(2, types.Uint(2, const=True)), - types.Bool(const=True), - ), - ) - # ...but not the other way around. - self.assertEqual( - expr.index(expr.lift(0xFF), expr.lift(2, try_const=True)), - expr.Index( - expr.Value(0xFF, types.Uint(8)), - expr.Value(2, types.Uint(2, const=True)), - types.Bool(), - ), - ) def test_index_forbidden(self): with self.assertRaisesRegex(TypeError, "invalid types"): @@ -592,15 +448,6 @@ def test_shift_explicit(self, function, opcode): opcode, expr.Var(cr, types.Uint(8)), expr.Value(5, types.Uint(3)), types.Uint(8) ), ) - self.assertEqual( - function(cr, expr.lift(5, try_const=True)), - expr.Binary( - opcode, - expr.Var(cr, types.Uint(8)), - expr.Value(5, types.Uint(3, const=True)), - types.Uint(8), - ), - ) self.assertEqual( function(a, cr), expr.Binary(opcode, a, expr.Var(cr, types.Uint(8)), types.Uint(4)), @@ -611,38 +458,6 @@ def test_shift_explicit(self, function, opcode): opcode, expr.Value(3, types.Uint(8)), expr.Value(5, types.Uint(3)), types.Uint(8) ), ) - self.assertEqual( - function(3, 5, types.Uint(8, const=True)), - expr.Binary( - opcode, - expr.Value(3, types.Uint(8, const=True)), - expr.Value(5, types.Uint(3, const=True)), - types.Uint(8, const=True), - ), - ) - self.assertEqual( - function(expr.lift(3, try_const=True), 5, types.Uint(8, const=True)), - expr.Binary( - opcode, - expr.Cast( - expr.Value(3, types.Uint(2, const=True)), - types.Uint(8, const=True), - implicit=False, - ), - expr.Value(5, types.Uint(3, const=True)), - types.Uint(8, const=True), - ), - ) - self.assertEqual( - function(expr.lift(3, try_const=True), 5, types.Uint(8)), - expr.Binary( - opcode, - expr.Cast(expr.Value(3, types.Uint(2, const=True)), types.Uint(8), implicit=False), - # Lifts as non-const because target type types.Uint(8) is non-const. - expr.Value(5, types.Uint(3)), - types.Uint(8), - ), - ) @ddt.data(expr.shift_left, expr.shift_right) def test_shift_forbidden(self, function): diff --git a/test/python/circuit/classical/test_types_ordering.py b/test/python/circuit/classical/test_types_ordering.py index 4179d3c11da..16c3791f70f 100644 --- a/test/python/circuit/classical/test_types_ordering.py +++ b/test/python/circuit/classical/test_types_ordering.py @@ -19,98 +19,45 @@ class TestTypesOrdering(QiskitTestCase): def test_order(self): self.assertIs(types.order(types.Uint(8), types.Uint(16)), types.Ordering.LESS) - self.assertIs(types.order(types.Uint(8, const=True), types.Uint(16)), types.Ordering.LESS) - self.assertIs(types.order(types.Uint(8), types.Uint(16, const=True)), types.Ordering.NONE) - self.assertIs( - types.order(types.Uint(8, const=True), types.Uint(16, const=True)), types.Ordering.LESS - ) - self.assertIs(types.order(types.Uint(16), types.Uint(8)), types.Ordering.GREATER) - self.assertIs( - types.order(types.Uint(16), types.Uint(8, const=True)), types.Ordering.GREATER - ) - self.assertIs(types.order(types.Uint(16, const=True), types.Uint(8)), types.Ordering.NONE) - self.assertIs( - types.order(types.Uint(16, const=True), types.Uint(8, const=True)), - types.Ordering.GREATER, - ) - self.assertIs(types.order(types.Uint(8), types.Uint(8)), types.Ordering.EQUAL) - self.assertIs(types.order(types.Uint(8, const=True), types.Uint(8)), types.Ordering.LESS) - self.assertIs(types.order(types.Uint(8), types.Uint(8, const=True)), types.Ordering.GREATER) - self.assertIs( - types.order(types.Uint(8, const=True), types.Uint(8, const=True)), types.Ordering.EQUAL - ) self.assertIs(types.order(types.Bool(), types.Bool()), types.Ordering.EQUAL) - self.assertIs(types.order(types.Bool(const=True), types.Bool()), types.Ordering.LESS) - self.assertIs(types.order(types.Bool(), types.Bool(const=True)), types.Ordering.GREATER) - self.assertIs( - types.order(types.Bool(const=True), types.Bool(const=True)), types.Ordering.EQUAL - ) self.assertIs(types.order(types.Bool(), types.Uint(8)), types.Ordering.NONE) self.assertIs(types.order(types.Uint(8), types.Bool()), types.Ordering.NONE) def test_is_subtype(self): self.assertTrue(types.is_subtype(types.Uint(8), types.Uint(16))) - self.assertTrue(types.is_subtype(types.Uint(8, const=True), types.Uint(16))) self.assertFalse(types.is_subtype(types.Uint(16), types.Uint(8))) - self.assertFalse(types.is_subtype(types.Uint(16, const=True), types.Uint(8))) - self.assertFalse(types.is_subtype(types.Uint(16), types.Uint(8, const=True))) self.assertTrue(types.is_subtype(types.Uint(8), types.Uint(8))) self.assertFalse(types.is_subtype(types.Uint(8), types.Uint(8), strict=True)) - self.assertTrue(types.is_subtype(types.Uint(8, const=True), types.Uint(8), strict=True)) self.assertTrue(types.is_subtype(types.Bool(), types.Bool())) self.assertFalse(types.is_subtype(types.Bool(), types.Bool(), strict=True)) - self.assertTrue(types.is_subtype(types.Bool(const=True), types.Bool(), strict=True)) self.assertFalse(types.is_subtype(types.Bool(), types.Uint(8))) self.assertFalse(types.is_subtype(types.Uint(8), types.Bool())) def test_is_supertype(self): self.assertFalse(types.is_supertype(types.Uint(8), types.Uint(16))) - self.assertFalse(types.is_supertype(types.Uint(8, const=True), types.Uint(16))) self.assertTrue(types.is_supertype(types.Uint(16), types.Uint(8))) - self.assertTrue(types.is_supertype(types.Uint(16), types.Uint(8, const=True))) - self.assertTrue(types.is_supertype(types.Uint(16, const=True), types.Uint(8, const=True))) - self.assertFalse(types.is_supertype(types.Uint(16, const=True), types.Uint(8))) self.assertTrue(types.is_supertype(types.Uint(8), types.Uint(8))) self.assertFalse(types.is_supertype(types.Uint(8), types.Uint(8), strict=True)) - self.assertTrue(types.is_supertype(types.Uint(8), types.Uint(8, const=True), strict=True)) self.assertTrue(types.is_supertype(types.Bool(), types.Bool())) self.assertFalse(types.is_supertype(types.Bool(), types.Bool(), strict=True)) - self.assertTrue(types.is_supertype(types.Bool(), types.Bool(const=True), strict=True)) self.assertFalse(types.is_supertype(types.Bool(), types.Uint(8))) self.assertFalse(types.is_supertype(types.Uint(8), types.Bool())) def test_greater(self): self.assertEqual(types.greater(types.Uint(16), types.Uint(8)), types.Uint(16)) - self.assertEqual(types.greater(types.Uint(16), types.Uint(8, const=True)), types.Uint(16)) self.assertEqual(types.greater(types.Uint(8), types.Uint(16)), types.Uint(16)) - self.assertEqual(types.greater(types.Uint(8, const=True), types.Uint(16)), types.Uint(16)) self.assertEqual(types.greater(types.Uint(8), types.Uint(8)), types.Uint(8)) - self.assertEqual(types.greater(types.Uint(8), types.Uint(8, const=True)), types.Uint(8)) - self.assertEqual(types.greater(types.Uint(8, const=True), types.Uint(8)), types.Uint(8)) - self.assertEqual( - types.greater(types.Uint(8, const=True), types.Uint(8, const=True)), - types.Uint(8, const=True), - ) self.assertEqual(types.greater(types.Bool(), types.Bool()), types.Bool()) - self.assertEqual(types.greater(types.Bool(const=True), types.Bool()), types.Bool()) - self.assertEqual(types.greater(types.Bool(), types.Bool(const=True)), types.Bool()) - self.assertEqual( - types.greater(types.Bool(const=True), types.Bool(const=True)), types.Bool(const=True) - ) with self.assertRaisesRegex(TypeError, "no ordering"): types.greater(types.Bool(), types.Uint(8)) - with self.assertRaisesRegex(TypeError, "no ordering"): - types.greater(types.Uint(16, const=True), types.Uint(8)) - with self.assertRaisesRegex(TypeError, "no ordering"): - types.greater(types.Uint(8), types.Uint(16, const=True)) class TestTypesCastKind(QiskitTestCase): @@ -118,39 +65,6 @@ 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.Bool(const=True), types.Bool(const=True)), types.CastKind.EQUAL - ) - self.assertIs( - types.cast_kind(types.Bool(const=True), types.Bool()), types.CastKind.IMPLICIT - ) - self.assertIs(types.cast_kind(types.Bool(), types.Bool(const=True)), types.CastKind.NONE) self.assertIs(types.cast_kind(types.Uint(8), types.Bool()), types.CastKind.IMPLICIT) - self.assertIs( - types.cast_kind(types.Uint(8, const=True), types.Bool(const=True)), - types.CastKind.IMPLICIT, - ) - self.assertIs( - types.cast_kind(types.Uint(8, const=True), types.Bool()), types.CastKind.IMPLICIT - ) - self.assertIs(types.cast_kind(types.Uint(8), types.Bool(const=True)), types.CastKind.NONE) self.assertIs(types.cast_kind(types.Bool(), types.Uint(8)), types.CastKind.LOSSLESS) - self.assertIs( - types.cast_kind(types.Bool(const=True), types.Uint(8, const=True)), - types.CastKind.LOSSLESS, - ) - self.assertIs( - types.cast_kind(types.Bool(const=True), types.Uint(8)), types.CastKind.LOSSLESS - ) - self.assertIs(types.cast_kind(types.Bool(), types.Uint(8, const=True)), types.CastKind.NONE) self.assertIs(types.cast_kind(types.Uint(16), types.Uint(8)), types.CastKind.DANGEROUS) - self.assertIs( - types.cast_kind(types.Uint(16, const=True), types.Uint(8, const=True)), - types.CastKind.DANGEROUS, - ) - self.assertIs( - types.cast_kind(types.Uint(16, const=True), types.Uint(8)), types.CastKind.DANGEROUS - ) - self.assertIs( - types.cast_kind(types.Uint(16), types.Uint(8, const=True)), types.CastKind.NONE - ) diff --git a/test/python/circuit/test_circuit_vars.py b/test/python/circuit/test_circuit_vars.py index 4bb8c262143..f6916dcb72d 100644 --- a/test/python/circuit/test_circuit_vars.py +++ b/test/python/circuit/test_circuit_vars.py @@ -92,12 +92,6 @@ def test_initialise_declarations_dependencies(self): ] self.assertEqual(operations, [("store", lvalue, rvalue) for lvalue, rvalue in vars_]) - def test_initialise_declarations_rejects_const_vars(self): - a = expr.Var.new("a", types.Uint(16, const=True)) - a_init = expr.lift(12, try_const=True) - with self.assertRaisesRegex(CircuitError, "const variables.*not supported"): - QuantumCircuit(declarations=[(a, a_init)]) - def test_initialise_inputs_declarations(self): a = expr.Var.new("a", types.Uint(16)) b = expr.Var.new("b", types.Uint(16)) @@ -117,11 +111,6 @@ def test_initialise_inputs_declarations(self): ] self.assertEqual(operations, [("store", b, b_init)]) - def test_initialise_inputs_declarations_rejects_const_vars(self): - a = expr.Var.new("a", types.Uint(16, const=True)) - with self.assertRaisesRegex(CircuitError, "const variables cannot be input variables"): - QuantumCircuit(inputs=[a]) - def test_initialise_captures_declarations(self): a = expr.Var.new("a", types.Uint(16)) b = expr.Var.new("b", types.Uint(16)) @@ -148,12 +137,6 @@ def test_add_uninitialized_var(self): self.assertEqual({a}, set(qc.iter_vars())) self.assertEqual([], list(qc.data)) - def test_add_uninitialized_var_rejects_const_lvalue(self): - a = expr.Var.new("a", types.Bool(const=True)) - qc = QuantumCircuit() - with self.assertRaisesRegex(CircuitError, "const variables.*not supported"): - qc.add_uninitialized_var(a) - def test_add_var_returns_good_var(self): qc = QuantumCircuit() a = qc.add_var("a", expr.lift(True)) @@ -171,34 +154,6 @@ def test_add_var_returns_input(self): a_other = qc.add_var(a, expr.lift(True)) self.assertIs(a, a_other) - def test_add_var_rejects_const_lvalue(self): - a = expr.Var.new("a", types.Bool(const=True)) - qc = QuantumCircuit() - with self.assertRaisesRegex(CircuitError, "const variables.*not supported"): - qc.add_var(a, True) - - def test_add_var_implicitly_casts_const_rvalue(self): - a = expr.Var.new("a", types.Bool()) - qc = QuantumCircuit() - qc.add_var(a, expr.lift(True, try_const=True)) - self.assertEqual(qc.num_vars, 1) - operations = [ - (instruction.operation.name, instruction.operation.lvalue, instruction.operation.rvalue) - for instruction in qc.data - ] - self.assertEqual( - operations, - [ - ( - "store", - a, - expr.Cast( - expr.Value(True, types.Bool(const=True)), types.Bool(), implicit=True - ), - ) - ], - ) - def test_add_input_returns_good_var(self): qc = QuantumCircuit() a = qc.add_input("a", types.Bool()) @@ -216,14 +171,6 @@ def test_add_input_returns_input(self): a_other = qc.add_input(a) self.assertIs(a, a_other) - def test_add_input_rejects_const_var(self): - a = expr.Var.new("a", types.Bool(const=True)) - qc = QuantumCircuit() - with self.assertRaisesRegex(CircuitError, "const variables cannot be input variables"): - qc.add_input(a) - with self.assertRaisesRegex(CircuitError, "const variables cannot be input variables"): - qc.add_input("a", types.Bool(const=True)) - def test_cannot_have_both_inputs_and_captures(self): a = expr.Var.new("a", types.Bool()) b = expr.Var.new("b", types.Bool()) diff --git a/test/python/circuit/test_store.py b/test/python/circuit/test_store.py index 79bbf22fbb6..53f5d13a447 100644 --- a/test/python/circuit/test_store.py +++ b/test/python/circuit/test_store.py @@ -45,14 +45,6 @@ def test_implicit_cast(self): self.assertEqual(constructed.lvalue, lvalue) self.assertEqual(constructed.rvalue, expr.Cast(rvalue, types.Bool(), implicit=True)) - def test_implicit_const_cast(self): - lvalue = expr.Var.new("a", types.Bool()) - rvalue = expr.Value("b", types.Bool(const=True)) - 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()) @@ -172,15 +164,6 @@ def test_lifts_integer_literals_to_full_width(self): qc.store(a, 255) self.assertEqual(qc.data[-1].operation, Store(a, expr.Value(255, a.type))) - def test_implicitly_casts_const_scalars(self): - a = expr.Var.new("a", types.Uint(8)) - qc = QuantumCircuit(inputs=[a]) - qc.store(a, expr.lift(1, types.Uint(8, const=True))) - self.assertEqual( - qc.data[-1].operation, - Store(a, expr.Cast(expr.Value(1, types.Uint(8, const=True)), a.type, implicit=True)), - ) - def test_does_not_widen_bool_literal(self): # `bool` is a subclass of `int` in Python (except some arithmetic operations have different # semantics...). It's not in Qiskit's value type system, though. diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 69160f6bbd6..4e286d5cffe 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -2224,7 +2224,7 @@ def _control_flow_expr_circuit(self): base.ry(a, 4) base.measure(4, 2) # Use a const Uint RHS to make sure we QPY can serialize it. - with base.switch(expr.bit_and(base.cregs[0], expr.lift(2, try_const=True))) as case_: + with base.switch(expr.bit_and(base.cregs[0], expr.lift(2))) as case_: with case_(0, 1): base.cz(3, 5) with case_(case_.DEFAULT): diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index d1a34f6cd44..7b438ec6823 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -1757,49 +1757,6 @@ def test_no_unnecessary_cast(self): """ self.assertEqual(dumps(qc), expected) - def test_const_expr(self): - """Test that const-typed expressions are implicitly converted without a cast.""" - qubit = Qubit() - creg = ClassicalRegister(2, "c") - circuit = QuantumCircuit([qubit], creg) - - body = QuantumCircuit([qubit], creg) - body.x(0) - body.y(0) - - circuit.if_test(expr.lift(True, types.Bool(const=True)), body, [0], body.clbits) - circuit.if_test( - expr.equal(creg, expr.lift(1, types.Uint(2, const=True))), body, [0], body.clbits - ) - circuit.if_test( - expr.equal( - expr.lift(1, types.Uint(2, const=True)), expr.lift(2, types.Uint(2, const=True)) - ), - body, - [0], - body.clbits, - ) - test = dumps(circuit) - expected = """\ -OPENQASM 3.0; -include "stdgates.inc"; -bit[2] c; -qubit _qubit0; -if (true) { - x _qubit0; - y _qubit0; -} -if (c == 1) { - x _qubit0; - y _qubit0; -} -if (1 == 2) { - x _qubit0; - y _qubit0; -} -""" - self.assertEqual(test, expected) - def test_var_use(self): """Test that input and declared vars work in simple local scopes and can be set.""" qc = QuantumCircuit() From a15141b305291c7dee3b1fc57c5e8be4541a1a98 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Wed, 26 Feb 2025 14:13:17 -0500 Subject: [PATCH 44/53] Revert QPY testing, no longer needed. --- .../circuit/test_circuit_load_from_qpy.py | 20 ------------------- test/qpy_compat/test_qpy.py | 20 ------------------- 2 files changed, 40 deletions(-) diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 511f154d303..3fff7ddf3e0 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -1911,26 +1911,6 @@ def test_pre_v12_rejects_index(self, version): ): dump(qc, fptr, version=version) - @ddt.idata(range(QPY_COMPATIBILITY_VERSION, 14)) - def test_pre_v14_rejects_const_typed_expr(self, version): - """Test that dumping to older QPY versions rejects const-typed expressions.""" - qc = QuantumCircuit() - with qc.if_test( - expr.not_equal( - expr.equal(expr.lift(1, types.Uint(1, const=True)), 1), - expr.lift(False, types.Bool(const=True)), - ) - ): - pass - - with ( - io.BytesIO() as fptr, - self.assertRaisesRegex( - UnsupportedFeatureForVersion, "version 14 is required.*const-typed expressions" - ), - ): - dump(qc, fptr, version=version) - class TestSymengineLoadFromQPY(QiskitTestCase): """Test use of symengine in qpy set of methods.""" diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index 123b4630ceb..eeca4bb041d 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -833,24 +833,6 @@ def generate_v12_expr(): return [index, shift] -def generate_v14_expr(): - """Circuits that contain const-typed expressions, new in QPY v14.""" - from qiskit.circuit.classical import expr, types - - cr = ClassicalRegister(4, "cr") - - qc = QuantumCircuit(cr, name="const_expr") - with qc.if_test( - expr.not_equal( - expr.equal(expr.lift(1, types.Uint(1, const=True)), 1), - expr.lift(False, types.Bool(const=True)), - ) - ): - pass - - return [qc] - - def generate_circuits(version_parts, current_version, load_context=False): """Generate reference circuits. @@ -916,8 +898,6 @@ def generate_circuits(version_parts, current_version, load_context=False): if version_parts >= (1, 1, 0): output_circuits["standalone_vars.qpy"] = generate_standalone_var() output_circuits["v12_expr.qpy"] = generate_v12_expr() - if version_parts >= (2, 0, 0): - output_circuits["v14_expr.qpy"] = generate_v14_expr() return output_circuits From ca2785d9ac244a98375a27d71865e05a4e2885f0 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Wed, 26 Feb 2025 14:13:52 -0500 Subject: [PATCH 45/53] Add explicit validation of const expr. --- .../classical/test_expr_constructors.py | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/test/python/circuit/classical/test_expr_constructors.py b/test/python/circuit/classical/test_expr_constructors.py index 80f997e0f98..d0e1d74a9dc 100644 --- a/test/python/circuit/classical/test_expr_constructors.py +++ b/test/python/circuit/classical/test_expr_constructors.py @@ -168,32 +168,6 @@ def test_binary_functions_lift_scalars(self, function, left, right): self.assertEqual(function(left, right), function(left, expr.lift(right))) self.assertEqual(function(left, right), function(expr.lift(left), expr.lift(right))) - @ddt.data( - (expr.bit_and, 6, 7), - (expr.bit_or, 5, 6), - (expr.bit_xor, 255, 254), - (expr.equal, 254, 255), - (expr.not_equal, 255, 255), - (expr.less, 5, 4), - (expr.less_equal, 3, 3), - (expr.greater, 254, 255), - (expr.greater_equal, 4, 5), - ) - @ddt.unpack - def test_binary_functions_lift_scalars_const(self, function, left, right): - """If one operand is an expr with a const type, the other scalar should be lifted as const. - Note that logical operators (e.g. logic_and, logic_or) are excluded since these lift operands - independently.""" - a = expr.Var.new("a", types.Uint(8)) - self.assertEqual( - function(expr.lift(left, try_const=True), right), - function(expr.lift(left, try_const=True), expr.lift(right, try_const=True)), - ) - self.assertEqual( - function(left, expr.lift(right, try_const=True)), - function(expr.lift(left, try_const=True), expr.lift(right, try_const=True)), - ) - @ddt.data( (expr.bit_and, expr.Binary.Op.BIT_AND), (expr.bit_or, expr.Binary.Op.BIT_OR), @@ -208,12 +182,16 @@ def test_binary_bitwise_explicit(self, function, opcode): opcode, expr.Var(cr, types.Uint(8)), expr.Value(255, types.Uint(8)), types.Uint(8) ), ) + self.assertFalse(function(cr, 255).const) + self.assertEqual( function(255, cr), expr.Binary( opcode, expr.Value(255, types.Uint(8)), expr.Var(cr, types.Uint(8)), types.Uint(8) ), ) + self.assertFalse(function(255, cr).const) + clbit = Clbit() self.assertEqual( function(True, clbit), @@ -224,6 +202,8 @@ def test_binary_bitwise_explicit(self, function, opcode): types.Bool(), ), ) + self.assertFalse(function(True, clbit).const) + self.assertEqual( function(clbit, False), expr.Binary( @@ -233,6 +213,8 @@ def test_binary_bitwise_explicit(self, function, opcode): types.Bool(), ), ) + self.assertFalse(function(clbit, True).const) + self.assertEqual( function(255, 255), expr.Binary( @@ -242,6 +224,7 @@ def test_binary_bitwise_explicit(self, function, opcode): types.Uint(8), ), ) + self.assertTrue(function(255, 255).const) @ddt.data( (expr.bit_and, expr.Binary.Op.BIT_AND), @@ -312,6 +295,7 @@ def test_binary_logical_explicit(self, function, opcode): types.Bool(), ), ) + self.assertFalse(function(cr, clbit).const) self.assertEqual( function(cr, 3), @@ -322,6 +306,7 @@ def test_binary_logical_explicit(self, function, opcode): types.Bool(), ), ) + self.assertFalse(function(cr, 3).const) self.assertEqual( function(False, clbit), @@ -332,6 +317,7 @@ def test_binary_logical_explicit(self, function, opcode): types.Bool(), ), ) + self.assertFalse(function(False, clbit).const) @ddt.data( (expr.equal, expr.Binary.Op.EQUAL), @@ -348,6 +334,7 @@ def test_binary_equal_explicit(self, function, opcode): opcode, expr.Var(cr, types.Uint(8)), expr.Value(255, types.Uint(8)), types.Bool() ), ) + self.assertFalse(function(cr, 255).const) self.assertEqual( function(7, cr), @@ -358,6 +345,7 @@ def test_binary_equal_explicit(self, function, opcode): types.Bool(), ), ) + self.assertFalse(function(7, cr).const) self.assertEqual( function(clbit, True), @@ -368,6 +356,7 @@ def test_binary_equal_explicit(self, function, opcode): types.Bool(), ), ) + self.assertFalse(function(clbit, True).const) @ddt.data(expr.equal, expr.not_equal) def test_binary_equal_forbidden(self, function): @@ -394,6 +383,7 @@ def test_binary_relation_explicit(self, function, opcode): opcode, expr.Var(cr, types.Uint(8)), expr.Value(200, types.Uint(8)), types.Bool() ), ) + self.assertFalse(function(cr, 200).const) self.assertEqual( function(12, cr), @@ -404,6 +394,7 @@ def test_binary_relation_explicit(self, function, opcode): types.Bool(), ), ) + self.assertFalse(function(12, cr).const) @ddt.data(expr.less, expr.less_equal, expr.greater, expr.greater_equal) def test_binary_relation_forbidden(self, function): @@ -422,10 +413,19 @@ def test_index_explicit(self): expr.index(cr, 3), expr.Index(expr.Var(cr, types.Uint(4)), expr.Value(3, types.Uint(2)), types.Bool()), ) + self.assertFalse(expr.index(cr, 3).const) + self.assertEqual( expr.index(a, cr), expr.Index(a, expr.Var(cr, types.Uint(4)), types.Bool()), ) + self.assertFalse(expr.index(a, cr).const) + + self.assertEqual( + expr.index(255, 1), + expr.Index(expr.Value(255, types.Uint(8)), expr.Value(1, types.Uint(1)), types.Bool()), + ) + self.assertTrue(expr.index(255, 1).const) def test_index_forbidden(self): with self.assertRaisesRegex(TypeError, "invalid types"): From e902009dca211614b1f6a7bf5ff3b601227a265a Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Wed, 26 Feb 2025 14:33:49 -0500 Subject: [PATCH 46/53] Revert stuff I didn't need to touch. --- qiskit/circuit/classical/expr/constructors.py | 201 ++++++------------ qiskit/circuit/classical/types/__init__.py | 12 +- qiskit/circuit/classical/types/ordering.py | 17 +- qiskit/circuit/classical/types/types.py | 34 ++- qiskit/circuit/quantumcircuit.py | 37 ++-- .../circuit/classical/test_expr_properties.py | 8 + test/python/compiler/test_transpiler.py | 3 +- 7 files changed, 121 insertions(+), 191 deletions(-) diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index 062795125ce..96ad322befe 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -80,11 +80,7 @@ def lift_legacy_condition( def lift(value: typing.Any, /, type: types.Type | None = None) -> Expr: """Lift the given Python ``value`` to a :class:`~.expr.Value` or :class:`~.expr.Var`. - By default, lifted scalars are not const. To lift supported scalars to const-typed - expressions, specify `try_const=True`. - - If an explicit ``type`` is given, the typing in the output will reflect that, - including its const-ness. The ``try_const`` parameter is ignored when this is specified. + If an explicit ``type`` is given, the typing in the output will reflect that. Examples: Lifting simple circuit objects to be :class:`~.expr.Var` instances:: @@ -92,9 +88,9 @@ def lift(value: typing.Any, /, type: types.Type | None = None) -> Expr: >>> from qiskit.circuit import Clbit, ClassicalRegister >>> from qiskit.circuit.classical import expr >>> expr.lift(Clbit()) - Var(, Bool(const=False)) + Var(, Bool()) >>> expr.lift(ClassicalRegister(3, "c")) - Var(ClassicalRegister(3, "c"), Uint(3, const=False)) + Var(ClassicalRegister(3, "c"), Uint(3)) The type of the return value can be influenced, if the given value could be interpreted losslessly as the given type (use :func:`cast` to perform a full set of casting @@ -103,7 +99,7 @@ def lift(value: typing.Any, /, type: types.Type | None = None) -> Expr: >>> from qiskit.circuit import ClassicalRegister >>> from qiskit.circuit.classical import expr, types >>> expr.lift(ClassicalRegister(3, "c"), types.Uint(5)) - Var(ClassicalRegister(3, "c"), Uint(5, const=False)) + Var(ClassicalRegister(3, "c"), Uint(5)) >>> expr.lift(5, types.Uint(4)) Value(5, Uint(4)) """ @@ -114,12 +110,9 @@ def lift(value: typing.Any, /, type: types.Type | None = None) -> Expr: from qiskit.circuit import Clbit, ClassicalRegister # pylint: disable=cyclic-import inferred: types.Type - if value is True or value is False: - inferred = types.Bool() - constructor = Value - elif isinstance(value, Clbit): + if value is True or value is False or isinstance(value, Clbit): inferred = types.Bool() - constructor = Var + constructor = Value if value is True or value is False else Var elif isinstance(value, ClassicalRegister): inferred = types.Uint(width=value.size) constructor = Var @@ -143,23 +136,14 @@ def lift(value: typing.Any, /, type: types.Type | None = None) -> Expr: def cast(operand: typing.Any, type: types.Type, /) -> Expr: """Create an explicit cast from the given value to the given type. - This can also be used to cast-away const status. - Examples: Add an explicit cast node that explicitly casts a higher precision type to a lower precision one:: >>> from qiskit.circuit.classical import expr, types - >>> value = expr.Value(5, types.Uint(32)) + >>> value = expr.value(5, types.Uint(32)) >>> expr.cast(value, types.Uint(8)) - Cast(Value(5, types.Uint(32, const=False)), types.Uint(8, const=False), implicit=False) - - Cast-away const status:: - - >>> from qiskit.circuit.classical import expr, types - >>> value = expr.Value(5, types.Uint(32, const=True)) - >>> expr.cast(value, types.Uint(32)) - Cast(Value(5, types.Uint(32, const=True)), types.Uint(32, const=False), implicit=False) + Cast(Value(5, types.Uint(32)), types.Uint(8), implicit=False) """ operand = lift(operand) if cast_kind(operand.type, type) is CastKind.NONE: @@ -177,8 +161,7 @@ def bit_not(operand: typing.Any, /) -> Expr: >>> from qiskit.circuit import ClassicalRegister >>> from qiskit.circuit.classical import expr >>> expr.bit_not(ClassicalRegister(3, "c")) - Unary(Unary.Op.BIT_NOT, \ -Var(ClassicalRegister(3, 'c'), Uint(3, const=False)), Uint(3, const=False)) + Unary(Unary.Op.BIT_NOT, Var(ClassicalRegister(3, 'c'), Uint(3)), Uint(3)) """ operand = lift(operand) if operand.type.kind not in (types.Bool, types.Uint): @@ -198,61 +181,43 @@ def logic_not(operand: typing.Any, /) -> Expr: >>> expr.logic_not(ClassicalRegister(3, "c")) Unary(\ Unary.Op.LOGIC_NOT, \ -Cast(Var(ClassicalRegister(3, 'c'), Uint(3, const=False)), \ -Bool(const=False), implicit=True), \ -Bool(const=False)) +Cast(Var(ClassicalRegister(3, 'c'), Uint(3)), Bool(), implicit=True), \ +Bool()) """ - operand = lift(operand) - operand = _coerce_lossless(operand, types.Bool()) + operand = _coerce_lossless(lift(operand), types.Bool()) return Unary(Unary.Op.LOGIC_NOT, operand, operand.type) def _lift_binary_operands(left: typing.Any, right: typing.Any) -> tuple[Expr, Expr]: """Lift two binary operands simultaneously, inferring the widths of integer literals in either - position to match the other operand. - - Const-ness is handled as follows: - * If neither operand is an expression, both are lifted as non-const. - * If only one operand is an expression, the other is lifted with the same const-ness, if possible. - Otherwise, the returned operands will have different const-ness, and thus may require a cast node - to be interoperable. - * If both operands are expressions, they are returned as-is, and may require a cast node. - """ + position to match the other operand.""" left_int = isinstance(left, int) and not isinstance(left, bool) right_int = isinstance(right, int) and not isinstance(right, bool) if not (left_int or right_int): - # They're either both bool, or neither are, so we lift them - # independently. left = lift(left) right = lift(right) elif not right_int: - # Left is an int. right = lift(right) if right.type.kind is types.Uint: if left.bit_length() > right.type.width: raise TypeError( f"integer literal '{left}' is wider than the other operand '{right}'" ) - # Left will share const-ness of right. left = Value(left, right.type) else: left = lift(left) elif not left_int: - # Right is an int. left = lift(left) if left.type.kind is types.Uint: if right.bit_length() > left.type.width: raise TypeError( f"integer literal '{right}' is wider than the other operand '{left}'" ) - # Right will share const-ness of left. right = Value(right, left.type) else: right = lift(right) else: # Both are `int`, so we take our best case to make things work. - # If the caller needs a const type, they should lift one side to - # a const type explicitly before calling this function. uint = types.Uint(max(left.bit_length(), right.bit_length(), 1)) left = Value(left, uint) right = Value(right, uint) @@ -265,15 +230,15 @@ def _binary_bitwise(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: if left.type.kind is right.type.kind is types.Bool: type = types.Bool() elif left.type.kind is types.Uint and right.type.kind is types.Uint: - if left.type.width != right.type.width: + if left.type != right.type: raise TypeError( "binary bitwise operations are defined between unsigned integers of the same width," f" but got {left.type.width} and {right.type.width}." ) - type = types.Uint(width=left.type.width) + type = left.type else: raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") - return Binary(op, _coerce_lossless(left, type), _coerce_lossless(right, type), type) + return Binary(op, left, right, type) def bit_and(left: typing.Any, right: typing.Any, /) -> Expr: @@ -288,9 +253,9 @@ def bit_and(left: typing.Any, right: typing.Any, /) -> Expr: >>> expr.bit_and(ClassicalRegister(3, "c"), 0b111) Binary(\ Binary.Op.BIT_AND, \ -Var(ClassicalRegister(3, 'c'), Uint(3, const=False)), \ -Value(7, Uint(3, const=False)), \ -Uint(3, const=False)) +Var(ClassicalRegister(3, 'c'), Uint(3)), \ +Value(7, Uint(3)), \ +Uint(3)) """ return _binary_bitwise(Binary.Op.BIT_AND, left, right) @@ -307,9 +272,9 @@ def bit_or(left: typing.Any, right: typing.Any, /) -> Expr: >>> expr.bit_or(ClassicalRegister(3, "c"), 0b101) Binary(\ Binary.Op.BIT_OR, \ -Var(ClassicalRegister(3, 'c'), Uint(3, const=False)), \ -Value(5, Uint(3, const=False)), \ -Uint(3, const=False)) +Var(ClassicalRegister(3, 'c'), Uint(3)), \ +Value(5, Uint(3)), \ +Uint(3)) """ return _binary_bitwise(Binary.Op.BIT_OR, left, right) @@ -326,20 +291,18 @@ def bit_xor(left: typing.Any, right: typing.Any, /) -> Expr: >>> expr.bit_xor(ClassicalRegister(3, "c"), 0b101) Binary(\ Binary.Op.BIT_XOR, \ -Var(ClassicalRegister(3, 'c'), Uint(3, const=False)), \ -Value(5, Uint(3, const=False)), \ -Uint(3, const=False)) +Var(ClassicalRegister(3, 'c'), Uint(3)), \ +Value(5, Uint(3)), \ +Uint(3)) """ return _binary_bitwise(Binary.Op.BIT_XOR, left, right) def _binary_logical(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: - left = lift(left) - right = lift(right) - type = types.Bool() - left = _coerce_lossless(left, type) - right = _coerce_lossless(right, type) - return Binary(op, left, right, type) + bool_ = types.Bool() + left = _coerce_lossless(lift(left), bool_) + right = _coerce_lossless(lift(right), bool_) + return Binary(op, left, right, bool_) def logic_and(left: typing.Any, right: typing.Any, /) -> Expr: @@ -351,11 +314,8 @@ def logic_and(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit import Clbit >>> from qiskit.circuit.classical import expr - >>> expr.logic_and(Clbit(), Clbit()) - Binary(Binary.Op.LOGIC_AND, \ -Var(, Bool(const=False)), \ -Var(, Bool(const=False)), \ -Bool(const=False)) + >>> expr.logical_and(Clbit(), Clbit()) + Binary(Binary.Op.LOGIC_AND, Var(, Bool()), Var(, Bool()), Bool()) """ return _binary_logical(Binary.Op.LOGIC_AND, left, right) @@ -370,28 +330,17 @@ def logic_or(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit import Clbit >>> from qiskit.circuit.classical import expr >>> expr.logical_and(Clbit(), Clbit()) - Binary(Binary.Op.LOGIC_OR, \ -Var(, Bool(const=False)), \ -Var(, Bool(const=False)), \ -Bool(const=False)) + Binary(Binary.Op.LOGIC_OR, Var(, Bool()), Var(, Bool()), Bool()) """ return _binary_logical(Binary.Op.LOGIC_OR, left, right) def _equal_like(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: left, right = _lift_binary_operands(left, right) - if ( - left.type.kind is not right.type.kind - or types.order(left.type, right.type) is types.Ordering.NONE - ): + if left.type.kind is not right.type.kind: raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") type = types.greater(left.type, right.type) - return Binary( - op, - _coerce_lossless(left, type), - _coerce_lossless(right, type), - types.Bool(), - ) + return Binary(op, _coerce_lossless(left, type), _coerce_lossless(right, type), types.Bool()) def equal(left: typing.Any, right: typing.Any, /) -> Expr: @@ -405,9 +354,9 @@ def equal(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit.classical import expr >>> expr.equal(ClassicalRegister(3, "c"), 7) Binary(Binary.Op.EQUAL, \ -Var(ClassicalRegister(3, "c"), Uint(3, const=False)), \ -Value(7, Uint(3, const=False)), \ -Uint(3, const=False)) +Var(ClassicalRegister(3, "c"), Uint(3)), \ +Value(7, Uint(3)), \ +Uint(3)) """ return _equal_like(Binary.Op.EQUAL, left, right) @@ -423,28 +372,19 @@ def not_equal(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit.classical import expr >>> expr.not_equal(ClassicalRegister(3, "c"), 7) Binary(Binary.Op.NOT_EQUAL, \ -Var(ClassicalRegister(3, "c"), Uint(3, const=False)), \ -Value(7, Uint(3, const=False)), \ -Uint(3, const=False)) +Var(ClassicalRegister(3, "c"), Uint(3)), \ +Value(7, Uint(3)), \ +Uint(3)) """ return _equal_like(Binary.Op.NOT_EQUAL, left, right) def _binary_relation(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: left, right = _lift_binary_operands(left, right) - if ( - left.type.kind is not right.type.kind - or left.type.kind is types.Bool - or types.order(left.type, right.type) is types.Ordering.NONE - ): + if left.type.kind is not right.type.kind or left.type.kind is types.Bool: raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") type = types.greater(left.type, right.type) - return Binary( - op, - _coerce_lossless(left, type), - _coerce_lossless(right, type), - types.Bool(), - ) + return Binary(op, _coerce_lossless(left, type), _coerce_lossless(right, type), types.Bool()) def less(left: typing.Any, right: typing.Any, /) -> Expr: @@ -458,9 +398,9 @@ def less(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit.classical import expr >>> expr.less(ClassicalRegister(3, "c"), 5) Binary(Binary.Op.LESS, \ -Var(ClassicalRegister(3, "c"), Uint(3, const=False)), \ -Value(5, Uint(3, const=False)), \ -Uint(3, const=False)) +Var(ClassicalRegister(3, "c"), Uint(3)), \ +Value(5, Uint(3)), \ +Uint(3)) """ return _binary_relation(Binary.Op.LESS, left, right) @@ -476,9 +416,9 @@ def less_equal(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit.classical import expr >>> expr.less(ClassicalRegister(3, "a"), ClassicalRegister(3, "b")) Binary(Binary.Op.LESS_EQUAL, \ -Var(ClassicalRegister(3, "a"), Uint(3, const=False)), \ -Var(ClassicalRegister(3, "b"), Uint(3, const=False)), \ -Uint(3,const=False)) +Var(ClassicalRegister(3, "a"), Uint(3)), \ +Var(ClassicalRegister(3, "b"), Uint(3)), \ +Uint(3)) """ return _binary_relation(Binary.Op.LESS_EQUAL, left, right) @@ -494,9 +434,9 @@ def greater(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit.classical import expr >>> expr.less(ClassicalRegister(3, "c"), 5) Binary(Binary.Op.GREATER, \ -Var(ClassicalRegister(3, "c"), Uint(3, const=False)), \ -Value(5, Uint(3, const=False)), \ -Uint(3, const=False)) +Var(ClassicalRegister(3, "c"), Uint(3)), \ +Value(5, Uint(3)), \ +Uint(3)) """ return _binary_relation(Binary.Op.GREATER, left, right) @@ -512,9 +452,9 @@ def greater_equal(left: typing.Any, right: typing.Any, /) -> Expr: >>> from qiskit.circuit.classical import expr >>> expr.less(ClassicalRegister(3, "a"), ClassicalRegister(3, "b")) Binary(Binary.Op.GREATER_EQUAL, \ -Var(ClassicalRegister(3, "a"), Uint(3, const=False)), \ -Var(ClassicalRegister(3, "b"), Uint(3, const=False)), \ -Uint(3, const=False)) +Var(ClassicalRegister(3, "a"), Uint(3)), \ +Var(ClassicalRegister(3, "b"), Uint(3)), \ +Uint(3)) """ return _binary_relation(Binary.Op.GREATER_EQUAL, left, right) @@ -531,12 +471,7 @@ def _shift_like( right = lift(right) if left.type.kind != types.Uint or right.type.kind != types.Uint: raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") - return Binary( - op, - left, - right, - types.Uint(width=left.type.width), - ) + return Binary(op, left, right, left.type) def shift_left(left: typing.Any, right: typing.Any, /, type: types.Type | None = None) -> Expr: @@ -552,17 +487,17 @@ def shift_left(left: typing.Any, right: typing.Any, /, type: types.Type | None = >>> a = expr.Var.new("a", types.Uint(8)) >>> expr.shift_left(a, 4) Binary(Binary.Op.SHIFT_LEFT, \ -Var(, Uint(8, const=False), name='a'), \ -Value(4, Uint(3, const=False)), \ -Uint(8, const=False)) +Var(, Uint(8), name='a'), \ +Value(4, Uint(3)), \ +Uint(8)) Shift an integer literal by a variable amount, coercing the type of the literal:: >>> expr.shift_left(3, a, types.Uint(16)) Binary(Binary.Op.SHIFT_LEFT, \ -Value(3, Uint(16, const=False)), \ -Var(, Uint(8, const=False), name='a'), \ -Uint(16, const=False)) +Value(3, Uint(16)), \ +Var(, Uint(8), name='a'), \ +Uint(16)) """ return _shift_like(Binary.Op.SHIFT_LEFT, left, right, type) @@ -580,9 +515,9 @@ def shift_right(left: typing.Any, right: typing.Any, /, type: types.Type | None >>> from qiskit.circuit.classical import expr >>> expr.shift_right(ClassicalRegister(8, "a"), 4) Binary(Binary.Op.SHIFT_RIGHT, \ -Var(ClassicalRegister(8, "a"), Uint(8, const=False)), \ -Value(4, Uint(3, const=False)), \ -Uint(8, const=False)) +Var(ClassicalRegister(8, "a"), Uint(8)), \ +Value(4, Uint(3)), \ +Uint(8)) """ return _shift_like(Binary.Op.SHIFT_RIGHT, left, right, type) @@ -599,13 +534,9 @@ def index(target: typing.Any, index: typing.Any, /) -> Expr: >>> from qiskit.circuit import ClassicalRegister >>> from qiskit.circuit.classical import expr >>> expr.index(ClassicalRegister(8, "a"), 3) - Index(\ -Var(ClassicalRegister(8, "a"), Uint(8, const=False)), \ -Value(3, Uint(2, const=False)), \ -Bool(const=False)) + Index(Var(ClassicalRegister(8, "a"), Uint(8)), Value(3, Uint(2)), Bool()) """ - target = lift(target) - index = lift(index) + target, index = lift(target), lift(index) if target.type.kind is not types.Uint or index.type.kind is not types.Uint: raise TypeError(f"invalid types for indexing: '{target.type}' and '{index.type}'") return Index(target, index, types.Bool()) diff --git a/qiskit/circuit/classical/types/__init__.py b/qiskit/circuit/classical/types/__init__.py index d10a4769ca5..ae38a0d97fb 100644 --- a/qiskit/circuit/classical/types/__init__.py +++ b/qiskit/circuit/classical/types/__init__.py @@ -40,13 +40,6 @@ .. autoclass:: Bool .. autoclass:: Uint -All types have a :attr:`~Type.const` field to indicate their const-ness. When the result type of -an expression is constant, the expression is considered a constant expression. Constant -expressions can be used in certain contexts that aren't valid for runtime-initialized variables. -This is not to be confused with the concept of a ``const`` variable in languages like C, where -the variable has a well-defined but immutable storage location. Qiskit's definition of const-ness -is more similar to C++'s ``constexpr``. - Note that :class:`Uint` defines a family of types parametrized by their width; it is not one single type, which may be slightly different to the 'classical' programming languages you are used to. @@ -66,10 +59,7 @@ 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 -ordering defines when one type may be losslessly directly interpreted as another. - -When two types differ only in const-ness, the non-const version is considered to be the -"greater" of the two. +ordering defines when one type may be lossless directly interpreted as another. The low-level interface to querying the subtyping relationship is the :func:`order` function. diff --git a/qiskit/circuit/classical/types/ordering.py b/qiskit/circuit/classical/types/ordering.py index 7e232880d7f..5a4365b8e14 100644 --- a/qiskit/circuit/classical/types/ordering.py +++ b/qiskit/circuit/classical/types/ordering.py @@ -83,18 +83,10 @@ def order(left: Type, right: Type, /) -> Ordering: >>> types.order(types.Uint(8), types.Uint(16)) Ordering.LESS - Compare two :class:`Bool` types of differing const-ness:: - - >>> from qiskit.circuit.classical import types - >>> types.order(types.Bool(), types.Bool(const=True)) - Ordering.GREATER - Compare two types that have no ordering between them:: >>> types.order(types.Uint(8), types.Bool()) Ordering.NONE - >>> types.order(types.Uint(8), types.Uint(16, const=True)) - Ordering.NONE """ if (orderer := _ORDERERS.get((left.kind, right.kind))) is None: return Ordering.NONE @@ -119,8 +111,6 @@ def is_subtype(left: Type, right: Type, /, strict: bool = False) -> bool: True >>> types.is_subtype(types.Bool(), types.Bool(), strict=True) False - >>> types.is_subtype(types.Bool(const=True), types.Bool(), strict=True) - True """ order_ = order(left, right) return order_ is Ordering.LESS or (not strict and order_ is Ordering.EQUAL) @@ -144,8 +134,6 @@ def is_supertype(left: Type, right: Type, /, strict: bool = False) -> bool: True >>> types.is_supertype(types.Bool(), types.Bool(), strict=True) False - >>> types.is_supertype(types.Bool(), types.Bool(const=True), strict=True) - True """ order_ = order(left, right) return order_ is Ordering.GREATER or (not strict and order_ is Ordering.EQUAL) @@ -227,8 +215,6 @@ def cast_kind(from_: Type, to_: Type, /) -> CastKind: >>> types.cast_kind(types.Uint(8), types.Bool()) - >>> types.cast_kind(types.Uint(8, const=True), types.Uint(8)) - >>> types.cast_kind(types.Bool(), types.Uint(8)) >>> types.cast_kind(types.Uint(16), types.Uint(8)) @@ -236,5 +222,4 @@ def cast_kind(from_: Type, to_: Type, /) -> CastKind: """ if (coercer := _ALLOWED_CASTS.get((from_.kind, to_.kind))) is None: return CastKind.NONE - cast_kind_ = coercer(from_, to_) - return cast_kind_ + return coercer(from_, to_) diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py index c65e063f52e..d20e7b5fd74 100644 --- a/qiskit/circuit/classical/types/types.py +++ b/qiskit/circuit/classical/types/types.py @@ -19,22 +19,42 @@ from __future__ import annotations -__all__ = ["Type", "Bool", "Uint"] +__all__ = [ + "Type", + "Bool", + "Uint", +] import typing +class _Singleton(type): + """Metaclass to make the child, which should take zero initialization arguments, a singleton + object.""" + + def _get_singleton_instance(cls): + return cls._INSTANCE + + @classmethod + def __prepare__(mcs, name, bases): # pylint: disable=unused-argument + return {"__new__": mcs._get_singleton_instance} + + @staticmethod + def __new__(cls, name, bases, namespace): + out = super().__new__(cls, name, bases, namespace) + out._INSTANCE = object.__new__(out) # pylint: disable=invalid-name + return out + + class Type: """Root base class of all nodes in the type tree. The base case should never be instantiated directly. This must not be subclassed by users; subclasses form the internal data of the representation of - expressions, and it does not make sense to add more outside of Qiskit library code. - """ + expressions, and it does not make sense to add more outside of Qiskit library code.""" __slots__ = () - @property def kind(self): """Get the kind of this type. This is exactly equal to the Python type object that defines @@ -61,13 +81,13 @@ def __setstate__(self, state): @typing.final -class Bool(Type): +class Bool(Type, metaclass=_Singleton): """The Boolean type. This has exactly two values: ``True`` and ``False``.""" __slots__ = () def __repr__(self): - return f"Bool()" + return "Bool()" def __hash__(self): return hash(self.__class__) @@ -91,7 +111,7 @@ def __repr__(self): return f"Uint({self.width})" def __hash__(self): - return hash(self.__class__) + 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 210b3a72213..e14c7aaf64a 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -2751,14 +2751,13 @@ def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.V Args: name_or_var: either a string of the variable name, or an existing instance of - a non-const-typed :class:`~.expr.Var` to re-use. Variables cannot shadow names - that are already in use within the circuit. + :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. If a const-typed expression is provided, it - will be automatically cast to its non-const counterpart. + 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`. @@ -2768,8 +2767,7 @@ def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.V object will be returned. Raises: - CircuitError: if the variable cannot be created due to shadowing an existing variable - or a const variable was specified for ``name_or_var``. + 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:: @@ -2810,16 +2808,17 @@ def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.V # Validate the initializer first to catch cases where the variable to be declared is being # used in the initializer. circuit_scope = self._current_scope() - coerce_type = None - if isinstance(name_or_var, expr.Var): - if ( - name_or_var.type.kind is types.Uint - and isinstance(initial, int) - and not isinstance(initial, bool) - ): - # Convenience method to widen Python integer literals to the right width during - # the initial lift, if the type is already known via the variable. - coerce_type = name_or_var.type + # Convenience method to widen Python integer literals to the right width during the initial + # lift, if the type is already known via the variable. + if ( + isinstance(name_or_var, expr.Var) + and name_or_var.type.kind is types.Uint + and isinstance(initial, int) + and not isinstance(initial, bool) + ): + coerce_type = name_or_var.type + else: + coerce_type = None initial = _validate_expr(circuit_scope, expr.lift(initial, coerce_type)) if isinstance(name_or_var, str): var = expr.Var.new(name_or_var, initial.type) @@ -2932,10 +2931,8 @@ def add_input( # pylint: disable=missing-raises-doc raise CircuitError("cannot add an input variable in a control-flow scope") if self._vars_capture: raise CircuitError("circuits to be enclosed with captures cannot have input variables") - if isinstance(name_or_var, expr.Var): - if type_ is not None: - raise ValueError("cannot give an explicit type with an existing Var") - + 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 diff --git a/test/python/circuit/classical/test_expr_properties.py b/test/python/circuit/classical/test_expr_properties.py index 60a4b1f9080..625db22cc12 100644 --- a/test/python/circuit/classical/test_expr_properties.py +++ b/test/python/circuit/classical/test_expr_properties.py @@ -25,6 +25,14 @@ @ddt.ddt class TestExprProperties(QiskitTestCase): + def test_bool_type_is_singleton(self): + """The `Bool` type is meant (and used) as a Python singleton object for efficiency. It must + always be referentially equal to all other references to it.""" + self.assertIs(types.Bool(), types.Bool()) + self.assertIs(types.Bool(), copy.copy(types.Bool())) + self.assertIs(types.Bool(), copy.deepcopy(types.Bool())) + self.assertIs(types.Bool(), pickle.loads(pickle.dumps(types.Bool()))) + @ddt.data(types.Bool(), types.Uint(8)) def test_types_can_be_cloned(self, obj): """Test that various ways of cloning a `Type` object are valid and produce equal output.""" diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 4e286d5cffe..99ee3fac321 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -2223,8 +2223,7 @@ def _control_flow_expr_circuit(self): base.append(CustomCX(), [2, 4]) base.ry(a, 4) base.measure(4, 2) - # Use a const Uint RHS to make sure we QPY can serialize it. - with base.switch(expr.bit_and(base.cregs[0], expr.lift(2))) as case_: + with base.switch(expr.bit_and(base.cregs[0], 2)) as case_: with case_(0, 1): base.cz(3, 5) with case_(case_.DEFAULT): From 16475c3a293465bad9bfa0733f403fe95a147086 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Wed, 26 Feb 2025 14:44:27 -0500 Subject: [PATCH 47/53] Update release note. --- .../notes/const-expr-397ff09042942b81.yaml | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/releasenotes/notes/const-expr-397ff09042942b81.yaml b/releasenotes/notes/const-expr-397ff09042942b81.yaml index 5c6f6352828..7f47f416674 100644 --- a/releasenotes/notes/const-expr-397ff09042942b81.yaml +++ b/releasenotes/notes/const-expr-397ff09042942b81.yaml @@ -2,20 +2,23 @@ features_circuits: - | The classical realtime-expressions module :mod:`qiskit.circuit.classical` can now represent - constant types and operations on them. All :class:`~.types.Type` classes now have a bool - :attr:`~.types.Type.const` property which is used to mark const-ness and enforce const - invariants across the types system. + constant expressions. The :class:`~.expr.Expr` class now has a bool + :attr:`~.expr.Expr.const` attribute which indicates the expression's const-ness. This allows + us to enforce that expressions in certain contexts must be possible to evaluate at compile-time. - To create a const value expression use :func:`~.expr.lift`, setting ``try_const=True``:: + All :class:`~.expr.Var` expressions are considered to be non-const, while all :class:`~.expr.Value` + expressions are const. + + An expression comprised only of other const expressions is also const:: from qiskit.circuit.classical import expr - expr.lift(5, try_const=True) - # Value(5, Uint(3, const=True)) + expr.bit_and(5, 6).const + # True - The result type of an operation applied to const types is also const:: + An expression that contains any non-const expression is non-const:: - from qiskit.circuit.classical import expr + from qiskit.circuit.classical import expr, types - expr.bit_and(expr.lift(5, try_const=True), expr.lift(6, try_const=True)).type.const - # True + expr.bit_and(5, expr.Var.new("a", types.Uint(5)).const + # False From 6b5930d2ef6ff977297c7cc4981911ce90b6671d Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Thu, 27 Feb 2025 11:18:27 -0500 Subject: [PATCH 48/53] A few finishing touches. --- qiskit/circuit/classical/expr/__init__.py | 6 +++++- qiskit/circuit/classical/expr/expr.py | 3 +++ test/python/circuit/classical/test_expr_constructors.py | 5 +++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/qiskit/circuit/classical/expr/__init__.py b/qiskit/circuit/classical/expr/__init__.py index 00f1c2e0676..44821dc7f9f 100644 --- a/qiskit/circuit/classical/expr/__init__.py +++ b/qiskit/circuit/classical/expr/__init__.py @@ -39,6 +39,10 @@ These objects are mutable and should not be reused in a different location without a copy. +All :class`Expr` instances define a boolean :attr:`~Expr.const` attribute, which indicates +whether the expression can be evaluated at compile-time. Most expression classes infer this +during construction based on the const-ness of their operands. + The base for dynamic variables is the :class:`Var`, which can be either an arbitrarily typed real-time variable, or a wrapper around a :class:`.Clbit` or :class:`.ClassicalRegister`. @@ -46,7 +50,7 @@ :members: var, name, new Similarly, literals used in expressions (such as integers) should be lifted to :class:`Value` nodes -with associated types. +with associated types. A :class:`Value` is always considered a constant expression. .. autoclass:: Value diff --git a/qiskit/circuit/classical/expr/expr.py b/qiskit/circuit/classical/expr/expr.py index 58fad02612e..dbb0f7e3b07 100644 --- a/qiskit/circuit/classical/expr/expr.py +++ b/qiskit/circuit/classical/expr/expr.py @@ -190,6 +190,7 @@ def __getstate__(self): def __setstate__(self, state): var, type, name = state super().__setattr__("type", type) + super().__setattr__("const", False) super().__setattr__("var", var) super().__setattr__("name", name) @@ -365,6 +366,7 @@ def __eq__(self, other): return ( isinstance(other, Binary) and self.type == other.type + and self.const == other.const and self.op is other.op and self.left == other.left and self.right == other.right @@ -399,6 +401,7 @@ def __eq__(self, other): return ( isinstance(other, Index) and self.type == other.type + and self.const == other.const and self.target == other.target and self.index == other.index ) diff --git a/test/python/circuit/classical/test_expr_constructors.py b/test/python/circuit/classical/test_expr_constructors.py index d0e1d74a9dc..ff172d7e000 100644 --- a/test/python/circuit/classical/test_expr_constructors.py +++ b/test/python/circuit/classical/test_expr_constructors.py @@ -448,16 +448,21 @@ def test_shift_explicit(self, function, opcode): opcode, expr.Var(cr, types.Uint(8)), expr.Value(5, types.Uint(3)), types.Uint(8) ), ) + self.assertFalse(function(cr, 5).const) + self.assertEqual( function(a, cr), expr.Binary(opcode, a, expr.Var(cr, types.Uint(8)), types.Uint(4)), ) + self.assertFalse(function(a, cr).const) + self.assertEqual( function(3, 5, types.Uint(8)), expr.Binary( opcode, expr.Value(3, types.Uint(8)), expr.Value(5, types.Uint(3)), types.Uint(8) ), ) + self.assertTrue(function(3, 5, types.Uint(8)).const) @ddt.data(expr.shift_left, expr.shift_right) def test_shift_forbidden(self, function): From 25f16933a5142c32d73e39a1365b09fbf4a75704 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Thu, 27 Feb 2025 13:56:39 -0500 Subject: [PATCH 49/53] Fix-up after merge. --- qiskit/circuit/classical/expr/constructors.py | 9 ++-- qiskit/circuit/classical/types/types.py | 2 +- qiskit/qpy/binary_io/circuits.py | 2 +- qiskit/qpy/binary_io/value.py | 50 +++++++++---------- qiskit/qpy/common.py | 2 +- .../classical/test_expr_constructors.py | 4 +- .../circuit/classical/test_types_ordering.py | 2 - test/python/qasm3/test_export.py | 3 ++ 8 files changed, 38 insertions(+), 36 deletions(-) diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index d16b0c8796c..65db45a843b 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -192,8 +192,9 @@ def logic_not(operand: typing.Any, /) -> Expr: Bool(const=False), implicit=True), \ Bool(const=False)) """ + operand = lift(operand) try: - operand = _coerce_lossless(lift(operand), types.Bool()) + operand = _coerce_lossless(operand, types.Bool()) return Unary(Unary.Op.LOGIC_NOT, operand, operand.type) except TypeError as ex: raise TypeError(f"cannot apply '{Unary.Op.BIT_NOT}' to type '{operand.type}'") from ex @@ -311,9 +312,11 @@ def bit_xor(left: typing.Any, right: typing.Any, /) -> Expr: def _binary_logical(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: bool_ = types.Bool() + left = lift(left) + right = lift(right) try: - left = _coerce_lossless(lift(left), bool_) - right = _coerce_lossless(lift(right), bool_) + left = _coerce_lossless(left, bool_) + right = _coerce_lossless(right, bool_) return Binary(op, left, right, bool_) except TypeError as ex: raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") from ex diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py index d897939e0bc..06328c280e2 100644 --- a/qiskit/circuit/classical/types/types.py +++ b/qiskit/circuit/classical/types/types.py @@ -122,7 +122,7 @@ class Float(Type, metaclass=_Singleton): __slots__ = () def __repr__(self): - return f"Float()" + return "Float()" def __hash__(self): return hash(self.__class__) diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 2297dbf6d34..6b9f253b367 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -1236,7 +1236,7 @@ def write_circuit( file_obj.write(metadata_raw) # Write header payload file_obj.write(registers_raw) - standalone_var_indices = value.write_standalone_vars(file_obj, circuit) + standalone_var_indices = value.write_standalone_vars(file_obj, circuit, version) else: if circuit.num_vars: raise exceptions.UnsupportedFeatureForVersion( diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 82109bb5c9b..1e1bb61e8db 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -261,12 +261,15 @@ def __init__(self, file_obj, clbit_indices, standalone_var_indices, version): self.standalone_var_indices = standalone_var_indices self.version = version + def _write_expr_type(self, type_, /): + _write_expr_type(self.file_obj, type_, self.version) + def visit_generic(self, node, /): raise exceptions.QpyError(f"unhandled Expr object '{node}'") def visit_var(self, node, /): self.file_obj.write(type_keys.Expression.VAR) - _write_expr_type(self.file_obj, node.type) + self._write_expr_type(node.type) if node.standalone: self.file_obj.write(type_keys.ExprVar.UUID) self.file_obj.write( @@ -296,7 +299,7 @@ def visit_var(self, node, /): def visit_value(self, node, /): self.file_obj.write(type_keys.Expression.VALUE) - _write_expr_type(self.file_obj, node.type) + self._write_expr_type(node.type) if node.value is True or node.value is False: self.file_obj.write(type_keys.ExprValue.BOOL) self.file_obj.write( @@ -327,7 +330,7 @@ def visit_value(self, node, /): def visit_cast(self, node, /): self.file_obj.write(type_keys.Expression.CAST) - _write_expr_type(self.file_obj, node.type) + self._write_expr_type(node.type) self.file_obj.write( struct.pack(formats.EXPRESSION_CAST_PACK, *formats.EXPRESSION_CAST(node.implicit)) ) @@ -335,7 +338,7 @@ def visit_cast(self, node, /): def visit_unary(self, node, /): self.file_obj.write(type_keys.Expression.UNARY) - _write_expr_type(self.file_obj, node.type) + self._write_expr_type(node.type) self.file_obj.write( struct.pack(formats.EXPRESSION_UNARY_PACK, *formats.EXPRESSION_UNARY(node.op.value)) ) @@ -343,7 +346,7 @@ def visit_unary(self, node, /): def visit_binary(self, node, /): self.file_obj.write(type_keys.Expression.BINARY) - _write_expr_type(self.file_obj, node.type) + self._write_expr_type(node.type) self.file_obj.write( struct.pack(formats.EXPRESSION_BINARY_PACK, *formats.EXPRESSION_BINARY(node.op.value)) ) @@ -356,7 +359,7 @@ def visit_index(self, node, /): "the 'Index' expression", required=12, target=self.version ) self.file_obj.write(type_keys.Expression.INDEX) - _write_expr_type(self.file_obj, node.type) + self._write_expr_type(node.type) node.target.accept(self) node.index.accept(self) @@ -371,16 +374,7 @@ def _write_expr( node.accept(_ExprWriter(file_obj, clbit_indices, standalone_var_indices, version)) -def _write_expr_type(file_obj, type_: types.Type): - if type_.kind is types.Float: - # TODO: actually support this when QPY version is new enough - raise exceptions.UnsupportedFeatureForVersion( - "float-typed expressions", required=14, target=version - ) - file_obj.write(type_keys.ExprType.FLOAT) - file_obj.write( - struct.pack(formats.EXPR_TYPE_FLOAT_PACK, *formats.EXPR_TYPE_FLOAT(type_.const)) - ) +def _write_expr_type(file_obj, type_: types.Type, version: int): if type_.kind is types.Bool: file_obj.write(type_keys.ExprType.BOOL) elif type_.kind is types.Uint: @@ -388,6 +382,12 @@ def _write_expr_type(file_obj, type_: types.Type): file_obj.write( struct.pack(formats.EXPR_TYPE_UINT_PACK, *formats.EXPR_TYPE_UINT(type_.width)) ) + elif type_.kind is types.Float: + if version < 14: + raise exceptions.UnsupportedFeatureForVersion( + "float-typed expressions", required=14, target=version + ) + file_obj.write(type_keys.ExprType.FLOAT) else: raise exceptions.QpyError(f"unhandled Type object '{type_};") @@ -750,10 +750,7 @@ def _read_expr_type(file_obj) -> types.Type: struct.unpack(formats.EXPR_TYPE_UINT_PACK, file_obj.read(formats.EXPR_TYPE_UINT_SIZE)) ) return types.Uint(elem.width) - if type_key == type_keys.ExprType.FLOAT: - elem = formats.EXPR_TYPE_FLOAT._make( - struct.unpack(formats.EXPR_TYPE_FLOAT_PACK, file_obj.read(formats.EXPR_TYPE_FLOAT_SIZE)) - ) + if type_key == type_keys.ExprType.FLOAT: return types.Float() raise exceptions.QpyError(f"Invalid classical-expression Type key '{type_key}'") @@ -791,7 +788,7 @@ def read_standalone_vars(file_obj, num_vars): return read_vars, var_order -def _write_standalone_var(file_obj, var, type_key): +def _write_standalone_var(file_obj, var, type_key, version): name = var.name.encode(common.ENCODE) file_obj.write( struct.pack( @@ -799,16 +796,17 @@ def _write_standalone_var(file_obj, var, type_key): *formats.EXPR_VAR_DECLARATION(var.var.bytes, type_key, len(name)), ) ) - _write_expr_type(file_obj, var.type) + _write_expr_type(file_obj, var.type, version) file_obj.write(name) -def write_standalone_vars(file_obj, circuit): +def write_standalone_vars(file_obj, circuit, version): """Write the standalone variables out from a circuit. Args: file_obj (File): the file-like object to write to. circuit (QuantumCircuit): the circuit to take the variables from. + version (int): the QPY target version. Returns: dict[expr.Var, int]: a mapping of the variables written to the index that they were written @@ -817,15 +815,15 @@ def write_standalone_vars(file_obj, circuit): index = 0 out = {} for var in circuit.iter_input_vars(): - _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.INPUT) + _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.INPUT, version) out[var] = index index += 1 for var in circuit.iter_captured_vars(): - _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.CAPTURE) + _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.CAPTURE, version) out[var] = index index += 1 for var in circuit.iter_declared_vars(): - _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.LOCAL) + _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.LOCAL, version) out[var] = index index += 1 return out diff --git a/qiskit/qpy/common.py b/qiskit/qpy/common.py index c2585d5be4d..0f30e90cc34 100644 --- a/qiskit/qpy/common.py +++ b/qiskit/qpy/common.py @@ -25,7 +25,7 @@ from qiskit.qpy import formats, exceptions -QPY_VERSION = 13 +QPY_VERSION = 14 QPY_COMPATIBILITY_VERSION = 10 ENCODE = "utf8" diff --git a/test/python/circuit/classical/test_expr_constructors.py b/test/python/circuit/classical/test_expr_constructors.py index 103f796019c..6b60c98c755 100644 --- a/test/python/circuit/classical/test_expr_constructors.py +++ b/test/python/circuit/classical/test_expr_constructors.py @@ -401,7 +401,7 @@ def test_binary_equal_explicit(self, function, opcode): opcode, expr.Value(7.0, types.Float()), expr.Value(7.0, types.Float()), - types.Bool(const=True), + types.Bool(), ), ) @@ -455,7 +455,7 @@ def test_binary_relation_explicit(self, function, opcode): opcode, expr.Value(12.0, types.Float()), expr.Value(12.0, types.Float()), - types.Bool(const=True), + types.Bool(), ), ) diff --git a/test/python/circuit/classical/test_types_ordering.py b/test/python/circuit/classical/test_types_ordering.py index f53f0e85bcc..67df997d253 100644 --- a/test/python/circuit/classical/test_types_ordering.py +++ b/test/python/circuit/classical/test_types_ordering.py @@ -76,7 +76,6 @@ def test_greater(self): self.assertEqual(types.greater(types.Uint(8), types.Uint(16)), types.Uint(16)) self.assertEqual(types.greater(types.Uint(8), types.Uint(8)), types.Uint(8)) self.assertEqual(types.greater(types.Bool(), types.Bool()), types.Bool()) - ) self.assertEqual(types.greater(types.Float(), types.Float()), types.Float()) with self.assertRaisesRegex(TypeError, "no ordering"): types.greater(types.Bool(), types.Uint(8)) @@ -98,4 +97,3 @@ def test_basic_examples(self): self.assertIs(types.cast_kind(types.Uint(16), types.Uint(8)), types.CastKind.DANGEROUS) self.assertIs(types.cast_kind(types.Uint(16), types.Float()), types.CastKind.DANGEROUS) self.assertIs(types.cast_kind(types.Float(), types.Uint(16)), types.CastKind.DANGEROUS) - diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 7b438ec6823..21af7c017d9 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -1767,6 +1767,7 @@ def test_var_use(self): qc.add_var("c", expr.bit_not(b)) # All inputs should come first, regardless of declaration order. qc.add_input("d", types.Bool()) + qc.add_var("e", expr.lift(7.5)) expected = """\ OPENQASM 3.0; @@ -1775,9 +1776,11 @@ def test_var_use(self): input uint[8] b; input bool d; uint[8] c; +float e; a = !a; b = b & 8; c = ~b; +e = 7.5; """ self.assertEqual(dumps(qc), expected) From 6bc728a02d82141036a87fe1986d812dbd912af0 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Thu, 27 Feb 2025 17:02:42 -0500 Subject: [PATCH 50/53] Fix comment and release note. --- qiskit/circuit/classical/expr/constructors.py | 6 +++--- releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index 65db45a843b..bcadff9598b 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -188,9 +188,9 @@ def logic_not(operand: typing.Any, /) -> Expr: >>> expr.logic_not(ClassicalRegister(3, "c")) Unary(\ Unary.Op.LOGIC_NOT, \ -Cast(Var(ClassicalRegister(3, 'c'), Uint(3, const=False)), \ -Bool(const=False), implicit=True), \ -Bool(const=False)) +Cast(Var(ClassicalRegister(3, 'c'), Uint(3)), \ +Bool(), implicit=True), \ +Bool()) """ operand = lift(operand) try: diff --git a/releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml b/releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml index 23c1117e07f..1792381b62a 100644 --- a/releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml +++ b/releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml @@ -10,9 +10,7 @@ features_circuits: from qiskit.circuit.classical import expr expr.lift(5.0) - # >>> Value(5.0, Float(const=False)) - expr.lift(5.0, try_const=True) - # >>> Value(5.0, Float(const=True)) + # >>> Value(5.0, Float()) This type is intended primarily for use in timing-related (duration and stretch) expressions. It is not compatible with bitwise or logical operations, though it From cbd55dbfe05aa0fd6e752cecc334c3b2a9f0c067 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Fri, 28 Feb 2025 15:44:07 -0500 Subject: [PATCH 51/53] Address review comments. --- qiskit/circuit/classical/expr/constructors.py | 28 ++++++++----------- qiskit/circuit/classical/types/__init__.py | 10 +++++-- qiskit/circuit/classical/types/types.py | 11 ++++++-- qiskit/qasm3/ast.py | 1 - qiskit/qasm3/exporter.py | 2 +- qiskit/qasm3/printer.py | 4 +-- qiskit/qpy/formats.py | 4 +-- .../classical/test_expr_constructors.py | 6 ++-- test/python/qasm3/test_export.py | 2 +- 9 files changed, 36 insertions(+), 32 deletions(-) diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index bcadff9598b..7341f254566 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -49,9 +49,9 @@ import qiskit -def _coerce_lossless(expr: Expr, type: types.Type) -> Expr: +def _coerce_lossless(expr: Expr, type: types.Type) -> Expr | None: """Coerce ``expr`` to ``type`` by inserting a suitable :class:`Cast` node, if the cast is - lossless. Otherwise, raise a ``TypeError``.""" + lossless. Otherwise, return ``None``.""" kind = cast_kind(expr.type, type) if kind is CastKind.EQUAL: return expr @@ -59,9 +59,7 @@ def _coerce_lossless(expr: Expr, type: types.Type) -> Expr: return Cast(expr, type, implicit=True) if kind is CastKind.LOSSLESS: return Cast(expr, type, implicit=False) - 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}'") + return None def lift_legacy_condition( @@ -193,11 +191,10 @@ def logic_not(operand: typing.Any, /) -> Expr: Bool()) """ operand = lift(operand) - try: - operand = _coerce_lossless(operand, types.Bool()) - return Unary(Unary.Op.LOGIC_NOT, operand, operand.type) - except TypeError as ex: - raise TypeError(f"cannot apply '{Unary.Op.BIT_NOT}' to type '{operand.type}'") from ex + coerced_operand = _coerce_lossless(operand, types.Bool()) + if coerced_operand is None: + raise TypeError(f"cannot apply '{Unary.Op.LOGIC_NOT}' to type '{operand.type}'") + return Unary(Unary.Op.LOGIC_NOT, coerced_operand, coerced_operand.type) def _lift_binary_operands(left: typing.Any, right: typing.Any) -> tuple[Expr, Expr]: @@ -314,12 +311,11 @@ def _binary_logical(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: bool_ = types.Bool() left = lift(left) right = lift(right) - try: - left = _coerce_lossless(left, bool_) - right = _coerce_lossless(right, bool_) - return Binary(op, left, right, bool_) - except TypeError as ex: - raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") from ex + coerced_left = _coerce_lossless(left, bool_) + coerced_right = _coerce_lossless(right, bool_) + if coerced_left is None or coerced_right is None: + raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") + return Binary(op, coerced_left, coerced_right, bool_) def logic_and(left: typing.Any, right: typing.Any, /) -> Expr: diff --git a/qiskit/circuit/classical/types/__init__.py b/qiskit/circuit/classical/types/__init__.py index cd97c5cdc90..2ec3630bae9 100644 --- a/qiskit/circuit/classical/types/__init__.py +++ b/qiskit/circuit/classical/types/__init__.py @@ -33,12 +33,13 @@ heap-allocating a new version of the same thing. Where possible, the class constructors will return singleton instances to facilitate this. -The two different types available are for Booleans (corresponding to :class:`.Clbit` and the -literals ``True`` and ``False``), and unsigned integers (corresponding to -:class:`.ClassicalRegister` and Python integers). +The :class:`Bool` type represents :class:`.Clbit` and the literals ``True`` and ``False``, the +:class:`Uint` type represents :class:`.ClassicalRegister` and Python integers, and the +:class:`Float` type represents Python floats. .. autoclass:: Bool .. autoclass:: Uint +.. autoclass:: Float Note that :class:`Uint` defines a family of types parametrized by their width; it is not one single type, which may be slightly different to the 'classical' programming languages you are used to. @@ -89,6 +90,9 @@ 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. +Note that casts between :class:`Float` and :class:`Uint` are considered dangerous in either +direction, and must be done explicitly. + .. autoclass:: CastKind """ diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py index 06328c280e2..3a36672cb33 100644 --- a/qiskit/circuit/classical/types/types.py +++ b/qiskit/circuit/classical/types/types.py @@ -19,7 +19,12 @@ from __future__ import annotations -__all__ = ["Type", "Bool", "Float", "Uint"] +__all__ = [ + "Type", + "Bool", + "Float", + "Uint", +] import typing @@ -115,8 +120,8 @@ def __eq__(self, other): @typing.final class Float(Type, metaclass=_Singleton): - """A floating point number of unspecified width. - In the future, this may also be used to represent a fixed-width float. + """An IEE-754 double-precision floating point number. + In the future, this may also be used to represent other fixed-width floats. """ __slots__ = () diff --git a/qiskit/qasm3/ast.py b/qiskit/qasm3/ast.py index 1d15c177d4f..492b57d0f33 100644 --- a/qiskit/qasm3/ast.py +++ b/qiskit/qasm3/ast.py @@ -136,7 +136,6 @@ class ClassicalType(ASTNode): class FloatType(ClassicalType, enum.Enum): """Allowed values for the width of floating-point types.""" - UNSPECIFIED = 0 HALF = 16 SINGLE = 32 DOUBLE = 64 diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index a2fad202838..0101e4501ce 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -1254,7 +1254,7 @@ def _build_ast_type(type_: types.Type) -> ast.ClassicalType: if type_.kind is types.Uint: return ast.UintType(type_.width) if type_.kind is types.Float: - return ast.FloatType.UNSPECIFIED + return ast.FloatType.DOUBLE raise RuntimeError(f"unhandled expr type '{type_}'") diff --git a/qiskit/qasm3/printer.py b/qiskit/qasm3/printer.py index 30c6ec20d1e..c8c25d60235 100644 --- a/qiskit/qasm3/printer.py +++ b/qiskit/qasm3/printer.py @@ -78,9 +78,7 @@ class BasicPrinter: ast.QuantumGateModifierName.POW: "pow", } - _FLOAT_TYPE_LOOKUP = {ast.FloatType.UNSPECIFIED: "float"} | { - type: f"float[{type.value}]" for type in ast.FloatType if type.value > 0 - } + _FLOAT_TYPE_LOOKUP = {type: f"float[{type.value}]" for type in ast.FloatType} # The visitor names include the class names, so they mix snake_case with PascalCase. # pylint: disable=invalid-name diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py index 1ff4b556c9c..45525a381eb 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -360,12 +360,12 @@ # EXPR_TYPE +EXPR_TYPE_DISCRIMINATOR_SIZE = 1 + EXPR_TYPE_FLOAT = namedtuple("EXPR_TYPE_FLOAT", []) EXPR_TYPE_FLOAT_PACK = "!" EXPR_TYPE_FLOAT_SIZE = struct.calcsize(EXPR_TYPE_FLOAT_PACK) -EXPR_TYPE_DISCRIMINATOR_SIZE = 1 - EXPR_TYPE_BOOL = namedtuple("EXPR_TYPE_BOOL", []) EXPR_TYPE_BOOL_PACK = "!" EXPR_TYPE_BOOL_SIZE = struct.calcsize(EXPR_TYPE_BOOL_PACK) diff --git a/test/python/circuit/classical/test_expr_constructors.py b/test/python/circuit/classical/test_expr_constructors.py index 531774a4602..9583ba8be4a 100644 --- a/test/python/circuit/classical/test_expr_constructors.py +++ b/test/python/circuit/classical/test_expr_constructors.py @@ -140,7 +140,7 @@ def test_bit_not_explicit(self): ) @ddt.data(expr.bit_not) - def test_urnary_bitwise_forbidden(self, function): + def test_unary_bitwise_forbidden(self, function): with self.assertRaisesRegex(TypeError, "cannot apply"): function(7.0) @@ -161,7 +161,7 @@ def test_logic_not_explicit(self): ) @ddt.data(expr.logic_not) - def test_urnary_logical_forbidden(self, function): + def test_unary_logical_forbidden(self, function): with self.assertRaisesRegex(TypeError, "cannot apply"): function(7.0) @@ -507,6 +507,8 @@ def test_index_forbidden(self): expr.index(ClassicalRegister(3, "a"), 1.0) with self.assertRaisesRegex(TypeError, "invalid types"): expr.index(0xFFFF, 1.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.index(ClassicalRegister(3, "a"), 1.0) @ddt.data( (expr.shift_left, expr.Binary.Op.SHIFT_LEFT), diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 21af7c017d9..c95cdee4b55 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -1776,7 +1776,7 @@ def test_var_use(self): input uint[8] b; input bool d; uint[8] c; -float e; +float[64] e; a = !a; b = b & 8; c = ~b; From f12afb4a2827fb8fc6d669d2d618db2bbdf4837a Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Fri, 28 Feb 2025 15:59:10 -0500 Subject: [PATCH 52/53] Update release note. --- releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml b/releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml index 1792381b62a..37f203211db 100644 --- a/releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml +++ b/releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml @@ -2,7 +2,7 @@ features_circuits: - | The classical realtime-expressions module :mod:`qiskit.circuit.classical` can now represent - floating point values of unspecified width, using the new type :class:`~.types.Float`. + IEE-754 double-precision floating point values using the new type :class:`~.types.Float`. The :func:`~.expr.lift` function can be used to create a value expression from a Python float:: From 0f14cca4e604c025fe1b1c09c6d85743c6b45014 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Fri, 28 Feb 2025 19:02:18 -0500 Subject: [PATCH 53/53] Address review comments. --- qiskit/circuit/classical/expr/constructors.py | 10 +++++++++- qiskit/circuit/classical/types/types.py | 2 +- releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml | 2 +- .../python/circuit/classical/test_expr_constructors.py | 2 ++ test/qpy_compat/test_qpy.py | 9 +++++++-- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index 7341f254566..851f2f9b83b 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -353,6 +353,8 @@ def _equal_like(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: if left.type.kind is not right.type.kind: raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") type = types.greater(left.type, right.type) + # Note that we don't check the return value of _coerce_lossless for these + # since 'left' and 'right' are guaranteed to be the same kind here. return Binary(op, _coerce_lossless(left, type), _coerce_lossless(right, type), types.Bool()) @@ -397,6 +399,8 @@ def _binary_relation(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr if left.type.kind is not right.type.kind or left.type.kind is types.Bool: raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") type = types.greater(left.type, right.type) + # Note that we don't check the return value of _coerce_lossless for these + # since 'left' and 'right' are guaranteed to be the same kind here. return Binary(op, _coerce_lossless(left, type), _coerce_lossless(right, type), types.Bool()) @@ -478,7 +482,11 @@ def _shift_like( if type is not None and type.kind is not types.Uint: raise TypeError(f"type '{type}' is not a valid bitshift operand type") if isinstance(left, Expr): - left = _coerce_lossless(left, type) if type is not None else left + if type is not None: + coerced_left = _coerce_lossless(left, type) + if coerced_left is None: + raise TypeError(f"type '{type}' cannot losslessly represent '{left.type}'") + left = coerced_left else: left = lift(left, type) right = lift(right) diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py index 3a36672cb33..31c2bdb44ba 100644 --- a/qiskit/circuit/classical/types/types.py +++ b/qiskit/circuit/classical/types/types.py @@ -120,7 +120,7 @@ def __eq__(self, other): @typing.final class Float(Type, metaclass=_Singleton): - """An IEE-754 double-precision floating point number. + """An IEEE-754 double-precision floating point number. In the future, this may also be used to represent other fixed-width floats. """ diff --git a/releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml b/releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml index 37f203211db..02a3d1dcb16 100644 --- a/releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml +++ b/releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml @@ -2,7 +2,7 @@ features_circuits: - | The classical realtime-expressions module :mod:`qiskit.circuit.classical` can now represent - IEE-754 double-precision floating point values using the new type :class:`~.types.Float`. + IEEE-754 double-precision floating point values using the new type :class:`~.types.Float`. The :func:`~.expr.lift` function can be used to create a value expression from a Python float:: diff --git a/test/python/circuit/classical/test_expr_constructors.py b/test/python/circuit/classical/test_expr_constructors.py index 9583ba8be4a..a3510a65722 100644 --- a/test/python/circuit/classical/test_expr_constructors.py +++ b/test/python/circuit/classical/test_expr_constructors.py @@ -553,3 +553,5 @@ def test_shift_forbidden(self, function): function(0xFFFF, 2.0) with self.assertRaisesRegex(TypeError, "invalid types"): function(255.0, 1) + with self.assertRaisesRegex(TypeError, "cannot losslessly represent"): + function(expr.lift(5.0), 3, types.Uint(8)) diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index cfaf31bc086..a87b0372429 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -844,8 +844,13 @@ def generate_v14_expr(): return [float_expr] -def generate_circuits(version_parts): - """Generate reference circuits.""" +def generate_circuits(version_parts, current_version, load_context=False): + """Generate reference circuits. + + If load_context is True, avoid generating Pulse-based reference + circuits. For those circuits, load_qpy only checks that the cached + circuits can be loaded without erroring.""" + output_circuits = { "full.qpy": [generate_full_circuit()], "unitary.qpy": [generate_unitary_gate_circuit()],