Skip to content

Commit ba032ee

Browse files
[python][test] Migrate Python tests + bridge widening for measure_handle single-API
Three concerns share this commit because they are exercised by the same test sweep: (1) Python MLIR FileCheck migrations (4 files): Mechanical type rename `!quake.measure` -> `!cc.measure_handle` in `bug_1777.py` and `call_qpu.py`; structural refresh in `bug_1775.py` and `bug_1875.py` for the new `cc.if`-carries-handle / short-circuit `else`-discriminate IR shape that the rewired bridge produces. (2) New `test_measure_handle.py` (25 tests, 23 active + 2 skipped on pre-existing bridge gaps `IfExp` / `AnnAssign`): host-scope rejection, scalar / vector mz emission shape, every bool-coercion site (`if`/`while`/`not`/`bool()`/`==`/`!=`/`return`), `and`/`or` short-circuit RHS-discriminate placement, `to_bools` vector lowering, `to_integer(to_bools(...))` composition, `to_integer(handles)` rejection, `discriminating an unbound measure_handle` for the default-constructed pattern, and the `measure_handle cannot cross the host-device boundary; entry-point kernels must discriminate first` diagnostic for direct + transitive handle positions. (3) Existing test migrations + a narrow bridge fix the sweep exposed: Full pytest sweep on `python/tests/kernel/` after the bridge rewire surfaced 11 failing tests that previously relied on `mz` returning bool. Migrations: * `test_to_integer.py`: compose `to_integer(to_bools(mz(q)))`. * `test_assignments.py::test_disallow_value_updates`: invert the now-supported reassign-handle-across-scopes case (the test's own TODO from before predicted exactly this). * `test_assignments.py::test_inner_functions`: explicit `bool(mz(q))` for the tuple-return aggregate-element case (FIXME inline; aggregate-element typing does not currently auto-discriminate). * `test_kernel_features.py::test_mid_circuit_measurements`: declare `result` slots as `bool` so the bridge's handle->i1/i8 path discriminates element stores. * `test_run_kernel.py` (3 tests): explicit `int(bool(mz(q)))` for handle-in-arithmetic; change `-> int` returning `mz(q)` to `-> bool`. * `test_kernel_shift_operators.py` (5 tests): bulk-discriminate with `cudaq.to_bools(mz(q))` so shift operands are integer-typed. Bridge fix in `changeOperandToType`: also auto-discriminate when the target type is `i8` (Python's storage representation for `bool`-element lists / dataclass slots), not just `i1`. After discriminate the recursive call promotes i1->i8 via the existing cast logic. Sanity gate: full `python/tests/kernel/` pytest sweep is 620 passed, 47 skipped, 1 xfailed (all pre-existing). Python MLIR lit suite runs 94/94 green. Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Pradnya Khalate <pkhalate@nvidia.com>
1 parent 88d8b4a commit ba032ee

11 files changed

Lines changed: 539 additions & 63 deletions

python/cudaq/kernel/ast_bridge.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -624,15 +624,17 @@ def changeOperandToType(self, ty, operand, allowDemotion=False):
624624
"""
625625
if ty == operand.type:
626626
return operand
627-
# `measure_handle -> bool` is the user-defined coercion sanctioned by
628-
# the spec; insert a `quake.discriminate` then continue with the
629-
# usual promotion/demotion rules. This catches `bool b = mz(q)`,
630-
# `return mz(q)` from a `-> bool` kernel, bool-typed function-call
631-
# arguments, and the recursive call from `__arithmetic_to_bool`.
632-
# Spec `measure_handle.bs` (Python API; IR Representation).
633-
if (IntegerType.isinstance(ty) and IntegerType(ty).width == 1 and
627+
# `measure_handle -> bool` is a user-defined coercion: insert a
628+
# `quake.discriminate` then continue with the usual promotion /
629+
# demotion rules. This catches `bool b = mz(q)`, `return mz(q)`
630+
# from a `-> bool` kernel, bool-typed function-call arguments,
631+
# the recursive call from `__arithmetic_to_bool`, and element-
632+
# stores into a `list[bool]` (which the bridge backs with `i8`
633+
# element storage but logically owns as bool).
634+
if (IntegerType.isinstance(ty) and IntegerType(ty).width in (1, 8) and
634635
cc.MeasureHandleType.isinstance(operand.type)):
635-
return self.__discriminateIfMeasureHandle(operand, self.currentNode)
636+
disc = self.__discriminateIfMeasureHandle(operand, self.currentNode)
637+
return self.changeOperandToType(ty, disc, allowDemotion)
636638
if (cc.StdvecType.isinstance(ty) and
637639
cc.StdvecType.getElementType(ty) == self.getIntegerType(1) and
638640
cc.StdvecType.isinstance(operand.type) and
@@ -847,10 +849,10 @@ def __isProvablyUnboundHandleSource(self, value):
847849
`VisitCastExpr::CK_UserDefinedConversion` (search for "Push a
848850
placeholder i1 so the enclosing statement's value stack stays
849851
balanced"). The C++ check examines the pattern
850-
`cc.load(cc.alloca)` and reports the diagnostic if the alloca has
852+
`cc.load(cc.alloca)` and reports the diagnostic if the `alloca` has
851853
no `cc.store` users; in Python the AST bridge stores the
852-
default-constructed `cc.UndefOp` value into the alloca (so the
853-
alloca always has at least one store), so we instead check that
854+
default-constructed `cc.UndefOp` value into the `alloca` (so the
855+
`alloca` always has at least one store), so we instead check that
854856
the only stored values come from `cc.UndefOp`. We also catch the
855857
direct `cc.UndefOp -> bool` flow (e.g. the spec example
856858
`bool(cudaq.measure_handle())`).
@@ -3653,7 +3655,7 @@ def check_vector_init():
36533655
# Spec §C++ API / Python API: `cudaq.to_bools` is
36543656
# the bulk counterpart to the per-element
36553657
# `measure_handle -> bool` coercion. Lower to a
3656-
# vectorized `quake.discriminate` on the handle
3658+
# vector form `quake.discriminate` on the handle
36573659
# vector. Mirrors `to_bools` in
36583660
# `lib/Frontend/nvqpp/ConvertExpr.cpp`.
36593661
if len(node.args) != 1 or node.keywords:

python/tests/kernel/test_assignments.py

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,25 +1150,23 @@ def test1() -> list[bool]:
11501150
assert 'variable defined in parent scope cannot be modified' in str(e.value)
11511151
assert '(offending source -> c = qs[1])' in str(e.value)
11521152

1153-
# TODO: The reason we cannot currently support this is
1154-
# because we store measurement results as values in the
1155-
# symbol table. This should be changed and supported when
1156-
# we do the change to properly distinguish measurement
1157-
# types from booleans.
1158-
with pytest.raises(RuntimeError) as e:
1159-
1160-
@cudaq.kernel
1161-
def test2() -> bool:
1162-
qs = cudaq.qvector(2)
1163-
res = mz(qs[0])
1164-
if True:
1165-
x(qs[1])
1166-
res = mz(qs[1])
1167-
return res
1153+
# Reassigning a `measure_handle`-typed variable across scopes is
1154+
# supported now that `mz` returns `cudaq.measure_handle` instead of
1155+
# `bool`: the symbol-table slot has handle type, the inner-scope
1156+
# store binds a fresh handle, and the bool-coercion at `return res`
1157+
# discriminates exactly once. Previously this case was disallowed
1158+
# because measurement results were stored as raw `i1` values in the
1159+
# symbol table.
1160+
@cudaq.kernel
1161+
def test2() -> bool:
1162+
qs = cudaq.qvector(2)
1163+
res = mz(qs[0])
1164+
if True:
1165+
x(qs[1])
1166+
res = mz(qs[1])
1167+
return res
11681168

1169-
test2()
1170-
assert 'variable defined in parent scope cannot be modified' in str(e.value)
1171-
assert '(offending source -> res = mz(qs[1]))' in str(e.value)
1169+
test2()
11721170

11731171

11741172
def test_var_scopes():
@@ -1391,7 +1389,8 @@ def fct():
13911389
x(q)
13921390

13931391
fct()
1394-
return i, mz(q)
1392+
# FIXME: aggregate-element typing does not currently auto-discriminate, so coerce explicitly.
1393+
return i, bool(mz(q))
13951394

13961395
out = cudaq.run(test1, True, False, shots_count=10)
13971396
assert all(res == (True, False) for res in out)

python/tests/kernel/test_kernel_features.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2720,7 +2720,7 @@ def test_mid_circuit_measurements():
27202720

27212721
@cudaq.kernel
27222722
def callee(register: cudaq.qview) -> list[bool]:
2723-
result = [0, 0, 0, 0, 0, 0, 0, 0]
2723+
result = [False, False, False, False, False, False, False, False]
27242724
for i in range(4):
27252725
j = i * 2
27262726
if i % 2 == 0:

python/tests/kernel/test_kernel_shift_operators.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def test_run_with_integer_left_shift_operator():
7676
@cudaq.kernel
7777
def kernel(n: int) -> int:
7878
q = cudaq.qvector(n)
79-
m = mz(q)
79+
m = cudaq.to_bools(mz(q))
8080
r = 0
8181
for i in range(n):
8282
r = r & (m[i] << i)
@@ -96,7 +96,7 @@ def test_run_with_non_integer_left_shift_operator():
9696
@cudaq.kernel
9797
def kernel(n: int) -> int:
9898
q = cudaq.qvector(n)
99-
m = mz(q)
99+
m = cudaq.to_bools(mz(q))
100100
r = 0
101101
for i in range(n):
102102
r = r & (m[i] << 1.0)
@@ -113,7 +113,7 @@ def test_run_with_integer_right_shift_operator():
113113
@cudaq.kernel
114114
def kernel(n: int) -> int:
115115
q = cudaq.qvector(n)
116-
m = mz(q)
116+
m = cudaq.to_bools(mz(q))
117117
r = 0
118118
for i in range(n):
119119
r = r & (m[i] >> i)
@@ -131,7 +131,7 @@ def test_run_with_integer_bitwise_or_operator():
131131
@cudaq.kernel
132132
def kernel(n: int) -> int:
133133
q = cudaq.qvector(n)
134-
m = mz(q)
134+
m = cudaq.to_bools(mz(q))
135135
r = 0
136136
for i in range(n):
137137
r = r | (m[i] >> i)
@@ -149,7 +149,7 @@ def test_run_with_integer_bitwise_xor_operator():
149149
@cudaq.kernel
150150
def kernel(n: int) -> int:
151151
q = cudaq.qvector(n)
152-
m = mz(q)
152+
m = cudaq.to_bools(mz(q))
153153
r = 0
154154
for i in range(n):
155155
r = r ^ (m[i] >> i)

0 commit comments

Comments
 (0)