Skip to content

♻️ Relax condition on modifiers#1751

Merged
denialhaag merged 44 commits into
mainfrom
more-control
Jun 10, 2026
Merged

♻️ Relax condition on modifiers#1751
denialhaag merged 44 commits into
mainfrom
more-control

Conversation

@denialhaag

@denialhaag denialhaag commented May 28, 2026

Copy link
Copy Markdown
Member

Description

This PR relaxes the condition that modifiers in QC and QCO must have a single unitary operation followed by a yield operation. Modifiers may now contain an arbitrary number of operations (unitary as well as, e.g., arith.constants).

Fixes #1678
Fixes #1727

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.

@denialhaag denialhaag added this to the MLIR Support milestone May 28, 2026
@denialhaag denialhaag self-assigned this May 28, 2026
@denialhaag denialhaag added refactor Anything related to code refactoring MLIR Anything related to MLIR labels May 28, 2026
@denialhaag denialhaag mentioned this pull request May 29, 2026
11 tasks
@denialhaag denialhaag force-pushed the more-control branch 2 times, most recently from 6911b65 to 7d6476a Compare May 29, 2026 17:40
@denialhaag denialhaag marked this pull request as ready for review June 1, 2026 15:31
@denialhaag

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

Refactors ctrl/inv modifiers so regions receive explicit target/qubit ValueRanges and body callbacks take ValueRange; adds target-alias parsing/printing utilities; converts op TDs and generated APIs to indexed body-unitary access; updates builders, canonicalizations, region inlining, conversions, translation state, verification, and tests.

Changes

Modifier API and Operation Definitions

Layer / File(s) Summary
Target-aliasing & op contracts
mlir/include/mlir/Dialect/Utils/Utils.h, mlir/include/mlir/Dialect/QC/IR/QCOps.td, mlir/include/mlir/Dialect/QCO/IR/QCOOps.td, mlir/include/mlir/Dialect/QC/IR/QCInterfaces.td
Add parse/print target-aliasing helpers; change QC/QCO op TDs to introduce variadic targets/qubits, AttrSizedOperandSegments, and switch generated APIs to indexed body-unitary accessors and OperandRange-based qubit/control/parameter accessors.

Builders and Public APIs

Layer / File(s) Summary
QCProgramBuilder API and macros
mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h, mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp
QCProgramBuilder::ctrl now takes (ValueRange controls, ValueRange targets, function_ref<void(ValueRange)>); QCProgramBuilder::inv now takes (ValueRange qubits, function_ref<void(ValueRange)>). Controlled-gate helpers and header docs updated to use targets(...) and %a* placeholders.

Modifier Implementations & Canonicalizations

Layer / File(s) Summary
QC Ctrl/Inv
mlir/lib/Dialect/QC/IR/Modifiers/CtrlOp.cpp, mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp
Builders create block args per explicit targets/qubits and invoke ValueRange-taking callbacks; add getNumBodyUnitaries()/getBodyUnitary(i); add EraseEmptyCtrl/EraseEmptyInv; relax structural verify() checks while forbidding non-unitary ops in bodies; canonicalizations rewritten to use sole-body checks.
QCO Ctrl/Inv & Barrier
mlir/lib/Dialect/QCO/IR/Modifiers/*, mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp
Analogous indexed body-unitary APIs and rewrites that remap inner block-args via outer targets; empty-modifier erasure; barrier input/output mapping helpers rewritten.

Conversion & Lowering

Layer / File(s) Summary
moveRegion & region inlining
mlir/include/mlir/Conversion/ConversionUtils.h, mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
Add moveRegion utility; refactor inlineRegion to infer arg counts and apply signature conversion after inlining.
QCO→Jeff
mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp
Materialize effective targets as SmallVector and remap body-arg indices to outer targets when lowering inside modifiers; update all gate-lowering sites and validate sole body unitaries for modifier lowers.
QC→QCO / QCO→QC
mlir/lib/Conversion/QCToQCO/QCToQCO.cpp, mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
Stop seeding alias args by numTargets; create destination ctrl/inv ops, move inline QC regions into destination regions with type conversion, and seed modifier frames from inlined entry-block args.
QC→QIR
mlir/lib/Conversion/QCToQIR/QCToQIR.cpp
Simplify LoweringState to a flat controls vector and counter; ctrl lowering rejects nested ctrl, sets inCtrlOp and state.controls from adaptor, and inlines region with adaptor.getTargets(); unitary lowering uses state.controls for control counts.

Translation Infrastructure

Layer / File(s) Summary
QuantumComputation → QC translation
mlir/lib/Dialect/QC/Translation/TranslateQuantumComputationToQC.cpp
Extend TranslationState with control-context fields (inCtrlOp, targetArgs); add state-aware getQubit/getControls; implement addCompoundOp() that calls builder.ctrl(controls, targets, callback) and populates/clears state.targetArgs while translating inner operations; update helpers to use TranslationState.

Verification & Optimizations

Layer / File(s) Summary
IR verification & optimizations
mlir/lib/Support/IRVerification.cpp, mlir/lib/Dialect/QCO/Transforms/Optimizations/HadamardLifting.cpp
Structural equivalence now handles qc::CtrlOp by mapping control targets into valueMap and comparing via getControls(); block-arg mapping preserves existing mappings; HadamardLifting tightened to require sole-body-unitary extraction.

Tests & Fixtures

Layer / File(s) Summary
Program helpers & test instantiations
mlir/unittests/programs/*, mlir/unittests/*/*/*
Mass-migrate QC program builders to ValueRange callbacks; add qc/qco/qir program helpers for empty, two-op, nested and mixed ctrl/inv scenarios; expand GoogleTest parameterizations across dialect, conversion, translation, and compiler-pipeline tests; update CHANGELOG PR link.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Suggested labels

c++

Suggested reviewers

  • burgholzer

"I’m a rabbit in a quantum patch,
hopping through regions, mapping each match.
Controls and targets now pass as a range,
indexed unitaries make rewrites less strange.
🐇✨"

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch more-control

@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: 6

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/QC/Builder/QCProgramBuilder.h (2)

903-921: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Documentation example does not match the new signature.

The example shows builder.ctrl(q0, [&] { builder.x(q1); }); but the new signature requires explicit targets and a callback accepting ValueRange. Update to reflect the new API:

builder.ctrl({q0}, {q1}, [&](ValueRange targets) { builder.x(targets[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/QC/Builder/QCProgramBuilder.h` around lines 903 -
921, Update the doc example for QCProgramBuilder::ctrl to match the new
signature ctrl(ValueRange controls, ValueRange targets, const
function_ref<void(ValueRange)>& body); — change the C++ and MLIR examples to
pass controls and targets as ValueRange (e.g., {q0}, {q1}) and make the callback
accept a ValueRange parameter (e.g., [&](ValueRange targets) { /* use targets[0]
*/ }); so the example shows using targets inside the lambda rather than a no-arg
lambda.

923-941: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Documentation example does not match the new signature.

The example shows builder.inv([&] { builder.s(q0); }); but the new signature requires explicit qubits and a callback accepting ValueRange. Update to reflect the new API:

builder.inv({q0}, [&](ValueRange qubits) { builder.s(qubits[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/QC/Builder/QCProgramBuilder.h` around lines 923 -
941, The doc example for QCProgramBuilder::inv is out of date; update the
comment to show the new signature that accepts explicit qubits and a callback
taking a ValueRange: replace the lambda-only example with one that calls inv
with a braced qubit list (e.g., {q0}) and a lambda taking a ValueRange parameter
(e.g., named qubits) that invokes builder.s on qubits[0]; ensure the text and
the MLIR example reflect the new ValueRange parameter usage.
🤖 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/Utils/Utils.h`:
- Around line 181-194: Add a precondition assertion at the start of
populateMapping to ensure innerQubits.size() == block.getNumArguments() before
indexing innerQubits in the loop: check this invariant (e.g., with assert(...)
or an LLVM-style assertion) referencing the symbols populateMapping,
innerQubits, and block.getNumArguments(), so the function fails fast and clearly
if the sizes mismatch rather than causing undefined behavior when accessing
innerQubits[arg.getArgNumber()].

In `@mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp`:
- Around line 946-952: The remapping uses op.getBody()->getArguments() which
assumes the inv's block args keep the same ordering as the outer targets;
instead iterate over the inv op's operands and use each operand's BlockArgument
index to pick the correct outer qubit. Concretely, replace the loop over
op.getBody()->getArguments() with a loop over op.getOperands(), cast each
operand to a BlockArgument (or otherwise retrieve its getArgNumber()), then push
outerQubits[thatIndex] into innerQubits and set state.targetsIn =
std::move(innerQubits); this preserves subset/reordering like qco.inv(%b,%a)
correctly.

In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp`:
- Around line 126-137: The inlineRegion helper can mis-handle mixed-type
operands when inlining SCF regions; before performing the llvm::zip_equal and
eraseArguments steps in inlineRegion, add explicit checks that the number of
replacementValues matches the number of block arguments from offset and that all
corresponding types are qubit types (e.g., assert or llvm_unreachable with clear
message if any arg.getType() or replacementVal.getType() is not the expected
qubit type); perform these guarded checks in inlineRegion (and analogously at
the other noted sites) so zip_equal assumptions hold and eraseArguments is only
called when the preconditions are satisfied.

In `@mlir/lib/Conversion/QCToQIR/QCToQIR.cpp`:
- Around line 212-215: The decrement and cleanup for control context are
incorrect: don’t unconditionally decrement state.inCtrlOp (which can underflow)
and check the post-decrement value when deciding to clear state.controls. Fix by
only decrementing when state.inCtrlOp > 0 and then test the updated value (e.g.,
use a pre-decrement on state.inCtrlOp or check state.inCtrlOp == 0 after
decrement) before calling state.controls.clear(); update the code paths that
reference inCtrlOp to use state.inCtrlOp so the captured old value isn’t used.

In `@mlir/lib/Dialect/QC/Translation/TranslateQuantumComputationToQC.cpp`:
- Around line 615-628: Add an explicit size-check before populating
state.ctrlTargets in the builder.ctrl callback: verify that targetArgs.size() ==
sortedPairs.size() (e.g., with an assert or
llvm::reportFatalInternalError/llvm::errs() + llvm_unreachable) and fail fast if
it doesn't match; then proceed to assign state.ctrlTargets[sortedPairs[i].first]
= targetArgs[i] and run translateOperation as before. This change touches the
builder.ctrl lambda in TranslateQuantumComputationToQC.cpp and ensures the
invariant between targetArgs and sortedPairs is enforced before using those
containers.

In `@mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp`:
- Around line 972-973: The test entry QCOTestCase{"inverseTwoX",
MQT_NAMED_BUILDER(twoX), MQT_NAMED_BUILDER(emptyQCO)} incorrectly reuses the
twoX builder; replace the second argument with the inverse-specific builder for
this case (e.g., MQT_NAMED_BUILDER(inverseTwoX) or the project’s dedicated
inverse builder) so that the "inverseTwoX" QCOTestCase uses the inverse-focused
builder instead of MQT_NAMED_BUILDER(twoX).

---

Outside diff comments:
In `@mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h`:
- Around line 903-921: Update the doc example for QCProgramBuilder::ctrl to
match the new signature ctrl(ValueRange controls, ValueRange targets, const
function_ref<void(ValueRange)>& body); — change the C++ and MLIR examples to
pass controls and targets as ValueRange (e.g., {q0}, {q1}) and make the callback
accept a ValueRange parameter (e.g., [&](ValueRange targets) { /* use targets[0]
*/ }); so the example shows using targets inside the lambda rather than a no-arg
lambda.
- Around line 923-941: The doc example for QCProgramBuilder::inv is out of date;
update the comment to show the new signature that accepts explicit qubits and a
callback taking a ValueRange: replace the lambda-only example with one that
calls inv with a braced qubit list (e.g., {q0}) and a lambda taking a ValueRange
parameter (e.g., named qubits) that invokes builder.s on qubits[0]; ensure the
text and the MLIR example reflect the new ValueRange parameter usage.
🪄 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: cca60f16-1ea9-4e24-8a0a-7d503638f8f4

📥 Commits

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

📒 Files selected for processing (33)
  • mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h
  • mlir/include/mlir/Dialect/QC/IR/QCOps.td
  • mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
  • mlir/include/mlir/Dialect/Utils/Utils.h
  • mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp
  • mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
  • mlir/lib/Conversion/QCToQCO/QCToQCO.cpp
  • mlir/lib/Conversion/QCToQIR/QCToQIR.cpp
  • mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp
  • mlir/lib/Dialect/QC/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp
  • mlir/lib/Dialect/QC/IR/QCOps.cpp
  • mlir/lib/Dialect/QC/Translation/TranslateQuantumComputationToQC.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/QCO/Transforms/Optimizations/HadamardLifting.cpp
  • mlir/lib/Support/IRVerification.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/QC/Translation/test_quantum_computation_translation.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_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
  • mlir/unittests/programs/quantum_computation_programs.cpp
  • mlir/unittests/programs/quantum_computation_programs.h

Comment thread mlir/include/mlir/Dialect/Utils/Utils.h Outdated
Comment thread mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp
Comment thread mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
Comment thread mlir/lib/Conversion/QCToQIR/QCToQIR.cpp Outdated
Comment thread mlir/lib/Dialect/QC/Translation/TranslateQuantumComputationToQC.cpp
Comment thread mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp Outdated
@denialhaag

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 1, 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.

…e on `getOutputForInput`

Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: burgholzer <burgholzer@me.com>
Signed-off-by: burgholzer <burgholzer@me.com>
@burgholzer

Copy link
Copy Markdown
Member

I am still working on this. I just pushed a first batch of changes to get a better PR diff again.

Signed-off-by: burgholzer <burgholzer@me.com>
Signed-off-by: burgholzer <burgholzer@me.com>
…erOp, CtrlOp, and InvOp

Signed-off-by: burgholzer <burgholzer@me.com>
Signed-off-by: burgholzer <burgholzer@me.com>
Signed-off-by: burgholzer <burgholzer@me.com>
Signed-off-by: burgholzer <burgholzer@me.com>
Signed-off-by: burgholzer <burgholzer@me.com>

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

Alright. I am through with all of the changes here.
This was a big one. It took a couple of hours to go through everything.
I managed to simplify a couple of concepts here and there while working my way through. Conceptually, this was pretty clean already.

Please have another close look at the changes that I introduced and see whether they make sense. Some of them are pretty nice simplifications I believe.
This is good to go from my side.

Signed-off-by: burgholzer <burgholzer@me.com>
@burgholzer

Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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: 2

Caution

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

⚠️ Outside diff range comments (4)
mlir/include/mlir/Dialect/Utils/Utils.h (1)

86-145: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add Doxygen blocks for the new aliasing helpers.

parseTargetAliasing and printTargetAliasing are new public header utilities, but their parser/printer contract is only implicit in the implementation right now. Please document both at the declaration site.

As per coding guidelines, "MUST use Doxygen-style comments in C++ code."

🤖 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/Utils/Utils.h` around lines 86 - 145, Add
Doxygen-style documentation for the two new public utilities parseTargetAliasing
and printTargetAliasing at their declaration site in the header: describe the
purpose, parameters (OpAsmParser& parser, Region& region,
SmallVectorImpl<OpAsmParser::UnresolvedOperand>& operands for
parseTargetAliasing; OpAsmPrinter& printer, Region& region, OperandRange
targetsIn for printTargetAliasing), expected assembly syntax/semantics (format
of "(%new = %old, ...)", that parseTargetAliasing binds block entry args of
QubitType), return behavior (ParseResult success/failure) and side effects
(populates operands and region or prints region), and any invariants (QubitType
hard-coded for targets). Ensure comments use Doxygen tags (brief/param/returns)
and follow the project's coding guidelines.

Source: Coding guidelines

mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp (1)

471-484: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Push modifier regions onto regionStack while the callback runs.

Unlike scfFor, scfWhile, and scfIf, these helpers never update regionStack around the body. Calls such as memrefLoad inside a ctrl/inv callback will still see the parent region and can either reject valid operations or record them under the wrong region.

🛠️ Suggested fix
 QCProgramBuilder&
 QCProgramBuilder::ctrl(ValueRange controls, ValueRange targets,
                        const function_ref<void(ValueRange)>& body) {
   checkFinalized();
-  CtrlOp::create(*this, controls, targets, body);
+  CtrlOp::create(*this, controls, targets, [&](ValueRange bodyTargets) {
+    regionStack.emplace_back(getInsertionBlock()->getParent());
+    body(bodyTargets);
+    regionStack.pop_back();
+  });
   return *this;
 }
 
 QCProgramBuilder&
 QCProgramBuilder::inv(ValueRange qubits,
                       const function_ref<void(ValueRange)>& body) {
   checkFinalized();
-  InvOp::create(*this, qubits, body);
+  InvOp::create(*this, qubits, [&](ValueRange bodyQubits) {
+    regionStack.emplace_back(getInsertionBlock()->getParent());
+    body(bodyQubits);
+    regionStack.pop_back();
+  });
   return *this;
 }
🤖 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/QC/Builder/QCProgramBuilder.cpp` around lines 471 - 484, The
ctrl and inv helpers (QCProgramBuilder::ctrl and QCProgramBuilder::inv) create
CtrlOp/InvOp but do not push the newly-created operation's region onto the
builder's regionStack while executing the body, so nested calls like memrefLoad
see the wrong parent region; update both functions to: create the CtrlOp/InvOp
(using CtrlOp::create and InvOp::create) but ensure you push the op's region
onto regionStack before invoking the body callback and pop it after the callback
returns (use the same region stack manipulation mechanics used by
scfFor/scfWhile/scfIf so regionStack.back() points to the op's region during
body execution).
mlir/lib/Dialect/QC/IR/Modifiers/CtrlOp.cpp (1)

101-110: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

The single-control gphase fold now pulls a potentially body-local angle out of the modifier. That replacement is only safe when theta already exists outside the region and there are no other helper ops in the body. With relaxed bodies, it can otherwise generate invalid SSA and silently discard setup ops. Please guard this fold or inline/clone the supporting defs first.

🤖 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/QC/IR/Modifiers/CtrlOp.cpp` around lines 101 - 110, The fold
that replaces a controlled gphase with a single POp (detecting GPhaseOp via
dyn_cast on innerOp and checking op.getNumControls() == 1, then calling
rewriter.replaceOpWithNewOp<POp>(..., gPhaseOp.getTheta())) is unsafe when
gPhaseOp.getTheta() is a body-local SSA value or the gphase body contains helper
ops; fix by first checking that gPhaseOp.getTheta() is defined outside the
region and that the gphase body contains no extra helper ops (or clone/inline
those supporting defs into the outer region) before performing the replacement,
otherwise bail out (return failure()) so you don’t create invalid SSA or drop
setup ops.
mlir/lib/Conversion/QCToQCO/QCToQCO.cpp (1)

513-524: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep walking into modifier regions when discovering SCF-carried state.

The new !isa<qc::CtrlOp, qc::InvOp>(operation) filter stops the enclosing SCF op from recording memrefs/qubits that are only referenced inside a modifier body. Those values then never become iter_args in ConvertSCFForOp / ConvertSCFWhileOp, so a load/extract inside the lowered modifier body falls back to the outer tensor mapping and loses updates across loop or branch boundaries.

Based on PR objectives: modifier bodies may now contain arbitrarily many operations, including non-unitary ops.

🤖 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/Conversion/QCToQCO/QCToQCO.cpp` around lines 513 - 524, The code is
incorrectly skipping collection for operations that are qc::CtrlOp or qc::InvOp,
preventing values used only inside modifier regions from becoming SCF iter_args;
remove the "!isa<qc::CtrlOp, qc::InvOp>(operation)" filter so that when an
operation has regions (operation.getNumRegions() > 0) you always call
collectQubitValuesInsideSCFOps(&operation, state) and merge results into
state->regionQubitMap[op] and state->regionRegisterMap[op] (and keep the
duplicate-removal using qubitInfoMap) so modifier-region-only qubits/registers
are recorded and later turned into iter_args used by
ConvertSCFForOp/ConvertSCFWhileOp.
♻️ Duplicate comments (1)
mlir/lib/Conversion/QCOToQC/QCOToQC.cpp (1)

127-139: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restore the all-qubit precondition before erasing SCF block arguments.

ConvertQCOSCFForOp and ConvertQCOSCFWhileOp still funnel illegal SCF ops through this helper, but the helper unconditionally replaces and erases every carried block argument after offset. If a non-qubit iter-arg slips through, it gets dropped as if it were rewritten qubit state.

Based on learnings: “all SCF conversion patterns ... assume and enforce that all operands are qubit types ... add explicit type checks or assertions to reject non-qubit operands.”

🤖 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/Conversion/QCOToQC/QCOToQC.cpp` around lines 127 - 139, inlineRegion
currently unconditionally replaces and erases every carried block argument after
offset, which drops non-qubit iter-args; update inlineRegion to verify the
precondition that all block arguments being replaced are qubit types before
performing replaceAllUsesWith/eraseArguments: iterate over
block.getArguments().drop_front(offset), check each argument’s type is the
quantum qubit type (e.g., mlir::quantum::QubitType::is or an isQubitType
helper), and if any argument is not a qubit either assert/fail fast or
return/report a conversion failure so the callers
ConvertQCOSCFForOp/ConvertQCOSCFWhileOp do not silently drop non-qubit args;
only perform the replacement and block.eraseArguments when the qubit-type check
succeeds.

Source: Learnings

🤖 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/IR/QCOps.td`:
- Around line 964-966: The getParameters() implementation for CtrlOp should not
fatal; change it to return an empty OperandRange to match getNumParams() == 0
and how qc.inv/CtrlOp variants expose parameters so generic UnitaryOpInterface
iteration works; leave getParameter(size_t i) as a fatal/error for out-of-range
access but ensure getParameters() returns an empty range (consistent with
getNumParams and other dialects).

In `@mlir/lib/Dialect/QC/IR/Modifiers/CtrlOp.cpp`:
- Around line 46-75: The current rewrite assumes the outer modifier body
contains only the inner unitary and drops surrounding helper ops; change CtrlOp
rewrite in the block using getSoleBodyUnitary, innerCtrlOp,
utils::getValueFromBlockArgument, merged, rewriter.inlineRegionBefore and
rewriter.eraseOp so it only proceeds when the outer body truly contains just the
single inner operation (e.g., check op.getBody()->getOperations() has exactly
the inner operation plus only an expected terminator) and otherwise bail out
with failure(); alternatively, if you must support multi-op bodies, clone/carry
the full outer body into the new merged region and remap block arguments/usages
(replacing block-arg lookups via utils::getValueFromBlockArgument) before
erasing the outer op so helper ops and dominance are preserved.

---

Outside diff comments:
In `@mlir/include/mlir/Dialect/Utils/Utils.h`:
- Around line 86-145: Add Doxygen-style documentation for the two new public
utilities parseTargetAliasing and printTargetAliasing at their declaration site
in the header: describe the purpose, parameters (OpAsmParser& parser, Region&
region, SmallVectorImpl<OpAsmParser::UnresolvedOperand>& operands for
parseTargetAliasing; OpAsmPrinter& printer, Region& region, OperandRange
targetsIn for printTargetAliasing), expected assembly syntax/semantics (format
of "(%new = %old, ...)", that parseTargetAliasing binds block entry args of
QubitType), return behavior (ParseResult success/failure) and side effects
(populates operands and region or prints region), and any invariants (QubitType
hard-coded for targets). Ensure comments use Doxygen tags (brief/param/returns)
and follow the project's coding guidelines.

In `@mlir/lib/Conversion/QCToQCO/QCToQCO.cpp`:
- Around line 513-524: The code is incorrectly skipping collection for
operations that are qc::CtrlOp or qc::InvOp, preventing values used only inside
modifier regions from becoming SCF iter_args; remove the "!isa<qc::CtrlOp,
qc::InvOp>(operation)" filter so that when an operation has regions
(operation.getNumRegions() > 0) you always call
collectQubitValuesInsideSCFOps(&operation, state) and merge results into
state->regionQubitMap[op] and state->regionRegisterMap[op] (and keep the
duplicate-removal using qubitInfoMap) so modifier-region-only qubits/registers
are recorded and later turned into iter_args used by
ConvertSCFForOp/ConvertSCFWhileOp.

In `@mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp`:
- Around line 471-484: The ctrl and inv helpers (QCProgramBuilder::ctrl and
QCProgramBuilder::inv) create CtrlOp/InvOp but do not push the newly-created
operation's region onto the builder's regionStack while executing the body, so
nested calls like memrefLoad see the wrong parent region; update both functions
to: create the CtrlOp/InvOp (using CtrlOp::create and InvOp::create) but ensure
you push the op's region onto regionStack before invoking the body callback and
pop it after the callback returns (use the same region stack manipulation
mechanics used by scfFor/scfWhile/scfIf so regionStack.back() points to the op's
region during body execution).

In `@mlir/lib/Dialect/QC/IR/Modifiers/CtrlOp.cpp`:
- Around line 101-110: The fold that replaces a controlled gphase with a single
POp (detecting GPhaseOp via dyn_cast on innerOp and checking op.getNumControls()
== 1, then calling rewriter.replaceOpWithNewOp<POp>(..., gPhaseOp.getTheta()))
is unsafe when gPhaseOp.getTheta() is a body-local SSA value or the gphase body
contains helper ops; fix by first checking that gPhaseOp.getTheta() is defined
outside the region and that the gphase body contains no extra helper ops (or
clone/inline those supporting defs into the outer region) before performing the
replacement, otherwise bail out (return failure()) so you don’t create invalid
SSA or drop setup ops.

---

Duplicate comments:
In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp`:
- Around line 127-139: inlineRegion currently unconditionally replaces and
erases every carried block argument after offset, which drops non-qubit
iter-args; update inlineRegion to verify the precondition that all block
arguments being replaced are qubit types before performing
replaceAllUsesWith/eraseArguments: iterate over
block.getArguments().drop_front(offset), check each argument’s type is the
quantum qubit type (e.g., mlir::quantum::QubitType::is or an isQubitType
helper), and if any argument is not a qubit either assert/fail fast or
return/report a conversion failure so the callers
ConvertQCOSCFForOp/ConvertQCOSCFWhileOp do not silently drop non-qubit args;
only perform the replacement and block.eraseArguments when the qubit-type check
succeeds.
🪄 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: dc8e0ca4-d48b-4979-8bc9-39765024f294

📥 Commits

Reviewing files that changed from the base of the PR and between e67219c and c160d6c.

📒 Files selected for processing (23)
  • CHANGELOG.md
  • mlir/include/mlir/Conversion/ConversionUtils.h
  • mlir/include/mlir/Dialect/QC/IR/QCDialect.h
  • mlir/include/mlir/Dialect/QC/IR/QCInterfaces.td
  • mlir/include/mlir/Dialect/QC/IR/QCOps.td
  • mlir/include/mlir/Dialect/QCO/IR/QCODialect.h
  • mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td
  • mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
  • mlir/include/mlir/Dialect/Utils/Utils.h
  • mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp
  • mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
  • mlir/lib/Conversion/QCToQCO/QCToQCO.cpp
  • mlir/lib/Conversion/QCToQIR/QCToQIR.cpp
  • mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp
  • mlir/lib/Dialect/QC/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp
  • mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp
  • mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/HadamardLifting.cpp
  • mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp
  • mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp
  • mlir/unittests/programs/qc_programs.cpp

Comment thread mlir/include/mlir/Dialect/QC/IR/QCOps.td Outdated
Comment thread mlir/lib/Dialect/QC/IR/Modifiers/CtrlOp.cpp
burgholzer and others added 3 commits June 10, 2026 22:18
Signed-off-by: burgholzer <burgholzer@me.com>
Signed-off-by: burgholzer <burgholzer@me.com>
@denialhaag

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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.

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/Utils/Utils.h (2)

199-204: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add assertion to guard against out-of-bounds access.

If qubit is a BlockArgument whose getArgNumber() exceeds qubits.size(), the index access at line 201 causes undefined behavior. This could occur if the block argument comes from a different block than the one qubits represents.

🛡️ Proposed fix
 inline Value getValueFromBlockArgument(Value qubit, ValueRange qubits) {
   if (auto blockArg = dyn_cast<BlockArgument>(qubit)) {
+    assert(blockArg.getArgNumber() < qubits.size() &&
+           "block argument index must be within qubits range");
     return qubits[blockArg.getArgNumber()];
   }
   return qubit;
 }
🤖 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/Utils/Utils.h` around lines 199 - 204, The function
getValueFromBlockArgument currently indexes qubits with blockArg.getArgNumber()
without bounds checking; add a guard/assert that blockArg.getArgNumber() <
qubits.size() before accessing qubits to avoid out-of-bounds UB (use an
llvm/standard assert or a conditional that returns qubit or emits an error),
referencing the BlockArgument, getArgNumber(), and qubits.size() in the fix;
ensure the code path still returns the original qubit when the check fails or
fails fast with a clear diagnostic.

171-193: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add assertion to validate entry block argument count matches targets.

The loop at line 186 accesses entryBlock.getArgument(i) assuming the entry block has at least numTargets arguments. If this invariant is violated, the index access causes undefined behavior before any error handling can occur.

🛡️ Proposed fix
 inline void printTargetAliasing(OpAsmPrinter& printer, Region& region,
                                 OperandRange targetsIn) {
   printer << "(";
   if (region.empty()) {
     printer << ") ";
     printer.printRegion(region, false);
     return;
   }
   auto& entryBlock = region.front();
+  assert(entryBlock.getNumArguments() >= targetsIn.size() &&
+         "entry block must have at least as many arguments as targets");

   const auto numTargets = targetsIn.size();
🤖 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/Utils/Utils.h` around lines 171 - 193, The function
printTargetAliasing assumes the region's entry block has at least as many
arguments as targetsIn; add a runtime assertion at the start of
printTargetAliasing (before the loop that calls entryBlock.getArgument(i)) that
checks entryBlock.getNumArguments() >= numTargets (use the project's assertion
mechanism, e.g., assert or LLVM_DEBUG/llvm_unreachable style) and include a
clear message referencing entryBlock and targetsIn length to avoid out-of-bounds
access when calling entryBlock.getArgument(i).
🤖 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.

Outside diff comments:
In `@mlir/include/mlir/Dialect/Utils/Utils.h`:
- Around line 199-204: The function getValueFromBlockArgument currently indexes
qubits with blockArg.getArgNumber() without bounds checking; add a guard/assert
that blockArg.getArgNumber() < qubits.size() before accessing qubits to avoid
out-of-bounds UB (use an llvm/standard assert or a conditional that returns
qubit or emits an error), referencing the BlockArgument, getArgNumber(), and
qubits.size() in the fix; ensure the code path still returns the original qubit
when the check fails or fails fast with a clear diagnostic.
- Around line 171-193: The function printTargetAliasing assumes the region's
entry block has at least as many arguments as targetsIn; add a runtime assertion
at the start of printTargetAliasing (before the loop that calls
entryBlock.getArgument(i)) that checks entryBlock.getNumArguments() >=
numTargets (use the project's assertion mechanism, e.g., assert or
LLVM_DEBUG/llvm_unreachable style) and include a clear message referencing
entryBlock and targetsIn length to avoid out-of-bounds access when calling
entryBlock.getArgument(i).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 52c8d034-6458-429c-bf68-42281507b759

📥 Commits

Reviewing files that changed from the base of the PR and between c160d6c and bcea1f8.

📒 Files selected for processing (6)
  • mlir/include/mlir/Dialect/QC/IR/QCOps.td
  • mlir/include/mlir/Dialect/Utils/Utils.h
  • mlir/lib/Dialect/QC/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp
  • mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp

@denialhaag

Copy link
Copy Markdown
Member Author

Thanks a lot for all the improvements, @burgholzer! I really like how clean this PR has become despite its size. Given that CodeRabbit is happy as well, I'll set this to auto-merge. 🎉

@denialhaag denialhaag enabled auto-merge (squash) June 10, 2026 23:26
@denialhaag denialhaag merged commit 72f997e into main Jun 10, 2026
29 checks passed
@denialhaag denialhaag deleted the more-control branch June 10, 2026 23:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

MLIR Anything related to MLIR refactor Anything related to code refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ Relax condition on modifiers 🐛 Operation folds are not modifier-aware, leaving modifier body in unexpected state after fold to identity

2 participants