Skip to content

✨ Add Dead Gate Elimination Pattern#1755

Open
DRovara wants to merge 91 commits into
mainfrom
mlir/dead-gate-elimination
Open

✨ Add Dead Gate Elimination Pattern#1755
DRovara wants to merge 91 commits into
mainfrom
mlir/dead-gate-elimination

Conversation

@DRovara

@DRovara DRovara commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR implements Dead Gate Elimination for unused operations on qubits.

An operation is considered unused if all of its outputs' users are dealloc operations.
This means a measurement can also be dead if its classical outcome is not used.
Similarly, reset and if operations can also be considered unused.

This PR marks all quantum gates as Pure (and marks the RecursiveMemoryEffects operations as Pure as well, as otherwise, any recursive memory effects would be overwritten by the compiler not knowing whether these region operations are pure to begin with). This allows us to simply check for memory effects before removing region operations.

Currently, this only considers individual qubits, rather than qtensor cases to keep this PR contained.
An extension to qtensors should also be possible, though it may require some additional assumptions to maintain overview on what specific qubits end up going unused.


The following new features/changes also had to be implemented to make this PR work with the rest of the framework:

Return values for builders and tests

Without return values, most quantum programs constructed by the builder would basically be "dead". Therefore, the QC and QCO ProgramBuilders now require all build functions to also indicate what values and types they return. The initialize(...) method now takes a function signature for the main function as an argument, while the finalize(...) method takes the actual return values. Similarly, the new retype(...) changes the type of the main function during the build process.

Translations between IRs:

As QC and QCO programs now have return values, other IRs also need some notion to represent that concept and ways to translate between them. We have adapted the conversion/translation methods so that:

  • QIR (Base and Adaptive) now uses the record QIR methods to indicate the output of measurement outcomes. (During translation from QC to QIR, return values have to be stripped out if they are direct measurement outcomes)
  • QuantumComputation now considers all measured qubits to be returned.
  • OpenQASM 2 also considers all measured qubits to be returned.
  • OpenQASM 3 only considers bit values marked with the output keyword to be returned.
  • Jeff supports return values, so the previous code that stripped away all returns during conversion to jeff was simply removed.

OpenQASM Parsing:

Consequently, handling for the output keyword was added to the OpenQASM parser code. Similarly, the translation from OpenQASM to QC was adapted. Note that the output keyword is ignored when parsing into QuantumComputation objects, rather than into the QC dialect.

QuantumComputation

For convenience (to keep the tests minimal), the measureAll method in the QuantumComputation was adapted to also take the addBarrier argument. If it is not set to true, barriers will no longer be inserted between the main circuit body and the measurements.

QIR Builder

To no longer make the QIRProgramBuilder consider all measurement outcomes for reporting, the measure method now also takes the record flag as an argument. Outcomes will only be reported if the flag is set to true.

Other

Various further helper methods have been added to make working with more complex programs easier.


AI Usage note: Some design decisions were taken after discussions with Gemini 3.1 Pro. Parts of the code were written with help from Gemini 3.1 Pro and Claude Opus 4.6.

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 commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds isOutput tracking to QASM3 declarations and an addBarrier flag to measureAll, marks QCO operations Pure with canonicalization enabled, reworks QC/QCO/QIR program builders to support explicit return types/values, adds selective QIR measurement output recording, introduces a dead-gate elimination canonicalization pattern, adds IfOp qubit input/output mapping helpers, and updates the entire test/program-fixture suite accordingly.

Changes

QASM3 Outputs, measureAll Barrier, and QCO Op Traits

Layer / File(s) Summary
Parser/statement output tracking
include/mqt-core/qasm3/Parser.hpp, include/mqt-core/qasm3/Statement.hpp, src/qasm3/Parser.cpp
parseDeclaration and DeclarationStatement gain an isOutput flag wired through parseStatement.
measureAll barrier flag
include/mqt-core/ir/QuantumComputation.hpp, src/ir/QuantumComputation.cpp, bindings/ir/register_quantum_computation.cpp, python/mqt/core/ir/__init__.pyi
measureAll gains an addBarrier parameter (default true), exposed through the C++ API, Python binding, and stub.
QCO op Pure traits & canonicalizers
mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
Numerous QCO operations gain the Pure trait, drop [MemRead] on qubit operands, and enable canonicalization.

Estimated code review effort: 4 (Complex) | ~75 minutes

Program Builder Return-Value API and Measurement Recording

Layer / File(s) Summary
QC/QCO/QIR builder APIs
mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h, mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp, mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h, mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp, mlir/include/mlir/Dialect/QIR/Builder/QIRProgramBuilder.h, mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp
initialize/retype/finalize/build are extended to support explicit return types/values, and QIR measurement overloads gain a record flag with internal tracking sets.
Translation call sites
mlir/lib/Dialect/QC/Translation/TranslateQASM3ToQC.cpp, mlir/lib/Dialect/QC/Translation/TranslateQuantumComputationToQC.cpp, mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp
Translations compute return types from output registers/classical bits and finalize using collected results.
QC-to-QIR measurement recording
mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h, mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp, mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp, mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp
Adds stripReturnedMeasurements and gates output recording so only returned measurements are recorded.
Dead gate elimination
mlir/lib/Dialect/QCO/IR/QubitManagement/SinkOp.cpp
A new DeadGateElimination canonicalization pattern removes gate chains only used by SinkOp.
IfOp qubit mapping helpers
mlir/lib/Dialect/QCO/IR/SCF/IfOp.cpp
Adds getInputForOutput/getOutputForInput for index-based qubit-result mapping.
Test infrastructure & fixtures
mlir/unittests/TestCaseUtils.h, mlir/unittests/programs/*, mlir/unittests/**/test_*.cpp
NamedBuilder is generalized, program fixtures return measured values/types, and test harnesses/instantiations are updated accordingly.

Estimated code review effort: 5 (Critical) | ~150 minutes

Sequence Diagram(s)

sequenceDiagram
  participant QCProgramBuilder
  participant QCToQIRBase
  participant QIRCommon
  participant stripReturnedMeasurements

  QCProgramBuilder->>QCProgramBuilder: build(callback) -> (values, types)
  QCProgramBuilder->>QCProgramBuilder: retype(types)
  QCProgramBuilder->>QCProgramBuilder: finalize(values)
  QCToQIRBase->>stripReturnedMeasurements: strip qc.measure operands from func.return
  stripReturnedMeasurements->>QCToQIRBase: state.returnedMeasurements updated
  QCToQIRBase->>QIRCommon: lower qc.measure with shouldRecord flag
  QIRCommon->>QIRCommon: addOutputRecording (only for recorded indices/arrays)
Loading

Possibly related PRs

Suggested labels: feature, c++, MLIR

Suggested reviewers: burgholzer

Poem

A rabbit hopped through gates so Pure,
Measured bits it would ensure,
Barriers raised, outputs declared,
SinkOp's dead gates gently spared,
Builders now return with grace —
Hop hop, review this whole codebase! 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.40% 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
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.
Title check ✅ Passed The title clearly matches the main change: adding a dead gate elimination pattern.
Description check ✅ Passed The description includes a change summary, motivation, context, checklist, and AI usage disclosure, with only the optional issue line left unspecified.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch mlir/dead-gate-elimination

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 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 `@mlir/include/mlir/Dialect/QCO/QCOUtils.h`:
- Line 252: Update the comment to use the dialect's terminology: replace
"deallocs" with "sinks" or "SinkOp" so it matches the check against SinkOp in
the code; e.g., modify the comment above the removal check to read something
like "If the operation is only used by sinks (SinkOp), we can safely remove it."
Ensure the comment near the function that inspects SinkOp usage (referencing
SinkOp) is updated for consistency.

In `@mlir/lib/Dialect/QCO/IR/QCOOps.cpp`:
- Around line 38-54: Add a Doxygen-style comment block for the
DeadGateElimination struct describing its purpose and behavior; place a brief
/** ... */ above the struct declaration for DeadGateElimination, mention that it
is an OpInterfaceRewritePattern for UnitaryOpInterface, summarize that
matchAndRewrite skips ops with no uses (while excluding GPhase/inverse) and
delegates removal to checkAndRemoveDeadGate, and keep the description concise
per repository guidelines.

In `@mlir/lib/Dialect/QCO/IR/SCF/IfOp.cpp`:
- Around line 239-241: Update the Doxygen summary for the DeadIfRemoval pass to
accurately describe its purpose: replace the misleading "Remove dead resets"
summary with a concise Doxygen-style line such as "Canonicalize and remove dead
IfOp patterns" (or "Remove dead IfOps / canonicalize IfOp patterns") so that the
comment above the DeadIfRemoval class/function reflects that it canonicalizes
IfOps; ensure the comment uses proper Doxygen syntax and is placed directly
above the DeadIfRemoval symbol.
- Around line 245-247: Guard the removal of IfOp in matchAndRewrite by ensuring
branch regions contain no side-effecting operations before calling
checkAndRemoveDeadGate; specifically, before erasing op in matchAndRewrite (or
inside checkAndRemoveDeadGate) inspect op.thenRegion() and op.elseRegion() (or
the region bodies) and bail out if any contained Operation
mayHaveSideEffects()/hasTrait<OpTrait::HasSideEffects>() or reports
MemoryEffects—only allow deletion when both regions are empty or provably
side-effect-free. Ensure you reference IfOp, matchAndRewrite,
checkAndRemoveDeadGate, thenRegion/elseRegion and use the Operation side-effect
query APIs to make the decision.

In `@mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp`:
- Around line 117-148: Extend the test to also exercise dead-gate elimination
for measurement and conditional ops by adding a measurement (MeasureOp) whose
result is unused and an IfOp that produces an output which is never consumed,
then sinking those unused outputs and ensuring the cleaned module matches a
reference without those ops; specifically, in the TEST_F(QCOTest,
CheckDeadGateElimination) case, use QCOProgramBuilder methods that create a
measurement (e.g., builder.measure or MeasureOp via builder) and an
if/conditional op (e.g., builder.ifOp/IfOp) producing qubits/bits, do not
consume their results (call builder.sink on those outputs), finalize module and
create a reference program that omits the measurement and if op, then run
verify, runQCOCleanupPipeline, verify again and assert
areModulesEquivalentWithPermutations(module.get(), ref.get()) to validate the
dead-gate paths for MeasureOp and IfOp.
🪄 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: 0906695c-971e-4416-bb67-64f8691df3f6

📥 Commits

Reviewing files that changed from the base of the PR and between e868fe9 and 4243ac0.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • mlir/include/mlir/Dialect/QCO/IR/QCODialect.td
  • mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
  • mlir/include/mlir/Dialect/QCO/QCOUtils.h
  • 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/unittests/Dialect/QCO/IR/test_qco_ir.cpp

Comment thread mlir/include/mlir/Dialect/QCO/QCOUtils.h Outdated
Comment thread mlir/lib/Dialect/QCO/IR/QCOOps.cpp Outdated
Comment thread mlir/lib/Dialect/QCO/IR/SCF/IfOp.cpp Outdated
Comment thread mlir/lib/Dialect/QCO/IR/SCF/IfOp.cpp Outdated
Comment thread mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp Outdated
@burgholzer burgholzer mentioned this pull request Jun 1, 2026
1 task
@DRovara

DRovara commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor
✅ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

44-46: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix the Doxygen summary for DeadGateElimination.

The comment says "Remove dead measurements" but this pattern operates on UnitaryOpInterface, not MeasureOp. The description should match the pattern's scope.

✏️ Suggested fix
 /**
- * `@brief` Remove dead measurements.
+ * `@brief` Remove dead unitary operations (gates).
  */
🤖 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/IR/QCOOps.cpp` around lines 44 - 46, Update the Doxygen
summary for DeadGateElimination to accurately describe its scope: replace
"Remove dead measurements" with a brief line stating that DeadGateElimination
removes dead gates (or eliminates operations) on UnitaryOpInterface instances
rather than referring to MeasureOp; ensure the summary references
DeadGateElimination and UnitaryOpInterface (and remove or avoid mentioning
MeasureOp) so the comment matches the pattern's actual target.
mlir/lib/Dialect/QCO/IR/SCF/IfOp.cpp (1)

245-248: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Guard dead IfOp elimination against side effects inside the branches.

IfOp has RecursiveMemoryEffects, so its memory effects are derived from child operations. Without checking isMemoryEffectFree(op), this pattern can incorrectly remove an IfOp whose branches contain side-effecting operations (e.g., GPhaseOp with MemWrite).

The analogous DeadGateElimination pattern in QCOOps.cpp correctly guards with !isMemoryEffectFree(op).

🐛 Proposed fix to add memory-effect guard
 struct DeadIfRemoval final : OpRewritePattern<IfOp> {
   using OpRewritePattern::OpRewritePattern;

   LogicalResult matchAndRewrite(IfOp op,
                                 PatternRewriter& rewriter) const override {
+    if (!isMemoryEffectFree(op)) {
+      return failure();
+    }
     return checkAndRemoveDeadGate(op, rewriter);
   }
 };
🤖 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/IR/SCF/IfOp.cpp` around lines 245 - 248, The
matchAndRewrite implementation for IfOp currently always calls
checkAndRemoveDeadGate(op, rewriter) and can remove IfOp with side-effecting
branch ops; update the pattern to first check isMemoryEffectFree(op) (like
DeadGateElimination in QCOOps.cpp) and only call checkAndRemoveDeadGate when the
check passes (i.e., skip removal if !isMemoryEffectFree(op)). Ensure you
reference the IfOp instance passed into matchAndRewrite and use the existing
isMemoryEffectFree utility to guard the removal.
🤖 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 `@mlir/include/mlir/Dialect/QCO/QCOUtils.h`:
- Around line 250-251: Replace the use of std::all_of with llvm::all_of for the
users check: change the call that currently reads
std::all_of(op->getUsers().begin(), op->getUsers().end(), [](Operation* user){
return isa<SinkOp>(user); }) to use llvm::all_of with the same range and lambda
(i.e., llvm::all_of(op->getUsers(), [](Operation *user) { return
isa<SinkOp>(user); }) or llvm::all_of(op->getUsers().begin(),
op->getUsers().end(), ...)); keep the op->getUsers() range and the isa<SinkOp>
predicate intact and ensure the appropriate LLVM header is available.

---

Duplicate comments:
In `@mlir/lib/Dialect/QCO/IR/QCOOps.cpp`:
- Around line 44-46: Update the Doxygen summary for DeadGateElimination to
accurately describe its scope: replace "Remove dead measurements" with a brief
line stating that DeadGateElimination removes dead gates (or eliminates
operations) on UnitaryOpInterface instances rather than referring to MeasureOp;
ensure the summary references DeadGateElimination and UnitaryOpInterface (and
remove or avoid mentioning MeasureOp) so the comment matches the pattern's
actual target.

In `@mlir/lib/Dialect/QCO/IR/SCF/IfOp.cpp`:
- Around line 245-248: The matchAndRewrite implementation for IfOp currently
always calls checkAndRemoveDeadGate(op, rewriter) and can remove IfOp with
side-effecting branch ops; update the pattern to first check
isMemoryEffectFree(op) (like DeadGateElimination in QCOOps.cpp) and only call
checkAndRemoveDeadGate when the check passes (i.e., skip removal if
!isMemoryEffectFree(op)). Ensure you reference the IfOp instance passed into
matchAndRewrite and use the existing isMemoryEffectFree utility to guard the
removal.
🪄 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: bcefdaa8-d6db-49c7-8ec3-d1552d3f463a

📥 Commits

Reviewing files that changed from the base of the PR and between 4243ac0 and 9717547.

📒 Files selected for processing (6)
  • mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
  • mlir/include/mlir/Dialect/QCO/QCOUtils.h
  • mlir/lib/Dialect/QCO/IR/Operations/MeasureOp.cpp
  • mlir/lib/Dialect/QCO/IR/QCOOps.cpp
  • mlir/lib/Dialect/QCO/IR/SCF/IfOp.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp

Comment thread mlir/include/mlir/Dialect/QCO/QCOUtils.h Outdated
@DRovara DRovara marked this pull request as ready for review June 2, 2026 08:14
@DRovara

DRovara commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator Author

@burgholzer the implementation itself should be done now, but the tests will still fail until we fixed the ordering issue in the tester that we discussed internally. I guess this PR can either be reviewed now already or once that fix has been merged in here.

@burgholzer

Copy link
Copy Markdown
Member

@burgholzer the implementation itself should be done now, but the tests will still fail until we fixed the ordering issue in the tester that we discussed internally. I guess this PR can either be reviewed now already or once that fix has been merged in here.

Thanks! I'll try to review this asap. Let's see if that turns out to be before or after the fix.

@DRovara DRovara self-assigned this Jun 2, 2026
@DRovara DRovara added feature New feature or request c++ Anything related to C++ code MLIR Anything related to MLIR labels Jun 2, 2026
@DRovara DRovara added this to the MLIR Support milestone Jun 2, 2026
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

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

⚠️ Outside diff range comments (5)
mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h (1)

408-418: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the stale measure(qubit, bit) example.

The overload now returns {output_qubit, measurement_result}, but the example still assigns the pair to a single Value.

📝 Proposed doc fix
-   * q0 = builder.measure(q0, c[0]);
+   * auto [q0_out, result] = builder.measure(q0, c[0]);
🤖 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/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h` around lines 408 -
418, Update the stale documentation for QCOProgramBuilder::measure(Value qubit,
const Bit& bit) so the example matches the current return type; the method now
returns a pair of output qubit and measurement result, so fix the C++ and MLIR
examples to show destructuring or separate bindings instead of assigning to a
single Value. Keep the wording aligned with the measure overload signature and
its documented pair return.
mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp (2)

358-412: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Avoid duplicate measurement side effects for cached result slots.

Both overloads reuse cached result pointers but still emit a fresh __quantum__qis__mz__body call for the same classical slot. Repeated measurement into the same bit should return the cached result without another measurement side effect.

Add a measurement-side-effect cache separate from resultPtrs/loadedResults, because those maps can contain allocated-but-not-yet-measured pointers. Based on learnings, “ensure a cache check prevents duplicate measurements … return early to avoid generating multiple mz calls for the same classical bit index.” <retrieved_learnings>

🤖 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/QIR/Builder/QIRProgramBuilder.cpp` around lines 358 - 412,
The measure overloads in QIRProgramBuilder::measure still emit a new
__quantum__qis__mz__body call even when the same classical slot has already been
measured; add a dedicated measurement-result cache keyed by the classical result
index/Bit and check it before creating the call. Update both measure(Value,
int64_t, bool) and measure(Value, const Bit&, bool) so they return the
previously recorded result early when the slot was already measured, without
reusing resultPtrs/loadedResults as the only guard.

Source: Learnings


1014-1062: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Do not release result storage before returning a result pointer.

build() can now pass the callback result directly to finalize(result). If that result is produced by measure(), adaptive finalization releases all resultPtrs/resultArrays before the terminator, so the function returns a released runtime result pointer. Either return an owned scalar value, or skip releasing result storage that is returned from main.

Also applies to: 1078-1087

🤖 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/QIR/Builder/QIRProgramBuilder.cpp` around lines 1014 - 1062,
`QIRProgramBuilder::finalize(Value returnValue)` is releasing adaptive result
storage before the function returns, which can invalidate a runtime result
pointer produced by `measure()`. Update the adaptive cleanup in `finalize` so
that any `resultPtrs` or `resultArrays` that may be returned from `main` are not
freed before the `LLVM::ReturnOp` executes, or convert the callback path to
return an owned scalar instead of a raw runtime pointer. Use the `finalize`,
`generateOutputRecording`, and `resultPtrs`/`resultArrays` logic to ensure
returned values remain valid through function exit.
mlir/include/mlir/Dialect/QIR/Builder/QIRProgramBuilder.h (1)

313-345: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align the docs with the new conditional recording and general return-value semantics.

These comments still say measurements are recorded unconditionally and describe returnValue as an exit code, but the API now supports record = false and arbitrary return values.

Proposed documentation cleanup
-   * The output is recorded via `__quantum__rt__result_record_output` during
-   * `finalize()`.
+   * When `record` is true, the output is recorded via
+   * `__quantum__rt__result_record_output` during `finalize()`.

-   * The output is recorded via `__quantum__rt__result_array_record_output`
-   * during `finalize()`.
+   * When `record` is true, the output is recorded via
+   * `__quantum__rt__result_array_record_output` during `finalize()`.

-   * `@brief` Finalize the program with a given exit code and return the
+   * `@brief` Finalize the program with a given return value and return the
...
-   * `@param` returnValue The value representing the exit code to return
+   * `@param` returnValue The value returned by the main function
...
-   * adds a return statement with a given exit code,
+   * adds a return statement with the given return value,

Also applies to: 347-382, 1100-1117

🤖 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/include/mlir/Dialect/QIR/Builder/QIRProgramBuilder.h` around lines 313 -
345, Update the documentation for QIRProgramBuilder methods like measure and the
return-value handling APIs to match the current behavior: describe that
measurement recording is conditional via the record parameter (including the
false case) rather than always happening, and replace any language that treats
returnValue as an exit code with wording that it can carry arbitrary return
values. Keep the comments/examples in sync across the affected declarations so
the semantics described in the docs match the implementation.
src/qasm3/Parser.cpp (1)

169-189: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

output is accepted on quantum declarations (qubit/qreg), which is invalid per OpenQASM 3.

The output branch calls parseDeclaration(false, true) without restricting the following type. Since parseType() accepts Qubit/Qreg, output qubit q; parses successfully with isOutput=true, even though the OpenQASM 3 spec documents output only for classical variables (e.g. output bit result;). Consider rejecting output when the parsed type is a quantum type.

🛡️ Proposed fix sketch (exact API for quantum-type check needs verification)
   auto statement = std::make_shared<DeclarationStatement>(DeclarationStatement{
       makeDebugInfo(tBegin, tEnd), isConst, isOutput, type, name, expression});
+
+  if (isOutput && /* type represents a quantum type, e.g. Qubit/Qreg */) {
+    error(tBegin, "The 'output' modifier is only valid for classical declarations");
+  }

Also applies to: 565-599

🤖 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 `@src/qasm3/Parser.cpp` around lines 169 - 189, The Parser::parseDeclaration
path currently allows output declarations for quantum types because the Output
branch unconditionally calls parseDeclaration(false, true). Update the
declaration parsing flow so that when isOutput is true, ParseType/parseType
rejects Qubit and Qreg (or the caller validates the returned type) and emits a
parse error for quantum output declarations. Keep the existing Const/Output
handling in Parser::parseStatement/parseDeclaration, but add a type check tied
to parseType so only classical types can be marked output.
🤖 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 `@mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h`:
- Around line 1166-1182: Update the Doxygen for
QCProgramBuilder::finalize(ValueRange) to describe generic main-function return
values instead of an exit code: revise the brief, the `@param` returnValues
description, and the details text so they consistently say the method finalizes
the module using the provided return values, while still noting the values must
match the main function signature. Keep the wording aligned with the
finalize(ValueRange) overload and its support for arbitrary return types.

In `@mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h`:
- Around line 1474-1506: The Doxygen for QCOProgramBuilder::finalize and
QCOProgramBuilder::build is outdated: finalize(ValueRange) should be described
as finalizing the module with the provided return values rather than an exit
code, and the build(...) documentation should remove any mention of a
returnTypes parameter and instead explain that the buildFunc result supplies
both values and types used for finalization. Update the comments on these two
API symbols so they match the current signatures and behavior.

In `@mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp`:
- Around line 1239-1242: Update the Doxygen conversion example near the
QCOToJeff rewrite to match the current behavior: the `convertFuncFuncOp`-style
pattern only strips the `passthrough` attribute and no longer rewrites `@main`
to `-> ()` with an empty return. Edit the nearby example/comments so they
describe preserving the existing returns and reflect the actual transformation
performed by `rewriter.startOpModification`, `op->removeAttr("passthrough")`,
and `rewriter.finalizeOpModification`.

In `@mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp`:
- Around line 474-503: The measurement-return stripping in
stripReturnedMeasurements currently rewrites every func::FuncOp, which can
change helper function return types without fixing callers. Restrict this logic
to the entry-point function only (using the function identified by the lowering
state), or else update all call sites and any dependent signatures consistently
when return operands/types are rewritten. Keep the returnedMeasurements tracking
tied to that entry-point-only handling.

In `@mlir/lib/Dialect/QC/Translation/TranslateQASM3ToQC.cpp`:
- Around line 232-247: The output collection in
TranslateQASM3ToQC::translateQASM3ToQC only checks the measured bits present in
bitValues for each register, so a partial prefix can pass unnoticed; update the
outputRegisters loop to compare the number of measured bits against the declared
register size before appending returnValues/returnTypes. Use the existing
bitValues lookup for each regName, but verify the full output bit[ ] declaration
is covered and reject any missing suffix bits with the same error path if the
measured count is smaller than the register’s expected size.

In `@mlir/lib/Dialect/QC/Translation/TranslateQuantumComputationToQC.cpp`:
- Around line 888-890: In the finalization path of the quantum-to-QC
translation, reject any classical bits that were never populated by a
measurement before calling builder.finalize(state.results). Update the logic
around the state.results assembly so it validates every entry is non-null for
getNcbits() > 0, and only pass the results vector to builder.finalize once all
classical bits have been measured; otherwise handle the invalid computation
explicitly instead of finalizing with null Values.

In `@mlir/unittests/Dialect/QTensor/IR/test_qtensor_ir.cpp`:
- Around line 197-202: The test in buildAndCanonicalize only calls qtensorAlloc,
so it never creates the DeallocOp that the assertion is supposed to eliminate.
Update the callback in test_qtensor_ir.cpp to explicitly build a matching
deallocation for the allocated tensor using the relevant QCOProgramBuilder
helpers (qtensorAlloc and the corresponding dealloc op creation path), so the
canonicalization is exercised on a real alloc/dealloc pair instead of passing
vacuously.

In `@mlir/unittests/programs/quantum_computation_programs.cpp`:
- Around line 118-122: The multipleControlledIdentity fixture in
QuantumComputation currently uses the wrong control/target mapping and only
measures one qubit, so update multipleControlledIdentity to match the
three-qubit contract used by the QC/QASM fixtures: keep the same
QuantumComputation setup but call mci with controls 0 and 1 targeting 2, and add
measurements for all three qubits into the classical register so translation and
equivalence tests see the same signature and operation mapping.

---

Outside diff comments:
In `@mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h`:
- Around line 408-418: Update the stale documentation for
QCOProgramBuilder::measure(Value qubit, const Bit& bit) so the example matches
the current return type; the method now returns a pair of output qubit and
measurement result, so fix the C++ and MLIR examples to show destructuring or
separate bindings instead of assigning to a single Value. Keep the wording
aligned with the measure overload signature and its documented pair return.

In `@mlir/include/mlir/Dialect/QIR/Builder/QIRProgramBuilder.h`:
- Around line 313-345: Update the documentation for QIRProgramBuilder methods
like measure and the return-value handling APIs to match the current behavior:
describe that measurement recording is conditional via the record parameter
(including the false case) rather than always happening, and replace any
language that treats returnValue as an exit code with wording that it can carry
arbitrary return values. Keep the comments/examples in sync across the affected
declarations so the semantics described in the docs match the implementation.

In `@mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp`:
- Around line 358-412: The measure overloads in QIRProgramBuilder::measure still
emit a new __quantum__qis__mz__body call even when the same classical slot has
already been measured; add a dedicated measurement-result cache keyed by the
classical result index/Bit and check it before creating the call. Update both
measure(Value, int64_t, bool) and measure(Value, const Bit&, bool) so they
return the previously recorded result early when the slot was already measured,
without reusing resultPtrs/loadedResults as the only guard.
- Around line 1014-1062: `QIRProgramBuilder::finalize(Value returnValue)` is
releasing adaptive result storage before the function returns, which can
invalidate a runtime result pointer produced by `measure()`. Update the adaptive
cleanup in `finalize` so that any `resultPtrs` or `resultArrays` that may be
returned from `main` are not freed before the `LLVM::ReturnOp` executes, or
convert the callback path to return an owned scalar instead of a raw runtime
pointer. Use the `finalize`, `generateOutputRecording`, and
`resultPtrs`/`resultArrays` logic to ensure returned values remain valid through
function exit.

In `@src/qasm3/Parser.cpp`:
- Around line 169-189: The Parser::parseDeclaration path currently allows output
declarations for quantum types because the Output branch unconditionally calls
parseDeclaration(false, true). Update the declaration parsing flow so that when
isOutput is true, ParseType/parseType rejects Qubit and Qreg (or the caller
validates the returned type) and emits a parse error for quantum output
declarations. Keep the existing Const/Output handling in
Parser::parseStatement/parseDeclaration, but add a type check tied to parseType
so only classical types can be marked output.
🪄 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: 40253feb-8ff4-4273-8720-e53c23ffbc1d

📥 Commits

Reviewing files that changed from the base of the PR and between c92ea1e and e327250.

📒 Files selected for processing (48)
  • CHANGELOG.md
  • bindings/ir/register_quantum_computation.cpp
  • include/mqt-core/ir/QuantumComputation.hpp
  • include/mqt-core/qasm3/Parser.hpp
  • include/mqt-core/qasm3/Statement.hpp
  • mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h
  • mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h
  • mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h
  • mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
  • mlir/include/mlir/Dialect/QIR/Builder/QIRProgramBuilder.h
  • mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp
  • mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp
  • mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp
  • mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp
  • mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp
  • mlir/lib/Dialect/QC/Translation/TranslateQASM3ToQC.cpp
  • mlir/lib/Dialect/QC/Translation/TranslateQuantumComputationToQC.cpp
  • mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp
  • mlir/lib/Dialect/QCO/IR/QubitManagement/SinkOp.cpp
  • mlir/lib/Dialect/QCO/IR/SCF/IfOp.cpp
  • mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp
  • mlir/unittests/Compiler/test_compiler_pipeline.cpp
  • mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp
  • mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp
  • mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp
  • mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp
  • mlir/unittests/Conversion/QCToQIR/QCToQIRBase/test_qc_to_qir_base.cpp
  • mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp
  • mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp
  • mlir/unittests/Dialect/QC/Translation/test_quantum_computation_translation.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp
  • mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp
  • mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp
  • mlir/unittests/Dialect/QTensor/IR/test_qtensor_ir.cpp
  • mlir/unittests/TestCaseUtils.h
  • mlir/unittests/programs/qasm_programs.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
  • mlir/unittests/programs/quantum_computation_programs.cpp
  • noxfile.py
  • python/mqt/core/ir/__init__.pyi
  • src/ir/QuantumComputation.cpp
  • src/qasm3/Parser.cpp

Comment thread mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h Outdated
Comment thread mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h Outdated
Comment thread mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp
Comment thread mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp
Comment thread mlir/lib/Dialect/QC/Translation/TranslateQASM3ToQC.cpp
Comment thread mlir/lib/Dialect/QC/Translation/TranslateQuantumComputationToQC.cpp Outdated
Comment thread mlir/unittests/Dialect/QTensor/IR/test_qtensor_ir.cpp Outdated
Comment thread mlir/unittests/programs/quantum_computation_programs.cpp Outdated
@DRovara DRovara requested review from burgholzer and denialhaag July 9, 2026 14:12
@burgholzer burgholzer force-pushed the mlir/dead-gate-elimination branch from 2bf7f84 to 0cd289a Compare July 10, 2026 01:11

@burgholzer burgholzer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks @DRovara for the Herculean effort here!
I went through almost everything here. In the process, I significantly simplified the signatures of the tests by automatically deducing the types and providing a couple of shortcuts.
This brought the diff down by 1.5k lines or so.

I still have to go through the tests once more to make sure they are fine as they are. However, there was one thing in the main code that will require changes (if I understood everything correctly). See the inline comment. This should also allow us to revert quite some of the changes in the qasm_programs.cpp file.
Could you please take a look at that? (and at the other changes as well; to check whether they make sense to you as well)

Comment on lines +403 to +407
if ((stmt->isOutput || openQASM2CompatMode) &&
sizedType->type == qasm3::Bit) {
// We return `output` bits in QASM3, or all named bits in QASM2.
outputRegisters.push_back(id);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is not entirely spec compliant. If an OpenQASM 3 program does not contain any output statements.
The spec states

Similarly, the output modifier can be used to indicate that one or more variables are to be provided as an explicit output of the quantum procedure. Note that OpenQASM 2 did not allow the programmer to specify that only a subset of its variables should be returned as output, and so it would return all classical variables (which were all creg variables) as output. For compatibility, OpenQASM 3 does not require an output declaration to be provided: in this case it assumes that all of the declared variables are to be returned as output. If the programmer provides one or more output declarations, then only those variables described as outputs will be returned as an output of the quantum process. A variable may not be marked as both input and output.

@DRovara

DRovara commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks a lot for the updates @burgholzer!

Automatically inferring the return types is smart, no idea how I missed that.

Regarding the built-in method to get corresponding inputs/outputs: I saw that too, but I didn't know we can just cast Values to OpOperand. In any case though, it feels a bit inconsistent that the methods have different names and signatures for IfOps, compared to all the unitaries. Is there a reason for that?

Also, regarding the OPENQASM 3 output thing: I can fix that, shouldn't be too much effort. And yeah, that should enable us to rever most qasm_programs

@burgholzer

Copy link
Copy Markdown
Member

Regarding the built-in method to get corresponding inputs/outputs: I saw that too, but I didn't know we can just cast Values to OpOperand. In any case though, it feels a bit inconsistent that the methods have different names and signatures for IfOps, compared to all the unitaries. Is there a reason for that?

This is mainly following the syntax and wording of the corresponding SCF operation. because the IfOP is much more an SCF operation than it is a unitary operation.
We could consider adjusting the naming of the QCO functions as well if we feel like that would help.

Also, regarding the OPENQASM 3 output thing: I can fix that, shouldn't be too much effort. And yeah, that should enable us to rever most qasm_programs

Would be great 👍🏼 I can take it from there again.

@DRovara

DRovara commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator Author

Would be great 👍🏼 I can take it from there again.

Update is ready.

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.

4 participants