diff --git a/.agent/plans/qiskit-structured-control-export.md b/.agent/plans/qiskit-structured-control-export.md new file mode 100644 index 0000000000..f9580e9d33 --- /dev/null +++ b/.agent/plans/qiskit-structured-control-export.md @@ -0,0 +1,202 @@ +# Export structured QC control flow to Qiskit + +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 + +MQT Core can import Qiskit control flow into QC MLIR, but flat-only export loses +structured `if`, `for`, `while`, and `switch` operations. After this change, +`QCProgram.to_qiskit()` recreates supported structured operations recursively. +It preserves root qubits, classical bits, scalar parameters, measurement +destinations, instruction order, and nested captures. Unsupported MLIR fails +before the exporter returns a partial Qiskit circuit. + +Qiskit and OpenQASM logical AND and OR use short-circuit evaluation. QC MLIR +represents these expressions as a single-result `scf.if`: AND evaluates its +right operand only in the then region and yields false from else; OR yields true +from then and evaluates its right operand only in else. The exporter accepts +only these two result-bearing forms. It does not reconstruct general Boolean +selection or multiple `scf.if` results. + +## Progress + +- [x] (2026-08-26 09:00Z) Reconstruct the PR tree on current `main` and resolve + the changelog conflict from the current unreleased entry. +- [x] (2026-08-26 09:15Z) Retain recursive structured export and the forwarded + measurement-result fix. +- [x] (2026-08-26 09:30Z) Align Qiskit logical import with OpenQASM by emitting + canonical short-circuit `scf.if` operations. +- [x] (2026-08-26 09:45Z) Remove general Boolean selection, expression cloning, + and multi-result sibling budgets from export. +- [x] (2026-08-26 10:00Z) Replace selection tests with canonical round-trip and + rejection tests. +- [x] (2026-08-26 11:00Z) Build the Python extension and run focused and + complete local validation. +- [x] (2026-08-26 12:30Z) Record validation evidence and prepare the signed + commit series for guarded publication. + +## Surprises & Discoveries + +- Observation: OpenQASM import already emits the required result-bearing + `scf.if` shapes. Evidence: `emitCondition` in + `mlir/lib/Dialect/QC/Translation/OpenQASMToQCEmitter.cpp` emits lazy AND and + OR regions. +- Observation: The old Qiskit importer mapped logical and bitwise operations to + the same eager `arith.andi` and `arith.ori` operations. This evaluated both + logical operands and disagreed with Qiskit and OpenQASM semantics. +- Observation: MLIR's result-bearing `scf.if` is the native structured form for + conditional evaluation. `arith.select` selects already-computed SSA values and + cannot provide short-circuit evaluation. + +## Decision Log + +- Decision: Keep the recursive normalized circuit model and version-specific + Qiskit writer. Rationale: Qiskit 2.5 exposes public Python constructors for + control-flow objects but no equivalent stable C API. The generic translator + remains independent of Python objects. Date/Author: 2026-08-26 / Codex. +- Decision: Use result-bearing `scf.if` as the shared logical short-circuit + representation for Qiskit and OpenQASM import. Rationale: It preserves lazy + evaluation and matches current MLIR structured-control practice. Date/Author: + 2026-08-26 / Codex. +- Decision: Export only canonical single-result Boolean AND and OR shapes. + Rationale: General ternary and multi-result reconstruction are outside issue + #2071 and added cloning, normalization, and independent budget machinery. + Date/Author: 2026-08-26 / Codex. +- Decision: Keep forwarded measurement-result recognition. Rationale: QC cleanup + can replace a classical load with the measurement SSA result; both values + denote the same validated destination Qiskit `Clbit`. Date/Author: 2026-08-26 + / Codex. + +## Outcomes & Retrospective + +Implementation and local validation are complete. The current design keeps the +structured export feature while removing speculative Boolean-selection support. +Compared with the previous PR head, the final diff contains 375 fewer net lines. +The release build, all 4,038 configured CTests, all 219 Qiskit translation +tests, stub generation, MLIR documentation, complete Sphinx documentation, and +the full lint session pass. One CTest is skipped by its existing test policy. + +## Context and Orientation + +`bindings/mlir/qiskit/QiskitTranslation.h` defines the frontend-neutral circuit, +control-flow, expression, register, and parameter records shared by the generic +translator and Qiskit adapters. `bindings/mlir/qiskit/QiskitExport.cpp` +validates a `mlir::QCProgram`, recursively collects instructions, and writes the +validated model through `CircuitWriter`. `bindings/mlir/qiskit/QiskitImport.cpp` +converts captured Qiskit expressions and control flow to QC MLIR. + +`bindings/mlir/qiskit/Qiskit2_5.cpp` is the only layer that constructs Qiskit +Python control-flow objects. It creates child circuits against the root bit +objects, places temporary barriers, finalizes child writers, and replaces the +barriers with Qiskit operations. `test/python/test_mlir_qiskit_translation.py` +contains the end-to-end contract. `docs/mlir/python_compiler_collection.md` +documents the supported MLIR forms. + +A classical snapshot is a `cbit.load` value used by a later condition. Export +rejects the snapshot if an intervening store can make it stale. A returned +classical register initialized as undefined becomes readable only after a +validated unconditional top-level measurement writes the bit. Cleanup-forwarded +measurement results map to that same destination bit. + +## Plan of Work + +Keep recursive collection for result-free `scf.if`, constant-range `scf.for` +without loop-carried state, expression-based `scf.while` without carried state, +and result-free `scf.index_switch`. Preserve capture validation, affine loop +parameter projection, packed-register expressions, snapshot checks, definite +CBit initialization, expression limits, and the preflight-before-writer rule. + +In Qiskit import, emit the left logical operand before a result-bearing +`scf.if`. For AND, emit the right operand and yield it only in the then region; +yield false in else. For OR, yield true in then and emit the right operand only +in else. Require Boolean operands. Keep bitwise AND and OR as eager arithmetic. + +In Qiskit export, require exactly one `i1` result. Recognize AND when the else +yield is false and use the then yield as the right operand. Recognize OR when +the then yield is true and use the else yield as the right operand. Export the +condition and selected right-hand expression recursively, require both regions +to contain only those expression operations or constants, and reject every other +result-bearing shape. + +## Concrete Steps + +Run commands from the repository root. Configure and build with: + + cmake --preset release + cmake --build --preset release + +Run the focused translation tests first: + + uv run --no-sync pytest test/python/test_mlir_qiskit_translation.py + +Then run binding, documentation, and repository checks: + + uvx nox -s stubs + cmake --build --preset release --target mlir-doc + uvx nox --non-interactive -s docs + uvx nox -s lint + git diff --check + +## Validation and Acceptance + +Qiskit logical AND and OR import must contain result-bearing `scf.if`, and +round-trip export must produce structurally equivalent Qiskit expressions. +Bitwise AND and OR must remain eager `arith` operations. Nested OpenQASM AND and +OR must export to the equivalent Qiskit expression. General Boolean ternaries +and multi-result `scf.if` must fail with a clear unsupported-shape error. + +Existing tests must continue to cover nested control flow, captures, loop +ranges, switches, packed registers, expression and depth bounds, snapshots, +undefined bits, and forwarded measurement conditions. Stub generation must +produce no uncommitted generated changes. The full lint session and final diff +check must pass. + +## Idempotence and Recovery + +Builds and tests write only generated files under ignored build or cache +directories and are safe to repeat. If configuration becomes stale, remove only +the named build preset directory after confirming it contains generated output, +then configure again. Never discard unrelated tracked changes. + +Before rewriting a published branch, record its exact remote SHA and create a +backup ref. Push with `--force-with-lease=:` so concurrent +remote updates stop the push. Do not rewrite the child PR #2178 in this task. + +## Artifacts and Notes + +The final history contains four signed commits: implementation, focused tests, +the forwarded-measurement fix, and documentation. Verify each commit with +`git verify-commit` before publication. + +Local validation evidence from 2026-08-26: + + cmake --build --preset release + # passed + ctest --preset release + # 100% tests passed, 0 failed out of 4038; 1 skipped + uv run --no-sync pytest -q test/python/test_mlir_qiskit_translation.py + # 219 passed + uvx nox -s stubs + # passed; no generated tracked changes + cmake --build --preset release --target mlir-doc + # passed + uvx nox --non-interactive -s docs + # passed + uvx nox -s lint + # passed + +## Interfaces and Dependencies + +This change adds no public C++ or Python API. It keeps the internal +`CircuitWriter::addControlFlow` interface and the existing Qiskit 2.5 adapter. +It uses MLIR SCF, Arith, CBit, and QC dialect operations already required by the +translation code. It adds no dependency. + +Revision note: On 2026-08-26, this plan was reduced to the final supported +contract. It now records canonical short-circuit `scf.if` behavior and removes +the abandoned general Boolean-selection design. diff --git a/CHANGELOG.md b/CHANGELOG.md index 53714b9b22..24f3eb6aa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,8 +37,8 @@ releases may include breaking changes. #### Import and export - ✨ Add Qiskit circuit import and target-aware export to the compiler - collection ([#2031], [#2133], [#2140], [#2150], [#2175]) ([**@burgholzer**], - [**@simon1hofmann**]) + collection ([#2031], [#2133], [#2140], [#2150], [#2175], [#2176]) + ([**@burgholzer**], [**@simon1hofmann**]) - ✨ Add conversions between `jeff` and QCO ([#1479], [#1548], [#1565], [#1637], [#1676], [#1706], [#1776], [#1836], [#1934], [#2000], [#2018], [#2105]) ([**@denialhaag**], [**@burgholzer**]) @@ -838,6 +838,7 @@ for previous changelogs._ [#2203]: https://github.com/munich-quantum-toolkit/core/pull/2203 [#2214]: https://github.com/munich-quantum-toolkit/core/pull/2214 [#2193]: https://github.com/munich-quantum-toolkit/core/pull/2193 +[#2176]: https://github.com/munich-quantum-toolkit/core/pull/2176 [#2175]: https://github.com/munich-quantum-toolkit/core/pull/2175 [#2169]: https://github.com/munich-quantum-toolkit/core/pull/2169 [#2168]: https://github.com/munich-quantum-toolkit/core/pull/2168 diff --git a/bindings/mlir/qiskit/Qiskit2_5.cpp b/bindings/mlir/qiskit/Qiskit2_5.cpp index c803ae7c97..21d47562b5 100644 --- a/bindings/mlir/qiskit/Qiskit2_5.cpp +++ b/bindings/mlir/qiskit/Qiskit2_5.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include @@ -1587,10 +1588,323 @@ NativeCircuitReader::controlFlow(const size_t index) const { nb::borrow(data_[index]), pythonCircuit_); } +class PythonClassicalBuilder final { +public: + explicit PythonClassicalBuilder(const nb::handle circuit) + : clbits_(pythonAttribute(circuit, "clbits", + "Qiskit circuit has no classical bits")), + cregs_(pythonAttribute(circuit, "cregs", + "Qiskit circuit has no classical registers")), + expressionModule_( + nb::module_::import_("qiskit.circuit.classical.expr")), + typesModule_(nb::module_::import_("qiskit.circuit.classical.types")) {} + + [[nodiscard]] nb::object expression(const Expression& value) const { + return expression(value, 0U); + } + + [[nodiscard]] nb::object condition(const ClassicalTarget& target) const { + switch (target.kind) { + case ClassicalTargetKind::ClassicalBit: + return nb::make_tuple(classicalBit(target.bit), + nb::bool_(target.expectedBit)); + case ClassicalTargetKind::ClassicalRegister: { + validateRegisterValue(target.reg, target.expectedRegister); + if (const auto reg = registeredClassicalRegister(target.reg)) { + return nb::make_tuple(*reg, nb::int_(target.expectedRegister)); + } + const auto packed = packedRegister(target.reg); + const auto expected = expressionModule_.attr("lift")( + nb::int_(target.expectedRegister), + classicalType(ClassicalType::Uint, + static_cast(target.reg.bits.size()))); + return expressionModule_.attr("equal")(packed, expected); + } + case ClassicalTargetKind::Expression: + if (!target.expression) { + throw std::runtime_error( + "Qiskit control-flow condition has no expression"); + } + if (target.expression->type != ClassicalType::Bool) { + throw std::runtime_error( + "Qiskit control-flow condition expression must be Boolean"); + } + return expression(*target.expression); + } + throw std::runtime_error("Qiskit control flow has an unknown condition"); + } + + [[nodiscard]] nb::object switchTarget(const ClassicalTarget& target) const { + switch (target.kind) { + case ClassicalTargetKind::ClassicalBit: + return classicalBit(target.bit); + case ClassicalTargetKind::ClassicalRegister: + if (target.reg.bits.empty() || target.reg.bits.size() > 64U) { + throw std::runtime_error( + "Qiskit switch registers must contain between 1 and 64 bits"); + } + if (const auto reg = registeredClassicalRegister(target.reg)) { + return *reg; + } + return packedRegister(target.reg); + case ClassicalTargetKind::Expression: + if (!target.expression) { + throw std::runtime_error("Qiskit switch target has no expression"); + } + if (target.expression->type == ClassicalType::Float) { + throw std::runtime_error( + "Qiskit switch target expression cannot be floating-point"); + } + return expression(*target.expression); + } + throw std::runtime_error( + "Qiskit control flow has an unknown switch target"); + } + +private: + [[nodiscard]] nb::object classicalType(const ClassicalType type, + const uint32_t width) const { + switch (type) { + case ClassicalType::Bool: + if (width != 1U) { + throw std::runtime_error("Qiskit Boolean expressions require width 1"); + } + 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"); + } + return typesModule_.attr("Uint")(width); + case ClassicalType::Float: + if (width != 64U) { + throw std::runtime_error( + "Qiskit floating-point expressions require width 64"); + } + return typesModule_.attr("Float")(); + } + throw std::runtime_error("Qiskit expression has an unknown type"); + } + + [[nodiscard]] nb::object classicalBit(const uint32_t bit) const { + if (bit >= nb::len(clbits_)) { + throw std::runtime_error( + "Qiskit classical expression references an invalid bit"); + } + return nb::borrow(clbits_[bit]); + } + + [[nodiscard]] std::optional + registeredClassicalRegister(const Register& reg) const { + if (reg.name.empty()) { + return std::nullopt; + } + for (const nb::handle candidateHandle : nb::iter(cregs_)) { + auto candidate = nb::borrow(candidateHandle); + if (pythonStringAttribute(candidate, "name", + "Qiskit classical register has no name") == + reg.name) { + return candidate; + } + } + return std::nullopt; + } + + static void validateRegisterValue(const Register& reg, const uint64_t value) { + if (reg.bits.empty() || reg.bits.size() > 64U) { + throw std::runtime_error( + "Qiskit condition registers must contain between 1 and 64 bits"); + } + if (reg.bits.size() < std::numeric_limits::digits && + value >= (uint64_t{1} << reg.bits.size())) { + throw std::runtime_error( + "Qiskit register condition value exceeds its register width"); + } + } + + [[nodiscard]] nb::object + packedRegister(const Register& reg, + const uint32_t expressionWidth = 0U) const { + const auto width = expressionWidth == 0U + ? static_cast(reg.bits.size()) + : expressionWidth; + if (reg.bits.empty() || reg.bits.size() > 64U || width < reg.bits.size() || + width > 64U) { + throw std::runtime_error( + "Qiskit expression register has an invalid width"); + } + std::unordered_set seen; + std::vector terms; + terms.reserve(reg.bits.size()); + const auto type = classicalType(ClassicalType::Uint, width); + for (size_t index = 0U; index < reg.bits.size(); ++index) { + if (!seen.insert(reg.bits[index]).second) { + throw std::runtime_error( + "Qiskit expression register contains a repeated bit"); + } + auto term = + expressionModule_.attr("cast")(classicalBit(reg.bits[index]), type); + if (index != 0U) { + term = expressionModule_.attr("shift_left")(term, nb::int_(index)); + } + terms.emplace_back(std::move(term)); + } + while (terms.size() > 1U) { + std::vector reduced; + reduced.reserve((terms.size() + 1U) / 2U); + for (size_t index = 0U; index < terms.size(); index += 2U) { + if (index + 1U == terms.size()) { + reduced.emplace_back(std::move(terms[index])); + continue; + } + reduced.emplace_back( + expressionModule_.attr("bit_or")(terms[index], terms[index + 1U])); + } + terms = std::move(reduced); + } + return std::move(terms.front()); + } + + [[nodiscard]] static const char* binaryFunction(const BinaryOperation op) { + switch (op) { + case BinaryOperation::BitAnd: + return "bit_and"; + case BinaryOperation::BitOr: + return "bit_or"; + case BinaryOperation::BitXor: + return "bit_xor"; + case BinaryOperation::LogicAnd: + return "logic_and"; + case BinaryOperation::LogicOr: + return "logic_or"; + case BinaryOperation::Equal: + return "equal"; + case BinaryOperation::NotEqual: + return "not_equal"; + case BinaryOperation::Less: + return "less"; + case BinaryOperation::LessEqual: + return "less_equal"; + case BinaryOperation::Greater: + return "greater"; + case BinaryOperation::GreaterEqual: + return "greater_equal"; + case BinaryOperation::ShiftLeft: + return "shift_left"; + case BinaryOperation::ShiftRight: + return "shift_right"; + case BinaryOperation::Add: + return "add"; + case BinaryOperation::Subtract: + return "sub"; + case BinaryOperation::Multiply: + return "mul"; + case BinaryOperation::Divide: + return "div"; + } + throw std::runtime_error( + "Qiskit expression has an unknown binary operation"); + } + + [[nodiscard]] static const char* unaryFunction(const UnaryOperation op) { + switch (op) { + case UnaryOperation::BitNot: + return "bit_not"; + case UnaryOperation::LogicNot: + return "logic_not"; + case UnaryOperation::Negate: + return "negate"; + } + throw std::runtime_error( + "Qiskit expression has an unknown unary operation"); + } + + [[nodiscard]] nb::object expression(const Expression& value, + const size_t depth) const { + if (depth >= MAX_EXPRESSION_DEPTH) { + throw std::runtime_error( + "Qiskit classical expressions exceed the nesting limit of 64"); + } + const auto requireOperand = [](const std::unique_ptr& operand) { + if (!operand) { + throw std::runtime_error( + "Qiskit classical expression has a missing operand"); + } + return operand.get(); + }; + switch (value.kind) { + case ExpressionKind::Value: { + const auto type = classicalType(value.type, value.width); + 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)) { + throw std::runtime_error( + "Qiskit unsigned expression value exceeds its width"); + } + return expressionModule_.attr("lift")(nb::int_(value.uintValue), type); + case ClassicalType::Float: + if (!std::isfinite(value.floatValue)) { + throw std::runtime_error( + "Qiskit floating-point expression value must be finite"); + } + return expressionModule_.attr("lift")(nb::float_(value.floatValue), + type); + } + break; + } + case ExpressionKind::ClassicalBit: + if (value.type != ClassicalType::Bool || value.width != 1U) { + throw std::runtime_error( + "Qiskit classical-bit expression must have Boolean type"); + } + 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) { + throw std::runtime_error( + "Qiskit classical-register expression has an invalid type"); + } + if (const auto reg = registeredClassicalRegister(value.reg)) { + return expressionModule_.attr("lift")( + *reg, classicalType(ClassicalType::Uint, value.width)); + } + return packedRegister(value.reg, value.width); + case ExpressionKind::Unary: + return expressionModule_.attr(unaryFunction(value.unaryOperation))( + expression(*requireOperand(value.left), depth + 1U)); + case ExpressionKind::Binary: + return expressionModule_.attr(binaryFunction(value.binaryOperation))( + expression(*requireOperand(value.left), depth + 1U), + expression(*requireOperand(value.right), depth + 1U)); + case ExpressionKind::Cast: + return expressionModule_.attr("cast")( + expression(*requireOperand(value.left), depth + 1U), + classicalType(value.type, value.width)); + case ExpressionKind::Index: + return expressionModule_.attr("index")( + expression(*requireOperand(value.left), depth + 1U), + expression(*requireOperand(value.right), depth + 1U)); + } + throw std::runtime_error("Qiskit classical expression has an unknown kind"); + } + + nb::object clbits_; + nb::object cregs_; + nb::object expressionModule_; + nb::object typesModule_; +}; + +using NativeSymbolTable = std::unordered_map; + class NativeCircuitWriter final : public CircuitWriter { public: - NativeCircuitWriter(const uint32_t looseQubits, const uint32_t looseClbits) - : circuit_(qk_circuit_new(looseQubits, looseClbits)) { + NativeCircuitWriter(const uint32_t looseQubits, const uint32_t looseClbits, + std::shared_ptr symbols) + : circuit_(qk_circuit_new(looseQubits, looseClbits)), + symbols_(std::move(symbols)) { if (circuit_ == nullptr) { throwPythonError("Qiskit failed to allocate a circuit"); } @@ -1707,7 +2021,65 @@ class NativeCircuitWriter final : public CircuitWriter { } } + void + addControlFlow(const ControlFlowKind kind, ClassicalTarget target, Loop loop, + std::vector switchCases, + std::vector> blocks) override { + const bool validBlockCount = [&]() { + switch (kind) { + case ControlFlowKind::IfElse: + return blocks.size() == 1U || blocks.size() == 2U; + case ControlFlowKind::While: + case ControlFlowKind::For: + return blocks.size() == 1U; + case ControlFlowKind::Switch: + return !blocks.empty() && blocks.size() == switchCases.size(); + case ControlFlowKind::Box: + case ControlFlowKind::Break: + case ControlFlowKind::Continue: + return false; + } + return false; + }(); + if (!validBlockCount) { + throw std::runtime_error( + "Qiskit control flow has an unexpected number of blocks"); + } + const auto numQubits = qk_circuit_num_qubits(circuit_); + const auto numClbits = qk_circuit_num_clbits(circuit_); + for (const auto& block : blocks) { + const auto* const native = + dynamic_cast(block.get()); + if (native == nullptr) { + throw std::runtime_error( + "Qiskit control-flow blocks use an incompatible writer"); + } + if (native->circuit_ == nullptr || + qk_circuit_num_qubits(native->circuit_) != numQubits || + qk_circuit_num_clbits(native->circuit_) != numClbits) { + throw std::runtime_error( + "Qiskit control-flow block has incompatible bit counts"); + } + } + const auto instructionIndex = qk_circuit_num_instructions(circuit_); + checkExitCode(qk_circuit_barrier(circuit_, nullptr, 0U), + "adding control-flow placeholder"); + pendingControlFlow_.push_back({.instructionIndex = instructionIndex, + .kind = kind, + .target = std::move(target), + .loop = std::move(loop), + .switchCases = std::move(switchCases), + .blockWriters = std::move(blocks)}); + } + [[nodiscard]] nb::object finish() override { + return finishImpl(false, nb::none(), nb::none()); + } + +private: + [[nodiscard]] nb::object finishImpl(const bool rebase, + const nb::handle exactQubits, + const nb::handle exactClbits) { if (circuit_ == nullptr) { throw std::runtime_error( "Qiskit circuit writer has already been finalized"); @@ -1719,21 +2091,33 @@ class NativeCircuitWriter final : public CircuitWriter { } auto pythonCircuit = nb::steal(result); try { + if (rebase) { + pythonCircuit = rebaseCircuit(pythonCircuit, exactQubits, exactClbits); + } replacePendingControlledUnitaries(pythonCircuit); + replacePendingControlFlow(pythonCircuit); } catch (const nb::python_error& error) { - throwPythonError("Qiskit failed to construct a controlled unitary", + throwPythonError("Qiskit failed to construct deferred instructions", error); } return pythonCircuit; } -private: struct PendingControlledUnitary { size_t instructionIndex = 0U; uint32_t numControls = 0U; std::vector qubits; }; + struct PendingControlFlow { + size_t instructionIndex = 0U; + ControlFlowKind kind = ControlFlowKind::IfElse; + ClassicalTarget target; + Loop loop; + std::vector switchCases; + std::vector> blockWriters; + }; + void replacePendingControlledUnitaries(const nb::handle pythonCircuit) const { auto data = pythonAttribute(pythonCircuit, "data", "Qiskit circuit has no instruction data"); @@ -1769,6 +2153,136 @@ class NativeCircuitWriter final : public CircuitWriter { } } + [[nodiscard]] static nb::object rebaseCircuit(const nb::handle circuit, + const nb::handle exactQubits, + const nb::handle exactClbits) { + auto rebased = nb::module_::import_("qiskit.circuit") + .attr("QuantumCircuit")(exactQubits, exactClbits); + pythonAttribute(rebased, "compose", + "Qiskit circuit cannot compose a control-flow block")( + circuit, nb::arg("inplace") = true, nb::arg("copy") = false); + return rebased; + } + + [[nodiscard]] static nb::object loopIndexSet(const Loop& loop) { + if (!loop.isRange) { + throw std::runtime_error( + "Qiskit circuit export supports only range-based for loops"); + } + return nb::module_::import_("builtins") + .attr("range")(loop.start, loop.stop, loop.step); + } + + [[nodiscard]] static nb::object loopParameter(const Loop& loop, + const nb::handle body) { + if (!loop.parameter) { + return nb::borrow(nb::none()); + } + const auto* symbol = loop.parameter->getSymbol(); + if (symbol == nullptr) { + throw std::runtime_error( + "Qiskit for-loop parameter has invalid symbol metadata"); + } + const auto parameters = pythonAttribute( + body, "parameters", "Qiskit circuit has no parameter collection"); + for (const nb::handle parameter : nb::iter(parameters)) { + if (pythonStringAttribute(parameter, "name", + "Qiskit circuit parameter has no name") == + symbol->name) { + return nb::borrow(parameter); + } + } + throw std::runtime_error( + "Qiskit for-loop parameter is absent from its body"); + } + + [[nodiscard]] static nb::object constructControlFlowOperation( + const PendingControlFlow& pending, const std::vector& blocks, + const PythonClassicalBuilder& classical, const nb::handle circuitModule) { + switch (pending.kind) { + case ControlFlowKind::IfElse: + return circuitModule.attr("IfElseOp")( + classical.condition(pending.target), blocks.front(), + blocks.size() == 2U ? blocks[1] : nb::borrow(nb::none())); + case ControlFlowKind::While: + return circuitModule.attr("WhileLoopOp")( + classical.condition(pending.target), blocks.front()); + case ControlFlowKind::For: + return circuitModule.attr("ForLoopOp")( + loopIndexSet(pending.loop), + loopParameter(pending.loop, blocks.front()), blocks.front()); + case ControlFlowKind::Switch: { + nb::list cases; + for (size_t index = 0U; index < pending.switchCases.size(); ++index) { + const auto& switchCase = pending.switchCases[index]; + nb::object labels; + if (switchCase.isDefault) { + labels = nb::borrow(circuitModule.attr("CASE_DEFAULT")); + } else { + if (switchCase.labels.size() != 1U) { + throw std::runtime_error( + "Qiskit circuit export requires one label per switch case"); + } + labels = nb::int_(switchCase.labels.front()); + } + cases.append(nb::make_tuple(labels, blocks[index])); + } + return circuitModule.attr("SwitchCaseOp")( + classical.switchTarget(pending.target), cases); + } + case ControlFlowKind::Box: + case ControlFlowKind::Break: + case ControlFlowKind::Continue: + break; + } + throw std::runtime_error( + "Qiskit circuit export encountered an unsupported control-flow kind"); + } + + void replacePendingControlFlow(const nb::handle pythonCircuit) { + auto data = pythonAttribute(pythonCircuit, "data", + "Qiskit circuit has no instruction data"); + const auto circuitQubits = pythonAttribute(pythonCircuit, "qubits", + "Qiskit circuit has no qubits"); + const auto circuitClbits = pythonAttribute( + pythonCircuit, "clbits", "Qiskit circuit has no classical bits"); + const auto circuitModule = nb::module_::import_("qiskit.circuit"); + const auto circuitInstruction = circuitModule.attr("CircuitInstruction"); + const PythonClassicalBuilder classical(pythonCircuit); + for (auto& pending : pendingControlFlow_) { + if (pending.instructionIndex >= nb::len(data)) { + throw std::runtime_error("Qiskit control-flow placeholder is missing"); + } + std::vector blocks; + blocks.reserve(pending.blockWriters.size()); + for (const auto& blockWriter : pending.blockWriters) { + auto* const writer = + dynamic_cast(blockWriter.get()); + if (writer == nullptr) { + throw std::runtime_error( + "Qiskit control-flow blocks use an incompatible writer"); + } + blocks.emplace_back( + writer->finishImpl(true, circuitQubits, circuitClbits)); + } + pending.blockWriters.clear(); + auto operation = constructControlFlowOperation(pending, blocks, classical, + circuitModule); + if (pythonUnsignedAttribute(operation, "num_qubits", + "Qiskit control flow has no qubit count") != + nb::len(circuitQubits) || + pythonUnsignedAttribute( + operation, "num_clbits", + "Qiskit control flow has no classical-bit count") != + nb::len(circuitClbits)) { + throw std::runtime_error( + "Qiskit control-flow operation has incompatible bit counts"); + } + data[pending.instructionIndex] = + circuitInstruction(operation, circuitQubits, circuitClbits); + } + } + [[nodiscard]] const QkParam* nativeParameter( const Parameter& parameter, std::vector>& ownedParameters) { @@ -1794,14 +2308,8 @@ class NativeCircuitWriter final : public CircuitWriter { throw std::runtime_error( "cannot export a symbolic parameter without a name"); } - const auto found = symbols_.find(symbol->name); - if (found != symbols_.end()) { - return found->second->get(); - } - auto [inserted, success] = symbols_.emplace( - symbol->name, std::make_unique(symbol->name)); - static_cast(success); - return inserted->second->get(); + return symbols_->try_emplace(symbol->name, symbol->name) + .first->second.get(); } auto output = std::make_unique(); @@ -1877,7 +2385,8 @@ class NativeCircuitWriter final : public CircuitWriter { QkCircuit* circuit_ = nullptr; std::vector pendingControlledUnitaries_; - std::unordered_map> symbols_; + std::vector pendingControlFlow_; + std::shared_ptr symbols_; }; class NativeTranslation final : public VersionedTranslation { @@ -1894,8 +2403,13 @@ class NativeTranslation final : public VersionedTranslation { [[nodiscard]] std::unique_ptr createCircuit(const uint32_t looseQubits, const uint32_t looseClbits) const override { - return std::make_unique(looseQubits, looseClbits); + return std::make_unique(looseQubits, looseClbits, + symbols_); } + +private: + std::shared_ptr symbols_ = + std::make_shared(); }; } // namespace diff --git a/bindings/mlir/qiskit/QiskitExport.cpp b/bindings/mlir/qiskit/QiskitExport.cpp index 1dc865c452..b90fea9f47 100644 --- a/bindings/mlir/qiskit/QiskitExport.cpp +++ b/bindings/mlir/qiskit/QiskitExport.cpp @@ -28,6 +28,8 @@ #include #include #include +#include +#include #include #include #include @@ -37,17 +39,21 @@ #include #include #include +#include #include #include #include #include +#include #include #include +#include #include #include #include #include +#include #include #include #include @@ -61,6 +67,12 @@ namespace mqt::bindings::qiskit { namespace { +constexpr size_t MAX_EXPORT_CONTROL_FLOW_DEPTH = 64U; +constexpr size_t MAX_EXPORT_EXPRESSION_DEPTH = 64U; +constexpr size_t MAX_EXPORT_EXPRESSION_NODES = 4096U; + +struct ExportedControlFlow; + struct ExportedInstruction { enum class Kind : uint8_t { Gate, @@ -68,6 +80,7 @@ struct ExportedInstruction { Reset, Barrier, Unitary, + ControlFlow, }; Kind kind = Kind::Gate; StandardGateMapping gate; @@ -76,10 +89,24 @@ struct ExportedInstruction { std::vector parameters; std::vector> matrix; uint32_t unitaryControls = 0; + std::unique_ptr controlFlow; }; using ExportedParameters = llvm::DenseMap; +struct ExportedCircuit { + Parameter globalPhase = Parameter::number(0.0); + std::vector instructions; +}; + +struct ExportedControlFlow { + ControlFlowKind kind = ControlFlowKind::IfElse; + ClassicalTarget target; + Loop loop; + std::vector switchCases; + std::vector blocks; +}; + [[noreturn]] void throwExportedParameterExpressionSizeError() { throw std::runtime_error("QC parameter expression exceeds the supported " + std::to_string(MAX_PARAMETER_EXPRESSION_NODES) + @@ -203,7 +230,7 @@ using ExportedParameters = llvm::DenseMap; } void validateExportParameterImpl(const Parameter& parameter, const size_t depth, - size_t& nodes) { + size_t& nodes, llvm::StringSet<>& names) { if (depth > MAX_PARAMETER_EXPRESSION_DEPTH) { throwExportedParameterExpressionDepthError(); } @@ -221,23 +248,25 @@ void validateExportParameterImpl(const Parameter& parameter, const size_t depth, throw std::runtime_error( "QC parameter symbol name contains a null character"); } + names.insert(symbol->name); return; } if (const auto* unary = parameter.getUnary()) { - validateExportParameterImpl(*unary->operand, depth + 1U, nodes); + validateExportParameterImpl(*unary->operand, depth + 1U, nodes, names); return; } if (const auto* binary = parameter.getBinary()) { - validateExportParameterImpl(*binary->left, depth + 1U, nodes); - validateExportParameterImpl(*binary->right, depth + 1U, nodes); + validateExportParameterImpl(*binary->left, depth + 1U, nodes, names); + validateExportParameterImpl(*binary->right, depth + 1U, nodes, names); return; } throw std::runtime_error("unknown QC parameter expression"); } -void validateExportParameter(const Parameter& parameter) { +void validateExportParameter(const Parameter& parameter, + llvm::StringSet<>& names) { size_t nodes = 0U; - validateExportParameterImpl(parameter, 1U, nodes); + validateExportParameterImpl(parameter, 1U, nodes, names); } [[nodiscard]] bool isParameterExpressionOperation(mlir::Operation& operation) { @@ -299,45 +328,85 @@ struct ExportState { llvm::DenseMap quantumBases; llvm::DenseMap quantumSizes; llvm::DenseMap classicalRegisterInfo; - std::vector instructions; + llvm::DenseMap> unconditionalWrites; + llvm::DenseMap> measurementDestinations; + llvm::DenseMap measurementResultBits; + llvm::DenseSet expressionOperations; std::vector quantumRegisters; std::vector classicalRegisters; ExportedParameters parameters; std::vector inputParameters; - Parameter globalPhase; + llvm::StringSet<> parameterNames; + size_t nextLoopParameter = 0U; uint32_t numQubits = 0; uint32_t numClbits = 0; }; -void collectParameterNames(const Parameter& parameter, - llvm::StringSet<>& names) { +[[nodiscard]] bool parameterUsesName(const Parameter& parameter, + const std::string_view name) { if (const auto* symbol = parameter.getSymbol()) { - names.insert(symbol->name); - return; + return symbol->name == name; } if (const auto* unary = parameter.getUnary()) { - collectParameterNames(*unary->operand, names); - return; + return parameterUsesName(*unary->operand, name); } if (const auto* binary = parameter.getBinary()) { - collectParameterNames(*binary->left, names); - collectParameterNames(*binary->right, names); + return parameterUsesName(*binary->left, name) || + parameterUsesName(*binary->right, name); } + return false; } -void validateExportParameters(const ExportState& state) { - llvm::StringSet<> usedNames; +[[nodiscard]] bool circuitUsesParameterName(const ExportedCircuit& circuit, + const std::string_view name) { + if (parameterUsesName(circuit.globalPhase, name)) { + return true; + } + for (const auto& instruction : circuit.instructions) { + if (llvm::any_of(instruction.parameters, [&](const auto& parameter) { + return parameterUsesName(parameter, name); + })) { + return true; + } + if (!instruction.controlFlow) { + continue; + } + if (llvm::any_of(instruction.controlFlow->blocks, [&](const auto& block) { + return circuitUsesParameterName(block, name); + })) { + return true; + } + } + return false; +} + +void validateExportParameters(const ExportedCircuit& circuit, + llvm::StringSet<>& usedNames) { const auto validate = [&](const Parameter& parameter) { - validateExportParameter(parameter); - collectParameterNames(parameter, usedNames); + validateExportParameter(parameter, usedNames); }; - validate(state.globalPhase); - for (const auto& instruction : state.instructions) { + validate(circuit.globalPhase); + for (const auto& instruction : circuit.instructions) { for (const auto& parameter : instruction.parameters) { validate(parameter); } + if (!instruction.controlFlow) { + continue; + } + if (instruction.controlFlow->loop.parameter) { + validate(*instruction.controlFlow->loop.parameter); + } + for (const auto& block : instruction.controlFlow->blocks) { + validateExportParameters(block, usedNames); + } } - for (const auto& input : state.inputParameters) { +} + +void validateExportParameters(const ExportedCircuit& circuit, + const std::vector& inputs) { + llvm::StringSet<> usedNames; + validateExportParameters(circuit, usedNames); + for (const auto& input : inputs) { const auto* symbol = input.getSymbol(); if (symbol == nullptr) { throw std::runtime_error("QC program input is not a parameter symbol"); @@ -360,35 +429,44 @@ void collectParameters(mlir::func::FuncOp function, ExportState& state) { throw std::runtime_error( "Qiskit circuit export requires named f64 program inputs"); } + if (name.getValue().contains('\0')) { + throw std::runtime_error( + "Qiskit circuit export does not support parameter names with null " + "characters"); + } + if (!state.parameterNames.insert(name.getValue()).second) { + throw std::runtime_error( + "Qiskit circuit export requires unique parameter names"); + } auto parameter = Parameter::symbol(name.str()); state.parameters[argument] = parameter; state.inputParameters.push_back(std::move(parameter)); } } -void addGlobalPhase(ExportState& state, const Parameter& phase) { +void addGlobalPhase(ExportedCircuit& circuit, const Parameter& phase) { if (const auto* number = phase.getNumber()) { - if (const auto* globalNumber = state.globalPhase.getNumber()) { + if (const auto* globalNumber = circuit.globalPhase.getNumber()) { const auto sum = globalNumber->value + number->value; if (!std::isfinite(sum)) { throw std::runtime_error( "QC global phase cannot be represented by Qiskit"); } - state.globalPhase = Parameter::number(sum); + circuit.globalPhase = Parameter::number(sum); return; } if (std::abs(number->value) <= mlir::mqt::PARAMETER_COMPARISON_TOLERANCE) { return; } - } else if (const auto* globalNumber = state.globalPhase.getNumber(); + } else if (const auto* globalNumber = circuit.globalPhase.getNumber(); globalNumber != nullptr && std::abs(globalNumber->value) <= mlir::mqt::PARAMETER_COMPARISON_TOLERANCE) { - state.globalPhase = phase; + circuit.globalPhase = phase; return; } - state.globalPhase = binaryParameter(BinaryParameterKind::Add, - std::move(state.globalPhase), phase); + circuit.globalPhase = binaryParameter(BinaryParameterKind::Add, + std::move(circuit.globalPhase), phase); } [[nodiscard]] std::vector @@ -713,11 +791,10 @@ void collectResources(mlir::func::FuncOp function, ExportState& state, "QC to Qiskit export encountered an unsupported memory allocation"); } } - for (auto& operation : function.getBody().front()) { - auto load = llvm::dyn_cast(operation); - if (!load || !llvm::isa(load.getResult().getType()) || + function.walk([&](mlir::memref::LoadOp load) { + if (!llvm::isa(load.getResult().getType()) || load.getIndices().size() != 1U) { - continue; + return; } const auto index = mlir::getConstantIntValue(load.getIndices().front()); if (!index) { @@ -736,7 +813,7 @@ void collectResources(mlir::func::FuncOp function, ExportState& state, "QC to Qiskit export encountered an out-of-bounds qubit index"); } state.qubits[load.getResult()] = checkedAdd(base->second, checked, "qubit"); - } + }); auto returnOp = llvm::dyn_cast(function.getBody().front().back()); @@ -744,12 +821,20 @@ void collectResources(mlir::func::FuncOp function, ExportState& state, throw std::runtime_error( "QC to Qiskit export requires an entry-function return"); } + if (returnOp.getNumOperands() == 1U) { + const auto result = returnOp.getOperand(0); + const auto sentinel = mlir::getConstantIntValue(result); + if (result.getType().isInteger(64) && sentinel && *sentinel == 0) { + return; + } + } llvm::DenseSet returnedRegisters; for (const auto result : returnOp.getOperands()) { const auto type = llvm::dyn_cast(result.getType()); if (!type) { - continue; + throw std::runtime_error( + "QC to Qiskit export supports only CBit function return values"); } if (!returnedRegisters.insert(result).second) { throw std::runtime_error( @@ -776,146 +861,1095 @@ void collectResources(mlir::func::FuncOp function, ExportState& state, } } -void collectFlatInstructions(mlir::func::FuncOp function, ExportState& state) { - llvm::DenseMap> writtenBits; - llvm::DenseMap measurementDestinations; +[[nodiscard]] std::optional +constantUnsignedInteger(const mlir::Value value) { + auto constant = value.getDefiningOp(); + const auto integer = + constant ? llvm::dyn_cast(constant.getValue()) + : mlir::IntegerAttr{}; + if (!integer || integer.getValue().getBitWidth() > 64U) { + return std::nullopt; + } + return integer.getValue().getZExtValue(); +} - for (auto store : function.getBody().front().getOps()) { - auto measure = store.getValue().getDefiningOp(); - if (!measure) { +void setExpressionType(Expression& expression, const mlir::Type type) { + if (type.isInteger(1)) { + expression.type = ClassicalType::Bool; + expression.width = 1U; + return; + } + if (const auto integer = llvm::dyn_cast(type)) { + if (integer.getWidth() == 0U || integer.getWidth() > 64U) { throw std::runtime_error( - "QC to Qiskit export does not support non-measurement classical " - "stores"); + "Qiskit unsigned classical values must be between 1 and 64 bits"); } - const auto info = state.classicalRegisterInfo.find(store.getReg()); - const auto index = mlir::getConstantIntValue(store.getIndex()); - if (info == state.classicalRegisterInfo.end()) { + expression.type = ClassicalType::Uint; + expression.width = integer.getWidth(); + return; + } + if (type.isF64()) { + expression.type = ClassicalType::Float; + expression.width = 64U; + return; + } + throw std::runtime_error( + "Qiskit classical expressions support only Bool, Uint, and Float"); +} + +[[nodiscard]] uint32_t classicalBitIndex(mlir::cbit::LoadOp load, + const ExportState& state) { + if (!load.getResult().getType().isInteger(1)) { + throw std::runtime_error( + "Qiskit classical expressions require a static classical-bit load"); + } + const auto info = state.classicalRegisterInfo.find(load.getReg()); + const auto index = mlir::getConstantIntValue(load.getIndex()); + if (info == state.classicalRegisterInfo.end() || !index) { + throw std::runtime_error( + "Qiskit classical expressions could not resolve a classical bit"); + } + const auto checked = checkedIndex(*index, "classical-bit"); + if (checked >= info->second.size) { + throw std::runtime_error( + "Qiskit classical expression uses an out-of-bounds classical bit"); + } + if (info->second.initialization != mlir::cbit::Initialization::Zero) { + const auto written = state.unconditionalWrites.find(load.getReg()); + if (written == state.unconditionalWrites.end() || + !written->second.contains(checked)) { throw std::runtime_error( - "QC measurement stores to a classical register that is not " - "returned"); + "Qiskit classical expression loads an undefined classical bit " + "before an unconditional measurement write"); } - if (!index) { + } + return checkedAdd(info->second.base, checked, "classical-bit"); +} + +[[noreturn]] void throwClassicalExpressionSizeError() { + throw std::runtime_error( + "QC classical expression exceeds the size limit of 4096 nodes"); +} + +[[noreturn]] void throwClassicalExpressionDepthError() { + throw std::runtime_error( + "QC classical expressions exceed the nesting limit of 64"); +} + +void countExpressionNode(size_t& nodeCount) { + if (++nodeCount > MAX_EXPORT_EXPRESSION_NODES) { + throwClassicalExpressionSizeError(); + } +} + +struct PackedRegister { + Register reg; + llvm::SmallPtrSet operations; +}; + +[[nodiscard]] std::optional +matchPackedRegister(mlir::Value value, ExportState& state, + mlir::Block& evaluationBlock); + +[[nodiscard]] std::unique_ptr +exportExpressionImpl(mlir::Value value, ExportState& state, + mlir::Block& evaluationBlock, const size_t depth, + size_t& nodeCount) { + if (depth >= MAX_EXPORT_EXPRESSION_DEPTH) { + throwClassicalExpressionDepthError(); + } + countExpressionNode(nodeCount); + auto* operation = value.getDefiningOp(); + if (operation == nullptr) { + throw std::runtime_error( + "Qiskit classical expressions cannot capture an SSA block argument"); + } + if (!llvm::isa(operation) && + operation->getBlock() != &evaluationBlock) { + throw std::runtime_error( + "Qiskit classical expressions cannot capture a computed SSA value " + "across a control-flow region"); + } + + auto result = std::make_unique(); + setExpressionType(*result, value.getType()); + if (const auto measured = state.measurementResultBits.find(value); + measured != state.measurementResultBits.end()) { + result->kind = ExpressionKind::ClassicalBit; + result->bit = measured->second; + return result; + } + if (result->type == ClassicalType::Uint) { + if (auto packed = matchPackedRegister(value, state, evaluationBlock)) { + result->kind = ExpressionKind::ClassicalRegister; + result->reg = std::move(packed->reg); + state.expressionOperations.insert(packed->operations.begin(), + packed->operations.end()); + return result; + } + } + if (auto constant = llvm::dyn_cast(operation)) { + result->kind = ExpressionKind::Value; + if (const auto integer = + llvm::dyn_cast(constant.getValue())) { + if (result->type == ClassicalType::Bool) { + result->boolValue = !integer.getValue().isZero(); + } else if (result->type == ClassicalType::Uint) { + result->uintValue = integer.getValue().getZExtValue(); + } else { + throw std::runtime_error( + "Qiskit Float expressions require a floating-point constant"); + } + return result; + } + const auto floating = llvm::dyn_cast(constant.getValue()); + if (!floating || result->type != ClassicalType::Float) { throw std::runtime_error( - "QC measurement uses a dynamic classical destination"); + "Qiskit classical expression contains an unsupported constant"); } - const auto checked = checkedIndex(*index, "classical-bit"); - if (checked >= info->second.size) { + result->floatValue = floating.getValueAsDouble(); + if (!std::isfinite(result->floatValue)) { throw std::runtime_error( - "QC measurement uses an out-of-bounds classical destination"); + "Qiskit classical floating-point literals must be finite"); } - if (!writtenBits[store.getReg()].insert(checked).second) { + return result; + } + if (auto load = llvm::dyn_cast(operation)) { + result->kind = ExpressionKind::ClassicalBit; + result->bit = classicalBitIndex(load, state); + state.expressionOperations.insert(operation); + return result; + } + if (auto ifOp = llvm::dyn_cast(operation)) { + if (ifOp.getNumResults() != 1U || !value.getType().isInteger(1) || + ifOp.getElseRegion().empty()) { + throw std::runtime_error( + "Qiskit classical expressions support only canonical " + "short-circuit Boolean scf.if results"); + } + auto& thenBlock = ifOp.getThenRegion().front(); + auto& elseBlock = ifOp.getElseRegion().front(); + auto thenYield = llvm::cast(thenBlock.getTerminator()); + auto elseYield = llvm::cast(elseBlock.getTerminator()); + const auto thenValue = thenYield.getOperand(0); + const auto elseValue = elseYield.getOperand(0); + mlir::Value right; + if (mlir::matchPattern(elseValue, mlir::m_Zero())) { + result->binaryOperation = BinaryOperation::LogicAnd; + right = thenValue; + } else if (mlir::matchPattern(thenValue, mlir::m_One())) { + result->binaryOperation = BinaryOperation::LogicOr; + right = elseValue; + } else { + throw std::runtime_error( + "Qiskit classical expressions support only canonical " + "short-circuit Boolean scf.if results"); + } + auto condition = exportExpressionImpl( + ifOp.getCondition(), state, *ifOp->getBlock(), depth + 1U, nodeCount); + auto rightExpression = exportExpressionImpl( + right, state, + result->binaryOperation == BinaryOperation::LogicAnd ? thenBlock + : elseBlock, + depth + 1U, nodeCount); + const auto validateBranch = [&](mlir::Block& branch) { + for (auto& nested : branch.without_terminator()) { + if (!llvm::isa(nested) && + !state.expressionOperations.contains(&nested)) { + throw std::runtime_error( + "Qiskit Boolean scf.if expressions must be side-effect free"); + } + } + }; + validateBranch(thenBlock); + validateBranch(elseBlock); + state.expressionOperations.insert(operation); + result->kind = ExpressionKind::Binary; + result->left = std::move(condition); + result->right = std::move(rightExpression); + return result; + } + + const auto unary = [&](const ExpressionKind kind, const mlir::Value operand) { + result->kind = kind; + result->left = exportExpressionImpl(operand, state, evaluationBlock, + depth + 1U, nodeCount); + state.expressionOperations.insert(operation); + return std::move(result); + }; + const auto binary = [&](const BinaryOperation kind, const mlir::Value left, + const mlir::Value right) { + result->kind = ExpressionKind::Binary; + result->binaryOperation = kind; + result->left = exportExpressionImpl(left, state, evaluationBlock, + depth + 1U, nodeCount); + result->right = exportExpressionImpl(right, state, evaluationBlock, + depth + 1U, nodeCount); + state.expressionOperations.insert(operation); + return std::move(result); + }; + + if (llvm::isa(operation)) { + return unary(ExpressionKind::Cast, operation->getOperand(0)); + } + if (auto cast = llvm::dyn_cast(operation)) { + if (!cast.getType().isInteger(1)) { + return unary(ExpressionKind::Cast, cast.getIn()); + } + result->kind = ExpressionKind::Index; + if (auto shift = cast.getIn().getDefiningOp()) { + result->left = exportExpressionImpl( + shift.getLhs(), state, evaluationBlock, depth + 1U, nodeCount); + result->right = exportExpressionImpl( + shift.getRhs(), state, evaluationBlock, depth + 1U, nodeCount); + state.expressionOperations.insert(shift); + } else { + result->left = exportExpressionImpl(cast.getIn(), state, evaluationBlock, + depth + 1U, nodeCount); + countExpressionNode(nodeCount); + auto zero = std::make_unique(); + setExpressionType(*zero, cast.getIn().getType()); + zero->kind = ExpressionKind::Value; + zero->uintValue = 0U; + result->right = std::move(zero); + } + state.expressionOperations.insert(operation); + return result; + } + if (auto cast = llvm::dyn_cast(operation)) { + state.expressionOperations.insert(operation); + return exportExpressionImpl(cast.getIn(), state, evaluationBlock, + depth + 1U, nodeCount); + } + if (auto op = llvm::dyn_cast(operation)) { + auto kind = BinaryOperation::Equal; + switch (op.getPredicate()) { + case mlir::arith::CmpIPredicate::eq: + kind = BinaryOperation::Equal; + break; + case mlir::arith::CmpIPredicate::ne: + kind = BinaryOperation::NotEqual; + break; + case mlir::arith::CmpIPredicate::ult: + kind = BinaryOperation::Less; + break; + case mlir::arith::CmpIPredicate::ule: + kind = BinaryOperation::LessEqual; + break; + case mlir::arith::CmpIPredicate::ugt: + kind = BinaryOperation::Greater; + break; + case mlir::arith::CmpIPredicate::uge: + kind = BinaryOperation::GreaterEqual; + break; + default: throw std::runtime_error( - "QC to Qiskit export does not support duplicate classical " - "destinations"); + "Qiskit Uint expressions do not support signed comparisons"); } - if (!measurementDestinations.try_emplace(measure.getOperation(), store) - .second) { + return binary(kind, op.getLhs(), op.getRhs()); + } + if (auto op = llvm::dyn_cast(operation)) { + auto kind = BinaryOperation::Equal; + switch (op.getPredicate()) { + case mlir::arith::CmpFPredicate::OEQ: + kind = BinaryOperation::Equal; + break; + case mlir::arith::CmpFPredicate::UNE: + kind = BinaryOperation::NotEqual; + break; + case mlir::arith::CmpFPredicate::OLT: + kind = BinaryOperation::Less; + break; + case mlir::arith::CmpFPredicate::OLE: + kind = BinaryOperation::LessEqual; + break; + case mlir::arith::CmpFPredicate::OGT: + kind = BinaryOperation::Greater; + break; + case mlir::arith::CmpFPredicate::OGE: + kind = BinaryOperation::GreaterEqual; + break; + default: throw std::runtime_error( - "QC measurement has more than one classical destination"); + "Qiskit Float expressions require ordered comparisons"); } + return binary(kind, op.getLhs(), op.getRhs()); + } + if (auto op = llvm::dyn_cast(operation)) { + return binary(value.getType().isInteger(1) ? BinaryOperation::LogicAnd + : BinaryOperation::BitAnd, + op.getLhs(), op.getRhs()); + } + if (auto op = llvm::dyn_cast(operation)) { + return binary(value.getType().isInteger(1) ? BinaryOperation::LogicOr + : BinaryOperation::BitOr, + op.getLhs(), op.getRhs()); + } + if (auto op = llvm::dyn_cast(operation)) { + return binary(BinaryOperation::BitXor, op.getLhs(), op.getRhs()); + } + if (auto op = llvm::dyn_cast(operation)) { + return binary(BinaryOperation::ShiftLeft, op.getLhs(), op.getRhs()); + } + if (auto op = llvm::dyn_cast(operation)) { + return binary(BinaryOperation::ShiftRight, op.getLhs(), op.getRhs()); } + if (llvm::isa(operation)) { + return binary(BinaryOperation::Add, operation->getOperand(0), + operation->getOperand(1)); + } + if (llvm::isa(operation)) { + return binary(BinaryOperation::Subtract, operation->getOperand(0), + operation->getOperand(1)); + } + if (llvm::isa(operation)) { + return binary(BinaryOperation::Multiply, operation->getOperand(0), + operation->getOperand(1)); + } + if (llvm::isa(operation)) { + return binary(BinaryOperation::Divide, operation->getOperand(0), + operation->getOperand(1)); + } + if (auto op = llvm::dyn_cast(operation)) { + result->unaryOperation = UnaryOperation::Negate; + return unary(ExpressionKind::Unary, op.getOperand()); + } + throw std::runtime_error( + "unsupported QC classical operation in Qiskit export: " + + operation->getName().getStringRef().str()); +} - for (auto& operation : function.getBody().front()) { - if (llvm::isa(operation)) { +void validateExpressionDepth(const Expression& expression, + const size_t depth = 0U) { + if (depth >= MAX_EXPORT_EXPRESSION_DEPTH) { + throwClassicalExpressionDepthError(); + } + if (expression.left) { + validateExpressionDepth(*expression.left, depth + 1U); + } + if (expression.right) { + validateExpressionDepth(*expression.right, depth + 1U); + } +} + +[[nodiscard]] std::unique_ptr +exportExpression(mlir::Value value, ExportState& state, + mlir::Block& evaluationBlock) { + size_t nodeCount = 0U; + auto result = + exportExpressionImpl(value, state, evaluationBlock, 0U, nodeCount); + validateExpressionDepth(*result); + return result; +} + +[[nodiscard]] std::optional +matchPackedRegister(mlir::Value value, ExportState& state, + mlir::Block& evaluationBlock) { + auto type = llvm::dyn_cast(value.getType()); + if (!type || type.getWidth() == 0U || type.getWidth() > 64U) { + return std::nullopt; + } + std::vector> bits(type.getWidth()); + llvm::SmallPtrSet operations; + size_t nodeCount = 0U; + const std::function collect = + [&](const mlir::Value current, const uint32_t shift, const size_t depth) { + if (depth >= MAX_EXPORT_EXPRESSION_DEPTH || + ++nodeCount > MAX_EXPORT_EXPRESSION_NODES) { + return false; + } + auto* operation = current.getDefiningOp(); + if (operation == nullptr) { + return false; + } + if (auto constant = + llvm::dyn_cast(operation)) { + const auto integer = + llvm::dyn_cast(constant.getValue()); + return integer && integer.getValue().isZero(); + } + if (operation->getBlock() != &evaluationBlock) { + return false; + } + if (auto op = llvm::dyn_cast(operation)) { + operations.insert(operation); + return collect(op.getLhs(), shift, depth + 1U) && + collect(op.getRhs(), shift, depth + 1U); + } + if (auto op = llvm::dyn_cast(operation)) { + const auto amount = constantUnsignedInteger(op.getRhs()); + if (!amount || *amount >= bits.size() || + *amount > std::numeric_limits::max() - shift) { + return false; + } + operations.insert(operation); + return collect(op.getLhs(), shift + static_cast(*amount), + depth + 1U); + } + if (auto op = llvm::dyn_cast(operation)) { + operations.insert(operation); + return collect(op.getIn(), shift, depth + 1U); + } + auto load = llvm::dyn_cast(operation); + if (!load || shift >= bits.size() || bits[shift]) { + return false; + } + bits[shift] = classicalBitIndex(load, state); + operations.insert(operation); + return true; + }; + if (!collect(value, 0U, 0U) || + llvm::any_of(bits, [](const auto& bit) { return !bit.has_value(); })) { + return std::nullopt; + } + Register reg; + reg.bits.reserve(bits.size()); + llvm::DenseSet seenBits; + for (const auto bit : bits) { + if (!seenBits.insert(*bit).second) { + return std::nullopt; + } + reg.bits.push_back(*bit); + } + for (const auto& candidate : state.classicalRegisters) { + if (candidate.bits == reg.bits) { + reg.name = candidate.name; + break; + } + } + return PackedRegister{.reg = std::move(reg), + .operations = std::move(operations)}; +} + +void acceptPackedRegister(PackedRegister& packed, ExportState& state) { + state.expressionOperations.insert(packed.operations.begin(), + packed.operations.end()); +} + +[[nodiscard]] bool storesToValueRecursively(mlir::Operation& operation, + const mlir::Value value) { + return operation + .walk([&](mlir::cbit::StoreOp store) { + return store.getReg() == value ? mlir::WalkResult::interrupt() + : mlir::WalkResult::advance(); + }) + .wasInterrupted(); +} + +void validateClassicalSnapshot(const mlir::Value expression, + mlir::Operation& consumer) { + llvm::DenseSet visited; + llvm::SmallVector loads; + llvm::SmallVector worklist{expression}; + while (!worklist.empty()) { + const auto value = worklist.pop_back_val(); + if (!visited.insert(value).second) { + continue; + } + if (visited.size() > MAX_EXPORT_EXPRESSION_NODES) { + throwClassicalExpressionSizeError(); + } + auto* operation = value.getDefiningOp(); + if (operation == nullptr) { + continue; + } + if (auto load = llvm::dyn_cast(operation)) { + loads.push_back(load); continue; } - if (auto alloc = llvm::dyn_cast(operation)) { - if (state.quantumBases.contains(alloc.getResult())) { + if (auto ifOp = llvm::dyn_cast(operation)) { + const auto resultIndex = + llvm::cast(value).getResultNumber(); + for (auto& region : ifOp->getRegions()) { + auto yield = + llvm::cast(region.front().getTerminator()); + worklist.push_back(yield.getOperand(resultIndex)); + } + } + worklist.append(operation->operand_begin(), operation->operand_end()); + } + for (auto load : loads) { + mlir::Operation* anchor = load; + auto* anchorBlock = load->getBlock(); + while (anchorBlock != consumer.getBlock()) { + auto* parent = anchorBlock->getParentOp(); + auto parentIf = llvm::dyn_cast_if_present(parent); + if (!parentIf || parentIf.getNumResults() == 0U) { + throw std::runtime_error( + "Qiskit control-flow expressions cannot capture a classical " + "snapshot across a region"); + } + anchor = parent; + anchorBlock = parent->getBlock(); + } + if (!anchor->isBeforeInBlock(&consumer)) { + throw std::runtime_error( + "Qiskit control-flow expressions cannot capture a classical " + "snapshot across a region"); + } + for (auto* operation = anchor->getNextNode(); operation != &consumer; + operation = operation->getNextNode()) { + if (operation == nullptr) { + throw std::runtime_error( + "Qiskit control-flow expression does not dominate its consumer"); + } + if (auto store = llvm::dyn_cast(operation); + store && store.getReg() == load.getReg()) { + throw std::runtime_error( + "Qiskit control-flow export cannot preserve a stale classical " + "snapshot"); + } + if (operation->getNumRegions() != 0U && + storesToValueRecursively(*operation, load.getReg())) { + throw std::runtime_error( + "Qiskit control-flow export cannot preserve a classical " + "snapshot across nested control flow"); + } + } + } +} + +[[nodiscard]] ClassicalTarget exportCondition(mlir::Value value, + ExportState& state, + mlir::Block& evaluationBlock, + mlir::Operation& consumer) { + if (!value.getType().isInteger(1)) { + throw std::runtime_error( + "Qiskit control-flow conditions must have Boolean type"); + } + validateClassicalSnapshot(value, consumer); + if (auto comparison = value.getDefiningOp(); + comparison && + comparison.getPredicate() == mlir::arith::CmpIPredicate::eq) { + for (const auto [actual, expected] : + std::array{std::pair{comparison.getLhs(), comparison.getRhs()}, + std::pair{comparison.getRhs(), comparison.getLhs()}}) { + const auto constant = constantUnsignedInteger(expected); + if (!constant) { continue; } + if (auto load = actual.getDefiningOp(); + load && actual.getType().isInteger(1) && *constant <= 1U) { + state.expressionOperations.insert(comparison); + state.expressionOperations.insert(load); + return {.kind = ClassicalTargetKind::ClassicalBit, + .bit = classicalBitIndex(load, state), + .expectedBit = *constant != 0U}; + } + if (auto packed = matchPackedRegister(actual, state, evaluationBlock)) { + if (packed->reg.bits.size() != 64U && + *constant >= (uint64_t{1} << packed->reg.bits.size())) { + continue; + } + state.expressionOperations.insert(comparison); + acceptPackedRegister(*packed, state); + return {.kind = ClassicalTargetKind::ClassicalRegister, + .reg = std::move(packed->reg), + .expectedRegister = *constant, + .width = + llvm::cast(actual.getType()).getWidth()}; + } + } + } + ClassicalTarget target{.kind = ClassicalTargetKind::Expression}; + target.expression = exportExpression(value, state, evaluationBlock); + return target; +} + +[[nodiscard]] ClassicalTarget exportSwitchTarget(mlir::Value value, + ExportState& state, + mlir::Block& evaluationBlock, + mlir::Operation& consumer) { + validateClassicalSnapshot(value, consumer); + if (auto cast = value.getDefiningOp()) { + state.expressionOperations.insert(cast); + value = cast.getIn(); + } else if (value.getType().isIndex()) { + if (const auto constant = constantUnsignedInteger(value)) { + auto expression = std::make_unique(); + expression->kind = ExpressionKind::Value; + expression->type = ClassicalType::Uint; + expression->width = 64U; + expression->uintValue = *constant; + return {.kind = ClassicalTargetKind::Expression, + .width = 64U, + .expression = std::move(expression)}; + } + throw std::runtime_error( + "Qiskit switch targets require a constant index or an unsigned " + "integer-to-index cast"); + } + if (auto load = value.getDefiningOp(); + load && value.getType().isInteger(1)) { + state.expressionOperations.insert(load); + return {.kind = ClassicalTargetKind::ClassicalBit, + .bit = classicalBitIndex(load, state)}; + } + if (auto packed = matchPackedRegister(value, state, evaluationBlock)) { + acceptPackedRegister(*packed, state); + return {.kind = ClassicalTargetKind::ClassicalRegister, + .reg = std::move(packed->reg), + .width = llvm::cast(value.getType()).getWidth()}; + } + ClassicalTarget target{.kind = ClassicalTargetKind::Expression}; + target.expression = exportExpression(value, state, evaluationBlock); + if (target.expression->type == ClassicalType::Float) { + throw std::runtime_error("Qiskit switch targets must be Boolean or Uint"); + } + target.width = target.expression->width; + return target; +} + +[[nodiscard]] int64_t checkedAffine(const int64_t multiplier, + const int64_t value, const int64_t offset, + const std::string_view kind) { + const llvm::APInt wideMultiplier(128U, static_cast(multiplier), + true); + const llvm::APInt wideValue(128U, static_cast(value), true); + const llvm::APInt wideOffset(128U, static_cast(offset), true); + const auto result = (wideMultiplier * wideValue) + wideOffset; + if (!result.isSignedIntN(64U)) { + throw std::runtime_error(std::string(kind) + + " cannot be represented safely by Qiskit"); + } + return result.getSExtValue(); +} + +[[nodiscard]] uint64_t rangeLength(const int64_t lower, const int64_t upper, + const int64_t step) { + if (lower >= upper) { + return 0U; + } + const auto distance = + static_cast(upper) - static_cast(lower); + return ((distance - 1U) / static_cast(step)) + 1U; +} + +struct LoopParameterProjection { + mlir::Value value; + int64_t multiplier = 1; + int64_t offset = 0; + llvm::SmallPtrSet operations; +}; + +[[nodiscard]] mlir::Operation* uniqueUser(const mlir::Value value) { + return value.hasOneUse() ? *value.getUsers().begin() : nullptr; +} + +[[nodiscard]] std::optional +matchLoopParameterProjection(mlir::scf::ForOp loop) { + auto* castOperation = uniqueUser(loop.getInductionVar()); + auto cast = + llvm::dyn_cast_if_present(castOperation); + if (!cast || !cast.getOut().getType().isInteger(64)) { + return std::nullopt; + } + LoopParameterProjection projection; + projection.operations.insert(castOperation); + auto current = cast.getOut(); + + if (auto* user = uniqueUser(current)) { + if (auto multiply = llvm::dyn_cast(user)) { + const auto other = + multiply.getLhs() == current ? multiply.getRhs() : multiply.getLhs(); + const auto constant = mlir::getConstantIntValue(other); + if (!constant) { + return std::nullopt; + } + projection.multiplier = *constant; + projection.operations.insert(user); + current = multiply.getResult(); + } + } + if (auto* user = uniqueUser(current)) { + if (auto add = llvm::dyn_cast(user)) { + const auto other = add.getLhs() == current ? add.getRhs() : add.getLhs(); + const auto constant = mlir::getConstantIntValue(other); + if (!constant) { + return std::nullopt; + } + projection.offset = *constant; + projection.operations.insert(user); + current = add.getResult(); + } + } + auto* conversionOperation = uniqueUser(current); + auto conversion = + llvm::dyn_cast_if_present(conversionOperation); + if (!conversion || !conversion.getOut().getType().isF64()) { + return std::nullopt; + } + projection.operations.insert(conversionOperation); + projection.value = conversion.getOut(); + return projection; +} + +[[nodiscard]] ExportedCircuit +collectBlock(mlir::Block& block, ExportState& state, size_t controlFlowDepth); + +void validateControlFlowDepth(const size_t controlFlowDepth) { + if (controlFlowDepth >= MAX_EXPORT_CONTROL_FLOW_DEPTH) { + throw std::runtime_error("QC control flow exceeds the nesting limit of 64"); + } +} + +[[nodiscard]] bool isFusableMeasurementStore(mlir::qc::MeasureOp measure, + mlir::cbit::StoreOp store) { + if (store.getValue() != measure.getResult() || + measure->getBlock() != store->getBlock()) { + return false; + } + for (auto* operation = measure->getNextNode(); operation != store; + operation = operation->getNextNode()) { + if (operation == nullptr || + !llvm::isa(operation)) { + return false; + } + } + return true; +} + +void validateExpressionBlock(mlir::Block& block, const ExportState& state) { + for (auto& operation : block.without_terminator()) { + if (llvm::isa(operation) || + state.expressionOperations.contains(&operation)) { + continue; + } + throw std::runtime_error( + "Qiskit while-loop condition regions must contain only classical " + "expression operations"); + } +} + +[[nodiscard]] std::unique_ptr +collectIf(mlir::scf::IfOp ifOp, ExportState& state, + const size_t controlFlowDepth) { + validateControlFlowDepth(controlFlowDepth); + auto result = std::make_unique(); + result->kind = ControlFlowKind::IfElse; + result->target = exportCondition(ifOp.getCondition(), state, + *ifOp->getBlock(), *ifOp.getOperation()); + result->blocks.push_back( + collectBlock(ifOp.getThenRegion().front(), state, controlFlowDepth + 1U)); + if (!ifOp.getElseRegion().empty()) { + result->blocks.push_back(collectBlock(ifOp.getElseRegion().front(), state, + controlFlowDepth + 1U)); + } + return result; +} + +[[nodiscard]] std::unique_ptr +collectFor(mlir::scf::ForOp loop, ExportState& state, + const size_t controlFlowDepth) { + if (!loop.getInitArgs().empty() || loop.getNumResults() != 0U) { + throw std::runtime_error( + "Qiskit for-loop export does not support loop-carried values"); + } + validateControlFlowDepth(controlFlowDepth); + const auto lower = mlir::getConstantIntValue(loop.getLowerBound()); + const auto upper = mlir::getConstantIntValue(loop.getUpperBound()); + const auto step = mlir::getConstantIntValue(loop.getStep()); + if (!lower || !upper || !step || *step <= 0) { + throw std::runtime_error( + "Qiskit for-loop export requires constant bounds and a positive step"); + } + + auto result = std::make_unique(); + result->kind = ControlFlowKind::For; + result->loop = { + .isRange = true, .start = *lower, .stop = *upper, .step = *step}; + std::optional projection; + std::optional loopParameter; + std::string loopParameterName; + if (!loop.getInductionVar().use_empty()) { + projection = matchLoopParameterProjection(loop); + if (!projection) { throw std::runtime_error( - "QC to Qiskit export encountered an unsupported memory allocation"); + "Qiskit for-loop export supports only a loop induction value used " + "as an f64 gate parameter"); + } + state.expressionOperations.insert(projection->operations.begin(), + projection->operations.end()); + if (!projection->value.use_empty()) { + size_t identity = 0U; + do { + identity = state.nextLoopParameter++; + loopParameterName = "_mqt_loop_" + std::to_string(identity); + } while (state.parameterNames.contains(loopParameterName)); + state.parameterNames.insert(loopParameterName); + loopParameter = Parameter::symbol(loopParameterName); + state.parameters[projection->value] = *loopParameter; + } + } + auto body = collectBlock(*loop.getBody(), state, controlFlowDepth + 1U); + if (projection && loopParameter && + circuitUsesParameterName(body, loopParameterName)) { + result->loop.parameter = *loopParameter; + const auto count = rangeLength(*lower, *upper, *step); + if (count != 0U) { + result->loop.start = + checkedAffine(projection->multiplier, *lower, projection->offset, + "scf.for induction start"); + result->loop.step = checkedAffine(projection->multiplier, *step, 0, + "scf.for induction step"); + if (result->loop.step == 0) { + throw std::runtime_error( + "Qiskit for-loop export cannot represent a constant induction " + "projection"); + } + if (count > static_cast(std::numeric_limits::max())) { + throw std::runtime_error( + "scf.for iteration count is too large for Qiskit"); + } + result->loop.stop = + checkedAffine(result->loop.step, static_cast(count), + result->loop.start, "scf.for induction stop"); } + } + result->blocks.push_back(std::move(body)); + return result; +} + +[[nodiscard]] std::unique_ptr +collectWhile(mlir::scf::WhileOp loop, ExportState& state, + const size_t controlFlowDepth) { + validateControlFlowDepth(controlFlowDepth); + auto& before = loop.getBefore().front(); + auto& after = loop.getAfter().front(); + auto condition = + llvm::dyn_cast(before.getTerminator()); + auto yield = llvm::dyn_cast(after.getTerminator()); + if (!loop.getInits().empty() || loop.getNumResults() != 0U || + before.getNumArguments() != 0U || after.getNumArguments() != 0U || + !condition || !condition.getArgs().empty() || !yield || + yield.getNumOperands() != 0U) { + throw std::runtime_error( + "Qiskit while-loop export does not support loop-carried values"); + } + auto result = std::make_unique(); + result->kind = ControlFlowKind::While; + result->target = exportCondition(condition.getCondition(), state, before, + *condition.getOperation()); + validateExpressionBlock(before, state); + result->blocks.push_back(collectBlock(after, state, controlFlowDepth + 1U)); + return result; +} + +[[nodiscard]] std::unique_ptr +collectSwitch(mlir::scf::IndexSwitchOp switchOp, ExportState& state, + const size_t controlFlowDepth) { + if (switchOp.getNumResults() != 0U) { + throw std::runtime_error( + "Qiskit switch export does not support SSA results"); + } + validateControlFlowDepth(controlFlowDepth); + auto result = std::make_unique(); + result->kind = ControlFlowKind::Switch; + result->target = + exportSwitchTarget(switchOp.getArg(), state, *switchOp->getBlock(), + *switchOp.getOperation()); + const uint32_t targetWidth = result->target.width; + for (const auto [index, label] : llvm::enumerate(switchOp.getCases())) { + if (label < 0) { + throw std::runtime_error( + "Qiskit switch export does not support negative case labels"); + } + if (targetWidth < 64U && + static_cast(label) >= (uint64_t{1} << targetWidth)) { + throw std::runtime_error("Qiskit switch case label " + + std::to_string(label) + " does not fit the " + + std::to_string(targetWidth) + "-bit target"); + } + result->switchCases.push_back({.labels = {static_cast(label)}}); + result->blocks.push_back( + collectBlock(switchOp.getCaseRegions()[index].front(), state, + controlFlowDepth + 1U)); + } + result->switchCases.push_back({.isDefault = true}); + result->blocks.push_back(collectBlock(switchOp.getDefaultRegion().front(), + state, controlFlowDepth + 1U)); + return result; +} + +[[nodiscard]] ExportedCircuit collectBlock(mlir::Block& block, + ExportState& state, + const size_t controlFlowDepth) { + const bool topLevel = controlFlowDepth == 0U; + ExportedCircuit circuit; + llvm::SmallVector deferredExpressions; + for (auto& operation : block) { if (llvm::isa(operation) || isParameterExpressionOperation(operation)) { continue; } + if (llvm::isa(operation)) { + if (!topLevel) { + throw std::runtime_error( + "Qiskit control-flow blocks cannot allocate or release circuit " + "resources"); + } + continue; + } if (auto load = llvm::dyn_cast(operation)) { if (state.qubits.contains(load.getResult())) { continue; } - throw std::runtime_error( - "QC to Qiskit export does not support classical or unknown memory " - "loads"); + deferredExpressions.push_back(&operation); + continue; + } + if (auto load = llvm::dyn_cast(operation)) { + static_cast(classicalBitIndex(load, state)); + deferredExpressions.push_back(&operation); + continue; } if (auto dealloc = llvm::dyn_cast(operation)) { - if (state.quantumBases.contains(dealloc.getMemref())) { + if (topLevel && state.quantumBases.contains(dealloc.getMemref())) { continue; } throw std::runtime_error( "QC to Qiskit export encountered an unsupported memory deallocation"); } - if (llvm::isa(operation)) { - throw std::runtime_error( - "QC to Qiskit export does not support classical loads or control " - "flow"); - } - if (llvm::isa(operation)) { - continue; - } - if (llvm::isa(operation)) { + if (auto store = llvm::dyn_cast(operation)) { + if (!store.getValue().getDefiningOp()) { + throw std::runtime_error( + "QC to Qiskit export does not support non-measurement classical " + "stores"); + } continue; } if (auto phase = llvm::dyn_cast(operation)) { - addGlobalPhase(state, + addGlobalPhase(circuit, exportParameter(phase.getTheta(), state.parameters)); continue; } if (auto measure = llvm::dyn_cast(operation)) { - const auto destination = - measurementDestinations.find(measure.getOperation()); - if (destination == measurementDestinations.end()) { + mlir::cbit::StoreOp destination; + for (auto& use : measure.getResult().getUses()) { + if (const auto store = + llvm::dyn_cast(use.getOwner())) { + if (destination) { + throw std::runtime_error( + "QC measurement has more than one classical destination"); + } + destination = store; + } + } + if (!destination) { throw std::runtime_error( "QC measurement is missing a static classical destination"); } - auto store = destination->second; - const auto info = state.classicalRegisterInfo.find(store.getReg()); - const auto index = mlir::getConstantIntValue(store.getIndex()); - if (info == state.classicalRegisterInfo.end() || !index) { + const auto info = state.classicalRegisterInfo.find(destination.getReg()); + const auto index = mlir::getConstantIntValue(destination.getIndex()); + if (info == state.classicalRegisterInfo.end()) { throw std::runtime_error( "QC measurement uses an unsupported classical destination"); } + if (!index) { + throw std::runtime_error( + "QC measurement uses a dynamic classical destination"); + } + if (!isFusableMeasurementStore(measure, destination)) { + throw std::runtime_error( + "QC measurement destination must follow the measurement in the " + "same block"); + } const auto checked = checkedIndex(*index, "classical-bit"); if (checked >= info->second.size) { throw std::runtime_error( "QC measurement uses an out-of-bounds classical destination"); } - state.instructions.push_back( + if (!state.measurementDestinations[destination.getReg()] + .insert(checked) + .second) { + throw std::runtime_error( + "QC to Qiskit export does not support duplicate classical " + "destinations"); + } + if (topLevel) { + state.unconditionalWrites[destination.getReg()].insert(checked); + } + const auto destinationBit = + checkedAdd(info->second.base, checked, "classical-bit"); + circuit.instructions.push_back( {.kind = ExportedInstruction::Kind::Measure, .qubits = mapQubits(measure.getQubit(), state.qubits), - .clbits = { - checkedAdd(info->second.base, checked, "classical-bit")}}); + .clbits = {destinationBit}}); + state.measurementResultBits.try_emplace(measure.getResult(), + destinationBit); continue; } if (auto reset = llvm::dyn_cast(operation)) { - state.instructions.push_back( + circuit.instructions.push_back( {.kind = ExportedInstruction::Kind::Reset, .qubits = mapQubits(reset.getQubit(), state.qubits)}); continue; } if (auto barrier = llvm::dyn_cast(operation)) { - state.instructions.push_back( + circuit.instructions.push_back( {.kind = ExportedInstruction::Kind::Barrier, .qubits = mapQubits(barrier.getQubits(), state.qubits)}); continue; } if (llvm::isa(operation)) { - state.instructions.push_back( + circuit.instructions.push_back( collectUnitaryInstruction(operation, state.qubits, state.parameters)); continue; } - if (llvm::isa(operation)) { - throw std::runtime_error( - "QC to Qiskit export cannot construct structured control flow " - "through the Qiskit 2.5 C API"); + if (auto ifOp = llvm::dyn_cast(operation)) { + if (ifOp.getNumResults() != 0U) { + if (ifOp.getNumResults() != 1U || + !ifOp.getResult(0).getType().isInteger(1)) { + throw std::runtime_error( + "Qiskit if/else export supports only one canonical " + "short-circuit Boolean SSA result"); + } + deferredExpressions.push_back(&operation); + continue; + } + circuit.instructions.push_back( + {.kind = ExportedInstruction::Kind::ControlFlow, + .controlFlow = collectIf(ifOp, state, controlFlowDepth)}); + continue; + } + if (auto loop = llvm::dyn_cast(operation)) { + circuit.instructions.push_back( + {.kind = ExportedInstruction::Kind::ControlFlow, + .controlFlow = collectFor(loop, state, controlFlowDepth)}); + continue; + } + if (auto loop = llvm::dyn_cast(operation)) { + circuit.instructions.push_back( + {.kind = ExportedInstruction::Kind::ControlFlow, + .controlFlow = collectWhile(loop, state, controlFlowDepth)}); + continue; + } + if (auto switchOp = llvm::dyn_cast(operation)) { + circuit.instructions.push_back( + {.kind = ExportedInstruction::Kind::ControlFlow, + .controlFlow = collectSwitch(switchOp, state, controlFlowDepth)}); + continue; } if (llvm::isa(operation)) { - state.instructions.push_back( + circuit.instructions.push_back( collectUnitaryInstruction(operation, state.qubits, state.parameters)); continue; } + if (llvm::isa(operation)) { + auto yield = llvm::cast(operation); + if (yield.getNumOperands() != 0U) { + throw std::runtime_error( + "Qiskit control-flow export does not support yielded SSA values"); + } + continue; + } + if (operation.getDialect() == + operation.getContext()->getLoadedDialect()) { + deferredExpressions.push_back(&operation); + continue; + } if (operation.getNumResults() == 1U && operation.getResult(0).getType().isF64()) { throw std::runtime_error("Qiskit circuit export does not support scalar " @@ -925,14 +1959,74 @@ void collectFlatInstructions(mlir::func::FuncOp function, ExportState& state) { throw std::runtime_error("unsupported QC operation in Qiskit export: " + operation.getName().getStringRef().str()); } + for (auto* operation : deferredExpressions) { + if (!state.expressionOperations.contains(operation)) { + throw std::runtime_error( + "QC to Qiskit export found classical execution outside a supported " + "control-flow expression"); + } + } + return circuit; +} - for (const auto& [reg, info] : state.classicalRegisterInfo) { - if (info.initialization == mlir::cbit::Initialization::Zero) { +void validateConstructibleGates(const ExportedCircuit& circuit, + const VersionedTranslation& translation) { + for (const auto& instruction : circuit.instructions) { + if (instruction.kind == ExportedInstruction::Kind::Gate && + !translation.supportsGate(instruction.gate)) { + const auto& descriptor = + mlir::qc::getStandardGateDescriptor(instruction.gate.gate); + throw std::runtime_error( + "Qiskit output cannot construct standard gate '" + + descriptor.operationSymbol.str() + "' with " + + std::to_string(instruction.gate.controls) + " controls"); + } + if (instruction.kind != ExportedInstruction::Kind::ControlFlow) { continue; } - if (writtenBits[reg].size() != info.size) { - throw std::runtime_error( - "QC to Qiskit export cannot return undefined classical bits"); + for (const auto& block : instruction.controlFlow->blocks) { + validateConstructibleGates(block, translation); + } + } +} + +void emitCircuit(ExportedCircuit& circuit, CircuitWriter& writer, + const VersionedTranslation& translation, + const uint32_t numQubits, const uint32_t numClbits) { + writer.setGlobalPhase(circuit.globalPhase); + for (auto& instruction : circuit.instructions) { + switch (instruction.kind) { + case ExportedInstruction::Kind::Gate: + writer.addGate(instruction.gate, instruction.qubits, + instruction.parameters); + break; + case ExportedInstruction::Kind::Measure: + writer.addMeasure(instruction.qubits.at(0), instruction.clbits.at(0)); + break; + case ExportedInstruction::Kind::Reset: + writer.addReset(instruction.qubits.at(0)); + break; + case ExportedInstruction::Kind::Barrier: + writer.addBarrier(instruction.qubits); + break; + case ExportedInstruction::Kind::Unitary: + writer.addUnitary(instruction.matrix, instruction.qubits, + instruction.unitaryControls); + break; + case ExportedInstruction::Kind::ControlFlow: { + auto& control = *instruction.controlFlow; + std::vector> blocks; + blocks.reserve(control.blocks.size()); + for (auto& block : control.blocks) { + auto blockWriter = translation.createCircuit(numQubits, numClbits); + emitCircuit(block, *blockWriter, translation, numQubits, numClbits); + blocks.push_back(std::move(blockWriter)); + } + writer.addControlFlow(control.kind, std::move(control.target), + std::move(control.loop), + std::move(control.switchCases), std::move(blocks)); + break; + } } } } @@ -960,8 +2054,19 @@ nb::object exportCircuit(const mlir::QCProgram& program, "target qubit count"); } collectResources(function, state, target); - collectFlatInstructions(function, state); - validateExportParameters(state); + auto circuit = collectBlock(function.getBody().front(), state, 0U); + for (const auto& [reg, info] : state.classicalRegisterInfo) { + if (info.initialization == mlir::cbit::Initialization::Zero) { + continue; + } + const auto written = state.unconditionalWrites.find(reg); + if (written == state.unconditionalWrites.end() || + written->second.size() != info.size) { + throw std::runtime_error( + "QC to Qiskit export cannot return undefined classical bits"); + } + } + validateExportParameters(circuit, state.inputParameters); if (target != nullptr) { Register reg{.name = "q"}; reg.bits.resize(state.numQubits); @@ -974,18 +2079,7 @@ nb::object exportCircuit(const mlir::QCProgram& program, state.numClbits, "classical"); auto translation = selectTranslation(); - for (const auto& instruction : state.instructions) { - if (instruction.kind != ExportedInstruction::Kind::Gate || - translation->supportsGate(instruction.gate)) { - continue; - } - const auto& descriptor = - mlir::qc::getStandardGateDescriptor(instruction.gate.gate); - throw std::runtime_error("Qiskit output cannot construct standard gate '" + - descriptor.operationSymbol.str() + "' with " + - std::to_string(instruction.gate.controls) + - " controls"); - } + validateConstructibleGates(circuit, *translation); auto writer = translation->createCircuit(looseQubits, looseClbits); for (const auto& reg : state.quantumRegisters) { writer->addQuantumRegister(reg.name, @@ -995,28 +2089,7 @@ nb::object exportCircuit(const mlir::QCProgram& program, writer->addClassicalRegister(reg.name, static_cast(reg.bits.size())); } - writer->setGlobalPhase(state.globalPhase); - for (const auto& instruction : state.instructions) { - switch (instruction.kind) { - case ExportedInstruction::Kind::Gate: - writer->addGate(instruction.gate, instruction.qubits, - instruction.parameters); - break; - case ExportedInstruction::Kind::Measure: - writer->addMeasure(instruction.qubits.at(0), instruction.clbits.at(0)); - break; - case ExportedInstruction::Kind::Reset: - writer->addReset(instruction.qubits.at(0)); - break; - case ExportedInstruction::Kind::Barrier: - writer->addBarrier(instruction.qubits); - break; - case ExportedInstruction::Kind::Unitary: - writer->addUnitary(instruction.matrix, instruction.qubits, - instruction.unitaryControls); - break; - } - } + emitCircuit(circuit, *writer, *translation, state.numQubits, state.numClbits); return writer->finish(); } diff --git a/bindings/mlir/qiskit/QiskitImport.cpp b/bindings/mlir/qiskit/QiskitImport.cpp index 9a1d395196..24dcd675a0 100644 --- a/bindings/mlir/qiskit/QiskitImport.cpp +++ b/bindings/mlir/qiskit/QiskitImport.cpp @@ -611,7 +611,8 @@ packRegister(mlir::qc::QCProgramBuilder& builder, } const auto width = static_cast(reg.bits.size()); const auto type = builder.getIntegerType(width); - auto packed = integerConstant(builder, width, 0U); + llvm::SmallVector terms; + terms.reserve(reg.bits.size()); for (size_t index = 0; index < reg.bits.size(); ++index) { auto bit = castInteger( builder, @@ -622,9 +623,23 @@ packRegister(mlir::qc::QCProgramBuilder& builder, integerConstant(builder, width, index)) .getResult(); } - packed = mlir::arith::OrIOp::create(builder, packed, bit).getResult(); + terms.push_back(bit); } - return packed; + while (terms.size() > 1U) { + const auto reducedSize = (terms.size() + 1U) / 2U; + for (size_t index = 0U; index < reducedSize; ++index) { + const auto left = 2U * index; + if (left + 1U < terms.size()) { + terms[index] = + mlir::arith::OrIOp::create(builder, terms[left], terms[left + 1U]) + .getResult(); + } else { + terms[index] = terms[left]; + } + } + terms.resize(reducedSize); + } + return terms.front(); } [[nodiscard]] mlir::Value @@ -738,6 +753,37 @@ emitExpression(mlir::qc::QCProgramBuilder& builder, case ExpressionKind::Binary: { auto left = emitExpression(builder, *expression.left, classicalBits, rootClbitMap); + if (expression.binaryOperation == BinaryOperation::LogicAnd || + expression.binaryOperation == BinaryOperation::LogicOr) { + if (!left.getType().isInteger(1)) { + throw std::runtime_error( + "Qiskit logical operation requires Boolean operands"); + } + const auto emitRight = [&]() { + auto right = emitExpression(builder, *expression.right, classicalBits, + rootClbitMap); + if (!right.getType().isInteger(1)) { + throw std::runtime_error( + "Qiskit logical operation requires Boolean operands"); + } + return right; + }; + const auto isAnd = + expression.binaryOperation == BinaryOperation::LogicAnd; + return mlir::scf::IfOp::create( + builder, left, + [&](mlir::OpBuilder&, mlir::Location) { + mlir::scf::YieldOp::create( + builder, + isAnd ? emitRight() : builder.boolConstant(true)); + }, + [&](mlir::OpBuilder&, mlir::Location) { + mlir::scf::YieldOp::create( + builder, + isAnd ? builder.boolConstant(false) : emitRight()); + }) + .getResult(0); + } auto right = emitExpression(builder, *expression.right, classicalBits, rootClbitMap); const auto comparison = [&]() -> std::optional { @@ -820,10 +866,8 @@ emitExpression(mlir::qc::QCProgramBuilder& builder, right = castInteger(builder, right, integerType); switch (expression.binaryOperation) { case BinaryOperation::BitAnd: - case BinaryOperation::LogicAnd: return mlir::arith::AndIOp::create(builder, left, right).getResult(); case BinaryOperation::BitOr: - case BinaryOperation::LogicOr: return mlir::arith::OrIOp::create(builder, left, right).getResult(); case BinaryOperation::BitXor: return mlir::arith::XOrIOp::create(builder, left, right).getResult(); diff --git a/bindings/mlir/qiskit/QiskitTranslation.h b/bindings/mlir/qiskit/QiskitTranslation.h index f6780ca7a4..c7bdc5490c 100644 --- a/bindings/mlir/qiskit/QiskitTranslation.h +++ b/bindings/mlir/qiskit/QiskitTranslation.h @@ -348,6 +348,10 @@ class CircuitWriter { virtual void addUnitary(const std::vector>& matrix, const std::vector& qubits, uint32_t numControls) = 0; + virtual void + addControlFlow(ControlFlowKind kind, ClassicalTarget target, Loop loop, + std::vector switchCases, + std::vector> blocks) = 0; /** Transfer the native circuit to a new owned Python QuantumCircuit. */ [[nodiscard]] virtual nb::object finish() = 0; }; diff --git a/docs/mlir/python_compiler_collection.md b/docs/mlir/python_compiler_collection.md index ed65eafc96..49bc54678f 100644 --- a/docs/mlir/python_compiler_collection.md +++ b/docs/mlir/python_compiler_collection.md @@ -159,8 +159,8 @@ This compiler route does not construct an intermediate interfaces remain independent and retain their existing version range and behavior. -Import and export have different contracts because Qiskit 2.5 can inspect more -program structures than its C API can construct. +Qiskit 2.5's C API cannot construct classical expressions or structured control +flow, so export uses Qiskit's public Python classes for these operations. | Circuit feature | Import | Export | | ----------------------------------------------------------------- | -------------------- | -------------- | @@ -169,10 +169,10 @@ program structures than its C API can construct. | Measurement, reset, and barrier | Supported | Supported | | Canonical named registers and leading loose bits | Supported | Supported | | Custom instructions with finite, acyclic definitions | Recursively expanded | Not applicable | -| Nested `if`/`else`, `for`, `while`, and `switch` | Supported | Rejected | -| Classical-bit and register conditions | Supported | Rejected | -| Constant Boolean, `Uint` up to 64 bits, and `Float` expressions | Supported | Rejected | -| Clbit and ClassicalRegister expression variables | Supported | Rejected | +| Nested `if`/`else`, `for`, `while`, and `switch` | Supported | Supported | +| Classical-bit and register conditions | Supported | Supported | +| Constant Boolean, `Uint` up to 64 bits, and `Float` expressions | Supported | Supported | +| Clbit and ClassicalRegister expression variables | Supported | Supported | | Standalone classical runtime variables | Rejected | Rejected | | Free symbols and supported real parameter expressions | Supported | Supported | | Parameter-vector elements | Rejected | Not emitted | @@ -199,6 +199,44 @@ after their symbols and expressions are resolved. Definition expansion rejects missing definitions, cycles, operand arity mismatches, nesting beyond 64 levels, and more than 10 million expanded operations. +Structured-control export accepts result-free {code}`scf.if`, constant-range +{code}`scf.for` without loop-carried values, expression-based {code}`scf.while` +without carried state, and result-free {code}`scf.index_switch`. A +result-bearing {code}`scf.if` is accepted only for one Boolean result in the +canonical short-circuit form. Logical AND evaluates its right operand in the +then branch and yields false from the else branch. Logical OR yields true from +the then branch and evaluates its right operand in the else branch. General +Boolean selection and multiple results are rejected. A live {code}`scf.for` +induction value must reduce to an affine {code}`f64` gate parameter. The +exporter preserves one Qiskit parameter identity for that value throughout its +lexical body. An {code}`scf.index_switch` selector must be a constant index or a +supported Boolean/Uint expression converted with {code}`arith.index_castui`. +Switch labels must be nonnegative constants that fit the target width. + +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 expression trees may contain at most 4,096 nodes. +Boolean, unsigned-integer up to 64 bits, and floating-point expression +operations must have a direct Qiskit equivalent. Unsupported operations, signed +interpretations, invalid widths, non-finite constants, dynamic bounds, +loop-carried values, and other SSA results fail during validation. The sole +exception is Core's canonical constant-zero `i64` exit-code sentinel for a +circuit without classical outputs. + +Conditions and switch targets may read a zero-initialized public CBit register. +An undefined public CBit may be read only after an unconditional top-level +measurement write to that bit, and every bit of an undefined returned register +must be written unconditionally. Branch-local writes do not establish definite +initialization. A captured classical snapshot must not cross a later CBit write +or a nested write to the same register. + +Each exported measurement must write to one static public CBit in the same +block, and destinations must be unique. Its destination store must follow the +measurement directly, apart from constant operations. A conditional or otherwise +delayed destination store is rejected because Qiskit cannot preserve it as one +measurement instruction. The measurement result may feed supported classical +expressions after that store and is exported as the destination CBit. + Dense numeric unitaries remain explicit matrix operations during import and export. Target compilation synthesizes supported one- and two-qubit matrices to the target gate set. Dense unitary operations support at most eight qubits. @@ -210,9 +248,11 @@ A circuit remains valid when {code}`circ.layout` is present. The importer translates the circuit operations and deliberately does not preserve physical or virtual layout metadata. -Input validation finishes before an MLIR module is created. Output validation -finishes before a Qiskit circuit is allocated. Unsupported programs therefore -fail without modifying the source object or exposing a partial result. +Input validation finishes before an MLIR module is created. Generic output +validation finishes before Qiskit construction starts; the version-specific +adapter validates its constructed blocks before returning the top-level circuit. +Unsupported programs therefore fail without modifying the source object or +exposing a partial result. The binding imports Qiskit only when circuit translation is requested. It accepts versions in the registered {code}`>=2.5.0,<2.6.0` range and verifies the diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index 02cad0652d..7d50f83e40 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -549,6 +549,61 @@ def test_target_compiled_openqasm2_measurements_export() -> None: assert restored.count_ops() == {"measure": 2, "x": 1} +def test_cleanup_forwards_measurement_results_to_qiskit_condition() -> None: + """Export a condition after cleanup forwards its measurement loads.""" + program = QCProgram.from_qasm_str( + """OPENQASM 2.0; +include "qelib1.inc"; +qreg q[3]; +creg c[2]; +measure q[0] -> c[0]; +measure q[1] -> c[1]; +if (c == 3) x q[2]; +""" + ) + optimized = program.to_qco(copy=True) + optimized.cleanup() + + restored = optimized.to_qc(copy=True).to_qiskit() + + assert restored.count_ops() == {"measure": 2, "if_else": 1} + assert restored.data[2].operation.blocks[0].count_ops() == {"x": 1} + condition = restored.data[2].operation.condition + assert isinstance(condition, expr.Expr) + assert expr.structurally_equivalent(condition, expr.logic_and(*restored.clbits)) + + +def test_openqasm_short_circuit_expression_exports_to_qiskit() -> None: + """Export nested OpenQASM short-circuit logic through canonical scf.if.""" + program = QCProgram.from_qasm_str( + """OPENQASM 3.0; +include "stdgates.inc"; +qubit[3] q; +bit[2] c; +c[0] = measure q[0]; +c[1] = measure q[1]; +if (c[0] && (c[1] || !c[0])) x q[2]; +""" + ) + + restored = program.to_qiskit() + condition = restored.data[2].operation.condition + expected = expr.logic_and( + restored.clbits[0], + expr.logic_or( + restored.clbits[1], + expr.bit_xor( + restored.clbits[0], + True, # ruff: ignore[boolean-positional-value-in-call] Qiskit expression arguments are positional-only. + ), + ), + ) + + assert program.ir.count("scf.if") >= 2 + assert isinstance(condition, expr.Expr) + assert expr.structurally_equivalent(condition, expected) + + def test_openqasm3_measurement_export_uses_undefined_cbit_register() -> None: """Represent OpenQASM 3 output initialization without poison values.""" program = QCProgram.from_qasm_str( @@ -591,6 +646,43 @@ def test_flat_export_rejects_undefined_returned_bits() -> None: program.to_qiskit() +def test_qiskit_export_rejects_noncanonical_i64_function_result() -> None: + """Reject a nonzero i64 result instead of treating it as the output sentinel.""" + program = QCProgram.from_mlir_str( + """module { + func.func @main() -> i64 attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + %value = arith.constant 1 : i64 + qc.dealloc %q : !qc.qubit + return %value : i64 + } +} +""" + ) + + with pytest.raises(RuntimeError, match="supports only CBit function return values"): + program.to_qiskit() + + +def test_qiskit_export_rejects_mixed_sentinel_and_cbit_results() -> None: + """Reject the zero sentinel when it is mixed with a public CBit result.""" + program = QCProgram.from_mlir_str( + """module { + func.func @main() -> (i64, !cbit.reg<1>) attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + %classical = cbit.alloc(#cbit.init) {mqt.register_name = "c"} : !cbit.reg<1> + %zero = arith.constant 0 : i64 + qc.dealloc %q : !qc.qubit + return %zero, %classical : i64, !cbit.reg<1> + } +} +""" + ) + + with pytest.raises(RuntimeError, match="supports only CBit function return values"): + program.to_qiskit() + + def test_qiskit_round_trip_preserves_anonymous_clbits() -> None: """Represent loose Qiskit clbits as one anonymous public CBit register.""" circuit = QuantumCircuit(1) @@ -976,12 +1068,13 @@ def test_noncanonical_register_membership_is_rejected(resource: str, layout: str def test_nested_structured_control_and_bound_loop_parameter() -> None: - """Import nested control flow while keeping induction values lexical.""" + """Round-trip structured control while keeping induction values lexical.""" circuit = QuantumCircuit(2, 2) with circuit.for_loop(range(1, 5, 2), None, None, None, None, label=None) as iteration: - circuit.rx(iteration, 0) - with circuit.if_test((circuit.clbits[0], False)): - circuit.cx(0, 1) + with circuit.if_test((circuit.clbits[0], False)) as else_: + circuit.ry(iteration, 1) + with else_: + circuit.rz(iteration, 1) with circuit.while_loop((circuit.cregs[0], 0), None, None, None, label=None): circuit.measure(0, 0) with circuit.switch(circuit.cregs[0], None, None, None, label=None) as case: @@ -991,15 +1084,694 @@ def test_nested_structured_control_and_bound_loop_parameter() -> None: circuit.z(1) program = compile_program(circuit) + source = program.ir assert "scf.for" in program.ir assert "scf.if" in program.ir assert "scf.while" in program.ir assert "scf.index_switch" in program.ir - with pytest.raises(RuntimeError, match=r"classical loads or control flow|cannot construct structured control flow"): + restored = program.to_qiskit() + + assert program.ir == source + assert [instruction.operation.name for instruction in restored.data] == [ + "for_loop", + "while_loop", + "switch_case", + ] + loop = restored.data[0].operation + loop_parameter = loop.params[1] + loop_body = loop.blocks[0] + branch = loop_body.data[0].operation + assert branch.name == "if_else" + assert branch.blocks[0].data[0].operation.params[0].uuid == loop_parameter.uuid + assert branch.blocks[1].data[0].operation.params[0].uuid == loop_parameter.uuid + switch_cases = list(restored.data[2].operation.cases_specifier()) + assert [labels for labels, _ in switch_cases] == [(0,), (1,), (CASE_DEFAULT,)] + assert [[instruction.operation.name for instruction in body.data] for _, body in switch_cases] == [ + ["x"], + ["x"], + ["z"], + ] + QCProgram.from_qiskit(restored) + + +def test_control_flow_and_controlled_unitary_preserve_instruction_order() -> None: + """Keep both deferred instruction kinds at their original positions.""" + circuit = QuantumCircuit(2, 1) + circuit.h(0) + controlled = library.UnitaryGate(np.asarray([[0.0, 1.0], [1.0, 0.0]])).control(1) + with circuit.if_test((circuit.clbits[0], True)): + circuit.append(controlled, [0, 1]) + circuit.z(1) + + restored = QCProgram.from_qiskit(circuit).to_qiskit() + + assert [instruction.operation.name for instruction in restored.data] == ["h", "if_else", "z"] + body_operation = restored.data[1].operation.blocks[0].data[0].operation + assert isinstance(body_operation, AnnotatedOperation) + assert isinstance(body_operation.modifiers[0], ControlModifier) + + +@pytest.mark.parametrize("num_clbits", [3, 64]) +def test_root_register_expression_and_nested_condition_preserve_captures(num_clbits: int) -> None: + """Keep a root register leaf and pack its nested block-local condition.""" + circuit = QuantumCircuit(1, num_clbits) + condition = expr.logic_and(expr.equal(circuit.cregs[0], 5), circuit.clbits[0]) + with circuit.if_test(condition), circuit.if_test((circuit.cregs[0], 2)): + circuit.x(0) + + restored = QCProgram.from_qiskit(circuit).to_qiskit() + + outer = restored.data[0].operation + assert isinstance(outer.condition, expr.Expr) + outer_variables = {variable.var for variable in expr.iter_vars(outer.condition)} + assert outer_variables == {restored.cregs[0], restored.clbits[0]} + inner = outer.blocks[0].data[0].operation + assert isinstance(inner.condition, expr.Expr) + assert {variable.var for variable in expr.iter_vars(inner.condition)} == set(outer.blocks[0].clbits) + + +def test_repeated_cbit_uint_expression_falls_back_to_expression_tree() -> None: + """Do not misidentify repeated source bits as a packed classical register.""" + program = _single_qubit_program( + [ + '%classical = cbit.alloc(#cbit.init) {mqt.register_name = "c"} : !cbit.reg<1>', + "%zero = arith.constant 0 : index", + "%one = arith.constant 1 : i2", + "%three = arith.constant 3 : i2", + "%bit = cbit.load %classical[%zero] : !cbit.reg<1>", + "%wide = arith.extui %bit : i1 to i2", + "%shifted = arith.shli %wide, %one : i2", + "%repeated = arith.ori %wide, %shifted : i2", + "%condition = arith.cmpi eq, %repeated, %three : i2", + "scf.if %condition {", + " qc.x %q : !qc.qubit", + "}", + ], + returns_classical=True, + ) + + restored = program.to_qiskit() + + condition = restored.data[0].operation.condition + assert isinstance(condition, expr.Expr) + assert {variable.var for variable in expr.iter_vars(condition)} == {restored.clbits[0]} + + +def test_free_parameter_identity_is_shared_with_control_flow_blocks() -> None: + """Canonicalize one scalar Parameter across root and nested writers.""" + theta = Parameter("theta") + circuit = QuantumCircuit(1, 1, global_phase=theta / 2) + circuit.rz(theta, 0) + with circuit.if_test((circuit.clbits[0], True)): + circuit.rx(theta + 1, 0) + + restored = QCProgram.from_qiskit(circuit).to_qiskit() + + restored_theta = next(iter(restored.parameters)) + assert restored.global_phase.parameters == {restored_theta} + assert restored.data[0].operation.params[0] == restored_theta + nested_parameter = restored.data[1].operation.blocks[0].data[0].operation.params[0] + assert nested_parameter.parameters == {restored_theta} + + +def test_nested_if_while_switch_preserve_capture_identity() -> None: + """Map nested control-flow operands through each block-local bit list.""" + circuit = QuantumCircuit(2, 2) + with ( + circuit.if_test(expr.logic_and(circuit.clbits[0], expr.logic_not(circuit.clbits[1]))), + circuit.while_loop(expr.logic_not(circuit.clbits[0]), None, None, None, label=None), + circuit.switch(expr.bit_xor(circuit.cregs[0], 1), None, None, None, label=None) as case, + ): + with case(0): + circuit.x(0) + with case(case.DEFAULT): + circuit.cx(0, 1) + + restored = QCProgram.from_qiskit(circuit).to_qiskit() + + outer = restored.data[0] + assert [restored.find_bit(bit).index for bit in outer.clbits] == [0, 1] + outer_body = outer.operation.blocks[0] + while_instruction = outer_body.data[0] + assert [outer_body.find_bit(bit).index for bit in while_instruction.clbits] == [0, 1] + assert isinstance(while_instruction.operation.condition, expr.Expr) + while_body = while_instruction.operation.blocks[0] + switch_instruction = while_body.data[0] + assert [while_body.find_bit(bit).index for bit in switch_instruction.clbits] == [0, 1] + assert switch_instruction.operation.name == "switch_case" + assert isinstance(switch_instruction.operation.target, expr.Expr) + + +def test_empty_if_else_branches_round_trip() -> None: + """Preserve an explicit else branch when both branches are empty.""" + circuit = QuantumCircuit(1, 1) + with circuit.if_test((circuit.clbits[0], True)) as else_: + pass + with else_: + pass + + restored = QCProgram.from_qiskit(circuit).to_qiskit() + + operation = restored.data[0].operation + assert operation.name == "if_else" + assert len(operation.blocks) == 2 + assert all(not block.data for block in operation.blocks) + + +def test_zero_qubit_cbit_only_control_flow_round_trip() -> None: + """Round-trip CBit-only structured control without allocating qubits.""" + program = QCProgram.from_mlir_str( + """module { + func.func @main() -> !cbit.reg<1> attributes {mqt.entry_point} { + %classical = cbit.alloc(#cbit.init) {mqt.register_name = "c"} : !cbit.reg<1> + %zero = arith.constant 0 : index + %phase = arith.constant 0.0 : f64 + qc.gphase(%phase) + %condition = cbit.load %classical[%zero] : !cbit.reg<1> + scf.if %condition { + } + return %classical : !cbit.reg<1> + } +} +""" + ) + + restored = program.to_qiskit() + + assert restored.num_qubits == 0 + assert restored.num_clbits == 1 + assert len(restored.data) == 1 + instruction = restored.data[0] + assert instruction.operation.name == "if_else" + assert instruction.qubits == () + assert instruction.clbits == (restored.clbits[0],) + block = instruction.operation.blocks[0] + assert block.num_qubits == 0 + assert block.num_clbits == 1 + QCProgram.from_qiskit(restored) + + +def _single_qubit_program(operations: list[str], *, returns_classical: bool = False) -> QCProgram: + """Wrap operations in a one-qubit QC entry function. + + Returns: + The parsed QC program. + """ + result_type = " -> !cbit.reg<1>" if returns_classical else "" + return_value = " %classical : !cbit.reg<1>" if returns_classical else "" + lines = [ + "module {", + f" func.func @main(){result_type} attributes {{mqt.entry_point}} {{", + " %q = qc.alloc : !qc.qubit", + ] + lines.extend(f" {operation}" for operation in operations) + lines.extend([ + " qc.dealloc %q : !qc.qubit", + f" return{return_value}", + " }", + "}", + ]) + return QCProgram.from_mlir_str("\n".join(lines)) + + +@pytest.mark.parametrize( + ("values", "expected"), + [(range(5, -2, -2), [5, 3, 1, -1]), (range(3, 3, -1), [])], + ids=["negative-step", "zero-iterations"], +) +def test_for_loop_range_edges_round_trip(values: range, expected: list[int]) -> None: + """Preserve descending induction values and empty iteration sets.""" + circuit = QuantumCircuit(1) + with circuit.for_loop(values, None, None, None, None, label=None) as iteration: + circuit.rx(iteration, 0) + + restored = QCProgram.from_qiskit(circuit).to_qiskit() + + loop = restored.data[0].operation + assert loop.name == "for_loop" + assert list(loop.params[0]) == expected + assert loop.blocks[0].data[0].operation.params[0].uuid == loop.params[1].uuid + + +def test_for_loop_affine_projection_checks_the_fused_result() -> None: + """Accept a fitting affine value whose intermediate product overflows.""" + program = _single_qubit_program([ + "%lower = arith.constant -2 : index", + "%upper = arith.constant -1 : index", + "%step = arith.constant 1 : index", + "%maximum = arith.constant 9223372036854775807 : i64", + "scf.for %iteration = %lower to %upper step %step {", + " %integer = arith.index_cast %iteration : index to i64", + " %scaled = arith.muli %integer, %maximum : i64", + " %shifted = arith.addi %scaled, %maximum : i64", + " %parameter = arith.sitofp %shifted : i64 to f64", + " qc.rz(%parameter) %q : !qc.qubit", + "}", + ]) + + loop = program.to_qiskit().data[0].operation + + assert list(loop.params[0]) == [-9223372036854775807] + assert loop.blocks[0].data[0].operation.params[0].uuid == loop.params[1].uuid + + +def test_empty_for_loop_ignores_unrepresentable_projection() -> None: + """Keep an empty loop even when its unused projection has a zero step.""" + program = _single_qubit_program([ + "%lower = arith.constant 1 : index", + "%upper = arith.constant 0 : index", + "%step = arith.constant 1 : index", + "%zero = arith.constant 0 : i64", + "scf.for %iteration = %lower to %upper step %step {", + " %integer = arith.index_cast %iteration : index to i64", + " %scaled = arith.muli %integer, %zero : i64", + " %parameter = arith.sitofp %scaled : i64 to f64", + " qc.rz(%parameter) %q : !qc.qubit", + "}", + ]) + + loop = program.to_qiskit().data[0].operation + + assert list(loop.params[0]) == [] + + +def test_nested_for_loop_induction_values_remain_lexically_scoped() -> None: + """Keep nested induction variables distinct while retaining outer captures.""" + circuit = QuantumCircuit(1) + with circuit.for_loop(range(2), None, None, None, None, label=None) as outer: + circuit.rz(outer, 0) + with circuit.for_loop(range(4, 0, -2), None, None, None, None, label=None) as inner: + circuit.rx(inner, 0) + circuit.ry(outer, 0) + + restored = QCProgram.from_qiskit(circuit).to_qiskit() + + outer_loop = restored.data[0].operation + outer_body = outer_loop.blocks[0] + inner_loop = outer_body.data[1].operation + outer_parameter = outer_loop.params[1] + inner_parameter = inner_loop.params[1] + assert outer_parameter.uuid != inner_parameter.uuid + assert outer_body.data[0].operation.params[0].uuid == outer_parameter.uuid + assert outer_body.data[2].operation.params[0].uuid == outer_parameter.uuid + assert inner_loop.blocks[0].data[0].operation.params[0].uuid == inner_parameter.uuid + + +def test_generated_loop_parameter_name_avoids_free_symbol_collision() -> None: + """Choose a loop symbol name distinct from every free program input.""" + free = Parameter("_mqt_loop_0") + circuit = QuantumCircuit(1) + circuit.rz(free, 0) + with circuit.for_loop(range(2), None, None, None, None, label=None) as iteration: + circuit.rx(iteration, 0) + + restored = compile_program(circuit).to_qiskit() + + assert restored.data[0].operation.params[0].name == "_mqt_loop_0" + loop = restored.data[1].operation + assert loop.params[1].name == "_mqt_loop_1" + assert loop.blocks[0].data[0].operation.params[0].uuid == loop.params[1].uuid + + +@pytest.mark.parametrize( + "dead_use", + ["", "%unused = math.sin %parameter : f64"], + ids=["direct", "transitive"], +) +def test_dead_for_loop_parameter_projection_is_ignored(dead_use: str) -> None: + """Omit a loop symbol whose projection has no emitted parameter use.""" + program = _single_qubit_program([ + "%lower = arith.constant 0 : index", + "%upper = arith.constant 2 : index", + "%step = arith.constant 1 : index", + "scf.for %iteration = %lower to %upper step %step {", + " %integer = arith.index_cast %iteration : index to i64", + " %parameter = arith.sitofp %integer : i64 to f64", + f" {dead_use}", + " qc.x %q : !qc.qubit", + "}", + ]) + + restored = program.to_qiskit() + + loop = restored.data[0].operation + assert loop.name == "for_loop" + assert loop.params[1] is None + assert loop.blocks[0].count_ops() == {"x": 1} + + +def test_switch_case_label_width_is_preflighted() -> None: + """Reject a switch label that cannot fit its one-bit target.""" + program = _single_qubit_program( + [ + '%classical = cbit.alloc(#cbit.init) {mqt.register_name = "c"} : !cbit.reg<1>', + "%zero = arith.constant 0 : index", + "%bit = cbit.load %classical[%zero] : !cbit.reg<1>", + "%index = arith.index_castui %bit : i1 to index", + "scf.index_switch %index", + "case 2 {", + " qc.x %q : !qc.qubit", + " scf.yield", + "}", + "default {", + " scf.yield", + "}", + ], + returns_classical=True, + ) + with pytest.raises(RuntimeError, match="case label 2 does not fit the 1-bit target"): + program.to_qiskit() + + +def test_constant_index_switch_exports() -> None: + """Lift a direct constant index selector into a Qiskit Uint expression.""" + program = _single_qubit_program([ + "%selector = arith.constant 0 : index", + "scf.index_switch %selector", + "case 0 {", + " qc.x %q : !qc.qubit", + " scf.yield", + "}", + "default {", + " qc.z %q : !qc.qubit", + " scf.yield", + "}", + ]) + + restored = program.to_qiskit() + switch = restored.data[0].operation + expected = expr.lift(0, types.Uint(64)) + assert isinstance(switch.target, expr.Expr) + assert expr.structurally_equivalent(switch.target, expected) + assert [labels for labels, _ in switch.cases_specifier()] == [(0,), (CASE_DEFAULT,)] + + +def test_shared_expression_dag_expansion_is_bounded() -> None: + """Bound tree expansion when both operands reuse the same SSA value.""" + operations = [ + '%classical = cbit.alloc(#cbit.init) {mqt.register_name = "c"} : !cbit.reg<1>', + "%zero = arith.constant 0 : index", + "%value0 = cbit.load %classical[%zero] : !cbit.reg<1>", + ] + operations.extend(f"%value{index} = arith.andi %value{index - 1}, %value{index - 1} : i1" for index in range(1, 14)) + operations.extend(["scf.if %value13 {", " qc.x %q : !qc.qubit", "}"]) + program = _single_qubit_program(operations, returns_classical=True) + with pytest.raises(RuntimeError, match="size limit of 4096 nodes"): + program.to_qiskit() + + +def test_shared_packed_register_candidate_expansion_is_bounded() -> None: + """Bound speculative packed-register matching on a shared SSA DAG.""" + operations = ["%value0 = arith.constant 0 : i64"] + operations.extend(f"%value{index} = arith.ori %value{index - 1}, %value{index - 1} : i64" for index in range(1, 31)) + operations.extend([ + "%condition = arith.cmpi eq, %value30, %value0 : i64", + "scf.if %condition {", + " qc.x %q : !qc.qubit", + "}", + ]) + program = _single_qubit_program(operations) + with pytest.raises(RuntimeError, match="size limit of 4096 nodes"): + program.to_qiskit() + + +def test_classical_snapshot_walk_is_bounded() -> None: + """Bound snapshot discovery before recursive expression export.""" + operations = [ + '%classical = cbit.alloc(#cbit.init) {mqt.register_name = "c"} : !cbit.reg<1>', + "%zero = arith.constant 0 : index", + "%value0 = cbit.load %classical[%zero] : !cbit.reg<1>", + ] + operations.extend(f"%value{index} = arith.andi %value{index - 1}, %value0 : i1" for index in range(1, 4097)) + operations.extend(["scf.if %value4096 {", " qc.x %q : !qc.qubit", "}"]) + program = _single_qubit_program(operations, returns_classical=True) + with pytest.raises(RuntimeError, match="size limit of 4096 nodes"): + program.to_qiskit() + + +def test_export_expression_depth_is_bounded() -> None: + """Reject a classical expression deeper than 64 levels during export.""" + operations = [ + '%classical = cbit.alloc(#cbit.init) {mqt.register_name = "c"} : !cbit.reg<1>', + "%zero = arith.constant 0 : index", + "%true = arith.constant true", + "%value0 = cbit.load %classical[%zero] : !cbit.reg<1>", + ] + operations.extend(f"%value{index} = arith.andi %value{index - 1}, %true : i1" for index in range(1, 65)) + operations.extend(["scf.if %value64 {", " qc.x %q : !qc.qubit", "}"]) + program = _single_qubit_program(operations, returns_classical=True) + + with pytest.raises(RuntimeError, match="classical expressions exceed the nesting limit of 64"): + program.to_qiskit() + + +def test_general_boolean_select_is_rejected() -> None: + """Reject a result-bearing scf.if that is not short-circuit logic.""" + program = _single_qubit_program( + [ + '%classical = cbit.alloc(#cbit.init) {mqt.register_name = "c"} : !cbit.reg<1>', + "%zero = arith.constant 0 : index", + "%condition = cbit.load %classical[%zero] : !cbit.reg<1>", + "%selected = scf.if %condition -> (i1) {", + " %then = cbit.load %classical[%zero] : !cbit.reg<1>", + " scf.yield %then : i1", + "} else {", + " %else = cbit.load %classical[%zero] : !cbit.reg<1>", + " scf.yield %else : i1", + "}", + "scf.if %selected {", + " qc.x %q : !qc.qubit", + "}", + ], + returns_classical=True, + ) + + with pytest.raises(RuntimeError, match=r"canonical short-circuit Boolean scf\.if"): + program.to_qiskit() + + +def test_export_control_flow_depth_is_bounded() -> None: + """Reject structured control flow deeper than 64 levels during export.""" + operations = ["%condition = arith.constant true"] + operations.extend(f"{' ' * depth}scf.if %condition {{" for depth in range(65)) + operations.append(f"{' ' * 65}qc.x %q : !qc.qubit") + operations.extend(f"{' ' * depth}}}" for depth in reversed(range(65))) + program = _single_qubit_program(operations) + + with pytest.raises(RuntimeError, match="control flow exceeds the nesting limit of 64"): + program.to_qiskit() + + +def test_nonboolean_result_bearing_if_is_rejected() -> None: + """Reject a result-bearing scf.if whose result is not Boolean.""" + program = _single_qubit_program([ + "%condition = arith.constant true", + "%result = scf.if %condition -> (i64) {", + " %one = arith.constant 1 : i64", + " scf.yield %one : i64", + "} else {", + " %zero = arith.constant 0 : i64", + " scf.yield %zero : i64", + "}", + "qc.x %q : !qc.qubit", + ]) + with pytest.raises(RuntimeError, match="canonical short-circuit Boolean SSA result"): + program.to_qiskit() + + +@pytest.mark.parametrize( + "write_operations", + [ + ( + "%measured = qc.measure %q : !qc.qubit -> i1", + "cbit.store %measured, %classical[%zero] : !cbit.reg<1>", + ), + ( + "%always = arith.constant true", + "scf.if %always {", + " %measured = qc.measure %q : !qc.qubit -> i1", + " cbit.store %measured, %classical[%zero] : !cbit.reg<1>", + "}", + ), + ], + ids=["flat-write", "nested-write"], +) +def test_stale_classical_snapshot_is_rejected(write_operations: tuple[str, ...]) -> None: + """Reject a condition whose classical snapshot crosses a later write.""" + program = _single_qubit_program( + [ + '%classical = cbit.alloc(#cbit.init) {mqt.register_name = "c"} : !cbit.reg<1>', + "%zero = arith.constant 0 : index", + "%stale = cbit.load %classical[%zero] : !cbit.reg<1>", + *write_operations, + "scf.if %stale {", + " qc.x %q : !qc.qubit", + "}", + ], + returns_classical=True, + ) + with pytest.raises(RuntimeError, match=r"cannot preserve a (?:stale )?classical snapshot"): + program.to_qiskit() + + +def test_delayed_measurement_store_is_rejected() -> None: + """Reject a delayed write that would change a captured bit snapshot.""" + program = QCProgram.from_mlir_str( + """module { + func.func @main() -> !cbit.reg<1> attributes {mqt.entry_point} { + %measured_qubit = qc.alloc : !qc.qubit + %controlled_qubit = qc.alloc : !qc.qubit + %classical = cbit.alloc(#cbit.init) {mqt.register_name = "c"} : !cbit.reg<1> + %zero = arith.constant 0 : index + %old = cbit.load %classical[%zero] : !cbit.reg<1> + %measured = qc.measure %measured_qubit : !qc.qubit -> i1 + scf.if %old { + qc.x %controlled_qubit : !qc.qubit + } + cbit.store %measured, %classical[%zero] : !cbit.reg<1> + qc.dealloc %measured_qubit : !qc.qubit + qc.dealloc %controlled_qubit : !qc.qubit + return %classical : !cbit.reg<1> + } +} +""" + ) + with pytest.raises(RuntimeError, match="destination must follow the measurement"): + program.to_qiskit() + + +def test_multi_result_boolean_select_is_rejected() -> None: + """Reject multiple results instead of reconstructing Boolean selections.""" + program = _single_qubit_program([ + "%condition = arith.constant true", + "%first, %second = scf.if %condition -> (i1, i1) {", + " %true = arith.constant true", + " %false = arith.constant false", + " scf.yield %true, %false : i1, i1", + "} else {", + " %true = arith.constant true", + " %false = arith.constant false", + " scf.yield %false, %true : i1, i1", + "}", + "scf.if %first {", + " qc.x %q : !qc.qubit", + "}", + ]) + + with pytest.raises(RuntimeError, match="only one canonical short-circuit Boolean SSA result"): program.to_qiskit() +def _undefined_cbit_program(operations: list[str]) -> QCProgram: + """Build a one-qubit program with one undefined public CBit. + + Returns: + The parsed QC program. + """ + return _single_qubit_program( + [ + '%classical = cbit.alloc(#cbit.init) {mqt.register_name = "c"} : !cbit.reg<1>', + "%zero = arith.constant 0 : index", + *operations, + ], + returns_classical=True, + ) + + +def test_undefined_cbits_can_be_read_after_unconditional_measurements() -> None: + """Treat preceding top-level measurement writes as definite initialization.""" + program = _undefined_cbit_program([ + "%measured = qc.measure %q : !qc.qubit -> i1", + "cbit.store %measured, %classical[%zero] : !cbit.reg<1>", + "%condition = cbit.load %classical[%zero] : !cbit.reg<1>", + "scf.if %condition {", + " qc.x %q : !qc.qubit", + "}", + ]) + + restored = program.to_qiskit() + + assert [instruction.operation.name for instruction in restored.data] == ["measure", "if_else"] + + +def test_undefined_cbit_load_before_measurement_is_rejected() -> None: + """Reject a read that precedes definite initialization of an output bit.""" + program = _undefined_cbit_program([ + "%condition = cbit.load %classical[%zero] : !cbit.reg<1>", + "scf.if %condition {", + " qc.x %q : !qc.qubit", + "}", + "%measured = qc.measure %q : !qc.qubit -> i1", + "cbit.store %measured, %classical[%zero] : !cbit.reg<1>", + ]) + + with pytest.raises(RuntimeError, match="loads an undefined classical bit"): + program.to_qiskit() + + +def test_conditional_measurement_does_not_initialize_returned_cbit() -> None: + """Do not count a branch-local measurement as a definite output write.""" + program = _undefined_cbit_program([ + "%condition = arith.constant true", + "scf.if %condition {", + " %measured = qc.measure %q : !qc.qubit -> i1", + " cbit.store %measured, %classical[%zero] : !cbit.reg<1>", + "}", + ]) + + with pytest.raises(RuntimeError, match="cannot return undefined classical bits"): + program.to_qiskit() + + +@pytest.mark.parametrize( + ("expression", "error"), + [ + ( + """%left = arith.constant 5 : i8 + %right = arith.constant 2 : i8 + %remainder = arith.remui %left, %right : i8 + %expected = arith.constant 1 : i8 + %condition = arith.cmpi eq, %remainder, %expected : i8""", + "unsupported QC classical operation in Qiskit export: arith.remui", + ), + ( + """%left = arith.constant 0 : i65 + %right = arith.constant 1 : i65 + %condition = arith.cmpi eq, %left, %right : i65""", + "unsigned classical values must be between 1 and 64 bits", + ), + ( + """%left = arith.constant 0 : i8 + %right = arith.constant 1 : i8 + %condition = arith.cmpi slt, %left, %right : i8""", + "Uint expressions do not support signed comparisons", + ), + ( + """%infinity = arith.constant 0x7FF0000000000000 : f64 + %zero = arith.constant 0.0 : f64 + %condition = arith.cmpf oeq, %infinity, %zero : f64""", + "floating-point literals must be finite", + ), + ], + ids=["unsupported-op", "width", "signed-compare", "nonfinite"], +) +def test_unsupported_export_expressions_fail_closed(expression: str, error: str) -> None: + """Reject unsupported expression forms before modifying the source program.""" + program = _single_qubit_program([ + *expression.splitlines(), + "scf.if %condition {", + " qc.x %q : !qc.qubit", + "}", + ]) + source = program.ir + + with pytest.raises(RuntimeError, match=error): + program.to_qiskit() + + assert program.ir == source + + def test_qiskit_import_zero_initializes_clbits_before_control_flow() -> None: """Initialize Qiskit clbits before a condition reads them.""" circuit = QuantumCircuit(1, 1) @@ -1017,7 +1789,8 @@ def test_qiskit_import_zero_initializes_clbits_before_control_flow() -> None: @pytest.mark.parametrize( ("condition", "operation"), [ - (expr.logic_and(expr.equal(1, 1), expr.equal(0, 1)), "arith.andi"), + (expr.logic_and(expr.equal(1, 1), expr.equal(0, 1)), "scf.if"), + (expr.equal(expr.bit_and(expr.lift(2, types.Uint(8)), 3), 2), "arith.andi"), (expr.equal(expr.bit_xor(expr.lift(2, types.Uint(8)), 3), 5), "arith.xori"), (expr.less(expr.add(expr.lift(2, types.Uint(8)), 1), 8), "arith.addi"), ( @@ -1029,14 +1802,61 @@ def test_qiskit_import_zero_initializes_clbits_before_control_flow() -> None: ], ) def test_bool_uint_and_float_expressions(condition: expr.Expr, operation: str) -> None: - """Lower representative constant classical expressions.""" + """Round-trip representative Bool, Uint, and Float expressions.""" circuit = QuantumCircuit(1) with circuit.if_test(condition): circuit.x(0) program = QCProgram.from_qiskit(circuit) + restored = program.to_qiskit() assert operation in program.ir + assert restored.data[0].operation.name == "if_else" + restored_condition = restored.data[0].operation.condition + assert isinstance(restored_condition, expr.Expr) + if operation == "scf.if": + expected = expr.logic_and( + expr.equal(True, True), # ruff: ignore[boolean-positional-value-in-call] Qiskit expression arguments are positional-only. + expr.equal(False, True), # ruff: ignore[boolean-positional-value-in-call] Qiskit expression arguments are positional-only. + ) + elif operation == "arith.cmpf une": + expected = expr.not_equal(expr.lift(0.5, types.Float()), 0.0) + else: + expected = condition + assert expr.structurally_equivalent(restored_condition, expected) + + +def test_index_expression_export_preserves_low_bit() -> None: + """Export integer truncation as bit indexing instead of a truthiness cast.""" + condition = expr.index(expr.lift(2, types.Uint(3)), expr.lift(0, types.Uint(3))) + circuit = QuantumCircuit(1) + with circuit.if_test(condition): + circuit.x(0) + + program = QCProgram.from_qiskit(circuit) + assert "arith.trunci" in program.ir + + restored = program.to_qiskit() + restored_condition = restored.data[0].operation.condition + assert isinstance(restored_condition, expr.Expr) + assert expr.structurally_equivalent(restored_condition, condition) + + +def test_integer_truncation_exports_as_low_bit_index() -> None: + """Preserve the low-bit semantics of a generic integer truncation.""" + program = _single_qubit_program([ + "%two = arith.constant 2 : i3", + "%condition = arith.trunci %two : i3 to i1", + "scf.if %condition {", + " qc.x %q : !qc.qubit", + "}", + ]) + + restored = program.to_qiskit() + restored_condition = restored.data[0].operation.condition + expected = expr.index(expr.lift(2, types.Uint(3)), expr.lift(0, types.Uint(3))) + assert isinstance(restored_condition, expr.Expr) + assert expr.structurally_equivalent(restored_condition, expected) def _cbit_load_indices(ir: str) -> list[int]: @@ -1066,7 +1886,8 @@ def test_boolean_expression_literals_are_imported() -> None: assert "arith.constant false" in ir assert "arith.constant true" in ir - assert "arith.ori" in ir + assert "scf.if" in ir + assert "arith.ori" not in ir def test_uint_register_cast_to_bool_tests_all_bits() -> None: @@ -1077,11 +1898,17 @@ def test_uint_register_cast_to_bool_tests_all_bits() -> None: with circuit.if_test(expr.cast(circuit.cregs[0], types.Bool())): circuit.z(0) - ir = QCProgram.from_qiskit(circuit).ir + program = QCProgram.from_qiskit(circuit) + ir = program.ir assert "arith.cmpi ne" in ir assert "arith.trunci" not in ir + restored = program.to_qiskit() + round_trip_ir = QCProgram.from_qiskit(restored).ir + assert "arith.cmpi ne" in round_trip_ir + assert "arith.trunci" not in round_trip_ir + def test_public_expression_condition_mutation_is_observed() -> None: """Import the current public expression after condition mutation.""" @@ -1092,10 +1919,13 @@ def test_public_expression_condition_mutation_is_observed() -> None: assert isinstance(operation, IfElseOp) operation.condition = expr.logic_or(circuit.clbits[0], circuit.clbits[1]) - ir = QCProgram.from_qiskit(circuit).ir + program = QCProgram.from_qiskit(circuit) + restored = program.to_qiskit() + condition = restored.data[0].operation.condition - assert "arith.ori" in ir - assert "arith.andi" not in ir + assert "scf.if" in program.ir + assert isinstance(condition, expr.Expr) + assert expr.structurally_equivalent(condition, expr.logic_or(*restored.clbits)) def test_public_tuple_condition_mutation_is_observed() -> None: @@ -1155,7 +1985,7 @@ def test_classical_expression_clbit_captures_import() -> None: assert _cbit_load_indices(ir) == [1, 0] assert "arith.xori" in ir - assert "arith.andi" in ir + assert "scf.if" in ir assert "scf.if" in ir