Skip to content

Commit 0b81e9c

Browse files
committed
✨ Enhance QubitType to support static and dynamic qubits, updating related operations and conversions in QCO and QC dialects. Add tests for static qubit type propagation and ensure correct handling in various operations.
1 parent c2d414f commit 0b81e9c

14 files changed

Lines changed: 326 additions & 112 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ This project adheres to [Semantic Versioning], with the exception that minor rel
1515
- ✨ Add conversions between Jeff and QCO ([#1479], [#1548], [#1565]) ([**@denialhaag**])
1616
- ✨ Add a `place-and-route` pass for mapping circuits to architectures with restricted topologies ([#1537], [#1547], [#1568]) ([**@MatthiasReumann**])
1717
- ✨ Add initial infrastructure for new QC and QCO MLIR dialects
18-
([#1264], [#1330], [#1402], [#1428], [#1430], [#1436], [#1443], [#1446], [#1464], [#1465], [#1470], [#1471], [#1472], [#1474], [#1475], [#1506], [#1510], [#1513], [#1521], [#1542], [#1548], [#1550], [#1554], [#1570])
18+
([#1264], [#1330], [#1402], [#1428], [#1430], [#1436], [#1443], [#1446], [#1464], [#1465], [#1470], [#1471], [#1472], [#1474], [#1475], [#1506], [#1510], [#1513], [#1521], [#1542], [#1548], [#1550], [#1554], [#1569], [#1570])
1919
([**@burgholzer**], [**@denialhaag**], [**@taminob**], [**@DRovara**], [**@li-mingbao**], [**@Ectras**], [**@MatthiasReumann**], [**@simon1hofmann**])
2020

2121
### Changed
@@ -335,6 +335,7 @@ _📚 Refer to the [GitHub Release Notes](https://github.com/munich-quantum-tool
335335

336336
[#1571]: https://github.com/munich-quantum-toolkit/core/pull/1571
337337
[#1570]: https://github.com/munich-quantum-toolkit/core/pull/1570
338+
[#1569]: https://github.com/munich-quantum-toolkit/core/pull/1569
338339
[#1568]: https://github.com/munich-quantum-toolkit/core/pull/1568
339340
[#1565]: https://github.com/munich-quantum-toolkit/core/pull/1565
340341
[#1564]: https://github.com/munich-quantum-toolkit/core/pull/1564

mlir/include/mlir/Dialect/QC/IR/QCOps.td

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,18 +78,24 @@ def AllocOp : QCOp<"alloc", [MemoryEffects<[MemAlloc]>]> {
7878
let hasVerifier = 1;
7979
}
8080

81+
def DynamicQubit : Type<
82+
And<[CPred<"::mlir::isa<::mlir::qc::QubitType>($_self)">,
83+
CPred<"!::mlir::cast<::mlir::qc::QubitType>($_self).getIsStatic()">]>,
84+
"dynamic qubit type (!qc.qubit)">;
85+
8186
def DeallocOp : QCOp<"dealloc", [MemoryEffects<[MemFree]>]> {
82-
let summary = "Deallocate a qubit";
87+
let summary = "Deallocate a dynamically allocated qubit";
8388
let description = [{
84-
Deallocates a qubit, releasing its resources.
89+
Deallocates a dynamically allocated qubit, releasing its resources.
90+
Static qubits (`!qc.qubit<static>`) cannot be deallocated.
8591

8692
Example:
8793
```mlir
8894
qc.dealloc %q : !qc.qubit
8995
```
9096
}];
9197

92-
let arguments = (ins QubitType:$qubit);
98+
let arguments = (ins DynamicQubit:$qubit);
9399
let assemblyFormat = "$qubit attr-dict `:` type($qubit)";
94100

95101
let hasCanonicalizer = 1;
@@ -104,13 +110,20 @@ def StaticOp : QCOp<"static", [Pure]> {
104110

105111
Example:
106112
```mlir
107-
%q = qc.static 0 : !qc.qubit
113+
%q = qc.static 0 : !qc.qubit<static>
108114
```
109115
}];
110116

111117
let arguments = (ins ConfinedAttr<I64Attr, [IntNonNegative]>:$index);
112118
let results = (outs QubitType:$qubit);
113119
let assemblyFormat = "$index attr-dict `:` type($qubit)";
120+
121+
let builders = [
122+
OpBuilder<(ins "int64_t":$index), [{
123+
build($_builder, $_state, QubitType::get($_builder.getContext(), /*isStatic=*/true),
124+
$_builder.getI64IntegerAttr(index));
125+
}]>
126+
];
114127
}
115128

116129
//===----------------------------------------------------------------------===//

mlir/include/mlir/Dialect/QCO/IR/QCOOps.td

Lines changed: 85 additions & 33 deletions
Large diffs are not rendered by default.

mlir/lib/Conversion/QCOToQC/QCOToQC.cpp

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ class QCOToQCTypeConverter final : public TypeConverter {
5656
// Identity conversion for all types by default
5757
addConversion([](Type type) { return type; });
5858

59-
// Convert QCO qubit values to QC qubit references
60-
addConversion([ctx](qco::QubitType /*type*/) -> Type {
61-
return qc::QubitType::get(ctx);
59+
// Convert QCO qubit values to QC qubit references, preserving isStatic
60+
addConversion([ctx](qco::QubitType type) -> Type {
61+
return qc::QubitType::get(ctx, type.getIsStatic());
6262
});
6363
}
6464
};
@@ -97,36 +97,25 @@ struct ConvertQCOAllocOp final : OpConversionPattern<qco::AllocOp> {
9797
};
9898

9999
/**
100-
* @brief Converts qco.dealloc to qc.dealloc (except for static qubits).
100+
* @brief Converts qco.dealloc to qc.dealloc (dynamic) or erases it (static).
101101
*
102102
* @details
103-
* Deallocates a qubit, releasing its resources. The OpAdaptor automatically
104-
* provides the type-converted qubit operand (!qc.qubit instead of
105-
* !qco.qubit), so we simply pass it through to the new operation.
106-
*
107-
* Example transformation:
108-
* ```mlir
109-
* qco.dealloc %q_qco : !qco.qubit
110-
* // becomes:
111-
* qc.dealloc %q_qc : !qc.qubit
112-
* ```
113-
*
114-
* For static qubits, we erase `qco.dealloc` because QC does
115-
* not require explicit sinks for static qubit handles.
103+
* For dynamic qubits (`!qco.qubit`), lowers to `qc.dealloc`.
104+
* For static qubits (`!qco.qubit<static>`), erases the op since QC does not
105+
* require explicit deallocation of static qubits.
116106
*/
117107
struct ConvertQCODeallocOp final : OpConversionPattern<qco::DeallocOp> {
118108
using OpConversionPattern::OpConversionPattern;
119109

120110
LogicalResult
121111
matchAndRewrite(qco::DeallocOp op, OpAdaptor adaptor,
122112
ConversionPatternRewriter& rewriter) const override {
123-
auto qcQubit = adaptor.getQubit();
124-
if (llvm::isa_and_nonnull<qc::StaticOp>(qcQubit.getDefiningOp())) {
113+
if (op.getQubit().getType().getIsStatic()) {
125114
rewriter.eraseOp(op);
126115
return success();
127116
}
128117

129-
rewriter.replaceOpWithNewOp<qc::DeallocOp>(op, qcQubit);
118+
rewriter.replaceOpWithNewOp<qc::DeallocOp>(op, adaptor.getQubit());
130119
return success();
131120
}
132121
};

mlir/lib/Conversion/QCToQCO/QCToQCO.cpp

Lines changed: 50 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -129,42 +129,55 @@ struct ConvertFuncReturnOp final : StatefulOpConversionPattern<func::ReturnOp> {
129129
ConversionPatternRewriter& rewriter) const override {
130130
auto& state = getState();
131131

132-
// Only insert sinks at function exit (never inside nested modifier
133-
// regions).
134-
if (state.inNestedRegion == 0) {
135-
llvm::SmallVector<Value> liveQubits;
136-
liveQubits.reserve(state.qubitMap.size());
132+
if (state.inNestedRegion != 0) {
133+
rewriter.replaceOpWithNewOp<func::ReturnOp>(op, adaptor.getOperands());
134+
return success();
135+
}
137136

138-
llvm::DenseSet<Value> escapedQubits;
139-
for (Value returned : adaptor.getOperands()) {
140-
escapedQubits.insert(returned);
137+
// Build return values from qubitMap (adaptor.getOperands() may carry stale
138+
// root values because gate patterns use eraseOp instead of replaceOp).
139+
llvm::SmallVector<Value> returnValues;
140+
llvm::DenseSet<Value> escapedQubits;
141+
returnValues.reserve(op.getNumOperands());
142+
for (auto [qcOperand, adaptorOperand] :
143+
llvm::zip(op.getOperands(), adaptor.getOperands())) {
144+
if (state.qubitMap.contains(qcOperand)) {
145+
auto latest = state.qubitMap[qcOperand];
146+
returnValues.push_back(latest);
147+
escapedQubits.insert(latest);
148+
} else {
149+
returnValues.push_back(adaptorOperand);
141150
}
151+
}
142152

143-
llvm::DenseSet<Value> seen;
144-
for (const auto& [/*qcQubit*/ _, qcoQubit] : state.qubitMap) {
145-
if (!escapedQubits.contains(qcoQubit) && seen.insert(qcoQubit).second) {
146-
liveQubits.push_back(qcoQubit);
147-
}
153+
// Collect non-escaped live qubits for deallocation.
154+
llvm::SmallVector<Value> liveQubits;
155+
liveQubits.reserve(state.qubitMap.size());
156+
llvm::DenseSet<Value> seen;
157+
for (const auto& [qcQubit, qcoQubit] : state.qubitMap) {
158+
if (escapedQubits.contains(qcoQubit) || !seen.insert(qcoQubit).second) {
159+
continue;
148160
}
161+
liveQubits.emplace_back(qcoQubit);
162+
}
149163

150-
// Sort deterministically (mirrors QCOProgramBuilder::finalize()).
151-
llvm::sort(liveQubits, [](Value a, Value b) {
152-
auto* opA = a.getDefiningOp();
153-
auto* opB = b.getDefiningOp();
154-
if (!opA || !opB || opA->getBlock() != opB->getBlock()) {
155-
return a.getAsOpaquePointer() < b.getAsOpaquePointer();
156-
}
157-
return opA->isBeforeInBlock(opB);
158-
});
159-
160-
for (auto qubit : liveQubits) {
161-
rewriter.create<qco::DeallocOp>(op.getLoc(), qubit);
164+
// Sort deterministically (mirrors QCOProgramBuilder::finalize()).
165+
llvm::sort(liveQubits, [](Value a, Value b) {
166+
auto* opA = a.getDefiningOp();
167+
auto* opB = b.getDefiningOp();
168+
if (!opA || !opB || opA->getBlock() != opB->getBlock()) {
169+
return a.getAsOpaquePointer() < b.getAsOpaquePointer();
162170
}
171+
return opA->isBeforeInBlock(opB);
172+
});
163173

164-
state.qubitMap.clear();
174+
for (Value qubit : liveQubits) {
175+
rewriter.create<qco::DeallocOp>(op.getLoc(), qubit);
165176
}
166177

167-
rewriter.replaceOpWithNewOp<func::ReturnOp>(op, adaptor.getOperands());
178+
state.qubitMap.clear();
179+
180+
rewriter.replaceOpWithNewOp<func::ReturnOp>(op, returnValues);
168181
return success();
169182
}
170183
};
@@ -186,9 +199,9 @@ class QCToQCOTypeConverter final : public TypeConverter {
186199
// Identity conversion for all types by default
187200
addConversion([](Type type) { return type; });
188201

189-
// Convert QC qubit references to QCO qubit values
190-
addConversion([ctx](qc::QubitType /*type*/) -> Type {
191-
return qco::QubitType::get(ctx);
202+
// Convert QC qubit references to QCO qubit values, preserving isStatic
203+
addConversion([ctx](qc::QubitType type) -> Type {
204+
return qco::QubitType::get(ctx, type.getIsStatic());
192205
});
193206
}
194207
};
@@ -1008,11 +1021,11 @@ struct ConvertQCCtrlOp final : StatefulOpConversionPattern<qc::CtrlOp> {
10081021
"QC ctrl region unexpectedly has entry block arguments");
10091022
SmallVector<Value> qcoTargetAliases;
10101023
qcoTargetAliases.reserve(numTargets);
1011-
const auto qubitType = qco::QubitType::get(qcoOp.getContext());
10121024
const auto opLoc = op.getLoc();
10131025
rewriter.modifyOpInPlace(qcoOp, [&] {
10141026
for (auto i = 0UL; i < numTargets; i++) {
1015-
qcoTargetAliases.emplace_back(entryBlock.addArgument(qubitType, opLoc));
1027+
qcoTargetAliases.emplace_back(
1028+
entryBlock.addArgument(qcoTargets[i].getType(), opLoc));
10161029
}
10171030
});
10181031
state.targetsIn[state.inNestedRegion] = std::move(qcoTargetAliases);
@@ -1092,11 +1105,11 @@ struct ConvertQCInvOp final : StatefulOpConversionPattern<qc::InvOp> {
10921105
"QC inv region unexpectedly has entry block arguments");
10931106
SmallVector<Value> qcoTargetAliases;
10941107
qcoTargetAliases.reserve(numTargets);
1095-
const auto qubitType = qco::QubitType::get(qcoOp.getContext());
10961108
const auto opLoc = op.getLoc();
10971109
rewriter.modifyOpInPlace(qcoOp, [&] {
10981110
for (auto i = 0UL; i < numTargets; i++) {
1099-
qcoTargetAliases.emplace_back(entryBlock.addArgument(qubitType, opLoc));
1111+
qcoTargetAliases.emplace_back(
1112+
entryBlock.addArgument(qcoTargets[i].getType(), opLoc));
11001113
}
11011114
});
11021115
targetsIn[inNestedRegion] = std::move(qcoTargetAliases);
@@ -1218,9 +1231,9 @@ struct QCToQCO final : impl::QCToQCOBase<QCToQCO> {
12181231
// Conversion of qc types in func.return
12191232
//
12201233
// Note: `func.return` may already be type-legal even though we still need
1221-
// to insert `qco.dealloc` sinks for remaining live qubits. Therefore, we
1222-
// make it dynamically illegal unless the lowering state has no remaining
1223-
// qubits.
1234+
// to insert sink operations (`qco.dealloc`) for remaining live
1235+
// qubits. Therefore, we make it dynamically illegal unless the lowering
1236+
// state has no remaining qubits.
12241237
patterns.add<ConvertFuncReturnOp>(typeConverter, context, &state);
12251238
populateReturnOpTypeConversionPattern(patterns, typeConverter);
12261239
target.addDynamicallyLegalOp<func::ReturnOp>([&](const func::ReturnOp op) {

mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,7 @@ Value QCProgramBuilder::staticQubit(const int64_t index) {
8686
llvm::reportFatalUsageError("Index must be non-negative");
8787
}
8888

89-
// Create the StaticOp with the given index
90-
auto indexAttr = getI64IntegerAttr(index);
91-
auto staticOp = StaticOp::create(*this, indexAttr);
89+
auto staticOp = StaticOp::create(*this, index);
9290
return staticOp.getQubit();
9391
}
9492

mlir/lib/Dialect/QC/IR/QCOps.cpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,24 @@ void QCDialect::initialize() {
4646
// Types
4747
//===----------------------------------------------------------------------===//
4848

49+
/// Print `!qc.qubit` (dynamic, default) or `!qc.qubit<static>`.
50+
void QubitType::print(AsmPrinter& printer) const {
51+
if (getIsStatic()) {
52+
printer << "<static>";
53+
}
54+
}
55+
56+
/// Parse `!qc.qubit` or `!qc.qubit<static>`.
57+
Type QubitType::parse(AsmParser& parser) {
58+
if (succeeded(parser.parseOptionalLess())) {
59+
if (parser.parseKeyword("static") || parser.parseGreater()) {
60+
return {};
61+
}
62+
return get(parser.getContext(), /*isStatic=*/true);
63+
}
64+
return get(parser.getContext(), /*isStatic=*/false);
65+
}
66+
4967
#define GET_TYPEDEF_CLASSES
5068
#include "mlir/Dialect/QC/IR/QCOpsTypes.cpp.inc"
5169

mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,7 @@ Value QCOProgramBuilder::staticQubit(const int64_t index) {
9090
llvm::reportFatalUsageError("Index must be non-negative");
9191
}
9292

93-
auto indexAttr = getI64IntegerAttr(index);
94-
auto staticOp = StaticOp::create(*this, indexAttr);
93+
auto staticOp = StaticOp::create(*this, index);
9594
const auto qubit = staticOp.getQubit();
9695

9796
// Track the static qubit as valid
@@ -747,9 +746,8 @@ std::pair<ValueRange, ValueRange> QCOProgramBuilder::ctrl(
747746

748747
auto ctrlOp = CtrlOp::create(*this, controls, targets);
749748
auto& block = ctrlOp.getBodyRegion().emplaceBlock();
750-
const auto qubitType = QubitType::get(getContext());
751749
for (const auto target : targets) {
752-
const auto arg = block.addArgument(qubitType, getLoc());
750+
const auto arg = block.addArgument(target.getType(), getLoc());
753751
updateQubitTracking(target, arg);
754752
}
755753
const InsertionGuard guard(*this);
@@ -785,9 +783,8 @@ ValueRange QCOProgramBuilder::inv(
785783

786784
// Add block arguments for all qubits
787785
auto& block = invOp.getBodyRegion().emplaceBlock();
788-
const auto qubitType = QubitType::get(getContext());
789786
for (const auto qubit : qubits) {
790-
const auto arg = block.addArgument(qubitType, getLoc());
787+
const auto arg = block.addArgument(qubit.getType(), getLoc());
791788
updateQubitTracking(qubit, arg);
792789
}
793790

mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -242,9 +242,8 @@ void CtrlOp::build(
242242
build(odsBuilder, odsState, controls, targets);
243243
auto& block = odsState.regions.front()->emplaceBlock();
244244

245-
const auto qubitType = QubitType::get(odsBuilder.getContext());
246245
for (size_t i = 0; i < targets.size(); ++i) {
247-
block.addArgument(qubitType, odsState.location);
246+
block.addArgument(targets[i].getType(), odsState.location);
248247
}
249248

250249
const OpBuilder::InsertionGuard guard(odsBuilder);
@@ -263,9 +262,8 @@ LogicalResult CtrlOp::verify() {
263262
return emitOpError(
264263
"number of block arguments must match the number of targets");
265264
}
266-
const auto qubitType = QubitType::get(getContext());
267265
for (size_t i = 0; i < numTargets; ++i) {
268-
if (block.getArgument(i).getType() != qubitType) {
266+
if (block.getArgument(i).getType() != getTargetsIn()[i].getType()) {
269267
return emitOpError("block argument type at index ")
270268
<< i << " does not match target type";
271269
}

mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -326,9 +326,8 @@ void InvOp::build(
326326
build(odsBuilder, odsState, qubits);
327327
auto& block = odsState.regions.front()->emplaceBlock();
328328

329-
const auto qubitType = QubitType::get(odsBuilder.getContext());
330329
for (size_t i = 0; i < qubits.size(); ++i) {
331-
block.addArgument(qubitType, odsState.location);
330+
block.addArgument(qubits[i].getType(), odsState.location);
332331
}
333332

334333
const OpBuilder::InsertionGuard guard(odsBuilder);
@@ -347,9 +346,8 @@ LogicalResult InvOp::verify() {
347346
return emitOpError(
348347
"number of block arguments must match the number of targets");
349348
}
350-
const auto qubitType = QubitType::get(getContext());
351349
for (size_t i = 0; i < numTargets; ++i) {
352-
if (block.getArgument(i).getType() != qubitType) {
350+
if (block.getArgument(i).getType() != getQubitsIn()[i].getType()) {
353351
return emitOpError("block argument type at index ")
354352
<< i << " does not match target type";
355353
}

0 commit comments

Comments
 (0)