[cc] Measure handle type#4403
Merged
khalatepradnya merged 3 commits intoApr 29, 2026
Merged
Conversation
Adds the IR alias of `cudaq::measure_handle`: an opaque, word-sized classical type whose only meaningful consumer is `quake.discriminate`. Registers the type in the CC dialect, lowers it to `i64` in the CC->LLVM type converter, and extends `cc.cast`'s verifier to permit `i64 <-> !cc.measure_handle` (no-op casts modeling the i64 payload). The type carries no payload yet -- ODS widening of the Quake measurement / DiscriminateOp signatures lands in the next commit, and the QIR conversion plus C++/Python frontend bindings land in follow-up PRs. Round-trip coverage is appended to `test/Transforms/roundtrip-ops.qke`. Made-with: Cursor Signed-off-by: Pradnya Khalate <pkhalate@nvidia.com>
2ef0a35 to
08c9c65
Compare
schweitzpgi
approved these changes
Apr 29, 2026
Collaborator
schweitzpgi
left a comment
There was a problem hiding this comment.
The intention is that both the measure type and measure_handle type may not escape the QPU (back to the CPU/GPU) correct? (I assume that check is added in a follow-up.)
ODS: the result of `quake.mz`/`mx`/`my` and the operand of `quake.discriminate` may now be `!cc.measure_handle` (single qubit) or `!cc.stdvec<!cc.measure_handle>` (register), in addition to the existing `!quake.measure` / `!cc.stdvec<!quake.measure>` forms. The vector forms are folded via `StdvecOf<[MeasureType, cc_MeasureHandleType]>` so the type list reads as one set of admitted element types rather than two separate stdvec instantiations. The two forms are interchangeable from the compiler's point of view; the handle form is emitted by the bridge for `cudaq::measure_handle` callers and is lowered to `i64` by `--convert-to-qir-api`'s `TypeConverter` in a follow-up PR. Verifier: `verifyMeasurements` and `DiscriminateOp::verify` accept either classical type. The arity diagnostics reference both spellings so users see why a scalar-typed result is rejected when measuring a register, and use `qvector` rather than the legacy `qreg` spelling. No existing measurement / discriminate FileCheck tests reference the previous diagnostic strings. Made-with: Cursor Signed-off-by: Pradnya Khalate <pkhalate@nvidia.com> Co-authored-by: Eric Schweitz <eschweitz@nvidia.com>
08c9c65 to
0374831
Compare
Collaborator
Author
Yes for both. The |
sacpis
approved these changes
Apr 29, 2026
khalatepradnya
added a commit
to khalatepradnya/cuda-quantum
that referenced
this pull request
Apr 30, 2026
…IA#4404) ## Summary * Lower `!cc.measure_handle` to its `i64` payload through `--convert-to-qir-api`'s existing `TypeConverter`, completing the IR-side of the `cudaq::measure_handle` feature. * Builds on NVIDIA#4403. ## Motivation NVIDIA#4403 introduced `!cc.measure_handle` as IR vocabulary; nothing yet routes it to QIR. This PR adds the converter rule plus boundary bridging on `quake.mz` (which still calls a QIR function returning `Result*`) and `quake.discriminate` (which still consumes `Result*`), so handle-form kernels reach the QIR pipeline as `i64` payloads through the same `TypeConverter` machinery the rest of QIR conversion already uses. ## What Changed - **`QIRAPITypeConverter`** gains three `addConversion` rules: `!cc.measure_handle -> i64`, plus recursive descent through `!cc.array<...>` and `!cc.stdvec<...>` so container-shaped function signatures, allocations, and pointers see consistent post-conversion element types. The `!cc.ptr<...>` recursion was already in place. - **`MeasurementOpPattern`** detects when the original `quake.mz` produced a handle (its `measOut` is `!cc.measure_handle`) and emits a `cc.cast Result* -> i64` so downstream uses see the converted payload. The cast is materialized in the mz call's block, ahead of the optional terminator-relative insertion point used for record-output, so it dominates downstream `quake.discriminate` uses. - **`DiscriminateOpToCallRewrite`** mirrors this on the read side: when the post-conversion operand is integer-typed it emits `cc.cast i64 -> Result*` before delegating to the existing read-result lowering. In the full-QIR (`!discriminateToClassical`) branch the bridge cast and the inner double-cast fold against each other, leaving a single `cc.cast i64 -> !cc.ptr<i1>` + `cc.load`. - **`ExpandMeasurements`** accepts `!cc.measure_handle` alongside `!quake.measure` in `usesIndividualQubit`, so single-qubit handle measurements aren't rewritten as registers. - **Predicate rename**: the misnamed `hasQuakeType` is now `needsTypeConversion`, leaf check extended to include `!cc.measure_handle`, recursion extended to descend through `!cc.array`/`!cc.stdvec`. The old name was incorrect — it has always reported "this op carries a type the converter rewrites," not "this op carries a Quake type." - **Test**: `test/Transforms/qir_api_measure_handle.qke` covering scalar handle measurement + discriminate, function signature with handle parameter and return, `cc.alloca` of a scalar handle, static- and dynamic-size arrays of handles, `cc.stdvec<!cc.measure_handle>` in a function signature, `cc.indirect_callable<() -> !cc.measure_handle>`, and a no-handle negative. ## Risks - `cc.loop` iter-args carrying `!cc.measure_handle` are not exercised by the conversion's region-aware patterns. Low immediate risk because no current frontend or test produces such IR; flagged as a follow-up. - Container types beyond `cc.array`/`cc.stdvec` (e.g., a `cc.struct` with a handle field) are not in the converter's recursion. None of the current frontends produce these; not a regression vs. the prototype. ## Downstream Impact - CUDA-QX: none. - Public API: none. - Stack: the next PR adds C++/Python frontend bindings that produce handle-form IR, which this PR now correctly routes. --------- Signed-off-by: Pradnya Khalate <pkhalate@nvidia.com> Signed-off-by: Pradnya Khalate <148914294+khalatepradnya@users.noreply.github.com>
khalatepradnya
added a commit
to khalatepradnya/cuda-quantum
that referenced
this pull request
May 5, 2026
…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>
khalatepradnya
added a commit
to khalatepradnya/cuda-quantum
that referenced
this pull request
May 7, 2026
…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>
khalatepradnya
added a commit
to khalatepradnya/cuda-quantum
that referenced
this pull request
May 11, 2026
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Introduce
!cc.measure_handle- the IR alias for the source-languagecudaq::measure_handle- and widenquake.mz/mx/myandquake.discriminateODS / verifiers to admit it alongside the existing!quake.measureform. Pure IR vocabulary: no path yet produces or consumes the new type. This is the prologue of a small stack; lowering throughconvert-to-qir-apilands in a follow-up PR, frontend bindings after that.Motivation
cudaq::measure_handleis a distinct source-language type from both raw integers and the existing measurement token: integer-shaped at the bit level, but identity-preserving for analyses that need to distinguish a measurement event from arbitrary i64 traffic. Landing the IR vocabulary first gives the QIR conversion PR and the frontend PRs a stable target without forcing the ODS contracts to churn step by step.What Changed
!cc.measure_handlein the CC dialect; i64 payload, opaque to the IR. Registered with the CC dialect, lowered toi64in the CC->LLVM type converter, andcc.castadmits no-opi64 <-> !cc.measure_handle.quake.mz/mx/myresults andquake.discriminateoperand: now!cc.measure_handleor!cc.stdvec<!cc.measure_handle>are admitted in addition to the prior!quake.measureforms.verifyMeasurementsandDiscriminateOp::verifyaccept the new shape; arity diagnostics mention both spellings so users see why a scalar-typed result is rejected when measuring a register.test/Transforms/roundtrip-ops.qke(passthrough +i64 <-> !cc.measure_handlecc.castround-trip),test/Transforms/invalid.qke(verifier negatives).Risks
No behavioral change in this PR: no path produces or consumes
!cc.measure_handleuntil the follow-up PR lands. Risk is bounded to ODS coverage gaps that would surface in the consumer; the follow-up wires up--convert-to-qir-apiand tests it.Downstream Impact
--convert-to-qir-apilands in a follow-up PR built on this branch; C++/Python frontend bindings land after that.