✨ Add SCF conversions between QC and QCO dialects#1638
Conversation
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>
# Conflicts: # CHANGELOG.md
burgholzer
left a comment
There was a problem hiding this comment.
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 😄
Dismissing Daniel's review as stale for now.
There was a problem hiding this comment.
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 winReinstate branch-yield checks in
IfOp::verify().
verify()now only ties block arguments and op results back to the input list. It no longer checks thatthenYield()andelseYield()have the same arity and types as theqco.ifresults, 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 winDon'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, soConvertMemRefLoadOpcan hitlookupMappedTensor()before any mapping exists and crash instead of producing a clean conversion failure.Based on learnings, tensor containers are supported for
scf.if/scf.whilein this PR, butfuncoperations 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 winRescan cloned modifier regions to populate SCF-use maps.
collectQubitValuesInsideSCFOps()is called once at the start and keysregionQubitMap/regionRegisterMapbyOperation*, butcloneRegionBefore()creates new operation instances for every nestedscf.*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 insideqc.ctrl/qc.invbodies will not be rewritten.♻️ Localized fix
Add a rescan after cloning in both
ConvertQCCtrlOp::matchAndRewrite(line 1118) andConvertQCInvOp::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 winDrive
scf.whilebookkeeping from the emittedscf.condition.
createBody()threads thebeforeBodyreturn value into theafterregion, but the real loop-carried state is whateverscfCondition()put on the terminator. If those ever differ, the builder updatesvalidQubits/validTensorsfor 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
📒 Files selected for processing (14)
CHANGELOG.mdmlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.hmlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.hmlir/include/mlir/Dialect/QCO/IR/QCOOps.tdmlir/lib/Conversion/QCOToJeff/QCOToJeff.cppmlir/lib/Conversion/QCOToQC/QCOToQC.cppmlir/lib/Conversion/QCToQCO/QCToQCO.cppmlir/lib/Dialect/QC/Builder/QCProgramBuilder.cppmlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cppmlir/lib/Dialect/QCO/IR/QCOOps.cppmlir/lib/Dialect/QCO/IR/SCF/IfOp.cppmlir/unittests/Dialect/QCO/IR/test_qco_ir.cppmlir/unittests/programs/qco_programs.cppmlir/unittests/programs/qco_programs.h
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp (1)
569-571:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReject
scfCondition()outsidescfWhile's before-region.This helper can still emit
scf.conditionin the main block, in anafterregion, or insidescf.if, which lets the public builder API create malformed IR beforescfWhile()ever validates it. The parent/region check discussed earlier still needs to live here, and the same guard should be mirrored inQCOProgramBuilder::scfCondition.Suggested fix
QCProgramBuilder& QCProgramBuilder::scfCondition(Value condition) { checkFinalized(); + auto* block = getInsertionBlock(); + auto* parentOp = block ? block->getParentOp() : nullptr; + if (!parentOp || !isa<scf::WhileOp>(parentOp) || + &cast<scf::WhileOp>(parentOp).getBefore().front() != block) { + llvm::reportFatalUsageError( + "scfCondition can only be emitted in the before region of scfWhile"); + } scf::ConditionOp::create(*this, condition, ValueRange{}); 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 569 - 571, Ensure QCProgramBuilder::scfCondition validates the insertion region is the before-region of an scf::WhileOp before emitting scf::ConditionOp: obtain the current insertion block (or parent op), find the nearest scf::WhileOp (e.g., getParentOfType<scf::WhileOp>()), check that the insertion block/region equals the while op's before-region (or whileOp.getBefore().front()), and bail/emit an error/assert if not; only then call scf::ConditionOp::create(...) — apply the same guard and logic to QCOProgramBuilder::scfCondition so both builders prevent emitting scf.condition outside the while before-region.
🤖 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/Dialect/QC/Builder/QCProgramBuilder.cpp`:
- Around line 145-156: The guard currently keys loadedQubits by the SSA Value
(index), which misses semantically-equal constant indices; change the logic to
canonicalize the slot index to a semantic integer before checking/inserting:
resolve the index Value to an integer slot id (e.g., detect arith.constant Index
ops and extract the IntegerAttr/getZExtValue, else fall back to a stable
non-constant key) and use that semantic id when checking
loadedQubits[curr][memref].contains(...) and when inserting into
loadedQubits[region][memref].insert(...); update the checks around regionStack,
the duplicate-load check, and the code that creates the load
(memref::LoadOp::create) so all use the same resolved slot id (also ensure
allocQubitRegister seeding uses the same canonicalization).
In `@mlir/unittests/programs/qco_programs.cpp`:
- Around line 2231-2237: Replace the non-obvious SmallVector construction that
takes a ValueRange in the second lambda by explicitly converting the ValueRange
to a vector; specifically, in the lambda capturing ValueRange iterArgs (the one
that currently returns SmallVector<Value>{iterArgs}), return
llvm::to_vector(iterArgs) (or construct SmallVector from iterArgs.begin()/end())
so the conversion from ValueRange to a SmallVector<Value> is explicit and uses
LLVM helpers.
---
Duplicate comments:
In `@mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp`:
- Around line 569-571: Ensure QCProgramBuilder::scfCondition validates the
insertion region is the before-region of an scf::WhileOp before emitting
scf::ConditionOp: obtain the current insertion block (or parent op), find the
nearest scf::WhileOp (e.g., getParentOfType<scf::WhileOp>()), check that the
insertion block/region equals the while op's before-region (or
whileOp.getBefore().front()), and bail/emit an error/assert if not; only then
call scf::ConditionOp::create(...) — apply the same guard and logic to
QCOProgramBuilder::scfCondition so both builders prevent emitting scf.condition
outside the while before-region.
🪄 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: 8e27c37f-9dd3-482a-8ae1-987e07fc6f3f
📒 Files selected for processing (6)
CHANGELOG.mdmlir/lib/Conversion/QCOToQC/QCOToQC.cppmlir/lib/Dialect/QC/Builder/QCProgramBuilder.cppmlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cppmlir/unittests/programs/qc_programs.cppmlir/unittests/programs/qco_programs.cpp
There was a problem hiding this comment.
Thanks for all the hard work here @li-mingbao 🙏🏼
This LGTM now 🎉
I'll get this in once #1694 is in. 🚀
Edit: Screw that. CodeRabbit requested further changes there. Getting this in first.
Description
This PR adds support for the conversion of the
scfoperationsscf.while,scf.for,scf.ifbetween theQCand theQCOdialect. 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:
scfoperations.qtensor.castoperation first.scf.ifis converted intoqco.ifqco.ifalso supports tensors of qubits now as input types.qco.yieldalso supports tensors of qubits now as input types.Checklist:
If PR contains AI-assisted content:
Assisted-by: [Model Name] via [Tool Name]footer.