From 8fca6fac38ab4fd37ee7361c1e0fde1fe97dd559 Mon Sep 17 00:00:00 2001 From: Damian Rovara Date: Fri, 28 Aug 2026 12:22:17 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20Derive=20linear-value=20corresp?= =?UTF-8?q?ondence=20across=20calls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thread qubit and tensor arguments through supported callees and fail closed when their correspondence cannot be derived. Assisted-by: Claude Opus 5 Assisted-by: Codex --- CHANGELOG.md | 8 +- .../mlir/Dialect/QCO/Utils/WireIterator.h | 77 +++++-- .../Dialect/QTensor/Utils/TensorIterator.h | 47 +++- mlir/lib/Dialect/QCO/Utils/CMakeLists.txt | 1 + mlir/lib/Dialect/QCO/Utils/WireIterator.cpp | 214 +++++++++++++++++- .../Dialect/QTensor/Utils/TensorIterator.cpp | 151 ++++++++++++ 6 files changed, 471 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a797ebaa65..6d19fb6511 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,9 +25,10 @@ releases may include breaking changes. [#1807], [#1808], [#1815], [#1824], [#1869], [#1872], [#1914], [#1925], [#1927], [#1935], [#1936], [#1938], [#1975], [#1976], [#2006], [#2014], [#2015], [#2017], [#2026], [#2028], [#2054], [#2058], [#2125], [#2136], - [#2149], [#2150], [#2158], [#2210], [#2211], [#2220]) ([**@burgholzer**], - [**@denialhaag**], [**@taminob**], [**@DRovara**], [**@li-mingbao**], - [**@Ectras**], [**@MatthiasReumann**], [**@simon1hofmann**], [**@J4MMlE**]) + [#2149], [#2150], [#2158], [#2194], [#2210], [#2211], [#2220]) + ([**@burgholzer**], [**@denialhaag**], [**@taminob**], [**@DRovara**], + [**@li-mingbao**], [**@Ectras**], [**@MatthiasReumann**], + [**@simon1hofmann**], [**@J4MMlE**]) - ✨ Add a library for typed structured quantum benchmarks with versioned instance specifications, analytic references, deterministic manifests, and C++, Python, and command-line interfaces ([#2135], [#2315]) @@ -889,6 +890,7 @@ for previous changelogs._ [#2216]: https://github.com/munich-quantum-toolkit/core/pull/2216 [#2203]: https://github.com/munich-quantum-toolkit/core/pull/2203 [#2214]: https://github.com/munich-quantum-toolkit/core/pull/2214 +[#2194]: https://github.com/munich-quantum-toolkit/core/pull/2194 [#2193]: https://github.com/munich-quantum-toolkit/core/pull/2193 [#2184]: https://github.com/munich-quantum-toolkit/core/pull/2184 [#2178]: https://github.com/munich-quantum-toolkit/core/pull/2178 diff --git a/mlir/include/mlir/Dialect/QCO/Utils/WireIterator.h b/mlir/include/mlir/Dialect/QCO/Utils/WireIterator.h index f40fe72542..b80637b732 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/WireIterator.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/WireIterator.h @@ -10,8 +10,15 @@ #pragma once +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QTensor/IR/QTensorOps.h" + +#include +#include +#include #include #include +#include #include #include @@ -19,10 +26,46 @@ namespace mlir::qco { -/// A bidirectional_iterator traversing the def-use chain of a qubit wire. +/// Resolves how qubits flow across call boundaries. +/// +/// The mapping follows each qubit argument through the callee instead of +/// assuming positional correspondence. Results are cached per callee. Mapping +/// fails for declarations, recursion, and non-straight-line bodies. +class CallQubitMapping { +public: + /// Gets the result continuing @p operand's wire. + /// + /// Returns a null value when the callee keeps the qubit and failure when the + /// correspondence cannot be derived. + [[nodiscard]] FailureOr getResultForOperand(func::CallOp callOp, + Value operand); + + /// Clears all cached correspondence after a callee is changed or erased. + void invalidate(); + +private: + friend class WireIterator; + + // Marks a qubit argument that never reaches a result. + static constexpr int64_t KEPT = -1; + + // Returns each qubit argument's call-result index, or KEPT. + FailureOr> mappingFor(func::CallOp callOp); + + // Derives a mapping by threading every qubit argument through the callee. + FailureOr> computeMapping(func::FuncOp callee); + + // Gets the call operand feeding a result's wire. + FailureOr getOperandForResult(func::CallOp callOp, Value result); + + DenseMap> cache; + DenseSet inProgress; +}; + +/// A bidirectional iterator over the def-use chain of a qubit wire. /// /// The iterator follows the flow of a qubit through a sequence of quantum -/// operations while respecting the semantics of the respective operation. +/// operations while respecting the semantics of each operation. class [[nodiscard]] WireIterator { public: using iterator_category = std::bidirectional_iterator_tag; @@ -85,24 +128,40 @@ class [[nodiscard]] WireIterator { } private: + friend class CallQubitMapping; + /// Labels the position on the wire. enum class Position : uint8_t { BeforeHead, Head, Between, Tail, PastTail }; + WireIterator(Value qubit, CallQubitMapping* mapping) : WireIterator(qubit) { + mapping_ = mapping; + } + /// Return true, if an op doesn't return, but only consumes, a qubit value. static bool isTail(Operation*); /// Return true, if an op doesn't consume, but only returns, a qubit value. static bool isHead(Operation*); - /// Move to the next operation on the qubit wire. + // Moves to the next operation on the qubit wire. void forward(); - /// Move to the previous operation on the qubit wire. + // Moves to the previous operation on the qubit wire. void backward(); Operation* op_; Value qubit_; Position pos_; + bool mappingFailed_ = false; + + // Resolves the call result continuing an operand's wire. + FailureOr resultForOperand(func::CallOp callOp, Value operand) const; + + // Resolves the call operand feeding a result's wire. + [[nodiscard]] Value operandForResult(func::CallOp callOp, Value result) const; + + // Null means that each call query uses a fresh mapping. + CallQubitMapping* mapping_ = nullptr; }; /// Categorizes the current traversal direction. @@ -118,15 +177,7 @@ template struct WireTraversalTraits { } }; -/** - * @brief A range over the def-use chain of a qubit wire, usable in range-based - * for-loops. - * - * Example: - * @code - * for (auto* op : WireRange(qubit)) { ... } - * @endcode - */ +/// A range over the def-use chain of a qubit wire. struct WireRange { explicit WireRange(Value qubit) : begin_(qubit) {} diff --git a/mlir/include/mlir/Dialect/QTensor/Utils/TensorIterator.h b/mlir/include/mlir/Dialect/QTensor/Utils/TensorIterator.h index 5824a38e8a..7f4b97ac80 100644 --- a/mlir/include/mlir/Dialect/QTensor/Utils/TensorIterator.h +++ b/mlir/include/mlir/Dialect/QTensor/Utils/TensorIterator.h @@ -10,18 +10,24 @@ #pragma once +#include +#include +#include +#include +#include #include #include #include #include +#include +#include +#include #include namespace mlir::qtensor { -/** - * @brief A bidirectional_iterator traversing the tensor chain. - **/ +/// A bidirectional iterator traversing the tensor chain. class [[nodiscard]] TensorIterator { public: using iterator_category = std::bidirectional_iterator_tag; @@ -75,10 +81,10 @@ class [[nodiscard]] TensorIterator { } private: - /// @brief Move to the next operation on the tensor def-use chain. + // Moves to the next operation on the tensor def-use chain. void forward(); - /// @brief Move to the previous operation on the tensor def-use chain. + // Moves to the previous operation on the tensor def-use chain. void backward(); Operation* op_; @@ -86,4 +92,35 @@ class [[nodiscard]] TensorIterator { bool isFinal_; bool isSentinel_; }; + +/// Resolves how qubit tensors flow across call boundaries. +/// +/// The mapping follows each tensor argument through the callee instead of +/// assuming positional correspondence. Results are cached per callee. Mapping +/// fails for declarations, recursion, and non-straight-line bodies. +class CallTensorMapping { +public: + /// Gets the result continuing @p operand's tensor chain. + /// + /// Returns a null value when the callee keeps the tensor and failure when the + /// correspondence cannot be derived. + [[nodiscard]] FailureOr getResultForOperand(func::CallOp callOp, + Value operand); + +private: + // Marks a tensor argument that never reaches a result. + static constexpr int64_t KEPT = -1; + + // Returns each tensor argument's call-result index, or KEPT. + FailureOr> mappingFor(func::CallOp callOp); + + // Derives a mapping by threading every tensor argument through the callee. + FailureOr> computeMapping(func::FuncOp callee); + + // Follows an argument to a return operand, hopping over calls. + FailureOr threadToResult(Value arg, func::ReturnOp returnOp); + + DenseMap> cache; + DenseSet inProgress; +}; } // namespace mlir::qtensor diff --git a/mlir/lib/Dialect/QCO/Utils/CMakeLists.txt b/mlir/lib/Dialect/QCO/Utils/CMakeLists.txt index 62edce61ac..37c613d178 100644 --- a/mlir/lib/Dialect/QCO/Utils/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Utils/CMakeLists.txt @@ -28,6 +28,7 @@ add_mlir_dialect_library( LINK_LIBS PUBLIC MLIRCBitDialect + MLIRFuncDialect MLIRMQTUtils MLIRQCODialect MLIRSCFDialect) diff --git a/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp b/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp index e68734aefe..3388a0e6d6 100644 --- a/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp +++ b/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp @@ -10,30 +10,190 @@ #include "mlir/Dialect/QCO/Utils/WireIterator.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" #include +#include #include #include #include #include #include +#include +#include #include #include #include +#include +#include #include +#include +#include namespace mlir::qco { -bool WireIterator::isTail(Operation* const op) { - return isa(op); +// Returns the position of a qubit among the qubit-typed values in a range. +template +static std::optional qubitPositionIn(RangeT range, Value qubit) { + size_t position = 0; + for (Value value : range) { + if (!isa(value.getType())) { + continue; + } + if (value == qubit) { + return position; + } + ++position; + } + return std::nullopt; +} + +// Returns the qubit-typed value at a position, or null if none exists. +template +static Value nthQubitOf(RangeT range, size_t position) { + size_t seen = 0; + for (Value value : range) { + if (!isa(value.getType())) { + continue; + } + if (seen == position) { + return value; + } + ++seen; + } + return nullptr; +} + +FailureOr> +CallQubitMapping::computeMapping(func::FuncOp callee) { + if (callee.isExternal()) { + return failure(); + } + + // Threading a callee already in progress would not terminate. + if (!inProgress.insert(callee.getOperation()).second) { + return failure(); + } + auto progressGuard = + llvm::make_scope_exit([&] { inProgress.erase(callee.getOperation()); }); + + // A body under construction may not have a terminator yet. + if (!callee.getBody().hasOneBlock() || + !callee.getBody().front().mightHaveTerminator()) { + return failure(); + } + auto returnOp = + dyn_cast(callee.getBody().front().getTerminator()); + if (!returnOp) { + return failure(); + } + + SmallVector mapping; + for (BlockArgument arg : callee.getArguments()) { + if (!isa(arg.getType())) { + continue; + } + + int64_t resultIndex = KEPT; + { + // Follow the argument to the end of its wire. + Value last = arg; + Operation* lastOp = nullptr; + WireIterator it(arg, this); + for (; it != std::default_sentinel; ++it) { + last = it.qubit(); + lastOp = it.operation(); + } + if (it.mappingFailed_) { + return failure(); + } + + if (isa_and_nonnull(lastOp)) { + for (const auto& [index, operand] : + llvm::enumerate(returnOp.getOperands())) { + if (operand == last) { + resultIndex = static_cast(index); + break; + } + } + } + } + mapping.emplace_back(resultIndex); + } + + return mapping; +} + +void CallQubitMapping::invalidate() { cache.clear(); } + +FailureOr> CallQubitMapping::mappingFor(func::CallOp callOp) { + auto callee = dyn_cast_or_null( + SymbolTable::lookupNearestSymbolFrom(callOp, callOp.getCalleeAttr())); + if (!callee) { + return failure(); + } + + auto* const key = callee.getOperation(); + if (const auto it = cache.find(key); it != cache.end()) { + return ArrayRef(it->second); + } + // Compute before caching so recursion is detected through inProgress. + auto mapping = computeMapping(callee); + if (failed(mapping)) { + return failure(); + } + return ArrayRef( + cache.insert_or_assign(key, std::move(*mapping)).first->second); +} + +FailureOr CallQubitMapping::getResultForOperand(func::CallOp callOp, + Value operand) { + const auto position = qubitPositionIn(callOp.getOperands(), operand); + assert(position && "expected a qubit operand of the call"); + auto mappingOr = mappingFor(callOp); + if (failed(mappingOr)) { + return failure(); + } + ArrayRef mapping = *mappingOr; + assert(*position < mapping.size() && "expected matching call signature"); + const auto resultIndex = mapping[*position]; + if (resultIndex == KEPT) { + return Value{}; + } + return callOp.getResult(static_cast(resultIndex)); +} + +FailureOr CallQubitMapping::getOperandForResult(func::CallOp callOp, + Value result) { + auto opResult = cast(result); + assert(opResult.getOwner() == callOp.getOperation() && + "expected a result of the call"); + const auto resultIndex = static_cast(opResult.getResultNumber()); + auto mappingOr = mappingFor(callOp); + if (failed(mappingOr)) { + return failure(); + } + ArrayRef mapping = *mappingOr; + for (const auto& [position, index] : llvm::enumerate(mapping)) { + if (index == resultIndex) { + return nthQubitOf(callOp.getOperands(), position); + } + } + return Value{}; +} + +bool WireIterator::isTail(Operation* op) { + // `qtensor.from_elements` takes qubits into a tensor just like + // `qtensor.insert` does, so a wire reaching either of them ends there. + return isa(op); } -bool WireIterator::isHead(Operation* const op) { +bool WireIterator::isHead(Operation* op) { return isa(op); } @@ -45,6 +205,20 @@ Operation* WireIterator::operation() const { return op_; } +FailureOr WireIterator::resultForOperand(func::CallOp callOp, + Value operand) const { + CallQubitMapping local; + auto& mapping = mapping_ == nullptr ? local : *mapping_; + return mapping.getResultForOperand(callOp, operand); +} + +Value WireIterator::operandForResult(func::CallOp callOp, Value result) const { + CallQubitMapping local; + auto& mapping = mapping_ == nullptr ? local : *mapping_; + auto operand = mapping.getOperandForResult(callOp, result); + return succeeded(operand) ? *operand : Value{}; +} + Value WireIterator::qubit() const { if (*this == std::default_sentinel) { llvm::reportFatalInternalError("Trying to access qubit of sentinel!"); @@ -82,6 +256,23 @@ void WireIterator::forward() { // Find the output from the input qubit SSA value. pos_ = Position::Between; + + // A call threads the qubit through to the matching result. When the callee + // keeps it, the wire ends here. + if (auto callOp = dyn_cast(op_)) { + auto result = resultForOperand(callOp, qubit_); + if (failed(result)) { + mappingFailed_ = true; + pos_ = Position::Tail; + return; + } + if (!*result) { + pos_ = Position::Tail; + return; + } + qubit_ = *result; + return; + } TypeSwitch(op_) .Case( [&](UnitaryOpInterface op) { qubit_ = op.getOutputForInput(qubit_); }) @@ -103,7 +294,10 @@ void WireIterator::forward() { .Case([&](IndexSwitchOp op) { qubit_ = op.getTiedResult(&(*qubit_.use_begin())); }) - .Default([&](Operation* const op) { pos_ = Position::Tail; }); + .Default([&](Operation*) { + mappingFailed_ = true; + pos_ = Position::Tail; + }); } void WireIterator::backward() { @@ -134,6 +328,14 @@ void WireIterator::backward() { bool unknown = false; // Find the input from the output qubit SSA value. TypeSwitch(op_) + .Case([&](func::CallOp callOp) { + Value operand = operandForResult(callOp, qubit_); + if (!operand) { + unknown = true; + return; + } + qubit_ = operand; + }) .Case([&](UnitaryOpInterface op) { qubit_ = op.getInputForOutput(qubit_); }) @@ -172,7 +374,7 @@ void WireIterator::backward() { } llvm::reportFatalInternalError("expected result lookup"); }) - .Default([&](Operation* const op) { unknown = true; }); + .Default([&](Operation*) { unknown = true; }); if (unknown) { pos_ = Position::BeforeHead; diff --git a/mlir/lib/Dialect/QTensor/Utils/TensorIterator.cpp b/mlir/lib/Dialect/QTensor/Utils/TensorIterator.cpp index a33f509bb0..c255e22fc7 100644 --- a/mlir/lib/Dialect/QTensor/Utils/TensorIterator.cpp +++ b/mlir/lib/Dialect/QTensor/Utils/TensorIterator.cpp @@ -10,21 +10,28 @@ #include "mlir/Dialect/QTensor/Utils/TensorIterator.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" #include +#include #include #include #include #include #include +#include #include +#include #include #include #include +#include #include +#include +#include namespace mlir::qtensor { TypedValue TensorIterator::tensor() const { @@ -228,4 +235,148 @@ void TensorIterator::backward() { static_assert(std::bidirectional_iterator); static_assert(std::sentinel_for, "std::default_sentinel_t must be a sentinel for TensorIterator."); + +// Returns whether a type is a tensor of qubits. +static bool isQubitTensor(Type type) { + auto tensorType = dyn_cast(type); + return tensorType && isa(tensorType.getElementType()); +} + +// Returns the position of a value among the qubit tensors in a range. +static std::optional tensorPositionIn(ValueRange range, Value value) { + size_t position = 0; + for (Value candidate : range) { + if (!isQubitTensor(candidate.getType())) { + continue; + } + if (candidate == value) { + return position; + } + ++position; + } + return std::nullopt; +} + +FailureOr CallTensorMapping::threadToResult(Value arg, + func::ReturnOp returnOp) { + Value current = arg; + while (true) { + // Follow the chain to its end. `tensor()` is null on the operations that + // consume a tensor without producing one, so the last non-null value is + // the one the terminating operation takes. + Value last = current; + Operation* lastOp = nullptr; + for (TensorIterator it(cast>(current)); + it != std::default_sentinel; ++it) { + if (Value currentTensor = it.tensor()) { + last = currentTensor; + } + lastOp = it.operation(); + } + + if (isa_and_nonnull(lastOp)) { + for (const auto& [index, operand] : + llvm::enumerate(returnOp.getOperands())) { + if (operand == last) { + return static_cast(index); + } + } + return KEPT; + } + + // The chain stops at a nested call. Step over it to the result that + // continues the tensor and keep following from there. Each hop moves + // forward along the def-use chain, so this terminates. + auto callOp = dyn_cast_or_null(lastOp); + if (!callOp) { + return KEPT; + } + auto next = getResultForOperand(callOp, last); + if (failed(next)) { + return failure(); + } + if (!*next) { + return KEPT; + } + current = *next; + } +} + +FailureOr> +CallTensorMapping::computeMapping(func::FuncOp callee) { + if (callee.isExternal()) { + return failure(); + } + + // Threading a callee already in progress would not terminate. + if (!inProgress.insert(callee.getOperation()).second) { + return failure(); + } + auto progressGuard = + llvm::make_scope_exit([&] { inProgress.erase(callee.getOperation()); }); + + // A body under construction may not have a terminator yet. + if (!callee.getBody().hasOneBlock() || + !callee.getBody().front().mightHaveTerminator()) { + return failure(); + } + auto returnOp = + dyn_cast(callee.getBody().front().getTerminator()); + if (!returnOp) { + return failure(); + } + + SmallVector mapping; + for (BlockArgument arg : callee.getArguments()) { + if (!isQubitTensor(arg.getType())) { + continue; + } + auto result = threadToResult(arg, returnOp); + if (failed(result)) { + return failure(); + } + mapping.emplace_back(*result); + } + + return mapping; +} + +FailureOr> +CallTensorMapping::mappingFor(func::CallOp callOp) { + auto callee = dyn_cast_or_null( + SymbolTable::lookupNearestSymbolFrom(callOp, callOp.getCalleeAttr())); + if (!callee) { + return failure(); + } + + auto* const key = callee.getOperation(); + if (const auto it = cache.find(key); it != cache.end()) { + return ArrayRef(it->second); + } + // Compute before caching so recursion is detected through inProgress. + auto mapping = computeMapping(callee); + if (failed(mapping)) { + return failure(); + } + return ArrayRef( + cache.insert_or_assign(key, std::move(*mapping)).first->second); +} + +FailureOr CallTensorMapping::getResultForOperand(func::CallOp callOp, + Value operand) { + const auto position = tensorPositionIn(callOp.getOperands(), operand); + assert(position && "expected a qubit-tensor operand of the call"); + auto mappingOr = mappingFor(callOp); + if (failed(mappingOr)) { + return failure(); + } + ArrayRef mapping = *mappingOr; + assert(*position < mapping.size() && "expected matching call signature"); + const auto resultIndex = mapping[*position]; + if (resultIndex == KEPT) { + return Value{}; + } + return callOp.getResult(static_cast(resultIndex)); +} + } // namespace mlir::qtensor From b20daa5a6afa4304e5a1f03c2c14157f8dd6edc1 Mon Sep 17 00:00:00 2001 From: Damian Rovara Date: Fri, 28 Aug 2026 12:22:18 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=E2=9C=85=20Cover=20call=20correspondence?= =?UTF-8?q?=20and=20unsupported=20callees?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exercise reordered, nested, kept, declared, and recursive call mappings together with iterator traversal in both directions. Assisted-by: Claude Opus 5 Assisted-by: Codex --- .../Dialect/QCO/Utils/test_wireiterator.cpp | 156 ++++++++++++++++++ .../QTensor/Utils/test_tensoriterator.cpp | 129 ++++++++++++++- 2 files changed, 276 insertions(+), 9 deletions(-) diff --git a/mlir/unittests/Dialect/QCO/Utils/test_wireiterator.cpp b/mlir/unittests/Dialect/QCO/Utils/test_wireiterator.cpp index 19e571fc97..8ea55841b6 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_wireiterator.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_wireiterator.cpp @@ -21,7 +21,9 @@ #include #include #include +#include #include +#include #include #include @@ -48,6 +50,20 @@ class WireIteratorFixture : public testing::Test { } std::unique_ptr context; + + [[nodiscard]] OwningOpRef parseModule(StringRef source) const { + return parseSourceString(source, context.get()); + } + + template [[nodiscard]] static OpT findOp(Operation* root) { + OpT found; + root->walk([&](OpT op) { + if (!found) { + found = op; + } + }); + return found; + } }; struct Chain { @@ -331,3 +347,143 @@ TEST_F(WireIteratorFixture, TraversalTerminatesAtUnknownCarrier) { --backward; EXPECT_EQ(backward, std::default_sentinel); } +TEST_F(WireIteratorFixture, CallMappingFollowsNestedReordering) { + auto module = parseModule(R"mlir( +func.func private @swap(%flag: i1, %a: !qco.qubit, %b: !qco.qubit) + -> (i1, !qco.qubit, !qco.qubit) { + return %flag, %b, %a : i1, !qco.qubit, !qco.qubit +} +func.func private @outer(%flag: i1, %a: !qco.qubit, %b: !qco.qubit) + -> (i1, !qco.qubit, !qco.qubit) { + %r:3 = func.call @swap(%flag, %a, %b) + : (i1, !qco.qubit, !qco.qubit) + -> (i1, !qco.qubit, !qco.qubit) + return %r#0, %r#1, %r#2 : i1, !qco.qubit, !qco.qubit +} +func.func @main() { + %flag = arith.constant true + %a = qco.alloc : !qco.qubit + %b = qco.alloc : !qco.qubit + %r:3 = func.call @outer(%flag, %a, %b) + : (i1, !qco.qubit, !qco.qubit) + -> (i1, !qco.qubit, !qco.qubit) + qco.sink %r#1 : !qco.qubit + qco.sink %r#2 : !qco.qubit + return +} +)mlir"); + ASSERT_TRUE(module); + auto main = module->lookupSymbol("main"); + auto call = findOp(main); + SmallVector allocs; + main.walk([&](qco::AllocOp op) { allocs.emplace_back(op.getResult()); }); + ASSERT_EQ(allocs.size(), 2U); + + qco::CallQubitMapping mapping; + auto mapped = mapping.getResultForOperand(call, call.getOperand(1)); + ASSERT_TRUE(succeeded(mapped)); + EXPECT_EQ(*mapped, call.getResult(2)); + mapped = mapping.getResultForOperand(call, call.getOperand(2)); + ASSERT_TRUE(succeeded(mapped)); + EXPECT_EQ(*mapped, call.getResult(1)); + + qco::WireIterator iterator(allocs[0]); + ++iterator; + EXPECT_EQ(iterator.qubit(), call.getResult(2)); + --iterator; + EXPECT_EQ(iterator.qubit(), allocs[0]); + + auto swap = module->lookupSymbol("swap"); + auto returnOp = cast(swap.getBody().front().getTerminator()); + returnOp->setOperands(swap.getArguments()); + mapping.invalidate(); + mapped = mapping.getResultForOperand(call, call.getOperand(1)); + ASSERT_TRUE(succeeded(mapped)); + EXPECT_EQ(*mapped, call.getResult(1)); +} + +TEST_F(WireIteratorFixture, CallMappingDistinguishesKeptAndCreatedQubits) { + auto module = parseModule(R"mlir( +func.func private @replace(%old: !qco.qubit) -> !qco.qubit { + qco.sink %old : !qco.qubit + %new = qco.alloc : !qco.qubit + return %new : !qco.qubit +} +func.func @main() { + %old = qco.alloc : !qco.qubit + %new = func.call @replace(%old) : (!qco.qubit) -> !qco.qubit + qco.sink %new : !qco.qubit + return +} +)mlir"); + ASSERT_TRUE(module); + auto main = module->lookupSymbol("main"); + auto call = findOp(main); + Value old = findOp(main).getResult(); + + qco::CallQubitMapping mapping; + auto mapped = mapping.getResultForOperand(call, old); + ASSERT_TRUE(succeeded(mapped)); + EXPECT_FALSE(*mapped); + + qco::WireIterator consumed(old); + ++consumed; + ASSERT_EQ(consumed.operation(), call); + ++consumed; + EXPECT_EQ(consumed, std::default_sentinel); + + qco::WireIterator created(call.getResult(0)); + --created; + EXPECT_EQ(created, std::default_sentinel); +} + +TEST_F(WireIteratorFixture, CallMappingFailsClosed) { + auto module = parseModule(R"mlir( +func.func private @external(!qco.qubit) -> !qco.qubit +func.func private @recursive(%q: !qco.qubit) -> !qco.qubit { + %r = func.call @recursive(%q) : (!qco.qubit) -> !qco.qubit + return %r : !qco.qubit +} +func.func private @unknown(%q: !qco.qubit) -> !qco.qubit { + %r = builtin.unrealized_conversion_cast %q : !qco.qubit to !qco.qubit + return %r : !qco.qubit +} +func.func @main() { + %a = qco.alloc : !qco.qubit + %x = func.call @external(%a) : (!qco.qubit) -> !qco.qubit + qco.sink %x : !qco.qubit + %b = qco.alloc : !qco.qubit + %y = func.call @recursive(%b) : (!qco.qubit) -> !qco.qubit + qco.sink %y : !qco.qubit + %c = qco.alloc : !qco.qubit + %z = func.call @unknown(%c) : (!qco.qubit) -> !qco.qubit + qco.sink %z : !qco.qubit + return +} +)mlir"); + ASSERT_TRUE(module); + auto main = module->lookupSymbol("main"); + func::CallOp external; + func::CallOp recursive; + func::CallOp unknown; + main.walk([&](func::CallOp call) { + if (call.getCallee() == "external") { + external = call; + } else if (call.getCallee() == "recursive") { + recursive = call; + } else { + unknown = call; + } + }); + ASSERT_TRUE(external); + ASSERT_TRUE(recursive); + ASSERT_TRUE(unknown); + + qco::CallQubitMapping mapping; + EXPECT_TRUE( + failed(mapping.getResultForOperand(external, external.getOperand(0)))); + EXPECT_TRUE( + failed(mapping.getResultForOperand(recursive, recursive.getOperand(0)))); + EXPECT_TRUE( + failed(mapping.getResultForOperand(unknown, unknown.getOperand(0)))); +} diff --git a/mlir/unittests/Dialect/QTensor/Utils/test_tensoriterator.cpp b/mlir/unittests/Dialect/QTensor/Utils/test_tensoriterator.cpp index 61ba045425..6ba8311d41 100644 --- a/mlir/unittests/Dialect/QTensor/Utils/test_tensoriterator.cpp +++ b/mlir/unittests/Dialect/QTensor/Utils/test_tensoriterator.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -53,6 +54,21 @@ class TensorIteratorTest : public ::testing::Test { context->appendDialectRegistry(registry); context->loadAllAvailableDialects(); } + + [[nodiscard]] OwningOpRef parseModule(StringRef source) const { + return parseSourceString(source, context.get()); + } + + [[nodiscard]] static func::CallOp findCall(Operation* root, + StringRef callee) { + func::CallOp found; + root->walk([&](func::CallOp call) { + if (call.getCallee() == callee) { + found = call; + } + }); + return found; + } }; } // namespace @@ -261,15 +277,6 @@ TEST_F(TensorIteratorTest, Traversal) { ASSERT_EQ(recIt.tensor(), tensorElse0); } -/** - * @brief A tensor returned by a call starts its own life-chain. - * - * @details - * A call sits on both sides of a chain: it consumes the caller's tensor and - * hands back a fresh one. Walking backward from the result therefore stops at - * the call, the same way it stops at an allocation, instead of continuing into - * the tensor that was passed in. - */ TEST_F(TensorIteratorTest, CallResultStartsALifeChain) { auto module = parseSourceString(R"mlir( func.func private @relabel(%t: tensor<2x!qco.qubit>) -> tensor<2x!qco.qubit> { @@ -455,3 +462,107 @@ TEST_F(TensorIteratorTest, TraversesWhileCarriedTensors) { ASSERT_EQ(swapped.operation(), tensor1.getDefiningOp()); ASSERT_EQ(swapped.tensor(), tensor1); } + +TEST_F(TensorIteratorTest, CallMappingFollowsNestedReordering) { + auto module = parseModule(R"mlir( +func.func private @swap( + %flag: i1, %a: tensor<2x!qco.qubit>, %b: tensor<2x!qco.qubit>) + -> (i1, tensor<2x!qco.qubit>, tensor<2x!qco.qubit>) { + return %flag, %b, %a + : i1, tensor<2x!qco.qubit>, tensor<2x!qco.qubit> +} +func.func private @outer( + %flag: i1, %a: tensor<2x!qco.qubit>, %b: tensor<2x!qco.qubit>) + -> (i1, tensor<2x!qco.qubit>, tensor<2x!qco.qubit>) { + %r:3 = func.call @swap(%flag, %a, %b) + : (i1, tensor<2x!qco.qubit>, tensor<2x!qco.qubit>) + -> (i1, tensor<2x!qco.qubit>, tensor<2x!qco.qubit>) + return %r#0, %r#1, %r#2 + : i1, tensor<2x!qco.qubit>, tensor<2x!qco.qubit> +} +func.func @main() { + %flag = arith.constant true + %c2 = arith.constant 2 : index + %a = qtensor.alloc(%c2) : tensor<2x!qco.qubit> + %b = qtensor.alloc(%c2) : tensor<2x!qco.qubit> + %r:3 = func.call @outer(%flag, %a, %b) + : (i1, tensor<2x!qco.qubit>, tensor<2x!qco.qubit>) + -> (i1, tensor<2x!qco.qubit>, tensor<2x!qco.qubit>) + qtensor.dealloc %r#1 : tensor<2x!qco.qubit> + qtensor.dealloc %r#2 : tensor<2x!qco.qubit> + return +} +)mlir"); + ASSERT_TRUE(module); + auto main = module->lookupSymbol("main"); + auto call = findCall(main, "outer"); + ASSERT_TRUE(call); + + CallTensorMapping mapping; + auto mapped = mapping.getResultForOperand(call, call.getOperand(1)); + ASSERT_TRUE(succeeded(mapped)); + EXPECT_EQ(*mapped, call.getResult(2)); + mapped = mapping.getResultForOperand(call, call.getOperand(2)); + ASSERT_TRUE(succeeded(mapped)); + EXPECT_EQ(*mapped, call.getResult(1)); +} + +TEST_F(TensorIteratorTest, CallMappingReportsAKeptTensor) { + auto module = parseModule(R"mlir( +func.func private @consume(%t: tensor<2x!qco.qubit>) { + qtensor.dealloc %t : tensor<2x!qco.qubit> + return +} +func.func @main() { + %c2 = arith.constant 2 : index + %t = qtensor.alloc(%c2) : tensor<2x!qco.qubit> + func.call @consume(%t) : (tensor<2x!qco.qubit>) -> () + return +} +)mlir"); + ASSERT_TRUE(module); + auto call = findCall(module->lookupSymbol("main"), "consume"); + ASSERT_TRUE(call); + + CallTensorMapping mapping; + auto mapped = mapping.getResultForOperand(call, call.getOperand(0)); + ASSERT_TRUE(succeeded(mapped)); + EXPECT_FALSE(*mapped); +} + +TEST_F(TensorIteratorTest, CallMappingFailsClosed) { + auto module = parseModule(R"mlir( +func.func private @external(tensor<2x!qco.qubit>) + -> tensor<2x!qco.qubit> +func.func private @recursive(%t: tensor<2x!qco.qubit>) + -> tensor<2x!qco.qubit> { + %r = func.call @recursive(%t) + : (tensor<2x!qco.qubit>) -> tensor<2x!qco.qubit> + return %r : tensor<2x!qco.qubit> +} +func.func @main() { + %c2 = arith.constant 2 : index + %a = qtensor.alloc(%c2) : tensor<2x!qco.qubit> + %x = func.call @external(%a) + : (tensor<2x!qco.qubit>) -> tensor<2x!qco.qubit> + qtensor.dealloc %x : tensor<2x!qco.qubit> + %b = qtensor.alloc(%c2) : tensor<2x!qco.qubit> + %y = func.call @recursive(%b) + : (tensor<2x!qco.qubit>) -> tensor<2x!qco.qubit> + qtensor.dealloc %y : tensor<2x!qco.qubit> + return +} +)mlir"); + ASSERT_TRUE(module); + auto main = module->lookupSymbol("main"); + auto external = findCall(main, "external"); + auto recursive = findCall(main, "recursive"); + ASSERT_TRUE(external); + ASSERT_TRUE(recursive); + + CallTensorMapping mapping; + EXPECT_TRUE( + failed(mapping.getResultForOperand(external, external.getOperand(0)))); + EXPECT_TRUE( + failed(mapping.getResultForOperand(recursive, recursive.getOperand(0)))); +} From 3bd74e3cadc7da42cb6eab6efe6848f3e63229cb Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Sun, 30 Aug 2026 13:48:28 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=93=9D=20Plan=20call-mapping=20stack?= =?UTF-8?q?=20refresh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Record the rebase, simplification, validation, and publication procedure for the interprocedural optimization stack. Assisted-by: Codex --- .agent/plans/rebase-call-mapping-stack.md | 237 ++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 .agent/plans/rebase-call-mapping-stack.md diff --git a/.agent/plans/rebase-call-mapping-stack.md b/.agent/plans/rebase-call-mapping-stack.md new file mode 100644 index 0000000000..be53e7da09 --- /dev/null +++ b/.agent/plans/rebase-call-mapping-stack.md @@ -0,0 +1,237 @@ +# Rebase and simplify the call-mapping stack + +This ExecPlan is a living document. The sections `Progress`, +`Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must +be kept up to date as work proceeds. + +This ExecPlan must be maintained in accordance with `.agent/PLANS.md` from the +repository root. + +## Purpose / Big Picture + +Pull requests `#2194` and `#2196` add call-aware linear-value tracking to the +QCO and QTensor MLIR utilities and to `QCOProgramBuilder`. The repository has +since merged new static-qubit validation, stricter conversion boundaries, and +new C++ and MLIR development rules. Rebase both pull requests onto the current +`main`, remove code that has no current consumer, and keep unsupported call +shapes fail-closed. A developer can then build multi-function QCO test programs +while the iterator utilities derive linear-value correspondence only when the +callee body proves it. + +The remaining pull requests `#2197` through `#2201` build on `#2196`. Their +production code is outside this audit. Rebase them only as required to preserve +the stack and make each pull request add its own changelog reference. + +## Progress + +- [x] (2026-08-30 13:36Z) Read the root and MLIR agent guides, canonical C++ and + MLIR policies, AI policy, and Ponytail skills. +- [x] (2026-08-30 13:36Z) Fetch current `main`, `#2194`, and `#2196` and save + local backup refs for both remote heads. +- [x] (2026-08-30 13:36Z) Inspect merged `#2281` and `#2282` and identify + changed files and contracts. +- [x] (2026-08-30 14:10Z) Rebase `#2194` on current `main`, reduce its mapping + tests to the downstream contracts, and pass both focused utility suites. +- [x] (2026-08-30 14:10Z) Rebase and simplify `#2196` on rewritten `#2194`. +- [x] (2026-08-30 14:10Z) Rebase `#2197` through `#2201` without production-code + changes and restore one changelog reference per pull request. +- [x] (2026-08-30 14:10Z) Run focused tests, all configured C++ tests, and both + required lint sessions; all pass after accepting the plan's Markdown + formatting. +- [x] (2026-08-30 14:16Z) Verify all nine rewritten commits, inspect every + pull-request diff, publish the seven branches atomically with exact + leases, and update the two audited pull-request descriptions and labels. + +## Surprises & Discoveries + +- Observation: The current `#2194` and `#2196` production files do not overlap + the files changed by `#2281` or `#2282`. Evidence: the only path changed by + both current `main` and `#2194` since their merge base is `CHANGELOG.md`; + `#2196` has no overlapping path. +- Observation: `#2281` strengthens the module-wide QCO linearity contract by + requiring unique static-qubit indices and program-level static roots in the + entry block. This does not replace call correspondence because function + arguments and call results still need body-derived pairing. +- Observation: `#2282` does not touch call mapping or the builder. Its relevant + precedent is to reject unsupported MLIR shapes before mutation and report + failure instead of guessing or terminating. +- Observation: Production consumers pass only type-filtered operands belonging + to the call. `#2196` uses both mappings for builder tracking; `#2199` + additionally shares and invalidates the qubit cache after callee mutation. No + production consumer queries classical or foreign values, uses a reverse + mapping, or invalidates tensor correspondence. + +## Decision Log + +- Decision: Keep the fail-closed `FailureOr` contract for call mapping. + Rationale: Declarations, recursion, incomplete bodies, and multi-block bodies + do not prove a linear-value correspondence. Positional pairing can join + unrelated values. Date/Author: 2026-08-30, Codex. +- Decision: Audit `#2194` and `#2196` only; mechanically rebase later stack pull + requests without production-code cleanup. Rationale: The user named the two + audit targets and separately requested per-pull-request changelog ownership. + Date/Author: 2026-08-30, Codex. +- Decision: `#2194` and `#2196` add their own references to the general compiler + infrastructure entry. `#2197` through `#2201` each add only their own + reference to the interprocedural-pass entry. Rationale: Iterator and builder + support are infrastructure, while an early pull request must not promise later + passes that can still change or be removed. Date/Author: 2026-08-30, Codex. +- Decision: Keep only three mapping scenarios per value kind: nested reordered + correspondence, kept/created values where applicable, and fail-closed + declarations/recursion. Rationale: These cover every distinct downstream + contract; the removed pass-through, foreign-value, and duplicated traversal + cases did not exercise another production behavior. Date/Author: 2026-08-30, + Codex. +- Decision: Require helper functions to be finished before operations are added + to `main`. Rationale: no production consumer needs to suspend partially built + `main`; the ordering rule removes copied tracking sets and a speculative + function-scope abstraction while preserving every downstream stack use. + Date/Author: 2026-08-30, Codex. + +## Outcomes & Retrospective + +The Ponytail audit reduced `#2194` production and tests from 1,126 added lines +to 762, excluding changelog and plan edits, and reduced `#2196` from 650 to 393. +The retained code has production consumers in the stack or enforces fail-closed +behavior; duplicate input-shape tests, unused flexibility, copied builder state, +and a non-discriminating tensor-swap test were removed. + +The focused QCO utility, QTensor utility, builder, and complete QCO IR suites +pass. A release build succeeds, all 3,923 configured tests pass with one +expected skip, and C++ lint reports zero clang-format or clang-tidy failures. +The general lint gate also passes cleanly. All nine commits verify as signed, +and every pull request adds only its own changelog reference. The stack was +published atomically with exact leases. Pull requests `#2194` and `#2196` retain +no assignees and use the `enhancement`, `c++`, and `MLIR` labels. Hosted checks +were not monitored because that was not requested. + +## Context and Orientation + +`mlir/include/mlir/Dialect/QCO/Utils/WireIterator.h` and +`mlir/lib/Dialect/QCO/Utils/WireIterator.cpp` define traversal over one linear +qubit value. Pull request `#2194` extends traversal across `func.call` by +tracing each callee argument through a supported body to its returned result. +`mlir/include/mlir/Dialect/QTensor/Utils/TensorIterator.h` and its +implementation provide the equivalent mapping for qubit tensors. + +`mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h` and +`mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp` own test-program +construction. Pull request `#2196` adds additional functions and calls. The +builder consumes the mapping utilities to transfer its tracked linear values +across calls. + +The pull requests form a Git stack: `#2194` is based on `main`; `#2196` is based +on `#2194`; `#2197` through `#2201` each use the preceding pull-request branch +as their base. Rewriting an early branch requires rebasing every later branch so +that GitHub continues to show only each pull request's own change. + +### Plan of Work + +Rebase `#2194` onto the current remote `main`. Resolve the changelog conflict by +retaining current `main` and adding only `#2194` to the general compiler +infrastructure entry. Compare the rebased diff with upstream MLIR and current +repository helpers. Use Ponytail review to identify public methods, caches, +wrappers, comments, tests, or branches with no current consumer. Remove each +finding that can be deleted without weakening the fail-closed contract or its +smallest regression test. Convert changed public documentation to `///`, remove +forbidden `const` qualifiers from MLIR handles and by-value parameters, and use +current terminology. + +Rebase `#2196` onto rewritten `#2194`. Add `#2196` and its link definition to +the same infrastructure changelog entry. Trace every new builder state field and +helper from `startFunction`, `endFunction`, and `call` to its callers. Remove +duplicated tracking paths or tests that only pin implementation details. Keep +one focused test for each supported behavior and each concrete failure contract. + +Rebase `#2197` through `#2201` in order. Do not change their production code. +Make each pull request add only its own number to the changelog entry and define +only its own link. Preserve human authorship and replace no legitimate human +trailer. + +### Concrete Steps + +Run all commands from the repository root. Refresh the refs with: + + git fetch --prune origin main mlir/call-aware-iterators mlir/builder-call-support + +Before rewriting, record each remote head and create a local backup ref. Rebase +the branches in stack order. After each substantive edit, inspect: + + git diff --check + git diff --stat ..HEAD + git range-diff .. ..HEAD + +Build and run the two `#2194` utility binaries: + + cmake --build --preset release --target mqt-core-mlir-unittest-qco-utils mqt-core-mlir-unittest-qtensor-utils -j2 + ./build/release/mlir/unittests/Dialect/QCO/Utils/mqt-core-mlir-unittest-qco-utils --gtest_brief=1 + ./build/release/mlir/unittests/Dialect/QTensor/Utils/mqt-core-mlir-unittest-qtensor-utils --gtest_brief=1 + +Build and run the `#2196` builder tests: + + cmake --build --preset release --target mqt-core-mlir-unittest-qco-ir -j2 + ./build/release/mlir/unittests/Dialect/QCO/IR/mqt-core-mlir-unittest-qco-ir --gtest_filter='QCOTest.Builder*' --gtest_brief=1 + +Run the repository-required validation on the final stack head: + + ctest --preset release --output-on-failure + uvx nox -s cpp-lint + uvx nox -s lint + +Expected focused output reports zero failed tests. `cpp-lint` and `lint` must +complete successfully and leave the worktree clean. + +### Validation and Acceptance + +Pull request `#2194` is acceptable when traversal crosses supported +straight-line calls in both directions, reports failure for unsupported callees +without positional guessing, and both QCO and QTensor utility binaries pass. Its +diff must not expose cache controls or reverse mappings without a production +consumer. + +Pull request `#2196` is acceptable when the builder creates completed additional +functions, tracks qubits and qubit tensors across supported calls, rejects +unsupported callee shapes with the documented diagnostic, and the +builder-focused QCO IR tests pass. The builder must preserve outer tracked +values and reject leaks. + +The stack is acceptable when each pull request adds only its own changelog +reference, every changed C++ file follows current MLIR and documentation policy, +all configured C++ tests pass, both lint sessions pass, all rewritten commits +verify as signed, and every GitHub diff contains only that pull request's scope. + +### Idempotence and Recovery + +Fetching, building, testing, and linting are repeatable. Local backup refs named +for the pull request and rewrite date preserve the pre-rewrite remote heads. +Before pushing, compare every remote head with its recorded lease. Push the +stack atomically with one exact `--force-with-lease` per branch. If a lease no +longer matches, stop without pushing and inspect the new remote work instead of +overwriting it. + +### Artifacts and Notes + +The pre-rewrite heads recorded on 2026-08-30 are: + + #2194 79d8ea2492224e6aca16e0d78ccf682bc76e9e10 + #2196 0c727ef274245a4d9ce45363466ad4c1fe1b38e4 + #2197 eabcd73a06054eed7b868b9e1df23c43fb55e9e8 + #2198 84eafd4d77493677aa3b1e4edb5dd0c7131a83be + #2199 1ae55bf064d88ace2a25584d98c3743e3faaba8c + #2200 6615c429b4f4514215f54dbff65110a08f03e567 + #2201 f4edd2d3ec57c4cdbff8a2be380bb722d61d531c + +The current `main` head after #2282 is: + + d1c19982952c79862c95a4903d302b2a8a295752 + +### Interfaces and Dependencies + +Use MLIR's `FailureOr` and `LogicalResult` for mapping failure. Use +`func::CallOp`, `func::FuncOp`, `Value`, `ArrayRef`, `SmallVector`, `DenseMap`, +and `DenseSet` from the existing LLVM and MLIR dependencies. Add no dependency, +new dialect operation, interface, trait, or general-purpose abstraction. + +Revision note (2026-08-30): Created after refreshing the stack, then updated +after the Ponytail audit and validation to record simplifications, +per-pull-request changelog ownership, and the required mechanical restack.