Skip to content

✨ Add pass and patterns for measurement lifting#1705

Draft
DRovara wants to merge 23 commits into
mainfrom
mlir/measurement-lifting
Draft

✨ Add pass and patterns for measurement lifting#1705
DRovara wants to merge 23 commits into
mainfrom
mlir/measurement-lifting

Conversation

@DRovara
Copy link
Copy Markdown
Collaborator

@DRovara DRovara commented May 13, 2026

Description

Part 1 out of 3 of the Qubit Reuse Implementation

This PR ports the previous implementation of the Measurement Lifting pass to the new dialect infrastructure.
Measurements can be lifted over:

  • Arbitrary controls of controlled gates
  • Diagonal gates such as Z, Rx, etc.
  • Anti-diagonal gates, provided the measurement outcome is then inverted.

This is one of 3 PRs that will, in total, constitute qubit reuse:

  1. Measurement Lifitng
  2. Replacing Classical Controls
  3. Reuse Qubits

Requires #1755 to be merged first.

AI Notice: Gemini 3.1 and Claude Sonnet 4.6 were used for the generation of some code.

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.

@DRovara DRovara self-assigned this May 13, 2026
@DRovara DRovara added feature New feature or request c++ Anything related to C++ code MLIR Anything related to MLIR labels May 13, 2026
@DRovara DRovara added this to the MLIR Support milestone May 13, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 13, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@DRovara
Copy link
Copy Markdown
Collaborator Author

DRovara commented Jun 2, 2026

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 2, 2026

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds two quantum circuit optimization passes to the QCO dialect: dead gate elimination removes quantum operations whose outputs are only consumed by sink operations, while measurement lifting moves measurements above certain gate patterns. The implementation includes trait/memory-effect updates marking operations as Pure and memory-effect-free, a dead-gate checking utility, canonicalization patterns, a measurement-lifting transformation pass with three rewrite strategies, program builder extensions, and comprehensive unit test coverage for both optimizations.

Changes

Dead Gate Elimination and Measurement Lifting Optimization

Layer / File(s) Summary
Semantic Updates: Pure Traits and Memory-Effect Removal
mlir/include/mlir/Dialect/QCO/IR/QCODialect.td, mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
Operations across measurement, reset, unitary gates, barrier, and control/modifiers are updated to declare Pure trait and remove MemRead memory-effect annotations from qubit operands, establishing side-effect-free semantics for canonicalization.
Dead Gate Elimination Infrastructure
mlir/include/mlir/Dialect/QCO/QCOUtils.h
checkAndRemoveDeadGate utility detects operations whose only users are SinkOp instances and rewrites them by replacing produced values with operand inputs, preserving qubit flow while discarding unused outputs.
Canonicalization Patterns for Dead Gate Elimination
mlir/lib/Dialect/QCO/IR/QCOOps.cpp, mlir/lib/Dialect/QCO/IR/Operations/MeasureOp.cpp, mlir/lib/Dialect/QCO/IR/Operations/ResetOp.cpp, mlir/lib/Dialect/QCO/IR/SCF/IfOp.cpp
Canonicalization rewrite patterns (DeadGateElimination, DeadMeasurementRemoval, DeadResetRemoval, DeadIfRemoval) use the dead-gate utility at dialect and operation levels; dialect enables canonicalizer hook via hasCanonicalizer = 1.
Measurement Lifting Pass Declaration and Infrastructure
mlir/include/mlir/Dialect/QCO/Transforms/Passes.td
Declares MeasurementLifting ModuleOp pass that commutes measurements above (controlled) unitary and diagonal gates, with Pauli-X measurement-outcome negation via boolean inversion.
Measurement Lifting Pass Implementation
mlir/lib/Dialect/QCO/Transforms/Optimizations/MeasurementLifting.cpp
Implements three rewrite patterns: lift above diagonal gates, lift above inverting gates (X/Y) with boolean XOrIOp outcome inversion, and lift above controls when qubit is not a gate target; includes gate classification predicates and swap-with-measurement utility.
Program Builder Extensions
mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h, mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp
Adds boolConstant(bool) method to create boolean constants, and refactors finalize() to accept optional exitCode Value parameter for caller-provided program exit codes.
Dead Gate Elimination Tests
mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp
Tests CheckDeadGateElimination and CheckIfOpDeadGateElimination verify elimination of dead unitary and if operations via canonicalization with reference IR equivalence checking.
Measurement Lifting Tests
mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt, mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_measurement_lifting.cpp
Comprehensive tests covering measurement lifting over positive/multiple controls, parametrized gates, diagonal gates (X/Y/phase), and control combinations; validates transformation via module equivalence with permutation support.
Changelog Documentation
CHANGELOG.md
Documents the addition of dead gate elimination pattern with PR reference.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • munich-quantum-toolkit/core#1506: The main PR adds an IfOp canonicalization rewrite (DeadIfRemoval) in mlir/lib/Dialect/QCO/IR/SCF/IfOp.cpp that reuses checkAndRemoveDeadGate, which directly builds on the qco.if/IfOp implementation introduced by PR #1506.
  • munich-quantum-toolkit/core#1569: The main PR's dead-gate elimination logic removes operations whose only users are qco.sink (SinkOp), while the retrieved PR redefines/renames the qubit "freeing" op to qco.sink and updates its canonicalization/consumption semantics—so the SinkOp users that the elimination patterns key off of are changed.

Suggested reviewers

  • burgholzer
  • denialhaag

Poem

🐰 Measurement lifting high, dead gates say goodbye,
Pure traits and sinks combine to optimize the sky,
Quantum circuits lean and clean, canonicalized and bright,
With boolean inversions dancing in the compiler light!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding a measurement lifting pass and patterns to the QCO dialect, which is the primary objective of this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description includes a clear summary of the change, mentions it is Part 1 of a multi-part feature, identifies dependencies, discloses AI tool usage, and confirms AI-assisted commits have appropriate footers.

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

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch mlir/measurement-lifting

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
Copy Markdown
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: 11

Caution

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

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

99-139: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don’t model recorded qco.measure as unconditionally Pure; it can be erased while still writing to classical registers.

MeasureOp is defined with [Pure], and canonicalization can drop it when its qubit result is only used by qco.sink (via checkAndRemoveDeadGate, which replaces the qubit output with the input and erases the op, explicitly ignoring the classical outcome). When any register_* attribute is present, the QCToQIR lowering allocates a result array and passes a result-pointer into __quantum__qis__mz__body, so the recorded measurement produces observable classical output even if the SSA i1 result is unused. Marking the op Pure therefore makes those register writes incorrectly erasable.

Remove Pure for recorded measurements (or otherwise make the operation report a write effect whenever register_name/register_size/register_index are present), while keeping it effect-free only for the non-recording case.

mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp (1)

1108-1160: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject non-i64 exit codes in the new overload.

initialize() hard-codes main to return i64, but finalize(Value exitCode) accepts any Value. Passing an i1 here will build a func.return with the wrong type and leave invalid IR; the new measurement-lifting tests already have to call i1ToI64(...) before using this overload for exactly that reason (mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_measurement_lifting.cpp:272-294). Add an early type check and document the i64 requirement in the header.

Suggested guard
 OwningOpRef<ModuleOp> QCOProgramBuilder::finalize(Value exitCode) {
   checkFinalized();
+  if (exitCode.getType() != getI64Type()) {
+    llvm::reportFatalUsageError("Exit code must have type i64");
+  }
 
   // Ensure that main function exists and insertion point is valid
   auto* insertionBlock = getInsertionBlock();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp` around lines 1108 - 1160,
The finalize(Value exitCode) overload accepts any Value but main() is hard-coded
to return i64, so add an early runtime/type check in QCOProgramBuilder::finalize
to reject non-i64 exitCode Values (e.g., verify exitCode.getType() is an
IntegerType of width 64 and call llvm::reportFatalUsageError or similar if not)
before creating func::ReturnOp; also update the QCOProgramBuilder::finalize
declaration comment in the header to document the required i64 exit code and
mirror the behavior of initialize() which sets main's return type to i64.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Line 14: In CHANGELOG.md fix the broken contributor reference by replacing the
incorrect label "[**DRovara**]" with the correct label "[**`@DRovara`**]" so it
matches the existing contributor reference definitions; update the line
containing "✨ Add Dead Gate Elimination Pattern ([`#1755`]) ([**DRovara**])" to
use "[**`@DRovara`**]" instead.

In `@mlir/include/mlir/Dialect/QCO/QCOUtils.h`:
- Around line 258-263: The MeasureOp removal currently replaces uses with
m.getQubitIn() and erases the op unconditionally; update the branch handling
MeasureOp so it first checks the measurement's classical-recording fields (e.g.,
register_name, register_size, register_index) and only perform
rewriter.replaceAllUsesWith(m.getQubitOut(), m.getQubitIn()) followed by
rewriter.eraseOp(op) when those fields indicate the measurement is not recorded
(empty/default). If any of register_name/register_size/register_index show the
measurement is recorded, skip the erase (return failure/skip) to preserve the
classical output.

In `@mlir/include/mlir/Dialect/QCO/Transforms/Passes.td`:
- Around line 181-183: Fix the typos in the MLIR pass summary string: update the
TD variable 'summary' (let summary = ...) to replace "shiftling" with "shifting"
and ensure there's a space between "as" and "possible" so the concatenated
strings read "as far up as possible, shifting them above...". Edit the two
adjacent string literals so they join correctly and contain the corrected word.
- Around line 184-188: The description string assigned to let description in
mlir/include/mlir/Dialect/QCO/Transforms/Passes.td contains a copy-paste error
("lifts measurements gates away from the measurements") likely from
HadamardLifting; update the description to accurately describe this pass's
behavior (measurement lifting as part of qubit reuse: moving/earlier placement
of measurement operations to enable qubit reuse and gate removal), editing the
let description block for the relevant pass (compare with the HadamardLifting
description to avoid repeating its phrasing) so the text clearly states that
measurement lifting moves measurements earlier to free qubits for reuse and
optimize/remove subsequent quantum gates.

In `@mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp`:
- Around line 1103-1105: The no-arg QCOProgramBuilder::finalize() currently
calls intConstant(0) before running the validation that finalize(Value)
performs, which can create ops or mutate the wrong block if the builder wasn't
initialized or the insertion point isn't main's entry; change
QCOProgramBuilder::finalize() to first validate that 'main' exists and that the
current insertion point is inside main's entry block (reusing the same checks
used by finalize(Value)), and only after those checks succeed call
intConstant(0) and forward to finalize(Value); reference the methods
QCOProgramBuilder::finalize(), QCOProgramBuilder::finalize(Value), and the
helper intConstant to locate where to reorder the work.

In `@mlir/lib/Dialect/QCO/Transforms/Optimizations/MeasurementLifting.cpp`:
- Line 44: The free helper functions are currently relying on
anonymous-namespace linkage but should be declared with static linkage per
project convention; change the declarations of the free functions isInverting,
isDiagonal, and swapGateWithMeasurement to be marked static (e.g., static bool
isInverting(...), static bool isDiagonal(...), static Operation*
swapGateWithMeasurement(...)) so they use internal linkage instead of an
anonymous namespace, leaving their bodies and call sites unchanged.
- Line 29: The file includes the unused header <numbers> which should be
removed; edit MeasurementLifting.cpp to delete the line '`#include` <numbers>'
(ensure no other uses of std::numbers remain in functions such as any helpers or
transformations in MeasurementLifting-related functions) so the build has no
superfluous include.
- Line 51: The isDiagonal predicate currently checks for ZOp, SOp, TOp, POp,
RZOp but omits diagonal gate types used in tests; update the isDiagonal function
to also recognize SdgOp, TdgOp, and IdOp so measurements can be lifted over id,
sdg, and tdg gates (i.e., include SdgOp, TdgOp, IdOp alongside the existing
isa<ZOp, SOp, TOp, POp, RZOp> check in the isDiagonal helper).

In `@mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp`:
- Around line 117-223: Add a new unit test (e.g., in QCOTest alongside
CheckIfOpDeadGateElimination) that creates a measurement with a
recorded/classical name (call builder.measure with the recorded-name overload
like measure("c", 1, 0) or the API that records the result), uses those qubit
outputs and sinks them (so both qubits are later sunk), and constructs a
reference module where the measurement is preserved; then run
verify/runQCOCleanupPipeline on both and assert
areModulesEquivalentWithPermutations(module, refModule). This ensures the
cleanup pass does not eliminate measurements whose classical result is
externally recorded; reference the existing QCOProgramBuilder usage,
builder.measure, builder.qcoIf, and areModulesEquivalentWithPermutations to
locate where to add the test.

In
`@mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_measurement_lifting.cpp`:
- Around line 318-342: The isDiagonal predicate in MeasurementLifting.cpp
currently checks only ZOp, SOp, TOp, POp, RZOp, so id, sdg and tdg gates are not
treated as diagonal; update the isDiagonal implementation to also return true
for the identity, sdg and tdg ops (e.g., IdOp, SdgOp, TdgOp or whatever the MLIR
op class names are in your codebase) so MeasurementLifting::lift (and related
code using isDiagonal) will permit lifting measurements over id, sdg and tdg
gates.
- Around line 296-316: The reference test liftMeasurementOverSingleY creates
xorOp but never uses its result, so canonicalization can drop the dead XOR;
update the referenceBuilder finalization to include xorOp's result (e.g.,
convert the xor i1 to i64 like i1ToI64 or otherwise pass it into
referenceBuilder.finalize()) so the XOR is live, and likewise ensure the
programBuilder path passes its measurement result to finalize() so the test
actually verifies the inversion inserted by runMeasurementLiftingPass; modify
usages around xorOp, referenceBuilder.finalize(), and programBuilder.finalize()
accordingly.

---

Outside diff comments:
In `@mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp`:
- Around line 1108-1160: The finalize(Value exitCode) overload accepts any Value
but main() is hard-coded to return i64, so add an early runtime/type check in
QCOProgramBuilder::finalize to reject non-i64 exitCode Values (e.g., verify
exitCode.getType() is an IntegerType of width 64 and call
llvm::reportFatalUsageError or similar if not) before creating func::ReturnOp;
also update the QCOProgramBuilder::finalize declaration comment in the header to
document the required i64 exit code and mirror the behavior of initialize()
which sets main's return type to i64.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 285fd211-de81-448a-8fe6-648bb7642bb9

📥 Commits

Reviewing files that changed from the base of the PR and between e868fe9 and 0edfeb1.

📒 Files selected for processing (15)
  • CHANGELOG.md
  • mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h
  • mlir/include/mlir/Dialect/QCO/IR/QCODialect.td
  • mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
  • mlir/include/mlir/Dialect/QCO/QCOUtils.h
  • mlir/include/mlir/Dialect/QCO/Transforms/Passes.td
  • mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp
  • mlir/lib/Dialect/QCO/IR/Operations/MeasureOp.cpp
  • mlir/lib/Dialect/QCO/IR/Operations/ResetOp.cpp
  • mlir/lib/Dialect/QCO/IR/QCOOps.cpp
  • mlir/lib/Dialect/QCO/IR/SCF/IfOp.cpp
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/MeasurementLifting.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_measurement_lifting.cpp

Comment thread CHANGELOG.md Outdated
Comment thread mlir/include/mlir/Dialect/QCO/QCOUtils.h
Comment thread mlir/include/mlir/Dialect/QCO/Transforms/Passes.td Outdated
Comment thread mlir/include/mlir/Dialect/QCO/Transforms/Passes.td
Comment thread mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp Outdated
Comment thread mlir/lib/Dialect/QCO/Transforms/Optimizations/MeasurementLifting.cpp Outdated
Comment thread mlir/lib/Dialect/QCO/Transforms/Optimizations/MeasurementLifting.cpp Outdated
Comment thread mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Anything related to C++ code feature New feature or request MLIR Anything related to MLIR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant