Skip to content

[cc] Measure handle type#4403

Merged
khalatepradnya merged 3 commits into
NVIDIA:mainfrom
khalatepradnya:pkhalate/measure-handle-pr1-dialect
Apr 29, 2026
Merged

[cc] Measure handle type#4403
khalatepradnya merged 3 commits into
NVIDIA:mainfrom
khalatepradnya:pkhalate/measure-handle-pr1-dialect

Conversation

@khalatepradnya
Copy link
Copy Markdown
Collaborator

@khalatepradnya khalatepradnya commented Apr 28, 2026

Summary

Introduce !cc.measure_handle - the IR alias for the source-language cudaq::measure_handle - and widen quake.mz/mx/my and quake.discriminate ODS / verifiers to admit it alongside the existing !quake.measure form. Pure IR vocabulary: no path yet produces or consumes the new type. This is the prologue of a small stack; lowering through convert-to-qir-api lands in a follow-up PR, frontend bindings after that.

Motivation

cudaq::measure_handle is 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

  • New type !cc.measure_handle in the CC dialect; i64 payload, opaque to the IR. Registered with the CC dialect, lowered to i64 in the CC->LLVM type converter, and cc.cast admits no-op i64 <-> !cc.measure_handle.
  • ODS widening on quake.mz/mx/my results and quake.discriminate operand: now !cc.measure_handle or !cc.stdvec<!cc.measure_handle> are admitted in addition to the prior !quake.measure forms.
  • Verifier widening: verifyMeasurements and DiscriminateOp::verify accept the new shape; arity diagnostics mention both spellings so users see why a scalar-typed result is rejected when measuring a register.
  • Tests: test/Transforms/roundtrip-ops.qke (passthrough + i64 <-> !cc.measure_handle cc.cast round-trip), test/Transforms/invalid.qke (verifier negatives).

Risks

No behavioral change in this PR: no path produces or consumes !cc.measure_handle until 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-api and tests it.

Downstream Impact

  • CUDA-QX: none.
  • Public API: none.
  • Stack: lowering through --convert-to-qir-api lands in a follow-up PR built on this branch; C++/Python frontend bindings land after that.

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>
Copy link
Copy Markdown
Collaborator

@schweitzpgi schweitzpgi left a comment

Choose a reason for hiding this comment

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

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.)

Comment thread include/cudaq/Optimizer/Dialect/Quake/QuakeOps.td Outdated
Comment thread include/cudaq/Optimizer/Dialect/Quake/QuakeOps.td Outdated
Comment thread lib/Optimizer/Dialect/Quake/QuakeOps.cpp Outdated
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>
@khalatepradnya khalatepradnya force-pushed the pkhalate/measure-handle-pr1-dialect branch from 08c9c65 to 0374831 Compare April 29, 2026 16:56
@khalatepradnya
Copy link
Copy Markdown
Collaborator Author

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.)

Yes for both. The measure_handle boundary check is in PR #4409

@khalatepradnya khalatepradnya added this pull request to the merge queue Apr 29, 2026
Merged via the queue into NVIDIA:main with commit b81421b Apr 29, 2026
217 checks passed
@khalatepradnya khalatepradnya deleted the pkhalate/measure-handle-pr1-dialect branch April 29, 2026 19:42
github-actions Bot pushed a commit that referenced this pull request 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants