From 74251f23d80c617be2e3eb75f9b71442382a8e7b Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 3 Sep 2026 15:21:43 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20Support=20wide=20Qiskit=20regis?= =?UTF-8?q?ter=20comparisons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preserve unsigned literals with APInt and allow direct complete-register comparisons beyond 64 bits. Keep computed and signed wide expressions rejected, and exchange Python integers through hexadecimal. Assisted-by: GPT-5.6 Sol via Codex Signed-off-by: Simon Hofmann --- .agent/plans/wide-cbit-qiskit-comparisons.md | 66 +++++++++ bindings/mlir/qiskit/Qiskit2_5.cpp | 72 +++++++--- bindings/mlir/qiskit/QiskitExport.cpp | 65 +++++++-- bindings/mlir/qiskit/QiskitImport.cpp | 119 +++++++++++++++- bindings/mlir/qiskit/QiskitTranslation.h | 3 +- docs/mlir/python_compiler_collection.md | 38 ++--- test/python/test_mlir_qiskit_translation.py | 137 +++++++++++++++++++ 7 files changed, 451 insertions(+), 49 deletions(-) create mode 100644 .agent/plans/wide-cbit-qiskit-comparisons.md diff --git a/.agent/plans/wide-cbit-qiskit-comparisons.md b/.agent/plans/wide-cbit-qiskit-comparisons.md new file mode 100644 index 0000000000..4b64593542 --- /dev/null +++ b/.agent/plans/wide-cbit-qiskit-comparisons.md @@ -0,0 +1,66 @@ +# Support wide CBit comparisons in Qiskit + +Status: complete. + +## Goal and scope + +Allow Qiskit interchange for unsigned comparisons between one complete named +classical register and one same-width literal when the width exceeds 64 bits. +Both operand orders and all equality and ordering predicates are supported. +Values must round-trip without truncation, including values large enough to +exceed Python's decimal string-conversion limit. + +The private interchange model lives in +`bindings/mlir/qiskit/QiskitTranslation.h`. Python conversion is implemented in +`bindings/mlir/qiskit/Qiskit2_5.cpp`, while +`bindings/mlir/qiskit/QiskitExport.cpp` and +`bindings/mlir/qiskit/QiskitImport.cpp` translate between that model and MLIR. +Focused coverage is in `test/python/test_mlir_qiskit_translation.py`. The +supported boundary is documented in `docs/mlir/python_compiler_collection.md`. + +Computed wide integer expressions, packed loose bits, and signed comparisons +wider than 64 bits remain unsupported. Those cases continue to fail with an +explicit diagnostic instead of silently widening the generic expression path. + +## Decisions + +- Store normalized unsigned literals as `llvm::APInt`, with the declared Qiskit + width kept separately. Active bits are validated before MLIR emission. This + preserves exact values and avoids allocating storage based only on an + untrusted declared width. +- Convert Python integers through unsigned hexadecimal text in both directions. + Python exempts power-of-two bases from its configurable decimal digit limit, + so this supports very wide values without changing interpreter-wide settings. +- Recognize the wide form before generic expression handling. Export accepts a + direct `cbit.read` and `arith.cmpi` with an integer constant; import emits the + same standard MLIR operations. The complete-register check prevents this + exception from expanding support to aliases or arbitrary computed values. +- Keep the existing 64-bit boundary everywhere else. Signed ordering currently + requires a computed sign-bit transform in Qiskit, so it is intentionally not + part of the direct-only wide path. + +## Validation + +Validation from the repository root completed as follows: + + cmake --build build/python/Release --target mqt-core-mlir-bindings -j 6 + uvx nox -s stubs + uvx nox -s lint + git diff --check + +The binding build passed. The focused selection passed all seven cases, and the +complete `test/python/test_mlir_qiskit_translation.py` file passed all 261 +tests. Stub generation completed without a generated-file diff, repository lint +passed, and `git diff --check` passed. The focused cases cover 65-, 151-, and +301-bit values, reversed ordering, Python's decimal digit limit, and the +deliberate computed-wide and signed-wide rejections. + +`uvx nox -s cpp-lint -- origin/main` could not start because this host does not +provide the required clang-tidy 22; it reported no source finding. + +## Outcome + +Direct unsigned complete-register comparisons now round-trip through Qiskit at +arbitrary representable widths using standard MLIR operations. The generic +64-bit boundary and explicit rejections remain intact, no dependency or public +Python API was added, and the user-facing support table records the exception. diff --git a/bindings/mlir/qiskit/Qiskit2_5.cpp b/bindings/mlir/qiskit/Qiskit2_5.cpp index 551dd4c3d5..8f2b2a8963 100644 --- a/bindings/mlir/qiskit/Qiskit2_5.cpp +++ b/bindings/mlir/qiskit/Qiskit2_5.cpp @@ -12,7 +12,9 @@ #include "mlir/Dialect/QC/Translation/StandardGate.h" #include +#include #include +#include #include // Qiskit requires its umbrella header before the extension function table. @@ -96,6 +98,16 @@ constexpr size_t MAX_ANNOTATED_OPERATION_DEPTH = 64U; } } +[[nodiscard]] static std::string pythonHex(const nb::handle object, + const std::string_view error) { + try { + return nb::cast( + nb::module_::import_("builtins").attr("hex")(object)); + } catch (const nb::python_error&) { + throw std::runtime_error(std::string(error)); + } +} + [[nodiscard]] static std::string pythonStringAttribute(const nb::handle object, const char* name, const std::string_view error) { @@ -113,6 +125,33 @@ pythonUnsignedAttribute(const nb::handle object, const char* name, return result; } +[[nodiscard]] static llvm::APInt +pythonUnsignedValue(const nb::handle object, const uint32_t width, + const std::string_view error) { + if (!nb::isinstance(object)) { + throw std::runtime_error(std::string(error)); + } + const auto text = pythonHex(object, error); + auto value = llvm::StringRef(text); + llvm::APInt result; + if (!value.consume_front("0x") || value.getAsInteger(16, result) || + result.getActiveBits() > width) { + throw std::runtime_error(std::string(error)); + } + return result; +} + +[[nodiscard]] static nb::object pythonInteger(const llvm::APInt& value, + const std::string_view error) { + const auto text = llvm::toString(value, 16, false); + try { + return nb::module_::import_("builtins") + .attr("int")(nb::str(text.c_str()), nb::int_(16)); + } catch (const nb::python_error&) { + throw std::runtime_error(std::string(error)); + } +} + [[noreturn]] static void throwPythonError(const std::string_view message) { const nb::python_error error; throw std::runtime_error(std::string(message) + ": " + error.what()); @@ -1106,9 +1145,9 @@ static void setPythonExpressionType(Expression& result, if (typeName == "Uint") { const auto width = pythonUnsignedAttribute( type, "width", "Qiskit Uint expression has no width"); - if (width == 0U || width > 64U) { + if (width == 0U || width > std::numeric_limits::max()) { throw std::runtime_error( - "Qiskit unsigned classical values must be between 1 and 64 bits"); + "Qiskit unsigned classical value width is out of range"); } result.type = ClassicalType::Uint; result.width = static_cast(width); @@ -1186,7 +1225,7 @@ static void normalizePythonVariable(Expression& result, } if (nb::isinstance(variable, circuitModule.attr("ClassicalRegister"))) { if (result.type != ClassicalType::Uint || nb::len(variable) == 0U || - nb::len(variable) > 64U || result.width < nb::len(variable)) { + result.width < nb::len(variable)) { throw std::runtime_error( "Qiskit classical-register variable has an invalid type"); } @@ -1241,12 +1280,9 @@ static void normalizePythonVariable(Expression& result, break; } case ClassicalType::Uint: - if (!nb::try_cast(value, result->uintValue) || - (result->width < 64U && - result->uintValue >= (uint64_t{1} << result->width))) { - throw std::runtime_error( - "Qiskit Uint literal does not fit its declared width"); - } + result->uintValue = pythonUnsignedValue( + value, result->width, + "Qiskit Uint literal does not fit its declared width"); break; case ClassicalType::Float: if (!nb::try_cast(value, result->floatValue) || @@ -1782,9 +1818,8 @@ class PythonClassicalBuilder final { } return typesModule_.attr("Bool")(); case ClassicalType::Uint: - if (width == 0U || width > 64U) { - throw std::runtime_error( - "Qiskit unsigned expressions require a width from 1 to 64"); + if (width == 0U) { + throw std::runtime_error("Qiskit unsigned expressions require a width"); } return typesModule_.attr("Uint")(width); case ClassicalType::Float: @@ -1892,13 +1927,16 @@ class PythonClassicalBuilder final { switch (value.type) { case ClassicalType::Bool: return expressionModule_.attr("lift")(nb::bool_(value.boolValue), type); - case ClassicalType::Uint: - if (value.width < std::numeric_limits::digits && - value.uintValue >= (uint64_t{1} << value.width)) { + case ClassicalType::Uint: { + if (value.uintValue.getActiveBits() > value.width) { throw std::runtime_error( "Qiskit unsigned expression value exceeds its width"); } - return expressionModule_.attr("lift")(nb::int_(value.uintValue), type); + return expressionModule_.attr("lift")( + pythonInteger(value.uintValue, + "Qiskit failed to convert a Uint literal"), + type); + } case ClassicalType::Float: if (!std::isfinite(value.floatValue)) { throw std::runtime_error( @@ -1917,7 +1955,7 @@ class PythonClassicalBuilder final { return expressionModule_.attr("lift")(classicalBit(value.bit)); case ExpressionKind::ClassicalRegister: if (value.type != ClassicalType::Uint || value.width == 0U || - value.width < value.reg.bits.size() || value.width > 64U) { + value.width < value.reg.bits.size()) { throw std::runtime_error( "Qiskit classical-register expression has an invalid type"); } diff --git a/bindings/mlir/qiskit/QiskitExport.cpp b/bindings/mlir/qiskit/QiskitExport.cpp index 59176f677b..806a4d7e5c 100644 --- a/bindings/mlir/qiskit/QiskitExport.cpp +++ b/bindings/mlir/qiskit/QiskitExport.cpp @@ -26,6 +26,7 @@ #include "mlir/Dialect/QC/Translation/StandardGate.h" #include "mlir/Support/IntegerExpressions.h" +#include #include #include #include @@ -1017,10 +1018,11 @@ static void setExpressionType(Expression& expression, const mlir::Type type) { } [[nodiscard]] static Register -classicalRegisterLayout(mlir::Value value, const ExportState& state) { +classicalRegisterLayout(mlir::Value value, const ExportState& state, + const bool allowWide = false) { const auto info = state.classicalRegisterInfo.find(value); if (info == state.classicalRegisterInfo.end() || info->second.size == 0U || - info->second.size > 64U) { + (!allowWide && info->second.size > 64U)) { throw std::runtime_error( "Qiskit classical registers require between 1 and 64 bits"); } @@ -1037,7 +1039,8 @@ classicalRegisterLayout(mlir::Value value, const ExportState& state) { } [[nodiscard]] static Register classicalRegister(mlir::Value value, - const ExportState& state) { + const ExportState& state, + const bool allowWide = false) { const auto info = state.classicalRegisterInfo.find(value); if (info != state.classicalRegisterInfo.end() && info->second.initialization != mlir::cbit::Initialization::Zero) { @@ -1048,7 +1051,7 @@ classicalRegisterLayout(mlir::Value value, const ExportState& state) { "Qiskit classical expression reads undefined classical bits"); } } - return classicalRegisterLayout(value, state); + return classicalRegisterLayout(value, state, allowWide); } [[nodiscard]] static BinaryOperation @@ -1123,7 +1126,7 @@ exportExpressionImpl(mlir::Value value, ExportState& state, if (result->type == ClassicalType::Bool) { result->boolValue = !integer.getValue().isZero(); } else if (result->type == ClassicalType::Uint) { - result->uintValue = integer.getValue().getZExtValue(); + result->uintValue = integer.getValue(); } else { throw std::runtime_error( "Qiskit Float expressions require a floating-point constant"); @@ -1236,7 +1239,7 @@ exportExpressionImpl(mlir::Value value, ExportState& state, if (operand->kind == ExpressionKind::Value && operand->type == ClassicalType::Bool && *bitVectorWidth == 1U) { operand->type = ClassicalType::Uint; - operand->uintValue = operand->boolValue; + operand->uintValue = llvm::APInt(1U, operand->boolValue); return; } countExpressionNode(nodeCount); @@ -1266,7 +1269,7 @@ exportExpressionImpl(mlir::Value value, ExportState& state, auto literal = std::make_unique(); literal->type = ClassicalType::Uint; literal->width = width; - literal->uintValue = bits; + literal->uintValue = llvm::APInt(width, bits); return literal; }; const auto uintCast = [&](std::unique_ptr operand, @@ -1379,7 +1382,7 @@ exportExpressionImpl(mlir::Value value, ExportState& state, auto zero = std::make_unique(); setExpressionType(*zero, cast.getIn().getType()); zero->kind = ExpressionKind::Value; - zero->uintValue = 0U; + zero->uintValue = llvm::APInt::getZero(zero->width); result->right = std::move(zero); } state.expressionOperations.insert(operation); @@ -1397,6 +1400,50 @@ exportExpressionImpl(mlir::Value value, ExportState& state, "Qiskit integer comparisons require integer operands"); } const auto width = type.getWidth(); + if (width > 64U) { + auto read = op.getLhs().getDefiningOp(); + auto constant = op.getRhs().getDefiningOp(); + bool reverse = false; + if (!read || !constant || + !llvm::isa(constant.getValue())) { + read = op.getRhs().getDefiningOp(); + constant = op.getLhs().getDefiningOp(); + reverse = true; + } + const auto integer = + constant ? llvm::dyn_cast(constant.getValue()) + : mlir::IntegerAttr{}; + if (read && integer) { + if (mlir::mqt::unsignedPredicate(op.getPredicate()) != + op.getPredicate()) { + throw std::runtime_error( + "Qiskit signed register comparisons support at most 64 bits"); + } + if (read->getBlock() != &evaluationBlock) { + throw std::runtime_error( + "Qiskit classical expressions cannot capture a computed SSA " + "value across a control-flow region"); + } + countExpressionNode(nodeCount); + auto reg = std::make_unique(); + reg->kind = ExpressionKind::ClassicalRegister; + reg->type = ClassicalType::Uint; + reg->width = width; + reg->reg = classicalRegister(read.getReg(), state, true); + countExpressionNode(nodeCount); + auto expected = std::make_unique(); + expected->type = ClassicalType::Uint; + expected->width = width; + expected->uintValue = integer.getValue(); + result->kind = ExpressionKind::Binary; + result->binaryOperation = comparisonOperation(op.getPredicate()); + result->left = reverse ? std::move(expected) : std::move(reg); + result->right = reverse ? std::move(reg) : std::move(expected); + state.expressionOperations.insert(read); + state.expressionOperations.insert(operation); + return result; + } + } auto comparison = binary( comparisonOperation(mlir::mqt::unsignedPredicate(op.getPredicate())), op.getLhs(), op.getRhs(), width); @@ -1761,7 +1808,7 @@ exportSwitchTarget(mlir::Value value, ExportState& state, expression->kind = ExpressionKind::Value; expression->type = ClassicalType::Uint; expression->width = 64U; - expression->uintValue = *constant; + expression->uintValue = llvm::APInt(64U, *constant); return {.kind = ClassicalTargetKind::Expression, .width = 64U, .expression = std::move(expression)}; diff --git a/bindings/mlir/qiskit/QiskitImport.cpp b/bindings/mlir/qiskit/QiskitImport.cpp index 49f9b877d8..7af0a72dbe 100644 --- a/bindings/mlir/qiskit/QiskitImport.cpp +++ b/bindings/mlir/qiskit/QiskitImport.cpp @@ -591,13 +591,22 @@ circuitRegisters(const CircuitReader& circuit, const bool quantum) { [[nodiscard]] static mlir::Value integerConstant(mlir::ImplicitLocOpBuilder& builder, const uint32_t width, - const uint64_t value) { + const llvm::APInt& value) { + if (value.getActiveBits() > width) { + throw std::runtime_error( + "Qiskit Uint literal does not fit its declared width"); + } const auto type = builder.getIntegerType(width); - const auto attribute = - builder.getIntegerAttr(type, llvm::APInt(width, value, false)); + const auto attribute = builder.getIntegerAttr(type, value.zextOrTrunc(width)); return mlir::arith::ConstantOp::create(builder, attribute).getResult(); } +[[nodiscard]] static mlir::Value +integerConstant(mlir::ImplicitLocOpBuilder& builder, const uint32_t width, + const uint64_t value) { + return integerConstant(builder, width, llvm::APInt(width, value, false)); +} + [[nodiscard]] static mlir::Value castInteger(mlir::ImplicitLocOpBuilder& builder, mlir::Value value, const mlir::IntegerType target) { @@ -708,6 +717,65 @@ packRegister(mlir::qc::QCProgramBuilder& builder, return terms.front(); } +namespace { +struct WideRegisterComparison { + const Expression* reg; + const Expression* expected; + mlir::arith::CmpIPredicate predicate; +}; +} // namespace + +[[nodiscard]] static std::optional +matchWideRegisterComparison(const Expression& expression) { + if (expression.kind != ExpressionKind::Binary || + expression.type != ClassicalType::Bool || expression.width != 1U || + !expression.left || !expression.right) { + return std::nullopt; + } + const bool reverse = + expression.left->kind == ExpressionKind::Value && + expression.right->kind == ExpressionKind::ClassicalRegister; + const auto* reg = reverse ? expression.right.get() : expression.left.get(); + const auto* expected = + reverse ? expression.left.get() : expression.right.get(); + if (reg->kind != ExpressionKind::ClassicalRegister || + reg->type != ClassicalType::Uint || reg->reg.bits.empty() || + reg->width <= 64U || reg->width != reg->reg.bits.size() || + expected->kind != ExpressionKind::Value || + expected->type != ClassicalType::Uint || expected->width != reg->width) { + return std::nullopt; + } + mlir::arith::CmpIPredicate predicate; + switch (expression.binaryOperation) { + case BinaryOperation::Equal: + predicate = mlir::arith::CmpIPredicate::eq; + break; + case BinaryOperation::NotEqual: + predicate = mlir::arith::CmpIPredicate::ne; + break; + case BinaryOperation::Less: + predicate = reverse ? mlir::arith::CmpIPredicate::ugt + : mlir::arith::CmpIPredicate::ult; + break; + case BinaryOperation::LessEqual: + predicate = reverse ? mlir::arith::CmpIPredicate::uge + : mlir::arith::CmpIPredicate::ule; + break; + case BinaryOperation::Greater: + predicate = reverse ? mlir::arith::CmpIPredicate::ult + : mlir::arith::CmpIPredicate::ugt; + break; + case BinaryOperation::GreaterEqual: + predicate = reverse ? mlir::arith::CmpIPredicate::ule + : mlir::arith::CmpIPredicate::uge; + break; + default: + return std::nullopt; + } + return WideRegisterComparison{ + .reg = reg, .expected = expected, .predicate = predicate}; +} + [[nodiscard]] static mlir::Value emitExpression(mlir::qc::QCProgramBuilder& builder, const Expression& expression, @@ -817,6 +885,24 @@ emitExpression(mlir::qc::QCProgramBuilder& builder, break; } case ExpressionKind::Binary: { + if (const auto direct = matchWideRegisterComparison(expression)) { + auto storage = + registerStorage(classicalBits, rootClbitMap, direct->reg->reg); + if (!storage) { + throw std::runtime_error( + "Qiskit wide register comparisons require one complete " + "classical register"); + } + const auto width = direct->reg->width; + auto value = mlir::cbit::ReadOp::create( + builder, builder.getIntegerType(width), storage) + .getResult(); + auto expected = + integerConstant(builder, width, direct->expected->uintValue); + return mlir::arith::CmpIOp::create(builder, direct->predicate, value, + expected) + .getResult(); + } auto left = emitExpression(builder, *expression.left, classicalBits, rootClbitMap); if (expression.binaryOperation == BinaryOperation::LogicAnd || @@ -1589,9 +1675,27 @@ static void validateCircuit(const CircuitReader& circuit, static void validateExpression(const Expression& expression, const uint32_t rootClbits) { + if (const auto direct = matchWideRegisterComparison(expression)) { + if (direct->expected->uintValue.getActiveBits() > direct->expected->width) { + throw std::runtime_error( + "Qiskit Uint literal does not fit its declared width"); + } + llvm::DenseSet seen; + for (const auto bit : direct->reg->reg.bits) { + if (bit >= rootClbits || !seen.insert(bit).second) { + throw std::runtime_error( + "Qiskit classical-register expression has an invalid bit"); + } + } + return; + } + if (expression.type == ClassicalType::Uint && expression.width > 64U) { + throw std::runtime_error( + "Qiskit unsigned expressions wider than 64 bits require a direct " + "register comparison"); + } if ((expression.type == ClassicalType::Bool && expression.width != 1U) || - (expression.type == ClassicalType::Uint && - (expression.width == 0U || expression.width > 64U)) || + (expression.type == ClassicalType::Uint && expression.width == 0U) || (expression.type == ClassicalType::Float && expression.width != 64U)) { throw std::runtime_error( "Qiskit classical expression has an invalid type width"); @@ -1618,6 +1722,11 @@ static void validateExpression(const Expression& expression, }; switch (expression.kind) { case ExpressionKind::Value: + if (expression.type == ClassicalType::Uint && + expression.uintValue.getActiveBits() > expression.width) { + throw std::runtime_error( + "Qiskit Uint literal does not fit its declared width"); + } return; case ExpressionKind::ClassicalBit: if (expression.type != ClassicalType::Bool || expression.width != 1U || diff --git a/bindings/mlir/qiskit/QiskitTranslation.h b/bindings/mlir/qiskit/QiskitTranslation.h index 394da28ab3..2516b63b48 100644 --- a/bindings/mlir/qiskit/QiskitTranslation.h +++ b/bindings/mlir/qiskit/QiskitTranslation.h @@ -12,6 +12,7 @@ #include "mlir/Dialect/QC/Translation/StandardGate.h" +#include #include #include @@ -252,7 +253,7 @@ struct Expression { BinaryOperation binaryOperation = BinaryOperation::Equal; UnaryOperation unaryOperation = UnaryOperation::LogicNot; bool boolValue = false; - uint64_t uintValue = 0; + llvm::APInt uintValue; double floatValue = 0.0; uint32_t bit = 0; Register reg; diff --git a/docs/mlir/python_compiler_collection.md b/docs/mlir/python_compiler_collection.md index e44df6c37b..e8a6524011 100644 --- a/docs/mlir/python_compiler_collection.md +++ b/docs/mlir/python_compiler_collection.md @@ -181,6 +181,7 @@ flow, so export uses Qiskit's public Python classes for these operations. | Constant Boolean, `Uint` up to 64 bits, and `Float` expressions | Supported | Supported | | Clbit and ClassicalRegister expression variables | Supported | Supported | | Fixed-width bitwise operations, comparisons, and bounded shifts | Supported | Supported | +| Direct complete-register comparisons wider than 64 bits | Supported | Supported | | Clbit, indexed-register, and whole-register `Store` assignments | Supported | Supported | | Standalone classical runtime variables | Rejected | Rejected | | Free symbols and supported real parameter expressions | Supported | Supported | @@ -229,23 +230,26 @@ Nested blocks may capture existing qubits and classical bits but may not allocate or release circuit resources. Control flow and classical expressions may nest up to 64 levels, and classical expression trees may contain at most 16,384 nodes (parameter-expression limits are unchanged). Integer values use -exact widths from 1 through 64. Standard `arith.cmpi` handles every comparison: -signed ordering is encoded by XOR-biasing both operands' sign bits, including -computed operands. Casts preserve truncation and sign/zero extension. Bitwise -operations, modular arithmetic, integer selection, and shifts share these typed -rules. Import guards runtime shifts so overshifts produce zero; export preserves -the guards. Rotations and population count are expanded through the same bounded -integer lowering used by jeff. Unsupported operations, invalid widths, -non-finite constants, dynamic bounds, loop-carried values, and other SSA results -fail during validation. Core's constant-zero `i64` status return is not a -classical output. Whole-register reads map to Qiskit `ClassicalRegister` -expressions, and writes map to atomic Qiskit `Store` operations. Indexed stores -assume that their runtime index is in bounds. The Qiskit C API does not expose -`Store`, so the adapter inspects and constructs that instruction through -Qiskit's public Python classes, as it already does for structured control flow. -Internal entry-block CBit storage becomes additional Qiskit registers, ordered -before returned registers; Qiskit exposes all circuit storage. OpenQASM remains -the source interchange path for arbitrary register widths. +exact widths from 1 through 64. The only wider form is a direct unsigned +comparison between one complete `ClassicalRegister` and one same-width literal; +computed, packed, and signed wide values remain rejected. Standard `arith.cmpi` +handles every comparison: signed ordering is encoded by XOR-biasing both +operands' sign bits, including computed operands. Casts preserve truncation and +sign/zero extension. Bitwise operations, modular arithmetic, integer selection, +and shifts share these typed rules. Import guards runtime shifts so overshifts +produce zero; export preserves the guards. Rotations and population count are +expanded through the same bounded integer lowering used by jeff. Unsupported +operations, invalid widths, non-finite constants, dynamic bounds, loop-carried +values, and other SSA results fail during validation. Core's constant-zero `i64` +status return is not a classical output. Whole-register reads map to Qiskit +`ClassicalRegister` expressions, and writes map to atomic Qiskit `Store` +operations. Indexed stores assume that their runtime index is in bounds. The +Qiskit C API does not expose `Store`, so the adapter inspects and constructs +that instruction through Qiskit's public Python classes, as it already does for +structured control flow. Internal entry-block CBit storage becomes additional +Qiskit registers, ordered before returned registers; Qiskit exposes all circuit +storage. OpenQASM remains the source interchange path for arbitrary register +widths. Every public CBit output is exported as a Qiskit `ClassicalRegister`; an unnamed allocation receives a collision-free `_mqt_cN` name. This preserves the CBit diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index 3228793d94..a22003433e 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -610,6 +610,143 @@ def test_openqasm_register_ordering_exports_to_qiskit_expression() -> None: assert expr.structurally_equivalent(reimported_condition, expr.greater_equal(reimported_circuit.cregs[0], 1)) +@pytest.mark.parametrize( + ("width", "expected"), + [(65, 0), (151, 1 << 150), (301, (1 << 301) - 1)], + ids=["zero", "highest-bit", "all-ones"], +) +def test_wide_cbit_register_comparisons_round_trip(width: int, expected: int) -> None: + """Preserve direct wide register comparisons as positive Python integers.""" + program = QCProgram.from_mlir_str( + f"""module {{ + func.func @main() -> !cbit.reg<{width}> attributes {{mqt.entry_point}} {{ + %q = qc.alloc : !qc.qubit + %classical = cbit.alloc(#cbit.init) {{mqt.register_name = "c"}} : !cbit.reg<{width}> + %highest = arith.constant {width - 1} : index + %measured = qc.measure %q : !qc.qubit -> i1 + cbit.store %measured, %classical[%highest] : !cbit.reg<{width}> + %value = cbit.read %classical : !cbit.reg<{width}> -> i{width} + %expected = arith.constant {expected} : i{width} + %condition = arith.cmpi eq, %value, %expected : i{width} + scf.if %condition {{ + qc.x %q : !qc.qubit + }} + qc.dealloc %q : !qc.qubit + return %classical : !cbit.reg<{width}> + }} +}} +""" + ) + + circuit = program.to_qiskit() + condition = circuit.data[1].operation.condition + + assert expr.structurally_equivalent(condition, expr.equal(circuit.cregs[0], expected)) + + reimported = QCProgram.from_qiskit(circuit) + assert reimported.ir.count("cbit.read") == 1 + assert reimported.ir.count("arith.cmpi eq") == 1 + restored = reimported.to_qiskit() + assert expr.structurally_equivalent( + restored.data[1].operation.condition, + expr.equal(restored.cregs[0], expected), + ) + + +def test_reversed_wide_cbit_register_comparison_round_trip() -> None: + """Preserve reversed wide comparisons while normalizing imported MLIR.""" + program = QCProgram.from_mlir_str( + """module { + func.func @main() -> !cbit.reg<65> attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + %classical = cbit.alloc(#cbit.init) {mqt.register_name = "c"} : !cbit.reg<65> + %value = cbit.read %classical : !cbit.reg<65> -> i65 + %one = arith.constant 1 : i65 + %condition = arith.cmpi ult, %one, %value : i65 + scf.if %condition { + qc.x %q : !qc.qubit + } + qc.dealloc %q : !qc.qubit + return %classical : !cbit.reg<65> + } +} +""" + ) + + circuit = program.to_qiskit() + assert expr.structurally_equivalent( + circuit.data[0].operation.condition, + expr.less(1, circuit.cregs[0]), + ) + + reimported = QCProgram.from_qiskit(circuit) + assert "arith.cmpi ugt" in reimported.ir + restored = reimported.to_qiskit() + assert expr.structurally_equivalent( + restored.data[0].operation.condition, + expr.greater(restored.cregs[0], 1), + ) + + +def test_wide_cbit_register_comparison_ignores_decimal_digit_limit() -> None: + """Exchange wide integers without Python's decimal string conversion.""" + previous_limit = sys.get_int_max_str_digits() + limit = sys.int_info.str_digits_check_threshold + width = limit * 4 + expected = 1 << (width - 1) + sys.set_int_max_str_digits(limit) + try: + circuit = QuantumCircuit(1, width) + with circuit.if_test(expr.equal(circuit.cregs[0], expected)): + circuit.x(0) + + program = QCProgram.from_qiskit(circuit) + restored = program.to_qiskit() + + assert "arith.cmpi eq" in program.ir + assert expr.structurally_equivalent( + restored.data[0].operation.condition, + expr.equal(restored.cregs[0], expected), + ) + finally: + sys.set_int_max_str_digits(previous_limit) + + +def test_wide_computed_qiskit_uint_expression_is_rejected() -> None: + """Keep computed Qiskit Uint expressions capped at 64 bits.""" + circuit = QuantumCircuit(1, 65) + condition = expr.equal(expr.bit_xor(circuit.cregs[0], 1), 2) + with circuit.if_test(condition): + circuit.x(0) + + with pytest.raises(RuntimeError, match="wider than 64 bits require a direct register comparison"): + QCProgram.from_qiskit(circuit) + + +def test_wide_signed_cbit_comparison_is_rejected() -> None: + """Keep signed wide comparisons outside the direct unsigned path.""" + program = QCProgram.from_mlir_str( + """module { + func.func @main() -> !cbit.reg<65> attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + %classical = cbit.alloc(#cbit.init) {mqt.register_name = "c"} : !cbit.reg<65> + %value = cbit.read %classical : !cbit.reg<65> -> i65 + %one = arith.constant 1 : i65 + %condition = arith.cmpi slt, %value, %one : i65 + scf.if %condition { + qc.x %q : !qc.qubit + } + qc.dealloc %q : !qc.qubit + return %classical : !cbit.reg<65> + } +} +""" + ) + + with pytest.raises(RuntimeError, match="signed register comparisons support at most 64 bits"): + program.to_qiskit() + + def test_openqasm_signed_register_ordering_exports_to_qiskit_uint_expression() -> None: """Encode signed register ordering with Qiskit's unsigned expressions.""" program = QCProgram.from_qasm_str( From 2a3efa5c20a541c16c0df41f62500a2aa8450ceb Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 4 Sep 2026 09:31:24 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20wide=20Qisk?= =?UTF-8?q?it=20condition=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Let validation enforce direct wide comparisons and reuse generic emission without a separate predicate reversal table. Normalize tuple conditions through the same expression path, preserving Boolean values and folding out-of-range equalities to false. Cover all six predicates in both operand orders, jeff conversion, tuple conditions, and invalid wide register mappings. Keep export unchanged. Assisted-by: Codex Signed-off-by: Lukas Burgholzer --- .agent/plans/wide-cbit-qiskit-comparisons.md | 79 +++++-------- bindings/mlir/qiskit/Qiskit2_5.cpp | 30 ++--- bindings/mlir/qiskit/QiskitImport.cpp | 112 +++++-------------- docs/mlir/python_compiler_collection.md | 38 ++++--- test/python/test_mlir_qiskit_translation.py | 102 ++++++++++++----- 5 files changed, 161 insertions(+), 200 deletions(-) diff --git a/.agent/plans/wide-cbit-qiskit-comparisons.md b/.agent/plans/wide-cbit-qiskit-comparisons.md index 4b64593542..b542cad9a7 100644 --- a/.agent/plans/wide-cbit-qiskit-comparisons.md +++ b/.agent/plans/wide-cbit-qiskit-comparisons.md @@ -4,63 +4,38 @@ Status: complete. ## Goal and scope -Allow Qiskit interchange for unsigned comparisons between one complete named -classical register and one same-width literal when the width exceeds 64 bits. -Both operand orders and all equality and ordering predicates are supported. -Values must round-trip without truncation, including values large enough to -exceed Python's decimal string-conversion limit. - -The private interchange model lives in -`bindings/mlir/qiskit/QiskitTranslation.h`. Python conversion is implemented in -`bindings/mlir/qiskit/Qiskit2_5.cpp`, while -`bindings/mlir/qiskit/QiskitExport.cpp` and -`bindings/mlir/qiskit/QiskitImport.cpp` translate between that model and MLIR. -Focused coverage is in `test/python/test_mlir_qiskit_translation.py`. The -supported boundary is documented in `docs/mlir/python_compiler_collection.md`. - -Computed wide integer expressions, packed loose bits, and signed comparisons -wider than 64 bits remain unsupported. Those cases continue to fail with an -explicit diagnostic instead of silently widening the generic expression path. +Support unsigned comparisons between one complete classical register and one +same-width literal beyond 64 bits, including both operand orders and all six +comparison predicates. Qiskit expression conditions and tuple equalities use the +same normalized expression path. Out-of-range tuple equalities become false. +Computed, packed, and signed wide expressions remain unsupported. ## Decisions -- Store normalized unsigned literals as `llvm::APInt`, with the declared Qiskit - width kept separately. Active bits are validated before MLIR emission. This - preserves exact values and avoids allocating storage based only on an - untrusted declared width. -- Convert Python integers through unsigned hexadecimal text in both directions. - Python exempts power-of-two bases from its configurable decimal digit limit, - so this supports very wide values without changing interpreter-wide settings. -- Recognize the wide form before generic expression handling. Export accepts a - direct `cbit.read` and `arith.cmpi` with an integer constant; import emits the - same standard MLIR operations. The complete-register check prevents this - exception from expanding support to aliases or arbitrary computed values. -- Keep the existing 64-bit boundary everywhere else. Signed ordering currently - requires a computed sign-bit transform in Qiskit, so it is intentionally not - part of the direct-only wide path. +- Store unsigned literals in `llvm::APInt`; transfer Python integers through + hexadecimal text to preserve values without Python's decimal digit limit. +- Import validation owns the supported-expression boundary. Only the immediate + register and literal leaves of a direct comparison may exceed 64 bits. Generic + emission handles constants, reads, and comparison predicates. +- Emission checks mapped register storage. A wide read requires one complete + storage object in bit order; packing remains limited to 64 bits. +- Preserve operand order during import. MLIR comparison folding normalizes + constant-left predicates where needed; jeff already enables this folding. +- Export retains the direct `cbit.read` and `arith.cmpi` recognition path and + the shared initialization and snapshot checks. ## Validation -Validation from the repository root completed as follows: - - cmake --build build/python/Release --target mqt-core-mlir-bindings -j 6 - uvx nox -s stubs - uvx nox -s lint - git diff --check - -The binding build passed. The focused selection passed all seven cases, and the -complete `test/python/test_mlir_qiskit_translation.py` file passed all 261 -tests. Stub generation completed without a generated-file diff, repository lint -passed, and `git diff --check` passed. The focused cases cover 65-, 151-, and -301-bit values, reversed ordering, Python's decimal digit limit, and the -deliberate computed-wide and signed-wide rejections. - -`uvx nox -s cpp-lint -- origin/main` could not start because this host does not -provide the required clang-tidy 22; it reported no source finding. +The LLVM/MLIR 23.1 binding build passed. All 281 Qiskit translation tests +passed, including 27 focused wide-comparison cases. The 12 predicate/order cases +also verify conversion through QCO to jeff. Stub generation produced no tracked +changes. C++ lint passed with zero findings. Repository lint passed. -## Outcome +Commands from the repository root: -Direct unsigned complete-register comparisons now round-trip through Qiskit at -arbitrary representable widths using standard MLIR operations. The generic -64-bit boundary and explicit rejections remain intact, no dependency or public -Python API was added, and the user-facing support table records the exception. +```console +uv run --no-sync pytest -n 0 test/python/test_mlir_qiskit_translation.py +uvx nox -s stubs +uvx nox -s cpp-lint +uvx nox -s lint +``` diff --git a/bindings/mlir/qiskit/Qiskit2_5.cpp b/bindings/mlir/qiskit/Qiskit2_5.cpp index 8f2b2a8963..0e14477aaa 100644 --- a/bindings/mlir/qiskit/Qiskit2_5.cpp +++ b/bindings/mlir/qiskit/Qiskit2_5.cpp @@ -29,7 +29,6 @@ #include #include -#include #include #include #include @@ -1491,28 +1490,29 @@ class NativeControlFlowReader final : public ControlFlowReader { throw std::runtime_error("Qiskit control-flow condition has an invalid " "shape"); } - uint64_t expected = 0U; - if (!nb::try_cast(condition[1], expected)) { - throw std::runtime_error( - "Qiskit control-flow condition has an invalid value"); - } - - auto result = normalizePythonTarget(condition[0]); - if (result.kind == ClassicalTargetKind::ClassicalBit) { - if (expected > 1U) { + const auto expected = pythonUnsignedValue( + condition[1], std::numeric_limits::max(), + "Qiskit control-flow condition has an invalid value"); + auto result = + normalizePythonTarget(expressionModule.attr("lift")(condition[0])); + const auto& target = *result.expression; + if (target.kind == ExpressionKind::ClassicalBit) { + if (expected.getActiveBits() > 1U) { throw std::runtime_error( "Qiskit classical-bit condition must compare against zero or one"); } return normalizePythonTarget(expressionModule.attr("equal")( - condition[0], nb::bool_(expected != 0U))); + condition[0], nb::bool_(!expected.isZero()))); } - if (result.kind == ClassicalTargetKind::ClassicalRegister) { - if (std::bit_width(expected) > result.reg.bits.size()) { + if (target.kind == ExpressionKind::ClassicalRegister) { + if (expected.getActiveBits() > target.reg.bits.size()) { return normalizePythonTarget( expressionModule.attr("lift")(nb::bool_(false))); } - return normalizePythonTarget( - expressionModule.attr("equal")(condition[0], nb::int_(expected))); + return normalizePythonTarget(expressionModule.attr("equal")( + condition[0], + pythonInteger(expected, + "Qiskit control-flow condition has an invalid value"))); } throw std::runtime_error("Qiskit control flow has an unknown condition " "target"); diff --git a/bindings/mlir/qiskit/QiskitImport.cpp b/bindings/mlir/qiskit/QiskitImport.cpp index 7af0a72dbe..129396758b 100644 --- a/bindings/mlir/qiskit/QiskitImport.cpp +++ b/bindings/mlir/qiskit/QiskitImport.cpp @@ -578,9 +578,9 @@ circuitRegisters(const CircuitReader& circuit, const bool quantum) { case ClassicalType::Bool: return builder.getI1Type(); case ClassicalType::Uint: - if (width == 0U || width > 64U) { + if (width == 0U) { throw std::runtime_error( - "Qiskit unsigned classical values must be between 1 and 64 bits"); + "Qiskit unsigned classical values require a nonzero width"); } return builder.getIntegerType(width); case ClassicalType::Float: @@ -677,15 +677,15 @@ registerStorage(const llvm::ArrayRef classicalBits, packRegister(mlir::qc::QCProgramBuilder& builder, const llvm::ArrayRef classicalBits, const llvm::ArrayRef rootClbitMap, const Register& reg) { - if (reg.bits.empty() || reg.bits.size() > 64U) { - throw std::runtime_error( - "Qiskit classical registers must contain between 1 and 64 bits"); - } const auto width = static_cast(reg.bits.size()); const auto type = builder.getIntegerType(width); if (auto storage = registerStorage(classicalBits, rootClbitMap, reg)) { return mlir::cbit::ReadOp::create(builder, type, storage).getResult(); } + if (reg.bits.empty() || reg.bits.size() > 64U) { + throw std::runtime_error("Qiskit wide register comparisons require one " + "complete classical register"); + } llvm::SmallVector terms; terms.reserve(reg.bits.size()); for (size_t index = 0; index < reg.bits.size(); ++index) { @@ -717,63 +717,35 @@ packRegister(mlir::qc::QCProgramBuilder& builder, return terms.front(); } -namespace { -struct WideRegisterComparison { - const Expression* reg; - const Expression* expected; - mlir::arith::CmpIPredicate predicate; -}; -} // namespace - -[[nodiscard]] static std::optional -matchWideRegisterComparison(const Expression& expression) { - if (expression.kind != ExpressionKind::Binary || - expression.type != ClassicalType::Bool || expression.width != 1U || - !expression.left || !expression.right) { - return std::nullopt; +[[nodiscard]] static bool +isWideRegisterComparison(const Expression& expression) { + if (expression.kind != ExpressionKind::Binary || !expression.left || + !expression.right) { + return false; + } + const auto* reg = expression.left.get(); + const auto* expected = expression.right.get(); + if (reg->kind == ExpressionKind::Value) { + std::swap(reg, expected); } - const bool reverse = - expression.left->kind == ExpressionKind::Value && - expression.right->kind == ExpressionKind::ClassicalRegister; - const auto* reg = reverse ? expression.right.get() : expression.left.get(); - const auto* expected = - reverse ? expression.left.get() : expression.right.get(); if (reg->kind != ExpressionKind::ClassicalRegister || - reg->type != ClassicalType::Uint || reg->reg.bits.empty() || - reg->width <= 64U || reg->width != reg->reg.bits.size() || + reg->type != ClassicalType::Uint || reg->width <= 64U || + reg->width != reg->reg.bits.size() || expected->kind != ExpressionKind::Value || expected->type != ClassicalType::Uint || expected->width != reg->width) { - return std::nullopt; + return false; } - mlir::arith::CmpIPredicate predicate; switch (expression.binaryOperation) { case BinaryOperation::Equal: - predicate = mlir::arith::CmpIPredicate::eq; - break; case BinaryOperation::NotEqual: - predicate = mlir::arith::CmpIPredicate::ne; - break; case BinaryOperation::Less: - predicate = reverse ? mlir::arith::CmpIPredicate::ugt - : mlir::arith::CmpIPredicate::ult; - break; case BinaryOperation::LessEqual: - predicate = reverse ? mlir::arith::CmpIPredicate::uge - : mlir::arith::CmpIPredicate::ule; - break; case BinaryOperation::Greater: - predicate = reverse ? mlir::arith::CmpIPredicate::ult - : mlir::arith::CmpIPredicate::ugt; - break; case BinaryOperation::GreaterEqual: - predicate = reverse ? mlir::arith::CmpIPredicate::ule - : mlir::arith::CmpIPredicate::uge; - break; + return true; default: - return std::nullopt; + return false; } - return WideRegisterComparison{ - .reg = reg, .expected = expected, .predicate = predicate}; } [[nodiscard]] static mlir::Value @@ -885,24 +857,6 @@ emitExpression(mlir::qc::QCProgramBuilder& builder, break; } case ExpressionKind::Binary: { - if (const auto direct = matchWideRegisterComparison(expression)) { - auto storage = - registerStorage(classicalBits, rootClbitMap, direct->reg->reg); - if (!storage) { - throw std::runtime_error( - "Qiskit wide register comparisons require one complete " - "classical register"); - } - const auto width = direct->reg->width; - auto value = mlir::cbit::ReadOp::create( - builder, builder.getIntegerType(width), storage) - .getResult(); - auto expected = - integerConstant(builder, width, direct->expected->uintValue); - return mlir::arith::CmpIOp::create(builder, direct->predicate, value, - expected) - .getResult(); - } auto left = emitExpression(builder, *expression.left, classicalBits, rootClbitMap); if (expression.binaryOperation == BinaryOperation::LogicAnd || @@ -1674,22 +1628,10 @@ static void validateCircuit(const CircuitReader& circuit, size_t definitionDepth, size_t controlFlowDepth); static void validateExpression(const Expression& expression, - const uint32_t rootClbits) { - if (const auto direct = matchWideRegisterComparison(expression)) { - if (direct->expected->uintValue.getActiveBits() > direct->expected->width) { - throw std::runtime_error( - "Qiskit Uint literal does not fit its declared width"); - } - llvm::DenseSet seen; - for (const auto bit : direct->reg->reg.bits) { - if (bit >= rootClbits || !seen.insert(bit).second) { - throw std::runtime_error( - "Qiskit classical-register expression has an invalid bit"); - } - } - return; - } - if (expression.type == ClassicalType::Uint && expression.width > 64U) { + const uint32_t rootClbits, + bool allowWideLeaf = false) { + if (expression.type == ClassicalType::Uint && expression.width > 64U && + !allowWideLeaf) { throw std::runtime_error( "Qiskit unsigned expressions wider than 64 bits require a direct " "register comparison"); @@ -1700,12 +1642,13 @@ static void validateExpression(const Expression& expression, throw std::runtime_error( "Qiskit classical expression has an invalid type width"); } + const bool wideComparison = isWideRegisterComparison(expression); const auto requireOperand = [&](const std::unique_ptr& operand) { if (!operand) { throw std::runtime_error( "Qiskit classical expression has a missing operand"); } - validateExpression(*operand, rootClbits); + validateExpression(*operand, rootClbits, wideComparison); }; const auto sameType = [](const Expression& first, const Expression& second) { return first.type == second.type && first.width == second.width; @@ -1737,7 +1680,6 @@ static void validateExpression(const Expression& expression, return; case ExpressionKind::ClassicalRegister: { if (expression.type != ClassicalType::Uint || expression.reg.bits.empty() || - expression.reg.bits.size() > 64U || expression.width < expression.reg.bits.size()) { throw std::runtime_error( "Qiskit classical-register expression has an invalid type"); diff --git a/docs/mlir/python_compiler_collection.md b/docs/mlir/python_compiler_collection.md index e8a6524011..4b760a08c8 100644 --- a/docs/mlir/python_compiler_collection.md +++ b/docs/mlir/python_compiler_collection.md @@ -232,24 +232,26 @@ may nest up to 64 levels, and classical expression trees may contain at most 16,384 nodes (parameter-expression limits are unchanged). Integer values use exact widths from 1 through 64. The only wider form is a direct unsigned comparison between one complete `ClassicalRegister` and one same-width literal; -computed, packed, and signed wide values remain rejected. Standard `arith.cmpi` -handles every comparison: signed ordering is encoded by XOR-biasing both -operands' sign bits, including computed operands. Casts preserve truncation and -sign/zero extension. Bitwise operations, modular arithmetic, integer selection, -and shifts share these typed rules. Import guards runtime shifts so overshifts -produce zero; export preserves the guards. Rotations and population count are -expanded through the same bounded integer lowering used by jeff. Unsupported -operations, invalid widths, non-finite constants, dynamic bounds, loop-carried -values, and other SSA results fail during validation. Core's constant-zero `i64` -status return is not a classical output. Whole-register reads map to Qiskit -`ClassicalRegister` expressions, and writes map to atomic Qiskit `Store` -operations. Indexed stores assume that their runtime index is in bounds. The -Qiskit C API does not expose `Store`, so the adapter inspects and constructs -that instruction through Qiskit's public Python classes, as it already does for -structured control flow. Internal entry-block CBit storage becomes additional -Qiskit registers, ordered before returned registers; Qiskit exposes all circuit -storage. OpenQASM remains the source interchange path for arbitrary register -widths. +computed, packed, and signed wide values remain rejected. Both expression +conditions and tuple conditions such as `if_test((register, value))` support +this form. Tuple equalities with a value outside the register range become +false. Standard `arith.cmpi` handles every comparison: signed ordering is +encoded by XOR-biasing both operands' sign bits, including computed operands. +Casts preserve truncation and sign/zero extension. Bitwise operations, modular +arithmetic, integer selection, and shifts share these typed rules. Import guards +runtime shifts so overshifts produce zero; export preserves the guards. +Rotations and population count are expanded through the same bounded integer +lowering used by jeff. Unsupported operations, invalid widths, non-finite +constants, dynamic bounds, loop-carried values, and other SSA results fail +during validation. Core's constant-zero `i64` status return is not a classical +output. Whole-register reads map to Qiskit `ClassicalRegister` expressions, and +writes map to atomic Qiskit `Store` operations. Indexed stores assume that their +runtime index is in bounds. The Qiskit C API does not expose `Store`, so the +adapter inspects and constructs that instruction through Qiskit's public Python +classes, as it already does for structured control flow. Internal entry-block +CBit storage becomes additional Qiskit registers, ordered before returned +registers; Qiskit exposes all circuit storage. OpenQASM remains the source +interchange path for arbitrary register widths. Every public CBit output is exported as a Qiskit `ClassicalRegister`; an unnamed allocation receives a collision-free `_mqt_cN` name. This preserves the CBit diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index a22003433e..3583e14aa4 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -653,39 +653,81 @@ def test_wide_cbit_register_comparisons_round_trip(width: int, expected: int) -> ) -def test_reversed_wide_cbit_register_comparison_round_trip() -> None: - """Preserve reversed wide comparisons while normalizing imported MLIR.""" - program = QCProgram.from_mlir_str( - """module { - func.func @main() -> !cbit.reg<65> attributes {mqt.entry_point} { - %q = qc.alloc : !qc.qubit - %classical = cbit.alloc(#cbit.init) {mqt.register_name = "c"} : !cbit.reg<65> - %value = cbit.read %classical : !cbit.reg<65> -> i65 - %one = arith.constant 1 : i65 - %condition = arith.cmpi ult, %one, %value : i65 - scf.if %condition { - qc.x %q : !qc.qubit - } - qc.dealloc %q : !qc.qubit - return %classical : !cbit.reg<65> - } -} -""" - ) +@pytest.mark.parametrize( + ("comparison", "swapped"), + [ + ("equal", "equal"), + ("not_equal", "not_equal"), + ("less", "greater"), + ("less_equal", "greater_equal"), + ("greater", "less"), + ("greater_equal", "less_equal"), + ], +) +@pytest.mark.parametrize("reverse", [False, True]) +def test_wide_qiskit_register_comparison_round_trip(comparison: str, swapped: str, *, reverse: bool) -> None: + """Preserve every unsigned comparison in either operand order.""" + circuit = QuantumCircuit(1, 65) + expected = 1 << 64 + operands = (expected, circuit.cregs[0]) if reverse else (circuit.cregs[0], expected) + with circuit.if_test(getattr(expr, comparison)(*operands)): + circuit.x(0) - circuit = program.to_qiskit() - assert expr.structurally_equivalent( - circuit.data[0].operation.condition, - expr.less(1, circuit.cregs[0]), + program = QCProgram.from_qiskit(circuit) + assert program.ir.count("cbit.read") == 1 + restored = program.to_qiskit() + operands = (expected, restored.cregs[0]) if reverse else (restored.cregs[0], expected) + assert any( + expr.structurally_equivalent(restored.data[0].operation.condition, condition) + for condition in ( + getattr(expr, comparison)(*operands), + getattr(expr, swapped)(*reversed(operands)), + ) ) + assert program.to_qco().to_jeff().ir - reimported = QCProgram.from_qiskit(circuit) - assert "arith.cmpi ugt" in reimported.ir - restored = reimported.to_qiskit() - assert expr.structurally_equivalent( - restored.data[0].operation.condition, - expr.greater(restored.cregs[0], 1), - ) + +@pytest.mark.parametrize( + ("width", "expected"), + [(65, 0), (65, 1 << 64), (65, 1 << 65), (2, False), (2, True), (65, False), (65, True)], + ids=["zero", "highest-bit", "out-of-range", "narrow-false", "narrow-true", "wide-false", "wide-true"], +) +def test_wide_qiskit_tuple_condition_round_trip(width: int, expected: int) -> None: + """Import integer and Boolean register equalities, folding impossible values.""" + circuit = QuantumCircuit(1, width) + with circuit.if_test((circuit.cregs[0], expected)): + circuit.x(0) + + restored = QCProgram.from_qiskit(circuit).to_qiskit() + condition = restored.data[0].operation.condition + if expected < 1 << width: + assert expr.structurally_equivalent(condition, expr.equal(restored.cregs[0], int(expected))) + else: + assert isinstance(condition, expr.Value) + assert not condition.value + + +def test_nested_wide_qiskit_tuple_condition_round_trip() -> None: + """Resolve a captured wide register in a nested tuple condition.""" + circuit = QuantumCircuit(1, 65) + with circuit.if_test((circuit.clbits[0], 0)), circuit.if_test((circuit.cregs[0], 1 << 64)): + circuit.x(0) + + restored = QCProgram.from_qiskit(circuit).to_qiskit() + body = restored.data[0].operation.blocks[0] + assert expr.structurally_equivalent(body.data[0].operation.condition, expr.equal(restored.cregs[0], 1 << 64)) + + +def test_wide_qiskit_reordered_register_capture_is_rejected() -> None: + """Require wide comparisons to read one complete register in bit order.""" + circuit = QuantumCircuit(1, 65) + body = QuantumCircuit(1, 65) + with body.if_test(expr.equal(body.cregs[0], 1)): + body.x(0) + circuit.append(IfElseOp((circuit.clbits[0], 0), body), circuit.qubits, list(reversed(circuit.clbits))) + + with pytest.raises(RuntimeError, match="complete classical register"): + QCProgram.from_qiskit(circuit) def test_wide_cbit_register_comparison_ignores_decimal_digit_limit() -> None: