Skip to content

✨ Improve Static Qubit Handling#1569

Open
simon1hofmann wants to merge 17 commits intomainfrom
static_qubit_handling
Open

✨ Improve Static Qubit Handling#1569
simon1hofmann wants to merge 17 commits intomainfrom
static_qubit_handling

Conversation

@simon1hofmann
Copy link
Contributor

@simon1hofmann simon1hofmann commented Mar 17, 2026

Description

Add explicit isStatic flag to QC/QCO qubit types, make static qubits distinct from dynamic ones, preserve staticness through conversions, treat StaticOp as allocation, skip lowering dealloc for static qubits, emit deallocs at top-level returns for remaining dynamic qubits, and add tests and builders for static-qubit scenarios.

Checklist

  • The pull request only contains commits that are focused and relevant to this change.
  • I have added appropriate tests that cover the new/changed functionality.
  • I have updated the documentation to reflect these changes.
  • I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals.
  • I have added migration instructions to the upgrade guide (if needed).
  • The changes follow the project's style guidelines and introduce no new warnings.
  • The changes are fully tested and pass the CI checks.
  • I have reviewed my own code changes.

If PR contains AI-assisted content:

  • I have disclosed the use of AI tools in the PR description as per our AI Usage Guidelines.
  • AI-assisted commits include an Assisted-by: [Model Name] via [Tool Name] footer.
  • I confirm that I have personally reviewed and understood all AI-generated content, and accept full responsibility for it.

@simon1hofmann simon1hofmann added enhancement Improvement of existing feature MLIR Anything related to MLIR labels Mar 17, 2026
@simon1hofmann simon1hofmann added this to the MLIR Support milestone Mar 17, 2026
@simon1hofmann simon1hofmann self-assigned this Mar 17, 2026
@codecov
Copy link

codecov bot commented Mar 17, 2026

Codecov Report

❌ Patch coverage is 96.55172% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
mlir/lib/Conversion/QCToQCO/QCToQCO.cpp 95.8% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@simon1hofmann simon1hofmann marked this pull request as ready for review March 17, 2026 14:23
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 17, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added static vs. dynamic qubit type distinction across quantum dialects for improved resource management.
    • Implemented type-preservation constraints ensuring quantum gate output types match their corresponding input types.
    • Enhanced qubit lifecycle management with proper handling of static qubit deallocation during compilation.
  • Tests

    • Expanded test coverage with comprehensive scenarios for static qubits, parametric operations, and mixed qubit types.

Walkthrough

Add an isStatic flag to QC/QCO qubit types and assembly; preserve staticness through conversions and builders; enforce per-value qubit-type preservation for gates and modifiers; skip lowering/erasing static deallocs and insert deterministic deallocs at function return; add tests and program builders for static/dynamic scenarios.

Changes

Cohort / File(s) Summary
Type/TableGen
mlir/include/mlir/Dialect/QCO/IR/QCOTypes.td, mlir/include/mlir/Dialect/QC/IR/QCTypes.td, mlir/include/mlir/Dialect/QCO/IR/QCOOps.td, mlir/include/mlir/Dialect/QC/IR/QCOps.td
Introduce isStatic bool on QubitType (default=false), custom assembly (<static>), add DynamicQubit/StaticQubit predicates, and update alloc/dealloc/StaticOp builders and docs.
Dialect parse/print & op verifiers
mlir/lib/Dialect/QCO/IR/QCOOps.cpp, mlir/lib/Dialect/QC/IR/QCOps.cpp
Add QubitType parse/print to round-trip <static>; accept/emit optional inline types in aliasing; update verifiers to validate per-value type preservation.
Conversions & lowering
mlir/lib/Conversion/QCOToQC/QCOToQC.cpp, mlir/lib/Conversion/QCToQCO/QCToQCO.cpp, mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp
Preserve isStatic in type conversion; QCO→QC dealloc lowering erases qco.dealloc for static qubits and lowers dynamic deallocs; add stateful ConvertFuncReturnOp to emit deterministic qco.dealloc for live mapped qubits and tighten func.return legality.
Op traits & unitary/measure updates
mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
Add TypesMatchWith constraints to measure and many 1-/2-target unitaries so outputs match corresponding input qubit types; update builders to reuse input value types.
Builders & modifiers (per-value types)
mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp, mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp, mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp, mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp
Use raw int64_t for StaticOp builder; stop using a single canonical QubitType—create region/block args and verify using each operand’s concrete qubit type.
Tensor alloc & uses
mlir/lib/Dialect/QTensor/IR/Operations/AllocOp.cpp
Ensure qtensor alloc builds element qubits as dynamic (isStatic=false) and add verify error if element type is static.
Mapping & internal casts
mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp, mlir/lib/Dialect/QCO/IR/QCOOps.cpp
Use explicit typed cast for qubit values in placement/population; adjust alias parsing/printing to preserve staticness.
Unit tests & program builders
mlir/unittests/programs/*, mlir/unittests/*/*/*test_*.cpp
Add staticQubitsWithOps and multiple QCO/QC/QIR program builders; instantiate parametrized tests covering static scenarios and add QCO unit tests asserting per-target type preservation.
Changelog
CHANGELOG.md
Add PR reference [#1569] under Unreleased Added.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant QCToQCO as QC→QCO Pass
    participant State as ConversionState
    participant ConvertReturn as ConvertFuncReturnOp
    participant QCO as QCO Dialect

    Client->>QCToQCO: run conversion on function
    QCToQCO->>State: update state.qubitMap for alloc/rewrites
    loop process ops
        QCToQCO->>State: update mapping / mark escapes
    end
    QCToQCO->>ConvertReturn: encounter func.return
    ConvertReturn->>State: collect live mapped qubits
    ConvertReturn->>ConvertReturn: sort qubits deterministically
    loop per live qubit
        ConvertReturn->>QCO: emit `qco.dealloc` (dynamic only)
    end
    ConvertReturn->>State: clear qubitMap
    ConvertReturn->>QCToQCO: replace func.return operands
    QCToQCO->>Client: return converted function
Loading
sequenceDiagram
    participant Client
    participant QCOToQC as QCO→QC Pass
    participant ConvertDealloc as ConvertQCODeallocOp
    participant QC as QC Dialect

    Client->>QCOToQC: process `qco.dealloc`
    QCOToQC->>ConvertDealloc: lower dealloc
    ConvertDealloc->>ConvertDealloc: check operand.isStatic?
    alt static qubit
        ConvertDealloc->>QCOToQC: erase `qco.dealloc`
    else dynamic qubit
        ConvertDealloc->>QC: create `qc.dealloc` with converted qubit
        ConvertDealloc->>QCOToQC: replace op
    end
    QCOToQC->>Client: continue lowering
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • burgholzer
  • denialhaag

Poem

🐇 I hop on qubits, static and fleet,
I nudge the builders so types always meet,
Statics hold their spots, dynamics roam free,
Deallocs at return tidy the tree,
A small rabbit cheer for type safety!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.31% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly summarizes the main change: improving static qubit handling through type constraints and verification mechanisms.
Description check ✅ Passed The PR description covers the core objectives comprehensively, including the isStatic flag addition, type preservation, MemoryEffects changes, dealloc handling, and test additions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch static_qubit_handling
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mlir/lib/Conversion/QCToQCO/QCToQCO.cpp`:
- Around line 136-163: state.qubitMap contains SSA values for return operands
too, so the current loop deallocates qubits that are about to escape via
func::ReturnOp; modify the qubit collection before sorting/Dealloc emission to
exclude any Values that appear in adaptor.getOperands() (the operands passed to
the new func::ReturnOp) so those escaping qubits are not deallocated; locate the
block that builds liveQubits from state.qubitMap, filter out
adaptor.getOperands() (or build a DenseSet of return operands and skip them when
inserting into liveQubits), then continue with the deterministic sort and
creation of qco::DeallocOp for the remaining qubits and clear state.qubitMap as
before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 535af8d9-20fd-4c6d-b9b8-f07a948730d4

📥 Commits

Reviewing files that changed from the base of the PR and between a5cf121 and 77e00a4.

📒 Files selected for processing (16)
  • mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
  • mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
  • mlir/lib/Conversion/QCToQCO/QCToQCO.cpp
  • mlir/unittests/Compiler/test_compiler_pipeline.cpp
  • mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp
  • mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp
  • mlir/unittests/Conversion/QCToQIR/test_qc_to_qir.cpp
  • mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp
  • mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp
  • mlir/unittests/programs/qc_programs.cpp
  • mlir/unittests/programs/qc_programs.h
  • mlir/unittests/programs/qco_programs.cpp
  • mlir/unittests/programs/qco_programs.h
  • mlir/unittests/programs/qir_programs.cpp
  • mlir/unittests/programs/qir_programs.h

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mlir/lib/Conversion/QCToQCO/QCToQCO.cpp`:
- Around line 139-142: The current code builds escapedQubits from
adaptor.getOperands(), which returns stale allocation values because gate
patterns (e.g., ConvertQCOneTargetZeroParameterToQCO) call rewriter.eraseOp()
and only update state.qubitMap while ConvertQCAllocOp uses replaceOpWithNewOp()
and updates the ConversionValueMapping; fix by iterating the original return
operands but for each Value check state.qubitMap for a mapped replacement and
insert that mapped Value into escapedQubits (falling back to the original
operand only if no entry exists), so ConvertFuncReturnOp/func::ReturnOp uses the
final mapped qubit values instead of stale adaptor.getOperands() results.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0f33f39f-095a-4961-8d6e-a6cbabbdf6ca

📥 Commits

Reviewing files that changed from the base of the PR and between 77e00a4 and 71bbe39.

📒 Files selected for processing (1)
  • mlir/lib/Conversion/QCToQCO/QCToQCO.cpp

@burgholzer
Copy link
Member

Is this one ready for review? 🙂

…ly an operation across multiple program builders (QC, QCO, QIR).
… QCO, QIR) to enhance coverage and ensure functionality.
…ted test cases across various dialects and programs to reflect the new functionality of allocating multiple static qubits and applying operations.
…lated operations and conversions in QCO and QC dialects. Add tests for static qubit type propagation and ensure correct handling in various operations.
…allocation, modifying builders and operations accordingly.
@simon1hofmann simon1hofmann force-pushed the static_qubit_handling branch from 71bbe39 to 0221058 Compare March 19, 2026 08:50
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mlir/include/mlir/Dialect/QC/IR/QCOps.td`:
- Around line 117-126: The builder currently creates a static qubit in C++ but
the ODS op signature still allows a generic QubitType result, so textual IR like
`qc.static 0 : !qc.qubit` can bypass the static-qubit invariant; update the op
definition in QCOps.td to constrain the result to a static qubit (e.g., change
the results from (outs QubitType:$qubit) to an explicit static qubit type such
as (outs QubitType<static>:$qubit) or the ODS equivalent !qc.qubit<static>), and
adjust the assemblyFormat if needed so the textual form requires the static
qubit type; keep the existing builder call that uses QubitType::get(...,
/*isStatic=*/true).

In `@mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp`:
- Around line 129-130: The new block argument is being created with a hardcoded
non-static QubitType which loses the promoted control's concrete staticness;
instead, fetch the original promoted control's type (e.g., from the control
operand collection like op.getControls()[<promotedIndex>] or
op.getControl(<promotedIndex>).getType()) and pass that Type into
op.getBody()->addArgument(...) so the added argument preserves the exact
QubitType (including isStatic) and location rather than using
QubitType::get(..., /*isStatic=*/false).

In `@mlir/lib/Dialect/QCO/IR/QCOOps.cpp`:
- Around line 74-75: The code is hardcoding QubitType::get(...,
/*isStatic=*/false) when creating aliased region block arguments (newArg.type),
which loses staticness; update parseTargetAliasing to accept the parsed target
type list (thread the parsed target types through the custom assembly directive
inputs) and use the original parsed target's staticness when constructing
newArg.type (i.e., derive isStatic from the corresponding parsed target type
instead of always false), ensuring QubitType::get preserves static vs dynamic
qubit semantics during parsing and verification.

In `@mlir/lib/Dialect/QTensor/IR/Operations/AllocOp.cpp`:
- Around line 34-36: AllocOp::verify currently allows a static qco::QubitType
element type but the builder now always materializes qtensor.alloc with a
non-static qubit; update AllocOp::verify() to enforce the new invariant by
checking the element type of the result RankedTensorType (or the operand/result
type used by AllocOp) and emit an error if qco::QubitType::isStatic (or the
equivalent static flag) is true, ensuring parser/hand-written IR cannot claim a
static allocation and referencing AllocOp::verify, qtensor.alloc and
qco::QubitType to locate the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a308d815-bb91-40ea-971c-273068af3b15

📥 Commits

Reviewing files that changed from the base of the PR and between 71bbe39 and 0221058.

📒 Files selected for processing (28)
  • CHANGELOG.md
  • mlir/include/mlir/Dialect/QC/IR/QCOps.td
  • mlir/include/mlir/Dialect/QC/IR/QCTypes.td
  • mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
  • mlir/include/mlir/Dialect/QCO/IR/QCOTypes.td
  • mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp
  • mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
  • mlir/lib/Conversion/QCToQCO/QCToQCO.cpp
  • mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp
  • mlir/lib/Dialect/QC/IR/QCOps.cpp
  • mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp
  • mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp
  • mlir/lib/Dialect/QCO/IR/QCOOps.cpp
  • mlir/lib/Dialect/QTensor/IR/Operations/AllocOp.cpp
  • mlir/unittests/Compiler/test_compiler_pipeline.cpp
  • mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp
  • mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp
  • mlir/unittests/Conversion/QCToQIR/test_qc_to_qir.cpp
  • mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp
  • mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp
  • mlir/unittests/programs/qc_programs.cpp
  • mlir/unittests/programs/qc_programs.h
  • mlir/unittests/programs/qco_programs.cpp
  • mlir/unittests/programs/qco_programs.h
  • mlir/unittests/programs/qir_programs.cpp
  • mlir/unittests/programs/qir_programs.h

… loop constructs in CtrlOp and InvOp, and ensure proper type casting in StaticOp creation.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp (1)

129-130: ⚠️ Potential issue | 🟠 Major

Preserve the promoted control's concrete qubit type.

After the segment size adjustment (lines 122-126), the first original control is now op.getInputTarget(0). Using hardcoded isStatic=false loses the static flag if the promoted control was a static qubit.

🛠️ Suggested fix
-    auto arg = op.getBody()->addArgument(
-        QubitType::get(rewriter.getContext(), /*isStatic=*/false), op.getLoc());
+    auto arg = op.getBody()->addArgument(op.getInputTarget(0).getType(), op.getLoc());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp` around lines 129 - 130, The new
argument creation uses a hardcoded QubitType::get(..., /*isStatic=*/false) which
discards the original control's static flag; fetch the original control's qubit
type from op.getInputTarget(0) (or its type accessor), extract its isStatic
property, and pass that boolean into QubitType::get(rewriter.getContext(),
isStatic) when calling op.getBody()->addArgument so the promoted control
preserves its concrete static/dynamic flag while keeping op.getLoc().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp`:
- Around line 129-130: The new argument creation uses a hardcoded
QubitType::get(..., /*isStatic=*/false) which discards the original control's
static flag; fetch the original control's qubit type from op.getInputTarget(0)
(or its type accessor), extract its isStatic property, and pass that boolean
into QubitType::get(rewriter.getContext(), isStatic) when calling
op.getBody()->addArgument so the promoted control preserves its concrete
static/dynamic flag while keeping op.getLoc().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8ecdc976-232b-4f31-92ae-03315e16bc8c

📥 Commits

Reviewing files that changed from the base of the PR and between 0221058 and a5ffdef.

📒 Files selected for processing (5)
  • mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
  • mlir/lib/Conversion/QCToQCO/QCToQCO.cpp
  • mlir/lib/Dialect/QC/IR/QCOps.cpp
  • mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp

… verification to ensure static qubit type, and update related operations to support static qubit handling.
@mergify mergify bot added the conflict label Mar 19, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp (1)

118-131: ⚠️ Potential issue | 🔴 Critical

Capture the promoted control type before rewriting the segment sizes.

After Line 123 updates the operand/result segment sizes, op.getControlsIn().back() no longer refers to the promoted control; it refers to the last remaining control. With mixed static/dynamic controls, this can create the new block argument with the wrong type and make the rewritten qco.ctrl fail verification or drop isStatic.

🛠️ Minimal fix
-    auto newSegments = DenseI32ArrayAttr::get(
-        rewriter.getContext(), {segmentsAttr[0] - 1, segmentsAttr[1] + 1});
+    auto promotedControlType = op.getControlsIn().back().getType();
+    auto newSegments = DenseI32ArrayAttr::get(
+        rewriter.getContext(), {segmentsAttr[0] - 1, segmentsAttr[1] + 1});
     op->setAttr(opSegmentsAttrName, newSegments);
     const auto opResultSegmentsAttrName = CtrlOp::getResultSegmentSizeAttr();
     op->setAttr(opResultSegmentsAttrName, newSegments);

     // Add a block argument for the promoted target qubit, preserving the
     // control's type (including isStatic)
-    auto promotedControlType = op.getControlsIn().back().getType();
     auto arg = op.getBody()->addArgument(promotedControlType, op.getLoc());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp` around lines 118 - 131, The
code adjusts operand/result segment sizes before capturing the promoted control
type, so op.getControlsIn().back() points to the wrong control after the
rewrite; fix it by reading and storing the promoted control type into
promotedControlType (from op.getControlsIn().back().getType()) before calling
CtrlOp::getOperandSegmentSizeAttr/getResultSegmentSizeAttr and updating
segments, then use that stored promotedControlType when calling
op.getBody()->addArgument so the new block arg preserves the original control's
type (including isStatic).
mlir/lib/Dialect/QCO/IR/QCOOps.cpp (1)

71-75: ⚠️ Potential issue | 🟠 Major

Aliased region arguments still downcast static qubits to dynamic.

parseTargetAliasing() is shared by qco.ctrl, qco.inv, and qco.if, so hardcoding !qco.qubit here strips isStatic from textual IR block arguments. Static-target programs will either fail verification later or lose the static bit during parsing. Please thread the corresponding operand types into this helper, or reorder the assembly so those types are available when the block arguments are created.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlir/lib/Dialect/QCO/IR/QCOOps.cpp` around lines 71 - 75, parseTargetAliasing
currently hardcodes newArg.type to QubitType::get(..., isStatic=false), which
strips the static bit for aliased region arguments used by
qco.ctrl/qco.inv/qco.if; update parseTargetAliasing to accept or compute the
intended operand type (preserve isStatic) instead of hardcoding — e.g., add a
parameter (or retrieve the operand type before creating block args) and set
newArg.type to that operand type rather than calling QubitType::get(...,
/*isStatic=*/false); ensure callers in qco.ctrl, qco.inv and qco.if pass the
correct static/dynamic qubit type so the static bit is preserved during parsing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp`:
- Around line 118-131: The code adjusts operand/result segment sizes before
capturing the promoted control type, so op.getControlsIn().back() points to the
wrong control after the rewrite; fix it by reading and storing the promoted
control type into promotedControlType (from op.getControlsIn().back().getType())
before calling CtrlOp::getOperandSegmentSizeAttr/getResultSegmentSizeAttr and
updating segments, then use that stored promotedControlType when calling
op.getBody()->addArgument so the new block arg preserves the original control's
type (including isStatic).

In `@mlir/lib/Dialect/QCO/IR/QCOOps.cpp`:
- Around line 71-75: parseTargetAliasing currently hardcodes newArg.type to
QubitType::get(..., isStatic=false), which strips the static bit for aliased
region arguments used by qco.ctrl/qco.inv/qco.if; update parseTargetAliasing to
accept or compute the intended operand type (preserve isStatic) instead of
hardcoding — e.g., add a parameter (or retrieve the operand type before creating
block args) and set newArg.type to that operand type rather than calling
QubitType::get(..., /*isStatic=*/false); ensure callers in qco.ctrl, qco.inv and
qco.if pass the correct static/dynamic qubit type so the static bit is preserved
during parsing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f92294df-f746-4c7d-844f-c93aef76d90e

📥 Commits

Reviewing files that changed from the base of the PR and between a5ffdef and 6af63db.

📒 Files selected for processing (6)
  • mlir/include/mlir/Dialect/QC/IR/QCOps.td
  • mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
  • mlir/lib/Dialect/QC/IR/QCOps.cpp
  • mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/QCO/IR/QCOOps.cpp
  • mlir/lib/Dialect/QTensor/IR/Operations/AllocOp.cpp

simon1hofmann and others added 2 commits March 19, 2026 10:49
…nce QubitType handling in parser and printer for static qubits, and ensure proper type preservation during control operation adjustments.
Signed-off-by: simon1hofmann <119581649+simon1hofmann@users.noreply.github.com>
@mergify mergify bot removed the conflict label Mar 19, 2026
@simon1hofmann
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 19, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@simon1hofmann
Copy link
Contributor Author

simon1hofmann commented Mar 19, 2026

@burgholzer Would be great to get some first feedback about the proposed changes. The mapping currently does not work as only the type of dynamic qubits gets changed to static, which also has to be taken into account for all other gates that consumed those dynamic qubits and therefore still return the wrong type.

Copy link
Member

@denialhaag denialhaag left a comment

Choose a reason for hiding this comment

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

Thanks a lot for the improvements! This is already looking really good to me!

I have a few comments below, and most should be straightforward to fix. I'm not super sure how to proceed with the conversion from QC to QCO. For now, it should be fine to deallocate all qubits that are still being tracked, but this will change once we allow functions that can return qubits.

* `qco.dealloc`. This pattern inserts `qco.dealloc` operations for all
* still-live qubits tracked in the lowering state right before the return.
*/
struct ConvertFuncReturnOp final : StatefulOpConversionPattern<func::ReturnOp> {
Copy link
Member

Choose a reason for hiding this comment

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

Here, we probably have to be a bit more careful. Once functions can return qubits, this won't work anymore.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does the implementation not treat returned qubits correctly? escapedQubits holds the return operands, and liveQubits is built by skipping those, so returned qubits are never deallocated.

Copy link
Member

Choose a reason for hiding this comment

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

Ahh, my bad... After looking at the implementation more carefully now, I have three comments:

  1. Is the check for nested regions necessary? I don't think our current implementation would allow a func.return in any of our modifiers.
  2. Is the seen map necessary? This would be relevant if several QC qubits are mapped to the same QCO qubit, right? If that happened, something went horribly wrong, right?
  3. The current implementation doesn't allow for multiple functions anyway, but at the moment, the qubit map is created once for the entire conversion, so we probably shouldn't clear it at the end of the first function. 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressing your comments:

  1. makes sense, can be removed.
  2. I tried to simplify it.
  3. Not entirely sure, but I think the qubit map has to be cleared for func.return to be considered legal and for multiple functions there would be individual qubit maps anyway right?

Copy link
Member

Choose a reason for hiding this comment

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

Regarding 3): I think this is what @li-mingbao already had set up to some degree in #1396

Comment on lines +314 to +315
auto qcoOp = qco::StaticOp::create(rewriter, op.getLoc(),
static_cast<int64_t>(op.getIndex()));
Copy link
Member

Choose a reason for hiding this comment

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

Is this cast necessary? The type should be the same for both operations, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because getIndex returns uint64_t and StaticOp::create expects int64_t.

Copy link
Member

Choose a reason for hiding this comment

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

See also my other comment, I was also confused by this.

…bit and StaticQubit types, update conversion logic for deallocation operations, and enhance documentation for clarity.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mlir/include/mlir/Dialect/QCO/IR/QCOOps.td (1)

102-126: 🧹 Nitpick | 🔵 Trivial

Consider adding ODS-level type constraint for StaticOp result.

Unlike QC's StaticOp which now uses StaticQubit:$qubit, QCO's StaticOp result is still QubitType:$qubit (line 116) with runtime verification via hasVerifier=1. While the verifier enforces the invariant, textual IR like qco.static 0 : !qco.qubit can be parsed without error until verification runs.

For consistency with QC and to catch malformed IR earlier, consider defining StaticQubit/DynamicQubit type constraints in QCO (similar to lines 33-41 in QCOps.td) and constraining the result type:

+def StaticQubit : Type<
+    And<[CPred<"::mlir::isa<::mlir::qco::QubitType>($_self)">,
+         CPred<"::mlir::cast<::mlir::qco::QubitType>($_self).getIsStatic()">]>,
+    "static qubit type (!qco.qubit<static>)">;

 def StaticOp : QCOOp<"static", [MemoryEffects<[MemAlloc]>]> {
     ...
-    let results = (outs QubitType:$qubit);
+    let results = (outs StaticQubit:$qubit);

This is optional since the verifier provides functional correctness, but would provide stricter IR well-formedness at parse time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlir/include/mlir/Dialect/QCO/IR/QCOOps.td` around lines 102 - 126, The
StaticOp currently returns a generic QubitType:$qubit with runtime-only checks
(hasVerifier=1); add ODS-level type constraints named StaticQubit and
DynamicQubit in QCO (mirroring the existing QC pattern) and change StaticOp to
return StaticQubit:$qubit so malformed IR is rejected at parse time; update the
Op builders/signatures that reference QubitType::$qubit (e.g., the OpBuilder in
StaticOp) to construct the StaticQubit type and ensure the verifier remains
consistent with the new constrained result type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@mlir/include/mlir/Dialect/QCO/IR/QCOOps.td`:
- Around line 102-126: The StaticOp currently returns a generic QubitType:$qubit
with runtime-only checks (hasVerifier=1); add ODS-level type constraints named
StaticQubit and DynamicQubit in QCO (mirroring the existing QC pattern) and
change StaticOp to return StaticQubit:$qubit so malformed IR is rejected at
parse time; update the Op builders/signatures that reference QubitType::$qubit
(e.g., the OpBuilder in StaticOp) to construct the StaticQubit type and ensure
the verifier remains consistent with the new constrained result type.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d1b777a5-c1d8-4bc8-a204-6b32dc3adbd2

📥 Commits

Reviewing files that changed from the base of the PR and between ec4f2fa and 6ff64e4.

📒 Files selected for processing (3)
  • mlir/include/mlir/Dialect/QC/IR/QCOps.td
  • mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
  • mlir/lib/Conversion/QCOToQC/QCOToQC.cpp

…pdate StaticOp to return StaticQubit, and improve type casting in mapping transformations for better static qubit handling.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mlir/include/mlir/Dialect/QCO/IR/QCOOps.td`:
- Around line 82-88: The AllocOp currently declares its result as the generic
QubitType which allows static qubits in textual IR; change the op result type in
the op definition to the DynamicQubit constraint (replace QubitType with
DynamicQubit) so the op signature only accepts dynamic qubits, and update the
OpBuilder factory signatures (the build overloads that call QubitType::get(...,
/*isStatic=*/false)) to use DynamicQubit as the result type reference; ensure
any related verify/build code (AllocOp::verify and the build(...) calls) remains
consistent with DynamicQubit so textual IR cannot create a static allocation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7ad2d62c-d4ea-43d0-ab3e-63043bdc20f3

📥 Commits

Reviewing files that changed from the base of the PR and between 6ff64e4 and e0689dc.

📒 Files selected for processing (4)
  • mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
  • mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
  • mlir/lib/Dialect/QCO/IR/QCOOps.cpp
  • mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp

…urnOp to improve live qubit collection, and enhance dynamic qubit handling in mapping transformations.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mlir/include/mlir/Dialect/QCO/IR/QCOOps.td`:
- Around line 953-955: The BarrierOp (qco.barrier) must be updated to
participate in the pairwise type-preservation rollout: change its declaration so
each qubits_out operand/result is tied to the corresponding qubits_in (same
pattern used by XXPlusYYOp/TwoTargetTwoParameter) and add a verifier to ensure
the qubits_out types match qubits_in types; specifically update the BarrierOp
(BarrierOp / qco.barrier) TD to include the appropriate TypesMatchWith/trait or
explicit result-to-operand tie and implement/enable a verifier that rejects
mismatched types (e.g., disallow !qco.qubit<static> -> !qco.qubit), mirroring
the approach used by XXPlusYYOp and other two-target two-parameter ops.
- Around line 29-41: The SameOperandsAndResultType trait on the qco.ctrl op is
too strict given the split between !qco.qubit and !qco.qubit<static>; replace it
with a custom check that enforces pairwise input->output type equality
separately for controls and for targets rather than one global equality.
Concretely, remove SameOperandsAndResultType from the qco.ctrl op definition in
QCOOps.td and add either a custom trait or a verifier that iterates over
controls.getTypes() and the corresponding control result types to assert
equality, then does the same for targets.getTypes() and their results (match the
pattern used by the builder that uses controls.getTypes()/targets.getTypes() and
keep compatibility with the ReduceCtrl canonicalization logic). Ensure the
verifier/trai t uses the op name qco.ctrl and maps each input index to the
corresponding result index rather than requiring a single unified type across
all qubits.

In `@mlir/lib/Conversion/QCToQCO/QCToQCO.cpp`:
- Around line 149-152: The if-statement that conditionally inserts into
liveQubitsSet should use braces to avoid lint errors and accidental breakage: in
the loop over state.qubitMap (variables qcQubit, qcoQubit) wrap the body of the
if (!escapedQubits.contains(qcoQubit)) in { ... } and place
liveQubitsSet.insert(qcoQubit); inside those braces so the insert is clearly
scoped to the conditional.
- Around line 1231-1235: Remove the call to
populateReturnOpTypeConversionPattern() so the generic func::ReturnOp conversion
is not registered alongside the specialized ConvertFuncReturnOp; specifically,
keep patterns.add<ConvertFuncReturnOp>(typeConverter, context, &state) and the
target.addDynamicallyLegalOp<func::ReturnOp>(...) dynamic legality check, but
delete the populateReturnOpTypeConversionPattern(patterns, typeConverter) line
to ensure ConvertFuncReturnOp (which uses state.qubitMap and emits qco.dealloc)
is the sole return conversion and prevent the generic pattern from rebuilding
returns from adaptor.getOperands().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 06603ade-89f1-4ab7-ac3b-a60fa5defeda

📥 Commits

Reviewing files that changed from the base of the PR and between e0689dc and c624c3d.

📒 Files selected for processing (3)
  • mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
  • mlir/lib/Conversion/QCToQCO/QCToQCO.cpp
  • mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp

Comment on lines +953 to +955
def XXPlusYYOp : QCOOp<"xx_plus_yy", traits = [UnitaryOpInterface, TwoTargetTwoParameter,
TypesMatchWith<"result type matches input", "qubit0_in", "qubit0_out", "$_self">,
TypesMatchWith<"result type matches input", "qubit1_in", "qubit1_out", "$_self">]> {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Include qco.barrier in this pairwise type-preservation rollout.

The simple value-producing qubit ops updated here now tie outputs to inputs, but BarrierOp still has independent qubits_out types and no verifier. That still admits IR like qco.barrier %q : !qco.qubit<static> -> !qco.qubit, which reintroduces the same type drift this PR is fixing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlir/include/mlir/Dialect/QCO/IR/QCOOps.td` around lines 953 - 955, The
BarrierOp (qco.barrier) must be updated to participate in the pairwise
type-preservation rollout: change its declaration so each qubits_out
operand/result is tied to the corresponding qubits_in (same pattern used by
XXPlusYYOp/TwoTargetTwoParameter) and add a verifier to ensure the qubits_out
types match qubits_in types; specifically update the BarrierOp (BarrierOp /
qco.barrier) TD to include the appropriate TypesMatchWith/trait or explicit
result-to-operand tie and implement/enable a verifier that rejects mismatched
types (e.g., disallow !qco.qubit<static> -> !qco.qubit), mirroring the approach
used by XXPlusYYOp and other two-target two-parameter ops.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp`:
- Around line 458-461: The explicit llvm::cast in the map_range lambda is likely
unnecessary; inspect AllocOp's generated accessor (AllocOp::getResult) and if it
already returns TypedValue<QubitType> (or a compatible typed accessor), replace
the lambda body to return op.getResult() directly (keeping QubitValue as the
SmallVector element type) and remove llvm::cast; if the generated accessor is
untyped, keep the cast but consider using a safer .template
cast<TypedValue<QubitType>>() or a typed getter if available.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e169feff-bd24-4e4e-b4b2-99f015262d3f

📥 Commits

Reviewing files that changed from the base of the PR and between e0689dc and c624c3d.

📒 Files selected for processing (3)
  • mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
  • mlir/lib/Conversion/QCToQCO/QCToQCO.cpp
  • mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp

…e equality checks for BarrierOp and CtrlOp, and update BarrierOp documentation for clarity on type preservation. Refactor ConvertFuncReturnOp to improve live qubit handling.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mlir/lib/Conversion/QCToQCO/QCToQCO.cpp (1)

1217-1240: ⚠️ Potential issue | 🔴 Critical

func.func / func.call still bypass LoweringState.

ConvertFuncReturnOp fixes escaping qubits, but function arguments and call results still go through the generic FuncConversions helpers. Those patterns never seed or update state.qubitMap, so a valid helper like func.func @f(%q: !qc.qubit) -> !qc.qubit { qc.h %q; func.return %q } will still hit the qubitMap.contains(...) asserts on the first gate, and the same applies after a qubit-returning func.call. This needs matching stateful func.func / func.call patterns, or the legality checks should reject qubit-typed helper functions until that support exists. Based on learnings: In the QCToQCO conversion pass, the dynamic legality checks for func::CallOp, func::FuncOp, and func::ReturnOp assume operations are either all classical types or all qubit types.

🛠️ Registration sketch
-    populateFunctionOpInterfaceTypeConversionPattern<func::FuncOp>(
-        patterns, typeConverter);
+    patterns.add<ConvertFuncFuncOp>(typeConverter, context, &state);
     target.addDynamicallyLegalOp<func::FuncOp>([&](func::FuncOp op) {
       return typeConverter.isSignatureLegal(op.getFunctionType()) &&
              typeConverter.isLegal(&op.getBody());
     });
@@
-    populateCallOpTypeConversionPattern(patterns, typeConverter);
+    patterns.add<ConvertFuncCallOp>(typeConverter, context, &state);
     target.addDynamicallyLegalOp<func::CallOp>(
         [&](const func::CallOp op) { return typeConverter.isLegal(op); });

ConvertFuncFuncOp should seed state.qubitMap from each original QC block argument to the converted QCO block argument, and ConvertFuncCallOp should record the mapping from each original QC call result to the new QCO call result.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlir/lib/Conversion/QCToQCO/QCToQCO.cpp` around lines 1217 - 1240, The
func.func/func.call conversions are bypassing LoweringState (state.qubitMap)
because you used populateFunctionOpInterfaceTypeConversionPattern and
populateCallOpTypeConversionPattern; implement stateful conversion patterns
instead: add a ConvertFuncFuncOp pattern that, when lowering a func::FuncOp,
seeds state.qubitMap by mapping each original QC block argument to the new
converted QCO block argument (so subsequent ops see those qubits), and add a
ConvertFuncCallOp pattern that, when lowering a func::CallOp, records mappings
from each original QC call result to the new QCO call result in state.qubitMap;
register these patterns in place of (or in addition to) the populate* helpers
and keep the dynamic legality checks (target.addDynamicallyLegalOp for
func::FuncOp and func::CallOp) so qubit-typed helpers are only considered legal
when state.qubitMap handling is present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp`:
- Around line 258-277: The verify() method in CtrlOp must first validate that
the result segments' arities match the input counts: add checks that
getControlsOut().size() == getNumControls() and getTargetsOut().size() ==
getNumTargets() before the pairwise type loops; if a mismatch, return
emitOpError(...) indicating the controls_out/targets_out arity mismatch. After
these guards, keep the existing pairwise comparisons that compare
getControlsIn()[i].getType() vs getControlsOut()[i].getType() and
getTargetsIn()[i].getType() vs getTargetsOut()[i].getType().

In `@mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp`:
- Around line 80-92: In BarrierOp::verify(), ensure the output arity matches the
expected number before the pairwise loop: compute numTargets via getNumTargets()
then check that getQubitsOut().size() == numTargets (or equivalently
getQubitsOut().size() == getQubitsIn().size()) and emit an error via emitOpError
when it does not; only after this guard iterate over i from 0 to numTargets and
compare getQubitsIn()[i].getType() with getQubitsOut()[i].getType() to preserve
safety when textual IR supplies fewer results than inputs.

---

Outside diff comments:
In `@mlir/lib/Conversion/QCToQCO/QCToQCO.cpp`:
- Around line 1217-1240: The func.func/func.call conversions are bypassing
LoweringState (state.qubitMap) because you used
populateFunctionOpInterfaceTypeConversionPattern and
populateCallOpTypeConversionPattern; implement stateful conversion patterns
instead: add a ConvertFuncFuncOp pattern that, when lowering a func::FuncOp,
seeds state.qubitMap by mapping each original QC block argument to the new
converted QCO block argument (so subsequent ops see those qubits), and add a
ConvertFuncCallOp pattern that, when lowering a func::CallOp, records mappings
from each original QC call result to the new QCO call result in state.qubitMap;
register these patterns in place of (or in addition to) the populate* helpers
and keep the dynamic legality checks (target.addDynamicallyLegalOp for
func::FuncOp and func::CallOp) so qubit-typed helpers are only considered legal
when state.qubitMap handling is present.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 345a36cd-c98f-4011-8321-85e37966cdf9

📥 Commits

Reviewing files that changed from the base of the PR and between e0689dc and 8499977.

📒 Files selected for processing (5)
  • mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
  • mlir/lib/Conversion/QCToQCO/QCToQCO.cpp
  • mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp
  • mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp

Comment on lines 258 to +277
LogicalResult CtrlOp::verify() {
// Pairwise input->output type equality for controls and targets separately
// (allows !qco.qubit and !qco.qubit<static> to differ between
// controls/targets)
for (size_t i = 0; i < getNumControls(); ++i) {
if (getControlsIn()[i].getType() != getControlsOut()[i].getType()) {
return emitOpError("qco.ctrl control ")
<< i << " input type must match output type (got "
<< getControlsIn()[i].getType() << " vs "
<< getControlsOut()[i].getType() << ")";
}
}
for (size_t i = 0; i < getNumTargets(); ++i) {
if (getTargetsIn()[i].getType() != getTargetsOut()[i].getType()) {
return emitOpError("qco.ctrl target ")
<< i << " input type must match output type (got "
<< getTargetsIn()[i].getType() << " vs "
<< getTargetsOut()[i].getType() << ")";
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate the result segment arities before the pairwise type checks.

getNumControls() and getNumTargets() come from the input segments, but these loops index the output segments immediately. Malformed textual IR with too few controls_out / targets_out results will walk past the result range here, and extra results currently go unnoticed. Add explicit getControlsOut().size() == getNumControls() and getTargetsOut().size() == getNumTargets() checks before the pairwise comparisons.

🛠️ Suggested guard
 LogicalResult CtrlOp::verify() {
+  if (getControlsOut().size() != getNumControls()) {
+    return emitOpError(
+        "number of control results must match number of control operands");
+  }
+  if (getTargetsOut().size() != getNumTargets()) {
+    return emitOpError(
+        "number of target results must match number of target operands");
+  }
+
   // Pairwise input->output type equality for controls and targets separately
   // (allows !qco.qubit and !qco.qubit<static> to differ between
   // controls/targets)
🧰 Tools
🪛 ast-grep (0.41.1)

[warning] 270-270: The getTargetsIn function returns NULL on error and this line dereferences the return value without checking for NULL.
Context: getTargetsIn()[i]
Note: [CWE-476] NULL Pointer Dereference. [REFERENCES]
- https://wiki.sei.cmu.edu/confluence/display/c/EXP34-C.+Do+not+dereference+null+pointers

(null-library-function-cpp)


[warning] 270-270: The getTargetsOut function returns NULL on error and this line dereferences the return value without checking for NULL.
Context: getTargetsOut()[i]
Note: [CWE-476] NULL Pointer Dereference. [REFERENCES]
- https://wiki.sei.cmu.edu/confluence/display/c/EXP34-C.+Do+not+dereference+null+pointers

(null-library-function-cpp)


[warning] 273-273: The getTargetsIn function returns NULL on error and this line dereferences the return value without checking for NULL.
Context: getTargetsIn()[i]
Note: [CWE-476] NULL Pointer Dereference. [REFERENCES]
- https://wiki.sei.cmu.edu/confluence/display/c/EXP34-C.+Do+not+dereference+null+pointers

(null-library-function-cpp)


[warning] 274-274: The getTargetsOut function returns NULL on error and this line dereferences the return value without checking for NULL.
Context: getTargetsOut()[i]
Note: [CWE-476] NULL Pointer Dereference. [REFERENCES]
- https://wiki.sei.cmu.edu/confluence/display/c/EXP34-C.+Do+not+dereference+null+pointers

(null-library-function-cpp)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp` around lines 258 - 277, The
verify() method in CtrlOp must first validate that the result segments' arities
match the input counts: add checks that getControlsOut().size() ==
getNumControls() and getTargetsOut().size() == getNumTargets() before the
pairwise type loops; if a mismatch, return emitOpError(...) indicating the
controls_out/targets_out arity mismatch. After these guards, keep the existing
pairwise comparisons that compare getControlsIn()[i].getType() vs
getControlsOut()[i].getType() and getTargetsIn()[i].getType() vs
getTargetsOut()[i].getType().

Comment on lines +80 to +92
LogicalResult BarrierOp::verify() {
// Pairwise input->output type equality (disallows e.g. !qco.qubit<static> ->
// !qco.qubit)
const auto numTargets = getNumTargets();
for (size_t i = 0; i < numTargets; ++i) {
if (getQubitsIn()[i].getType() != getQubitsOut()[i].getType()) {
return emitOpError("qco.barrier qubit ")
<< i << " input type must match output type (got "
<< getQubitsIn()[i].getType() << " vs "
<< getQubitsOut()[i].getType() << ")";
}
}
return success();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Verify the output arity before the pairwise type loop.

numTargets is derived from qubits_in, so getQubitsOut()[i] is unsafe when textual IR provides fewer results than inputs, and extra results are silently accepted today. Add a getQubitsOut().size() == getNumTargets() check before iterating.

🛠️ Suggested guard
 LogicalResult BarrierOp::verify() {
   // Pairwise input->output type equality (disallows e.g. !qco.qubit<static> ->
   // !qco.qubit)
   const auto numTargets = getNumTargets();
+  if (getQubitsOut().size() != numTargets) {
+    return emitOpError(
+        "number of output qubits must match number of input qubits");
+  }
   for (size_t i = 0; i < numTargets; ++i) {
     if (getQubitsIn()[i].getType() != getQubitsOut()[i].getType()) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp` around lines
80 - 92, In BarrierOp::verify(), ensure the output arity matches the expected
number before the pairwise loop: compute numTargets via getNumTargets() then
check that getQubitsOut().size() == numTargets (or equivalently
getQubitsOut().size() == getQubitsIn().size()) and emit an error via emitOpError
when it does not; only after this guard iterate over i from 0 to numTargets and
compare getQubitsIn()[i].getType() with getQubitsOut()[i].getType() to preserve
safety when textual IR supplies fewer results than inputs.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp (1)

80-92: ⚠️ Potential issue | 🟠 Major

Add the missing output-arity guard before the pairwise type loop.

numTargets comes from qubits_in, so getQubitsOut()[i] is still unsafe when textual IR provides fewer results, and extra results are accepted silently today. Please reject getQubitsOut().size() != numTargets before indexing the result range.

Suggested guard
 LogicalResult BarrierOp::verify() {
   // Pairwise input->output type equality (disallows e.g. !qco.qubit<static> ->
   // !qco.qubit)
   const auto numTargets = getNumTargets();
+  if (getQubitsOut().size() != numTargets) {
+    return emitOpError(
+        "number of output qubits must match number of input qubits");
+  }
   for (size_t i = 0; i < numTargets; ++i) {
     if (getQubitsIn()[i].getType() != getQubitsOut()[i].getType()) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp` around lines
80 - 92, The verify() implementation in BarrierOp must check that the output
arity matches the input arity before indexing getQubitsOut(); add a guard at the
top of BarrierOp::verify that compares getQubitsOut().size() (or
getNumResults()) to getNumTargets() (which is derived from getQubitsIn()) and
return emitOpError(...) if they differ so you don't index out of bounds, then
proceed with the existing pairwise type loop using getQubitsIn() and
getQubitsOut().
mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp (1)

258-277: ⚠️ Potential issue | 🟠 Major

Validate result segment sizes before indexing controls_out / targets_out.

These loops still derive their bounds from the input segments, so malformed textual IR with too few results will walk past the result ranges, and extra results currently go unnoticed. Guard both output segment sizes first.

Suggested guard
 LogicalResult CtrlOp::verify() {
+  if (getControlsOut().size() != getNumControls()) {
+    return emitOpError(
+        "number of control results must match number of control operands");
+  }
+  if (getTargetsOut().size() != getNumTargets()) {
+    return emitOpError(
+        "number of target results must match number of target operands");
+  }
+
   // Pairwise input->output type equality for controls and targets separately
   // (allows !qco.qubit and !qco.qubit<static> to differ between
   // controls/targets)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp` around lines 258 - 277, The
verify() method should first validate that the result segments for controls and
targets have the expected sizes before indexing: check that
getControlsOut().size() (or the appropriate result segment access) matches
getControlsIn().size() / getNumControls() and that getTargetsOut().size()
matches getTargetsIn().size() / getNumTargets(); if either mismatches, return
emitOpError from CtrlOp::verify (e.g., "mismatched control/target result segment
size") so malformed IR can't index past available results, then continue with
the existing pairwise type-equality loops over getControlsIn()/getControlsOut()
and getTargetsIn()/getTargetsOut().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp`:
- Around line 258-277: The verify() method should first validate that the result
segments for controls and targets have the expected sizes before indexing: check
that getControlsOut().size() (or the appropriate result segment access) matches
getControlsIn().size() / getNumControls() and that getTargetsOut().size()
matches getTargetsIn().size() / getNumTargets(); if either mismatches, return
emitOpError from CtrlOp::verify (e.g., "mismatched control/target result segment
size") so malformed IR can't index past available results, then continue with
the existing pairwise type-equality loops over getControlsIn()/getControlsOut()
and getTargetsIn()/getTargetsOut().

In `@mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp`:
- Around line 80-92: The verify() implementation in BarrierOp must check that
the output arity matches the input arity before indexing getQubitsOut(); add a
guard at the top of BarrierOp::verify that compares getQubitsOut().size() (or
getNumResults()) to getNumTargets() (which is derived from getQubitsIn()) and
return emitOpError(...) if they differ so you don't index out of bounds, then
proceed with the existing pairwise type loop using getQubitsIn() and
getQubitsOut().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1eee49df-392b-4413-83d6-88748239223b

📥 Commits

Reviewing files that changed from the base of the PR and between e0689dc and 8499977.

📒 Files selected for processing (5)
  • mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
  • mlir/lib/Conversion/QCToQCO/QCToQCO.cpp
  • mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp
  • mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp

@mergify mergify bot added the conflict label Mar 19, 2026
Copy link
Member

@burgholzer burgholzer left a comment

Choose a reason for hiding this comment

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

This feels very clean already 🙌🏼 Thanks @simon1hofmann 🙏🏼

Most of the comments are rather on the nitpick side of things, so take them with a grain of salt.
I very much like where this is heading.

After main is merged in here, there will probably be some auto formatting to the Tablegen files.

}

def StaticOp : QCOOp<"static", [Pure]> {
def StaticOp : QCOOp<"static", [MemoryEffects<[MemAlloc]>]> {
Copy link
Member

Choose a reason for hiding this comment

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

Is there a particular reason for why we could not keep Pure here?
Or, asked differently, do we lose anything by not declaring this operation as pure?

Comment on lines +30 to +31
`!qc.qubit<static>` denotes a qubit that has been mapped to a fixed
hardware position.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
`!qc.qubit<static>` denotes a qubit that has been mapped to a fixed
hardware position.
`!qc.qubit<static>` denotes a qubit with a statically known identifier.

I believe it is helpful to keep this a little more generic.

Comment on lines +30 to +31
`!qco.qubit<static>` denotes a qubit that has been mapped to a fixed
hardware position.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
`!qco.qubit<static>` denotes a qubit that has been mapped to a fixed
hardware position.
`!qco.qubit<static>` denotes a qubit with a statically known identifier.

I believe it is helpful to keep this a little more generic.


addConversion([ctx](jeff::QubitType /*type*/) -> Type {
return qco::QubitType::get(ctx);
return qco::QubitType::get(ctx, /*isStatic=*/false);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return qco::QubitType::get(ctx, /*isStatic=*/false);
return qco::QubitType::get(ctx);

Shouldn't this be the default?
I guess it doesn't really hurt to be explicit, but it also does not really feel necessary.

Comment on lines +84 to +91
for (size_t i = 0; i < numTargets; ++i) {
if (getQubitsIn()[i].getType() != getQubitsOut()[i].getType()) {
return emitOpError("qco.barrier qubit ")
<< i << " input type must match output type (got "
<< getQubitsIn()[i].getType() << " vs "
<< getQubitsOut()[i].getType() << ")";
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Haven't checked in the IDE, but there might be a more concise way to write this, either via llvm::zip or via something like getQubitsIn().getTypes() == getQubitsOut().getTypes()
(i vaguely remember that something like that exists)

returnValues.push_back(latest);
escapedQubits.insert(latest);
} else {
returnValues.push_back(adaptorOperand);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
returnValues.push_back(adaptorOperand);
returnValues.emplace_back(adaptorOperand);

}

// Collect non-escaped live qubits for deallocation.
llvm::DenseSet<Value> liveQubitsSet;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
llvm::DenseSet<Value> liveQubitsSet;
llvm::DenseSet<Value> liveQubits;

This is me just being pedantic, but it is clear from the type that this is a set.

Comment on lines +150 to +154
for (const auto& [qcQubit, qcoQubit] : state.qubitMap) {
if (!escapedQubits.contains(qcoQubit)) {
liveQubitsSet.insert(qcoQubit);
}
}
Copy link
Member

Choose a reason for hiding this comment

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

I don't know if it changes much, but LLVM has the concept of intersections of sets, which could be a shorthand for some of the code here. Obviously not a must-have, but might create cleaner code overall.

Comment on lines +159 to +165
llvm::sort(liveQubits, [](Value a, Value b) {
auto* opA = a.getDefiningOp();
auto* opB = b.getDefiningOp();
if (!opA || !opB || opA->getBlock() != opB->getBlock()) {
return a.getAsOpaquePointer() < b.getAsOpaquePointer();
}
return opA->isBeforeInBlock(opB);
Copy link
Member

Choose a reason for hiding this comment

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

Since this is also used in the QCO program builder, it might be nice to extract this into the utils file so that it can be reused and doesn't go out of sync over time.

Comment on lines +314 to +315
auto qcoOp = qco::StaticOp::create(rewriter, op.getLoc(),
static_cast<int64_t>(op.getIndex()));
Copy link
Member

Choose a reason for hiding this comment

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

See also my other comment, I was also confused by this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

conflict enhancement Improvement of existing feature MLIR Anything related to MLIR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants