Skip to content

[Python] Measurement handle type#4439

Draft
khalatepradnya wants to merge 10 commits intoNVIDIA:mainfrom
khalatepradnya:pkhalate/measure-handle-pr3c-python-frontend
Draft

[Python] Measurement handle type#4439
khalatepradnya wants to merge 10 commits intoNVIDIA:mainfrom
khalatepradnya:pkhalate/measure-handle-pr3c-python-frontend

Conversation

@khalatepradnya
Copy link
Copy Markdown
Collaborator

Summary

  • Python-side counterpart to PR [C++] Implement cudaq::measure_handle #4409.
  • Binds cudaq.measure_handle, rewires the Python AST bridge so mz/mx/my emit handles directly, lowers every Python bool-coercion site to quake.discriminate, surfaces cudaq.to_bools for bulk discrimination, and rejects measure_handle in entry-point kernel signatures with the spec-canonical diagnostic.

What Changed

  • cudaq.measure_handle is a kernel-only stub that raises on host instantiation; !cc.measure_handle registered as a Python-visible MLIR type.
  • mz/mx/my produce !cc.measure_handle (scalar) or !cc.stdvec<!cc.measure_handle> (vector); __discriminateIfMeasureHandle is inserted at every bool-coercion site (if/while/not/bool()/==/!=/return, and/or short-circuit RHS, list-of-bool element stores via i8 storage); cudaq.to_bools lowers to vectorized quake.discriminate; cudaq.to_integer(list[measure_handle]) is rejected and requires explicit to_integer(to_bools(...)) composition.
  • entry-point Python kernels with measure_handle (direct or transitive) in the signature get the spec-canonical 'measure_handle cannot cross the host-device boundary; entry-point kernels must discriminate first' diagnostic, mirroring the C++ wording for cross-frontend consistency.

Breaking Changes

  • Python kernels that relied on mz returning bool directly (e.g., if mz(q): used to type-check as bool; now type-checks as measure_handle and is implicitly discriminated by the new bridge path). User-visible behavior in the common case is unchanged, but deeper integrations that introspect the return type will see the new class.
  • Aggregate-element auto-discrimination (e.g., (mz(q), 1) returning a tuple) is a known gap; documented as FIXME inline in the affected test and tracked in followups.md. Users hit a clear bridge diagnostic, not a silent miscompile.

Dependency

@khalatepradnya khalatepradnya added the breaking change Change breaks backwards compatibility label May 2, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 2, 2026

CI Summary — ❌ failed

Run #25239067615 · trigger push · ✅ 5 · ⏩ 7 · ❌ 1 · ⛔ 0

❌ Failed or cancelled
Job Result Link
build_and_test ❌ failure view
Top-level jobs (13)
Job Result
binaries ⏩ skipped
build_and_test ❌ failure
config_devdeps ✅ success
config_source_build ⏩ skipped
config_wheeldeps ✅ success
devdeps ✅ success
docker_image ⏩ skipped
gen_code_coverage ⏩ skipped
metadata ✅ success
python_metapackages ⏩ skipped
python_wheels ⏩ skipped
source_build ⏩ skipped
wheeldeps ✅ success
⏩ Skipped jobs (7) — intentionally skipped on PR builds; run on merge_group / workflow_dispatch
Job
binaries
config_source_build
docker_image
gen_code_coverage
python_metapackages
python_wheels
source_build
All sub-jobs (50) — every matrix leg, with links
Job Status Link
Build and test (amd64, clang16, openmpi) / Dev environment (Debug) ❌ failure view
Build and test (amd64, clang16, openmpi) / Dev environment (Python) ❌ failure view
Build and test (amd64, gcc11, openmpi) / Dev environment (Debug) ❌ failure view
Build and test (amd64, gcc11, openmpi) / Dev environment (Python) ❌ failure view
Build and test (amd64, gcc12, openmpi) / Dev environment (Debug) ❌ failure view
Build and test (amd64, gcc12, openmpi) / Dev environment (Python) ❌ failure view
Build and test (arm64, clang16, openmpi) / Dev environment (Debug) ❌ failure view
Build and test (arm64, clang16, openmpi) / Dev environment (Python) ❌ failure view
CI Summary ❔ in_progress view
Configure build (devdeps) ✅ success view
Configure build (source_build) ⏩ skipped view
Configure build (wheeldeps) ✅ success view
Create CUDA Quantum installer ⏩ skipped view
Create Docker images ⏩ skipped view
Create Python metapackages ⏩ skipped view
Create Python wheels ⏩ skipped view
Gen code coverage ⏩ skipped view
Load dependencies (amd64, clang16) / Caching ✅ success view
Load dependencies (amd64, clang16) / Finalize ✅ success view
Load dependencies (amd64, clang16) / Metadata ✅ success view
Load dependencies (amd64, gcc11) / Caching ✅ success view
Load dependencies (amd64, gcc11) / Finalize ✅ success view
Load dependencies (amd64, gcc11) / Metadata ✅ success view
Load dependencies (amd64, gcc12) / Caching ✅ success view
Load dependencies (amd64, gcc12) / Finalize ✅ success view
Load dependencies (amd64, gcc12) / Metadata ✅ success view
Load dependencies (arm64, clang16) / Caching ✅ success view
Load dependencies (arm64, clang16) / Finalize ✅ success view
Load dependencies (arm64, clang16) / Metadata ✅ success view
Load dependencies (arm64, gcc11) / Caching ✅ success view
Load dependencies (arm64, gcc11) / Finalize ✅ success view
Load dependencies (arm64, gcc11) / Metadata ✅ success view
Load dependencies (arm64, gcc12) / Caching ✅ success view
Load dependencies (arm64, gcc12) / Finalize ✅ success view
Load dependencies (arm64, gcc12) / Metadata ✅ success view
Load source build cache ⏩ skipped view
Load wheel dependencies (amd64, 12.6) / Caching ✅ success view
Load wheel dependencies (amd64, 12.6) / Finalize ✅ success view
Load wheel dependencies (amd64, 12.6) / Metadata ✅ success view
Load wheel dependencies (amd64, 13.0) / Caching ✅ success view
Load wheel dependencies (amd64, 13.0) / Finalize ✅ success view
Load wheel dependencies (amd64, 13.0) / Metadata ✅ success view
Load wheel dependencies (arm64, 12.6) / Caching ✅ success view
Load wheel dependencies (arm64, 12.6) / Finalize ✅ success view
Load wheel dependencies (arm64, 12.6) / Metadata ✅ success view
Load wheel dependencies (arm64, 13.0) / Caching ✅ success view
Load wheel dependencies (arm64, 13.0) / Finalize ✅ success view
Load wheel dependencies (arm64, 13.0) / Metadata ✅ success view
Prepare cache clean-up ✅ success view
Retrieve PR info ✅ success view
⚠️ Required checks (0/8) — 8 missing — declared in .github/required-checks.yml for push
Required check Status Link
Build and test (amd64, clang16, openmpi) / Dev environment (Debug) ❌ failure view
Build and test (amd64, clang16, openmpi) / Dev environment (Python) ❌ failure view
Build and test (amd64, gcc11, openmpi) / Dev environment (Debug) ❌ failure view
Build and test (amd64, gcc11, openmpi) / Dev environment (Python) ❌ failure view
Build and test (amd64, gcc12, openmpi) / Dev environment (Debug) ❌ failure view
Build and test (amd64, gcc12, openmpi) / Dev environment (Python) ❌ failure view
Build and test (arm64, clang16, openmpi) / Dev environment (Debug) ❌ failure view
Build and test (arm64, clang16, openmpi) / Dev environment (Python) ❌ failure view

khalatepradnya and others added 4 commits May 4, 2026 17:12
Widen the `expand-measurements` pass to lower vector-form
`quake.mz`/`mx`/`my` whose result is `!cc.stdvec<!cc.measure_handle>`,
mirroring the legacy `!cc.stdvec<!quake.measure>` path. Adds a
secondary `ExpandStdvecHandleDiscriminate` pattern for the
post-SSA-boundary case where `quake.discriminate` consumes a handle
vector that the bridge has stored to / loaded from memory.

Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Pradnya Khalate <pkhalate@nvidia.com>
Implements the C++ runtime half of the measure_handle spec
(cudaq-spec/proposals/measure_handle.bs). measure_handle is the new
return type of mz / mx / my; the legacy `measure_result` spelling is
preserved via Option C — under MLIR mode `measure_result` is a `using`
alias for `measure_handle`, so existing callers compile unchanged but
gain the new handle semantics. Library mode keeps the legacy
`class measure_result` block untouched, including the
`__nvqpp__MeasureResultBoolConversion` adaptive-feedback hook.

Spec invariant: mz / mx / my are __qpu__-only entry points (Kernel
Signature Rule). MLIR-mode inline bodies trap with `std::abort()` so
host-scope misuse fails loudly instead of computing on a meaningless
value. measure_handle::operator bool() also aborts: the bridge
intercepts every legitimate bool coercion at AST time and emits
quake.discriminate, so reaching the host body means a bridge
interception path was missed and the program would otherwise compute
on a meaningless `bool`.

Files
- runtime/cudaq/qis/measure_handle.h (new): class declaration gated by
  `#ifndef CUDAQ_LIBRARY_MODE`. Tag-dispatched
  `measure_handle(handle_index, idx)` constructor reserved for runtime
  use; default constructor produces an unbound handle whose `index`
  carries a `numeric_limits<int64_t>::max()` sentinel.
  `static_assert`s pin the i64 payload width / trivially copyable /
  standard layout invariants the bridge marshalling relies on.
- runtime/cudaq/qis/execution_manager.h: under
  `#ifdef CUDAQ_LIBRARY_MODE` the existing `class measure_result` is
  unchanged; under `#else` the previous `using measure_result = bool`
  becomes `using measure_result = measure_handle`.
- runtime/cudaq/qis/qubit_qis.h: MLIR-mode bodies of `measureZ` /
  `measureX` / `measureY` and the bulk `to_bools` overload trap with
  `std::abort()`. Library-mode behavior is preserved verbatim.

Bridge wiring, byte-size machinery, QIR conversion gap fix, boundary
verifier, test migration, and docs updates follow as separate commits
in this PR.

Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Pradnya Khalate <pkhalate@nvidia.com>
…nery, and QIR conversion

Consolidates the bridge-side, type-system, and QIR-conversion work for
the measure_handle PR stack. The runtime API arrived in the previous
commit; this commit makes the AST bridge produce !cc.measure_handle
SSA values, teaches the verifier to reject handles at the host-device
boundary, fills the byte-size and marshaling gaps, and patches the QIR
conversion so handle pointer/stdvec ops survive --convert-to-qir-api.

Type-system support
- include/cudaq/Optimizer/Dialect/CC/CCTypes.h, CCTypes.cpp:
  containsMeasureHandle (value-shape check) and
  containsMeasureHandleAtBoundary (recursive into callable signatures
  and bare function types). The boundary variant is required so
  `std::function<void(measure_handle)>` and `cudaq::qkernel<...>`
  parameters are caught at entry-point classification.
- lib/Optimizer/Dialect/CC/CCOps.cpp:
  MeasureHandleType case in getByteSizeOfType returning a constant
  8 bytes, the IR-mode width of a class with a single std::int64_t
  field. Without this, a pure-device kernel returning
  std::vector<measure_handle> aborts in ConvertStmt.cpp with
  "unhandled vector element type" because __nvqpp_vectorCopyCtor
  cannot get a constant element size for the heap-copy prologue.

AST bridge
- lib/Frontend/nvqpp/ConvertType.cpp, ConvertDecl.cpp:
  cudaq::measure_handle maps to !cc.measure_handle;
  std::vector<measure_handle> is recognised in measurement
  register-name handling.
- lib/Frontend/nvqpp/ConvertExpr.cpp: the central rewire.
  * mz / mx / my emit !cc.measure_handle (scalar) or
    !cc.stdvec<!cc.measure_handle> (range/variadic) directly.
  * CK_UserDefinedConversion at measure_handle::operator bool inserts
    quake.discriminate at every spec-mandated bool-coercion site.
  * The discriminate-insertion path runs an isBoundHandle check that
    walks through cc.compute_ptr / cc.cast to the base alloca and,
    on the scalar-handle alloca shape, requires that a binding store
    dominate the load (mlir::DominanceInfo, computed lazily once per
    coercion site). Conditional-store shapes that previously emitted
    a discriminate over an uninitialized i64 payload now diagnose.
  * cudaq::to_bools is intercepted by name and lowered to a vectorized
    quake.discriminate on the entire handle stdvec; it is the bulk
    counterpart to operator bool.
  * cudaq::to_integer rejects vector<measure_handle> with a
    spec-named diagnostic (per measure_handle.bs §C++ API): the
    silent auto-insert that hid the bulk-discrimination API is gone.
  * measure_handle copy/move construction and operator= are
    intercepted as value-typed aliasing of the sub-i64 stack value;
    chained `h3 = h2 = h;` works because the dispatch drops the
    callee value the visitor pushed.
  * default-construct produces only the storage slot (cc.alloca);
    VisitVarDecl binds it directly so any read at a discriminate
    site is statically diagnosed by the unbound-handle path.
- lib/Frontend/nvqpp/ASTBridge.cpp: __qpu__ entry-point classification
  rejects functor operator() shapes whose signature transitively
  mentions measure_handle, the only disambiguable spec violation at
  AST time.

Marshaling and QIR conversion
- lib/Optimizer/Builder/Marshal.cpp:
  hasLegalType extends the entry-point predicate to reject
  measure_handle alongside qubit-typed parameters/results.
  lookupHostEntryPointFunc early-returns for device-only kernels
  whose signature cannot cross the host boundary, so the host-side
  rewriter skips them entirely.
- lib/Optimizer/CodeGen/ConvertToQIRAPI.cpp:
  The TypeConverter rewrites !cc.measure_handle to i64, but
  cc.compute_ptr / cc.stdvec_data / cc.stdvec_init / cc.stdvec_size
  carrying handle pointer or stdvec types had no patterns and no
  dynamic-legality predicates, so the framework left them
  legal-by-default and inserted unrealized_conversion_casts that
  applyPartialConversion could not resolve. Add OpInterfacePattern
  instantiations and extend the legality predicate so all four ops
  participate in the same operand/result-type rewrite the existing
  CC pointer ops already use.

LLVM 22 idiom
- All 15 op-creation sites added by this commit in ConvertExpr.cpp
  use the LLVM 22 form Op::create(builder, loc, ...). Two of those
  sites are arith::ConstantIntOp poison-result fallbacks (the
  unbound-handle and to_integer-rejection paths) and additionally
  use the LLVM 22 (builder, loc, type, value) signature.

Tests, runtime helpers, and docs follow as the next commit in this
PR. The dialect type itself (PR NVIDIA#4403) and the QIR conversion's
TypeConverter entry for !cc.measure_handle (PR NVIDIA#4404) are already on
main.

Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Pradnya Khalate <pkhalate@nvidia.com>
Closes the test surface for the measure_handle PR stack: every existing
AST-Quake / Transforms / targettests / docs site that observed the
old `!quake.measure` SSA shape now matches the new `!cc.measure_handle`
shape, and the new test files locking down spec-mandated behavior land
alongside.

New tests
- test/AST-Quake/measure_handle.cpp: scalar-handle bridge shape lock,
  including the bind-store / load-with-dominance round-trip and the
  `quake.discriminate` insertion at every CK_UserDefinedConversion site
  the bridge intercepts.
- test/AST-Quake/measure_handle_qir.cpp: end-to-end QIR conversion
  shape for a kernel whose handle escapes a basic block, exercising
  the cc.compute_ptr / cc.stdvec_data / cc.stdvec_init / cc.stdvec_size
  patterns added to ConvertToQIRAPI in the previous commit.
- test/AST-error/measure_handle.cpp: spec-named diagnostics for the
  unbound-handle path, the host-boundary `std::vector<measure_handle>`
  rejection, and the `cudaq::to_integer(mz(qvec))` rejection.
- test/Transforms/qir_api_measure_handle.qke: lit-replay of the QIR
  conversion's measure_handle paths against hand-rolled IR, so a
  bridge-side regression cannot mask a conversion-side regression.
- test/Transforms/cast_fold.qke: extends the cast-fold checks to the
  no-op case where a discriminate result is immediately re-cast.
- test/AST-Quake/qir_profiles.cpp: rename of base_profile-1.cpp; the
  rename is the only PR-3b contribution to that file -- LLVM 22 had
  already removed the `read_result` lines this PR originally targeted.

CHECK migrations (all unchanged shape, just the type string)
- 33 test/AST-Quake/*.cpp + 11 targettests/{Kernel,execution}/*.cpp +
  3 test/Transforms/*.qke + 3 docs/sphinx/examples/cpp/**/*.cpp
  switched from `!quake.measure` to `!cc.measure_handle` and, where
  the bridge inserts the bound-handle round-trip, picked up the
  matching `cc.alloca` / `cc.store` / `cc.load` CHECK lines.

LLVM 22 conflict resolutions
- test/AST-Quake/bug_3270.cpp: layered PR 3b's type change on LLVM 22's
  loosened `result%{{.*}}` regex form; the unused SSA captures from
  the pre-LLVM-22 form are dropped (no downstream CHECK referenced
  them).
- test/AST-Quake/if.cpp: PR 3b's `kernel_short_circuit_or` CHECK form
  is preserved verbatim (alloca/store/load + discriminate + cmpi ne
  against `arith.constant false`); the `arith.constant false` CHECK
  line LLVM 22 dropped is re-added because PR 3b's bool coercion still
  emits a `cmpi ne, x, false`.
- test/AST-Quake/to_qir.cpp: layered PR 3b's load-delay intent (move
  the `load i1, ptr %VAL_9` from before the second mz to inside the
  successor block of `br i1 %VAL_12`) on top of LLVM 22's opaque-ptr
  form; the typed-pointer / `bitcast %Result*` CHECK form from the
  pre-LLVM-22 base is dropped, and basic-block label captures stay on
  LLVM 22's loose `{{[0-9]+}}` form.
- test/AST-Quake/qir_profiles.cpp: PR 3b's only intent vs the OLD base
  was removing 6 `__quantum__qis__read_result__body` CHECK lines;
  LLVM 22 had already removed the same six lines (and additionally
  added an `array_record_output` line and the typed-to-opaque pointer
  conversion). The rename is the remaining PR 3b contribution.
- test/AST-Quake/cudaq_run.cpp, test/AST-Quake/qalloc_initialization.cpp,
  test/Transforms/qir_base_profile.qke: 3-way merge resolved cleanly;
  LLVM 22's `!llvm.ptr` / opaque-pointer churn lives alongside PR 3b's
  type-name updates without conflict.

Dropped
- unittests/CMakeLists.txt: PR 3b's directory-scope
  `add_compile_definitions(CUDAQ_LIBRARY_MODE)` is already on main as
  of PR NVIDIA#4427 (which split out the same workaround). PR 3b's wording
  reword would be a drive-by; main's wording stays.

Docs and examples
- docs/sphinx/examples/cpp/measuring_kernels.cpp,
  docs/sphinx/examples/cpp/sample_to_run_migration.cpp,
  docs/sphinx/examples/cpp/basics/mid_circuit_measurement.cpp:
  reflect the spec-mandated `cudaq::to_bools(mz(...))` and
  `bool` return-type form so the examples compile under measure_handle.

Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Pradnya Khalate <pkhalate@nvidia.com>
@khalatepradnya khalatepradnya force-pushed the pkhalate/measure-handle-pr3c-python-frontend branch from ba032ee to f0887b9 Compare May 5, 2026 02:21
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 5, 2026

CI Summary (push) — ❌ failed

Run #25408136439 · ✅ 5 · ⏩ 7 · ❌ 1 · ⛔ 0

❌ Failed or cancelled
Job Result Link
build_and_test ❌ failure view
Top-level jobs (13)
Job Result
binaries ⏩ skipped
build_and_test ❌ failure
config_devdeps ✅ success
config_source_build ⏩ skipped
config_wheeldeps ✅ success
devdeps ✅ success
docker_image ⏩ skipped
gen_code_coverage ⏩ skipped
metadata ✅ success
python_metapackages ⏩ skipped
python_wheels ⏩ skipped
source_build ⏩ skipped
wheeldeps ✅ success
⏩ Skipped jobs (7) — intentionally skipped on PR builds; run on merge_group / workflow_dispatch
Job
binaries
config_source_build
docker_image
gen_code_coverage
python_metapackages
python_wheels
source_build
All sub-jobs (40) — every matrix leg, with links
Job Status Link
Build and test (amd64, llvm, openmpi) / Dev environment (Debug) ❌ failure view
Build and test (amd64, llvm, openmpi) / Dev environment (Python) ❌ failure view
Build and test (arm64, llvm, openmpi) / Dev environment (Debug) ❌ failure view
Build and test (arm64, llvm, openmpi) / Dev environment (Python) ❌ failure view
CI Summary ❔ in_progress view
Configure build (devdeps) ✅ success view
Configure build (source_build) ⏩ skipped view
Configure build (wheeldeps) ✅ success view
Create CUDA Quantum installer ⏩ skipped view
Create Docker images ⏩ skipped view
Create Python metapackages ⏩ skipped view
Create Python wheels ⏩ skipped view
Gen code coverage ⏩ skipped view
Load dependencies (amd64, gcc12) / Caching ✅ success view
Load dependencies (amd64, gcc12) / Finalize ✅ success view
Load dependencies (amd64, gcc12) / Metadata ✅ success view
Load dependencies (amd64, llvm) / Caching ✅ success view
Load dependencies (amd64, llvm) / Finalize ✅ success view
Load dependencies (amd64, llvm) / Metadata ✅ success view
Load dependencies (arm64, gcc12) / Caching ✅ success view
Load dependencies (arm64, gcc12) / Finalize ✅ success view
Load dependencies (arm64, gcc12) / Metadata ✅ success view
Load dependencies (arm64, llvm) / Caching ✅ success view
Load dependencies (arm64, llvm) / Finalize ✅ success view
Load dependencies (arm64, llvm) / Metadata ✅ success view
Load source build cache ⏩ skipped view
Load wheel dependencies (amd64, 12.6) / Caching ✅ success view
Load wheel dependencies (amd64, 12.6) / Finalize ✅ success view
Load wheel dependencies (amd64, 12.6) / Metadata ✅ success view
Load wheel dependencies (amd64, 13.0) / Caching ✅ success view
Load wheel dependencies (amd64, 13.0) / Finalize ✅ success view
Load wheel dependencies (amd64, 13.0) / Metadata ✅ success view
Load wheel dependencies (arm64, 12.6) / Caching ✅ success view
Load wheel dependencies (arm64, 12.6) / Finalize ✅ success view
Load wheel dependencies (arm64, 12.6) / Metadata ✅ success view
Load wheel dependencies (arm64, 13.0) / Caching ✅ success view
Load wheel dependencies (arm64, 13.0) / Finalize ✅ success view
Load wheel dependencies (arm64, 13.0) / Metadata ✅ success view
Prepare cache clean-up ❔ in_progress view
Retrieve PR info ✅ success view
⚠️ Required checks (0/6) — 6 missing — declared in .github/required-checks.yml for push
Required check Status Link
Build and test (amd64, llvm, openmpi) / Dev environment (Debug) ❌ failure view
Build and test (amd64, llvm, openmpi) / Dev environment (Python) ❌ failure view
Build and test (arm64, llvm, openmpi) / Dev environment (Debug) ❌ failure view
Build and test (arm64, llvm, openmpi) / Dev environment (Python) ❌ failure view
Build and test (amd64, gcc12, openmpi) / Dev environment (Debug) ❔ missing
Build and test (amd64, gcc12, openmpi) / Dev environment (Python) ❔ missing

@khalatepradnya khalatepradnya force-pushed the pkhalate/measure-handle-pr3c-python-frontend branch from 08404ed to 3ef9232 Compare May 5, 2026 22:55
Python-side counterpart to PR 3b's C++ frontend rewire, per spec
cudaq-spec/proposals/measure_handle.bs (Python API; Operational
Semantics; IR Representation).

Bindings and host stubs
- python/runtime/mlir/py_register_dialects.cpp: bind
  `!cc.measure_handle` so the AST bridge can construct, check, and
  emit the type from Python.
- python/cudaq/kernel_types.py: `cudaq.measure_handle` kernel-type
  stub (mirrors `qubit` / `qvector` / `qview`); host-scope
  construction raises a device-only RuntimeError.
- python/cudaq/__init__.py: re-export `cudaq.measure_handle` and add
  a host stub for `cudaq.to_bools(handles)` raising the same
  device-only error -- kernel-side calls are intercepted by the AST
  bridge.
- python/cudaq/kernel/utils.py: `containsMeasureHandle(ty)`,
  mirroring `cudaq::cc::containsMeasureHandle` from PR 3b
  (`lib/Optimizer/Dialect/CC/CCTypes.cpp`); used by the bridge's
  boundary check.

AST bridge (`PyASTBridge` in `python/cudaq/kernel/ast_bridge.py`)
- mz / mx / my emit `quake.{mz,mx,my}` producing
  `!cc.measure_handle` (scalar) or `!cc.stdvec<!cc.measure_handle>`
  (vector). Discriminate insertion is deferred to coercion sites.
- `__discriminateIfMeasureHandle` inserts `quake.discriminate` at
  every spec-listed coercion site: arithmetic-to-bool (if / while /
  not / and / or), `changeOperandToType` (i1 + i8 + stdvec<i1>),
  Compare (==/!=), explicit `bool(...)`, IfExp test, Assert test;
  surfaces the `discriminating an unbound measure_handle`
  diagnostic for the default-constructed pattern.
- `cudaq.measure_handle()` -> `cc.UndefOp(!cc.measure_handle)` so
  the unbound-handle diagnostic has a recognizable source.
- `cudaq.to_bools(handles)` -> vectorized `quake.discriminate` on
  `!cc.stdvec<!cc.measure_handle>`; legacy
  `cudaq.to_integer(mz(qvec))` is rejected with a targeted
  diagnostic matching PR 3b's C++ rejection.
- Boundary check at entry-point creation: walk the kernel signature
  with `containsMeasureHandle` and reject any parameter / return
  position transitively containing a `measure_handle`. Diagnostic
  matches PR 3b's spec-canonical wording.

Tests
- python/tests/kernel/test_measure_handle.py: new spec-coverage
  suite (host-scope rejections, scalar/vector emission shape, every
  bool-coercion site, `to_bools` lowering, `to_integer(to_bools(...))`
  composition, unbound-handle diagnostic, boundary diagnostic).
- python/tests/mlir/{bug_1775,bug_1777,bug_1875,call_qpu}.py:
  type-rename and structural CHECK refresh for the new bridge IR
  shape.
- python/tests/kernel/test_{assignments,kernel_features,run_kernel,
  to_integer,kernel_shift_operators}.py: pytest migrations off
  the implicit `mz`-returns-bool assumption.

Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Pradnya Khalate <pkhalate@nvidia.com>
@khalatepradnya khalatepradnya force-pushed the pkhalate/measure-handle-pr3c-python-frontend branch from 3ef9232 to 12c9308 Compare May 5, 2026 23:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Change breaks backwards compatibility

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant