Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Stretch] Support const classical expressions. #13811

Merged
merged 47 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
af4ba52
WIP
kevinhartman Feb 7, 2025
e423bf9
Add try_const to lift.
kevinhartman Feb 7, 2025
0a5917b
Try multiple singletons, new one for const.
kevinhartman Feb 7, 2025
a97434d
Revert "Try multiple singletons, new one for const."
kevinhartman Feb 7, 2025
1afc965
Remove Bool singleton test.
kevinhartman Feb 7, 2025
86655f1
Add const handling for stores, fix test bugs.
kevinhartman Feb 7, 2025
aaeae9b
Fix formatting.
kevinhartman Feb 7, 2025
a2a444b
Remove Duration and Stretch for now.
kevinhartman Feb 7, 2025
8ac2dc3
Cleanup, fix const bug in index.
kevinhartman Feb 7, 2025
9f8313c
Fix ordering issue for types with differing const-ness.
kevinhartman Feb 9, 2025
db9d9cb
Fix QPY serialization.
kevinhartman Feb 10, 2025
71b7e7a
Make expr.Lift default to non-const.
kevinhartman Feb 11, 2025
2091557
Revert to old test_expr_constructors.py.
kevinhartman Feb 11, 2025
7307be9
Make binary_logical lift independent again.
kevinhartman Feb 11, 2025
9b30284
Update tests, handle a few edge cases.
kevinhartman Feb 12, 2025
ce1faf1
Fix docstring.
kevinhartman Feb 12, 2025
4fee48f
Remove now redundant arg from tests.
kevinhartman Feb 12, 2025
88ab046
Add const testing for ordering.
kevinhartman Feb 12, 2025
c5a230f
Add const tests for shifts.
kevinhartman Feb 12, 2025
7c88d88
Add release note.
kevinhartman Feb 12, 2025
c58a7b8
Add const store tests.
kevinhartman Feb 12, 2025
d9e9a8c
Address lint, minor cleanup.
kevinhartman Feb 13, 2025
25508bf
Reject const vars in add_var and add_input.
kevinhartman Feb 17, 2025
2c8ce43
Merge branch 'main' of github.com:Qiskit/qiskit into const-expr
kevinhartman Feb 17, 2025
ccf9441
Implement QPY support for const-typed expressions.
kevinhartman Feb 18, 2025
c6eab02
Remove invalid test.
kevinhartman Feb 18, 2025
edd7806
Update QPY version 14 desc.
kevinhartman Feb 18, 2025
8afa92e
Fix lint.
kevinhartman Feb 18, 2025
4e0f2df
Add serialization testing.
kevinhartman Feb 18, 2025
15ba943
Merge branch 'main' of github.com:Qiskit/qiskit into const-expr
kevinhartman Feb 18, 2025
50c31d3
Test pre-v14 QPY rejects const-typed exprs.
kevinhartman Feb 18, 2025
7e43322
Merge branch 'main' of github.com:Qiskit/qiskit into const-expr
kevinhartman Feb 18, 2025
07771f1
Merge branch 'main' of github.com:Qiskit/qiskit into const-expr
kevinhartman Feb 20, 2025
d3bca5f
Merge branch 'main' of github.com:Qiskit/qiskit into const-expr
kevinhartman Feb 24, 2025
50deb93
Revert visitors.py.
kevinhartman Feb 24, 2025
f1dc1a1
Address review comments.
kevinhartman Feb 24, 2025
1f439a0
Merge branch 'main' of github.com:Qiskit/qiskit into const-expr
kevinhartman Feb 25, 2025
166d7b2
Improve type docs.
kevinhartman Feb 26, 2025
10b2c8d
Merge branch 'main' of github.com:Qiskit/qiskit into const-expr
kevinhartman Feb 26, 2025
9d6cf39
Revert QPY, since the old format can support constexprs.
kevinhartman Feb 26, 2025
8021e00
Move const-ness from Type to Expr.
kevinhartman Feb 26, 2025
a15141b
Revert QPY testing, no longer needed.
kevinhartman Feb 26, 2025
ca2785d
Add explicit validation of const expr.
kevinhartman Feb 26, 2025
e902009
Revert stuff I didn't need to touch.
kevinhartman Feb 26, 2025
16475c3
Update release note.
kevinhartman Feb 26, 2025
6b5930d
A few finishing touches.
kevinhartman Feb 27, 2025
8e466a6
Address review comments.
kevinhartman Feb 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 157 additions & 76 deletions qiskit/circuit/classical/expr/constructors.py

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions qiskit/circuit/classical/expr/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, /):
Expand Down
10 changes: 8 additions & 2 deletions qiskit/circuit/classical/expr/visitors.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,13 +276,15 @@ 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::
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly this is clearer as

non-const :class:`~.expr.Var` nodes are l-values ...

since the const bit is pretty fundamental here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this comment is actually no longer valid, since I decided not to "decide" const-typed expressions can never be lvalues. Originally, I'd changed the implementation of is_lvalue to return False for any const-typed expression.

The reason for my deferral was that it seemed reasonable to me to use a store instruction to initialize a const variable, even though we block it at the moment. I'm not sure if it's valid in QASM to initialize a constant without an initializer, e.g.:

duration d;
d = 1200dt;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I don't know if that's valid or not. But tbh, even if it is valid, I'd just rather not support it in Qiskit - a few languages let you forward-declare storage, and initialise it in some scope, as long as the set of initialising statements dominates the set of statements using the variable. Rust is one of those - it's valid to have let x; and then assign to x in some scope, provided the structure of the program guarantees that x is assigned if it's used.

I'd rather just not support that in Qiskit, just to reduce complexity. If we gain more complex handling for classical control flow analysis later, we can always revisit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me. I thought of the let x; pattern in Rust too, but then I realized that Rust doesn't allow const x;, which is closer to the semantics I'm alluding to here—it requires an inline initializer.


>>> 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=False)))
False
>>> expr.is_lvalue(expr.lift(Clbit()))
True

Expand All @@ -297,4 +299,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)
42 changes: 40 additions & 2 deletions qiskit/circuit/classical/types/ordering.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,39 @@ 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
return orderer(left, right)
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 is True and right.const is False:
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 order_ is Ordering.EQUAL:
return Ordering.GREATER
if order_ is Ordering.LESS:
return Ordering.NONE
return order_


def is_subtype(left: Type, right: Type, /, strict: bool = False) -> bool:
Expand All @@ -111,6 +136,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)
Expand All @@ -134,6 +161,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)
Expand Down Expand Up @@ -215,11 +244,20 @@ def cast_kind(from_: Type, to_: Type, /) -> CastKind:
<CastKind.EQUAL: 1>
>>> types.cast_kind(types.Uint(8), types.Bool())
<CastKind.IMPLICIT: 2>
>>> types.cast_kind(types.Uint(8, const=True), types.Uint(8))
<CastKind.IMPLICIT: 2>
>>> types.cast_kind(types.Bool(), types.Uint(8))
<CastKind.LOSSLESS: 3>
>>> types.cast_kind(types.Uint(16), types.Uint(8))
<CastKind.DANGEROUS: 4>
"""
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_
56 changes: 23 additions & 33 deletions qiskit/circuit/classical/types/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,33 +19,11 @@

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.
Expand All @@ -62,6 +40,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):
Expand All @@ -81,37 +64,44 @@ 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.width))
return hash((self.__class__, self.const, self.width))

def __eq__(self, other):
return isinstance(other, Uint) and self.width == other.width
return isinstance(other, Uint) and self.const == other.const and self.width == other.width
5 changes: 3 additions & 2 deletions qiskit/qpy/binary_io/value.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
21 changes: 21 additions & 0 deletions releasenotes/notes/const-expr-397ff09042942b81.yaml
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading