Skip to content

✨ Add SCF conversions between QC and QCO dialects#1638

Open
li-mingbao wants to merge 77 commits intomunich-quantum-toolkit:mainfrom
li-mingbao:scf-conversions
Open

✨ Add SCF conversions between QC and QCO dialects#1638
li-mingbao wants to merge 77 commits intomunich-quantum-toolkit:mainfrom
li-mingbao:scf-conversions

Conversation

@li-mingbao
Copy link
Copy Markdown
Contributor

Description

This PR adds support for the conversion of the scf operations scf.while, scf.for, scf.if between the QC and the QCO dialect. This allows the conversion of programs with nonlinear controlflow.

This PR is the revamped version of the #1396 PR to integrate the newest changes.
Changes compared to the PR and new features:

  • Conversions support registers and qubits inside the scf operations.
  • The func conversion is not added since this also requires a qtensor.cast operation first.
  • scf.if is converted into qco.if
  • qco.if also supports tensors of qubits now as input types.
  • qco.yield also supports tensors of qubits now as input types.

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.

@li-mingbao
Copy link
Copy Markdown
Contributor Author

Hey @burgholzer,
Just wanted to quickly ask when you have time for a review since it's been a while since I (or in this case @denialhaag) requested it.

@burgholzer
Copy link
Copy Markdown
Member

Hey @burgholzer,

Just wanted to quickly ask when you have time for a review since it's been a while since I (or in this case @denialhaag) requested it.

Hey 👋🏻
Sorry, this somehow fell through the crack a little bit. I'll try to get to it asap. Although this may mean early next week.

Signed-off-by: Daniel Haag <121057143+denialhaag@users.noreply.github.com>
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: 5

Caution

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

⚠️ Outside diff range comments (1)
mlir/lib/Dialect/QCO/IR/SCF/IfOp.cpp (1)

246-281: ⚠️ Potential issue | 🟠 Major

Restore branch-yield/result validation in IfOp::verify.

Line 140 replaces the qco.if with the selected branch terminator operands. Right now verify() never checks that thenYield() and elseYield() produce the same number and types of values as the op results, so malformed IR can pass verification and then break RemoveStaticCondition or later rewrites on an arity/type mismatch.

💡 Suggested fix
   if (numInputQubits != numOutputQubits) {
     return emitOpError("Operation must return the same number of qubits as the "
                        "number of input qubits.");
   }
+  auto thenYieldTypes = thenYield().getOperandTypes();
+  auto elseYieldTypes = elseYield().getOperandTypes();
+  if (thenYieldTypes.size() != numOutputQubits ||
+      elseYieldTypes.size() != numOutputQubits) {
+    return emitOpError("Both regions must yield the same number of values as "
+                       "the operation returns.");
+  }
+  for (auto [thenType, elseType, resultType] :
+       llvm::zip_equal(thenYieldTypes, elseYieldTypes, getResultTypes())) {
+    if (thenType != resultType || elseType != resultType) {
+      return emitOpError("Both regions must yield the same types as the "
+                         "operation results.");
+    }
+  }
   for (auto [inputQubitType, outputQubitType] :
        llvm::zip_equal(inputQubits.getTypes(), outputQubits.getTypes())) {
     if (inputQubitType != outputQubitType) {
       return emitOpError("Operation must return the same qubit types as its "
                          "input qubit types.");

Based on learnings: qco.if guarantees both then and else regions exist and each is terminated by qco.yield.

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

In `@mlir/lib/Dialect/QCO/IR/SCF/IfOp.cpp` around lines 246 - 281, The verify() in
IfOp must also validate branch yields: ensure thenYield() and elseYield() exist
and that both produce the same number of values and types that match the
operation results (getResults()) and input qubits (getQubits())—i.e., check
thenYield()->getNumOperands() == elseYield()->getNumOperands() ==
getResults().size() and that each yielded type equals the corresponding result
type (and/or input qubit type where appropriate); if any mismatch emitOpError
with a clear message; update IfOp::verify to perform these checks using the
existing symbols thenYield(), elseYield(), getResults(), getQubits() and return
failure on mismatch so downstream passes like RemoveStaticCondition can rely on
arity/type consistency.
♻️ Duplicate comments (1)
mlir/lib/Conversion/QCToQCO/QCToQCO.cpp (1)

1253-1282: ⚠️ Potential issue | 🟠 Major

Reject or remap existing SCF carried block arguments before erasing the old op.

Line 1266 only rewires the induction variable, and the scf.while rewrite never rewires the old before/after block arguments at all. If this pass ever sees a QC scf.for/scf.while with pre-existing qubit/tensor carried state, the spliced body will keep using the old region arguments and rewriter.eraseOp(op) will invalidate them. Please either RAUW the old carried block arguments to the newly created block arguments or fail fast when the source op already has carriers/results outside the builder-generated zero-carrier shape.

Based on learnings, the rewrite is intentionally scoped to all-qubit SCF ops, but that invariant does not remap already-existing qubit/qtensor region arguments.

Also applies to: 1333-1374

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

In `@mlir/lib/Conversion/QCToQCO/QCToQCO.cpp` around lines 1253 - 1282, The
rewrite currently only replaces the induction variable and then erases the old
op, which leaves any pre-existing carried block arguments (qubits/qtensors) in
the spliced body referring to invalid region arguments; update the logic after
splicing (around where newForOp is created and dstBlock/dstBlock.getArguments()
are used) to RAUW each old block argument from op.getRegion().front() to the
corresponding new block argument in newForOp.getRegion().front() (use
dstBlock.getArguments().drop_front(1).take_back(numQubits) and
.take_front(numRegisters) for mapping), or if the source op contains
carriers/results beyond the builder-generated zero-carrier shape, bail out with
a clear failure instead; apply the same replacement/failure pattern to the
analogous scf.while rewrite path referenced in the comment (the block around
lines 1333-1374).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h`:
- Around line 1260-1343: The documentation for scfFor and scfWhile is out of
date: it still states iter-args are qubits only and the scfFor example mixes
builder names (uses b.* calls while declaring builder) which is misleading now
that tensor containers are supported; update the prose to say iter-args/initArgs
may be qubits or tensor containers (tensor<...!qco.qubit>) and that
qtensorExtract/qtensorInsert are used when an iter-arg is a tensor, fix the
example code snippets to use the same builder variable name (e.g.,
builder.qtensorExtract / builder.h / builder.qtensorInsert) and provide a
parallel scfWhile example that shows tensor initArgs as well; also add a short
note that qco.if was similarly extended to accept tensor iter-args so callers
know the API change (reference symbols: scfFor, scfWhile, qtensorExtract,
qtensorInsert, qco.h, and qco.if).

In `@mlir/include/mlir/Dialect/QCO/IR/QCOOps.td`:
- Around line 1059-1060: Update the dialect documentation strings to reflect
that qco.yield and qco.if signatures (and the related scf variants) accept
LinearType, not just qubits: change descriptions around the definitions for
qco.yield and qco.if (and the other affected spots noted near lines 1246-1249)
to state that init/iter arguments may be LinearType including tensor containers
like tensor<...x!qco.qubit>; mention that tensor containers are supported for
scf.for, scf.while (scfWhile) and qco.if/scf.if (qcoIf) so the generated docs
accurately show tensor<...x!qco.qubit> support. Ensure any wording that
currently says “qubit-only” is replaced with “LinearType (including
tensor<...x!qco.qubit>)” and keep the summaries consistent with the changed ins
types (Variadic<LinearType>:$targets).

In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp`:
- Around line 763-775: The current lowering for scf::ForOp (and similarly for
the scf::If case around lines 812–823) unconditionally erases loop results and
replaces uses with adaptor.getInitArgs(), which is only correct if each carried
slot preserves positional aliasing across every scf.yield/scf.condition; add a
verification or fallback: before calling inlineRegion and rewriter.replaceOp in
the scf::ForOp lowering (symbols: scf::ForOp::create, inlineRegion,
rewriter.replaceOp, adaptor.getInitArgs()), scan the loop body and all
scf.yield/scf.condition sites to ensure each yielded value for index i is the
same physical container/operand as the corresponding init arg (or provably
equivalent); if the invariant fails, either emit a diagnostic and return failure
(or skip lowering) or adjust the lowering to preserve distinct results (i.e.,
keep the scf::ForOp result values instead of replacing them). Ensure a short doc
comment near the lowering and/or add a verifier check that enforces this
positional-aliasing invariant for loop carriers.

In `@mlir/lib/Dialect/QCO/IR/QCOOps.cpp`:
- Around line 203-207: The printer currently emits the result signature by
iterating the then-region entry block args
(getThenRegion().front().getArgumentTypes()), which can diverge from the op's
actual results; change the printing to use the op's explicit result types via
getResultTypes() (i.e., replace the call to
getThenRegion().front().getArgumentTypes() with getResultTypes() in the block
that prints the "-> (...)" clause so interleaveComma prints the op's real result
types).

In `@mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp`:
- Around line 154-194: The test IfOpParser (TEST_F QCOTest, IfOpParser) only
covers scalar !qco.qubit paths; add a new tensor-based case that exercises
qco.if/qco.yield with tensor<...x!qco.qubit> init/iter args to catch
parser/cleanup regressions. Update the mlirCode snippet used to parse (the
module string inside IfOpParser or add a new TEST_F) to allocate/insert/extract
a tensor<1x!qco.qubit> and use qco.if with qubits(%arg0 = %q2) where %arg0 has
tensor<1x!qco.qubit>, and ensure qco.yield returns that tensor form in both
branches; also update or add a matching reference builder (use
QCOProgramBuilder::build with a new named builder like simpleIfTensor or extend
MQT_NAMED_BUILDER(simpleIf) to produce a tensor-typed result) and keep the same
verification/cleanup/assertion sequence (parseSourceString, verify,
runQCOCleanupPipeline, areModulesEquivalentWithPermutations) to validate tensor
support.

---

Outside diff comments:
In `@mlir/lib/Dialect/QCO/IR/SCF/IfOp.cpp`:
- Around line 246-281: The verify() in IfOp must also validate branch yields:
ensure thenYield() and elseYield() exist and that both produce the same number
of values and types that match the operation results (getResults()) and input
qubits (getQubits())—i.e., check thenYield()->getNumOperands() ==
elseYield()->getNumOperands() == getResults().size() and that each yielded type
equals the corresponding result type (and/or input qubit type where
appropriate); if any mismatch emitOpError with a clear message; update
IfOp::verify to perform these checks using the existing symbols thenYield(),
elseYield(), getResults(), getQubits() and return failure on mismatch so
downstream passes like RemoveStaticCondition can rely on arity/type consistency.

---

Duplicate comments:
In `@mlir/lib/Conversion/QCToQCO/QCToQCO.cpp`:
- Around line 1253-1282: The rewrite currently only replaces the induction
variable and then erases the old op, which leaves any pre-existing carried block
arguments (qubits/qtensors) in the spliced body referring to invalid region
arguments; update the logic after splicing (around where newForOp is created and
dstBlock/dstBlock.getArguments() are used) to RAUW each old block argument from
op.getRegion().front() to the corresponding new block argument in
newForOp.getRegion().front() (use
dstBlock.getArguments().drop_front(1).take_back(numQubits) and
.take_front(numRegisters) for mapping), or if the source op contains
carriers/results beyond the builder-generated zero-carrier shape, bail out with
a clear failure instead; apply the same replacement/failure pattern to the
analogous scf.while rewrite path referenced in the comment (the block around
lines 1333-1374).
🪄 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: 7ecbe3b4-1e8b-483b-8b98-f1a0cd268e70

📥 Commits

Reviewing files that changed from the base of the PR and between d661f1e and 2e29ffc.

📒 Files selected for processing (12)
  • 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/lib/Conversion/QCOToJeff/QCOToJeff.cpp
  • mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
  • mlir/lib/Conversion/QCToQCO/QCToQCO.cpp
  • mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp
  • mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.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
  • mlir/unittests/programs/qco_programs.cpp

Comment thread mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h Outdated
Comment thread mlir/include/mlir/Dialect/QCO/IR/QCOOps.td
Comment thread mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
Comment thread mlir/lib/Dialect/QCO/IR/QCOOps.cpp
Comment thread mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp
@denialhaag
Copy link
Copy Markdown
Member

@li-mingbao, I wanted to bring this PR up to speed after the merge of #1673. Unfortunately, my activity triggered CodeRabbit, which then found issues unrelated to the rebase. 😕

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

Caution

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

⚠️ Outside diff range comments (1)
mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp (1)

129-137: ⚠️ Potential issue | 🔴 Critical

Return the Bit on the non-error path.

For valid indices, this function reaches the end without returning a value. Any call to register[i] is therefore undefined.

Suggested fix
   if (index < 0 || index >= size) {
     const std::string msg = "Bit index " + std::to_string(index) +
                             " out of bounds for register '" + name +
                             "' of size " + std::to_string(size);
     llvm::reportFatalUsageError(msg.c_str());
-    return {.registerName = name, .registerSize = size, .registerIndex = index};
   }
+  return {.registerName = name, .registerSize = size, .registerIndex = index};
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp` around lines 129 - 137, The
operator ClassicalRegister::operator[](const int64_t index) const currently only
returns on the error path; for valid indices it falls off without returning a
Bit. Fix by returning a Bit constructed from this register for the non-error
path: in QCProgramBuilder::ClassicalRegister::operator[] create and return a Bit
with .registerName = name, .registerSize = size, and .registerIndex = index (the
same shape as the error return uses) when index is within bounds so every
control flow returns a Bit.
♻️ Duplicate comments (3)
mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h (1)

1217-1258: ⚠️ Potential issue | 🟡 Minor

Docs still read as qubit-only for the new tensor-capable SCF builders.

qcoIf, scfFor, and scfWhile now take ValueRange initArgs, but the prose still says “qubit values” in multiple places, and the scfFor example switches from builder to b. That leaves the public header out of sync with the API right where the new tensor-carrying control-flow support is introduced. Based on learnings: tensor containers (tensor<...x!qco.qubit>) are supported as initArgs/iter-args for scf.for, scf.while, and qco.if as of PR #1638.

📝 Suggested doc fix
- * Constructs an if operation that takes a bool Value and a range of qubit
- * values that are used in the then/else region of this operation. The qubit
- * values are passed down as block arguments to each region. Qubits that were
+ * Constructs an if operation that takes a bool Value and a range of qubit or
+ * tensor-of-qubit values that are used in the then/else region of this
+ * operation. The input values are passed down as block arguments to each
+ * region. Qubits that were
  * extracted from a tensor that is used as an argument for this operation are
  * automatically inserted before the operation is constructed.
...
- * Constructs a scf.for operation with the given loop boundaries and stepsize
- * and a range of qubit values for its iter args.
+ * Constructs a scf.for operation with the given loop boundaries and step size
+ * and a range of qubit or tensor-of-qubit values for its iter args.
...
- *   auto [t0, q0] = b.qtensorExtract(iterArgs[0], iv);
- *   auto q1 = b.h(q0);
- *   auto insert = b.qtensorInsert(q1, t0, iv);
+ *   auto [t0, q0] = builder.qtensorExtract(iterArgs[0], iv);
+ *   auto q1 = builder.h(q0);
+ *   auto insert = builder.qtensorInsert(q1, t0, iv);
...
- * Constructs a scf.while with a range of qubit values for its iter args.
+ * Constructs a scf.while with a range of qubit or tensor-of-qubit values for
+ * its iter args.

Also applies to: 1260-1342

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

In `@mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h` around lines 1217
- 1258, The header docs incorrectly describe initArgs as "qubit values" and
contain an inconsistent example variable (uses "b" instead of "builder"); update
the documentation for qcoIf (and the corresponding scfFor/scfWhile blocks) to
state that initArgs can be qubit values or tensor containers (e.g., tensor<... x
!qco.qubit>), adjust the prose to mention tensor-carrying init/iter-args, and
fix the example to consistently use "builder" (not "b"); ensure the updated
descriptions appear for qcoIf, scfFor, and scfWhile (also apply the same fixes
around the other commented range mentioned).
mlir/lib/Conversion/QCOToQC/QCOToQC.cpp (1)

762-773: ⚠️ Potential issue | 🟠 Major

Don’t erase loop-carried results unless slot identity is enforced.

Both loop lowerings replace the original scf.for / scf.while results with adaptor.getInitArgs() / adaptor.getInits(). That is only semantics-preserving if each carried slot is guaranteed to denote the same physical qubit/container in the same position across every scf.yield / scf.condition. A loop that permutes carriers or yields a different resource in a slot will be silently mislowered here.

Either reject such loops explicitly (assert/diagnostic/verifier) or keep the carriers/results threaded through the lowered SCF op instead of dropping them.

Also applies to: 815-820

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

In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp` around lines 762 - 773, The lowering
currently drops loop-carried results by replacing the original scf.for/scf.while
op with adaptor.getInitArgs() (see scf::ForOp::create, inlineRegion, and
rewriter.replaceOp(op, adaptor.getInitArgs())), which is only valid if each
carried slot preserves identity; fix by preserving/threading the loop-carried
results instead of discarding them: after inlining the region, capture the
values produced by the inlined terminator(s) (the scf.yield/scf.condition
results) and replace the original op with those yielded values (wiring them
through the newFor/newWhile results), or alternatively add an explicit
verifier/assertion that each yielded slot is identical to the corresponding init
slot before allowing the current replacement; apply the same change for the
scf.while lowering code paths referenced near the other occurrence.
mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp (1)

968-999: ⚠️ Potential issue | 🔴 Critical

Use the scf.condition operands as the single source of truth.

Lines 993-994 thread the vector returned from beforeBody() into the after region, but Line 1072 builds the actual scf.condition from an independent yieldedValues list. If those two sets ever differ, the builder state no longer matches the IR and region-local values can leak or be validated against the wrong carried state.

This looks like the same root issue that was flagged earlier, and it still appears present in the current implementation. The fix needs to bind bookkeeping to the operands passed to scfCondition() itself, or derive both the terminator and the threaded values from one shared result.

Also applies to: 1057-1072

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

In `@mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp` around lines 968 - 999,
The builder currently threads the vector from beforeBody() into the after region
(beforeResults) but constructs the scf.condition and/or uses a separate
yieldedValues list, causing divergence between region-local values and the
condition operands; fix this by making the scf.condition operands the single
source of truth: when you create the scf::ConditionOp (scfCondition()) use the
exact vector returned by createBody(beforeBlock, beforeBody, ...)
(beforeResults) and reuse that same vector for building the after region and for
any scf::YieldOp creation and calls to updateQubitValueTracking (i.e., replace
any independent yieldedValues with beforeResults/afterResults so createBody,
scf::ConditionOp creation, scf::YieldOp creation, and updateQubitValueTracking
all reference the same Value vector such that beforeResults -> scf::ConditionOp
operands -> after region inputs remain identical).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mlir/lib/Conversion/QCToQCO/QCToQCO.cpp`:
- Around line 1200-1203: The terminator replacements (e.g., the block where
currentModifierFrame(state) is used, resolveMappedQubits(state, operation,
frame.qcQubits), rewriter.replaceOpWithNewOp<qco::YieldOp>(op, targets),
popModifierFrame(state)) should be rewritten to replace the terminator in place:
set the rewriter insertion point to the existing terminator (call
rewriter.setInsertionPoint(op)), create the new qco::YieldOp (or the
corresponding terminator used at the other sites) at that insertion point, then
erase the old terminator op with rewriter.eraseOp(op) instead of
replaceOpWithNewOp; apply this same pattern for the other two locations
referenced (the replaceOpWithNewOp sites around lines 1493–1495 and 1531–1533).
- Around line 1250-1263: The new scf::ForOp created by scf::ForOp::create(...)
includes an auto-generated body with a terminator, so before splicing srcBlock
into newForOp’s body you must remove that auto-generated terminator/block to
avoid duplicate scf.yield terminators; locate the creation and subsequent
splicing around scf::ForOp::create(...), newForOp.getRegion().front(),
srcBlock/dstBlock and either erase the auto-created body block or erase its
terminator (the auto-generated scf.yield) on dstBlock before doing
dstBlock.getOperations().splice(...), then proceed with assignMappedTensors,
assignMappedQubits and the rewriter.replaceAllUsesWith(...) call.

In `@mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp`:
- Around line 229-269: Detect and reject duplicate non-qubit region operands
before mutating tensor tracking: before accessing validTensors[initArg] or
calling updateTensorTracking inside the loop over initArgs, check whether the
same tensor SSA value appears earlier in initArgs/updatedArgs (e.g., keep a
SmallPtrSet<Value> seenTensors or test updatedArgs.contains(initArg)); if a
duplicate non-qubit tensor is found, emit the same linear-use error
(llvm::reportFatalUsageError) and return instead of proceeding. Also avoid using
operator[] on validTensors for presence checks—use find/contains or at() so you
don't accidentally default-insert a TensorInfo for the second occurrence; only
mutate tracking (updateTensorTracking, validQubits.erase, currentTensor
assignment, updatedArgs.emplace_back) after the duplicate check passes.

---

Outside diff comments:
In `@mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp`:
- Around line 129-137: The operator ClassicalRegister::operator[](const int64_t
index) const currently only returns on the error path; for valid indices it
falls off without returning a Bit. Fix by returning a Bit constructed from this
register for the non-error path: in
QCProgramBuilder::ClassicalRegister::operator[] create and return a Bit with
.registerName = name, .registerSize = size, and .registerIndex = index (the same
shape as the error return uses) when index is within bounds so every control
flow returns a Bit.

---

Duplicate comments:
In `@mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h`:
- Around line 1217-1258: The header docs incorrectly describe initArgs as "qubit
values" and contain an inconsistent example variable (uses "b" instead of
"builder"); update the documentation for qcoIf (and the corresponding
scfFor/scfWhile blocks) to state that initArgs can be qubit values or tensor
containers (e.g., tensor<... x !qco.qubit>), adjust the prose to mention
tensor-carrying init/iter-args, and fix the example to consistently use
"builder" (not "b"); ensure the updated descriptions appear for qcoIf, scfFor,
and scfWhile (also apply the same fixes around the other commented range
mentioned).

In `@mlir/lib/Conversion/QCOToQC/QCOToQC.cpp`:
- Around line 762-773: The lowering currently drops loop-carried results by
replacing the original scf.for/scf.while op with adaptor.getInitArgs() (see
scf::ForOp::create, inlineRegion, and rewriter.replaceOp(op,
adaptor.getInitArgs())), which is only valid if each carried slot preserves
identity; fix by preserving/threading the loop-carried results instead of
discarding them: after inlining the region, capture the values produced by the
inlined terminator(s) (the scf.yield/scf.condition results) and replace the
original op with those yielded values (wiring them through the newFor/newWhile
results), or alternatively add an explicit verifier/assertion that each yielded
slot is identical to the corresponding init slot before allowing the current
replacement; apply the same change for the scf.while lowering code paths
referenced near the other occurrence.

In `@mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp`:
- Around line 968-999: The builder currently threads the vector from
beforeBody() into the after region (beforeResults) but constructs the
scf.condition and/or uses a separate yieldedValues list, causing divergence
between region-local values and the condition operands; fix this by making the
scf.condition operands the single source of truth: when you create the
scf::ConditionOp (scfCondition()) use the exact vector returned by
createBody(beforeBlock, beforeBody, ...) (beforeResults) and reuse that same
vector for building the after region and for any scf::YieldOp creation and calls
to updateQubitValueTracking (i.e., replace any independent yieldedValues with
beforeResults/afterResults so createBody, scf::ConditionOp creation,
scf::YieldOp creation, and updateQubitValueTracking all reference the same Value
vector such that beforeResults -> scf::ConditionOp operands -> after region
inputs remain identical).
🪄 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: b043b258-2ea4-491c-9d42-fadbc8505b84

📥 Commits

Reviewing files that changed from the base of the PR and between 2e29ffc and 5bb31a5.

📒 Files selected for processing (6)
  • mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h
  • mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h
  • mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
  • mlir/lib/Conversion/QCToQCO/QCToQCO.cpp
  • mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp
  • mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp

Comment thread mlir/lib/Conversion/QCToQCO/QCToQCO.cpp
Comment thread mlir/lib/Conversion/QCToQCO/QCToQCO.cpp
Comment thread mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp
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: 1

♻️ Duplicate comments (2)
mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp (1)

973-1003: ⚠️ Potential issue | 🔴 Critical

Drive scf.while tracking from the actual scf.condition operands.

The builder currently seeds the after-region from beforeBody's return value and maps loop results from afterBody's return value, but in MLIR the values passed to scf.condition are the false-exit results and the after-region may not execute at all. That lets builder tracking diverge from the IR whenever beforeBody() returns something different from scfCondition(...), or when the loop exits after mutating values in the before-region.

Also applies to: 1062-1077

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

In `@mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp` around lines 973 - 1003,
The current builder seeds the after-region and loop-result tracking from
beforeBody/afterBody return values (createBody + updateQubitValueTracking),
which can diverge from the IR; instead locate the scf.condition operation tied
to the scf.while (the scf::ConditionOp created for the loop), extract its
operand Values (the false-exit results passed to the condition) and use those
operands as the source for updateQubitValueTracking for the whileOp results and
as the inputs to the after-region mapping, i.e., change the code around
createBody/use of beforeResults/afterResults so that
updateQubitValueTracking(afterOperandsFromCondition, whileOp->getResults()) and
the call to createBody(afterBlock, afterBody, /*initArgs=*/conditionOperands,
true) drive the tracking rather than using beforeBody/afterBody return values;
keep createBody(beforeBlock, ...) behavior for before-region execution but do
not seed the loop-result mapping from beforeBody results.
mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h (1)

1278-1283: ⚠️ Potential issue | 🟡 Minor

Fix inconsistent builder variable name in example.

The example uses builder.scfFor(...) on the outer call but b.qtensorExtract, b.h, b.qtensorInsert inside the lambda. This inconsistency can confuse users since b is never defined. Use builder consistently throughout.

📝 Suggested documentation fix
    * `@par` Example:
    * ```c++
    * builder.scfFor(lb, ub, step, initArgs, [&](Value iv, ValueRange iterArgs)
    * -> SmallVector<Value> {
-   *   auto [t0, q0] = b.qtensorExtract(iterArgs[0], iv);
-   *   auto q1 = b.h(q0);
-   *   auto insert = b.qtensorInsert(q1, t0, iv);
+   *   auto [t0, q0] = builder.qtensorExtract(iterArgs[0], iv);
+   *   auto q1 = builder.h(q0);
+   *   auto insert = builder.qtensorInsert(q1, t0, iv);
    *   return {insert};
    * });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h` around lines 1278
- 1283, The example uses builder.scfFor(...) but then refers to an undefined
variable b inside the lambda; replace b with builder for consistency so the
calls to qtensorExtract, h, and qtensorInsert use the same builder instance
(i.e., change b.qtensorExtract -> builder.qtensorExtract, b.h -> builder.h,
b.qtensorInsert -> builder.qtensorInsert inside the lambda passed to
builder.scfFor).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mlir/lib/Conversion/QCToQCO/QCToQCO.cpp`:
- Around line 1259-1264: The moved ops still reference the old scf.for iter-arg
block arguments, so before erasing the source loop (op) you must remap each
srcBlock argument to the corresponding dstBlock argument; iterate over
srcBlock.getNumArguments(), for each index get srcArg = srcBlock.getArgument(i)
and dstArg = dstBlock.getArgument(i) and call
rewriter.replaceAllUsesWith(srcArg, dstArg) (using the same rewriter used for
induction var RAUW of op.getInductionVar() -> newForOp.getInductionVar()), then
proceed with splicing and erasing the original op so no uses or invalid defs
remain.

---

Duplicate comments:
In `@mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h`:
- Around line 1278-1283: The example uses builder.scfFor(...) but then refers to
an undefined variable b inside the lambda; replace b with builder for
consistency so the calls to qtensorExtract, h, and qtensorInsert use the same
builder instance (i.e., change b.qtensorExtract -> builder.qtensorExtract, b.h
-> builder.h, b.qtensorInsert -> builder.qtensorInsert inside the lambda passed
to builder.scfFor).

In `@mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp`:
- Around line 973-1003: The current builder seeds the after-region and
loop-result tracking from beforeBody/afterBody return values (createBody +
updateQubitValueTracking), which can diverge from the IR; instead locate the
scf.condition operation tied to the scf.while (the scf::ConditionOp created for
the loop), extract its operand Values (the false-exit results passed to the
condition) and use those operands as the source for updateQubitValueTracking for
the whileOp results and as the inputs to the after-region mapping, i.e., change
the code around createBody/use of beforeResults/afterResults so that
updateQubitValueTracking(afterOperandsFromCondition, whileOp->getResults()) and
the call to createBody(afterBlock, afterBody, /*initArgs=*/conditionOperands,
true) drive the tracking rather than using beforeBody/afterBody return values;
keep createBody(beforeBlock, ...) behavior for before-region execution but do
not seed the loop-result mapping from beforeBody results.
🪄 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: a0f00bfc-f63e-44b8-9fc7-8c4089073add

📥 Commits

Reviewing files that changed from the base of the PR and between 5bb31a5 and b0b6bb1.

📒 Files selected for processing (7)
  • 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/lib/Conversion/QCToQCO/QCToQCO.cpp
  • mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp
  • mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp
  • mlir/lib/Dialect/QCO/IR/QCOOps.cpp

Comment thread mlir/lib/Conversion/QCToQCO/QCToQCO.cpp
burgholzer added 5 commits May 6, 2026 00:41
Signed-off-by: Lukas Burgholzer <burgholzer@me.com>
Signed-off-by: Lukas Burgholzer <burgholzer@me.com>
Signed-off-by: Lukas Burgholzer <burgholzer@me.com>
Signed-off-by: Lukas Burgholzer <burgholzer@me.com>
Signed-off-by: Lukas Burgholzer <burgholzer@me.com>
@mergify mergify Bot added the conflict label May 6, 2026
@mergify mergify Bot removed the conflict label May 6, 2026
Copy link
Copy Markdown
Member

@burgholzer burgholzer left a comment

Choose a reason for hiding this comment

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

Thanks @li-mingbao for all the work on this PR! 🙏🏼
I finally found some time to get to this.
I pushed a couple of commits that cleaned up a couple of smaller details and improved small bits and pieces.
I also have a handful of smaller comments left, which you will find inline. These should be quick to address and I believe this should be ready to go in afterwards.

I have the feeling that the conversions between QC and QCO have accumulated quite some tracking code and data structures that feel a bit much. And I am wondering whether some of the respective code could be simplified. However, these are all concerns for a potential follow-up PR.
Let's get this one in sooner rather than later 😄

Comment on lines +970 to +974
* scf.for %iv = %lb to %ub step %step {
* %q0 = memref.load %memref[%iv] : memref<3x!qc.qubit>
* qc.h %q0 : !qc.qubit
* scf.yield
* }
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.

scf.for has an implicit terminator trait, so I would have guessed that the scf.yield is never printed in valid IR. Am I wrong here?
This also holds for the other builder examples down below.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You are right for scf.for and scf.if. I removed these instances from the docstrings.
In the scf.while operation the yield operation is still printed.

//===--------------------------------------------------------------------===//

/**
* @brief Construct a scf.for operation
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.

I am pretty sure that the correct spelling is "... an scf.<...>" because the pronounciation of scf starts with an "es" sound. Applies everywhere across the entire PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I should have corrected all instances of this.

Comment thread mlir/lib/Dialect/QCO/IR/QCOOps.cpp Outdated
parseIfOpAliasing(OpAsmParser& parser, Region& thenRegion, Region& elseRegion,
SmallVectorImpl<OpAsmParser::UnresolvedOperand>& operands) {
// Parse the qubits keyword
if (parser.parseKeyword("qubits")) {
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.

Is "qubits" still the right keyword here? Especially since this may now also contain qtensor values?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I am not sure what a good word it would be so I decided to replace them with a generic args keyword to represent both qubits and qtensors here. This is inline with other scf operations like iter_args for scf.for.

Comment on lines 1159 to +1164
// Automatically deallocate all still-allocated tensors
for (auto& [tensor, tensorInfo] : validTensors) {
for (auto& [tensor, tensorInfo] : sortedTensors) {
auto currentTensor = tensor;
// Filter out qubits belonging to this tensor
for (auto& [qubit, qubitInfo] : qubitsByRegister[tensorInfo.regId]) {
auto indexValue = constantFromScalar(*this, getLoc(), qubitInfo.regIndex);
currentTensor =
qtensor::InsertOp::create(*this, qubit, currentTensor, indexValue)
.getResult();
currentTensor = qtensor::InsertOp::create(*this, qubit, currentTensor,
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.

There was some sorting of the tensor values here before. I typically avoided such sorting in the conversions as it is not strictly necessary for equivalence and costs time. However, I noticed that one of the tests would sporadically (very rarely fail) specifically the QCOTest.IfOpParser test. Could it be that the module verifier is not yet flexible enough to catch these differences? If so, can we extend it so that this is supported?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think I added the sorting since the IfOpParser test would fail sometimes because it creates 2 different registers and the deallocation order of the registers was not deterministic. So the areModulesEquivalentWithPermutations function would fail.

Comment on lines +981 to +996
target.addDynamicallyLegalDialect<scf::SCFDialect>([](Operation* op) {
// Some types are not converted yet so QC and QCO types have to be checked
auto isQubitType = [](Type t) {
return TypeSwitch<Type, bool>(t)
.Case<qc::QubitType, qco::QubitType>([](auto) { return true; })
.Case<MemRefType>([](MemRefType t) {
return llvm::isa<qc::QubitType>(t.getElementType());
})
.Case<RankedTensorType>([](RankedTensorType t) {
return llvm::isa<qco::QubitType>(t.getElementType());
})
.Default([](auto) { return false; });
};

return !llvm::any_of(op->getOperandTypes(), isQubitType);
});
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.

I have to say that this also confuses me a little bit. Are we missing something elsewhere that makes this necessary?

@burgholzer burgholzer dismissed denialhaag’s stale review May 6, 2026 00:07

Dismissing Daniel's review as stale for now.

@burgholzer burgholzer mentioned this pull request May 6, 2026
11 tasks
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: 3

Caution

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

⚠️ Outside diff range comments (3)
mlir/lib/Dialect/QCO/IR/SCF/IfOp.cpp (1)

246-281: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reinstate branch-yield checks in IfOp::verify().

verify() now only ties block arguments and op results back to the input list. It no longer checks that thenYield() and elseYield() have the same arity and types as the qco.if results, so malformed IR can pass verification and later break the constant-folding path on Lines 133-141 or the QCO→QC lowering, which assumes the carried values line up.

Suggested fix
 LogicalResult IfOp::verify() {
   const auto& inputQubits = getQubits();
   const auto numInputQubits = inputQubits.size();
   const auto& outputQubits = getResults();
   const auto numOutputQubits = outputQubits.size();
@@
   if (numInputQubits != numOutputQubits) {
     return emitOpError("Operation must return the same number of qubits as the "
                        "number of input qubits.");
   }
+  if (thenYield().getOperands().size() != numOutputQubits ||
+      elseYield().getOperands().size() != numOutputQubits) {
+    return emitOpError("Both regions must yield the same number of values as "
+                       "the operation returns.");
+  }
   for (auto [inputQubitType, outputQubitType] :
        llvm::zip_equal(inputQubits.getTypes(), outputQubits.getTypes())) {
     if (inputQubitType != outputQubitType) {
       return emitOpError("Operation must return the same qubit types as its "
                          "input qubit types.");
     }
   }
+  for (auto [yieldType, resultType] :
+       llvm::zip_equal(thenYield().getOperands().getTypes(),
+                       outputQubits.getTypes())) {
+    if (yieldType != resultType) {
+      return emitOpError(
+          "Then-region yield types must match the operation result types.");
+    }
+  }
+  for (auto [yieldType, resultType] :
+       llvm::zip_equal(elseYield().getOperands().getTypes(),
+                       outputQubits.getTypes())) {
+    if (yieldType != resultType) {
+      return emitOpError(
+          "Else-region yield types must match the operation result types.");
+    }
+  }
   SmallPtrSet<Value, 4> uniqueQubitsIn;
   for (auto qubit : inputQubits) {
     if (!uniqueQubitsIn.insert(qubit).second) {
       return emitOpError("Input qubits must be unique.");
     }
🤖 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 246 - 281, IfOp::verify()
removed checks that thenYield() and elseYield() match the op results and input
qubits; restore verification so that thenYield() and elseYield() both exist,
have the same arity as getResults() (and as
getQubits()/thenBlock()->getNumArguments()), and that each corresponding yielded
type equals the corresponding getResults() type (and input qubit type). Also
ensure thenYield() and elseYield() have identical types to each other (same
arity and per-index type equality) and emitOpError() with a clear message when
any of these conditions fail so malformed IR cannot pass verification.
mlir/lib/Conversion/QCToQCO/QCToQCO.cpp (2)

245-259: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't hard-assert on unsupported qubit-memref boundaries.

This helper still aborts when a qubit-bearing memref has no seeded tensor mapping. That is reachable for function/block arguments or call edges carrying memref<...x!qc.qubit>: func-boundary conversion is still out of scope here, so ConvertMemRefLoadOp can hit lookupMappedTensor() before any mapping exists and crash instead of producing a clean conversion failure.

Based on learnings, tensor containers are supported for scf.if/scf.while in this PR, but func operations remain restricted to scalar qubit types only.

🤖 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 245 - 259, The helper
lookupMappedTensor currently aborts via assert when findRegionLocalMap returns
no mapping, which crashes during func/block boundary cases (e.g.,
memref<...x!qc.qubit>) before conversion can fail cleanly; change
lookupMappedTensor to not hard-assert—have it return a null/empty Value (e.g.,
Value()) when tensorMap/tensorValue are missing so callers can detect absence,
then update callers such as ConvertMemRefLoadOp (and any users relying on
lookupMappedTensor/currentModifierFrame) to check the returned Value and emit a
proper conversion failure or diagnostic instead of relying on an assertion; keep
use of findRegionLocalMap and currentModifierFrame but remove the assert and
propagate the empty result.

1116-1127: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Rescan cloned modifier regions to populate SCF-use maps.

collectQubitValuesInsideSCFOps() is called once at the start and keys regionQubitMap / regionRegisterMap by Operation*, but cloneRegionBefore() creates new operation instances for every nested scf.* op inside the cloned region. Those cloned SCF ops therefore have no entries in the pre-computed maps and appear empty to the legality predicate, remaining legal and unconverted. Nested SCF operations carrying qubits or registers inside qc.ctrl / qc.inv bodies will not be rewritten.

♻️ Localized fix

Add a rescan after cloning in both ConvertQCCtrlOp::matchAndRewrite (line 1118) and ConvertQCInvOp::matchAndRewrite (line 1168):

     auto& dstRegion = qcoOp.getRegion();
     rewriter.cloneRegionBefore(op.getRegion(), dstRegion, dstRegion.end());
+    collectQubitValuesInsideSCFOps(qcoOp.getOperation(), &state);
🤖 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 1116 - 1127, The
precomputed maps (regionQubitMap/regionRegisterMap populated by
collectQubitValuesInsideSCFOps) reference original SCF ops, but
rewriter.cloneRegionBefore(...) creates new SCF op instances so the cloned SCF
bodies are missing from those maps; after cloning the region in
ConvertQCCtrlOp::matchAndRewrite and ConvertQCInvOp::matchAndRewrite
(immediately after rewriter.cloneRegionBefore(op.getRegion(), dstRegion,
dstRegion.end())), re-run collectQubitValuesInsideSCFOps on the cloned region to
repopulate regionQubitMap/regionRegisterMap for the new operations so nested
scf.* ops carrying qubits/registers are recognized by the legality predicate
before proceeding (i.e., call the same scan helper with the newly-cloned
ops/region).
♻️ Duplicate comments (1)
mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp (1)

967-984: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Drive scf.while bookkeeping from the emitted scf.condition.

createBody() threads the beforeBody return value into the after region, but the real loop-carried state is whatever scfCondition() put on the terminator. If those ever differ, the builder updates validQubits/validTensors for the wrong values and diverges from the IR.

🔧 Proposed fix
   auto createBody =
       [&](Block* block, function_ref<SmallVector<Value>(ValueRange)> body,
           ValueRange innerInitArgs, bool createYield) -> SmallVector<Value> {
     auto blockArgs = block->getArguments();
     // Update the qubit values to the block args
     updateQubitValueTracking(innerInitArgs, blockArgs);
     // Construct the body
     const auto& results = body(blockArgs);

     if (results.size() != innerInitArgs.size()) {
       llvm::reportFatalUsageError(
           "scf.while body must return exactly one value per iter arg");
     }
     if (createYield) {
       scf::YieldOp::create(*this, results);
-    }
-    return results;
+      return SmallVector<Value>(results.begin(), results.end());
+    }
+
+    auto condOp = dyn_cast<scf::ConditionOp>(block->getTerminator());
+    if (!condOp) {
+      llvm::reportFatalUsageError(
+          "scf.while beforeBody must terminate with scf.condition");
+    }
+    if (condOp.getArgs().size() != innerInitArgs.size()) {
+      llvm::reportFatalUsageError(
+          "scf.condition must yield exactly one value per iter arg");
+    }
+    return SmallVector<Value>(condOp.getArgs().begin(), condOp.getArgs().end());
   };

Also applies to: 1048-1064

🤖 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 967 - 984,
The loop body builder createBody currently uses the body() return (the
`results`/`beforeBody`) to update qubit/tensor tracking and to emit the
scf::YieldOp, but the true loop-carried values come from the terminator produced
by scfCondition(); update createBody to accept and use the values returned by
scfCondition (the condition terminator results) when calling
updateQubitValueTracking and when creating the scf::YieldOp (i.e., thread the
scfCondition terminator values into the `after` region instead of using the
body() results), so validQubits/validTensors are updated from the scfCondition
outputs rather than from `results`/`beforeBody`. Ensure you preserve the size
checks (results.size() vs innerInitArgs.size()) but base bookkeeping and the
yielded values on the scfCondition outputs.
🤖 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/lib/Conversion/QCOToQC/QCOToQC.cpp`:
- Around line 851-852: In ConvertQCOIfOp and ConvertQCOSCFConditionOp replace
uses of op.getCondition() with the remapped operand adaptor.getCondition() when
constructing the new SCF ops (e.g. the scf::IfOp::create call in ConvertQCOIfOp
and the scf::condition creation in ConvertQCOSCFConditionOp), and
restore/un-comment the adaptor parameter in ConvertQCOSCFConditionOp so you can
call adaptor.getCondition(); this ensures the recreated scf ops use the adapted
(converted) SSA value produced by the OpAdaptor rather than the original
op.getCondition().

In `@mlir/unittests/programs/qco_programs.cpp`:
- Around line 2077-2088: Extend the ifOneQubitOneTensor test to include a
non-trivial else branch for b.qcoIf that returns the same mixed qubit+tensor
result types as the then branch (use qtensorExtract/qtensorInsert on the tensor
operand in the else path and perform an operation on the qubit operand in that
branch), and add assertions after lowering to verify the physical qubit identity
is preserved across then/else (i.e., the qubit Value produced by both branches
refers to the same underlying resource); update the usage sites in
ifOneQubitOneTensor (references: qcoIf, qtensorExtract, qtensorInsert,
allocQubitRegister, measure) so both branches return matching SmallVector shapes
and include checks that compare the qubit Values for identity after the
transformation.

---

Outside diff comments:
In `@mlir/lib/Conversion/QCToQCO/QCToQCO.cpp`:
- Around line 245-259: The helper lookupMappedTensor currently aborts via assert
when findRegionLocalMap returns no mapping, which crashes during func/block
boundary cases (e.g., memref<...x!qc.qubit>) before conversion can fail cleanly;
change lookupMappedTensor to not hard-assert—have it return a null/empty Value
(e.g., Value()) when tensorMap/tensorValue are missing so callers can detect
absence, then update callers such as ConvertMemRefLoadOp (and any users relying
on lookupMappedTensor/currentModifierFrame) to check the returned Value and emit
a proper conversion failure or diagnostic instead of relying on an assertion;
keep use of findRegionLocalMap and currentModifierFrame but remove the assert
and propagate the empty result.
- Around line 1116-1127: The precomputed maps (regionQubitMap/regionRegisterMap
populated by collectQubitValuesInsideSCFOps) reference original SCF ops, but
rewriter.cloneRegionBefore(...) creates new SCF op instances so the cloned SCF
bodies are missing from those maps; after cloning the region in
ConvertQCCtrlOp::matchAndRewrite and ConvertQCInvOp::matchAndRewrite
(immediately after rewriter.cloneRegionBefore(op.getRegion(), dstRegion,
dstRegion.end())), re-run collectQubitValuesInsideSCFOps on the cloned region to
repopulate regionQubitMap/regionRegisterMap for the new operations so nested
scf.* ops carrying qubits/registers are recognized by the legality predicate
before proceeding (i.e., call the same scan helper with the newly-cloned
ops/region).

In `@mlir/lib/Dialect/QCO/IR/SCF/IfOp.cpp`:
- Around line 246-281: IfOp::verify() removed checks that thenYield() and
elseYield() match the op results and input qubits; restore verification so that
thenYield() and elseYield() both exist, have the same arity as getResults() (and
as getQubits()/thenBlock()->getNumArguments()), and that each corresponding
yielded type equals the corresponding getResults() type (and input qubit type).
Also ensure thenYield() and elseYield() have identical types to each other (same
arity and per-index type equality) and emitOpError() with a clear message when
any of these conditions fail so malformed IR cannot pass verification.

---

Duplicate comments:
In `@mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp`:
- Around line 967-984: The loop body builder createBody currently uses the
body() return (the `results`/`beforeBody`) to update qubit/tensor tracking and
to emit the scf::YieldOp, but the true loop-carried values come from the
terminator produced by scfCondition(); update createBody to accept and use the
values returned by scfCondition (the condition terminator results) when calling
updateQubitValueTracking and when creating the scf::YieldOp (i.e., thread the
scfCondition terminator values into the `after` region instead of using the
body() results), so validQubits/validTensors are updated from the scfCondition
outputs rather than from `results`/`beforeBody`. Ensure you preserve the size
checks (results.size() vs innerInitArgs.size()) but base bookkeeping and the
yielded values on the scfCondition outputs.
🪄 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: 385c3c14-c66e-4b6c-bc5a-6d696f32d1bc

📥 Commits

Reviewing files that changed from the base of the PR and between b0b6bb1 and aa77eed.

📒 Files selected for processing (14)
  • CHANGELOG.md
  • 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/lib/Conversion/QCOToJeff/QCOToJeff.cpp
  • mlir/lib/Conversion/QCOToQC/QCOToQC.cpp
  • mlir/lib/Conversion/QCToQCO/QCToQCO.cpp
  • mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp
  • mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.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
  • mlir/unittests/programs/qco_programs.cpp
  • mlir/unittests/programs/qco_programs.h

Comment thread mlir/lib/Conversion/QCOToQC/QCOToQC.cpp Outdated
Comment thread mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp
Comment on lines +2077 to +2088
void ifOneQubitOneTensor(QCOProgramBuilder& b) {
auto q = b.allocQubitRegister(1);
auto t0 = b.allocQubitRegister(1);
auto q0 = b.h(q[0]);
auto [measuredQubit, measureResult] = b.measure(q0);
b.qcoIf(measureResult, {measuredQubit, t0.value}, [&](ValueRange qubits) {
auto innerQubit0 = b.x(qubits[0]);
auto [t1, innerQubit1] = b.qtensorExtract(qubits[1], 0);
auto innerQubit2 = b.x(innerQubit1);
auto t2 = b.qtensorInsert(innerQubit2, t1, 0);
return SmallVector{innerQubit0, t2};
});
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.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add a non-trivial else-branch variant for mixed qubit/tensor qco.if.

This helper only exercises the then region. The new lowering drops qco.if results and relies on both branches preserving the same underlying resource, so a mixed qubit+tensor case with an active else branch would give much better coverage for the new tensor support.

Based on learnings: "for scf.if with qubit operands, both then and else branches yield the same physical qubits (resources) after conversion ... Add checks (assertions or tests) to verify qubit identity is preserved across branches when performing the transformation."

🧰 Tools
🪛 Cppcheck (2.20.0)

[style] 2077-2077: The function 'ifOneQubitOneTensor' is never used.

(unusedFunction)

🤖 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/unittests/programs/qco_programs.cpp` around lines 2077 - 2088, Extend
the ifOneQubitOneTensor test to include a non-trivial else branch for b.qcoIf
that returns the same mixed qubit+tensor result types as the then branch (use
qtensorExtract/qtensorInsert on the tensor operand in the else path and perform
an operation on the qubit operand in that branch), and add assertions after
lowering to verify the physical qubit identity is preserved across then/else
(i.e., the qubit Value produced by both branches refers to the same underlying
resource); update the usage sites in ifOneQubitOneTensor (references: qcoIf,
qtensorExtract, qtensorInsert, allocQubitRegister, measure) so both branches
return matching SmallVector shapes and include checks that compare the qubit
Values for identity after the transformation.

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

Labels

feature New feature or request MLIR Anything related to MLIR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants