Skip to content

Commit b9bdc5a

Browse files
authored
[Stretch] Support const classical expressions. (Qiskit#13811)
* WIP * Add try_const to lift. * Try multiple singletons, new one for const. * Revert "Try multiple singletons, new one for const." This reverts commit e2b3221. * Remove Bool singleton test. * Add const handling for stores, fix test bugs. * Fix formatting. * Remove Duration and Stretch for now. * Cleanup, fix const bug in index. * 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). * 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. * 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. * Revert to old test_expr_constructors.py. * Make binary_logical lift independent again. Since we're going for using a Cast node when const-ness differs, this will be fine. * Update tests, handle a few edge cases. * Fix docstring. * Remove now redundant arg from tests. * Add const testing for ordering. * Add const tests for shifts. * Add release note. * Add const store tests. * Address lint, minor cleanup. * 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. * Implement QPY support for const-typed expressions. * 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. * Update QPY version 14 desc. * Fix lint. * Add serialization testing. * Test pre-v14 QPY rejects const-typed exprs. * Revert visitors.py. * Address review comments. * Improve type docs. * 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. * Move const-ness from Type to Expr. * Revert QPY testing, no longer needed. * Add explicit validation of const expr. * Revert stuff I didn't need to touch. * Update release note. * A few finishing touches. * Address review comments.
1 parent a00279b commit b9bdc5a

File tree

4 files changed

+82
-4
lines changed

4 files changed

+82
-4
lines changed

qiskit/circuit/classical/expr/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,18 @@
3939
4040
These objects are mutable and should not be reused in a different location without a copy.
4141
42+
All :class`Expr` instances define a boolean :attr:`~Expr.const` attribute, which indicates
43+
whether the expression can be evaluated at compile time. Most expression classes infer this
44+
during construction based on the const-ness of their operands.
45+
4246
The base for dynamic variables is the :class:`Var`, which can be either an arbitrarily typed
4347
real-time variable, or a wrapper around a :class:`.Clbit` or :class:`.ClassicalRegister`.
4448
4549
.. autoclass:: Var
4650
:members: var, name, new
4751
4852
Similarly, literals used in expressions (such as integers) should be lifted to :class:`Value` nodes
49-
with associated types.
53+
with associated types. A :class:`Value` is always considered a constant expression.
5054
5155
.. autoclass:: Value
5256

qiskit/circuit/classical/expr/expr.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,10 @@ class Expr(abc.ABC):
5555
All subclasses are responsible for setting their ``type`` attribute in their ``__init__``, and
5656
should not call the parent initializer."""
5757

58-
__slots__ = ("type",)
58+
__slots__ = ("type", "const")
5959

6060
type: types.Type
61+
const: bool
6162

6263
# Sentinel to prevent instantiation of the base class.
6364
@abc.abstractmethod
@@ -89,6 +90,7 @@ class Cast(Expr):
8990

9091
def __init__(self, operand: Expr, type: types.Type, implicit: bool = False):
9192
self.type = type
93+
self.const = operand.const
9294
self.operand = operand
9395
self.implicit = implicit
9496

@@ -99,6 +101,7 @@ def __eq__(self, other):
99101
return (
100102
isinstance(other, Cast)
101103
and self.type == other.type
104+
and self.const == other.const
102105
and self.operand == other.operand
103106
and self.implicit == other.implicit
104107
)
@@ -141,6 +144,7 @@ def __init__(
141144
name: str | None = None,
142145
):
143146
super().__setattr__("type", type)
147+
super().__setattr__("const", False)
144148
super().__setattr__("var", var)
145149
super().__setattr__("name", name)
146150

@@ -151,8 +155,9 @@ def new(cls, name: str, type: types.Type) -> typing.Self:
151155

152156
@property
153157
def standalone(self) -> bool:
154-
"""Whether this :class:`Var` is a standalone variable that owns its storage location. If
155-
false, this is a wrapper :class:`Var` around a pre-existing circuit object."""
158+
"""Whether this :class:`Var` is a standalone variable that owns its storage
159+
location, if applicable. If false, this is a wrapper :class:`Var` around a
160+
pre-existing circuit object."""
156161
return isinstance(self.var, uuid.UUID)
157162

158163
def accept(self, visitor, /):
@@ -185,6 +190,7 @@ def __getstate__(self):
185190
def __setstate__(self, state):
186191
var, type, name = state
187192
super().__setattr__("type", type)
193+
super().__setattr__("const", False)
188194
super().__setattr__("var", var)
189195
super().__setattr__("name", name)
190196

@@ -206,6 +212,7 @@ class Value(Expr):
206212
def __init__(self, value: typing.Any, type: types.Type):
207213
self.type = type
208214
self.value = value
215+
self.const = True
209216

210217
def accept(self, visitor, /):
211218
return visitor.visit_value(self)
@@ -257,6 +264,7 @@ def __init__(self, op: Unary.Op, operand: Expr, type: types.Type):
257264
self.op = op
258265
self.operand = operand
259266
self.type = type
267+
self.const = operand.const
260268

261269
def accept(self, visitor, /):
262270
return visitor.visit_unary(self)
@@ -265,6 +273,7 @@ def __eq__(self, other):
265273
return (
266274
isinstance(other, Unary)
267275
and self.type == other.type
276+
and self.const == other.const
268277
and self.op is other.op
269278
and self.operand == other.operand
270279
)
@@ -348,6 +357,7 @@ def __init__(self, op: Binary.Op, left: Expr, right: Expr, type: types.Type):
348357
self.left = left
349358
self.right = right
350359
self.type = type
360+
self.const = left.const and right.const
351361

352362
def accept(self, visitor, /):
353363
return visitor.visit_binary(self)
@@ -356,6 +366,7 @@ def __eq__(self, other):
356366
return (
357367
isinstance(other, Binary)
358368
and self.type == other.type
369+
and self.const == other.const
359370
and self.op is other.op
360371
and self.left == other.left
361372
and self.right == other.right
@@ -381,6 +392,7 @@ def __init__(self, target: Expr, index: Expr, type: types.Type):
381392
self.target = target
382393
self.index = index
383394
self.type = type
395+
self.const = target.const and index.const
384396

385397
def accept(self, visitor, /):
386398
return visitor.visit_index(self)
@@ -389,6 +401,7 @@ def __eq__(self, other):
389401
return (
390402
isinstance(other, Index)
391403
and self.type == other.type
404+
and self.const == other.const
392405
and self.target == other.target
393406
and self.index == other.index
394407
)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
features_circuits:
3+
- |
4+
The classical realtime-expressions module :mod:`qiskit.circuit.classical` can now represent
5+
constant expressions. The :class:`~.expr.Expr` class now has a bool
6+
:attr:`~.expr.Expr.const` attribute which indicates the expression's const-ness. This allows
7+
us to enforce that expressions in certain contexts must be possible to evaluate at compile time.
8+
9+
All :class:`~.expr.Var` expressions are considered to be non-const, while all :class:`~.expr.Value`
10+
expressions are const.
11+
12+
An expression comprised only of other const expressions is also const::
13+
14+
from qiskit.circuit.classical import expr
15+
16+
assert expr.bit_and(5, 6).const
17+
18+
An expression that contains any non-const expression is non-const::
19+
20+
from qiskit.circuit.classical import expr, types
21+
22+
assert not expr.bit_and(5, expr.Var.new("a", types.Uint(5)).const

test/python/circuit/classical/test_expr_constructors.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,15 @@ def test_binary_bitwise_explicit(self, function, opcode):
182182
opcode, expr.Var(cr, types.Uint(8)), expr.Value(255, types.Uint(8)), types.Uint(8)
183183
),
184184
)
185+
self.assertFalse(function(cr, 255).const)
186+
185187
self.assertEqual(
186188
function(255, cr),
187189
expr.Binary(
188190
opcode, expr.Value(255, types.Uint(8)), expr.Var(cr, types.Uint(8)), types.Uint(8)
189191
),
190192
)
193+
self.assertFalse(function(255, cr).const)
191194

192195
clbit = Clbit()
193196
self.assertEqual(
@@ -199,6 +202,8 @@ def test_binary_bitwise_explicit(self, function, opcode):
199202
types.Bool(),
200203
),
201204
)
205+
self.assertFalse(function(True, clbit).const)
206+
202207
self.assertEqual(
203208
function(clbit, False),
204209
expr.Binary(
@@ -208,6 +213,18 @@ def test_binary_bitwise_explicit(self, function, opcode):
208213
types.Bool(),
209214
),
210215
)
216+
self.assertFalse(function(clbit, True).const)
217+
218+
self.assertEqual(
219+
function(255, 255),
220+
expr.Binary(
221+
opcode,
222+
expr.Value(255, types.Uint(8)),
223+
expr.Value(255, types.Uint(8)),
224+
types.Uint(8),
225+
),
226+
)
227+
self.assertTrue(function(255, 255).const)
211228

212229
@ddt.data(
213230
(expr.bit_and, expr.Binary.Op.BIT_AND),
@@ -278,6 +295,7 @@ def test_binary_logical_explicit(self, function, opcode):
278295
types.Bool(),
279296
),
280297
)
298+
self.assertFalse(function(cr, clbit).const)
281299

282300
self.assertEqual(
283301
function(cr, 3),
@@ -288,6 +306,7 @@ def test_binary_logical_explicit(self, function, opcode):
288306
types.Bool(),
289307
),
290308
)
309+
self.assertFalse(function(cr, 3).const)
291310

292311
self.assertEqual(
293312
function(False, clbit),
@@ -298,6 +317,7 @@ def test_binary_logical_explicit(self, function, opcode):
298317
types.Bool(),
299318
),
300319
)
320+
self.assertFalse(function(False, clbit).const)
301321

302322
@ddt.data(
303323
(expr.equal, expr.Binary.Op.EQUAL),
@@ -314,6 +334,7 @@ def test_binary_equal_explicit(self, function, opcode):
314334
opcode, expr.Var(cr, types.Uint(8)), expr.Value(255, types.Uint(8)), types.Bool()
315335
),
316336
)
337+
self.assertFalse(function(cr, 255).const)
317338

318339
self.assertEqual(
319340
function(7, cr),
@@ -324,6 +345,7 @@ def test_binary_equal_explicit(self, function, opcode):
324345
types.Bool(),
325346
),
326347
)
348+
self.assertFalse(function(7, cr).const)
327349

328350
self.assertEqual(
329351
function(clbit, True),
@@ -334,6 +356,7 @@ def test_binary_equal_explicit(self, function, opcode):
334356
types.Bool(),
335357
),
336358
)
359+
self.assertFalse(function(clbit, True).const)
337360

338361
@ddt.data(expr.equal, expr.not_equal)
339362
def test_binary_equal_forbidden(self, function):
@@ -360,6 +383,7 @@ def test_binary_relation_explicit(self, function, opcode):
360383
opcode, expr.Var(cr, types.Uint(8)), expr.Value(200, types.Uint(8)), types.Bool()
361384
),
362385
)
386+
self.assertFalse(function(cr, 200).const)
363387

364388
self.assertEqual(
365389
function(12, cr),
@@ -370,6 +394,7 @@ def test_binary_relation_explicit(self, function, opcode):
370394
types.Bool(),
371395
),
372396
)
397+
self.assertFalse(function(12, cr).const)
373398

374399
@ddt.data(expr.less, expr.less_equal, expr.greater, expr.greater_equal)
375400
def test_binary_relation_forbidden(self, function):
@@ -388,10 +413,19 @@ def test_index_explicit(self):
388413
expr.index(cr, 3),
389414
expr.Index(expr.Var(cr, types.Uint(4)), expr.Value(3, types.Uint(2)), types.Bool()),
390415
)
416+
self.assertFalse(expr.index(cr, 3).const)
417+
391418
self.assertEqual(
392419
expr.index(a, cr),
393420
expr.Index(a, expr.Var(cr, types.Uint(4)), types.Bool()),
394421
)
422+
self.assertFalse(expr.index(a, cr).const)
423+
424+
self.assertEqual(
425+
expr.index(255, 1),
426+
expr.Index(expr.Value(255, types.Uint(8)), expr.Value(1, types.Uint(1)), types.Bool()),
427+
)
428+
self.assertTrue(expr.index(255, 1).const)
395429

396430
def test_index_forbidden(self):
397431
with self.assertRaisesRegex(TypeError, "invalid types"):
@@ -414,16 +448,21 @@ def test_shift_explicit(self, function, opcode):
414448
opcode, expr.Var(cr, types.Uint(8)), expr.Value(5, types.Uint(3)), types.Uint(8)
415449
),
416450
)
451+
self.assertFalse(function(cr, 5).const)
452+
417453
self.assertEqual(
418454
function(a, cr),
419455
expr.Binary(opcode, a, expr.Var(cr, types.Uint(8)), types.Uint(4)),
420456
)
457+
self.assertFalse(function(a, cr).const)
458+
421459
self.assertEqual(
422460
function(3, 5, types.Uint(8)),
423461
expr.Binary(
424462
opcode, expr.Value(3, types.Uint(8)), expr.Value(5, types.Uint(3)), types.Uint(8)
425463
),
426464
)
465+
self.assertTrue(function(3, 5, types.Uint(8)).const)
427466

428467
@ddt.data(expr.shift_left, expr.shift_right)
429468
def test_shift_forbidden(self, function):

0 commit comments

Comments
 (0)