diff --git a/.agent/plans/uniform-classical-expressions.md b/.agent/plans/uniform-classical-expressions.md new file mode 100644 index 0000000000..75c3912502 --- /dev/null +++ b/.agent/plans/uniform-classical-expressions.md @@ -0,0 +1,173 @@ +# Simplify fixed-width classical expressions + +This ExecPlan follows `.agent/PLANS.md`, the repository development policy, and +`docs/ai_usage.md`. Keep Progress, Surprises & Discoveries, Decision Log, and +Outcomes & Retrospective current. The plan grants no remote GitHub authority. + +## Purpose / Big Picture + +OpenQASM, Qiskit, and jeff exchange classical computations through standard MLIR +integer operations. CBit owns mutable register storage. This cleanup removes +obsolete comparison reconstruction and private callback wrappers while +preserving supported format behavior. Focused regressions protect defects found +during the review, including stale measurement values and narrow integer +signedness. + +## Progress + +- [x] (2026-09-03) Review all PR files with OpenQASM, Qiskit, and adversarial + specialists; distinguish production code from tests and design records. +- [x] (2026-09-03) Challenge the cleanup plan against width, memory ordering, + expression depth, and backend capability contracts. +- [x] (2026-09-03) Remove obsolete Qiskit comparison reconstruction, unused + OpenQASM state, and single-use CBit and QIR wrappers. +- [x] (2026-09-03) Consolidate the superseded CBit comparison design here. +- [x] (2026-09-03) Correct the confirmed frontend, snapshot, and jeff conversion + defects and complete an independent cross-review of the cleanup. +- [x] (2026-09-03) Pass all 3,069 MLIR tests, 394 Qiskit/interchange tests, and + stub regeneration, with no tracked stub changes. +- [x] (2026-09-03) Resolve the two C++ lint diagnostics and pass C++ and general + lint without suppressions. +- [x] (2026-09-03) Inspect the final diff: 182 fewer production lines, with + semantic regression tests and one consolidated design record. + +## Surprises & Discoveries + +Ordinary Qiskit expression emission already builds exact-width register reads, +casts, and comparisons. The former register-comparison recognizers only change +operation shape after removal of `cbit.cmp`; they are unnecessary for meaning. + +Runtime probes found a measurement snapshot changing from 0 to 1 after Qiskit +export and import when its destination was overwritten. The existing snapshot +validator checked register loads and reads but omitted measurement values. + +Sized integer declarations also reach consumers that previously assumed i64: +rotation distances, checked bit indices, integer gate powers, and switch +controls. These consumers need the existing signedness-aware cast before using +machine-width constants. jeff's signed comparison conversion similarly assumed +integer operands and crashed on valid index operands; promoted signed min/max +also selected the wrong result. + +jeff has native integer widths 1, 8, 16, 32, and 64, with no general integer +cast or selection operation. Its array values are immutable, whereas CBit +registers are mutable; updating shared values requires snapshot preservation. +These are real backend constraints, not opportunities to remove validation. + +## Decision Log + +- Decision: Keep `cbit.read` and `cbit.write`, and represent computation with + standard MLIR operations. Rationale: storage owns memory effects; integer + operations already define width and comparison signedness. Date: 2026-09-03. +- Decision: Delete specialized Qiskit comparison recognition. Rationale: generic + emission preserves the same values, including XOR-biased signed comparisons + and unsigned widening. Date: 2026-09-03. +- Decision: Retain balanced bit reconstruction, compact population count, + guarded shifts, and snapshot checks. Rationale: they prevent excessive source + expression depth, oversized expansion, poison, and changed values. The + adversarial review confirmed these constraints. Date: 2026-09-03. +- Decision: Reject unsupported scalar widths and array snapshot forms before + producing a lossy result. Rationale: backend limits must be explicit; this + cleanup does not add multiword arithmetic or a new alias model. Date: + 2026-09-03. + +## Context and Orientation + +`mlir/include/mlir/Dialect/CBit/IR/CBitOps.td` defines registers and their +memory operations. A whole-register read produces an immutable integer value +with bit zero least significant; a write updates the register from an +equal-width value. `mlir/lib/Dialect/CBit/IR/CBitOps.cpp` decomposes these +operations for MemRef and Adaptive QIR consumers. The private decomposition no +longer needs callbacks. + +`mlir/lib/Target/OpenQASM/OpenQASMSemantics.cpp` records scalar signedness and +width. `mlir/lib/Dialect/QC/Translation/OpenQASMToQCEmitter.cpp` emits typed +MLIR; `TranslateQCToOpenQASM3.cpp` in the same directory exports expressions. +The Qiskit adapter, importer, and exporter live in `bindings/mlir/qiskit/`. + +`mlir/lib/Support/IntegerExpressions.cpp` expands integer intrinsics only for +targets that lack them. `mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp` promotes +non-native widths at the jeff boundary and masks results to preserve their +logical width. `mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp` restores mutable +register operations while retaining observable array snapshots. + +## Plan of Work + +The first milestone removes dead and duplicated code without changing the +supported contract. Inline only private single-caller wrappers; keep the shared +CBit decomposition API. Use ordinary Qiskit expression emission for all +comparisons and existing constructors for signed comparison export. + +The second milestone fixes confirmed semantic defects at their shared consumer +boundaries. Extend Qiskit's snapshot dependency walk to measurement destination +stores. Normalize narrow values before machine-width OpenQASM operations. Make +jeff integer conversion preserve index and signed ordering, and prevent switch +shortcuts from dropping effects. Each correction needs a small regression that +fails on the original PR head. + +The final milestone validates the complete change and updates user documentation +for any explicit unsupported form. Keep public text about the resulting design; +do not retain obsolete CBit comparison instructions or duplicate design plans. + +## Concrete Steps and Validation + +Run commands from the repository root. Configure with `cmake --preset release`. +Use the configured LLVM/MLIR package matching the branch, then build affected +targets with `cmake --build --preset release`. + +Run CTest filters for CBit, OpenQASM, jeff round trips, QIR, QC/QCO modifiers, +and DD functionality. These cover memory effects, decomposition, dynamic +indices, signed ordering, snapshot use, and conversion diagnostics. The focused +binaries include: + + build/release/mlir/unittests/Target/OpenQASM/mqt-core-mlir-unittest-openqasm-target + build/release/mlir/unittests/Conversion/JeffRoundTrip/mqt-core-mlir-unittest-jeff-round-trip + build/release/mlir/unittests/Dialect/QC/Translation/mqt-core-mlir-unittest-qc-translation + +Refresh the Python extension with +`uv sync --inexact --no-dev --no-build-isolation-package mqt-core`, then run +`uv run --no-sync pytest -q test/python/test_mlir_integer_interchange.py test/python/test_mlir_qiskit_translation.py`. +The interchange suite compares observable values through serialized jeff, +OpenQASM, and Qiskit, including cleanup, widths 1 through 64, and wide constant +comparisons. Keep those semantic checks; do not require one comparison graph. + +Run `uvx nox -s stubs` after binding changes and `uvx nox -s cpp-lint` for every +changed C++ file. Inspect `git diff --check` and the final diff. Finish with +`uvx nox -s lint`. Report failed and unrun checks separately. + +## Idempotence and Recovery + +All edits and validation are local and repeatable. Preserve unrelated changes; +keep generated files and logs in ignored build directories. Do not edit fetched +dependencies or generated stubs. Any later commit must be signed and verified; +pushing or editing GitHub content requires explicit authorization. + +## Interfaces and Dependencies + +No dependency or public API is added. CBit uses signless exact-width integers. +OpenQASM and Qiskit retain their existing import/export APIs. Qiskit uses Uint +expressions and sign-bit XOR to encode signed order. jeff general integers +remain bounded at 64 bits; arbitrary-width register-versus-constant comparisons +retain their narrow bitwise lowering. QIR Base rejects whole-register +computation and Adaptive QIR lowers supported internal values. CBit register +calls remain unsupported in QIR. + +## Outcomes & Retrospective + +The final MLIR CTest suite passes all 3,069 tests. The Qiskit and integer +interchange suite passes all 394 tests. Stub regeneration succeeds without +tracked changes. Focused baseline probes demonstrated the measurement mismatch, +narrow-integer verifier failures, index assertion, and signed min/max mismatch +before their corrections. + +The cleanup removes 182 net production lines and consolidates two design records +while adding semantic regressions. No dependency or public API was added. jeff +conservatively rejects live old arrays across mutating control flow, even when +the live array and mutated array differ. Preserving that external-jeff case +requires stronger alias reasoning and remains outside this cleanup. + +C++ lint passes with zero findings, and general lint passes. No remote state was +changed. + +Revision note (2026-09-03): Consolidated the superseded comparison plan, +recorded the specialist and adversarial review, and made cleanup validation +explicit. diff --git a/bindings/mlir/qiskit/Qiskit2_5.cpp b/bindings/mlir/qiskit/Qiskit2_5.cpp index ccb66db6b5..551dd4c3d5 100644 --- a/bindings/mlir/qiskit/Qiskit2_5.cpp +++ b/bindings/mlir/qiskit/Qiskit2_5.cpp @@ -11,6 +11,7 @@ #include "QiskitTranslation.h" #include "mlir/Dialect/QC/Translation/StandardGate.h" +#include #include #include @@ -38,7 +39,6 @@ #include #include #include -#include #include #include @@ -74,7 +74,7 @@ namespace mqt::bindings::qiskit { namespace nb = nanobind; constexpr size_t MAX_EXPRESSION_DEPTH = 64U; -constexpr size_t MAX_EXPRESSION_NODES = 4096U; +constexpr size_t MAX_EXPRESSION_NODES = 16384U; constexpr size_t MAX_ANNOTATED_OPERATION_DEPTH = 64U; [[nodiscard]] static nb::object pythonAttribute(const nb::handle object, @@ -547,6 +547,11 @@ static void appendControlModifier(const nb::handle object, return nb::isinstance(terminal, unitaryGate); } +[[nodiscard]] static bool isPythonStore(const nb::handle operation) { + return nb::isinstance(operation, + nb::module_::import_("qiskit.circuit").attr("Store")); +} + static void normalizePythonModifier(const nb::handle modifier, std::vector& modifiers) { const auto type = pythonAttribute(modifier, "__class__", @@ -864,9 +869,15 @@ class NativeCircuitReader final : public CircuitReader { if (kind == OperationKind::ControlFlow) { return {.kind = kind, .name = "control_flow"}; } + const auto operation = pythonOperation(index); + if (pythonStringAttribute(operation, "name", + "Qiskit operation has an invalid name") == + "store" && + isPythonStore(operation)) { + return {.kind = OperationKind::Store, .name = "store"}; + } std::optional normalizedUnknown; if (kind == OperationKind::Unknown) { - const auto operation = pythonOperation(index); if (isPythonUnitaryGate(operation)) { Instruction result{.kind = OperationKind::Unitary, .name = "unitary"}; normalizePythonGate(operation, result); @@ -899,7 +910,7 @@ class NativeCircuitReader final : public CircuitReader { if (result.kind == OperationKind::Gate || result.kind == OperationKind::Unknown) { const auto parameters = - pythonAttribute(pythonOperation(index), "params", + pythonAttribute(operation, "params", "Qiskit operation does not expose its parameters"); try { for (const nb::handle parameter : nb::iter(parameters)) { @@ -1007,6 +1018,8 @@ class NativeCircuitReader final : public CircuitReader { [[nodiscard]] std::unique_ptr controlFlow(size_t index) const override; + [[nodiscard]] ClassicalAssignment store(size_t index) const override; + [[nodiscard]] std::unique_ptr definition(const size_t index) const override { const auto operation = pythonOperation(index); @@ -1073,7 +1086,272 @@ class NativeCircuitReader final : public CircuitReader { const QkCircuit* rootCircuit_ = circuit_; const QkControlFlowInstruction* parent_ = nullptr; }; +} // namespace + +using ClassicalBitResolver = llvm::function_ref; + +static void setPythonExpressionType(Expression& result, + const nb::handle pythonExpression) { + const auto type = pythonAttribute(pythonExpression, "type", + "Qiskit expression has no type"); + const auto typeName = pythonStringAttribute( + pythonAttribute(type, "__class__", + "Qiskit expression type has no Python class"), + "__name__", "Qiskit expression type has no class name"); + if (typeName == "Bool") { + result.type = ClassicalType::Bool; + result.width = 1U; + return; + } + if (typeName == "Uint") { + const auto width = pythonUnsignedAttribute( + type, "width", "Qiskit Uint expression has no width"); + if (width == 0U || width > 64U) { + throw std::runtime_error( + "Qiskit unsigned classical values must be between 1 and 64 bits"); + } + result.type = ClassicalType::Uint; + result.width = static_cast(width); + return; + } + if (typeName == "Float") { + result.type = ClassicalType::Float; + result.width = 64U; + return; + } + if (typeName == "Duration") { + throw std::runtime_error( + "Qiskit circuit import does not support duration expressions"); + } + throw std::runtime_error("Qiskit expression has an unknown Python type"); +} + +[[nodiscard]] static BinaryOperation +pythonBinaryOperation(const std::string_view name) { + const auto operation = + llvm::StringSwitch>(name) + .Case("BIT_AND", BinaryOperation::BitAnd) + .Case("BIT_OR", BinaryOperation::BitOr) + .Case("BIT_XOR", BinaryOperation::BitXor) + .Case("LOGIC_AND", BinaryOperation::LogicAnd) + .Case("LOGIC_OR", BinaryOperation::LogicOr) + .Case("EQUAL", BinaryOperation::Equal) + .Case("NOT_EQUAL", BinaryOperation::NotEqual) + .Case("LESS", BinaryOperation::Less) + .Case("LESS_EQUAL", BinaryOperation::LessEqual) + .Case("GREATER", BinaryOperation::Greater) + .Case("GREATER_EQUAL", BinaryOperation::GreaterEqual) + .Case("SHIFT_LEFT", BinaryOperation::ShiftLeft) + .Case("SHIFT_RIGHT", BinaryOperation::ShiftRight) + .Case("ADD", BinaryOperation::Add) + .Case("SUB", BinaryOperation::Subtract) + .Case("MUL", BinaryOperation::Multiply) + .Case("DIV", BinaryOperation::Divide) + .Default(std::nullopt); + if (!operation) { + throw std::runtime_error( + "Qiskit expression has an unknown Python binary operation"); + } + return *operation; +} + +[[nodiscard]] static UnaryOperation +pythonUnaryOperation(const std::string_view name) { + const auto operation = llvm::StringSwitch>(name) + .Case("BIT_NOT", UnaryOperation::BitNot) + .Case("LOGIC_NOT", UnaryOperation::LogicNot) + .Case("NEGATE", UnaryOperation::Negate) + .Default(std::nullopt); + if (!operation) { + throw std::runtime_error( + "Qiskit expression has an unknown Python unary operation"); + } + return *operation; +} + +static void normalizePythonVariable(Expression& result, + const nb::handle pythonExpression, + const ClassicalBitResolver& resolveBit) { + const auto variable = pythonAttribute( + pythonExpression, "var", "Qiskit variable expression has no value"); + const auto circuitModule = nb::module_::import_("qiskit.circuit"); + if (nb::isinstance(variable, circuitModule.attr("Clbit"))) { + if (result.type != ClassicalType::Bool || result.width != 1U) { + throw std::runtime_error( + "Qiskit classical-bit variable must have Boolean type"); + } + result.kind = ExpressionKind::ClassicalBit; + result.bit = resolveBit(variable); + return; + } + if (nb::isinstance(variable, circuitModule.attr("ClassicalRegister"))) { + if (result.type != ClassicalType::Uint || nb::len(variable) == 0U || + nb::len(variable) > 64U || result.width < nb::len(variable)) { + throw std::runtime_error( + "Qiskit classical-register variable has an invalid type"); + } + result.kind = ExpressionKind::ClassicalRegister; + result.reg.name = pythonStringAttribute( + variable, "name", "Qiskit classical register has no name"); + result.reg.bits.reserve(nb::len(variable)); + for (const nb::handle bit : nb::iter(variable)) { + result.reg.bits.push_back(resolveBit(bit)); + } + return; + } + throw std::runtime_error( + "Qiskit circuit import does not support standalone variables in " + "classical expressions"); +} +[[nodiscard]] static std::unique_ptr normalizePythonExpressionOnly( + const nb::handle pythonExpression, size_t& nodeCount, + const ClassicalBitResolver& resolveBit, const size_t depth = 0U) { + if (depth >= MAX_EXPRESSION_DEPTH) { + throw std::runtime_error( + "Qiskit classical expressions exceed the nesting limit of 64"); + } + if (nodeCount >= MAX_EXPRESSION_NODES) { + throw std::runtime_error( + "Qiskit classical expressions exceed the node limit of 16384"); + } + ++nodeCount; + auto result = std::make_unique(); + setPythonExpressionType(*result, pythonExpression); + const auto className = pythonStringAttribute( + pythonAttribute(pythonExpression, "__class__", + "Qiskit expression has no Python class"), + "__name__", "Qiskit expression has no class name"); + if (className == "Var") { + normalizePythonVariable(*result, pythonExpression, resolveBit); + return result; + } + if (className == "Value") { + result->kind = ExpressionKind::Value; + const auto value = pythonAttribute( + pythonExpression, "value", "Qiskit literal expression has no value"); + switch (result->type) { + case ClassicalType::Bool: { + uint64_t boolValue = 0U; + if (!nb::try_cast(value, boolValue) || boolValue > 1U) { + throw std::runtime_error( + "Qiskit Boolean expression has an invalid value"); + } + result->boolValue = boolValue != 0U; + break; + } + case ClassicalType::Uint: + if (!nb::try_cast(value, result->uintValue) || + (result->width < 64U && + result->uintValue >= (uint64_t{1} << result->width))) { + throw std::runtime_error( + "Qiskit Uint literal does not fit its declared width"); + } + break; + case ClassicalType::Float: + if (!nb::try_cast(value, result->floatValue) || + !std::isfinite(result->floatValue)) { + throw std::runtime_error( + "Qiskit Float expression has an invalid value"); + } + break; + } + return result; + } + if (className == "Unary") { + result->kind = ExpressionKind::Unary; + result->unaryOperation = pythonUnaryOperation(pythonStringAttribute( + pythonAttribute(pythonExpression, "op", + "Qiskit unary expression has no operation"), + "name", "Qiskit unary expression operation has no name")); + result->left = normalizePythonExpressionOnly( + pythonAttribute(pythonExpression, "operand", + "Qiskit unary expression has no operand"), + nodeCount, resolveBit, depth + 1U); + return result; + } + if (className == "Binary") { + result->kind = ExpressionKind::Binary; + result->binaryOperation = pythonBinaryOperation(pythonStringAttribute( + pythonAttribute(pythonExpression, "op", + "Qiskit binary expression has no operation"), + "name", "Qiskit binary expression operation has no name")); + result->left = normalizePythonExpressionOnly( + pythonAttribute(pythonExpression, "left", + "Qiskit binary expression has no left operand"), + nodeCount, resolveBit, depth + 1U); + result->right = normalizePythonExpressionOnly( + pythonAttribute(pythonExpression, "right", + "Qiskit binary expression has no right operand"), + nodeCount, resolveBit, depth + 1U); + return result; + } + if (className == "Cast") { + result->kind = ExpressionKind::Cast; + result->left = normalizePythonExpressionOnly( + pythonAttribute(pythonExpression, "operand", + "Qiskit cast expression has no operand"), + nodeCount, resolveBit, depth + 1U); + return result; + } + if (className == "Index") { + result->kind = ExpressionKind::Index; + result->left = normalizePythonExpressionOnly( + pythonAttribute(pythonExpression, "target", + "Qiskit index expression has no target"), + nodeCount, resolveBit, depth + 1U); + result->right = normalizePythonExpressionOnly( + pythonAttribute(pythonExpression, "index", + "Qiskit index expression has no index"), + nodeCount, resolveBit, depth + 1U); + return result; + } + if (className == "Stretch") { + throw std::runtime_error( + "Qiskit circuit import does not support stretch expressions"); + } + throw std::runtime_error("Qiskit expression has an unknown Python node"); +} + +[[nodiscard]] static ClassicalTarget +normalizePythonTarget(const nb::handle target, + const ClassicalBitResolver& resolveBit) { + ClassicalTarget result; + const auto circuitModule = nb::module_::import_("qiskit.circuit"); + if (nb::isinstance(target, circuitModule.attr("Clbit"))) { + result.kind = ClassicalTargetKind::ClassicalBit; + result.bit = resolveBit(target); + return result; + } + if (nb::isinstance(target, circuitModule.attr("ClassicalRegister"))) { + const auto size = nb::len(target); + if (size == 0U || size > 64U) { + throw std::runtime_error( + "Qiskit classical targets require between 1 and 64 bits"); + } + result.kind = ClassicalTargetKind::ClassicalRegister; + result.reg.name = pythonStringAttribute( + target, "name", "Qiskit classical target register has no name"); + result.reg.bits.reserve(size); + for (const nb::handle bit : nb::iter(target)) { + result.reg.bits.push_back(resolveBit(bit)); + } + result.width = static_cast(size); + return result; + } + const auto expressionModule = + nb::module_::import_("qiskit.circuit.classical.expr"); + if (nb::isinstance(target, expressionModule.attr("Expr"))) { + result.kind = ClassicalTargetKind::Expression; + size_t nodeCount = 0U; + result.expression = + normalizePythonExpressionOnly(target, nodeCount, resolveBit); + return result; + } + throw std::runtime_error("Qiskit classical target has an unknown type"); +} + +namespace { class NativeControlFlowReader final : public ControlFlowReader { public: NativeControlFlowReader(const QkCircuit* rootCircuit, @@ -1307,38 +1585,8 @@ class NativeControlFlowReader final : public ControlFlowReader { private: [[nodiscard]] ClassicalTarget normalizePythonTarget(const nb::handle target) const { - ClassicalTarget result; - const auto circuitModule = nb::module_::import_("qiskit.circuit"); - if (nb::isinstance(target, circuitModule.attr("Clbit"))) { - result.kind = ClassicalTargetKind::ClassicalBit; - result.bit = rootClbitIndex(target); - return result; - } - if (nb::isinstance(target, circuitModule.attr("ClassicalRegister"))) { - const auto size = nb::len(target); - if (size == 0U || size > 64U) { - throw std::runtime_error( - "Qiskit classical targets require between 1 and 64 bits"); - } - result.kind = ClassicalTargetKind::ClassicalRegister; - result.reg.name = pythonStringAttribute( - target, "name", "Qiskit classical target register has no name"); - result.reg.bits.reserve(size); - for (const nb::handle bit : nb::iter(target)) { - result.reg.bits.push_back(rootClbitIndex(bit)); - } - result.width = static_cast(size); - return result; - } - const auto expressionModule = - nb::module_::import_("qiskit.circuit.classical.expr"); - if (nb::isinstance(target, expressionModule.attr("Expr"))) { - result.kind = ClassicalTargetKind::Expression; - size_t nodeCount = 0U; - result.expression = normalizePythonExpressionOnly(target, nodeCount); - return result; - } - throw std::runtime_error("Qiskit classical target has an unknown type"); + return mqt::bindings::qiskit::normalizePythonTarget( + target, [&](const nb::handle bit) { return rootClbitIndex(bit); }); } [[nodiscard]] uint32_t rootClbitIndex(const nb::handle bit) const { @@ -1390,230 +1638,6 @@ class NativeControlFlowReader final : public ControlFlowReader { } } - static void setPythonExpressionType(Expression& result, - const nb::handle pythonExpression) { - const auto type = pythonAttribute(pythonExpression, "type", - "Qiskit expression has no type"); - const auto typeName = pythonStringAttribute( - pythonAttribute(type, "__class__", - "Qiskit expression type has no Python class"), - "__name__", "Qiskit expression type has no class name"); - if (typeName == "Bool") { - result.type = ClassicalType::Bool; - result.width = 1U; - return; - } - if (typeName == "Uint") { - const auto width = pythonUnsignedAttribute( - type, "width", "Qiskit Uint expression has no width"); - if (width == 0U || width > 64U) { - throw std::runtime_error( - "Qiskit unsigned classical values must be between 1 and 64 bits"); - } - result.type = ClassicalType::Uint; - result.width = static_cast(width); - return; - } - if (typeName == "Float") { - result.type = ClassicalType::Float; - result.width = 64U; - return; - } - if (typeName == "Duration") { - throw std::runtime_error( - "Qiskit circuit import does not support duration expressions"); - } - throw std::runtime_error("Qiskit expression has an unknown Python type"); - } - - [[nodiscard]] static BinaryOperation - pythonBinaryOperation(const std::string_view name) { - const auto operation = - llvm::StringSwitch>(name) - .Case("BIT_AND", BinaryOperation::BitAnd) - .Case("BIT_OR", BinaryOperation::BitOr) - .Case("BIT_XOR", BinaryOperation::BitXor) - .Case("LOGIC_AND", BinaryOperation::LogicAnd) - .Case("LOGIC_OR", BinaryOperation::LogicOr) - .Case("EQUAL", BinaryOperation::Equal) - .Case("NOT_EQUAL", BinaryOperation::NotEqual) - .Case("LESS", BinaryOperation::Less) - .Case("LESS_EQUAL", BinaryOperation::LessEqual) - .Case("GREATER", BinaryOperation::Greater) - .Case("GREATER_EQUAL", BinaryOperation::GreaterEqual) - .Case("SHIFT_LEFT", BinaryOperation::ShiftLeft) - .Case("SHIFT_RIGHT", BinaryOperation::ShiftRight) - .Case("ADD", BinaryOperation::Add) - .Case("SUB", BinaryOperation::Subtract) - .Case("MUL", BinaryOperation::Multiply) - .Case("DIV", BinaryOperation::Divide) - .Default(std::nullopt); - if (!operation) { - throw std::runtime_error( - "Qiskit expression has an unknown Python binary operation"); - } - return *operation; - } - - [[nodiscard]] static UnaryOperation - pythonUnaryOperation(const std::string_view name) { - const auto operation = - llvm::StringSwitch>(name) - .Case("BIT_NOT", UnaryOperation::BitNot) - .Case("LOGIC_NOT", UnaryOperation::LogicNot) - .Case("NEGATE", UnaryOperation::Negate) - .Default(std::nullopt); - if (!operation) { - throw std::runtime_error( - "Qiskit expression has an unknown Python unary operation"); - } - return *operation; - } - - [[nodiscard]] std::unique_ptr - normalizePythonExpressionOnly(const nb::handle pythonExpression, - size_t& nodeCount, - const size_t depth = 0U) const { - if (depth >= MAX_EXPRESSION_DEPTH) { - throw std::runtime_error( - "Qiskit classical expressions exceed the nesting limit of 64"); - } - if (nodeCount >= MAX_EXPRESSION_NODES) { - throw std::runtime_error( - "Qiskit classical expressions exceed the node limit of 4096"); - } - ++nodeCount; - auto result = std::make_unique(); - setPythonExpressionType(*result, pythonExpression); - const auto className = pythonStringAttribute( - pythonAttribute(pythonExpression, "__class__", - "Qiskit expression has no Python class"), - "__name__", "Qiskit expression has no class name"); - if (className == "Var") { - normalizePythonVariable(*result, pythonExpression); - return result; - } - if (className == "Value") { - result->kind = ExpressionKind::Value; - const auto value = pythonAttribute( - pythonExpression, "value", "Qiskit literal expression has no value"); - switch (result->type) { - case ClassicalType::Bool: { - uint64_t boolValue = 0U; - if (!nb::try_cast(value, boolValue) || boolValue > 1U) { - throw std::runtime_error( - "Qiskit Boolean expression has an invalid value"); - } - result->boolValue = boolValue != 0U; - break; - } - case ClassicalType::Uint: - if (!nb::try_cast(value, result->uintValue) || - (result->width < 64U && - result->uintValue >= (uint64_t{1} << result->width))) { - throw std::runtime_error( - "Qiskit Uint literal does not fit its declared width"); - } - break; - case ClassicalType::Float: - if (!nb::try_cast(value, result->floatValue) || - !std::isfinite(result->floatValue)) { - throw std::runtime_error( - "Qiskit Float expression has an invalid value"); - } - break; - } - return result; - } - if (className == "Unary") { - result->kind = ExpressionKind::Unary; - result->unaryOperation = pythonUnaryOperation(pythonStringAttribute( - pythonAttribute(pythonExpression, "op", - "Qiskit unary expression has no operation"), - "name", "Qiskit unary expression operation has no name")); - result->left = normalizePythonExpressionOnly( - pythonAttribute(pythonExpression, "operand", - "Qiskit unary expression has no operand"), - nodeCount, depth + 1U); - return result; - } - if (className == "Binary") { - result->kind = ExpressionKind::Binary; - result->binaryOperation = pythonBinaryOperation(pythonStringAttribute( - pythonAttribute(pythonExpression, "op", - "Qiskit binary expression has no operation"), - "name", "Qiskit binary expression operation has no name")); - result->left = normalizePythonExpressionOnly( - pythonAttribute(pythonExpression, "left", - "Qiskit binary expression has no left operand"), - nodeCount, depth + 1U); - result->right = normalizePythonExpressionOnly( - pythonAttribute(pythonExpression, "right", - "Qiskit binary expression has no right operand"), - nodeCount, depth + 1U); - return result; - } - if (className == "Cast") { - result->kind = ExpressionKind::Cast; - result->left = normalizePythonExpressionOnly( - pythonAttribute(pythonExpression, "operand", - "Qiskit cast expression has no operand"), - nodeCount, depth + 1U); - return result; - } - if (className == "Index") { - result->kind = ExpressionKind::Index; - result->left = normalizePythonExpressionOnly( - pythonAttribute(pythonExpression, "target", - "Qiskit index expression has no target"), - nodeCount, depth + 1U); - result->right = normalizePythonExpressionOnly( - pythonAttribute(pythonExpression, "index", - "Qiskit index expression has no index"), - nodeCount, depth + 1U); - return result; - } - if (className == "Stretch") { - throw std::runtime_error( - "Qiskit circuit import does not support stretch expressions"); - } - throw std::runtime_error("Qiskit expression has an unknown Python node"); - } - - void normalizePythonVariable(Expression& result, - const nb::handle pythonExpression) const { - const auto variable = pythonAttribute( - pythonExpression, "var", "Qiskit variable expression has no value"); - const auto circuitModule = nb::module_::import_("qiskit.circuit"); - if (nb::isinstance(variable, circuitModule.attr("Clbit"))) { - if (result.type != ClassicalType::Bool || result.width != 1U) { - throw std::runtime_error( - "Qiskit classical-bit variable must have Boolean type"); - } - result.kind = ExpressionKind::ClassicalBit; - result.bit = rootClbitIndex(variable); - return; - } - if (nb::isinstance(variable, circuitModule.attr("ClassicalRegister"))) { - if (result.type != ClassicalType::Uint || nb::len(variable) == 0U || - nb::len(variable) > 64U || result.width < nb::len(variable)) { - throw std::runtime_error( - "Qiskit classical-register variable has an invalid type"); - } - result.kind = ExpressionKind::ClassicalRegister; - result.reg.name = pythonStringAttribute( - variable, "name", "Qiskit classical register has no name"); - result.reg.bits.reserve(nb::len(variable)); - for (const nb::handle bit : nb::iter(variable)) { - result.reg.bits.push_back(rootClbitIndex(bit)); - } - return; - } - throw std::runtime_error( - "Qiskit circuit import does not support standalone variables in " - "classical expressions"); - } - const QkCircuit* rootCircuit_ = nullptr; const QkCircuit* circuit_ = nullptr; const QkControlFlowInstruction* parent_ = nullptr; @@ -1624,6 +1648,53 @@ class NativeControlFlowReader final : public ControlFlowReader { }; } // namespace +ClassicalAssignment NativeCircuitReader::store(const size_t index) const { + const auto operation = pythonOperation(index); + if (!isPythonStore(operation)) { + throw std::runtime_error( + "requested classical assignment for a non-Store instruction"); + } + const auto resolveBit = [&](const nb::handle bit) -> uint32_t { + try { + const auto location = + pythonAttribute(pythonCircuit_, "find_bit", + "Qiskit circuit cannot resolve Store variables")(bit); + const auto position = pythonUnsignedAttribute( + location, "index", "Qiskit Store variable has an invalid index"); + if (position >= numClbits()) { + throw std::runtime_error( + "Qiskit Store variable has an invalid classical-bit index"); + } + return static_cast(position); + } catch (const nb::python_error& error) { + throwPythonError("Qiskit Store variable is absent from its circuit", + error); + } + }; + auto target = normalizePythonTarget( + pythonAttribute(operation, "lvalue", "Qiskit Store has no lvalue"), + resolveBit); + if (target.kind == ClassicalTargetKind::Expression && target.expression) { + if (target.expression->kind == ExpressionKind::ClassicalBit) { + target.kind = ClassicalTargetKind::ClassicalBit; + target.bit = target.expression->bit; + target.expression.reset(); + } else if (target.expression->kind == ExpressionKind::ClassicalRegister) { + target.kind = ClassicalTargetKind::ClassicalRegister; + target.width = target.expression->width; + target.reg = std::move(target.expression->reg); + target.expression.reset(); + } + } + size_t nodeCount = 0U; + return { + .target = std::move(target), + .value = normalizePythonExpressionOnly( + pythonAttribute(operation, "rvalue", "Qiskit Store has no rvalue"), + nodeCount, resolveBit), + }; +} + std::unique_ptr NativeCircuitReader::controlFlow(const size_t index) const { return std::make_unique( @@ -1647,6 +1718,24 @@ class PythonClassicalBuilder final { return expression(value, 0U); } + [[nodiscard]] nb::object lvalue(const ClassicalTarget& target) const { + switch (target.kind) { + case ClassicalTargetKind::ClassicalBit: + return expressionModule_.attr("lift")(classicalBit(target.bit)); + case ClassicalTargetKind::ClassicalRegister: + return expressionModule_.attr("lift")( + registeredClassicalRegister(target.reg), + classicalType(ClassicalType::Uint, + static_cast(target.reg.bits.size()))); + case ClassicalTargetKind::Expression: + if (!target.expression) { + throw std::runtime_error("Qiskit Store has no lvalue expression"); + } + return expression(*target.expression); + } + throw std::runtime_error("Qiskit Store has an unknown lvalue"); + } + [[nodiscard]] nb::object condition(const ClassicalTarget& target) const { if (target.kind != ClassicalTargetKind::Expression || !target.expression) { throw std::runtime_error( @@ -1668,10 +1757,7 @@ class PythonClassicalBuilder final { 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); + return registeredClassicalRegister(target.reg); case ClassicalTargetKind::Expression: if (!target.expression) { throw std::runtime_error("Qiskit switch target has no expression"); @@ -1719,11 +1805,8 @@ class PythonClassicalBuilder final { return nb::borrow(clbits_[bit]); } - [[nodiscard]] std::optional + [[nodiscard]] nb::object 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", @@ -1732,50 +1815,8 @@ class PythonClassicalBuilder final { return candidate; } } - return std::nullopt; - } - - [[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()); + throw std::runtime_error( + "Qiskit classical expression references a missing register"); } [[nodiscard]] static const char* binaryFunction(const BinaryOperation op) { @@ -1880,11 +1921,9 @@ class PythonClassicalBuilder final { 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); + return expressionModule_.attr("lift")( + registeredClassicalRegister(value.reg), + classicalType(ClassicalType::Uint, value.width)); case ExpressionKind::Unary: return expressionModule_.attr(unaryFunction(value.unaryOperation))( expression(*requireOperand(value.left), depth + 1U)); @@ -2017,6 +2056,19 @@ class NativeCircuitWriter final : public CircuitWriter { "adding barrier"); } + void addStore(ClassicalTarget target, + std::unique_ptr value) override { + if (!value) { + throw std::runtime_error("Qiskit Store has no rvalue"); + } + const auto instructionIndex = qk_circuit_num_instructions(circuit_); + checkExitCode(qk_circuit_barrier(circuit_, nullptr, 0U), + "adding Store placeholder"); + pendingStores_.push_back({.instructionIndex = instructionIndex, + .target = std::move(target), + .value = std::move(value)}); + } + void addUnitary(const std::vector>& matrix, const std::vector& qubits, const uint32_t numControls) override { @@ -2097,14 +2149,15 @@ class NativeCircuitWriter final : public CircuitWriter { [[nodiscard]] nb::object finish() override { PythonParameterGroups groups; - return finishImpl(false, nb::none(), nb::none(), groups); + return finishImpl(false, nb::none(), nb::none(), nb::none(), nb::none(), + groups); } private: - [[nodiscard]] nb::object finishImpl(const bool rebase, - const nb::handle exactQubits, - const nb::handle exactClbits, - PythonParameterGroups& groups) { + [[nodiscard]] nb::object + finishImpl(const bool rebase, const nb::handle exactQubits, + const nb::handle exactClbits, const nb::handle exactQregs, + const nb::handle exactCregs, PythonParameterGroups& groups) { if (circuit_ == nullptr) { throw std::runtime_error( "Qiskit circuit writer has already been finalized"); @@ -2117,10 +2170,12 @@ class NativeCircuitWriter final : public CircuitWriter { auto pythonCircuit = nb::steal(result); try { if (rebase) { - pythonCircuit = rebaseCircuit(pythonCircuit, exactQubits, exactClbits); + pythonCircuit = rebaseCircuit(pythonCircuit, exactQubits, exactClbits, + exactQregs, exactCregs); } replacePendingControlledUnitaries(pythonCircuit); restoreParameterGroups(pythonCircuit, *symbols_, groups); + replacePendingStores(pythonCircuit); replacePendingControlFlow(pythonCircuit, groups); } catch (const nb::python_error& error) { throwPythonError("Qiskit failed to construct deferred instructions", @@ -2135,6 +2190,12 @@ class NativeCircuitWriter final : public CircuitWriter { std::vector qubits; }; + struct PendingStore { + size_t instructionIndex = 0U; + ClassicalTarget target; + std::unique_ptr value; + }; + struct PendingControlFlow { size_t instructionIndex = 0U; ControlFlowKind kind = ControlFlowKind::IfElse; @@ -2220,15 +2281,47 @@ class NativeCircuitWriter final : public CircuitWriter { [[nodiscard]] static nb::object rebaseCircuit(const nb::handle circuit, const nb::handle exactQubits, - const nb::handle exactClbits) { + const nb::handle exactClbits, + const nb::handle exactQregs, + const nb::handle exactCregs) { auto rebased = nb::module_::import_("qiskit.circuit") .attr("QuantumCircuit")(exactQubits, exactClbits); + for (const nb::handle reg : nb::iter(exactQregs)) { + pythonAttribute(rebased, "add_register", + "Qiskit circuit cannot restore a quantum register")(reg); + } + for (const nb::handle reg : nb::iter(exactCregs)) { + pythonAttribute(rebased, "add_register", + "Qiskit circuit cannot restore a classical register")( + reg); + } pythonAttribute(rebased, "compose", "Qiskit circuit cannot compose a control-flow block")( circuit, nb::arg("inplace") = true, nb::arg("copy") = false); return rebased; } + void replacePendingStores(const nb::handle pythonCircuit) const { + auto data = pythonAttribute(pythonCircuit, "data", + "Qiskit circuit has no instruction data"); + const auto circuitModule = nb::module_::import_("qiskit.circuit"); + const PythonClassicalBuilder classical(pythonCircuit); + for (const auto& pending : pendingStores_) { + if (pending.instructionIndex >= nb::len(data)) { + throw std::runtime_error("Qiskit Store placeholder is missing"); + } + const auto placeholder = + nb::borrow(data[pending.instructionIndex]); + const auto operation = + circuitModule.attr("Store")(classical.lvalue(pending.target), + classical.expression(*pending.value)); + data[pending.instructionIndex] = + pythonAttribute(placeholder, "replace", + "Qiskit Store placeholder cannot be replaced")( + nb::arg("operation") = operation); + } + } + [[nodiscard]] static nb::object loopIndexSet(const Loop& loop) { if (!loop.isRange) { throw std::runtime_error( @@ -2308,6 +2401,10 @@ class NativeCircuitWriter final : public CircuitWriter { "Qiskit circuit has no qubits"); const auto circuitClbits = pythonAttribute( pythonCircuit, "clbits", "Qiskit circuit has no classical bits"); + const auto circuitQregs = pythonAttribute( + pythonCircuit, "qregs", "Qiskit circuit has no quantum registers"); + const auto circuitCregs = pythonAttribute( + pythonCircuit, "cregs", "Qiskit circuit has no classical registers"); const auto circuitModule = nb::module_::import_("qiskit.circuit"); const auto circuitInstruction = circuitModule.attr("CircuitInstruction"); const PythonClassicalBuilder classical(pythonCircuit); @@ -2324,8 +2421,9 @@ class NativeCircuitWriter final : public CircuitWriter { throw std::runtime_error( "Qiskit control-flow blocks use an incompatible writer"); } - blocks.emplace_back( - writer->finishImpl(true, circuitQubits, circuitClbits, groups)); + blocks.emplace_back(writer->finishImpl(true, circuitQubits, + circuitClbits, circuitQregs, + circuitCregs, groups)); } pending.blockWriters.clear(); auto operation = constructControlFlowOperation(pending, blocks, classical, @@ -2447,6 +2545,7 @@ class NativeCircuitWriter final : public CircuitWriter { QkCircuit* circuit_ = nullptr; std::vector pendingControlledUnitaries_; + std::vector pendingStores_; std::vector pendingControlFlow_; std::shared_ptr symbols_; }; diff --git a/bindings/mlir/qiskit/QiskitExport.cpp b/bindings/mlir/qiskit/QiskitExport.cpp index da5280c355..59176f677b 100644 --- a/bindings/mlir/qiskit/QiskitExport.cpp +++ b/bindings/mlir/qiskit/QiskitExport.cpp @@ -24,6 +24,7 @@ #include "mlir/Dialect/QC/IR/QCInterfaces.h" #include "mlir/Dialect/QC/IR/QCOps.h" #include "mlir/Dialect/QC/Translation/StandardGate.h" +#include "mlir/Support/IntegerExpressions.h" #include #include @@ -46,6 +47,7 @@ #include #include #include +#include #include #include @@ -69,7 +71,8 @@ namespace mqt::bindings::qiskit { 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; +/// Bounded bit-based casts between jeff native widths expand expression trees. +constexpr size_t MAX_EXPORT_EXPRESSION_NODES = 16384U; namespace { struct ExportedControlFlow; @@ -81,6 +84,7 @@ struct ExportedInstruction { Reset, Barrier, Unitary, + Store, ControlFlow, }; Kind kind = Kind::Gate; @@ -90,6 +94,8 @@ struct ExportedInstruction { std::vector parameters; std::vector> matrix; uint32_t unitaryControls = 0; + ClassicalTarget target; + std::unique_ptr value; std::unique_ptr controlFlow; }; @@ -336,7 +342,6 @@ struct ExportState { llvm::DenseMap quantumSizes; llvm::DenseMap classicalRegisterInfo; llvm::DenseMap> unconditionalWrites; - llvm::DenseMap> measurementDestinations; llvm::DenseMap measurementResultBits; llvm::DenseSet expressionOperations; std::vector quantumRegisters; @@ -872,15 +877,42 @@ static void collectResources(mlir::func::FuncOp function, ExportState& state, throw std::runtime_error( "QC to Qiskit export requires an entry-function return"); } + mlir::ValueRange returnedValues = returnOp.getOperands(); if (returnOp.getNumOperands() == 1U) { auto result = returnOp.getOperand(0); const auto sentinel = mlir::getConstantIntValue(result); if (result.getType().isInteger(64) && sentinel && *sentinel == 0) { - return; + returnedValues = {}; } } llvm::DenseSet returnedRegisters; - for (auto result : returnOp.getOperands()) { + llvm::StringSet<> usedNames; + for (const auto& reg : state.quantumRegisters) { + usedNames.insert(reg.name); + } + for (const auto& parameter : state.parameterNames) { + usedNames.insert(parameter.getKey()); + } + llvm::SmallVector registers; + for (auto alloc : function.getBody().front().getOps()) { + if (!alloc.getResult().use_empty() && + !llvm::is_contained(returnedValues, alloc.getResult())) { + registers.push_back(alloc.getResult()); + } + } + llvm::append_range(registers, returnedValues); + /// Qiskit exposes all register storage; put observable outputs last in count + /// order. + for (auto result : registers) { + if (auto alloc = result.getDefiningOp()) { + if (const auto name = alloc->getAttrOfType( + mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr())) { + usedNames.insert(name.getValue()); + } + } + } + size_t generatedRegister = 0U; + for (auto result : registers) { const auto type = llvm::dyn_cast(result.getType()); if (!type) { @@ -901,13 +933,20 @@ static void collectResources(mlir::func::FuncOp function, ExportState& state, .size = size, .initialization = alloc.getInitialization()}; - if (const auto name = alloc->getAttrOfType( - mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr())) { - Register reg{.name = name.str()}; - reg.bits.resize(size); - std::iota(reg.bits.begin(), reg.bits.end(), state.numClbits); - state.classicalRegisters.push_back(std::move(reg)); + const auto sourceName = alloc->getAttrOfType( + mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()); + std::string name; + if (sourceName) { + name = sourceName.str(); + } else { + do { + name = "_mqt_c" + std::to_string(generatedRegister++); + } while (!usedNames.insert(name).second); } + Register reg{.name = std::move(name)}; + reg.bits.resize(size); + std::iota(reg.bits.begin(), reg.bits.end(), state.numClbits); + state.classicalRegisters.push_back(std::move(reg)); state.numClbits = checkedAdd(state.numClbits, size, "classical-bit"); } } @@ -971,27 +1010,19 @@ static void setExpressionType(Expression& expression, const mlir::Type type) { !written->second.contains(checked)) { throw std::runtime_error( "Qiskit classical expression loads an undefined classical bit " - "before an unconditional measurement write"); + "before an unconditional write"); } } return checkedAdd(info->second.base, checked, "classical-bit"); } -[[nodiscard]] static Register classicalRegister(mlir::Value value, - const ExportState& state) { +[[nodiscard]] static Register +classicalRegisterLayout(mlir::Value value, const ExportState& state) { const auto info = state.classicalRegisterInfo.find(value); if (info == state.classicalRegisterInfo.end() || info->second.size == 0U || info->second.size > 64U) { throw std::runtime_error( - "Qiskit register comparisons require between 1 and 64 bits"); - } - if (info->second.initialization != mlir::cbit::Initialization::Zero) { - const auto written = state.unconditionalWrites.find(value); - if (written == state.unconditionalWrites.end() || - written->second.size() != info->second.size) { - throw std::runtime_error( - "Qiskit register comparison reads undefined classical bits"); - } + "Qiskit classical registers require between 1 and 64 bits"); } Register result; result.bits.resize(info->second.size); @@ -1005,28 +1036,45 @@ static void setExpressionType(Expression& expression, const mlir::Type type) { return result; } +[[nodiscard]] static Register classicalRegister(mlir::Value value, + const ExportState& state) { + const auto info = state.classicalRegisterInfo.find(value); + if (info != state.classicalRegisterInfo.end() && + info->second.initialization != mlir::cbit::Initialization::Zero) { + const auto written = state.unconditionalWrites.find(value); + if (written == state.unconditionalWrites.end() || + written->second.size() != info->second.size) { + throw std::runtime_error( + "Qiskit classical expression reads undefined classical bits"); + } + } + return classicalRegisterLayout(value, state); +} + [[nodiscard]] static BinaryOperation -comparisonOperation(const mlir::cbit::ComparisonPredicate predicate) { +comparisonOperation(const mlir::arith::CmpIPredicate predicate) { switch (predicate) { - case mlir::cbit::ComparisonPredicate::Equal: + case mlir::arith::CmpIPredicate::eq: return BinaryOperation::Equal; - case mlir::cbit::ComparisonPredicate::NotEqual: + case mlir::arith::CmpIPredicate::ne: return BinaryOperation::NotEqual; - case mlir::cbit::ComparisonPredicate::Less: + case mlir::arith::CmpIPredicate::ult: return BinaryOperation::Less; - case mlir::cbit::ComparisonPredicate::LessEqual: + case mlir::arith::CmpIPredicate::ule: return BinaryOperation::LessEqual; - case mlir::cbit::ComparisonPredicate::Greater: + case mlir::arith::CmpIPredicate::ugt: return BinaryOperation::Greater; - case mlir::cbit::ComparisonPredicate::GreaterEqual: + case mlir::arith::CmpIPredicate::uge: return BinaryOperation::GreaterEqual; + default: + throw std::runtime_error( + "Qiskit Uint expressions do not support signed comparisons"); } - llvm_unreachable("unknown CBit comparison predicate"); } [[noreturn]] static void throwClassicalExpressionSizeError() { throw std::runtime_error( - "QC classical expression exceeds the size limit of 4096 nodes"); + "QC classical expression exceeds the size limit of 16384 nodes"); } [[noreturn]] static void throwClassicalExpressionDepthError() { @@ -1040,17 +1088,6 @@ static void countExpressionNode(size_t& nodeCount) { } } -namespace { -struct PackedRegister { - Register reg; - llvm::SmallPtrSet operations; -}; -} // namespace - -[[nodiscard]] static std::optional -matchPackedRegister(mlir::Value value, ExportState& state, - mlir::Block& evaluationBlock); - [[nodiscard]] static std::unique_ptr exportExpressionImpl(mlir::Value value, ExportState& state, mlir::Block& evaluationBlock, const size_t depth, @@ -1079,15 +1116,6 @@ exportExpressionImpl(mlir::Value value, ExportState& state, 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 = @@ -1120,28 +1148,13 @@ exportExpressionImpl(mlir::Value value, ExportState& state, state.expressionOperations.insert(operation); return result; } - if (auto comparison = llvm::dyn_cast(operation)) { - const auto width = comparison.getRhs().getBitWidth(); - if (width > 64U) { - throw std::runtime_error( - "Qiskit register comparisons support at most 64 bits"); + if (auto read = llvm::dyn_cast(operation)) { + result->kind = ExpressionKind::ClassicalRegister; + result->reg = classicalRegister(read.getReg(), state); + if (read.getType().isInteger(1)) { + result->kind = ExpressionKind::ClassicalBit; + result->bit = result->reg.bits.front(); } - countExpressionNode(nodeCount); - auto left = std::make_unique(); - left->kind = ExpressionKind::ClassicalRegister; - left->type = ClassicalType::Uint; - left->width = width; - left->reg = classicalRegister(comparison.getReg(), state); - countExpressionNode(nodeCount); - auto right = std::make_unique(); - right->kind = ExpressionKind::Value; - right->type = ClassicalType::Uint; - right->width = width; - right->uintValue = comparison.getRhs().getZExtValue(); - result->kind = ExpressionKind::Binary; - result->binaryOperation = comparisonOperation(comparison.getPredicate()); - result->left = std::move(left); - result->right = std::move(right); state.expressionOperations.insert(operation); return result; } @@ -1203,24 +1216,154 @@ exportExpressionImpl(mlir::Value value, ExportState& state, return std::move(result); }; const auto binary = [&](const BinaryOperation kind, mlir::Value left, - mlir::Value right) { + mlir::Value right, + const std::optional bitVectorWidth = + std::nullopt) { 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); + const auto requireUint = [&](std::unique_ptr& operand) { + if (!bitVectorWidth) { + return; + } + if (operand->type == ClassicalType::Uint && + operand->width == *bitVectorWidth) { + return; + } + if (operand->kind == ExpressionKind::Value && + operand->type == ClassicalType::Bool && *bitVectorWidth == 1U) { + operand->type = ClassicalType::Uint; + operand->uintValue = operand->boolValue; + return; + } + countExpressionNode(nodeCount); + auto cast = std::make_unique(); + cast->kind = ExpressionKind::Cast; + cast->type = ClassicalType::Uint; + cast->width = *bitVectorWidth; + cast->left = std::move(operand); + operand = std::move(cast); + }; + requireUint(result->left); + requireUint(result->right); state.expressionOperations.insert(operation); + if (bitVectorWidth == 1U && !llvm::isa(operation)) { + result->type = ClassicalType::Uint; + countExpressionNode(nodeCount); + auto boolean = std::make_unique(); + boolean->kind = ExpressionKind::Cast; + boolean->left = std::move(result); + return boolean; + } return std::move(result); }; - if (llvm::isa(operation)) { + const auto uintLiteral = [&](unsigned width, uint64_t bits) { + countExpressionNode(nodeCount); + auto literal = std::make_unique(); + literal->type = ClassicalType::Uint; + literal->width = width; + literal->uintValue = bits; + return literal; + }; + const auto uintCast = [&](std::unique_ptr operand, + unsigned width) { + if (operand->type == ClassicalType::Uint && operand->width == width) { + return operand; + } + if (operand->kind == ExpressionKind::ClassicalRegister) { + /// Qiskit's OpenQASM exporter needs a matching-width register cast first. + countExpressionNode(nodeCount); + auto exact = std::make_unique(); + exact->kind = ExpressionKind::Cast; + exact->type = ClassicalType::Uint; + exact->width = operand->width; + exact->left = std::move(operand); + operand = std::move(exact); + } + countExpressionNode(nodeCount); + auto cast = std::make_unique(); + cast->kind = ExpressionKind::Cast; + cast->type = ClassicalType::Uint; + cast->width = width; + cast->left = std::move(operand); + return cast; + }; + const auto uintBinary = [&](BinaryOperation kind, unsigned width, + std::unique_ptr lhs, + std::unique_ptr rhs) { + countExpressionNode(nodeCount); + auto expression = std::make_unique(); + expression->kind = ExpressionKind::Binary; + expression->type = ClassicalType::Uint; + expression->width = width; + expression->binaryOperation = kind; + expression->left = std::move(lhs); + expression->right = std::move(rhs); + return expression; + }; + if (auto cast = llvm::dyn_cast(operation)) { + const auto sourceWidth = + llvm::cast(cast.getIn().getType()).getWidth(); + const auto width = result->width; + auto operand = + uintCast(exportExpressionImpl(cast.getIn(), state, evaluationBlock, + depth + 1U, nodeCount), + width); + const auto sign = uint64_t{1} << (sourceWidth - 1U); + state.expressionOperations.insert(operation); + return uintBinary(BinaryOperation::Subtract, width, + uintBinary(BinaryOperation::BitXor, width, + std::move(operand), uintLiteral(width, sign)), + uintLiteral(width, sign)); + } + if (auto select = llvm::dyn_cast(operation)) { + if (!llvm::isa(value.getType())) { + throw std::runtime_error("Qiskit selection requires integer values"); + } + const auto width = result->width; + const auto operand = [&](mlir::Value input) { + return uintCast(exportExpressionImpl(input, state, evaluationBlock, + depth + 1U, nodeCount), + width); + }; + auto mask = + uintBinary(BinaryOperation::Subtract, width, uintLiteral(width, 0), + operand(select.getCondition())); + auto difference = uintBinary(BinaryOperation::BitXor, width, + operand(select.getTrueValue()), + operand(select.getFalseValue())); + auto selected = uintBinary( + BinaryOperation::BitXor, width, operand(select.getFalseValue()), + uintBinary(BinaryOperation::BitAnd, width, std::move(difference), + std::move(mask))); + state.expressionOperations.insert(operation); + if (width != 1U) { + return selected; + } + result->kind = ExpressionKind::Cast; + result->left = std::move(selected); + return result; + } + + if (auto cast = llvm::dyn_cast(operation)) { + state.expressionOperations.insert(operation); + return uintCast(exportExpressionImpl(cast.getIn(), state, evaluationBlock, + depth + 1U, nodeCount), + result->width); + } + 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()); + state.expressionOperations.insert(operation); + return uintCast(exportExpressionImpl(cast.getIn(), state, evaluationBlock, + depth + 1U, nodeCount), + result->width); } result->kind = ExpressionKind::Index; if (auto shift = cast.getIn().getDefiningOp()) { @@ -1248,31 +1391,23 @@ exportExpressionImpl(mlir::Value value, ExportState& state, 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: + const auto type = llvm::dyn_cast(op.getLhs().getType()); + if (!type) { throw std::runtime_error( - "Qiskit Uint expressions do not support signed comparisons"); + "Qiskit integer comparisons require integer operands"); + } + const auto width = type.getWidth(); + auto comparison = binary( + comparisonOperation(mlir::mqt::unsignedPredicate(op.getPredicate())), + op.getLhs(), op.getRhs(), width); + if (mlir::mqt::unsignedPredicate(op.getPredicate()) != op.getPredicate()) { + for (auto* operand : {&comparison->left, &comparison->right}) { + *operand = + uintBinary(BinaryOperation::BitXor, width, std::move(*operand), + uintLiteral(width, uint64_t{1} << (width - 1U))); + } } - return binary(kind, op.getLhs(), op.getRhs()); + return comparison; } if (auto op = llvm::dyn_cast(operation)) { auto kind = BinaryOperation::Equal; @@ -1302,39 +1437,106 @@ exportExpressionImpl(mlir::Value value, ExportState& state, 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()); + const bool bitVector = !value.getType().isInteger(1); + return binary( + bitVector ? BinaryOperation::BitAnd : BinaryOperation::LogicAnd, + op.getLhs(), op.getRhs(), + bitVector ? std::optional(result->width) : std::nullopt); } if (auto op = llvm::dyn_cast(operation)) { - return binary(value.getType().isInteger(1) ? BinaryOperation::LogicOr - : BinaryOperation::BitOr, - op.getLhs(), op.getRhs()); + const bool bitVector = !value.getType().isInteger(1); + return binary(bitVector ? BinaryOperation::BitOr : BinaryOperation::LogicOr, + op.getLhs(), op.getRhs(), + bitVector ? std::optional(result->width) + : std::nullopt); } if (auto op = llvm::dyn_cast(operation)) { - return binary(BinaryOperation::BitXor, op.getLhs(), op.getRhs()); + const bool bitVector = !value.getType().isInteger(1); + return binary(BinaryOperation::BitXor, op.getLhs(), op.getRhs(), + bitVector ? std::optional(result->width) + : std::nullopt); } if (auto op = llvm::dyn_cast(operation)) { - return binary(BinaryOperation::ShiftLeft, op.getLhs(), op.getRhs()); + return binary(BinaryOperation::ShiftLeft, op.getLhs(), op.getRhs(), + result->width); } if (auto op = llvm::dyn_cast(operation)) { - return binary(BinaryOperation::ShiftRight, op.getLhs(), op.getRhs()); + return binary(BinaryOperation::ShiftRight, op.getLhs(), op.getRhs(), + result->width); + } + if (auto op = llvm::dyn_cast(operation)) { + const auto width = result->width; + const auto operand = [&](mlir::Value input) { + return uintCast(exportExpressionImpl(input, state, evaluationBlock, + depth + 1U, nodeCount), + width); + }; + const auto sign = uint64_t{1} << (width - 1U); + auto shifted = + uintBinary(BinaryOperation::ShiftRight, width, + uintBinary(BinaryOperation::BitXor, width, + operand(op.getLhs()), uintLiteral(width, sign)), + operand(op.getRhs())); + auto bias = uintBinary(BinaryOperation::ShiftRight, width, + uintLiteral(width, sign), operand(op.getRhs())); + auto difference = uintBinary(BinaryOperation::Subtract, width, + std::move(shifted), std::move(bias)); + state.expressionOperations.insert(operation); + if (width != 1U) { + return difference; + } + result->kind = ExpressionKind::Cast; + result->left = std::move(difference); + return result; + } + if (auto op = llvm::dyn_cast(operation)) { + const auto width = result->width; + const auto operand = [&](mlir::Value input) { + return uintCast(exportExpressionImpl(input, state, evaluationBlock, + depth + 1U, nodeCount), + width); + }; + auto quotient = uintBinary(BinaryOperation::Divide, width, + operand(op.getLhs()), operand(op.getRhs())); + auto product = uintBinary(BinaryOperation::Multiply, width, + std::move(quotient), operand(op.getRhs())); + state.expressionOperations.insert(operation); + auto remainder = uintBinary(BinaryOperation::Subtract, width, + operand(op.getLhs()), std::move(product)); + if (width != 1U) { + return remainder; + } + result->kind = ExpressionKind::Cast; + result->left = std::move(remainder); + return result; } if (llvm::isa(operation)) { return binary(BinaryOperation::Add, operation->getOperand(0), - operation->getOperand(1)); + operation->getOperand(1), + llvm::isa(value.getType()) + ? std::optional(result->width) + : std::nullopt); } if (llvm::isa(operation)) { return binary(BinaryOperation::Subtract, operation->getOperand(0), - operation->getOperand(1)); + operation->getOperand(1), + llvm::isa(value.getType()) + ? std::optional(result->width) + : std::nullopt); } if (llvm::isa(operation)) { return binary(BinaryOperation::Multiply, operation->getOperand(0), - operation->getOperand(1)); + operation->getOperand(1), + llvm::isa(value.getType()) + ? std::optional(result->width) + : std::nullopt); } if (llvm::isa(operation)) { return binary(BinaryOperation::Divide, operation->getOperand(0), - operation->getOperand(1)); + operation->getOperand(1), + llvm::isa(value.getType()) + ? std::optional(result->width) + : std::nullopt); } if (auto op = llvm::dyn_cast(operation)) { result->unaryOperation = UnaryOperation::Negate; @@ -1368,96 +1570,19 @@ exportExpression(mlir::Value value, ExportState& state, 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 = - [&](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)}; -} - -static void acceptPackedRegister(PackedRegister& packed, ExportState& state) { - state.expressionOperations.insert(packed.operations.begin(), - packed.operations.end()); -} - [[nodiscard]] static bool storesToValueRecursively(mlir::Operation& operation, mlir::Value value) { return operation - .walk([&](mlir::cbit::StoreOp store) { - return store.getReg() == value ? mlir::WalkResult::interrupt() - : mlir::WalkResult::advance(); + .walk([&](mlir::Operation* candidate) { + if (auto store = llvm::dyn_cast(candidate)) { + return store.getReg() == value ? mlir::WalkResult::interrupt() + : mlir::WalkResult::advance(); + } + if (auto write = llvm::dyn_cast(candidate)) { + return write.getReg() == value ? mlir::WalkResult::interrupt() + : mlir::WalkResult::advance(); + } + return mlir::WalkResult::advance(); }) .wasInterrupted(); } @@ -1483,8 +1608,16 @@ static void validateClassicalSnapshot(mlir::Value expression, reads.emplace_back(load, load.getReg()); continue; } - if (auto comparison = llvm::dyn_cast(operation)) { - reads.emplace_back(comparison, comparison.getReg()); + if (auto read = llvm::dyn_cast(operation)) { + reads.emplace_back(read, read.getReg()); + continue; + } + if (auto measure = llvm::dyn_cast(operation)) { + for (auto* user : measure.getResult().getUsers()) { + if (auto store = llvm::dyn_cast(user)) { + reads.emplace_back(store, store.getReg()); + } + } continue; } if (auto ifOp = llvm::dyn_cast(operation)) { @@ -1529,6 +1662,12 @@ static void validateClassicalSnapshot(mlir::Value expression, "Qiskit control-flow export cannot preserve a stale classical " "snapshot"); } + if (auto write = llvm::dyn_cast(operation); + write && write.getReg() == reg) { + throw std::runtime_error( + "Qiskit control-flow export cannot preserve a stale classical " + "snapshot"); + } if (operation->getNumRegions() != 0U && storesToValueRecursively(*operation, reg)) { throw std::runtime_error( @@ -1539,6 +1678,63 @@ static void validateClassicalSnapshot(mlir::Value expression, } } +[[nodiscard]] static std::unique_ptr +castBoolToUintOne(std::unique_ptr expression) { + if (expression->type != ClassicalType::Bool) { + return expression; + } + auto cast = std::make_unique(); + cast->kind = ExpressionKind::Cast; + cast->type = ClassicalType::Uint; + cast->width = 1U; + cast->left = std::move(expression); + return cast; +} + +[[nodiscard]] static ClassicalTarget +exportStoreTarget(mlir::cbit::StoreOp store, ExportState& state, + mlir::Block& evaluationBlock) { + const auto info = state.classicalRegisterInfo.find(store.getReg()); + if (info == state.classicalRegisterInfo.end()) { + throw std::runtime_error( + "Qiskit Store uses an unsupported classical register"); + } + if (const auto index = mlir::getConstantIntValue(store.getIndex())) { + const auto checked = checkedIndex(*index, "classical-bit"); + if (checked >= info->second.size) { + throw std::runtime_error( + "Qiskit Store uses an out-of-bounds classical destination"); + } + return {.kind = ClassicalTargetKind::ClassicalBit, + .bit = checkedAdd(info->second.base, checked, "classical-bit")}; + } + auto cast = store.getIndex().getDefiningOp(); + if (!cast) { + throw std::runtime_error( + "Qiskit dynamic Store indices require an unsigned integer-to-index " + "cast"); + } + validateClassicalSnapshot(cast.getIn(), *store.getOperation()); + state.expressionOperations.insert(cast); + auto target = std::make_unique(); + target->kind = ExpressionKind::Index; + target->type = ClassicalType::Bool; + target->width = 1U; + target->left = std::make_unique(); + target->left->kind = ExpressionKind::ClassicalRegister; + target->left->type = ClassicalType::Uint; + target->left->width = info->second.size; + target->left->reg = classicalRegisterLayout(store.getReg(), state); + target->right = + castBoolToUintOne(exportExpression(cast.getIn(), state, evaluationBlock)); + if (target->right->type != ClassicalType::Uint) { + throw std::runtime_error("Qiskit dynamic Store index must be Uint"); + } + return {.kind = ClassicalTargetKind::Expression, + .width = 1U, + .expression = std::move(target)}; +} + [[nodiscard]] static ClassicalTarget exportCondition(mlir::Value value, ExportState& state, mlir::Block& evaluationBlock, mlir::Operation& consumer) { @@ -1580,12 +1776,6 @@ exportSwitchTarget(mlir::Value value, ExportState& state, 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) { @@ -1909,7 +2099,7 @@ collectSwitch(mlir::scf::IndexSwitchOp switchOp, ExportState& state, deferredExpressions.push_back(&operation); continue; } - if (llvm::isa(operation)) { + if (llvm::isa(operation)) { deferredExpressions.push_back(&operation); continue; } @@ -1921,11 +2111,45 @@ collectSwitch(mlir::scf::IndexSwitchOp switchOp, ExportState& state, "QC to Qiskit export encountered an unsupported memory deallocation"); } if (auto store = llvm::dyn_cast(operation)) { - if (!store.getValue().getDefiningOp()) { + if (store.getValue().getDefiningOp()) { + continue; + } + validateClassicalSnapshot(store.getValue(), operation); + auto target = exportStoreTarget(store, state, block); + auto value = exportExpression(store.getValue(), state, block); + if (topLevel && target.kind == ClassicalTargetKind::ClassicalBit) { + const auto info = state.classicalRegisterInfo.find(store.getReg()); + state.unconditionalWrites[store.getReg()].insert(target.bit - + info->second.base); + } + circuit.instructions.push_back({.kind = ExportedInstruction::Kind::Store, + .target = std::move(target), + .value = std::move(value)}); + continue; + } + if (auto write = llvm::dyn_cast(operation)) { + validateClassicalSnapshot(write.getValue(), operation); + const auto info = state.classicalRegisterInfo.find(write.getReg()); + if (info == state.classicalRegisterInfo.end()) { throw std::runtime_error( - "QC to Qiskit export does not support non-measurement classical " - "stores"); + "Qiskit Store uses an unsupported classical register"); + } + auto value = exportExpression(write.getValue(), state, block); + if (info->second.size == 1U) { + value = castBoolToUintOne(std::move(value)); + } + if (topLevel) { + auto& written = state.unconditionalWrites[write.getReg()]; + for (uint32_t index = 0U; index < info->second.size; ++index) { + written.insert(index); + } } + circuit.instructions.push_back( + {.kind = ExportedInstruction::Kind::Store, + .target = {.kind = ClassicalTargetKind::ClassicalRegister, + .reg = classicalRegisterLayout(write.getReg(), state), + .width = info->second.size}, + .value = std::move(value)}); continue; } if (auto phase = llvm::dyn_cast(operation)) { @@ -1968,13 +2192,6 @@ collectSwitch(mlir::scf::IndexSwitchOp switchOp, ExportState& state, throw std::runtime_error( "QC measurement uses an out-of-bounds classical destination"); } - 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); } @@ -2121,6 +2338,10 @@ static void emitCircuit(ExportedCircuit& circuit, CircuitWriter& writer, writer.addUnitary(instruction.matrix, instruction.qubits, instruction.unitaryControls); break; + case ExportedInstruction::Kind::Store: + writer.addStore(std::move(instruction.target), + std::move(instruction.value)); + break; case ExportedInstruction::Kind::ControlFlow: { auto& control = *instruction.controlFlow; std::vector> blocks; @@ -2141,7 +2362,17 @@ static void emitCircuit(ExportedCircuit& circuit, CircuitWriter& writer, nb::object exportCircuit(const mlir::QCProgram& program, const mlir::CompilerTarget* const target) { - auto moduleOp = program.module(); + mlir::OwningOpRef expanded = program.module().clone(); + auto moduleOp = *expanded; + mlir::RewritePatternSet patterns(moduleOp.getContext()); + mlir::mqt::populateIntegerExpansionPatterns(patterns); + /// Expand missing operations and eliminate dead expressions, without folding + /// unrelated control flow or changing the source program. + if (mlir::failed(mlir::applyPatternsGreedily( + moduleOp, std::move(patterns), + mlir::GreedyRewriteConfig().enableFolding(false)))) { + throw std::runtime_error("failed to expand integer operations for Qiskit"); + } const auto functions = moduleOp.getOps(); if (functions.empty() || !llvm::hasSingleElement(functions)) { throw std::runtime_error( diff --git a/bindings/mlir/qiskit/QiskitImport.cpp b/bindings/mlir/qiskit/QiskitImport.cpp index 40d0a8d61b..49f9b877d8 100644 --- a/bindings/mlir/qiskit/QiskitImport.cpp +++ b/bindings/mlir/qiskit/QiskitImport.cpp @@ -23,6 +23,7 @@ #include "mlir/Dialect/QC/Translation/StandardGate.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" +#include "mlir/Support/IntegerExpressions.h" #include #include @@ -641,11 +642,10 @@ loadClassicalBit(mlir::qc::QCProgramBuilder& builder, return builder.loadClassicalBit(bit.storage, bit.index); } -[[nodiscard]] static mlir::Value emitRegisterComparison( - mlir::qc::QCProgramBuilder& builder, - const llvm::ArrayRef classicalBits, - const llvm::ArrayRef rootClbitMap, const Register& reg, - mlir::cbit::ComparisonPredicate predicate, uint64_t expected) { +[[nodiscard]] static mlir::Value +registerStorage(const llvm::ArrayRef classicalBits, + const llvm::ArrayRef rootClbitMap, + const Register& reg) { mlir::Value storage; for (size_t index = 0U; index < reg.bits.size(); ++index) { const auto& bit = @@ -656,18 +656,12 @@ loadClassicalBit(mlir::qc::QCProgramBuilder& builder, } storage = bit.storage; } - if (!storage) { - return {}; - } - const auto width = static_cast(reg.bits.size()); - if (llvm::cast(storage.getType()).getWidth() != - width) { + if (!storage || + llvm::cast(storage.getType()).getWidth() != + static_cast(reg.bits.size())) { return {}; } - const auto rhs = builder.getIntegerAttr(builder.getIntegerType(width), - llvm::APInt(width, expected, false)); - return mlir::cbit::CompareOp::create(builder, builder.getI1Type(), predicate, - storage, rhs); + return storage; } [[nodiscard]] static mlir::Value @@ -680,6 +674,9 @@ packRegister(mlir::qc::QCProgramBuilder& builder, } const auto width = static_cast(reg.bits.size()); const auto type = builder.getIntegerType(width); + if (auto storage = registerStorage(classicalBits, rootClbitMap, reg)) { + return mlir::cbit::ReadOp::create(builder, type, storage).getResult(); + } llvm::SmallVector terms; terms.reserve(reg.bits.size()); for (size_t index = 0; index < reg.bits.size(); ++index) { @@ -711,31 +708,6 @@ packRegister(mlir::qc::QCProgramBuilder& builder, return terms.front(); } -[[nodiscard]] static std::optional -registerComparisonPredicate(const BinaryOperation operation, - const bool reverse) { - switch (operation) { - case BinaryOperation::Equal: - return mlir::cbit::ComparisonPredicate::Equal; - case BinaryOperation::NotEqual: - return mlir::cbit::ComparisonPredicate::NotEqual; - case BinaryOperation::Less: - return reverse ? mlir::cbit::ComparisonPredicate::Greater - : mlir::cbit::ComparisonPredicate::Less; - case BinaryOperation::LessEqual: - return reverse ? mlir::cbit::ComparisonPredicate::GreaterEqual - : mlir::cbit::ComparisonPredicate::LessEqual; - case BinaryOperation::Greater: - return reverse ? mlir::cbit::ComparisonPredicate::Less - : mlir::cbit::ComparisonPredicate::Greater; - case BinaryOperation::GreaterEqual: - return reverse ? mlir::cbit::ComparisonPredicate::LessEqual - : mlir::cbit::ComparisonPredicate::GreaterEqual; - default: - return std::nullopt; - } -} - [[nodiscard]] static mlir::Value emitExpression(mlir::qc::QCProgramBuilder& builder, const Expression& expression, @@ -769,15 +741,6 @@ emitExpression(mlir::qc::QCProgramBuilder& builder, target); } case ExpressionKind::Cast: { - if (expression.type == ClassicalType::Bool && - expression.left->kind == ExpressionKind::ClassicalRegister && - expression.left->width == expression.left->reg.bits.size()) { - if (auto comparison = emitRegisterComparison( - builder, classicalBits, rootClbitMap, expression.left->reg, - mlir::cbit::ComparisonPredicate::NotEqual, 0U)) { - return comparison; - } - } auto operand = emitExpression(builder, *expression.left, classicalBits, rootClbitMap); if (operand.getType() == resultType) { @@ -854,27 +817,6 @@ emitExpression(mlir::qc::QCProgramBuilder& builder, break; } case ExpressionKind::Binary: { - const auto reverse = - expression.left->kind == ExpressionKind::Value && - expression.right->kind == ExpressionKind::ClassicalRegister; - const auto& registerExpression = - reverse ? *expression.right : *expression.left; - const auto& expected = reverse ? *expression.left : *expression.right; - if (const auto predicate = - registerComparisonPredicate(expression.binaryOperation, reverse); - predicate && - registerExpression.kind == ExpressionKind::ClassicalRegister && - registerExpression.type == ClassicalType::Uint && - registerExpression.width == registerExpression.reg.bits.size() && - expected.kind == ExpressionKind::Value && - expected.type == ClassicalType::Uint && - expected.width == registerExpression.width) { - if (auto comparison = emitRegisterComparison( - builder, classicalBits, rootClbitMap, registerExpression.reg, - *predicate, expected.uintValue)) { - return comparison; - } - } auto left = emitExpression(builder, *expression.left, classicalBits, rootClbitMap); if (expression.binaryOperation == BinaryOperation::LogicAnd || @@ -911,8 +853,8 @@ emitExpression(mlir::qc::QCProgramBuilder& builder, auto right = emitExpression(builder, *expression.right, classicalBits, rootClbitMap); const auto comparison = [&]() -> std::optional { - std::optional integerPredicate; - std::optional floatPredicate; + mlir::arith::CmpIPredicate integerPredicate; + mlir::arith::CmpFPredicate floatPredicate; switch (expression.binaryOperation) { case BinaryOperation::Equal: integerPredicate = mlir::arith::CmpIPredicate::eq; @@ -942,8 +884,7 @@ emitExpression(mlir::qc::QCProgramBuilder& builder, return std::nullopt; } if (left.getType().isF64() && right.getType().isF64()) { - return mlir::arith::CmpFOp::create(builder, *floatPredicate, left, - right) + return mlir::arith::CmpFOp::create(builder, floatPredicate, left, right) .getResult(); } if (left.getType() != right.getType() || @@ -951,8 +892,7 @@ emitExpression(mlir::qc::QCProgramBuilder& builder, throw std::runtime_error( "Qiskit classical comparison has incompatible operand types"); } - return mlir::arith::CmpIOp::create(builder, *integerPredicate, left, - right) + return mlir::arith::CmpIOp::create(builder, integerPredicate, left, right) .getResult(); }(); if (comparison) { @@ -981,11 +921,13 @@ emitExpression(mlir::qc::QCProgramBuilder& builder, if (expression.binaryOperation == BinaryOperation::ShiftLeft || expression.binaryOperation == BinaryOperation::ShiftRight) { const auto shiftType = llvm::dyn_cast(right.getType()); - if (!shiftType || shiftType.getWidth() > integerType.getWidth()) { + if (!shiftType) { throw std::runtime_error( - "Qiskit circuit import does not support a shift amount wider " - "than its integer operand"); + "Qiskit shift distance must have integer type"); } + return mlir::mqt::buildZeroFillingShift( + builder, builder.getUnknownLoc(), left, right, + expression.binaryOperation == BinaryOperation::ShiftLeft); } right = castInteger(builder, right, integerType); switch (expression.binaryOperation) { @@ -995,10 +937,6 @@ emitExpression(mlir::qc::QCProgramBuilder& builder, return mlir::arith::OrIOp::create(builder, left, right).getResult(); case BinaryOperation::BitXor: return mlir::arith::XOrIOp::create(builder, left, right).getResult(); - case BinaryOperation::ShiftLeft: - return mlir::arith::ShLIOp::create(builder, left, right).getResult(); - case BinaryOperation::ShiftRight: - return mlir::arith::ShRUIOp::create(builder, left, right).getResult(); case BinaryOperation::Add: return mlir::arith::AddIOp::create(builder, left, right).getResult(); case BinaryOperation::Subtract: @@ -1042,6 +980,62 @@ emitExpression(mlir::qc::QCProgramBuilder& builder, throw std::runtime_error("unsupported normalized Qiskit expression"); } +static void emitStore(mlir::qc::QCProgramBuilder& builder, + const ClassicalAssignment& assignment, + const llvm::ArrayRef classicalBits, + const llvm::ArrayRef clbitMap) { + if (!assignment.value) { + throw std::runtime_error("Qiskit Store has no rvalue"); + } + auto value = + emitExpression(builder, *assignment.value, classicalBits, clbitMap); + switch (assignment.target.kind) { + case ClassicalTargetKind::ClassicalBit: { + if (!value.getType().isInteger(1)) { + throw std::runtime_error("Qiskit Clbit Store requires a Boolean rvalue"); + } + const auto& bit = + classicalBitRef(classicalBits, clbitMap, assignment.target.bit); + builder.storeClassicalBit(value, bit.storage, bit.index); + return; + } + case ClassicalTargetKind::ClassicalRegister: { + auto storage = + registerStorage(classicalBits, clbitMap, assignment.target.reg); + if (!storage || value.getType() != builder.getIntegerType( + assignment.target.reg.bits.size())) { + throw std::runtime_error( + "Qiskit register Store requires a matching canonical register"); + } + mlir::cbit::WriteOp::create(builder, value, storage); + return; + } + case ClassicalTargetKind::Expression: + break; + } + const auto* target = assignment.target.expression.get(); + if (target == nullptr || target->kind != ExpressionKind::Index || + target->left == nullptr || target->right == nullptr || + target->left->kind != ExpressionKind::ClassicalRegister || + !value.getType().isInteger(1)) { + throw std::runtime_error( + "Qiskit Store supports only register-index lvalue expressions"); + } + auto storage = registerStorage(classicalBits, clbitMap, target->left->reg); + if (!storage) { + throw std::runtime_error( + "Qiskit indexed Store requires a canonical classical register"); + } + auto index = emitExpression(builder, *target->right, classicalBits, clbitMap); + if (!llvm::isa(index.getType())) { + throw std::runtime_error("Qiskit Store index must have Uint type"); + } + index = + mlir::arith::IndexCastUIOp::create(builder, builder.getIndexType(), index) + .getResult(); + builder.storeClassicalBit(value, storage, index); +} + [[nodiscard]] static mlir::Value emitCondition(mlir::qc::QCProgramBuilder& builder, const ClassicalTarget& target, @@ -1438,6 +1432,9 @@ void translateCircuit(mlir::qc::QCProgramBuilder& builder, builder.unitary(targetArguments, matrix); }); } break; + case OperationKind::Store: + emitStore(builder, circuit.store(index), classicalBits, clbitMap); + break; case OperationKind::ControlFlow: { const auto controlFlow = circuit.controlFlow(index); translateControlFlow(builder, *controlFlow, allQubits, classicalBits, @@ -2008,6 +2005,48 @@ void validateCircuit(const CircuitReader& circuit, case OperationKind::Unitary: static_cast(denseUnitaryArity(instruction)); break; + case OperationKind::Store: { + if (!instruction.qubits.empty() || !instruction.clbits.empty() || + !instruction.parameters.empty() || !instruction.modifiers.empty()) { + throw std::runtime_error("Qiskit Store has an invalid operand arity"); + } + const auto assignment = circuit.store(index); + if (!assignment.value) { + throw std::runtime_error("Qiskit Store has no rvalue"); + } + validateTarget(assignment.target, circuit.numClbits()); + validateExpression(*assignment.value, circuit.numClbits()); + switch (assignment.target.kind) { + case ClassicalTargetKind::ClassicalBit: + if (assignment.value->type != ClassicalType::Bool) { + throw std::runtime_error( + "Qiskit Clbit Store requires a Boolean rvalue"); + } + break; + case ClassicalTargetKind::ClassicalRegister: + if (assignment.value->type != ClassicalType::Uint || + assignment.value->width != assignment.target.reg.bits.size()) { + throw std::runtime_error( + "Qiskit register Store requires a matching Uint rvalue"); + } + break; + case ClassicalTargetKind::Expression: { + const auto* target = assignment.target.expression.get(); + if (target == nullptr || target->kind != ExpressionKind::Index || + target->left == nullptr || target->right == nullptr || + target->left->kind != ExpressionKind::ClassicalRegister || + target->left->type != ClassicalType::Uint || + target->left->width != target->left->reg.bits.size() || + target->right->type != ClassicalType::Uint || + assignment.value->type != ClassicalType::Bool) { + throw std::runtime_error( + "Qiskit Store supports only register-index lvalues"); + } + break; + } + } + break; + } case OperationKind::ControlFlow: { const auto controlFlow = circuit.controlFlow(index); validateControlFlow(*controlFlow, localParameters, freeParameters, diff --git a/bindings/mlir/qiskit/QiskitTranslation.h b/bindings/mlir/qiskit/QiskitTranslation.h index fa5e442073..394da28ab3 100644 --- a/bindings/mlir/qiskit/QiskitTranslation.h +++ b/bindings/mlir/qiskit/QiskitTranslation.h @@ -37,6 +37,7 @@ enum class OperationKind : uint8_t { Measure, Reset, Unitary, + Store, ControlFlow, Unknown, }; @@ -282,6 +283,11 @@ struct ClassicalTarget { std::unique_ptr expression; }; +struct ClassicalAssignment { + ClassicalTarget target; + std::unique_ptr value; +}; + struct Loop { bool isRange = true; int64_t start = 0; @@ -319,6 +325,7 @@ class CircuitReader { [[nodiscard]] virtual std::vector parameters() const = 0; [[nodiscard]] virtual Parameter globalPhase() const = 0; [[nodiscard]] virtual Instruction instruction(size_t index) const = 0; + [[nodiscard]] virtual ClassicalAssignment store(size_t index) const = 0; [[nodiscard]] virtual std::vector> unitary(size_t index) const = 0; [[nodiscard]] virtual std::unique_ptr @@ -367,6 +374,8 @@ class CircuitWriter { virtual void addMeasure(uint32_t qubit, uint32_t clbit) = 0; virtual void addReset(uint32_t qubit) = 0; virtual void addBarrier(const std::vector& qubits) = 0; + virtual void addStore(ClassicalTarget target, + std::unique_ptr value) = 0; virtual void addUnitary(const std::vector>& matrix, const std::vector& qubits, uint32_t numControls) = 0; diff --git a/docs/mlir/OpenQASM.md b/docs/mlir/OpenQASM.md index 62bc657e93..028e02f41e 100644 --- a/docs/mlir/OpenQASM.md +++ b/docs/mlir/OpenQASM.md @@ -35,26 +35,51 @@ mqt-cc --input-format=qasm program.txt ### Input support -| OpenQASM concept | Support and restrictions | -| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Versions and includes | Versionless input and versions 3.0 and 3.1 use the maintained OpenQASM profile. `stdgates.inc`, `qelib1.inc`, and nested textual includes are supported. | -| Classical types | Unsized `bit`, `bool`, `int`, `uint`, and `float` declarations are supported. Initialized compile-time `angle[N]` values support widths 1 through 52. Other width-qualified numeric types, arrays, complex values, and aliases are not yet supported. | -| Outputs | Explicit `output` declarations are preserved in source order. Without any explicit output, global classical variables become outputs. | -| Gates | Language gates, the standard libraries, custom gates, broadcasting, and `inv`, `ctrl`, `negctrl`, and `pow` modifiers are supported. Recursive custom gates are rejected. | -| Quantum statements | Measurement, reset, barrier, logical qubits, and physical qubits are supported. The QC target rejects programs that mix logical allocation with physical qubits. | -| Expressions | Scalar arithmetic, comparisons, Boolean expressions, and the supported math functions are type checked before translation. `popcount`, `rotl`, and `rotr` operate on initialized bit registers. | -| Structured control | `if`, inclusive `for`, `while`, and `switch` lower to SCF operations. Switch controls and case labels must be integers; labels must be unique constant expressions. | -| Dynamic indexing | Classical bit indices can be dynamic and receive runtime bounds checks. A nonconstant qubit index must be a proven affine expression as described below. | -| Unsupported language areas | Subroutines, `extern`, calibration and timing constructs, input declarations, arbitrary arrays, `break`, and `continue` are diagnosed. | +| OpenQASM concept | Support and restrictions | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Versions and includes | Versionless input and versions 3.0 and 3.1 use the maintained OpenQASM profile. `stdgates.inc`, `qelib1.inc`, and nested textual includes are supported. | +| Classical types | `bit`, `bool`, `int`, `uint`, and `float` declarations are supported, including integer widths 1–64. Initialized compile-time `angle[N]` values support widths 1–52. Other sized numeric declarations, general arrays, complex values, and aliases are not yet supported. | +| Outputs | Explicit `output` declarations are preserved in source order. Without any explicit output, global classical variables become outputs. | +| Gates | Language gates, the standard libraries, custom gates, broadcasting, and `inv`, `ctrl`, `negctrl`, and `pow` modifiers are supported. Recursive custom gates are rejected. | +| Quantum statements | Measurement, reset, barrier, logical qubits, and physical qubits are supported. The QC target rejects programs that mix logical allocation with physical qubits. | +| Expressions | Scalar arithmetic, comparisons, Boolean expressions, and the supported math functions are type checked before translation. Initialized bit registers support `~`, `&`, `\|`, `^`, `<<`, `>>`, `popcount`, `rotl`, and `rotr`. | +| Structured control | `if`, inclusive `for`, `while`, and `switch` lower to SCF operations. Switch controls and case labels must be integers; labels must be unique constant expressions. | +| Dynamic indexing | Classical bit indices can be dynamic and receive runtime bounds checks. A nonconstant qubit index must be a proven affine expression as described below. | +| Unsupported language areas | Subroutines, `extern`, calibration and timing constructs, input declarations, arbitrary arrays, `break`, and `continue` are diagnosed. | + +Sized `uint[N](bits)` and `int[N](bits)` casts accept an initialized `bit[N]` +register when the constant width is 1 through 64. Bit zero is the least +significant bit. Signed casts use two's-complement representation, with bit +`N - 1` as the sign bit. Syntax and semantic diagnostics retain source locations and include stacks. -Runtime integer preconditions and classical-index bounds are represented -explicitly in QC. This safety machinery is supported by the normal compiler and -QIR paths, but it is intentionally outside the export subset described below. - -OpenQASM 3 supports all six unsigned comparisons with a bit register on the left -and an integer constant on the right. OpenQASM 2 retains its equality-only -register condition. +Classical-index bounds and integer-power preconditions are represented +explicitly in QC. Runtime integer arithmetic uses machine-width promotion and +wraps modulo that width; explicit integer casts truncate or extend to their +declared width. Compile-time invalid arithmetic is diagnosed. Runtime division +by zero remains undefined. Dynamic-index checks are supported by compiler/QIR +paths but remain outside the source-export subset. + +OpenQASM 3 supports all six comparisons between fixed-width bit-register +expressions. Direct register comparisons use unsigned meaning. An exact-width +`int[N]` cast selects signed two's-complement interpretation before the +language's usual integer promotion. The frontend also accepts these conditions +in OpenQASM 2 as a compatibility extension; version-specific initialization and +gates are unchanged. + +Runtime shift distances have unsigned interpretation. Overshifts produce zero. +The frontend checks the original distance before narrowing it and uses a safe +count even in the unselected shift. Constant distances fold without guards. The +same helper is used by Qiskit import. + +For Qiskit-generated source, nonnegative constant operands of typed bitwise +expressions are accepted when they fit the unsigned operand width. Standalone +unsized constant bitwise expressions use the 64-bit machine width. This does not +give runtime signed integers an implicit unsigned interpretation. + +For the same compatibility reason, a whole-register assignment accepts a +nonnegative integer constant that fits the register width. Use an exact-width +bit-string literal for strict OpenQASM source. Fixed-width angles are a compile-time input feature. An omitted angle width resolves to 52 bits. Both `const angle[N]` and initialized `angle[N]` @@ -87,12 +112,19 @@ do not index qubits keep their runtime behavior. Bit registers use `!cbit.reg` in QC. OpenQASM 2 initializes each register to zero. OpenQASM 3 leaves each register undefined until a statement writes it. -Whole-register comparisons lower to `cbit.cmp` and keep their unsigned integer -meaning without expanding into per-bit expression trees. Explicit outputs and -implicit global outputs are returned by the entry function; internal CBit -allocations are not outputs. Other scalar outputs use builtin MLIR scalar types. -A scalar `qubit` lowers to `qc.alloc`, while `qubit[1]` remains a one-element -qubit register. +Whole-register reads and writes lower to `cbit.read` and `cbit.write`. Standard +integer operations represent computation, including all comparisons: `cbit.read` +produces the snapshot, `arith.constant` the comparison constant, and +`arith.cmpi` determines signedness. CBit operations carry storage memory +effects. jeff legalization preserves native widths and promotes other widths up +to 64 to 8, 16, 32, or 64 bits, masking results to retain exact-width semantics. +Wider register-versus-constant comparisons remain supported; wider general +integer expressions are rejected. Integer-to-floating-point casts (for example, +using a runtime population count as a rotation angle) remain outside the jeff +subset. Explicit outputs and implicit global outputs are returned by the entry +function; internal CBit allocations are not outputs. Other scalar outputs use +builtin MLIR scalar types. A scalar `qubit` lowers to `qc.alloc`, while +`qubit[1]` remains a one-element qubit register. ## Export OpenQASM @@ -158,7 +190,7 @@ bypasses that QCO optimization round trip. | Qubits and classical bits | Logical and physical qubits, scalar qubit allocations, static rank-one qubit memrefs, and CBit registers. Qubit memory indices must resolve statically. CBit indices can be dynamic. | | Quantum operations | Measurement, reset, barrier, deallocation, global phase, and QC unitary operations. The exporter uses standard gates where available; for example, `sxdg` becomes `inv @ sx` and `u2` uses the standard compatibility alias. | | Gate modifiers | Nested `ctrl`, `inv`, and `pow`. A multi-operation modifier body with target qubits becomes a private generated gate. | -| Scalar values | `i1`, `i64`, `f64`, and internal `index` values, including arithmetic, comparisons, Boolean operations, value-preserving casts, and supported math functions. | +| Scalar values | Integers of widths 1–64, `f64`, and internal `index` values, including arithmetic, comparisons, Boolean operations, value-preserving casts, and supported math functions. | | Structured control | Result-free `scf.if` and `scf.index_switch`, constant-range `scf.for` without iterated state, and zero-state expression-based `scf.while`. Index switches use native `switch`, `case`, and `default` statements. | | Results | Multiple scalar and bit-register outputs using the canonical type and naming rules below. | @@ -184,31 +216,43 @@ Output types follow a deliberately small canonical mapping: | `i1` produced directly by measure | `bit` | | Other `i1` | `bool` | | `i64` or `index` | `int` | +| Other integers of 2–63 bits | `uint[N]` | | `f64` | `float` | A lone constant-zero `i64` result is treated as the frontend's status return and is not emitted. Import and export do not preserve `uint`, fixed-angle spelling or width, scalar-versus-one-element bit spelling, or scalar output names. -Unsigned constants therefore normalize to `int`. Operations whose signedness -affects their meaning, such as unsigned division, comparison, or conversion, are -rejected instead of being approximated. Integer sign extension and truncation -are also rejected because OpenQASM scalar casts have different value semantics. -Direct `cbit.cmp` operations retain their unsigned register semantics. - -Emitted scalar casts use standard OpenQASM conversion syntax. The MQT Core -frontend does not yet parse that syntax, so cast-containing output is outside -the current MQT strict round-trip subset. +Integer computations use explicit `int[N]`/`uint[N]` casts, so signedness is +chosen by each MLIR operation rather than inferred from its source register. +Truncation, sign/zero extension, arithmetic, bitwise operations, comparisons, +shifts, and integer selection are supported. Selection uses a fixed-width bit +mask and does not allocate a temporary register. The frontend accepts the casts +and expressions emitted by the exporter, including Boolean/integer conversions. ### Export limitations Export accepts exactly one defined, argument-free function. It rejects calls, arbitrary CFGs, multi-block SCF regions, dynamic qubit indices or ranges, -general memrefs, unsupported integer widths, packed bit-vector operations, -unknown operations, and non-unitary content inside modifier regions. CBit loads, -stores, and dynamic indices are supported. SCF results, loop-carried values, -nonempty `scf.yield`, and `arith.select` are outside the export subset. -Multi-operation modifier bodies must have a target qubit and cannot capture -additional qubits from an enclosing scope. +general memrefs, unsupported integer widths, unknown operations, and non-unitary +content inside modifier regions. CBit loads, stores, whole-register reads and +writes, fixed-width bitwise operations, and dynamic indices are supported. SCF +results, loop-carried values, and nonempty `scf.yield` are outside the export +subset. Multi-operation modifier bodies must have a target qubit and cannot +capture additional qubits from an enclosing scope. + +OpenQASM export supports arbitrary bit-register widths for bitwise operations, +unsigned comparisons, `popcount`, `rotl`, and `rotr`. Scalar arithmetic, integer +casts, signed comparisons, and logical shifts require widths of at most 64 bits. +Rotation counts must be constant or represented by at most 64 bits, optionally +zero-extended to the register width. Qiskit interoperability uses the common +subset described in the Python compiler documentation. + +The exporter inlines a whole-register read only in the block that contains the +read and only when no later write to that register precedes the expression use. +It rejects stale and cross-region snapshots instead of reading newer register +state. Shift interpretation is determined by the MLIR operation, not by the +history of its operands. Arithmetic right shifts are encoded with unsigned +bitwise operations and explicit sign-bit biasing. Export accepts an expression nesting depth of at most 256 and an expansion budget of 4,096 values per expression. The total width of classical registers is @@ -217,8 +261,9 @@ limited to 1,048,576 bits. The exporter does not reconstruct the runtime checks created for dynamic indices or checked integer arithmetic. Surviving assertions, checked-index control flow, or live poison values cause an explicit diagnostic. Programs with static qubit -and bit indices and without scalar casts can be exported and parsed again -through the strict frontend. Programs that rely on the input safety machinery +and bit indices and supported integer/Boolean casts can be exported and parsed +again through the strict frontend. Floating-point/integer conversions remain +outside that round-trip subset. Programs that rely on the input safety machinery must continue through another output path such as QIR. :::{important} diff --git a/docs/mlir/python_compiler_collection.md b/docs/mlir/python_compiler_collection.md index a62cebe4dd..e44df6c37b 100644 --- a/docs/mlir/python_compiler_collection.md +++ b/docs/mlir/python_compiler_collection.md @@ -169,23 +169,25 @@ This compiler route is the Qiskit circuit interface in MQT Core v4. 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 | -| ----------------------------------------------------------------- | -------------------- | -------------- | -| Standard gates, constructible numeric modifiers, and global phase | Supported | Supported | -| Other finite numeric modifiers | Supported | Rejected | -| 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 | 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 | Supported | Supported | -| Dense numeric unitaries up to eight qubits | Supported | Supported | -| Register aliases or interleaved membership | Rejected | Rejected | -| Transpiler layout metadata | Accepted and ignored | Not emitted | +| Circuit feature | Import | Export | +| ----------------------------------------------------------------- | -------------------- | ------------------ | +| Standard gates, constructible numeric modifiers, and global phase | Supported | Supported | +| Other finite numeric modifiers | Supported | Rejected | +| Measurement, reset, and barrier | Supported | Supported | +| Canonical named registers and leading loose bits | Supported | Explicit registers | +| Custom instructions with finite, acyclic definitions | Recursively expanded | Not applicable | +| 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 | +| Fixed-width bitwise operations, comparisons, and bounded shifts | Supported | Supported | +| Clbit, indexed-register, and whole-register `Store` assignments | Supported | Supported | +| Standalone classical runtime variables | Rejected | Rejected | +| Free symbols and supported real parameter expressions | Supported | Supported | +| Parameter-vector elements | Supported | Supported | +| Dense numeric unitaries up to eight qubits | Supported | Supported | +| Register aliases or interleaved membership | Rejected | Rejected | +| Transpiler layout metadata | Accepted and ignored | Not emitted | Classical-expression variables may refer to Clbits or ClassicalRegisters in the containing circuit. This includes values used only by the condition or switch @@ -225,27 +227,46 @@ 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. +may nest up to 64 levels, and classical expression trees may contain at most +16,384 nodes (parameter-expression limits are unchanged). Integer values use +exact widths from 1 through 64. Standard `arith.cmpi` handles every comparison: +signed ordering is encoded by XOR-biasing both operands' sign bits, including +computed operands. Casts preserve truncation and sign/zero extension. Bitwise +operations, modular arithmetic, integer selection, and shifts share these typed +rules. Import guards runtime shifts so overshifts produce zero; export preserves +the guards. Rotations and population count are expanded through the same bounded +integer lowering used by jeff. Unsupported operations, invalid widths, +non-finite constants, dynamic bounds, loop-carried values, and other SSA results +fail during validation. Core's constant-zero `i64` status return is not a +classical output. Whole-register reads map to Qiskit `ClassicalRegister` +expressions, and writes map to atomic Qiskit `Store` operations. Indexed stores +assume that their runtime index is in bounds. The Qiskit C API does not expose +`Store`, so the adapter inspects and constructs that instruction through +Qiskit's public Python classes, as it already does for structured control flow. +Internal entry-block CBit storage becomes additional Qiskit registers, ordered +before returned registers; Qiskit exposes all circuit storage. OpenQASM remains +the source interchange path for arbitrary register widths. + +Every public CBit output is exported as a Qiskit `ClassicalRegister`; an unnamed +allocation receives a collision-free `_mqt_cN` name. This preserves the CBit +register boundary and gives whole-register writes a valid Qiskit lvalue. Loose +input Clbits therefore round trip semantically, but not as loose output bits. + +Conditions and switch targets may read a zero-initialized CBit register. An +undefined CBit may be read only after an unconditional top-level 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. +block. Destinations may be reused; later measurements overwrite earlier values +in program order. A measurement's destination store must follow it 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 before any overwrite of the destination register. It 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 @@ -358,6 +379,12 @@ pipeline. It is applied when compilation proceeds beyond the raw {code}`jeff` is a serializable representation that can be stored and compiled again in a later process. +Integer expressions support widths through 64 bits. Integer absolute value and +power require jeff's native widths: 1, 8, 16, 32, or 64. Import preserves +straight-line array snapshots, but rejects live old array values across mutating +control flow and shared array updates inside switch or while regions. Integer +selection branches must contain only constants and their yielded values. + ```{code-cell} ipython3 from pathlib import Path from tempfile import TemporaryDirectory diff --git a/mlir/include/mlir/Conversion/QCOToJeff/QCOToJeff.td b/mlir/include/mlir/Conversion/QCOToJeff/QCOToJeff.td index 58ea959982..979f010250 100644 --- a/mlir/include/mlir/Conversion/QCOToJeff/QCOToJeff.td +++ b/mlir/include/mlir/Conversion/QCOToJeff/QCOToJeff.td @@ -21,5 +21,6 @@ def QCOToJeff : Pass<"qco-to-jeff", "mlir::ModuleOp"> { As the index is not preserved in `jeff`, it is not possible to round-tripping static qubits. }]; - let dependentDialects = ["mlir::jeff::JeffDialect"]; + let dependentDialects = ["mlir::jeff::JeffDialect", + "mlir::arith::ArithDialect"]; } diff --git a/mlir/include/mlir/Dialect/CBit/IR/CBitDialect.td b/mlir/include/mlir/Dialect/CBit/IR/CBitDialect.td index ecbefc6644..60a0aff250 100644 --- a/mlir/include/mlir/Dialect/CBit/IR/CBitDialect.td +++ b/mlir/include/mlir/Dialect/CBit/IR/CBitDialect.td @@ -10,6 +10,15 @@ #define MLIR_DIALECT_CBIT_IR_CBITDIALECT_TD include "mlir/IR/DialectBase.td" +include "mlir/IR/EnumAttr.td" + +def CBit_Initialization + : I32EnumAttr<"Initialization", "Classical-bit register initialization", + [I32EnumAttrCase<"Zero", 0, "zero">, + I32EnumAttrCase<"Undefined", 1, "undefined">]> { + let cppNamespace = "::mlir::cbit"; + let genSpecializedAttr = 0; +} def CBitDialect : Dialect { let name = "cbit"; diff --git a/mlir/include/mlir/Dialect/CBit/IR/CBitOps.h b/mlir/include/mlir/Dialect/CBit/IR/CBitOps.h index c0686ad430..f2ca7b97ab 100644 --- a/mlir/include/mlir/Dialect/CBit/IR/CBitOps.h +++ b/mlir/include/mlir/Dialect/CBit/IR/CBitOps.h @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -30,9 +31,8 @@ namespace mlir::cbit { void validateStaticRegisterIndex(Value reg, const std::variant& index); -/// Builds an equivalent comparison from individual register bits. -Value buildComparison(OpBuilder& builder, Location location, - ComparisonPredicate predicate, const llvm::APInt& rhs, - llvm::function_ref loadBit); +/// Populates patterns that decompose whole-register operations into static +/// bit loads, stores, and ordinary integer arithmetic. +void populateCBitDecompositionPatterns(RewritePatternSet& patterns); } // namespace mlir::cbit diff --git a/mlir/include/mlir/Dialect/CBit/IR/CBitOps.td b/mlir/include/mlir/Dialect/CBit/IR/CBitOps.td index 3b530490a7..3872217c65 100644 --- a/mlir/include/mlir/Dialect/CBit/IR/CBitOps.td +++ b/mlir/include/mlir/Dialect/CBit/IR/CBitOps.td @@ -20,30 +20,11 @@ include "mlir/Interfaces/SideEffectInterfaces.td" // Attributes and types //===----------------------------------------------------------------------===// -def CBit_Initialization - : I32EnumAttr<"Initialization", "Classical-bit register initialization", - [I32EnumAttrCase<"Zero", 0, "zero">, - I32EnumAttrCase<"Undefined", 1, "undefined">]> { - let cppNamespace = "::mlir::cbit"; - let genSpecializedAttr = 0; -} - def CBit_InitializationAttr : EnumAttr { let assemblyFormat = "`<` $value `>`"; } -def CBit_ComparisonPredicate - : I64EnumAttr<"ComparisonPredicate", "unsigned comparison predicate", - [I64EnumAttrCase<"Equal", 0, "eq">, - I64EnumAttrCase<"NotEqual", 1, "ne">, - I64EnumAttrCase<"Less", 2, "ult">, - I64EnumAttrCase<"LessEqual", 3, "ule">, - I64EnumAttrCase<"Greater", 4, "ugt">, - I64EnumAttrCase<"GreaterEqual", 5, "uge">]> { - let cppNamespace = "::mlir::cbit"; -} - def CBit_RegisterType : TypeDef { let mnemonic = "reg"; let summary = "A static classical-bit register"; @@ -107,25 +88,46 @@ def LoadOp : CBitOp<"load"> { let hasVerifier = 1; } -def CompareOp : CBitOp<"cmp"> { - let summary = "Compare a classical-bit register with an integer"; +def ReadOp : CBitOp<"read"> { + let summary = "Read a classical-bit register as an integer"; let description = [{ - Compares the complete register with an unsigned integer of the same width. - The predicate must be `eq`, `ne`, `ult`, `ule`, `ugt`, or `uge`. + Reads the complete register as an exact-width integer bit pattern. Register + element zero is the least-significant result bit, and the result width must + equal the static register width. Example: ```mlir - %matches = cbit.cmp eq, %c, 1 : i2 : !cbit.reg<2> + %value = cbit.read %c : !cbit.reg<3> -> i3 ``` }]; - let arguments = (ins CBit_ComparisonPredicate:$predicate, - Arg:$reg, APIntAttr:$rhs); - let results = (outs I1:$result); + let arguments = (ins Arg:$reg); + let results = (outs AnySignlessInteger:$result); let assemblyFormat = [{ - $predicate `,` $reg `,` $rhs attr-dict `:` qualified(type($reg)) + $reg attr-dict `:` qualified(type($reg)) `->` qualified(type($result)) + }]; + let hasVerifier = 1; +} + +def WriteOp : CBitOp<"write"> { + let summary = "Write an integer to a classical-bit register"; + let description = [{ + Writes a fixed-width integer to the complete register. Integer bit zero is + stored in register element zero, and the value width must equal the static + register width. + + Example: + ```mlir + cbit.write %value, %c : i3, !cbit.reg<3> + ``` + }]; + + let arguments = (ins AnySignlessInteger:$value, + Arg:$reg); + let assemblyFormat = [{ + $value `,` $reg attr-dict `:` qualified(type($value)) `,` + qualified(type($reg)) }]; - let hasCanonicalizer = 1; let hasVerifier = 1; } diff --git a/mlir/include/mlir/Dialect/CBit/IR/CMakeLists.txt b/mlir/include/mlir/Dialect/CBit/IR/CMakeLists.txt index 5d6690081a..715fe0d4dc 100644 --- a/mlir/include/mlir/Dialect/CBit/IR/CMakeLists.txt +++ b/mlir/include/mlir/Dialect/CBit/IR/CMakeLists.txt @@ -13,6 +13,8 @@ mlir_tablegen(CBitOpsTypes.h.inc -gen-typedef-decls -typedefs-dialect=cbit) mlir_tablegen(CBitOpsTypes.cpp.inc -gen-typedef-defs -typedefs-dialect=cbit) mlir_tablegen(CBitOpsAttributes.h.inc -gen-attrdef-decls -attrdefs-dialect=cbit) mlir_tablegen(CBitOpsAttributes.cpp.inc -gen-attrdef-defs -attrdefs-dialect=cbit) + +set(LLVM_TARGET_DEFINITIONS CBitDialect.td) mlir_tablegen(CBitOpsEnums.h.inc -gen-enum-decls) mlir_tablegen(CBitOpsEnums.cpp.inc -gen-enum-defs) mlir_tablegen(CBitOpsDialect.h.inc -gen-dialect-decls -dialect=cbit) diff --git a/mlir/include/mlir/Support/IntegerExpressions.h b/mlir/include/mlir/Support/IntegerExpressions.h new file mode 100644 index 0000000000..ba3be6803a --- /dev/null +++ b/mlir/include/mlir/Support/IntegerExpressions.h @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace mlir { +class RewritePatternSet; +namespace mqt { + +/// Expand bounded integer rotations and population count for targets without +/// them. +void populateIntegerExpansionPatterns(RewritePatternSet& patterns); + +/// Maps signed ordering to the corresponding unsigned predicate. +inline arith::CmpIPredicate unsignedPredicate(arith::CmpIPredicate predicate) { + switch (predicate) { + case arith::CmpIPredicate::slt: + return arith::CmpIPredicate::ult; + case arith::CmpIPredicate::sle: + return arith::CmpIPredicate::ule; + case arith::CmpIPredicate::sgt: + return arith::CmpIPredicate::ugt; + case arith::CmpIPredicate::sge: + return arith::CmpIPredicate::uge; + default: + return predicate; + } +} + +/// Builds a logical shift with unsigned distance and zero on overshift. +/// Check before narrowing the distance and keep even unselected shifts valid +/// for interpreters that evaluate SSA operations eagerly. +inline Value buildZeroFillingShift(OpBuilder& builder, Location location, + Value value, Value distance, bool left) { + auto type = cast(value.getType()); + auto distanceType = cast(distance.getType()); + const auto width = type.getWidth(); + const auto constant = [&](const llvm::APInt& bits) -> Value { + return arith::ConstantOp::create(builder, location, + builder.getIntegerAttr(type, bits)); + }; + const auto shift = [&](Value amount) -> Value { + if (left) { + return arith::ShLIOp::create(builder, location, value, amount); + } + return arith::ShRUIOp::create(builder, location, value, amount); + }; + llvm::APInt amount; + if (matchPattern(distance, m_ConstantInt(&amount))) { + if (amount.uge(width)) { + return constant(llvm::APInt(width, 0)); + } + if (amount.isZero()) { + return value; + } + return shift(constant(amount.zextOrTrunc(width))); + } + Value inRange; + if (llvm::APInt::getMaxValue(distanceType.getWidth()).uge(width)) { + auto limit = arith::ConstantOp::create( + builder, location, builder.getIntegerAttr(distanceType, width)); + inRange = arith::CmpIOp::create(builder, location, + arith::CmpIPredicate::ult, distance, limit); + } + if (distanceType.getWidth() < width) { + distance = arith::ExtUIOp::create(builder, location, type, distance); + } else if (distanceType.getWidth() > width) { + distance = arith::TruncIOp::create(builder, location, type, distance); + } + if (!inRange) { + return shift(distance); + } + auto zero = constant(llvm::APInt(width, 0)); + auto safe = + arith::SelectOp::create(builder, location, inRange, distance, zero); + return arith::SelectOp::create(builder, location, inRange, shift(safe), zero); +} + +} // namespace mqt +} // namespace mlir diff --git a/mlir/include/mlir/Target/OpenQASM/Detail/OpenQASMParser.h b/mlir/include/mlir/Target/OpenQASM/Detail/OpenQASMParser.h index 8c7929cf1b..77af048d73 100644 --- a/mlir/include/mlir/Target/OpenQASM/Detail/OpenQASMParser.h +++ b/mlir/include/mlir/Target/OpenQASM/Detail/OpenQASMParser.h @@ -66,6 +66,10 @@ struct Expr { Float, Bool, Identifier, + IntCast, + BoolCast, + BitCast, + UintCast, AngleCast, Index, Neg, @@ -537,19 +541,15 @@ class Parser { advance(); // type const Expr* size = nullptr; - if (kind == TokenKind::Angle && current().kind == TokenKind::LBracket) { + if ((kind == TokenKind::Angle || kind == TokenKind::Int || + kind == TokenKind::Uint) && + current().kind == TokenKind::LBracket) { auto designator = parseDesignator(); if (failed(designator)) { return failure(); } size = *designator; } - if ((kind == TokenKind::Int || kind == TokenKind::Uint) && - current().kind == TokenKind::LBracket) { - return sink.error(current().loc, - "Integer declarations currently require the default " - "64-bit width"); - } if (current().kind != TokenKind::Identifier) { return expectedIdentifier("expected identifier"); } @@ -1604,7 +1604,12 @@ class Parser { } advance(); return expr; + case TokenKind::Bool: + case TokenKind::Bit: + case TokenKind::Int: + case TokenKind::Uint: case TokenKind::Angle: { + const auto type = current().kind; advance(); const Expr* size = nullptr; if (current().kind == TokenKind::LBracket) { @@ -1621,7 +1626,11 @@ class Parser { if (failed(operand) || failed(expect(TokenKind::RParen))) { return failure(); } - expr->kind = Expr::Kind::AngleCast; + expr->kind = type == TokenKind::Int ? Expr::Kind::IntCast + : type == TokenKind::Uint ? Expr::Kind::UintCast + : type == TokenKind::Bool ? Expr::Kind::BoolCast + : type == TokenKind::Bit ? Expr::Kind::BitCast + : Expr::Kind::AngleCast; expr->lhs = size; expr->rhs = *operand; return expr; diff --git a/mlir/include/mlir/Target/OpenQASM/Frontend.h b/mlir/include/mlir/Target/OpenQASM/Frontend.h index 6b071cadb1..f11bf9e24a 100644 --- a/mlir/include/mlir/Target/OpenQASM/Frontend.h +++ b/mlir/include/mlir/Target/OpenQASM/Frontend.h @@ -105,6 +105,14 @@ enum class ExpressionKind : uint8_t { GateParameter, Variable, Cast, + BitVectorCast, + Condition, + BitNot, + BitAnd, + BitOr, + BitXor, + ShiftLeft, + ShiftRight, Negate, ArcCos, ArcSin, @@ -135,10 +143,21 @@ struct ScalarExpression { ExpressionId lhs = 0; ExpressionId rhs = 0; BitVectorExpressionId bitVector = 0; + /// Zero denotes the default 64-bit machine integer type. + unsigned integerWidth = 0; + ConditionId condition = 0; }; enum class BitVectorExpressionKind : uint8_t { + ScalarCast, + Constant, Register, + Not, + And, + Or, + Xor, + ShiftLeft, + ShiftRight, RotateLeft, RotateRight, }; @@ -146,13 +165,17 @@ enum class BitVectorExpressionKind : uint8_t { struct BitVectorExpression { BitVectorExpressionKind kind = BitVectorExpressionKind::Register; uint64_t width = 0; + llvm::APInt constant = llvm::APInt(1, 0); RegisterId reg = 0; BitVectorExpressionId operand = 0; + BitVectorExpressionId rhs = 0; ExpressionId distance = 0; + ExpressionId scalar = 0; }; struct ScalarDeclaration { ScalarType type = ScalarType::Int; + unsigned integerWidth = 0; std::string name; SourceLocation location; }; @@ -210,6 +233,7 @@ enum class ConditionKind : uint8_t { And, Or, RegisterComparison, + BitVectorComparison, Comparison, }; @@ -224,6 +248,8 @@ struct ConditionExpression { ConditionId rhs = 0; RegisterId reg = 0; llvm::APInt expected = llvm::APInt(1, 0); + BitVectorExpressionId bitVectorComparisonLhs = 0; + BitVectorExpressionId bitVectorComparisonRhs = 0; ExpressionId comparisonLhs = 0; ExpressionId comparisonRhs = 0; ComparisonKind comparison = ComparisonKind::Equal; diff --git a/mlir/lib/Conversion/CBitToMemRef/CBitToMemRef.cpp b/mlir/lib/Conversion/CBitToMemRef/CBitToMemRef.cpp index 3840441811..94749fe7d2 100644 --- a/mlir/lib/Conversion/CBitToMemRef/CBitToMemRef.cpp +++ b/mlir/lib/Conversion/CBitToMemRef/CBitToMemRef.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include @@ -88,25 +89,6 @@ struct ConvertLoadOp final : OpConversionPattern { } }; -struct ConvertCompareOp final : OpConversionPattern { - using OpConversionPattern::OpConversionPattern; - - LogicalResult - matchAndRewrite(cbit::CompareOp op, OpAdaptor adaptor, - ConversionPatternRewriter& rewriter) const override { - auto result = cbit::buildComparison( - rewriter, op.getLoc(), op.getPredicate(), op.getRhs(), - [&](const int64_t index) -> Value { - auto indexValue = - arith::ConstantIndexOp::create(rewriter, op.getLoc(), index); - return memref::LoadOp::create(rewriter, op.getLoc(), adaptor.getReg(), - ValueRange{indexValue}); - }); - rewriter.replaceOp(op, result); - return success(); - } -}; - struct ConvertStoreOp final : OpConversionPattern { using OpConversionPattern::OpConversionPattern; @@ -129,6 +111,13 @@ struct ConvertCBitToMemRef final auto moduleOp = getOperation(); CBitTypeConverter typeConverter; ConversionTarget target(*context); + + { + RewritePatternSet patterns(context); + cbit::populateCBitDecompositionPatterns(patterns); + const FrozenRewritePatternSet frozen(std::move(patterns)); + walkAndApplyPatterns(moduleOp, frozen); + } RewritePatternSet patterns(context); target.addIllegalDialect(); @@ -142,9 +131,8 @@ struct ConvertCBitToMemRef final target.addDynamicallyLegalOp( [&](Operation* op) { return typeConverter.isLegal(op); }); - patterns - .add( - typeConverter, context); + patterns.add(typeConverter, + context); populateFunctionOpInterfaceTypeConversionPattern( patterns, typeConverter); populateReturnOpTypeConversionPattern(patterns, typeConverter); diff --git a/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp b/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp index 92877f3b7f..7671136365 100644 --- a/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp +++ b/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp @@ -196,8 +196,8 @@ static Value toIndex(Location loc, Value value, return input; } } - return arith::IndexCastOp::create(rewriter, loc, rewriter.getIndexType(), - value); + return rewriter.createOrFold(loc, rewriter.getIndexType(), + value); } /** @@ -266,6 +266,19 @@ static cbit::RegisterType getCBitType(Type type) { return cbit::RegisterType::get(type.getContext(), tensorType.getShape()[0]); } +/// Earlier bit reads finish using an array before a later storage update. +/// Other users can pass an alias to values that remain live after the update. +static bool needsArrayCopy(Value value, Operation* update) { + return llvm::any_of(value.getUsers(), [&](Operation* user) { + if (user == update) { + return false; + } + auto* ancestor = update->getBlock()->findAncestorOpInBlock(*user); + return !isa(user) || ancestor == nullptr || + !ancestor->isBeforeInBlock(update); + }); +} + /** * @brief Moves a region from a jeff operation to a QCO/SCF operation */ @@ -339,7 +352,12 @@ struct ConvertJeffIntArrayZeroOpToCBit final /// Converts a jeff i1-array update to a CBit store. struct ConvertJeffIntArraySetIndexOpToCBit final : OpConversionPattern { - using OpConversionPattern::OpConversionPattern; + ConvertJeffIntArraySetIndexOpToCBit(TypeConverter& converter, + MLIRContext* context, + const DenseSet& shared) + : OpConversionPattern(converter, context, PatternBenefit(2)), + shared(shared) {} + const DenseSet& shared; LogicalResult matchAndRewrite(jeff::IntArraySetIndexOp op, OpAdaptor adaptor, @@ -348,6 +366,15 @@ struct ConvertJeffIntArraySetIndexOpToCBit final if (!isa(reg.getType())) { return failure(); } + if (shared.contains(op)) { + auto type = cast(reg.getType()); + auto snapshot = cbit::ReadOp::create( + rewriter, op.getLoc(), + rewriter.getIntegerType(static_cast(type.getWidth())), reg); + reg = cbit::AllocOp::create(rewriter, op.getLoc(), type, + cbit::Initialization::Zero); + cbit::WriteOp::create(rewriter, op.getLoc(), snapshot, reg); + } auto index = toIndex(op.getLoc(), adaptor.getIndex(), rewriter); cbit::StoreOp::create(rewriter, op.getLoc(), adaptor.getValue(), reg, index); @@ -356,6 +383,21 @@ struct ConvertJeffIntArraySetIndexOpToCBit final } }; +/// The schema's right shift is logical, independently of operand signedness. +struct ConvertJeffLogicalShift final : OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + LogicalResult + matchAndRewrite(jeff::IntBinaryOp op, OpAdaptor adaptor, + ConversionPatternRewriter& rewriter) const override { + if (op.getOp() != jeff::IntBinaryOperation::_shr) { + return failure(); + } + rewriter.replaceOpWithNewOp(op, adaptor.getA(), + adaptor.getB()); + return success(); + } +}; + /// Converts a jeff i1-array access to a CBit load. struct ConvertJeffIntArrayGetIndexOpToCBit final : OpConversionPattern { @@ -965,7 +1007,13 @@ struct ConvertJeffSwitchOpToQCO final : OpConversionPattern { if (!adaptor.getSelection().getType().isInteger(1)) { return rewriter.notifyMatchFailure(op, "qco.if requires an i1 selector"); } - if (op.getDefault().front().getOperations().size() != 1) { + if (op.getDefault().empty()) { + return rewriter.notifyMatchFailure(op, + "qco.if requires a default branch"); + } + if (op.getDefault().front().getOperations().size() != 1 && + !llvm::all_of(op.getResultTypes(), + [](Type type) { return isa(type); })) { return rewriter.notifyMatchFailure( op, "qco.if requires a trivial default branch"); } @@ -976,6 +1024,51 @@ struct ConvertJeffSwitchOpToQCO final : OpConversionPattern { auto inValues = adaptor.getInValues(); + /// Pure selections are ordinary SSA values, not mutable register aliases. + if (llvm::all_of(op.getResultTypes(), + [](Type type) { return isa(type); })) { + SmallVector falseValues; + SmallVector trueValues; + for (auto [index, region] : llvm::enumerate(op.getBranches())) { + if (llvm::any_of( + region.front().without_terminator(), [](Operation& nested) { + return !isa(nested); + })) { + return rewriter.notifyMatchFailure( + op, + "integer switches require expression-only selection branches"); + } + auto yield = cast(region.front().getTerminator()); + auto& values = index == 0 ? falseValues : trueValues; + for (auto value : yield.getOperands()) { + auto argument = dyn_cast(value); + if (argument && argument.getOwner() == ®ion.front()) { + values.push_back(inValues[argument.getArgNumber()]); + } else if (auto* constant = value.getDefiningOp(); + isa_and_nonnull( + constant)) { + values.push_back(rewriter.clone(*constant)->getResult(0)); + } else { + return rewriter.notifyMatchFailure( + op, "selection must yield an input or integer constant"); + } + } + } + SmallVector results; + for (auto [trueValue, falseValue] : + llvm::zip_equal(trueValues, falseValues)) { + results.push_back(arith::SelectOp::create(rewriter, op.getLoc(), + adaptor.getSelection(), + trueValue, falseValue)); + } + rewriter.replaceOp(op, results); + return success(); + } + // The operands may already carry converted types, which `isLinearType` does // not recognize. The results still carry jeff types and correspond to the // in-values positionally, so they decide which in-values are qubits. @@ -1277,6 +1370,43 @@ struct JeffToQCO final : impl::JeffToQCOBase { return; } + DenseSet sharedArrayUpdates; + const auto unsupportedSnapshots = moduleOp.walk([&](Operation* operation) { + if (auto update = dyn_cast(operation); + update && getCBitType(update.getInArray().getType()) && + needsArrayCopy(update.getInArray(), update)) { + if (update->getParentOfType() || + update->getParentOfType()) { + update.emitError("live old array values inside jeff switch or while " + "regions are not supported"); + return WalkResult::interrupt(); + } + sharedArrayUpdates.insert(update); + } + /// ponytail: reject live arrays across any mutating region; track region + /// argument aliases if independent live arrays need support. + if (isa(operation) && + llvm::any_of(operation->getOperands(), [&](Value value) { + return getCBitType(value.getType()) && + needsArrayCopy(value, operation); + })) { + bool updatesArray = false; + operation->walk([&](jeff::IntArraySetIndexOp update) { + updatesArray |= + static_cast(getCBitType(update.getInArray().getType())); + }); + if (updatesArray) { + operation->emitError("live old array values across jeff control " + "flow are not supported"); + return WalkResult::interrupt(); + } + } + return WalkResult::advance(); + }); + if (unsupportedSnapshots.wasInterrupted()) { + signalPassFailure(); + return; + } ConversionTarget target(*context); RewritePatternSet patterns(context); JeffToQCOTypeConverter typeConverter(context); @@ -1301,8 +1431,9 @@ struct JeffToQCO final : impl::JeffToQCOBase { populateFunctionOpInterfaceTypeConversionPattern( patterns, typeConverter); populateReturnOpTypeConversionPattern(patterns, typeConverter); - patterns.add(typeConverter, context, + sharedArrayUpdates); + patterns.add( typeConverter, context, PatternBenefit(2)); patterns.add< diff --git a/mlir/lib/Conversion/QCOToJeff/CMakeLists.txt b/mlir/lib/Conversion/QCOToJeff/CMakeLists.txt index 9028a06963..217cd8d2d1 100644 --- a/mlir/lib/Conversion/QCOToJeff/CMakeLists.txt +++ b/mlir/lib/Conversion/QCOToJeff/CMakeLists.txt @@ -15,6 +15,9 @@ add_mlir_conversion_library( QCOToJeffIncGen LINK_LIBS MLIRDialect + MLIRFuncTransforms + MLIRLLVMDialect + MLIRSupportMQT MLIRCBitDialect MLIRJeff MLIRNativeToJeff diff --git a/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp b/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp index 97a77e0aab..255349cca0 100644 --- a/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp +++ b/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp @@ -19,6 +19,7 @@ #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" +#include "mlir/Support/IntegerExpressions.h" #include #include @@ -26,8 +27,11 @@ #include #include #include +#include #include #include +#include +#include #include #include #include @@ -46,6 +50,7 @@ #include #include #include +#include #include #include @@ -137,9 +142,9 @@ class ClassicalRegisterSSAState { /// Records source register operands before dialect conversion remaps them. void recordRegisterUses(Operation* root) { root->walk([&](Operation* operation) { - if (isa(operation)) { + if (isa(operation)) { operationRegisters[operation] = operation->getOperand(0); - } else if (isa(operation)) { + } else if (isa(operation)) { operationRegisters[operation] = operation->getOperand(1); } }); @@ -631,14 +636,200 @@ struct ConvertCBitLoadOpToJeff final } }; -/// Converts a CBit register comparison to jeff array reads and Boolean logic. -struct ConvertCBitCompareOpToJeff final - : StatefulOpConversionPattern { +} // namespace + +/// Integer representation limits belong to this backend, not QC/QCO. +static unsigned nativeIntegerWidth(unsigned width) { + for (const unsigned candidate : {1U, 8U, 16U, 32U, 64U}) { + if (width <= candidate) { + return candidate; + } + } + return 0; +} + +static Value integerConstant(OpBuilder& builder, Location loc, IntegerType type, + const APInt& value) { + auto attribute = + builder.getIntegerAttr(type, value.zextOrTrunc(type.getWidth())); + switch (type.getWidth()) { + case 1: + return jeff::IntConst1Op::create(builder, loc, attribute); + case 8: + return jeff::IntConst8Op::create(builder, loc, attribute); + case 16: + return jeff::IntConst16Op::create(builder, loc, attribute); + case 32: + return jeff::IntConst32Op::create(builder, loc, attribute); + case 64: + return jeff::IntConst64Op::create(builder, loc, attribute); + default: + llvm_unreachable("unsupported jeff integer width"); + } +} + +static Value selectInteger(OpBuilder& builder, Location loc, Value condition, + Value trueValue, Value falseValue) { + /// The current serializer infers result types from the input signature. + /// Carry one difference value and yield either that value or zero. + auto difference = jeff::IntBinaryOp::create( + builder, loc, trueValue, falseValue, jeff::IntBinaryOperation::_xor); + auto select = + jeff::SwitchOp::create(builder, loc, TypeRange{trueValue.getType()}, + condition, ValueRange{difference}, 2); + { + OpBuilder::InsertionGuard guard(builder); + for (auto& region : select->getRegions()) { + auto* block = builder.createBlock(®ion, {}, + TypeRange{trueValue.getType()}, {loc}); + Value result = block->getArgument(0); + if (®ion != &select.getBranches()[1]) { + auto type = cast(trueValue.getType()); + result = integerConstant(builder, loc, type, APInt(type.getWidth(), 0)); + } + jeff::YieldOp::create(builder, loc, result); + } + } + return jeff::IntBinaryOp::create(builder, loc, select.getResult(0), + falseValue, jeff::IntBinaryOperation::_xor); +} + +static Value maskInteger(OpBuilder& builder, Location loc, Value value, + unsigned width) { + auto type = cast(value.getType()); + if (width == type.getWidth()) { + return value; + } + auto mask = integerConstant(builder, loc, type, + APInt::getLowBitsSet(type.getWidth(), width)); + return jeff::IntBinaryOp::create(builder, loc, value, mask, + jeff::IntBinaryOperation::_and); +} + +/// Extend the sign bit in a promoted representation using (x xor sign) - sign. +static Value signedInteger(OpBuilder& builder, Location loc, Value value, + unsigned width) { + auto type = cast(value.getType()); + if (width == type.getWidth()) { + return value; + } + auto sign = integerConstant(builder, loc, type, + APInt::getOneBitSet(type.getWidth(), width - 1)); + auto biased = jeff::IntBinaryOp::create(builder, loc, value, sign, + jeff::IntBinaryOperation::_xor); + return jeff::IntBinaryOp::create(builder, loc, biased, sign, + jeff::IntBinaryOperation::_sub); +} + +/// Keep reconstructed integers shallow enough for expression-based exporters. +static Value joinBits(OpBuilder& builder, Location loc, + SmallVector bits) { + while (bits.size() > 1) { + size_t output = 0; + for (size_t input = 0; input < bits.size(); input += 2) { + bits[output++] = input + 1 == bits.size() + ? bits[input] + : jeff::IntBinaryOp::create( + builder, loc, bits[input], bits[input + 1], + jeff::IntBinaryOperation::_or) + .getResult(); + } + bits.resize(output); + } + return bits.front(); +} + +/// jeff has no integer cast: extract at most 64 bits into the target +/// representation. +static Value castInteger(OpBuilder& builder, Location loc, Value value, + unsigned sourceWidth, IntegerType targetType, + unsigned targetWidth, bool signExtend) { + auto sourceType = cast(value.getType()); + Value result = value; + if (sourceType != targetType) { + SmallVector bits; + auto zero = integerConstant(builder, loc, sourceType, + APInt(sourceType.getWidth(), 0)); + for (unsigned bit = 0; bit < std::min(sourceWidth, targetWidth); ++bit) { + auto mask = + integerConstant(builder, loc, sourceType, + APInt::getOneBitSet(sourceType.getWidth(), bit)); + auto masked = jeff::IntBinaryOp::create(builder, loc, value, mask, + jeff::IntBinaryOperation::_and); + auto isZero = jeff::IntComparisonOp::create( + builder, loc, masked, zero, jeff::IntComparisonOperation::_eq); + auto targetBit = + integerConstant(builder, loc, targetType, + APInt::getOneBitSet(targetType.getWidth(), bit)); + auto selected = + selectInteger(builder, loc, isZero, + integerConstant(builder, loc, targetType, + APInt(targetType.getWidth(), 0)), + targetBit); + bits.push_back(selected); + } + result = joinBits(builder, loc, std::move(bits)); + } + if (signExtend && targetWidth > sourceWidth) { + result = signedInteger(builder, loc, result, sourceWidth); + } + return maskInteger(builder, loc, result, targetWidth); +} + +namespace { + +struct ConvertCBitReadOpToJeff final + : StatefulOpConversionPattern { using StatefulOpConversionPattern::StatefulOpConversionPattern; + LogicalResult + matchAndRewrite(cbit::ReadOp op, OpAdaptor, + ConversionPatternRewriter& rewriter) const override { + const auto width = op.getType().getWidth(); + if (width > 64) { + return op.emitError( + "jeff supports general integer expressions only up to 64 bits"); + } + auto& state = getState().cbitState; + auto reg = state.resolveRegisterUse(op, op.getReg()); + auto array = state.getCurrentValue(reg, op); + if (!array) { + return rewriter.notifyMatchFailure(op, "unknown classical register"); + } + array = rewriter.getRemappedValue(array); + auto type = + cast(getTypeConverter()->convertType(op.getType())); + auto zero = + integerConstant(rewriter, op.getLoc(), type, APInt(type.getWidth(), 0)); + SmallVector bits; + for (unsigned bit = 0; bit < width; ++bit) { + auto index = integerConstant(rewriter, op.getLoc(), rewriter.getI32Type(), + APInt(32, bit)); + auto value = jeff::IntArrayGetIndexOp::create( + rewriter, op.getLoc(), rewriter.getI1Type(), array, index); + Value selected = value; + if (width != 1) { + auto mask = integerConstant(rewriter, op.getLoc(), type, + APInt::getOneBitSet(type.getWidth(), bit)); + selected = selectInteger(rewriter, op.getLoc(), value, mask, zero); + } + bits.push_back(selected); + } + rewriter.replaceOp(op, joinBits(rewriter, op.getLoc(), std::move(bits))); + return success(); + } +}; +struct ConvertCBitWriteOpToJeff final + : StatefulOpConversionPattern { + using StatefulOpConversionPattern::StatefulOpConversionPattern; LogicalResult - matchAndRewrite(cbit::CompareOp op, OpAdaptor /*adaptor*/, + matchAndRewrite(cbit::WriteOp op, OpAdaptor adaptor, ConversionPatternRewriter& rewriter) const override { + const auto width = cast(op.getValue().getType()).getWidth(); + if (width > 64) { + return op.emitError( + "jeff supports general integer expressions only up to 64 bits"); + } auto& state = getState().cbitState; auto reg = state.resolveRegisterUse(op, op.getReg()); auto array = state.getCurrentValue(reg, op); @@ -646,16 +837,258 @@ struct ConvertCBitCompareOpToJeff final return rewriter.notifyMatchFailure(op, "unknown classical register"); } array = rewriter.getRemappedValue(array); - auto result = cbit::buildComparison( - rewriter, op.getLoc(), op.getPredicate(), op.getRhs(), - [&](const int64_t index) -> Value { - auto position = jeff::IntConst32Op::create( - rewriter, op.getLoc(), - rewriter.getI32IntegerAttr(static_cast(index))); - return jeff::IntArrayGetIndexOp::create( - rewriter, op.getLoc(), rewriter.getI1Type(), array, position); + auto value = adaptor.getValue(); + auto type = cast(value.getType()); + auto zero = + integerConstant(rewriter, op.getLoc(), type, APInt(type.getWidth(), 0)); + for (unsigned bit = 0; bit < width; ++bit) { + auto mask = integerConstant(rewriter, op.getLoc(), type, + APInt::getOneBitSet(type.getWidth(), bit)); + auto masked = jeff::IntBinaryOp::create( + rewriter, op.getLoc(), value, mask, jeff::IntBinaryOperation::_and); + auto isZero = + jeff::IntComparisonOp::create(rewriter, op.getLoc(), masked, zero, + jeff::IntComparisonOperation::_eq); + auto one = integerConstant(rewriter, op.getLoc(), rewriter.getI1Type(), + APInt(1, 1)); + auto selected = jeff::IntBinaryOp::create( + rewriter, op.getLoc(), isZero, one, jeff::IntBinaryOperation::_xor); + auto index = integerConstant(rewriter, op.getLoc(), rewriter.getI32Type(), + APInt(32, bit)); + array = jeff::IntArraySetIndexOp::create( + rewriter, op.getLoc(), array.getType(), array, index, selected); + } + state.setCurrentValue(reg, array, op); + state.addAlias(array, reg); + rewriter.eraseOp(op); + return success(); + } +}; + +/// Override the dependency adapter for exact-width integer computations. +struct ConvertIntegerExpression final : ConversionPattern { + ConvertIntegerExpression(TypeConverter& converter, MLIRContext* context) + : ConversionPattern(converter, MatchAnyOpTypeTag(), 10, context) {} + LogicalResult + matchAndRewrite(Operation* op, ArrayRef operands, + ConversionPatternRewriter& rewriter) const override { + if (op->getNumResults() != 1 || + !isa(op->getResult(0).getType()) || + op->getName().getDialectNamespace() != "arith") { + return failure(); + } + auto originalType = cast(op->getResult(0).getType()); + auto width = originalType.getWidth(); + if (width > 64) { + return op->emitError( + "jeff supports general integer expressions only up to 64 bits"); + } + auto type = + cast(getTypeConverter()->convertType(originalType)); + auto loc = op->getLoc(); + if (auto constant = dyn_cast(op)) { + rewriter.replaceOp( + op, + integerConstant(rewriter, loc, type, + cast(constant.getValue()).getValue())); + return success(); + } + if (isa(op)) { + rewriter.replaceOp( + op, + castInteger(rewriter, loc, operands[0], + cast(op->getOperand(0).getType()).getWidth(), + type, width, isa(op))); + return success(); + } + if (isa(op)) { + rewriter.replaceOp(op, selectInteger(rewriter, loc, operands[0], + operands[1], operands[2])); + return success(); + } + if (auto comparison = dyn_cast(op)) { + auto lhs = operands[0]; + auto rhs = operands[1]; + auto predicate = comparison.getPredicate(); + const auto unsignedPredicate = mqt::unsignedPredicate(predicate); + if (unsignedPredicate != predicate) { + auto operandType = cast(lhs.getType()); + auto sourceType = dyn_cast(comparison.getLhs().getType()); + auto sourceWidth = + sourceType ? sourceType.getWidth() : operandType.getWidth(); + auto sign = integerConstant( + rewriter, loc, operandType, + APInt::getOneBitSet(operandType.getWidth(), sourceWidth - 1)); + lhs = jeff::IntBinaryOp::create(rewriter, loc, lhs, sign, + jeff::IntBinaryOperation::_xor); + rhs = jeff::IntBinaryOp::create(rewriter, loc, rhs, sign, + jeff::IntBinaryOperation::_xor); + } + predicate = unsignedPredicate; + if (predicate == arith::CmpIPredicate::ugt || + predicate == arith::CmpIPredicate::uge) { + std::swap(lhs, rhs); + } + auto operation = predicate == arith::CmpIPredicate::eq || + predicate == arith::CmpIPredicate::ne + ? jeff::IntComparisonOperation::_eq + : predicate == arith::CmpIPredicate::ult || + predicate == arith::CmpIPredicate::ugt + ? jeff::IntComparisonOperation::_ltU + : jeff::IntComparisonOperation::_lteU; + Value result = + jeff::IntComparisonOp::create(rewriter, loc, lhs, rhs, operation); + if (predicate == arith::CmpIPredicate::ne) { + result = jeff::IntBinaryOp::create( + rewriter, loc, result, + integerConstant(rewriter, loc, type, APInt(1, 1)), + jeff::IntBinaryOperation::_xor); + } + rewriter.replaceOp(op, result); + return success(); + } + auto operation = + llvm::StringSwitch>( + op->getName().getStringRef()) + .Case("arith.addi", jeff::IntBinaryOperation::_add) + .Case("arith.subi", jeff::IntBinaryOperation::_sub) + .Case("arith.muli", jeff::IntBinaryOperation::_mul) + .Case("arith.divui", jeff::IntBinaryOperation::_divU) + .Case("arith.divsi", jeff::IntBinaryOperation::_divS) + .Case("arith.remui", jeff::IntBinaryOperation::_remU) + .Case("arith.remsi", jeff::IntBinaryOperation::_remS) + .Case("arith.minsi", jeff::IntBinaryOperation::_minS) + .Case("arith.maxsi", jeff::IntBinaryOperation::_maxS) + .Case("arith.andi", jeff::IntBinaryOperation::_and) + .Case("arith.ori", jeff::IntBinaryOperation::_or) + .Case("arith.xori", jeff::IntBinaryOperation::_xor) + .Case("arith.shli", jeff::IntBinaryOperation::_shl) + .Cases({"arith.shrui", "arith.shrsi"}, + jeff::IntBinaryOperation::_shr) + .Default(std::nullopt); + if (!operation) { + return failure(); + } + auto lhs = operands[0]; + auto rhs = operands[1]; + if (isa( + op)) { + lhs = signedInteger(rewriter, loc, lhs, width); + rhs = signedInteger(rewriter, loc, rhs, width); + } + Value result = + jeff::IntBinaryOp::create(rewriter, loc, lhs, rhs, *operation); + if (isa(op)) { + auto sign = integerConstant( + rewriter, loc, type, APInt::getOneBitSet(type.getWidth(), width - 1)); + auto signBit = jeff::IntBinaryOp::create(rewriter, loc, lhs, sign, + jeff::IntBinaryOperation::_and); + auto zero = + integerConstant(rewriter, loc, type, APInt(type.getWidth(), 0)); + auto nonnegative = jeff::IntComparisonOp::create( + rewriter, loc, signBit, zero, jeff::IntComparisonOperation::_eq); + auto ones = integerConstant(rewriter, loc, type, + APInt::getLowBitsSet(type.getWidth(), width)); + auto shiftedMask = jeff::IntBinaryOp::create( + rewriter, loc, ones, rhs, jeff::IntBinaryOperation::_shr); + auto fill = jeff::IntBinaryOp::create(rewriter, loc, ones, shiftedMask, + jeff::IntBinaryOperation::_xor); + auto selected = selectInteger(rewriter, loc, nonnegative, zero, fill); + result = jeff::IntBinaryOp::create(rewriter, loc, result, selected, + jeff::IntBinaryOperation::_or); + } + rewriter.replaceOp(op, maskInteger(rewriter, loc, result, width)); + return success(); + } +}; + +} // namespace + +static Value +buildBitComparison(OpBuilder& builder, const Location location, + const arith::CmpIPredicate predicate, const llvm::APInt& rhs, + const llvm::function_ref loadBit) { + const auto encodedPredicate = mqt::unsignedPredicate(predicate); + auto encodedRhs = rhs; + const bool biasSignBit = encodedPredicate != predicate; + if (biasSignBit) { + encodedRhs.flipBit(encodedRhs.getBitWidth() - 1U); + } + + auto one = arith::ConstantIntOp::create(builder, location, 1, 1); + Value equal = one; + Value less; + if (encodedPredicate != arith::CmpIPredicate::eq && + encodedPredicate != arith::CmpIPredicate::ne) { + less = arith::ConstantIntOp::create(builder, location, 0, 1); + } + for (int64_t index = static_cast(encodedRhs.getBitWidth()) - 1; + index >= 0; --index) { + auto bit = loadBit(index); + if (biasSignBit && + index == static_cast(encodedRhs.getBitWidth()) - 1) { + bit = arith::XOrIOp::create(builder, location, bit, one); + } + Value matches = bit; + if (!encodedRhs[static_cast(index)]) { + matches = arith::XOrIOp::create(builder, location, bit, one); + } else if (less) { + auto lower = arith::XOrIOp::create(builder, location, bit, one); + auto firstDifference = + arith::AndIOp::create(builder, location, equal, lower); + less = arith::OrIOp::create(builder, location, less, firstDifference); + } + equal = arith::AndIOp::create(builder, location, equal, matches); + } + switch (encodedPredicate) { + case arith::CmpIPredicate::eq: + return equal; + case arith::CmpIPredicate::ne: + return arith::XOrIOp::create(builder, location, equal, one); + case arith::CmpIPredicate::ult: + return less; + case arith::CmpIPredicate::ule: + return arith::OrIOp::create(builder, location, less, equal); + case arith::CmpIPredicate::ugt: { + auto lessOrEqual = arith::OrIOp::create(builder, location, less, equal); + return arith::XOrIOp::create(builder, location, lessOrEqual, one); + } + case arith::CmpIPredicate::uge: + return arith::XOrIOp::create(builder, location, less, one); + default: + llvm_unreachable("signed CBit predicate must be encoded as unsigned"); + } +} + +namespace { + +/// Lower explicit register snapshots compared with constants at the read point. +struct LowerRegisterComparison final : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(arith::CmpIOp op, + PatternRewriter& rewriter) const override { + auto read = op.getLhs().getDefiningOp(); + llvm::APInt constant; + if (!read || read.getType().getWidth() <= 64 || + !matchPattern(op.getRhs(), m_ConstantInt(&constant))) { + return failure(); + } + OpBuilder::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(read); + auto result = buildBitComparison( + rewriter, op.getLoc(), op.getPredicate(), constant, + [&](int64_t index) -> Value { + auto position = + arith::ConstantIndexOp::create(rewriter, read.getLoc(), index); + return cbit::LoadOp::create(rewriter, read.getLoc(), + rewriter.getI1Type(), read.getReg(), + position); }); rewriter.replaceOp(op, result); + if (read->use_empty()) { + rewriter.eraseOp(read); + } return success(); } }; @@ -1709,12 +2142,12 @@ struct ConvertQCOMainToJeff final : StatefulOpConversionPattern { return failure(); } + if (failed( + rewriter.convertRegionTypes(&op.getBody(), *getTypeConverter()))) { + return failure(); + } rewriter.startOpModification(op); op.setType(rewriter.getFunctionType(newInputs, newResults)); - for (const auto& [argument, type] : - llvm::zip_equal(block->getArguments(), newInputs)) { - argument.setType(type); - } mqt::removeEntryPoint(op); rewriter.finalizeOpModification(op); @@ -1754,6 +2187,11 @@ class QCOToJeffTypeConverter final : public TypeConverter { // Identity conversion for all types by default addConversion([](Type type) { return type; }); + addConversion([ctx](IntegerType type) -> Type { + const auto width = nativeIntegerWidth(type.getWidth()); + return width != 0 ? IntegerType::get(ctx, width) : Type{}; + }); + addConversion([ctx](IndexType /*type*/) -> Type { return IntegerType::get(ctx, 32); }); @@ -1884,6 +2322,29 @@ struct QCOToJeff final : impl::QCOToJeffBase { return; } + RewritePatternSet comparisons(context); + comparisons.add(context); + arith::CmpIOp::getCanonicalizationPatterns(comparisons, context); + mqt::populateIntegerExpansionPatterns(comparisons); + if (failed(applyPatternsGreedily(moduleOp, std::move(comparisons)))) { + signalPassFailure(); + return; + } + const auto unsupportedMath = moduleOp.walk([](Operation* op) { + if (isa(op)) { + auto type = dyn_cast(op->getResult(0).getType()); + if (type && nativeIntegerWidth(type.getWidth()) != type.getWidth()) { + op->emitError( + "jeff requires a native integer width for this operation"); + return WalkResult::interrupt(); + } + } + return WalkResult::advance(); + }); + if (unsupportedMath.wasInterrupted()) { + signalPassFailure(); + return; + } ConversionTarget target(*context); RewritePatternSet patterns(context); QCOToJeffTypeConverter typeConverter(context); @@ -1897,25 +2358,30 @@ struct QCOToJeff final : impl::QCOToJeffBase { math::MathDialect, tensor::TensorDialect, scf::SCFDialect, memref::MemRefDialect>(); target.addLegalDialect(); + target.addIllegalOp(); - target.addDynamicallyLegalOp( - [](func::FuncOp op) { return !mqt::isEntryPoint(op); }); - target.addDynamicallyLegalOp([](func::ReturnOp op) { - return llvm::none_of(op.getOperandTypes(), [](Type type) { - return isa(type); - }); + target.addDynamicallyLegalOp([&](func::FuncOp op) { + return !mqt::isEntryPoint(op) && + typeConverter.isSignatureLegal(op.getFunctionType()) && + typeConverter.isLegal(&op.getBody()); + }); + target.addDynamicallyLegalOp([&](func::ReturnOp op) { + return typeConverter.isLegal(op.getOperandTypes()); }); + populateFunctionOpInterfaceTypeConversionPattern( + patterns, typeConverter); // Register operation conversion patterns jeff::populateNativeToJeffConversionPatterns(patterns); - patterns.add( - typeConverter, context, &state); + patterns.add(typeConverter, context); + patterns.add(typeConverter, context, &state); using JK = JeffKind; using PP = PPRPaulis; diff --git a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp index 0bd7f02c9c..33a6af4819 100644 --- a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp +++ b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp @@ -649,8 +649,9 @@ collectRegisterAccesses(Operation* root, LoweringState& state) { } } - if (!isagetName().getDialectNamespace() != + cbit::CBitDialect::getDialectNamespace() && + !isa(operation)) { return WalkResult::advance(); } diff --git a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp index f234c04b64..512acd8b71 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp @@ -44,6 +44,7 @@ #include #include #include +#include #include #include @@ -66,8 +67,8 @@ constexpr unsigned MIXED_CBIT_REGISTER = } // namespace -static LogicalResult prepareCBitRegisterReads(Operation* moduleOp, - LoweringState& state) { +static LogicalResult prepareCBitRegisterAccesses(Operation* moduleOp, + LoweringState& state) { DenseMap> forwardedRegisters; moduleOp->walk([&](Operation* operation) { for (auto& region : operation->getRegions()) { @@ -140,24 +141,35 @@ static LogicalResult prepareCBitRegisterReads(Operation* moduleOp, } } - bool hasMixedRepresentation = false; + bool hasInvalidAccess = false; const auto prepareRead = [&](Operation* operation, Value reg) { const auto representation = representations.lookup(reg); if (representation == MIXED_CBIT_REGISTER) { operation->emitOpError( "adaptive QIR conversion cannot merge returned and local CBit " "registers"); - hasMixedRepresentation = true; + hasInvalidAccess = true; } else if (representation == RETURNED_CBIT_REGISTER) { state.returnedCBitReads.insert(operation); } }; moduleOp->walk( [&](cbit::LoadOp loadOp) { prepareRead(loadOp, loadOp.getReg()); }); - moduleOp->walk([&](cbit::CompareOp compareOp) { - prepareRead(compareOp, compareOp.getReg()); + moduleOp->walk([&](cbit::StoreOp storeOp) { + const auto representation = representations.lookup(storeOp.getReg()); + if (representation == MIXED_CBIT_REGISTER) { + storeOp.emitOpError( + "adaptive QIR conversion cannot merge returned and local CBit " + "registers"); + hasInvalidAccess = true; + } else if (representation == RETURNED_CBIT_REGISTER) { + storeOp.emitOpError( + "adaptive QIR conversion does not support non-measurement writes " + "to returned CBit registers"); + hasInvalidAccess = true; + } }); - return success(!hasMixedRepresentation); + return success(!hasInvalidAccess); } /** @@ -331,28 +343,6 @@ struct ConvertCBitLoadOp final : StatefulOpConversionPattern { } }; -struct ConvertCBitCompareOp final - : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; - - LogicalResult - matchAndRewrite(cbit::CompareOp op, OpAdaptor adaptor, - ConversionPatternRewriter& rewriter) const override { - const auto returnedRegister = - getState().returnedCBitReads.contains(op.getOperation()); - auto result = cbit::buildComparison( - rewriter, op.getLoc(), op.getPredicate(), op.getRhs(), - [&](const int64_t index) -> Value { - auto indexValue = LLVM::ConstantOp::create( - rewriter, op.getLoc(), rewriter.getI64Type(), index); - return loadCBit(op, adaptor.getReg(), indexValue, rewriter, - returnedRegister); - }); - rewriter.replaceOp(op, result); - return success(); - } -}; - struct ConvertCBitStoreOp final : StatefulOpConversionPattern { using StatefulOpConversionPattern::StatefulOpConversionPattern; @@ -677,8 +667,8 @@ static void populateQCToQIRAdaptivePatterns(RewritePatternSet& patterns, MLIRContext* ctx, LoweringState& state) { populateQCToQIRPatterns(patterns, typeConverter, ctx, state); - patterns.add(typeConverter, ctx, &state); @@ -858,7 +848,13 @@ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { signalPassFailure(); return; } - if (failed(prepareCBitRegisterReads(moduleOp, state))) { + { + RewritePatternSet patterns(ctx); + cbit::populateCBitDecompositionPatterns(patterns); + const FrozenRewritePatternSet frozen(std::move(patterns)); + walkAndApplyPatterns(moduleOp, frozen); + } + if (failed(prepareCBitRegisterAccesses(moduleOp, state))) { signalPassFailure(); return; } diff --git a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp index e0ce975244..1add8d0cb3 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp @@ -142,14 +142,25 @@ struct RejectCBitLoadOp final : OpConversionPattern { } }; -struct RejectCBitCompareOp final : OpConversionPattern { +struct RejectCBitReadOp final : OpConversionPattern { using OpConversionPattern::OpConversionPattern; LogicalResult - matchAndRewrite(cbit::CompareOp op, OpAdaptor /*adaptor*/, + matchAndRewrite(cbit::ReadOp op, OpAdaptor /*adaptor*/, ConversionPatternRewriter& /*rewriter*/) const override { return op.emitError( - "QIR Base Profile does not support classical-register comparisons"); + "QIR Base Profile does not support classical-register reads"); + } +}; + +struct RejectCBitWriteOp final : OpConversionPattern { + using OpConversionPattern::OpConversionPattern; + + LogicalResult + matchAndRewrite(cbit::WriteOp op, OpAdaptor /*adaptor*/, + ConversionPatternRewriter& /*rewriter*/) const override { + return op.emitError( + "QIR Base Profile does not support classical-register writes"); } }; @@ -346,7 +357,8 @@ static void populateQCToQIRBasePatterns(RewritePatternSet& patterns, patterns.add(typeConverter, ctx, &state); - patterns.add(typeConverter, ctx); + patterns.add( + typeConverter, ctx); } namespace { diff --git a/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp b/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp index bd8dc30da8..ec69d82448 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp @@ -433,6 +433,24 @@ Value getResultPtr(LoweringState& state, Operation* op, LogicalResult prepareClassicalResults(Operation* moduleOp, LoweringState& state) { bool hasInvalidMemory = false; + moduleOp->walk([&](Operation* operation) { + if (!isa(operation)) { + return; + } + const auto isRegister = [](Type type) { + return isa(type); + }; + if (llvm::any_of(operation->getOperandTypes(), isRegister) || + llvm::any_of(operation->getResultTypes(), isRegister)) { + operation->emitError( + "QIR conversion does not support CBit registers in calls; " + "read or write scalar values before the call"); + hasInvalidMemory = true; + } + }); + if (hasInvalidMemory) { + return failure(); + } SmallVector consumedStores; moduleOp->walk([&](func::FuncOp funcOp) { if (!mqt::isEntryPoint(funcOp)) { diff --git a/mlir/lib/Dialect/CBit/IR/CBitOps.cpp b/mlir/lib/Dialect/CBit/IR/CBitOps.cpp index f25a82b7c7..3f9c554651 100644 --- a/mlir/lib/Dialect/CBit/IR/CBitOps.cpp +++ b/mlir/lib/Dialect/CBit/IR/CBitOps.cpp @@ -14,6 +14,8 @@ #include "mlir/Dialect/CBit/IR/CBitDialect.h" #include +#include +#include #include // IWYU pragma: keep #include #include @@ -171,42 +173,56 @@ struct ForwardKnownLoad final : OpRewritePattern { } }; -struct FoldUntouchedZeroComparison final : OpRewritePattern { +struct DecomposeRead final : OpRewritePattern { using OpRewritePattern::OpRewritePattern; - LogicalResult matchAndRewrite(CompareOp compare, + LogicalResult matchAndRewrite(ReadOp read, PatternRewriter& rewriter) const override { - auto alloc = compare.getReg().getDefiningOp(); - if (!alloc || alloc.getInitialization() != Initialization::Zero || - alloc->getBlock() != compare->getBlock() || - !alloc->isBeforeInBlock(compare)) { - return failure(); - } - for (auto* user : compare.getReg().getUsers()) { - if (!isa(user)) { - auto* ancestor = compare->getBlock()->findAncestorOpInBlock(*user); - if (ancestor != nullptr && ancestor->isBeforeInBlock(compare)) { - return failure(); - } + auto loc = read.getLoc(); + const auto type = read.getResult().getType(); + const auto width = type.getWidth(); + Value result; + for (unsigned index = 0; index < width; ++index) { + auto indexValue = arith::ConstantIndexOp::create(rewriter, loc, index); + Value bit = LoadOp::create(rewriter, loc, rewriter.getI1Type(), + read.getReg(), indexValue); + if (width != 1) { + bit = arith::ExtUIOp::create(rewriter, loc, type, bit); + } + if (index == 0) { + result = bit; + } else { + auto shift = arith::ConstantIntOp::create(rewriter, loc, type, index); + bit = arith::ShLIOp::create(rewriter, loc, bit, shift); + result = arith::OrIOp::create(rewriter, loc, result, bit); } } - const auto zero = compare.getRhs().isZero(); - const auto result = [&] { - switch (compare.getPredicate()) { - case ComparisonPredicate::Equal: - case ComparisonPredicate::GreaterEqual: - return zero; - case ComparisonPredicate::NotEqual: - case ComparisonPredicate::Less: - return !zero; - case ComparisonPredicate::LessEqual: - return true; - case ComparisonPredicate::Greater: - return false; + rewriter.replaceOp(read, result); + return success(); + } +}; + +struct DecomposeWrite final : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(WriteOp write, + PatternRewriter& rewriter) const override { + auto loc = write.getLoc(); + const auto type = write.getValue().getType(); + const auto width = type.getWidth(); + for (unsigned index = 0; index < width; ++index) { + Value bit = write.getValue(); + if (index != 0) { + auto shift = arith::ConstantIntOp::create(rewriter, loc, type, index); + bit = arith::ShRUIOp::create(rewriter, loc, bit, shift); + } + if (width != 1) { + bit = arith::TruncIOp::create(rewriter, loc, rewriter.getI1Type(), bit); } - llvm_unreachable("unknown CBit comparison predicate"); - }(); - rewriter.replaceOpWithNewOp(compare, result, 1); + auto indexValue = arith::ConstantIndexOp::create(rewriter, loc, index); + StoreOp::create(rewriter, loc, bit, write.getReg(), indexValue); + } + rewriter.eraseOp(write); return success(); } }; @@ -217,56 +233,25 @@ LogicalResult LoadOp::verify() { return verifyIndex(getOperation(), getReg(), getIndex()); } -LogicalResult CompareOp::verify() { - if (std::cmp_not_equal(getRhs().getBitWidth(), +LogicalResult ReadOp::verify() { + if (std::cmp_not_equal(getResult().getType().getWidth(), getReg().getType().getWidth())) { - return emitOpError("expected integer width must match register width"); + return emitOpError("result width must match register width"); } return success(); } -Value mlir::cbit::buildComparison( - OpBuilder& builder, const Location location, - const ComparisonPredicate predicate, const llvm::APInt& rhs, - const llvm::function_ref loadBit) { - auto one = arith::ConstantIntOp::create(builder, location, 1, 1); - Value equal = one; - Value less; - if (predicate != ComparisonPredicate::Equal && - predicate != ComparisonPredicate::NotEqual) { - less = arith::ConstantIntOp::create(builder, location, 0, 1); - } - for (int64_t index = static_cast(rhs.getBitWidth()) - 1; index >= 0; - --index) { - auto bit = loadBit(index); - Value matches = bit; - if (!rhs[static_cast(index)]) { - matches = arith::XOrIOp::create(builder, location, bit, one); - } else if (less) { - auto lower = arith::XOrIOp::create(builder, location, bit, one); - auto firstDifference = - arith::AndIOp::create(builder, location, equal, lower); - less = arith::OrIOp::create(builder, location, less, firstDifference); - } - equal = arith::AndIOp::create(builder, location, equal, matches); - } - switch (predicate) { - case ComparisonPredicate::Equal: - return equal; - case ComparisonPredicate::NotEqual: - return arith::XOrIOp::create(builder, location, equal, one); - case ComparisonPredicate::Less: - return less; - case ComparisonPredicate::LessEqual: - return arith::OrIOp::create(builder, location, less, equal); - case ComparisonPredicate::Greater: { - auto lessOrEqual = arith::OrIOp::create(builder, location, less, equal); - return arith::XOrIOp::create(builder, location, lessOrEqual, one); - } - case ComparisonPredicate::GreaterEqual: - return arith::XOrIOp::create(builder, location, less, one); +LogicalResult WriteOp::verify() { + if (std::cmp_not_equal(getValue().getType().getWidth(), + getReg().getType().getWidth())) { + return emitOpError("value width must match register width"); } - llvm_unreachable("unknown CBit comparison predicate"); + return success(); +} + +void mlir::cbit::populateCBitDecompositionPatterns( + RewritePatternSet& patterns) { + patterns.add(patterns.getContext()); } void LoadOp::getCanonicalizationPatterns(RewritePatternSet& results, @@ -274,11 +259,6 @@ void LoadOp::getCanonicalizationPatterns(RewritePatternSet& results, results.add(context); } -void CompareOp::getCanonicalizationPatterns(RewritePatternSet& results, - MLIRContext* context) { - results.add(context); -} - LogicalResult StoreOp::verify() { return verifyIndex(getOperation(), getReg(), getIndex()); } diff --git a/mlir/lib/Dialect/QC/IR/Modifiers/ModifierUtils.cpp b/mlir/lib/Dialect/QC/IR/Modifiers/ModifierUtils.cpp index 7d96a687cb..67fda2b7de 100644 --- a/mlir/lib/Dialect/QC/IR/Modifiers/ModifierUtils.cpp +++ b/mlir/lib/Dialect/QC/IR/Modifiers/ModifierUtils.cpp @@ -10,7 +10,7 @@ #include "ModifierUtils.h" -#include "mlir/Dialect/CBit/IR/CBitOps.h" +#include "mlir/Dialect/CBit/IR/CBitDialect.h" #include "mlir/Dialect/MQT/Utils/Modifiers.h" #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/IR/QCOps.h" @@ -34,9 +34,10 @@ namespace mlir::qc::detail { LogicalResult verifyModifierBody(Operation* modifierOp, Block& body) { const auto hasNonUnitaryOperation = body.walk([](Operation* operation) { - return isa(operation) + return operation->getName().getDialectNamespace() == + cbit::CBitDialect::getDialectNamespace() || + isa(operation) ? WalkResult::interrupt() : WalkResult::advance(); }) diff --git a/mlir/lib/Dialect/QC/Translation/OpenQASMToQCEmitter.cpp b/mlir/lib/Dialect/QC/Translation/OpenQASMToQCEmitter.cpp index 966f8c59e6..9fb811dae5 100644 --- a/mlir/lib/Dialect/QC/Translation/OpenQASMToQCEmitter.cpp +++ b/mlir/lib/Dialect/QC/Translation/OpenQASMToQCEmitter.cpp @@ -16,6 +16,7 @@ #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/IR/QCOps.h" #include "mlir/Dialect/QC/Translation/StandardGate.h" +#include "mlir/Support/IntegerExpressions.h" #include "mlir/Target/OpenQASM/Frontend.h" #include "mlir/Target/OpenQASM/GateCatalog.h" @@ -341,15 +342,36 @@ class OpenQASMToQCEmitter { case frontend::ExpressionKind::GateParameter: case frontend::ExpressionKind::Variable: return remember(0); + case frontend::ExpressionKind::Condition: { + size_t cost = 0; + if (!chargeConditionEmission( + expression.condition, 1, cost, + program.conditions.at(expression.condition).location)) { + return remember(PROJECTED_EMISSION_LIMIT + 1); + } + return remember(cost); + } + case frontend::ExpressionKind::BitNot: + return remember(unary(2)); + case frontend::ExpressionKind::BitAnd: + case frontend::ExpressionKind::BitOr: + case frontend::ExpressionKind::BitXor: + return remember(binary(1)); + case frontend::ExpressionKind::ShiftLeft: + case frontend::ExpressionKind::ShiftRight: + return remember(binary(9)); case frontend::ExpressionKind::Cast: - return remember(unary(1)); + return remember(unary(2)); + case frontend::ExpressionKind::BitVectorCast: { + return remember( + add(bitVectorExpressionEmissionCost(expression.bitVector), 1)); + } case frontend::ExpressionKind::Negate: if (expression.type == frontend::ScalarType::Float || expression.type == frontend::ScalarType::Angle) { return remember(unary(1)); } - return remember( - unary(expression.type == frontend::ScalarType::Uint ? 2 : 10)); + return remember(unary(2)); case frontend::ExpressionKind::ArcCos: case frontend::ExpressionKind::ArcSin: case frontend::ExpressionKind::ArcTan: @@ -363,28 +385,15 @@ class OpenQASMToQCEmitter { case frontend::ExpressionKind::Sqrt: return remember(unary(2)); case frontend::ExpressionKind::PopCount: { - const auto& bitVector = - program.bitVectorExpressions.at(expression.bitVector); - return remember(add(bitVectorExpressionEmissionCost(expression.bitVector), - (4 * static_cast(bitVector.width)) + 2)); + return remember( + add(bitVectorExpressionEmissionCost(expression.bitVector), 2)); } case frontend::ExpressionKind::Add: case frontend::ExpressionKind::Subtract: case frontend::ExpressionKind::Multiply: - if (expression.type == frontend::ScalarType::Float || - expression.type == frontend::ScalarType::Angle) { - return remember(binary(3)); - } - return remember( - binary(expression.type == frontend::ScalarType::Uint ? 2 : 11)); case frontend::ExpressionKind::Divide: case frontend::ExpressionKind::Modulo: - if (expression.type == frontend::ScalarType::Float || - expression.type == frontend::ScalarType::Angle) { - return remember(binary(3)); - } - return remember( - binary(expression.type == frontend::ScalarType::Uint ? 5 : 13)); + return remember(binary(1)); case frontend::ExpressionKind::Power: if (expression.type == frontend::ScalarType::Float) { return remember(binary(3)); @@ -450,33 +459,45 @@ class OpenQASMToQCEmitter { return *bitVectorExpressionEmissionCosts[id]; } const auto& expression = program.bitVectorExpressions.at(id); + const auto add = [](const size_t lhs, const size_t rhs) { + return lhs > PROJECTED_EMISSION_LIMIT || rhs > PROJECTED_EMISSION_LIMIT || + lhs > PROJECTED_EMISSION_LIMIT - rhs + ? PROJECTED_EMISSION_LIMIT + 1 + : lhs + rhs; + }; const auto remember = [&](const size_t cost) { bitVectorExpressionEmissionCosts[id] = cost; return cost; }; - if (expression.kind == frontend::BitVectorExpressionKind::Register) { - const auto width = static_cast(expression.width); - return remember(width > PROJECTED_EMISSION_LIMIT / 2 - ? PROJECTED_EMISSION_LIMIT + 1 - : 2 * width); + switch (expression.kind) { + case frontend::BitVectorExpressionKind::ScalarCast: + return remember(expressionEmissionCost(expression.scalar)); + case frontend::BitVectorExpressionKind::Constant: + case frontend::BitVectorExpressionKind::Register: + return remember(1); + case frontend::BitVectorExpressionKind::Not: + return remember( + add(bitVectorExpressionEmissionCost(expression.operand), 2)); + case frontend::BitVectorExpressionKind::And: + case frontend::BitVectorExpressionKind::Or: + case frontend::BitVectorExpressionKind::Xor: + return remember( + add(add(bitVectorExpressionEmissionCost(expression.operand), + bitVectorExpressionEmissionCost(expression.rhs)), + 1)); + case frontend::BitVectorExpressionKind::ShiftLeft: + case frontend::BitVectorExpressionKind::ShiftRight: + return remember( + add(add(bitVectorExpressionEmissionCost(expression.operand), + expressionEmissionCost(expression.distance)), + 9)); + case frontend::BitVectorExpressionKind::RotateLeft: + case frontend::BitVectorExpressionKind::RotateRight: + break; } const auto operand = bitVectorExpressionEmissionCost(expression.operand); - if (program.expressions.at(expression.distance).kind == - frontend::ExpressionKind::Constant) { - return remember(operand > PROJECTED_EMISSION_LIMIT - 2 - ? PROJECTED_EMISSION_LIMIT + 1 - : operand + 2); - } - const auto width = static_cast(expression.width); - const auto local = (8 * width) + 8; const auto distance = expressionEmissionCost(expression.distance); - if (operand > PROJECTED_EMISSION_LIMIT || - distance > PROJECTED_EMISSION_LIMIT || - operand > PROJECTED_EMISSION_LIMIT - distance || - operand + distance > PROJECTED_EMISSION_LIMIT - local) { - return remember(PROJECTED_EMISSION_LIMIT + 1); - } - return remember(operand + distance + local); + return remember(add(add(operand, distance), 6)); } [[nodiscard]] bool @@ -573,7 +594,16 @@ class OpenQASMToQCEmitter { projectedEmission, source); } if (condition.kind == frontend::ConditionKind::RegisterComparison) { - return chargeScaledEmission(1, multiplicity, projectedEmission, source); + return chargeScaledEmission(3, multiplicity, projectedEmission, source); + } + if (condition.kind == frontend::ConditionKind::BitVectorComparison) { + return chargeScaledEmission(bitVectorExpressionEmissionCost( + condition.bitVectorComparisonLhs), + multiplicity, projectedEmission, source) && + chargeScaledEmission(bitVectorExpressionEmissionCost( + condition.bitVectorComparisonRhs), + multiplicity, projectedEmission, source) && + chargeScaledEmission(1, multiplicity, projectedEmission, source); } if (condition.kind == frontend::ConditionKind::Comparison) { return chargeExpressionEmission(condition.comparisonLhs, multiplicity, @@ -749,10 +779,8 @@ class OpenQASMToQCEmitter { if (!chargeScaledEmission( bitVectorExpressionEmissionCost(assignment->value), multiplicity, projectedEmission, statement.location) || - !chargeScaledEmission( - 2 * static_cast( - program.registers.at(assignment->target).width), - multiplicity, projectedEmission, statement.location)) { + !chargeScaledEmission(1, multiplicity, projectedEmission, + statement.location)) { return false; } } else if (const auto* declaration = @@ -927,25 +955,6 @@ class OpenQASMToQCEmitter { return preflightStatements(program.body, projectedEmission); } - [[nodiscard]] static Value checkedSignedResult(OpBuilder& opBuilder, - Location loc, Value wide, - const StringRef message) { - auto i128 = opBuilder.getIntegerType(128); - auto minimum = arith::ConstantIntOp::create( - opBuilder, loc, i128, std::numeric_limits::min()); - auto maximum = arith::ConstantIntOp::create( - opBuilder, loc, i128, std::numeric_limits::max()); - auto aboveMinimum = arith::CmpIOp::create( - opBuilder, loc, arith::CmpIPredicate::sge, wide, minimum); - auto belowMaximum = arith::CmpIOp::create( - opBuilder, loc, arith::CmpIPredicate::sle, wide, maximum); - auto fits = - arith::AndIOp::create(opBuilder, loc, aboveMinimum, belowMaximum); - cf::AssertOp::create(opBuilder, loc, fits, message); - return arith::TruncIOp::create(opBuilder, loc, opBuilder.getI64Type(), - wide); - } - [[nodiscard]] static Value conditionalIntegerMultiply(OpBuilder& opBuilder, Location loc, Value condition, @@ -1025,6 +1034,9 @@ class OpenQASMToQCEmitter { [[nodiscard]] static Value emitExactlyRepresentableIntegerAsF64(OpBuilder& opBuilder, Location loc, Value integer, const bool isUnsigned) { + const auto type = + isUnsigned ? frontend::ScalarType::Uint : frontend::ScalarType::Int; + integer = emitScalarCast(opBuilder, loc, integer, type, type); auto zero = arith::ConstantIntOp::create(opBuilder, loc, 0, 64); Value magnitude = integer; if (!isUnsigned) { @@ -1074,88 +1086,57 @@ class OpenQASMToQCEmitter { .getResult(); } - [[nodiscard]] static Value packBits(OpBuilder& opBuilder, - ArrayRef bits) { - assert(!bits.empty()); - const auto width = static_cast(bits.size()); - auto packedType = opBuilder.getIntegerType(width); - auto loc = UnknownLoc::get(opBuilder.getContext()); - Value packed = - width == 1 - ? bits.front() - : arith::ExtUIOp::create(opBuilder, loc, packedType, bits.front()); - for (unsigned bit = 1; bit < width; ++bit) { - auto extended = - arith::ExtUIOp::create(opBuilder, loc, packedType, bits[bit]); - auto shift = arith::ConstantIntOp::create(opBuilder, loc, bit, width); - auto shifted = arith::ShLIOp::create(opBuilder, loc, extended, shift); - packed = arith::OrIOp::create(opBuilder, loc, packed, shifted); - } - return packed; - } - - [[nodiscard]] static SmallVector - unpackBits(OpBuilder& opBuilder, Value packed, const uint64_t width) { - SmallVector bits; - bits.reserve(width); - if (width == 1) { - bits.push_back(packed); - return bits; - } - auto loc = UnknownLoc::get(opBuilder.getContext()); - for (uint64_t bit = 0; bit < width; ++bit) { - Value selected = packed; - if (bit != 0) { - auto shift = arith::ConstantIntOp::create(opBuilder, loc, - static_cast(bit), - static_cast(width)); - selected = arith::ShRUIOp::create(opBuilder, loc, packed, shift); - } - bits.push_back(arith::TruncIOp::create(opBuilder, loc, - opBuilder.getI1Type(), selected)); - } - return bits; - } - - struct EmittedBitVector { - uint64_t width = 0; - SmallVector bits; - Value packed; - }; - - static Value ensurePacked(OpBuilder& opBuilder, EmittedBitVector& value) { - if (!value.packed) { - value.packed = packBits(opBuilder, value.bits); - } - return value.packed; - } - - static ArrayRef ensureBits(OpBuilder& opBuilder, - EmittedBitVector& value) { - if (value.bits.empty()) { - value.bits = unpackBits(opBuilder, value.packed, value.width); - } - return value.bits; - } - - [[nodiscard]] EmittedBitVector + [[nodiscard]] Value emitBitVectorExpression(OpBuilder& opBuilder, const frontend::BitVectorExpressionId id) { const auto& expression = program.bitVectorExpressions.at(id); auto loc = UnknownLoc::get(opBuilder.getContext()); - if (expression.kind == frontend::BitVectorExpressionKind::Register) { - SmallVector bits; - bits.reserve(expression.width); + const auto type = + opBuilder.getIntegerType(static_cast(expression.width)); + switch (expression.kind) { + case frontend::BitVectorExpressionKind::ScalarCast: + return emitExpression(opBuilder, expression.scalar, {}); + case frontend::BitVectorExpressionKind::Constant: + return arith::ConstantOp::create( + opBuilder, loc, IntegerAttr::get(type, expression.constant)); + case frontend::BitVectorExpressionKind::Register: { auto reg = classicalRegisters.at(expression.reg); assert(reg && "semantic analysis must declare bit registers before use"); - for (uint64_t bit = 0; bit < expression.width; ++bit) { - auto index = arith::ConstantIndexOp::create(opBuilder, loc, - static_cast(bit)); - bits.push_back(cbit::LoadOp::create(opBuilder, loc, - opBuilder.getI1Type(), reg, index)); + return cbit::ReadOp::create(opBuilder, loc, type, reg); + } + case frontend::BitVectorExpressionKind::Not: { + auto operand = emitBitVectorExpression(opBuilder, expression.operand); + auto ones = arith::ConstantOp::create( + opBuilder, loc, + IntegerAttr::get(type, APInt::getAllOnes(expression.width))); + return arith::XOrIOp::create(opBuilder, loc, operand, ones); + } + case frontend::BitVectorExpressionKind::And: + case frontend::BitVectorExpressionKind::Or: + case frontend::BitVectorExpressionKind::Xor: { + auto lhs = emitBitVectorExpression(opBuilder, expression.operand); + auto rhs = emitBitVectorExpression(opBuilder, expression.rhs); + if (expression.kind == frontend::BitVectorExpressionKind::And) { + return arith::AndIOp::create(opBuilder, loc, lhs, rhs); + } + if (expression.kind == frontend::BitVectorExpressionKind::Or) { + return arith::OrIOp::create(opBuilder, loc, lhs, rhs); } - return {.width = expression.width, .bits = std::move(bits)}; + return arith::XOrIOp::create(opBuilder, loc, lhs, rhs); + } + case frontend::BitVectorExpressionKind::ShiftLeft: + case frontend::BitVectorExpressionKind::ShiftRight: { + auto operand = emitBitVectorExpression(opBuilder, expression.operand); + auto distance = emitExpression(opBuilder, expression.distance, {}); + return mqt::buildZeroFillingShift( + opBuilder, loc, operand, distance, + expression.kind == frontend::BitVectorExpressionKind::ShiftLeft); } + case frontend::BitVectorExpressionKind::RotateLeft: + case frontend::BitVectorExpressionKind::RotateRight: + break; + } + auto operand = emitBitVectorExpression(opBuilder, expression.operand); const auto& distanceExpression = program.expressions.at(expression.distance); @@ -1166,37 +1147,22 @@ class OpenQASMToQCEmitter { if (normalized < 0) { normalized += width; } - if (operand.bits.empty()) { - if (normalized == 0) { - return operand; - } - auto shift = arith::ConstantIntOp::create( - opBuilder, loc, normalized, - static_cast(expression.width)); - Value rotated = - expression.kind == frontend::BitVectorExpressionKind::RotateLeft - ? LLVM::FshlOp::create(opBuilder, loc, operand.packed, - operand.packed, shift) - .getResult() - : LLVM::FshrOp::create(opBuilder, loc, operand.packed, - operand.packed, shift) - .getResult(); - return {.width = expression.width, .packed = rotated}; - } - const auto bits = ensureBits(opBuilder, operand); - SmallVector rotated(expression.width); - for (uint64_t bit = 0; bit < expression.width; ++bit) { - const auto source = - expression.kind == frontend::BitVectorExpressionKind::RotateLeft - ? (bit + expression.width - static_cast(normalized)) % - expression.width - : (bit + static_cast(normalized)) % expression.width; - rotated[bit] = bits[source]; - } - return {.width = expression.width, .bits = std::move(rotated)}; + if (normalized == 0) { + return operand; + } + auto shift = arith::ConstantIntOp::create( + opBuilder, loc, normalized, static_cast(expression.width)); + return expression.kind == frontend::BitVectorExpressionKind::RotateLeft + ? LLVM::FshlOp::create(opBuilder, loc, operand, operand, shift) + .getResult() + : LLVM::FshrOp::create(opBuilder, loc, operand, operand, shift) + .getResult(); } auto distance = emitExpression(opBuilder, expression.distance, {}); + distance = + emitScalarCast(opBuilder, loc, distance, frontend::ScalarType::Int, + frontend::ScalarType::Int); auto widthConstant = arith::ConstantIntOp::create( opBuilder, loc, static_cast(expression.width), 64); auto remainder = @@ -1205,9 +1171,7 @@ class OpenQASMToQCEmitter { arith::AddIOp::create(opBuilder, loc, remainder, widthConstant); auto normalized = arith::RemSIOp::create(opBuilder, loc, positive, widthConstant); - auto packed = ensurePacked(opBuilder, operand); - auto packedType = - opBuilder.getIntegerType(static_cast(expression.width)); + auto packedType = type; Value shift = normalized; if (expression.width < 64) { shift = arith::TruncIOp::create(opBuilder, loc, packedType, normalized); @@ -1216,11 +1180,11 @@ class OpenQASMToQCEmitter { } Value rotated = expression.kind == frontend::BitVectorExpressionKind::RotateLeft - ? LLVM::FshlOp::create(opBuilder, loc, packed, packed, shift) + ? LLVM::FshlOp::create(opBuilder, loc, operand, operand, shift) .getResult() - : LLVM::FshrOp::create(opBuilder, loc, packed, packed, shift) + : LLVM::FshrOp::create(opBuilder, loc, operand, operand, shift) .getResult(); - return {.width = expression.width, .packed = rotated}; + return rotated; } Value emitExpression(OpBuilder& opBuilder, const frontend::ExpressionId id, @@ -1238,13 +1202,19 @@ class OpenQASMToQCEmitter { static_cast(std::get(expression.constant)), 1); case frontend::ScalarType::Int: return arith::ConstantIntOp::create( - opBuilder, loc, std::get(expression.constant), 64); + opBuilder, loc, std::get(expression.constant), + expression.integerWidth != 0 ? expression.integerWidth : 64); case frontend::ScalarType::Uint: return arith::ConstantOp::create( opBuilder, loc, - IntegerAttr::get(opBuilder.getI64Type(), - APInt(64, std::get(expression.constant), - /*isSigned=*/false))); + IntegerAttr::get( + opBuilder.getIntegerType(expression.integerWidth != 0 + ? expression.integerWidth + : 64), + APInt(expression.integerWidth != 0 ? expression.integerWidth + : 64, + std::get(expression.constant), + /*isSigned=*/false))); case frontend::ScalarType::Float: case frontend::ScalarType::Angle: return arith::ConstantFloatOp::create( @@ -1260,23 +1230,47 @@ class OpenQASMToQCEmitter { auto operand = emitExpression(opBuilder, expression.lhs, gateParameters); return emitScalarCast(opBuilder, loc, operand, program.expressions.at(expression.lhs).type, - expression.type); + expression.type, expression.integerWidth); + } + case frontend::ExpressionKind::BitVectorCast: { + return emitBitVectorExpression(opBuilder, expression.bitVector); + } + case frontend::ExpressionKind::Condition: + return emitCondition(expression.condition, gateParameters, {}); + case frontend::ExpressionKind::BitNot: { + auto operand = emitExpression(opBuilder, expression.lhs, gateParameters); + auto ones = + arith::ConstantIntOp::create(opBuilder, loc, operand.getType(), -1); + return arith::XOrIOp::create(opBuilder, loc, operand, ones); + } + case frontend::ExpressionKind::BitAnd: + case frontend::ExpressionKind::BitOr: + case frontend::ExpressionKind::BitXor: + case frontend::ExpressionKind::ShiftLeft: + case frontend::ExpressionKind::ShiftRight: { + auto lhs = emitExpression(opBuilder, expression.lhs, gateParameters); + auto rhs = emitExpression(opBuilder, expression.rhs, gateParameters); + switch (expression.kind) { + case frontend::ExpressionKind::BitAnd: + return arith::AndIOp::create(opBuilder, loc, lhs, rhs); + case frontend::ExpressionKind::BitOr: + return arith::OrIOp::create(opBuilder, loc, lhs, rhs); + case frontend::ExpressionKind::BitXor: + return arith::XOrIOp::create(opBuilder, loc, lhs, rhs); + default: + return mqt::buildZeroFillingShift( + opBuilder, loc, lhs, rhs, + expression.kind == frontend::ExpressionKind::ShiftLeft); + } } case frontend::ExpressionKind::Negate: { auto operand = emitExpression(opBuilder, expression.lhs, gateParameters); if (isa(operand.getType())) { return arith::NegFOp::create(opBuilder, loc, operand); } - if (expression.type == frontend::ScalarType::Uint) { - auto zero = arith::ConstantIntOp::create(opBuilder, loc, 0, 64); - return arith::SubIOp::create(opBuilder, loc, zero, operand); - } - auto i128 = opBuilder.getIntegerType(128); - auto zero = arith::ConstantIntOp::create(opBuilder, loc, 0, 128); - auto operandWide = arith::ExtSIOp::create(opBuilder, loc, i128, operand); - auto negated = arith::SubIOp::create(opBuilder, loc, zero, operandWide); - return checkedSignedResult(opBuilder, loc, negated, - "integer negation overflows i64"); + auto zero = + arith::ConstantIntOp::create(opBuilder, loc, operand.getType(), 0); + return arith::SubIOp::create(opBuilder, loc, zero, operand); } case frontend::ExpressionKind::ArcCos: case frontend::ExpressionKind::ArcSin: @@ -1322,8 +1316,7 @@ class OpenQASMToQCEmitter { case frontend::ExpressionKind::PopCount: { const auto& bitVector = program.bitVectorExpressions.at(expression.bitVector); - auto value = emitBitVectorExpression(opBuilder, expression.bitVector); - auto packed = ensurePacked(opBuilder, value); + auto packed = emitBitVectorExpression(opBuilder, expression.bitVector); auto count = math::CtPopOp::create(opBuilder, loc, packed); if (bitVector.width < 64) { return arith::ExtUIOp::create(opBuilder, loc, opBuilder.getI64Type(), @@ -1346,33 +1339,8 @@ class OpenQASMToQCEmitter { if (expression.type != frontend::ScalarType::Float && expression.type != frontend::ScalarType::Angle) { const bool isUnsigned = expression.type == frontend::ScalarType::Uint; - auto zero = arith::ConstantIntOp::create(opBuilder, loc, 0, 64); if (expression.kind == frontend::ExpressionKind::Divide || expression.kind == frontend::ExpressionKind::Modulo) { - auto nonzero = arith::CmpIOp::create( - opBuilder, loc, arith::CmpIPredicate::ne, rhs, zero); - cf::AssertOp::create(opBuilder, loc, nonzero, - expression.kind == - frontend::ExpressionKind::Divide - ? "division by zero" - : "modulo by zero"); - if (!isUnsigned) { - auto minimum = arith::ConstantIntOp::create( - opBuilder, loc, std::numeric_limits::min(), 64); - auto minusOne = - arith::ConstantIntOp::create(opBuilder, loc, -1, 64); - auto lhsIsMinimum = arith::CmpIOp::create( - opBuilder, loc, arith::CmpIPredicate::eq, lhs, minimum); - auto rhsIsMinusOne = arith::CmpIOp::create( - opBuilder, loc, arith::CmpIPredicate::eq, rhs, minusOne); - auto overflows = arith::AndIOp::create(opBuilder, loc, lhsIsMinimum, - rhsIsMinusOne); - auto valid = arith::XOrIOp::create( - opBuilder, loc, overflows, - arith::ConstantIntOp::create(opBuilder, loc, 1, 1)); - cf::AssertOp::create(opBuilder, loc, valid, - "integer division overflows i64"); - } if (expression.kind == frontend::ExpressionKind::Divide) { return isUnsigned ? arith::DivUIOp::create(opBuilder, loc, lhs, rhs) .getResult() @@ -1389,37 +1357,18 @@ class OpenQASMToQCEmitter { program.expressions.at(expression.rhs).type == frontend::ScalarType::Uint); } - if (isUnsigned) { - switch (expression.kind) { - case frontend::ExpressionKind::Add: - return arith::AddIOp::create(opBuilder, loc, lhs, rhs); - case frontend::ExpressionKind::Subtract: - return arith::SubIOp::create(opBuilder, loc, lhs, rhs); - case frontend::ExpressionKind::Multiply: - return arith::MulIOp::create(opBuilder, loc, lhs, rhs); - default: - llvm_unreachable("not an unsigned integer binary expression"); - } - } - auto i128 = opBuilder.getIntegerType(128); - auto lhsWide = arith::ExtSIOp::create(opBuilder, loc, i128, lhs); - auto rhsWide = arith::ExtSIOp::create(opBuilder, loc, i128, rhs); - Value result; + /// Like explicit integer casts, runtime integer arithmetic wraps at its + /// width. switch (expression.kind) { case frontend::ExpressionKind::Add: - result = arith::AddIOp::create(opBuilder, loc, lhsWide, rhsWide); - break; + return arith::AddIOp::create(opBuilder, loc, lhs, rhs); case frontend::ExpressionKind::Subtract: - result = arith::SubIOp::create(opBuilder, loc, lhsWide, rhsWide); - break; + return arith::SubIOp::create(opBuilder, loc, lhs, rhs); case frontend::ExpressionKind::Multiply: - result = arith::MulIOp::create(opBuilder, loc, lhsWide, rhsWide); - break; + return arith::MulIOp::create(opBuilder, loc, lhs, rhs); default: - llvm_unreachable("not a signed integer binary expression"); + llvm_unreachable("not an integer binary expression"); } - return checkedSignedResult(opBuilder, loc, result, - "integer arithmetic overflows i64"); } switch (expression.kind) { case frontend::ExpressionKind::Add: @@ -1446,6 +1395,8 @@ class OpenQASMToQCEmitter { const int64_t width, const llvm::StringRef message) { auto index = emitExpression(builder, expression, {}); + const auto type = program.expressions.at(expression).type; + index = emitScalarCast(builder, builder.getLoc(), index, type, type); auto zero = builder.intConstant(0); auto upper = builder.intConstant(width); Value inBounds; @@ -1817,7 +1768,26 @@ class OpenQASMToQCEmitter { [[nodiscard]] static Value emitScalarCast(OpBuilder& opBuilder, const Location loc, Value value, const frontend::ScalarType source, - const frontend::ScalarType target) { + const frontend::ScalarType target, + const unsigned integerWidth = 0) { + if (isa(value.getType()) && + (target == frontend::ScalarType::Int || + target == frontend::ScalarType::Uint)) { + auto resultType = + opBuilder.getIntegerType(integerWidth != 0 ? integerWidth : 64); + auto width = cast(value.getType()).getWidth(); + if (width == resultType.getWidth()) { + return value; + } + if (width > resultType.getWidth()) { + return arith::TruncIOp::create(opBuilder, loc, resultType, value); + } + return source == frontend::ScalarType::Int + ? arith::ExtSIOp::create(opBuilder, loc, resultType, value) + .getResult() + : arith::ExtUIOp::create(opBuilder, loc, resultType, value) + .getResult(); + } if (source == target || (source == frontend::ScalarType::Int && target == frontend::ScalarType::Uint) || @@ -1846,13 +1816,17 @@ class OpenQASMToQCEmitter { if ((source == frontend::ScalarType::Float || source == frontend::ScalarType::Angle) && target == frontend::ScalarType::Uint) { - return arith::FPToUIOp::create(opBuilder, loc, opBuilder.getI64Type(), - value); + return arith::FPToUIOp::create( + opBuilder, loc, + opBuilder.getIntegerType(integerWidth != 0 ? integerWidth : 64), + value); } if (source == frontend::ScalarType::Float || source == frontend::ScalarType::Angle) { - return arith::FPToSIOp::create(opBuilder, loc, opBuilder.getI64Type(), - value); + return arith::FPToSIOp::create( + opBuilder, loc, + opBuilder.getIntegerType(integerWidth != 0 ? integerWidth : 64), + value); } llvm_unreachable("unsupported standard scalar conversion"); } @@ -1874,23 +1848,24 @@ class OpenQASMToQCEmitter { return builder.loadClassicalBit(reg, registerIndex.getResult()); } - [[nodiscard]] static cbit::ComparisonPredicate - registerPredicate(const frontend::ComparisonKind comparison) { + [[nodiscard]] static arith::CmpIPredicate + integerPredicate(const frontend::ComparisonKind comparison, + const bool isUnsigned) { switch (comparison) { case frontend::ComparisonKind::Equal: - return cbit::ComparisonPredicate::Equal; + return arith::CmpIPredicate::eq; case frontend::ComparisonKind::NotEqual: - return cbit::ComparisonPredicate::NotEqual; + return arith::CmpIPredicate::ne; case frontend::ComparisonKind::Less: - return cbit::ComparisonPredicate::Less; + return isUnsigned ? arith::CmpIPredicate::ult : arith::CmpIPredicate::slt; case frontend::ComparisonKind::LessEqual: - return cbit::ComparisonPredicate::LessEqual; + return isUnsigned ? arith::CmpIPredicate::ule : arith::CmpIPredicate::sle; case frontend::ComparisonKind::Greater: - return cbit::ComparisonPredicate::Greater; + return isUnsigned ? arith::CmpIPredicate::ugt : arith::CmpIPredicate::sgt; case frontend::ComparisonKind::GreaterEqual: - return cbit::ComparisonPredicate::GreaterEqual; + return isUnsigned ? arith::CmpIPredicate::uge : arith::CmpIPredicate::sge; } - llvm_unreachable("unknown register comparison"); + llvm_unreachable("unknown integer comparison"); } [[nodiscard]] Value @@ -1924,28 +1899,8 @@ class OpenQASMToQCEmitter { return arith::CmpFOp::create(builder, predicate, lhs, rhs); } - const bool isUnsigned = lhsType == frontend::ScalarType::Uint; - const auto predicate = [&] { - switch (condition.comparison) { - case frontend::ComparisonKind::Equal: - return arith::CmpIPredicate::eq; - case frontend::ComparisonKind::NotEqual: - return arith::CmpIPredicate::ne; - case frontend::ComparisonKind::Less: - return isUnsigned ? arith::CmpIPredicate::ult - : arith::CmpIPredicate::slt; - case frontend::ComparisonKind::LessEqual: - return isUnsigned ? arith::CmpIPredicate::ule - : arith::CmpIPredicate::sle; - case frontend::ComparisonKind::Greater: - return isUnsigned ? arith::CmpIPredicate::ugt - : arith::CmpIPredicate::sgt; - case frontend::ComparisonKind::GreaterEqual: - return isUnsigned ? arith::CmpIPredicate::uge - : arith::CmpIPredicate::sge; - } - llvm_unreachable("unknown integer comparison"); - }(); + const auto predicate = integerPredicate( + condition.comparison, lhsType == frontend::ScalarType::Uint); return arith::CmpIOp::create(builder, predicate, lhs, rhs); } @@ -1970,9 +1925,20 @@ class OpenQASMToQCEmitter { auto rhs = builder.getIntegerAttr( builder.getIntegerType(condition.expected.getBitWidth()), condition.expected); - return cbit::CompareOp::create(builder, builder.getI1Type(), - registerPredicate(condition.comparison), - reg, rhs); + const auto predicate = + integerPredicate(condition.comparison, /*isUnsigned=*/true); + auto value = cbit::ReadOp::create(builder, rhs.getType(), reg); + auto constant = arith::ConstantOp::create(builder, rhs); + return arith::CmpIOp::create(builder, predicate, value, constant); + } + case frontend::ConditionKind::BitVectorComparison: { + auto lhs = + emitBitVectorExpression(builder, condition.bitVectorComparisonLhs); + auto rhs = + emitBitVectorExpression(builder, condition.bitVectorComparisonRhs); + return arith::CmpIOp::create( + builder, integerPredicate(condition.comparison, /*isUnsigned=*/true), + lhs, rhs); } case frontend::ConditionKind::Not: return arith::XOrIOp::create( @@ -2155,13 +2121,14 @@ class OpenQASMToQCEmitter { statement.data); } - [[nodiscard]] Type scalarType(const frontend::ScalarType type) { + [[nodiscard]] Type scalarType(const frontend::ScalarType type, + const unsigned integerWidth = 0) { switch (type) { case frontend::ScalarType::Bool: return builder.getI1Type(); case frontend::ScalarType::Int: case frontend::ScalarType::Uint: - return builder.getI64Type(); + return builder.getIntegerType(integerWidth != 0 ? integerWidth : 64); case frontend::ScalarType::Float: case frontend::ScalarType::Angle: return builder.getF64Type(); @@ -2173,7 +2140,11 @@ class OpenQASMToQCEmitter { emitScalarDeclaration(const frontend::ScalarDeclarationStatement& statement, ValueRange gateQubits) { const auto type = program.scalars.at(statement.scalar).type; - Value value = ub::PoisonOp::create(builder, scalarType(type)).getResult(); + Value value = + ub::PoisonOp::create( + builder, + scalarType(type, program.scalars.at(statement.scalar).integerWidth)) + .getResult(); if (statement.initializer) { value = emitExpression(builder, *statement.initializer, {}); } else if (statement.conditionInitializer) { @@ -2246,12 +2217,9 @@ class OpenQASMToQCEmitter { void emitBitVectorAssignment( const frontend::BitVectorAssignmentStatement& assignment) { auto value = emitBitVectorExpression(builder, assignment.value); - const auto bits = ensureBits(builder, value); auto reg = classicalRegisters[assignment.target]; assert(reg && "semantic analysis must declare bit registers before use"); - for (auto [index, bit] : llvm::enumerate(bits)) { - builder.storeClassicalBit(bit, reg, static_cast(index)); - } + cbit::WriteOp::create(builder, builder.getUnknownLoc(), value, reg); } void emitMeasurement(const frontend::MeasurementStatement& measurement, @@ -2561,6 +2529,8 @@ class OpenQASMToQCEmitter { const auto savedScalars = scalarValues; auto control = emitExpression(builder, switchStatement.control, {}); + const auto type = program.expressions.at(switchStatement.control).type; + control = emitScalarCast(builder, builder.getLoc(), control, type, type); auto selector = arith::IndexCastOp::create(builder, builder.getIndexType(), control); auto switchOp = scf::IndexSwitchOp::create( diff --git a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp index 01808553f0..5133925ef2 100644 --- a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp +++ b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp @@ -17,6 +17,7 @@ #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/IR/QCInterfaces.h" #include "mlir/Dialect/QC/IR/QCOps.h" +#include "mlir/Support/IntegerExpressions.h" #include "mlir/Target/OpenQASM/GateCatalog.h" #include @@ -146,6 +147,7 @@ class OpenQASMEmitter { raw_indented_ostream bodyOutput(bodyStream); output = &bodyOutput; + indexClassicalWrites(function.getBody().front()); if (failed(emitDeclarations()) || failed(emitBlock(function.getBody().front()))) { return failure(); @@ -176,6 +178,9 @@ class OpenQASMEmitter { llvm::StringSet<> usedNames; llvm::StringSet<> fixedHelpers; SmallVector compositeHelpers; + DenseMap operationPositions; + DenseMap>> classicalWrites; + Operation* expressionConsumer = nullptr; size_t nextQubit = 0; size_t nextBit = 0; size_t nextScalar = 0; @@ -225,79 +230,6 @@ class OpenQASMEmitter { return uniqueName("q", nextQubit); } - [[nodiscard]] static bool storesToRegister(Operation& operation, Value reg) { - return operation - .walk([&](cbit::StoreOp store) { - return store.getReg() == reg ? WalkResult::interrupt() - : WalkResult::advance(); - }) - .wasInterrupted(); - } - - [[nodiscard]] static bool - hasInterveningRegisterWrite(cbit::CompareOp comparison, Operation& consumer) { - Operation* anchor = &consumer; - Block* block = consumer.getBlock(); - Block* comparisonBlock = comparison->getBlock(); - while (block != comparisonBlock) { - for (Operation& operation : *block) { - if (&operation == anchor) { - break; - } - if (storesToRegister(operation, comparison.getReg())) { - return true; - } - } - Operation* parent = block->getParentOp(); - if (parent == nullptr) { - return true; - } - if (isa(parent) && - storesToRegister(*parent, comparison.getReg())) { - return true; - } - anchor = parent; - block = parent->getBlock(); - } - - for (Operation* operation = comparison->getNextNode(); operation != anchor; - operation = operation->getNextNode()) { - if (operation == nullptr || - storesToRegister(*operation, comparison.getReg())) { - return true; - } - } - return false; - } - - [[nodiscard]] LogicalResult validateRegisterComparisonSnapshots() { - const auto walkResult = function.walk([&](cbit::CompareOp comparison) { - SmallVector consumers; - llvm::append_range(consumers, comparison.getResult().getUsers()); - DenseSet visited; - while (!consumers.empty()) { - Operation* consumer = consumers.pop_back_val(); - if (!visited.insert(consumer).second) { - continue; - } - if (hasInterveningRegisterWrite(comparison, *consumer)) { - std::ignore = - fail(comparison, - "register comparison crosses an intervening register write"); - return WalkResult::interrupt(); - } - if (!isInlineExpressionOperation(*consumer)) { - continue; - } - for (Value result : consumer->getResults()) { - llvm::append_range(consumers, result.getUsers()); - } - } - return WalkResult::advance(); - }); - return walkResult.wasInterrupted() ? failure() : success(); - } - [[nodiscard]] LogicalResult preflight() { SmallVector functions(moduleOp.getOps()); if (functions.size() != 1) { @@ -335,7 +267,7 @@ class OpenQASMEmitter { "scope"); } } - return validateRegisterComparisonSnapshots(); + return success(); } [[nodiscard]] LogicalResult collectProgramShape() { @@ -451,6 +383,10 @@ class OpenQASMEmitter { if (type.isInteger(64) || type.isIndex()) { return "int"; } + if (auto integer = dyn_cast(type); + integer && integer.getWidth() <= 64) { + return (Twine("uint[") + Twine(integer.getWidth()) + "]").str(); + } if (type.isF64()) { return "float"; } @@ -497,16 +433,16 @@ class OpenQASMEmitter { } [[nodiscard]] LogicalResult emitOperation(Operation& operation) { - if (isa(&operation)) { - return fail(&operation, "arith.select is not supported"); - } - if (isa(&operation)) { + auto* previousConsumer = std::exchange(expressionConsumer, &operation); + auto consumerGuard = + llvm::make_scope_exit([&] { expressionConsumer = previousConsumer; }); + if (isa(&operation)) { return success(); } if (isInlineExpressionOperation(operation)) { - return validateInlineExpressionOperation(operation); + return success(); } if (isa(&operation) || (isa(&operation) && @@ -520,6 +456,26 @@ class OpenQASMEmitter { if (auto store = dyn_cast(&operation)) { return emitStore(store); } + if (auto write = dyn_cast(&operation)) { + const auto resource = resources.find(write.getReg()); + if (resource == resources.end() || + resource->second.kind != ResourceKind::Bit) { + return fail(write, "register write refers to unsupported storage"); + } + auto value = + emitExpression(write.getValue(), ExpressionContext::BitVector); + if (succeeded(value) && resource->second.width <= 64) { + value = (Twine("bit[") + Twine(resource->second.width) + "](uint[" + + Twine(resource->second.width) + "](" + *value + "))") + .str(); + } + if (failed(value)) { + return failure(); + } + *output << resource->second.name; + *output << " = " << *value << ";\n"; + return success(); + } if (auto measurement = dyn_cast(&operation)) { return emitMeasurement(measurement); } @@ -572,24 +528,65 @@ class OpenQASMEmitter { [[nodiscard]] static bool isInlineExpressionOperation(Operation& operation) { const auto name = operation.getName().getStringRef(); - return isa(&operation) || + return isa(&operation) || !binaryOperator(name).empty() || name == "arith.negf" || - name == "arith.remf" || isScalarCast(name) || + name == "arith.remf" || name == "llvm.intr.fshl" || + name == "llvm.intr.fshr" || isScalarCast(name) || !mathFunction(name).empty(); } - [[nodiscard]] LogicalResult - validateInlineExpressionOperation(Operation& operation) { - for (auto result : operation.getResults()) { - const auto type = result.getType(); - if (!type.isInteger(1) && !type.isInteger(64) && !type.isIndex() && - !type.isF64()) { - return fail(&operation, "unsupported scalar expression result type"); + void indexClassicalWrites(Block& block) { + size_t position = 0; + for (Operation& operation : block) { + operationPositions[&operation] = position; + DenseSet writtenRegisters; + operation.walk([&](Operation* nested) { + if (auto store = dyn_cast(nested)) { + writtenRegisters.insert(store.getReg()); + } else if (auto write = dyn_cast(nested)) { + writtenRegisters.insert(write.getReg()); + } + }); + for (auto reg : writtenRegisters) { + classicalWrites[&block][reg].push_back(position); } - if (failed(emitExpression(result))) { - return failure(); + for (Region& region : operation.getRegions()) { + for (Block& nested : region) { + indexClassicalWrites(nested); + } } + ++position; + } + } + + [[nodiscard]] LogicalResult validateClassicalSnapshot(Operation* read, + Value reg) { + if (expressionConsumer == nullptr || + read->getBlock() != expressionConsumer->getBlock()) { + return fail(read, "cannot preserve a classical snapshot across a " + "control-flow region"); + } + const auto readPosition = operationPositions.at(read); + const auto consumerPosition = operationPositions.at(expressionConsumer); + if (readPosition >= consumerPosition) { + return fail(read, "classical snapshot does not dominate its use"); + } + const auto blockWrites = classicalWrites.find(read->getBlock()); + if (blockWrites == classicalWrites.end()) { + return success(); + } + const auto registerWrites = blockWrites->second.find(reg); + if (registerWrites == blockWrites->second.end()) { + return success(); + } + auto* const nextWrite = + std::upper_bound(registerWrites->second.begin(), + registerWrites->second.end(), readPosition); + if (nextWrite != registerWrites->second.end() && + *nextWrite < consumerPosition) { + return fail(read, "cannot preserve a stale classical snapshot"); } return success(); } @@ -640,7 +637,11 @@ class OpenQASMEmitter { return (Twine(resource->second.name) + "[" + *dynamicIndex + "]").str(); } - [[nodiscard]] FailureOr emitExpression(Value value) { + enum class ExpressionContext : uint8_t { Scalar, BitVector }; + + [[nodiscard]] FailureOr + emitExpression(Value value, + const ExpressionContext context = ExpressionContext::Scalar) { if (expressionNesting == 0) { expressionWork = 0; } @@ -657,59 +658,167 @@ class OpenQASMEmitter { "maximum of " + Twine(MAX_EXPRESSION_WORK) + " values"); } + const auto type = value.getType(); if (const auto found = valueNames.find(value); found != valueNames.end()) { return found->second; } if (auto load = value.getDefiningOp()) { + if (failed(validateClassicalSnapshot(load, load.getReg()))) { + return failure(); + } return emitBitReference(load.getReg(), load.getIndex()); } - if (auto comparison = value.getDefiningOp()) { - const auto resource = resources.find(comparison.getReg()); + if (auto read = value.getDefiningOp()) { + if (failed(validateClassicalSnapshot(read, read.getReg()))) { + return failure(); + } + const auto resource = resources.find(read.getReg()); if (resource == resources.end() || resource->second.kind != ResourceKind::Bit) { return failExpression(value, - "register comparison refers to unsupported " - "storage"); - } - const auto* predicate = [&] { - switch (comparison.getPredicate()) { - case cbit::ComparisonPredicate::Equal: - return "=="; - case cbit::ComparisonPredicate::NotEqual: - return "!="; - case cbit::ComparisonPredicate::Less: - return "<"; - case cbit::ComparisonPredicate::LessEqual: - return "<="; - case cbit::ComparisonPredicate::Greater: - return ">"; - case cbit::ComparisonPredicate::GreaterEqual: - return ">="; - } - llvm_unreachable("unknown CBit comparison predicate"); - }(); - llvm::SmallString<32> rhs; - comparison.getRhs().toString(rhs, 10, false); - return (Twine("(") + resource->second.name + " " + predicate + " " + rhs + - ")") - .str(); + "register read refers to unsupported storage"); + } + return context == ExpressionContext::Scalar && type.isInteger(1) + ? resource->second.name + "[0]" + : resource->second.name; } auto* operation = value.getDefiningOp(); if (operation == nullptr) { return failExpression(value, "unmapped block argument"); } if (auto constant = dyn_cast(operation)) { - return emitConstant(constant); + return emitConstant(constant, context != ExpressionContext::Scalar); } if (isa(operation)) { return failExpression(value, "poison values are not supported"); } + const auto integer = [&](Value operand, + bool isSigned = false) -> FailureOr { + auto text = emitExpression(operand, ExpressionContext::BitVector); + if (failed(text)) { + return failure(); + } + const auto operandType = dyn_cast(operand.getType()); + if (!operandType) { + return text; + } + const auto width = operandType.getWidth(); + if (width > 64) { + return isSigned + ? failExpression(operand, + "signed integers support at most 64 bits") + : text; + } + return (Twine(isSigned ? "int[" : "uint[") + Twine(width) + "](" + *text + + ")") + .str(); + }; if (auto cmp = dyn_cast(operation)) { - auto predicate = integerPredicate(cmp.getPredicate()); - if (predicate.empty()) { - return failExpression(value, "unsupported integer comparison"); + const bool isSigned = + mqt::unsignedPredicate(cmp.getPredicate()) != cmp.getPredicate(); + auto lhs = integer(cmp.getLhs(), isSigned); + auto rhs = integer(cmp.getRhs(), isSigned); + if (failed(lhs) || failed(rhs)) { + return failure(); } - return emitBinary(cmp.getLhs(), predicate, cmp.getRhs()); + return (Twine("(") + *lhs + " " + integerPredicate(cmp.getPredicate()) + + " " + *rhs + ")") + .str(); + } + if (auto selection = dyn_cast(operation)) { + auto condition = emitExpression(selection.getCondition()); + auto lhs = integer(selection.getTrueValue()); + auto rhs = integer(selection.getFalseValue()); + if (failed(condition) || failed(lhs) || failed(rhs)) { + return failure(); + } + const auto integerType = dyn_cast(type); + if (!integerType || integerType.getWidth() > 64) { + return failExpression( + value, "selection requires an integer of at most 64 bits"); + } + const auto width = Twine(integerType.getWidth()).str(); + const auto mask = + "uint[" + width + "](0 - uint[" + width + "](" + *condition + "))"; + const auto selected = + "(" + *rhs + " ^ ((" + *lhs + " ^ " + *rhs + ") & " + mask + "))"; + return type.isInteger(1) ? "bool(" + selected + ")" : selected; + } + if (isa(operation)) { + auto input = operation->getOperand(0); + if (cast(type).getWidth() > 64 || + (cast(input.getType()).getWidth() > 64 && + (input.getDefiningOp() == nullptr || + input.getDefiningOp()->getName().getStringRef() != "math.ctpop"))) { + return failExpression(value, "integer casts support at most 64 bits"); + } + auto operand = integer(input, isa(operation)); + if (failed(operand)) { + return failure(); + } + const auto width = cast(type).getWidth(); + if (width == 1) { + return (Twine("((") + *operand + " & uint[" + + Twine(std::min(cast(input.getType()).getWidth(), + 64U)) + + "](1)) != 0)") + .str(); + } + return (Twine("uint[") + Twine(width) + "](" + *operand + ")").str(); + } + if (isa(operation) && + isa(type)) { + const auto width = cast(type).getWidth(); + if (width > 64 && + !isa(operation)) { + return failExpression(value, + "integer arithmetic supports at most 64 bits"); + } + const bool isSigned = + isa(operation); + auto lhs = integer(operation->getOperand(0), isSigned); + auto rhs = integer(operation->getOperand(1), + isSigned && !isa(operation)); + if (failed(lhs) || failed(rhs)) { + return failure(); + } + if (isa(operation)) { + /// Unsigned machine arithmetic preserves every narrower modular result. + *lhs = "uint[64](" + *lhs + ")"; + *rhs = "uint[64](" + *rhs + ")"; + } + if (isa(operation)) { + /// OpenQASM only has a zero-filling shift. Bias the sign bit around it. + auto source = integer(operation->getOperand(0)); + if (failed(source)) { + return failExpression(value, + "signed right shifts support at most 64 bits"); + } + const auto sign = (Twine("uint[") + Twine(width) + "](" + + Twine(uint64_t{1} << (width - 1)) + ")") + .str(); + return (Twine("uint[") + Twine(width) + "](uint[64](((" + *source + + " ^ " + sign + ") >> " + *rhs + ")) - uint[64](" + sign + + " >> " + *rhs + "))") + .str(); + } + const auto name = operation->getName().getStringRef(); + const auto op = isa(operation) ? StringRef("&") + : isa(operation) ? StringRef("|") + : isa(operation) ? StringRef("^") + : binaryOperator(name); + const auto expression = + (Twine("(") + *lhs + " " + op + " " + *rhs + ")").str(); + if (width == 1) { + return (Twine("(uint[1](") + expression + ") != 0)").str(); + } + return width > 64 + ? expression + : (Twine("uint[") + Twine(width) + "](" + expression + ")") + .str(); } if (auto cmp = dyn_cast(operation)) { auto predicate = floatPredicate(cmp.getPredicate()); @@ -719,6 +828,48 @@ class OpenQASMEmitter { return emitBinary(cmp.getLhs(), predicate, cmp.getRhs()); } const auto name = operation->getName().getStringRef(); + if (name == "llvm.intr.fshl" || name == "llvm.intr.fshr") { + if (operation->getNumOperands() != 3 || + operation->getOperand(0) != operation->getOperand(1)) { + return failExpression( + value, "only rotations with identical data operands are supported"); + } + auto operand = emitExpression(operation->getOperand(0), + ExpressionContext::BitVector); + const auto width = cast(type).getWidth(); + auto count = operation->getOperand(2); + if (auto extension = count.getDefiningOp()) { + count = extension.getIn(); + } + FailureOr distance = failure(); + if (auto constant = count.getDefiningOp()) { + const auto bits = cast(constant.getValue()).getValue(); + distance = std::to_string(bits.urem(width)); + } else if (cast(count.getType()).getWidth() <= 64) { + distance = integer(count); + if (succeeded(distance)) { + /// OpenQASM rotations take signed counts; reduce before interpreting. + *distance = (Twine("int[64](uint[64](") + *distance + + ") % uint[64](" + Twine(width) + "))") + .str(); + } + } + if (failed(operand) || failed(distance)) { + return failExpression(value, "rotation counts support at most 64 bits"); + } + if (width <= 64) { + *operand = (Twine("bit[") + Twine(width) + "](uint[" + Twine(width) + + "](" + *operand + "))") + .str(); + } + auto rotation = (Twine(name == "llvm.intr.fshl" ? "rotl(" : "rotr(") + + *operand + ", " + *distance + ")") + .str(); + return width > 64 + ? rotation + : (Twine("uint[") + Twine(width) + "](" + rotation + ")") + .str(); + } if (name == "arith.remf") { auto lhs = emitExpression(operation->getOperand(0)); if (failed(lhs)) { @@ -734,13 +885,6 @@ class OpenQASMEmitter { if (operation->getNumOperands() != 2) { return failExpression(value, "malformed binary expression"); } - if ((name == "arith.andi" || name == "arith.ori" || - name == "arith.xori") && - !value.getType().isInteger(1)) { - return failExpression(value, - "packed integer bitwise operations are not " - "supported"); - } return emitBinary(operation->getOperand(0), binary, operation->getOperand(1)); } @@ -752,7 +896,10 @@ class OpenQASMEmitter { return (Twine("(-") + *operand + ")").str(); } if (isScalarCast(name)) { - auto operand = emitExpression(operation->getOperand(0)); + auto input = operation->getOperand(0); + auto operand = isa(input.getType()) + ? integer(input, name != "arith.uitofp") + : emitExpression(input); if (failed(operand)) { return failure(); } @@ -768,6 +915,19 @@ class OpenQASMEmitter { } return (Twine(type) + "(" + *operand + ")").str(); } + if (name == "math.ctpop") { + auto operand = integer(operation->getOperand(0)); + if (failed(operand)) { + return failure(); + } + const auto width = cast(type).getWidth(); + if (width > 64) { + return (Twine("popcount(") + *operand + ")").str(); + } + return (Twine("uint[") + Twine(width) + "](popcount(bit[" + Twine(width) + + "](" + *operand + ")))") + .str(); + } if (const auto functionName = mathFunction(name); !functionName.empty()) { SmallVector arguments; for (auto operand : operation->getOperands()) { @@ -785,14 +945,15 @@ class OpenQASMEmitter { } [[nodiscard]] static FailureOr - emitConstant(arith::ConstantOp constant) { + emitConstant(arith::ConstantOp constant, + const bool bitVectorContext = false) { if (auto integer = dyn_cast(constant.getValue())) { - if (integer.getType().isInteger(1)) { + if (integer.getType().isInteger(1) && !bitVectorContext) { return integer.getValue().isZero() ? std::string("false") : std::string("true"); } llvm::SmallString<32> text; - integer.getValue().toString(text, 10, true); + integer.getValue().toString(text, 10, !bitVectorContext); return text.str().str(); } if (auto floating = dyn_cast(constant.getValue())) { @@ -835,11 +996,13 @@ class OpenQASMEmitter { .Cases({"arith.addi", "arith.addf"}, "+") .Cases({"arith.subi", "arith.subf"}, "-") .Cases({"arith.muli", "arith.mulf"}, "*") - .Cases({"arith.divsi", "arith.divf"}, "/") - .Case("arith.remsi", "%") + .Cases({"arith.divsi", "arith.divui", "arith.divf"}, "/") + .Cases({"arith.remsi", "arith.remui"}, "%") .Case("arith.andi", "&&") .Case("arith.ori", "||") .Case("arith.xori", "!=") + .Case("arith.shli", "<<") + .Case("arith.shrui", ">>") .Default({}); } @@ -851,18 +1014,17 @@ class OpenQASMEmitter { case arith::CmpIPredicate::ne: return "!="; case arith::CmpIPredicate::slt: + case arith::CmpIPredicate::ult: return "<"; case arith::CmpIPredicate::sle: + case arith::CmpIPredicate::ule: return "<="; case arith::CmpIPredicate::sgt: + case arith::CmpIPredicate::ugt: return ">"; case arith::CmpIPredicate::sge: - return ">="; - case arith::CmpIPredicate::ult: - case arith::CmpIPredicate::ule: - case arith::CmpIPredicate::ugt: case arith::CmpIPredicate::uge: - return {}; + return ">="; } return {}; } @@ -890,22 +1052,28 @@ class OpenQASMEmitter { [[nodiscard]] static bool isScalarCast(const StringRef name) { return llvm::StringSwitch(name) .Case("arith.index_cast", true) - .Case("arith.sitofp", true) - .Case("arith.fptosi", true) + .Cases({"arith.sitofp", "arith.uitofp"}, true) + .Cases({"arith.fptosi", "arith.fptoui"}, true) .Default(false); } - [[nodiscard]] static StringRef castTarget(const StringRef name, - const Type resultType) { - if (name == "arith.sitofp" || resultType.isF64()) { + [[nodiscard]] static std::string castTarget(const StringRef name, + const Type resultType) { + if (resultType.isF64()) { return "float"; } if (resultType.isInteger(1)) { return "bool"; } - if (resultType.isInteger(64) || resultType.isIndex()) { + if (resultType.isIndex()) { return "int"; } + if (auto type = dyn_cast(resultType); + type && type.getWidth() <= 64) { + return (Twine(name == "arith.fptoui" ? "uint[" : "int[") + + Twine(type.getWidth()) + "]") + .str(); + } return {}; } @@ -920,6 +1088,7 @@ class OpenQASMEmitter { .Case("math.floor", "floor") .Case("math.log", "log") .Case("math.powf", "pow") + .Case("math.ctpop", "popcount") .Case("math.sin", "sin") .Case("math.sqrt", "sqrt") .Case("math.tan", "tan") @@ -1042,21 +1211,17 @@ class OpenQASMEmitter { return fail(whileOp, "scf.while loop-carried values are not supported"); } for (Operation& operation : before.without_terminator()) { - if (isa(operation)) { - if (failed(emitExpression(operation.getResult(0)))) { - return failure(); - } - continue; - } if (!isInlineExpressionOperation(operation) || - !isMemoryEffectFree(&operation)) { + (!isa(&operation) && + !isMemoryEffectFree(&operation))) { return fail(&operation, "scf.while condition region must be side-effect free"); } - if (failed(validateInlineExpressionOperation(operation))) { - return failure(); - } } + auto* previousConsumer = + std::exchange(expressionConsumer, conditionOp.getOperation()); + auto consumerGuard = + llvm::make_scope_exit([&] { expressionConsumer = previousConsumer; }); auto condition = emitExpression(conditionOp.getCondition()); if (failed(condition)) { return failure(); @@ -1121,6 +1286,10 @@ class OpenQASMEmitter { if (failed(expression)) { return failure(); } + if (auto integer = dyn_cast(value.getType()); + integer && integer.getWidth() > 1) { + *expression = scalarOutputs[scalarIndex].kind + "(" + *expression + ")"; + } *output << scalarOutputs[scalarIndex].name << " = " << *expression << ";\n"; ++scalarIndex; diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.cpp index 9e0d5db824..65d03e317c 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.cpp @@ -10,7 +10,7 @@ #include "ModifierUtils.h" -#include "mlir/Dialect/CBit/IR/CBitOps.h" +#include "mlir/Dialect/CBit/IR/CBitDialect.h" #include "mlir/Dialect/MQT/Utils/Modifiers.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" @@ -33,9 +33,10 @@ namespace mlir::qco::detail { LogicalResult verifyModifierBody(Operation* modifierOp, Block& body) { const auto hasNonUnitaryOperation = body.walk([](Operation* operation) { - return isa(operation) + return operation->getName().getDialectNamespace() == + cbit::CBitDialect::getDialectNamespace() || + isa( + operation) ? WalkResult::interrupt() : WalkResult::advance(); }) diff --git a/mlir/lib/Dialect/QCO/Utils/CMakeLists.txt b/mlir/lib/Dialect/QCO/Utils/CMakeLists.txt index 80a2439793..2693625dfe 100644 --- a/mlir/lib/Dialect/QCO/Utils/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Utils/CMakeLists.txt @@ -72,6 +72,7 @@ add_mlir_library( MLIRArithDialect MLIRFuncDialect MLIRMathDialect + MLIRLLVMDialect MLIRSCFDialect MQT::CoreDD PRIVATE diff --git a/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp b/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp index 4655a76090..beac4bacde 100644 --- a/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp +++ b/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -165,7 +166,9 @@ struct ClassicalEnv { return op->emitError() << "classical SSA value is not mapped for QCO DD simulation"; } - values[dest] = it->second; + /// Inserting the destination can grow the map and invalidate the iterator. + auto value = it->second; + values[dest] = value; return success(); } }; @@ -655,44 +658,42 @@ static LogicalResult loadRegister(cbit::LoadOp load, ClassicalEnv& classical) { classical); } -static LogicalResult compareRegister(cbit::CompareOp compare, - ClassicalEnv& classical) { - const auto regIt = classical.registers.find(compare.getReg()); +static LogicalResult readRegister(cbit::ReadOp read, ClassicalEnv& classical) { + const auto regIt = classical.registers.find(read.getReg()); if (regIt == classical.registers.end()) { - return compare.emitError() + return read.emitError() << "CBit register is not mapped for QCO DD simulation"; } - llvm::APInt actual(compare.getRhs().getBitWidth(), 0); + llvm::APInt value(read.getResult().getType().getWidth(), 0); for (const auto [index, cell] : llvm::enumerate(*regIt->second)) { if (cell.deferredWire && classical.deferredMeasurementUse != nullptr) { - *classical.deferredMeasurementUse = compare.getOperation(); + *classical.deferredMeasurementUse = read.getOperation(); return failure(); } if (!cell.value) { - return compare.emitError() - << "read from an undefined CBit register element"; - } - actual.setBitVal(static_cast(index), *cell.value); - } - const auto result = [&] { - switch (compare.getPredicate()) { - case cbit::ComparisonPredicate::Equal: - return actual.eq(compare.getRhs()); - case cbit::ComparisonPredicate::NotEqual: - return actual.ne(compare.getRhs()); - case cbit::ComparisonPredicate::Less: - return actual.ult(compare.getRhs()); - case cbit::ComparisonPredicate::LessEqual: - return actual.ule(compare.getRhs()); - case cbit::ComparisonPredicate::Greater: - return actual.ugt(compare.getRhs()); - case cbit::ComparisonPredicate::GreaterEqual: - return actual.uge(compare.getRhs()); + return read.emitError() << "read from an undefined CBit register element"; } - llvm_unreachable("unknown CBit comparison predicate"); - }(); - return bindInteger(compare.getResult(), - llvm::APInt(1, static_cast(result)), classical); + value.setBitVal(static_cast(index), *cell.value); + } + return bindInteger(read.getResult(), value, classical); +} + +static LogicalResult writeRegister(cbit::WriteOp write, + ClassicalEnv& classical) { + const auto regIt = classical.registers.find(write.getReg()); + if (regIt == classical.registers.end()) { + return write.emitError() + << "CBit register is not mapped for QCO DD simulation"; + } + auto value = lookupInteger(write.getValue(), classical, write); + if (failed(value)) { + return failure(); + } + for (auto&& [index, bit] : llvm::enumerate(*regIt->second)) { + bit.value.emplace((*value)[static_cast(index)]); + bit.deferredWire.reset(); + } + return success(); } static FailureOr lookupMemRefSlot(Value memref, ValueRange indices, @@ -890,10 +891,26 @@ static LogicalResult applyClassicalOp(Operation& op, ClassicalEnv& classical) { arith::MinSIOp, arith::MaxUIOp, arith::MinUIOp, arith::MaximumFOp, arith::MinimumFOp, arith::MaxNumFOp, arith::MinNumFOp, math::AbsFOp, math::CeilOp, math::CosOp, math::ExpOp, math::FloorOp, math::LogOp, - math::SinOp, math::SqrtOp, math::TanOp, math::PowFOp>( - [&](Operation* foldable) { - return foldClassicalOp(*foldable, classical); - }) + math::SinOp, math::SqrtOp, math::TanOp, math::PowFOp, + math::CtPopOp>([&](Operation* foldable) { + return foldClassicalOp(*foldable, classical); + }) + .Case([&](Operation* shift) -> LogicalResult { + auto lhs = lookupInteger(shift->getOperand(0), classical, shift); + auto rhs = lookupInteger(shift->getOperand(1), classical, shift); + auto distance = lookupInteger(shift->getOperand(2), classical, shift); + if (failed(lhs) || failed(rhs) || failed(distance)) { + return failure(); + } + const auto width = lhs->getBitWidth(); + const auto amount = static_cast(distance->urem(width)); + const bool left = isa(shift); + auto value = amount == 0 + ? (left ? *lhs : *rhs) + : lhs->shl(left ? amount : width - amount) | + rhs->lshr(left ? width - amount : amount); + return bindInteger(shift->getResult(0), value, classical); + }) .Case([&](arith::DivUIOp value) { return applyDivision( value, classical, @@ -1322,8 +1339,11 @@ static LogicalResult applyOp(Operation& op, WalkState& walk, StateDD& state) { .template Case([&](cbit::LoadOp load) { return loadRegister(load, *walk.classical); }) - .template Case([&](cbit::CompareOp compare) { - return compareRegister(compare, *walk.classical); + .template Case([&](cbit::ReadOp read) { + return readRegister(read, *walk.classical); + }) + .template Case([&](cbit::WriteOp write) { + return writeRegister(write, *walk.classical); }) .template Case([&](cbit::StoreOp store) { return storeRegister(store, *walk.classical); @@ -1593,7 +1613,8 @@ static LogicalResult applyOp(Operation& op, WalkState& walk, StateDD& state) { .Default([&](Operation* unsupported) -> LogicalResult { const StringRef dialect = unsupported->getName().getDialectNamespace(); if (dialect == arith::ArithDialect::getDialectNamespace() || - dialect == math::MathDialect::getDialectNamespace()) { + dialect == math::MathDialect::getDialectNamespace() || + isa(unsupported)) { return applyClassicalOp(*unsupported, *walk.classical); } return unsupported->emitError() diff --git a/mlir/lib/Support/CMakeLists.txt b/mlir/lib/Support/CMakeLists.txt index 340f4dfe0f..1d97ac75ad 100644 --- a/mlir/lib/Support/CMakeLists.txt +++ b/mlir/lib/Support/CMakeLists.txt @@ -8,6 +8,7 @@ add_mlir_library( MLIRSupportMQT + IntegerExpressions.cpp Passes.cpp PrettyPrinting.cpp ADDITIONAL_HEADER_DIRS @@ -16,6 +17,9 @@ add_mlir_library( PUBLIC MLIRSupport MLIRIR + MLIRArithDialect + MLIRLLVMDialect + MLIRMathDialect MLIRPass MLIRTransforms MLIRControlFlowInterfaces diff --git a/mlir/lib/Support/IntegerExpressions.cpp b/mlir/lib/Support/IntegerExpressions.cpp new file mode 100644 index 0000000000..d7a5c6d7d4 --- /dev/null +++ b/mlir/lib/Support/IntegerExpressions.cpp @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Support/IntegerExpressions.h" + +#include +#include +#include +#include + +#include + +using namespace mlir; + +namespace { +/// Expand the operations absent from jeff using the same exact-width +/// arithmetic. +struct ExpandIntegerOperation final : RewritePattern { + explicit ExpandIntegerOperation(MLIRContext* context) + : RewritePattern(MatchAnyOpTypeTag(), 1, context) {} + LogicalResult matchAndRewrite(Operation* op, + PatternRewriter& rewriter) const override { + if (!isa(op)) { + return failure(); + } + auto type = dyn_cast(op->getResult(0).getType()); + if (!type || type.getWidth() > 64) { + return failure(); + } + const auto width = type.getWidth(); + auto loc = op->getLoc(); + if (width == 1) { + rewriter.replaceOp(op, op->getOperand(isa(op) ? 1 : 0)); + return success(); + } + if (isa(op)) { + /// Count pairs, nibbles, then bytes. Multiplication sums the byte counts. + /// This uses eight copies of the input when expanded to a source tree, + /// rather than one copy per bit. + unsigned wordWidth = 8; + while (wordWidth < width) { + wordWidth *= 2; + } + auto wordType = rewriter.getIntegerType(wordWidth); + const auto constant = [&](uint64_t bits) -> Value { + return arith::ConstantOp::create( + rewriter, loc, + rewriter.getIntegerAttr(wordType, + APInt(64, bits).trunc(wordWidth))); + }; + const auto shift = [&](Value value, unsigned distance) -> Value { + return arith::ShRUIOp::create(rewriter, loc, value, constant(distance)); + }; + const auto mask = [&](Value value, uint64_t bits) -> Value { + return arith::AndIOp::create(rewriter, loc, value, constant(bits)); + }; + Value count = op->getOperand(0); + if (width != wordWidth) { + count = arith::ExtUIOp::create(rewriter, loc, wordType, count); + } + count = arith::SubIOp::create( + rewriter, loc, count, mask(shift(count, 1), 0x5555555555555555ULL)); + count = arith::AddIOp::create( + rewriter, loc, mask(count, 0x3333333333333333ULL), + mask(shift(count, 2), 0x3333333333333333ULL)); + count = mask(arith::AddIOp::create(rewriter, loc, count, shift(count, 4)), + 0x0F0F0F0F0F0F0F0FULL); + if (wordWidth > 8) { + count = arith::MulIOp::create(rewriter, loc, count, + constant(0x0101010101010101ULL)); + count = shift(count, wordWidth - 8); + } + if (width != wordWidth) { + count = arith::TruncIOp::create(rewriter, loc, type, count); + } + rewriter.replaceOp(op, count); + return success(); + } + auto size = arith::ConstantIntOp::create(rewriter, loc, type, width); + auto amount = + arith::RemUIOp::create(rewriter, loc, op->getOperand(2), size); + auto inverse = arith::SubIOp::create(rewriter, loc, size, amount); + const bool left = isa(op); + auto first = mqt::buildZeroFillingShift( + rewriter, loc, op->getOperand(0), + left ? amount.getResult() : inverse.getResult(), true); + auto second = mqt::buildZeroFillingShift( + rewriter, loc, op->getOperand(1), + left ? inverse.getResult() : amount.getResult(), false); + rewriter.replaceOpWithNewOp(op, first, second); + return success(); + } +}; + +} // namespace + +void mlir::mqt::populateIntegerExpansionPatterns(RewritePatternSet& patterns) { + patterns.add(patterns.getContext()); +} diff --git a/mlir/lib/Target/OpenQASM/OpenQASMSemantics.cpp b/mlir/lib/Target/OpenQASM/OpenQASMSemantics.cpp index adb3564dd1..369bea6f2c 100644 --- a/mlir/lib/Target/OpenQASM/OpenQASMSemantics.cpp +++ b/mlir/lib/Target/OpenQASM/OpenQASMSemantics.cpp @@ -77,6 +77,7 @@ struct FixedAngle { struct Constant { ScalarType type = ScalarType::Int; std::variant value = int64_t{0}; + unsigned integerWidth = 0; }; struct GateSignature { @@ -99,6 +100,7 @@ struct Symbol { ScalarType type = ScalarType::Int; uint32_t id = 0; std::optional constant; + unsigned integerWidth = 0; }; } // namespace @@ -283,6 +285,28 @@ belongsToStdGates(const GateAvailability availability) { return std::get(constant.value); } +/// Qiskit emits untyped integer literals in otherwise typed bitwise +/// expressions. Accept a nonnegative constant when its value fits the chosen +/// unsigned width. +static bool coerceUnsignedConstant(Constant& constant, unsigned width) { + if (constant.type != ScalarType::Uint && constant.type != ScalarType::Int) { + return false; + } + if (constant.type == ScalarType::Int && + std::get(constant.value) < 0) { + return false; + } + const auto bits = + constant.type == ScalarType::Uint + ? std::get(constant.value) + : static_cast(std::get(constant.value)); + if (!llvm::isUIntN(width, bits)) { + return false; + } + constant = {.type = ScalarType::Uint, .value = bits, .integerWidth = width}; + return true; +} + [[nodiscard]] static bool canImplicitlyPromote(const Constant& initializer, const ScalarType destination) { if (initializer.type == destination) { @@ -309,8 +333,18 @@ belongsToStdGates(const GateAvailability availability) { llvm_unreachable("unknown scalar type"); } -[[nodiscard]] static int compareNumericConstants(const Constant& lhs, - const Constant& rhs) { +/// Every narrower unsigned integer fits the language's signed machine integer. +static void promoteIntegerConstant(Constant& value) { + if (value.type == ScalarType::Uint && value.integerWidth > 0 && + value.integerWidth < 64) { + value.type = ScalarType::Int; + value.value = static_cast(std::get(value.value)); + } +} + +[[nodiscard]] static int compareNumericConstants(Constant lhs, Constant rhs) { + promoteIntegerConstant(lhs); + promoteIntegerConstant(rhs); if (lhs.type == ScalarType::Float || lhs.type == ScalarType::Angle || rhs.type == ScalarType::Float || rhs.type == ScalarType::Angle) { const auto left = asDouble(lhs); @@ -550,6 +584,9 @@ class SemanticAnalyzer { [[nodiscard]] std::optional buildAffineForm(const ExpressionId expression) const { const auto& value = program.expressions.at(expression); + if (value.integerWidth != 0) { + return std::nullopt; + } std::optional result; switch (value.kind) { case ExpressionKind::Constant: @@ -847,7 +884,8 @@ class SemanticAnalyzer { const auto& right = program.expressions[rhs]; if (left.kind != right.kind || left.type != right.type || left.constant != right.constant || left.parameter != right.parameter || - left.variable != right.variable) { + left.variable != right.variable || + left.integerWidth != right.integerWidth) { return false; } switch (left.kind) { @@ -856,7 +894,11 @@ class SemanticAnalyzer { case ExpressionKind::Variable: return true; case ExpressionKind::PopCount: + case ExpressionKind::BitVectorCast: return sameBitVectorExpression(left.bitVector, right.bitVector); + case ExpressionKind::Condition: + return left.condition == right.condition; + case ExpressionKind::BitNot: case ExpressionKind::Cast: case ExpressionKind::Negate: case ExpressionKind::ArcCos: @@ -885,23 +927,61 @@ class SemanticAnalyzer { if (left.kind != right.kind || left.width != right.width) { return false; } - if (left.kind == BitVectorExpressionKind::Register) { + switch (left.kind) { + case BitVectorExpressionKind::ScalarCast: + return sameExpression(left.scalar, right.scalar); + case BitVectorExpressionKind::Constant: + return left.constant == right.constant; + case BitVectorExpressionKind::Register: return left.reg == right.reg; - } - return sameBitVectorExpression(left.operand, right.operand) && - sameExpression(left.distance, right.distance); + case BitVectorExpressionKind::Not: + return sameBitVectorExpression(left.operand, right.operand); + case BitVectorExpressionKind::And: + case BitVectorExpressionKind::Or: + case BitVectorExpressionKind::Xor: + return sameBitVectorExpression(left.operand, right.operand) && + sameBitVectorExpression(left.rhs, right.rhs); + case BitVectorExpressionKind::ShiftLeft: + case BitVectorExpressionKind::ShiftRight: + case BitVectorExpressionKind::RotateLeft: + case BitVectorExpressionKind::RotateRight: + return sameBitVectorExpression(left.operand, right.operand) && + sameExpression(left.distance, right.distance); + } + llvm_unreachable("unknown bit-vector expression kind"); } void collectBitVectorDependencies( const BitVectorExpressionId expression, std::vector>& dependencies) const { const auto& value = program.bitVectorExpressions[expression]; - if (value.kind == BitVectorExpressionKind::Register) { + switch (value.kind) { + case BitVectorExpressionKind::ScalarCast: + collectDependencies(value.scalar, dependencies); + return; + case BitVectorExpressionKind::Constant: + return; + case BitVectorExpressionKind::Register: dependencies.emplace_back(value.reg, bitGenerations[value.reg]); return; + case BitVectorExpressionKind::Not: + collectBitVectorDependencies(value.operand, dependencies); + return; + case BitVectorExpressionKind::And: + case BitVectorExpressionKind::Or: + case BitVectorExpressionKind::Xor: + collectBitVectorDependencies(value.operand, dependencies); + collectBitVectorDependencies(value.rhs, dependencies); + return; + case BitVectorExpressionKind::ShiftLeft: + case BitVectorExpressionKind::ShiftRight: + case BitVectorExpressionKind::RotateLeft: + case BitVectorExpressionKind::RotateRight: + collectBitVectorDependencies(value.operand, dependencies); + collectDependencies(value.distance, dependencies); + return; } - collectBitVectorDependencies(value.operand, dependencies); - collectDependencies(value.distance, dependencies); + llvm_unreachable("unknown bit-vector expression kind"); } void collectDependencies( @@ -917,12 +997,25 @@ class SemanticAnalyzer { value.kind == ExpressionKind::GateParameter) { return; } - if (value.kind == ExpressionKind::PopCount) { + if (value.kind == ExpressionKind::PopCount || + value.kind == ExpressionKind::BitVectorCast) { collectBitVectorDependencies(value.bitVector, dependencies); return; } + if (value.kind == ExpressionKind::Condition) { + /// Boolean expressions used as indices depend on the current classical + /// state. + for (const auto [id, generation] : llvm::enumerate(scalarGenerations)) { + dependencies.emplace_back((uint64_t{1} << 63U) | id, generation); + } + for (const auto [id, generation] : llvm::enumerate(bitGenerations)) { + dependencies.emplace_back(id, generation); + } + return; + } collectDependencies(value.lhs, dependencies); - if (value.kind != ExpressionKind::Cast && + if (value.kind != ExpressionKind::BitNot && + value.kind != ExpressionKind::Cast && value.kind != ExpressionKind::Negate && value.kind != ExpressionKind::ArcCos && value.kind != ExpressionKind::ArcSin && @@ -1126,7 +1219,7 @@ class SemanticAnalyzer { addBitVectorExpression(BitVectorExpression expression) { const auto id = static_cast(program.bitVectorExpressions.size()); - program.bitVectorExpressions.push_back(expression); + program.bitVectorExpressions.push_back(std::move(expression)); return id; } @@ -1151,7 +1244,8 @@ class SemanticAnalyzer { } else { return addExpression({.kind = ExpressionKind::Constant, .type = constant.type, - .constant = value}); + .constant = value, + .integerWidth = constant.integerWidth}); } }, constant.value); @@ -1181,9 +1275,10 @@ class SemanticAnalyzer { [[nodiscard]] FailureOr castExpression(const ExpressionId expression, const ScalarType target, - const SMLoc location) { + const SMLoc location, const unsigned width = 0) { const auto source = program.expressions[expression].type; - if (source == target) { + if (source == target && + program.expressions[expression].integerWidth == width) { return expression; } if (!canImplicitlyConvert(source, target)) { @@ -1191,8 +1286,10 @@ class SemanticAnalyzer { "' cannot be implicitly converted to '" + scalarTypeName(target) + "'"); } - return addExpression( - {.kind = ExpressionKind::Cast, .type = target, .lhs = expression}); + return addExpression({.kind = ExpressionKind::Cast, + .type = target, + .lhs = expression, + .integerWidth = width}); } [[nodiscard]] FailureOr @@ -1293,10 +1390,35 @@ class SemanticAnalyzer { return expression.integer; } + [[nodiscard]] FailureOr + bitVectorCastWidth(std::optional size, + SMLoc location) const { + if (!size) { + return 64; + } + if (!isConstantExpression(*size)) { + return fail(location, + "bit-register cast width must be a constant integer " + "expression"); + } + MQT_OQ3_TRY_ASSIGN(constant, evaluateConstant(*size)); + if (!isInteger(constant.type)) { + return fail(location, + "bit-register cast width must be an integer expression"); + } + const auto width = asSigned(constant); + if (!width || *width < 1 || *width > 64) { + return fail(location, + "bit-register cast supports widths from 1 through 64"); + } + return static_cast(*width); + } + [[nodiscard]] bool expressionProducesBool(const SyntaxExpressionId id) const { const auto& expression = syntax.expressions[id]; switch (expression.kind) { case Expr::Kind::Bool: + case Expr::Kind::BoolCast: case Expr::Kind::Not: case Expr::Kind::And: case Expr::Kind::Or: @@ -1326,6 +1448,20 @@ class SemanticAnalyzer { [[nodiscard]] FailureOr analyzeBoolValue(const SyntaxExpressionId syntaxId) { + if (isBitVectorExpression(syntaxId)) { + MQT_OQ3_TRY_ASSIGN(value, analyzeBitVectorExpression(syntaxId)); + const auto width = program.bitVectorExpressions[value].width; + const auto zero = addBitVectorExpression( + {.kind = BitVectorExpressionKind::Constant, + .width = width, + .constant = llvm::APInt(static_cast(width), 0)}); + return addCondition( + {.kind = ConditionKind::BitVectorComparison, + .location = getSourceLocation(syntax.expressions[syntaxId].location), + .bitVectorComparisonLhs = value, + .bitVectorComparisonRhs = zero, + .comparison = ComparisonKind::NotEqual}); + } if (expressionProducesBool(syntaxId)) { return analyzeCondition(syntaxId); } @@ -1344,6 +1480,7 @@ class SemanticAnalyzer { } else if (type == ScalarType::Uint) { zeroValue = Constant{.type = ScalarType::Uint, .value = uint64_t{0}}; } + zeroValue.integerWidth = program.expressions[value].integerWidth; const auto zero = addConstant(zeroValue); return addCondition({.kind = ConditionKind::Comparison, .location = sourceLocation( @@ -1410,6 +1547,45 @@ class SemanticAnalyzer { MQT_OQ3_TRY_ASSIGN(operand, evaluateConstant(*expression.rhs)); return convertToFixedAngle(operand, width, expression.location); } + case Expr::Kind::BoolCast: { + if (expression.lhs) { + return fail(expression.location, "bool casts do not have a width"); + } + MQT_OQ3_TRY_ASSIGN(operand, evaluateConstant(*expression.rhs)); + return Constant{.type = ScalarType::Bool, + .value = asDouble(operand) != 0}; + } + case Expr::Kind::BitCast: + return fail(expression.location, + "bit-register value is not a scalar constant"); + case Expr::Kind::IntCast: + case Expr::Kind::UintCast: { + MQT_OQ3_TRY_ASSIGN( + width, bitVectorCastWidth(expression.lhs, expression.location)); + MQT_OQ3_TRY_ASSIGN(operand, evaluateConstant(*expression.rhs)); + if (!isInteger(operand.type) && operand.type != ScalarType::Bool) { + return fail(expression.location, + "integer casts require integer or bool operands"); + } + const auto bits = + operand.type == ScalarType::Int + ? static_cast(std::get(operand.value)) + : operand.type == ScalarType::Bool + ? static_cast(std::get(operand.value)) + : std::get(operand.value); + const auto narrowed = + llvm::APInt(64, bits).trunc(static_cast(width)); + const auto resultWidth = + expression.lhs ? static_cast(width) : 0; + if (expression.kind == Expr::Kind::IntCast) { + return Constant{.type = ScalarType::Int, + .value = narrowed.getSExtValue(), + .integerWidth = resultWidth}; + } + return Constant{.type = ScalarType::Uint, + .value = narrowed.getZExtValue(), + .integerWidth = resultWidth}; + } case Expr::Kind::Neg: { MQT_OQ3_TRY_ASSIGN(operand, evaluateConstant(*expression.lhs)); if (operand.type == ScalarType::Bool) { @@ -1455,12 +1631,8 @@ class SemanticAnalyzer { return Constant{.type = ScalarType::Bool, .value = !std::get(operand.value)}; } - case Expr::Kind::BitNot: { - return fail( - expression.location, - "bitwise operators require explicitly sized uint, bit, or angle " - "operands, which are not supported yet"); - } + case Expr::Kind::BitNot: + return evaluateConstantBitwise(expression); case Expr::Kind::And: case Expr::Kind::Or: { MQT_OQ3_TRY_ASSIGN(lhs, evaluateConstant(*expression.lhs)); @@ -1661,10 +1833,7 @@ class SemanticAnalyzer { case Expr::Kind::BitXor: case Expr::Kind::ShiftLeft: case Expr::Kind::ShiftRight: - return fail( - expression.location, - "bitwise operators require explicitly sized uint, bit, or angle " - "operands, which are not supported yet"); + return evaluateConstantBitwise(expression); case Expr::Kind::Index: return fail(expression.location, "expression is not a compile-time constant"); @@ -1716,10 +1885,16 @@ class SemanticAnalyzer { } return symbol->constant->type; } + case Expr::Kind::BoolCast: + case Expr::Kind::BitCast: case Expr::Kind::AngleCast: { MQT_OQ3_TRY_ASSIGN(constant, evaluateConstant(id)); return constant.type; } + case Expr::Kind::IntCast: + return ScalarType::Int; + case Expr::Kind::UintCast: + return ScalarType::Uint; case Expr::Kind::Neg: { MQT_OQ3_TRY_ASSIGN(type, constantExpressionType(*expression.lhs)); if (type == ScalarType::Bool) { @@ -1866,11 +2041,10 @@ class SemanticAnalyzer { case Expr::Kind::BitOr: case Expr::Kind::BitXor: case Expr::Kind::ShiftLeft: - case Expr::Kind::ShiftRight: - return fail( - expression.location, - "bitwise operators require explicitly sized uint, bit, or angle " - "operands, which are not supported yet"); + case Expr::Kind::ShiftRight: { + MQT_OQ3_TRY_ASSIGN(constant, evaluateConstantBitwise(expression)); + return constant.type; + } case Expr::Kind::Index: return fail(expression.location, "expression is not a compile-time constant"); @@ -1890,10 +2064,61 @@ class SemanticAnalyzer { return *result; } + [[nodiscard]] FailureOr + evaluateConstantBitwise(const SyntaxExpression& expression) const { + MQT_OQ3_TRY_ASSIGN(lhs, evaluateConstant(*expression.lhs)); + const auto width = lhs.integerWidth != 0 ? lhs.integerWidth : 64; + if (!coerceUnsignedConstant(lhs, width)) { + return fail(expression.location, + "bitwise operands require explicitly sized uint"); + } + llvm::APInt result(lhs.integerWidth, std::get(lhs.value)); + if (expression.kind == Expr::Kind::BitNot) { + result.flipAllBits(); + } else { + MQT_OQ3_TRY_ASSIGN(rhs, evaluateConstant(*expression.rhs)); + const bool shift = expression.kind == Expr::Kind::ShiftLeft || + expression.kind == Expr::Kind::ShiftRight; + if (shift) { + if (!isInteger(rhs.type)) { + return fail(expression.location, + "shift distance must be a nonnegative integer"); + } + auto distance = asSigned(rhs); + if (rhs.type == ScalarType::Int && (!distance || *distance < 0)) { + return fail(expression.location, + "shift distance must be a nonnegative integer"); + } + const auto count = rhs.type == ScalarType::Uint + ? std::get(rhs.value) + : static_cast(*distance); + result = count >= lhs.integerWidth ? llvm::APInt(lhs.integerWidth, 0) + : expression.kind == Expr::Kind::ShiftLeft + ? result.shl(count) + : result.lshr(count); + } else { + if (!coerceUnsignedConstant(rhs, lhs.integerWidth)) { + return fail( + expression.location, + "bitwise operands require matching explicitly sized uint"); + } + llvm::APInt right(rhs.integerWidth, std::get(rhs.value)); + result = expression.kind == Expr::Kind::BitAnd ? result & right + : expression.kind == Expr::Kind::BitOr ? result | right + : result ^ right; + } + } + return Constant{.type = ScalarType::Uint, + .value = result.getZExtValue(), + .integerWidth = lhs.integerWidth}; + } + [[nodiscard]] FailureOr evaluateConstantBinary(const SyntaxExpression& expression) const { MQT_OQ3_TRY_ASSIGN(lhs, evaluateConstant(*expression.lhs)); MQT_OQ3_TRY_ASSIGN(rhs, evaluateConstant(*expression.rhs)); + promoteIntegerConstant(lhs); + promoteIntegerConstant(rhs); if (lhs.type == ScalarType::Bool || rhs.type == ScalarType::Bool) { return fail(expression.location, "arithmetic operators require numeric operands"); @@ -2150,6 +2375,7 @@ class SemanticAnalyzer { return true; case Expr::Kind::Index: case Expr::Kind::PopCount: + case Expr::Kind::BitCast: case Expr::Kind::RotateLeft: case Expr::Kind::RotateRight: return false; @@ -2179,9 +2405,65 @@ class SemanticAnalyzer { return success(); } - [[nodiscard]] FailureOr - analyzeBitVectorExpression(const SyntaxExpressionId syntaxId) { + [[nodiscard]] bool + isBitVectorExpression(const SyntaxExpressionId syntaxId) const { + const auto& expression = syntax.expressions[syntaxId]; + if (expression.kind == Expr::Kind::Identifier) { + const auto* symbol = lookup(expression.identifier); + return symbol != nullptr && symbol->kind == SymbolKind::Register && + program.registers[symbol->id].kind == RegisterKind::Bit && + !program.registers[symbol->id].isScalar; + } + switch (expression.kind) { + case Expr::Kind::BitCast: + return true; + case Expr::Kind::BitNot: + case Expr::Kind::ShiftLeft: + case Expr::Kind::ShiftRight: + case Expr::Kind::RotateLeft: + case Expr::Kind::RotateRight: + return expression.lhs && isBitVectorExpression(*expression.lhs); + case Expr::Kind::BitAnd: + case Expr::Kind::BitOr: + case Expr::Kind::BitXor: + return (expression.lhs && isBitVectorExpression(*expression.lhs)) || + (expression.rhs && isBitVectorExpression(*expression.rhs)); + default: + return false; + } + } + + [[nodiscard]] FailureOr analyzeBitVectorExpression( + const SyntaxExpressionId syntaxId, + const std::optional expectedWidth = std::nullopt) { const auto& expression = syntax.expressions[syntaxId]; + if (expression.kind == Expr::Kind::BitCast) { + MQT_OQ3_TRY_ASSIGN( + width, bitVectorCastWidth(expression.lhs, expression.location)); + if (expectedWidth && *expectedWidth != width) { + return fail(expression.location, + "bit-vector operand widths must match"); + } + if (isBitVectorExpression(*expression.rhs)) { + return analyzeBitVectorExpression(*expression.rhs, width); + } + MQT_OQ3_TRY_ASSIGN(scalar, analyzeExpression(*expression.rhs)); + if (program.expressions[scalar].type == ScalarType::Bool) { + scalar = addExpression({.kind = ExpressionKind::Cast, + .type = ScalarType::Uint, + .lhs = scalar, + .integerWidth = static_cast(width)}); + } + if (!isInteger(program.expressions[scalar].type) || + program.expressions[scalar].integerWidth != width) { + return fail(expression.location, + "bit-register casts require a matching-width integer"); + } + return addBitVectorExpression( + {.kind = BitVectorExpressionKind::ScalarCast, + .width = width, + .scalar = scalar}); + } if (expression.kind == Expr::Kind::Identifier) { const auto* symbol = lookup(expression.identifier); if (symbol == nullptr || symbol->kind != SymbolKind::Register || @@ -2195,7 +2477,16 @@ class SemanticAnalyzer { expression.location, "bit-vector expression requires a bit register, not scalar bit"); } + if (insideGate) { + return fail(expression.location, + "gate definitions cannot capture outer bit register '" + + expression.identifier + "'"); + } const auto width = program.registers[reg].width; + if (expectedWidth && *expectedWidth != width) { + return fail(expression.location, + "bit-vector operand widths must match"); + } for (uint64_t bit = 0; bit < width; ++bit) { if (failed(ensureBitInitialized({.reg = reg, .index = bit}, expression.location))) { @@ -2206,24 +2497,160 @@ class SemanticAnalyzer { .width = width, .reg = reg}); } - if (expression.kind != Expr::Kind::RotateLeft && - expression.kind != Expr::Kind::RotateRight) { + + if (expectedWidth && isConstantExpression(syntaxId) && + expression.kind != Expr::Kind::BitNot && + expression.kind != Expr::Kind::BitAnd && + expression.kind != Expr::Kind::BitOr && + expression.kind != Expr::Kind::BitXor && + expression.kind != Expr::Kind::ShiftLeft && + expression.kind != Expr::Kind::ShiftRight) { + llvm::APInt value(static_cast(*expectedWidth), 0); + if (expression.kind == Expr::Kind::Int && + !expression.wideInteger.empty()) { + llvm::SmallString<64> digits; + for (const char digit : expression.wideInteger) { + if (digit != '_') { + digits.push_back(digit); + } + } + const auto workingWidth = static_cast( + std::max(*expectedWidth, digits.size() * 4)); + llvm::APInt parsed(workingWidth, digits, /*radix=*/10); + if (parsed.getActiveBits() > *expectedWidth) { + return fail(expression.location, + "bit-vector constant does not fit the operand width"); + } + value = parsed.trunc(static_cast(*expectedWidth)); + } else { + MQT_OQ3_TRY_ASSIGN(constant, evaluateConstant(syntaxId)); + std::optional integer; + if (constant.type == ScalarType::Uint) { + integer = std::get(constant.value); + } else if (constant.type == ScalarType::Int) { + const auto signedValue = std::get(constant.value); + if (signedValue >= 0) { + integer = static_cast(signedValue); + } + } + if (!integer || + (*expectedWidth < 64 && !llvm::isUIntN(*expectedWidth, *integer))) { + return fail(expression.location, + "bit-vector constant must be nonnegative and fit the " + "operand width"); + } + value = llvm::APInt(static_cast(*expectedWidth), *integer); + } + return addBitVectorExpression({.kind = BitVectorExpressionKind::Constant, + .width = *expectedWidth, + .constant = std::move(value)}); + } + + if (expression.kind == Expr::Kind::BitNot) { + MQT_OQ3_TRY_ASSIGN( + operand, analyzeBitVectorExpression(*expression.lhs, expectedWidth)); + return addBitVectorExpression( + {.kind = BitVectorExpressionKind::Not, + .width = program.bitVectorExpressions[operand].width, + .operand = operand}); + } + + if (expression.kind == Expr::Kind::BitAnd || + expression.kind == Expr::Kind::BitOr || + expression.kind == Expr::Kind::BitXor) { + auto width = expectedWidth; + std::optional lhs; + std::optional rhs; + if (!width && isBitVectorExpression(*expression.lhs)) { + MQT_OQ3_TRY_ASSIGN(value, analyzeBitVectorExpression(*expression.lhs)); + lhs = value; + width = program.bitVectorExpressions[*lhs].width; + } else if (!width && isBitVectorExpression(*expression.rhs)) { + MQT_OQ3_TRY_ASSIGN(value, analyzeBitVectorExpression(*expression.rhs)); + rhs = value; + width = program.bitVectorExpressions[*rhs].width; + } + if (!width) { + return fail(expression.location, + "bit-vector operators require a bit-register operand"); + } + if (!lhs) { + MQT_OQ3_TRY_ASSIGN(value, + analyzeBitVectorExpression(*expression.lhs, width)); + lhs = value; + } + if (!rhs) { + MQT_OQ3_TRY_ASSIGN(value, + analyzeBitVectorExpression(*expression.rhs, width)); + rhs = value; + } + auto kind = BitVectorExpressionKind::And; + if (expression.kind == Expr::Kind::BitOr) { + kind = BitVectorExpressionKind::Or; + } else if (expression.kind == Expr::Kind::BitXor) { + kind = BitVectorExpressionKind::Xor; + } + return addBitVectorExpression( + {.kind = kind, .width = *width, .operand = *lhs, .rhs = *rhs}); + } + + const bool shift = expression.kind == Expr::Kind::ShiftLeft || + expression.kind == Expr::Kind::ShiftRight; + const bool rotation = expression.kind == Expr::Kind::RotateLeft || + expression.kind == Expr::Kind::RotateRight; + if (!shift && !rotation) { return fail(expression.location, - "bit-vector expression requires a bit register or rotation"); + "bit-vector expression requires a bit register or bitwise " + "operation"); } - MQT_OQ3_TRY_ASSIGN(operand, analyzeBitVectorExpression(*expression.lhs)); - MQT_OQ3_TRY_ASSIGN(distance, analyzeExpression(*expression.rhs)); - if (program.expressions[distance].type != ScalarType::Int) { + MQT_OQ3_TRY_ASSIGN( + operand, analyzeBitVectorExpression(*expression.lhs, expectedWidth)); + std::optional distance; + if (shift && isBitVectorExpression(*expression.rhs)) { + MQT_OQ3_TRY_ASSIGN(bitVector, + analyzeBitVectorExpression(*expression.rhs)); + const auto width = program.bitVectorExpressions[bitVector].width; + if (width > 64) { + return fail(syntax.expressions[*expression.rhs].location, + "bit-register shift distance supports at most 64 bits"); + } + distance = addExpression({.kind = ExpressionKind::BitVectorCast, + .type = ScalarType::Uint, + .bitVector = bitVector}); + } else { + MQT_OQ3_TRY_ASSIGN(value, analyzeExpression(*expression.rhs)); + distance = value; + } + const auto& distanceExpression = program.expressions[*distance]; + if (shift) { + if (!isInteger(distanceExpression.type) || + (distanceExpression.kind != ExpressionKind::Constant && + distanceExpression.type != ScalarType::Uint)) { + return fail(syntax.expressions[*expression.rhs].location, + "bit-register shift distance must have unsigned integer " + "type"); + } + if (distanceExpression.type == ScalarType::Int && + std::get(distanceExpression.constant) < 0) { + return fail(syntax.expressions[*expression.rhs].location, + "bit-register shift distance must be nonnegative"); + } + } else if (distanceExpression.type != ScalarType::Int) { return fail(syntax.expressions[*expression.rhs].location, "bit-register rotation distance must have signed int type"); } + auto kind = expression.kind == Expr::Kind::ShiftLeft + ? BitVectorExpressionKind::ShiftLeft + : expression.kind == Expr::Kind::ShiftRight + ? BitVectorExpressionKind::ShiftRight + : expression.kind == Expr::Kind::RotateLeft + ? BitVectorExpressionKind::RotateLeft + : BitVectorExpressionKind::RotateRight; return addBitVectorExpression( - {.kind = expression.kind == Expr::Kind::RotateLeft - ? BitVectorExpressionKind::RotateLeft - : BitVectorExpressionKind::RotateRight, + {.kind = kind, .width = program.bitVectorExpressions[operand].width, .operand = operand, - .distance = distance}); + .distance = *distance}); } [[nodiscard]] FailureOr @@ -2243,6 +2670,48 @@ class SemanticAnalyzer { .type = ScalarType::Uint, .bitVector = bitVector}); } + if (expression.kind == Expr::Kind::BoolCast) { + if (expression.lhs) { + return fail(expression.location, "bool casts do not have a width"); + } + MQT_OQ3_TRY_ASSIGN(condition, analyzeBoolValue(*expression.rhs)); + return addExpression({.kind = ExpressionKind::Condition, + .type = ScalarType::Bool, + .condition = condition}); + } + if (expression.kind == Expr::Kind::IntCast || + expression.kind == Expr::Kind::UintCast) { + MQT_OQ3_TRY_ASSIGN( + width, bitVectorCastWidth(expression.lhs, expression.location)); + const auto target = expression.kind == Expr::Kind::IntCast + ? ScalarType::Int + : ScalarType::Uint; + if (isBitVectorExpression(*expression.rhs)) { + MQT_OQ3_TRY_ASSIGN(bitVector, + analyzeBitVectorExpression(*expression.rhs)); + if (program.bitVectorExpressions[bitVector].width != width) { + return fail( + expression.location, + "bit-register cast width must match the bit-register width"); + } + return addExpression({.kind = ExpressionKind::BitVectorCast, + .type = target, + .bitVector = bitVector, + .integerWidth = static_cast(width)}); + } + MQT_OQ3_TRY_ASSIGN(operand, analyzeExpression(*expression.rhs)); + return addExpression( + {.kind = ExpressionKind::Cast, + .type = target, + .lhs = operand, + .integerWidth = expression.lhs ? static_cast(width) : 0}); + } + if (expressionProducesBool(syntaxId)) { + MQT_OQ3_TRY_ASSIGN(condition, analyzeCondition(syntaxId)); + return addExpression({.kind = ExpressionKind::Condition, + .type = ScalarType::Bool, + .condition = condition}); + } if (expression.kind == Expr::Kind::Identifier) { const auto* symbol = lookup(expression.identifier); if (symbol == nullptr) { @@ -2266,11 +2735,18 @@ class SemanticAnalyzer { } return addExpression({.kind = ExpressionKind::Variable, .type = symbol->type, - .variable = symbol->id}); + .variable = symbol->id, + .integerWidth = symbol->integerWidth}); } auto kind = ExpressionKind::Constant; switch (expression.kind) { + case Expr::Kind::IntCast: + case Expr::Kind::UintCast: + case Expr::Kind::BoolCast: + llvm_unreachable("handled cast"); + case Expr::Kind::BitCast: + return fail(expression.location, "expected a scalar, not a bit register"); case Expr::Kind::AngleCast: return fail(expression.location, "runtime angle conversions are not supported"); @@ -2278,10 +2754,8 @@ class SemanticAnalyzer { kind = ExpressionKind::Negate; break; case Expr::Kind::BitNot: - return fail( - expression.location, - "bitwise operators require explicitly sized uint, bit, or angle " - "operands, which are not supported yet"); + kind = ExpressionKind::BitNot; + break; case Expr::Kind::Add: kind = ExpressionKind::Add; break; @@ -2303,14 +2777,20 @@ class SemanticAnalyzer { kind = ExpressionKind::Power; break; case Expr::Kind::BitAnd: + kind = ExpressionKind::BitAnd; + break; case Expr::Kind::BitOr: + kind = ExpressionKind::BitOr; + break; case Expr::Kind::BitXor: + kind = ExpressionKind::BitXor; + break; case Expr::Kind::ShiftLeft: + kind = ExpressionKind::ShiftLeft; + break; case Expr::Kind::ShiftRight: - return fail( - expression.location, - "bitwise operators require explicitly sized uint, bit, or angle " - "operands, which are not supported yet"); + kind = ExpressionKind::ShiftRight; + break; case Expr::Kind::RotateLeft: case Expr::Kind::RotateRight: return fail(expression.location, @@ -2384,6 +2864,77 @@ class SemanticAnalyzer { "arithmetic operators require numeric operands"); } + const bool shift = + kind == ExpressionKind::ShiftLeft || kind == ExpressionKind::ShiftRight; + const bool bitwise = shift || kind == ExpressionKind::BitNot || + kind == ExpressionKind::BitAnd || + kind == ExpressionKind::BitOr || + kind == ExpressionKind::BitXor; + if (bitwise) { + const auto width = lhsType == ScalarType::Uint + ? (program.expressions[lhs].integerWidth != 0 + ? program.expressions[lhs].integerWidth + : 64) + : rhs && *rhsType == ScalarType::Uint + ? (program.expressions[*rhs].integerWidth != 0 + ? program.expressions[*rhs].integerWidth + : 64) + : 64; + const auto convertLiteral = [&](SyntaxExpressionId syntaxId, + ExpressionId& id) -> LogicalResult { + if (program.expressions[id].kind != ExpressionKind::Constant) { + return success(); + } + MQT_OQ3_TRY_ASSIGN(constant, evaluateConstant(syntaxId)); + if (coerceUnsignedConstant(constant, width)) { + id = addConstant(constant); + } + return success(); + }; + if (failed(convertLiteral(*expression.lhs, lhs)) || + (rhs && !shift && failed(convertLiteral(*expression.rhs, *rhs)))) { + return failure(); + } + lhsType = program.expressions[lhs].type; + rhsType = rhs ? std::optional(program.expressions[*rhs].type) + : std::nullopt; + if (lhsType != ScalarType::Uint || width == 0 || + (rhs && + ((!shift && *rhsType != ScalarType::Uint) || !isInteger(*rhsType) || + (!shift && (program.expressions[*rhs].integerWidth != 0 + ? program.expressions[*rhs].integerWidth + : 64) != width)))) { + return fail(expression.location, + "bitwise operands require matching explicitly sized uint"); + } + if (shift && *rhsType == ScalarType::Int && + (program.expressions[*rhs].kind != ExpressionKind::Constant || + std::get(program.expressions[*rhs].constant) < 0)) { + return fail(expression.location, + "runtime shift distance must be unsigned"); + } + return addExpression({.kind = kind, + .type = lhsType, + .lhs = lhs, + .rhs = rhs.value_or(0), + .integerWidth = width}); + } + /// Integer arithmetic applies machine-width promotion before computation. + if (isInteger(lhsType) && program.expressions[lhs].integerWidth < 64 && + program.expressions[lhs].integerWidth != 0) { + MQT_OQ3_TRY_ASSIGN( + promoted, castExpression(lhs, ScalarType::Int, expression.location)); + lhs = promoted; + lhsType = ScalarType::Int; + } + if (rhs && isInteger(*rhsType) && + program.expressions[*rhs].integerWidth < 64 && + program.expressions[*rhs].integerWidth != 0) { + MQT_OQ3_TRY_ASSIGN( + promoted, castExpression(*rhs, ScalarType::Int, expression.location)); + *rhs = promoted; + rhsType = ScalarType::Int; + } const bool inverseTrig = kind == ExpressionKind::ArcCos || kind == ExpressionKind::ArcSin || kind == ExpressionKind::ArcTan; @@ -2704,6 +3255,11 @@ class SemanticAnalyzer { return fail(location, "outputs must be declared at global scope"); } const auto type = scalarType(declaration.kind); + unsigned integerWidth = 0; + if (isInteger(type) && declaration.size) { + MQT_OQ3_TRY_ASSIGN(width, bitVectorCastWidth(declaration.size, location)); + integerWidth = static_cast(width); + } if (type == ScalarType::Angle) { if (declaration.output) { return fail(location, "angle outputs are not supported"); @@ -2736,20 +3292,39 @@ class SemanticAnalyzer { evaluateConstant(*declaration.initializer)); MQT_OQ3_TRY_ASSIGN(constant, promoteConstInitializer(initializer, type, location)); - return declare( - location, declaration.identifier, - {.kind = SymbolKind::Constant, .type = type, .constant = constant}); + if (integerWidth != 0) { + const auto bits = + type == ScalarType::Int + ? static_cast(std::get(constant.value)) + : std::get(constant.value); + const auto narrowed = llvm::APInt(64, bits).trunc(integerWidth); + if (type == ScalarType::Int) { + constant.value = narrowed.getSExtValue(); + } else { + constant.value = narrowed.getZExtValue(); + } + constant.integerWidth = integerWidth; + } + return declare(location, declaration.identifier, + {.kind = SymbolKind::Constant, + .type = type, + .constant = constant, + .integerWidth = integerWidth}); } const auto id = static_cast(program.scalars.size()); program.scalars.push_back({.type = type, + .integerWidth = integerWidth, .name = declaration.identifier.str(), .location = getSourceLocation(location)}); initializedScalars.push_back(false); scalarGenerations.push_back(0); affineScalarValues.emplace_back(); if (failed(declare(location, declaration.identifier, - {.kind = SymbolKind::Scalar, .type = type, .id = id}))) { + {.kind = SymbolKind::Scalar, + .type = type, + .id = id, + .integerWidth = integerWidth}))) { return failure(); } if (global) { @@ -2772,7 +3347,8 @@ class SemanticAnalyzer { convertedInitializer, castExpression( initializer, type, - syntax.expressions[*declaration.initializer].location)); + syntax.expressions[*declaration.initializer].location, + integerWidth)); typed.initializer = convertedInitializer; if (buildAffineForm(convertedInitializer)) { affineScalarValues[id] = @@ -2821,7 +3397,8 @@ class SemanticAnalyzer { MQT_OQ3_TRY_ASSIGN( convertedValue, castExpression(value, symbol->type, - syntax.expressions[assignment.value].location)); + syntax.expressions[assignment.value].location, + symbol->integerWidth)); typed.value = convertedValue; affineScalarValues[symbol->id] = buildAffineForm(convertedValue) @@ -2840,23 +3417,10 @@ class SemanticAnalyzer { "cannot assign to '" + assignment.target.identifier + "'"); } const auto targetReg = static_cast(symbol->id); - const auto& value = syntax.expressions[assignment.value]; - const auto* valueSymbol = value.kind == Expr::Kind::Identifier - ? lookup(value.identifier) - : nullptr; - const bool bitVectorValue = - value.kind == Expr::Kind::RotateLeft || - value.kind == Expr::Kind::RotateRight || - (valueSymbol != nullptr && valueSymbol->kind == SymbolKind::Register && - program.registers[valueSymbol->id].kind == RegisterKind::Bit && - !program.registers[valueSymbol->id].isScalar); - if (!assignment.target.index && bitVectorValue) { - MQT_OQ3_TRY_ASSIGN(bitVector, - analyzeBitVectorExpression(assignment.value)); - if (program.bitVectorExpressions[bitVector].width != - program.registers[targetReg].width) { - return fail(location, "bit-register assignment widths must match"); - } + if (!assignment.target.index && !program.registers[targetReg].isScalar) { + MQT_OQ3_TRY_ASSIGN( + bitVector, analyzeBitVectorExpression( + assignment.value, program.registers[targetReg].width)); for (uint64_t bit = 0; bit < program.registers[targetReg].width; ++bit) { markBitInitialized({.reg = targetReg, .index = bit}); } @@ -2931,11 +3495,6 @@ class SemanticAnalyzer { addStatement(location, DeclarationStatement{.reg = id})); destination.push_back(statement); if (!isQubit && initializer) { - if (width != 1) { - return fail( - location, - "bit expression initializers require a scalar bit declaration"); - } return analyzeAssignment( location, SyntaxAssignment{.target = @@ -3620,6 +4179,43 @@ class SemanticAnalyzer { return addStatement(location, std::move(result)); } + [[nodiscard]] static ComparisonKind comparisonKind(const Expr::Kind kind) { + switch (kind) { + case Expr::Kind::Equal: + return ComparisonKind::Equal; + case Expr::Kind::NotEqual: + return ComparisonKind::NotEqual; + case Expr::Kind::Less: + return ComparisonKind::Less; + case Expr::Kind::LessEqual: + return ComparisonKind::LessEqual; + case Expr::Kind::Greater: + return ComparisonKind::Greater; + case Expr::Kind::GreaterEqual: + return ComparisonKind::GreaterEqual; + default: + llvm_unreachable("not a comparison expression"); + } + } + + [[nodiscard]] static ComparisonKind + swapComparison(const ComparisonKind comparison) { + switch (comparison) { + case ComparisonKind::Equal: + case ComparisonKind::NotEqual: + return comparison; + case ComparisonKind::Less: + return ComparisonKind::Greater; + case ComparisonKind::LessEqual: + return ComparisonKind::GreaterEqual; + case ComparisonKind::Greater: + return ComparisonKind::Less; + case ComparisonKind::GreaterEqual: + return ComparisonKind::LessEqual; + } + llvm_unreachable("unknown comparison"); + } + [[nodiscard]] FailureOr analyzeCondition(const SyntaxExpressionId syntaxId) { const auto& condition = syntax.expressions[syntaxId]; @@ -3635,6 +4231,14 @@ class SemanticAnalyzer { return addCondition(std::move(typed)); } switch (condition.kind) { + case Expr::Kind::BoolCast: + if (condition.lhs) { + return fail(condition.location, "bool casts do not have a width"); + } + return analyzeBoolValue(*condition.rhs); + case Expr::Kind::BitCast: + return fail(condition.location, + "bit registers require an explicit comparison"); case Expr::Kind::Identifier: { const auto* symbol = lookup(condition.identifier); if (symbol == nullptr) { @@ -3707,18 +4311,41 @@ class SemanticAnalyzer { case Expr::Kind::LessEqual: case Expr::Kind::Greater: case Expr::Kind::GreaterEqual: { + typed.comparison = comparisonKind(condition.kind); const auto& lhsSyntax = syntax.expressions[*condition.lhs]; + const auto& rhsSyntax = syntax.expressions[*condition.rhs]; const auto* lhsSymbol = lhsSyntax.kind == Expr::Kind::Identifier ? lookup(lhsSyntax.identifier) : nullptr; - if ((!program.openQASM2 || condition.kind == Expr::Kind::Equal) && - lhsSymbol != nullptr && lhsSymbol->kind == SymbolKind::Register && - program.registers[lhsSymbol->id].kind == RegisterKind::Bit && - isConstantExpression(*condition.rhs)) { - const auto& rhsSyntax = syntax.expressions[*condition.rhs]; - MQT_OQ3_TRY_ASSIGN(bits, - resolveBits({.location = lhsSyntax.location, - .identifier = lhsSyntax.identifier})); + const auto* registerSyntax = &lhsSyntax; + const auto* constantSyntax = &rhsSyntax; + auto constantSyntaxId = *condition.rhs; + auto registerComparison = typed.comparison; + const auto* registerSymbol = lhsSymbol; + bool directRegisterComparison = + registerSymbol != nullptr && + registerSymbol->kind == SymbolKind::Register && + program.registers[registerSymbol->id].kind == RegisterKind::Bit && + isConstantExpression(constantSyntaxId); + if (!directRegisterComparison) { + const auto* rhsSymbol = rhsSyntax.kind == Expr::Kind::Identifier + ? lookup(rhsSyntax.identifier) + : nullptr; + if (rhsSymbol != nullptr && rhsSymbol->kind == SymbolKind::Register && + program.registers[rhsSymbol->id].kind == RegisterKind::Bit && + isConstantExpression(*condition.lhs)) { + registerSyntax = &rhsSyntax; + constantSyntax = &lhsSyntax; + constantSyntaxId = *condition.lhs; + registerComparison = swapComparison(registerComparison); + registerSymbol = rhsSymbol; + directRegisterComparison = true; + } + } + if (directRegisterComparison) { + MQT_OQ3_TRY_ASSIGN( + bits, resolveBits({.location = registerSyntax->location, + .identifier = registerSyntax->identifier})); if (!program.openQASM2) { for (const auto& bit : bits) { if (failed(ensureBitInitialized(bit, condition.location))) { @@ -3727,10 +4354,10 @@ class SemanticAnalyzer { } } llvm::APInt expectedBits; - if (rhsSyntax.kind == Expr::Kind::Int && - !rhsSyntax.wideInteger.empty()) { + if (constantSyntax->kind == Expr::Kind::Int && + !constantSyntax->wideInteger.empty()) { llvm::SmallString<64> digits; - for (const char value : rhsSyntax.wideInteger) { + for (const char value : constantSyntax->wideInteger) { if (value != '_') { digits.push_back(value); } @@ -3739,7 +4366,7 @@ class SemanticAnalyzer { std::max(bits.size(), digits.size() * 4)); expectedBits = llvm::APInt(width, digits, /*radix=*/10); } else { - MQT_OQ3_TRY_ASSIGN(expected, evaluateConstant(*condition.rhs)); + MQT_OQ3_TRY_ASSIGN(expected, evaluateConstant(constantSyntaxId)); if (!isInteger(expected.type) || (expected.type == ScalarType::Int && std::get(expected.value) < 0)) { @@ -3754,9 +4381,9 @@ class SemanticAnalyzer { expectedBits = llvm::APInt(/*numBits=*/64, expectedValue); } if (expectedBits.getActiveBits() > bits.size()) { - const bool result = condition.kind == Expr::Kind::NotEqual || - condition.kind == Expr::Kind::Less || - condition.kind == Expr::Kind::LessEqual; + const bool result = registerComparison == ComparisonKind::NotEqual || + registerComparison == ComparisonKind::Less || + registerComparison == ComparisonKind::LessEqual; return addCondition( {.kind = ConditionKind::Literal, .location = getSourceLocation(condition.location), @@ -3769,26 +4396,35 @@ class SemanticAnalyzer { } return addCondition({.kind = ConditionKind::RegisterComparison, .location = getSourceLocation(condition.location), - .reg = lhsSymbol->id, + .reg = registerSymbol->id, .expected = std::move(expectedBits), - .comparison = [&] { - switch (condition.kind) { - case Expr::Kind::Equal: - return ComparisonKind::Equal; - case Expr::Kind::NotEqual: - return ComparisonKind::NotEqual; - case Expr::Kind::Less: - return ComparisonKind::Less; - case Expr::Kind::LessEqual: - return ComparisonKind::LessEqual; - case Expr::Kind::Greater: - return ComparisonKind::Greater; - case Expr::Kind::GreaterEqual: - return ComparisonKind::GreaterEqual; - default: - llvm_unreachable("not a comparison"); - } - }()}); + .comparison = registerComparison}); + } + if (isBitVectorExpression(*condition.lhs) || + isBitVectorExpression(*condition.rhs)) { + std::optional lhs; + std::optional rhs; + uint64_t width = 0; + if (isBitVectorExpression(*condition.lhs)) { + MQT_OQ3_TRY_ASSIGN(value, analyzeBitVectorExpression(*condition.lhs)); + lhs = value; + width = program.bitVectorExpressions[*lhs].width; + MQT_OQ3_TRY_ASSIGN(other, + analyzeBitVectorExpression(*condition.rhs, width)); + rhs = other; + } else { + MQT_OQ3_TRY_ASSIGN(value, analyzeBitVectorExpression(*condition.rhs)); + rhs = value; + width = program.bitVectorExpressions[*rhs].width; + MQT_OQ3_TRY_ASSIGN(other, + analyzeBitVectorExpression(*condition.lhs, width)); + lhs = other; + } + return addCondition({.kind = ConditionKind::BitVectorComparison, + .location = getSourceLocation(condition.location), + .bitVectorComparisonLhs = *lhs, + .bitVectorComparisonRhs = *rhs, + .comparison = typed.comparison}); } typed.kind = ConditionKind::Comparison; MQT_OQ3_TRY_ASSIGN(comparisonLhs, analyzeExpression(*condition.lhs)); @@ -3814,7 +4450,16 @@ class SemanticAnalyzer { } else if (lhsType == ScalarType::Float || rhsType == ScalarType::Float) { comparisonType = ScalarType::Float; - } else if (lhsType == ScalarType::Uint || rhsType == ScalarType::Uint) { + } else if ((lhsType == ScalarType::Uint && + (program.expressions[typed.comparisonLhs].integerWidth == + 0 || + program.expressions[typed.comparisonLhs].integerWidth == + 64)) || + (rhsType == ScalarType::Uint && + (program.expressions[typed.comparisonRhs].integerWidth == + 0 || + program.expressions[typed.comparisonRhs].integerWidth == + 64))) { comparisonType = ScalarType::Uint; } MQT_OQ3_TRY_ASSIGN( @@ -3828,33 +4473,13 @@ class SemanticAnalyzer { typed.comparisonLhs = convertedLhs; typed.comparisonRhs = convertedRhs; } - switch (condition.kind) { - case Expr::Kind::Equal: - typed.comparison = ComparisonKind::Equal; - break; - case Expr::Kind::NotEqual: - typed.comparison = ComparisonKind::NotEqual; - break; - case Expr::Kind::Less: - typed.comparison = ComparisonKind::Less; - break; - case Expr::Kind::LessEqual: - typed.comparison = ComparisonKind::LessEqual; - break; - case Expr::Kind::Greater: - typed.comparison = ComparisonKind::Greater; - break; - case Expr::Kind::GreaterEqual: - typed.comparison = ComparisonKind::GreaterEqual; - break; - default: - llvm_unreachable("not a comparison expression"); - } break; } case Expr::Kind::Int: case Expr::Kind::Float: case Expr::Kind::Bool: + case Expr::Kind::IntCast: + case Expr::Kind::UintCast: case Expr::Kind::AngleCast: case Expr::Kind::Neg: case Expr::Kind::BitNot: diff --git a/mlir/unittests/Conversion/CBitToMemRef/test_cbit_to_memref.cpp b/mlir/unittests/Conversion/CBitToMemRef/test_cbit_to_memref.cpp index 8ed0ac25e2..67318cd8b8 100644 --- a/mlir/unittests/Conversion/CBitToMemRef/test_cbit_to_memref.cpp +++ b/mlir/unittests/Conversion/CBitToMemRef/test_cbit_to_memref.cpp @@ -137,15 +137,40 @@ TEST_F(CBitToMemRefTest, LargeZeroInitializationProducesBoundedIR) { TEST_F(CBitToMemRefTest, LowersRegisterComparisons) { auto moduleOp = convert(R"mlir( module { - func.func @main() -> (i1, i1, i1, i1, i1, i1) { + func.func @main() -> (i1, i1, i1, i1, i1, i1, i1, i1, i1, i1) { %reg = cbit.alloc(#cbit.init) : !cbit.reg<3> - %eq = cbit.cmp eq, %reg, 5 : i3 : !cbit.reg<3> - %ne = cbit.cmp ne, %reg, 5 : i3 : !cbit.reg<3> - %ult = cbit.cmp ult, %reg, 5 : i3 : !cbit.reg<3> - %ule = cbit.cmp ule, %reg, 5 : i3 : !cbit.reg<3> - %ugt = cbit.cmp ugt, %reg, 5 : i3 : !cbit.reg<3> - %uge = cbit.cmp uge, %reg, 5 : i3 : !cbit.reg<3> - return %eq, %ne, %ult, %ule, %ugt, %uge : i1, i1, i1, i1, i1, i1 + %eq_read = cbit.read %reg : !cbit.reg<3> -> i3 + %eq_rhs = arith.constant 5 : i3 + %eq = arith.cmpi eq, %eq_read, %eq_rhs : i3 + %ne_read = cbit.read %reg : !cbit.reg<3> -> i3 + %ne_rhs = arith.constant 5 : i3 + %ne = arith.cmpi ne, %ne_read, %ne_rhs : i3 + %ult_read = cbit.read %reg : !cbit.reg<3> -> i3 + %ult_rhs = arith.constant 5 : i3 + %ult = arith.cmpi ult, %ult_read, %ult_rhs : i3 + %ule_read = cbit.read %reg : !cbit.reg<3> -> i3 + %ule_rhs = arith.constant 5 : i3 + %ule = arith.cmpi ule, %ule_read, %ule_rhs : i3 + %ugt_read = cbit.read %reg : !cbit.reg<3> -> i3 + %ugt_rhs = arith.constant 5 : i3 + %ugt = arith.cmpi ugt, %ugt_read, %ugt_rhs : i3 + %uge_read = cbit.read %reg : !cbit.reg<3> -> i3 + %uge_rhs = arith.constant 5 : i3 + %uge = arith.cmpi uge, %uge_read, %uge_rhs : i3 + %slt_read = cbit.read %reg : !cbit.reg<3> -> i3 + %slt_rhs = arith.constant 5 : i3 + %slt = arith.cmpi slt, %slt_read, %slt_rhs : i3 + %sle_read = cbit.read %reg : !cbit.reg<3> -> i3 + %sle_rhs = arith.constant 5 : i3 + %sle = arith.cmpi sle, %sle_read, %sle_rhs : i3 + %sgt_read = cbit.read %reg : !cbit.reg<3> -> i3 + %sgt_rhs = arith.constant 5 : i3 + %sgt = arith.cmpi sgt, %sgt_read, %sgt_rhs : i3 + %sge_read = cbit.read %reg : !cbit.reg<3> -> i3 + %sge_rhs = arith.constant 5 : i3 + %sge = arith.cmpi sge, %sge_read, %sge_rhs : i3 + return %eq, %ne, %ult, %ule, %ugt, %uge, %slt, %sle, %sgt, %sge + : i1, i1, i1, i1, i1, i1, i1, i1, i1, i1 } } )mlir"); @@ -159,7 +184,35 @@ TEST_F(CBitToMemRefTest, LowersRegisterComparisons) { EXPECT_FALSE(containsCBit); size_t loads = 0; moduleOp->walk([&](memref::LoadOp) { ++loads; }); - EXPECT_EQ(loads, 18); + EXPECT_GT(loads, 0); +} + +TEST_F(CBitToMemRefTest, LowersWholeRegisterReadsAndWrites) { + auto moduleOp = convert(R"mlir( + module { + func.func @main() -> i3 { + %reg = cbit.alloc(#cbit.init) : !cbit.reg<3> + %value = cbit.read %reg : !cbit.reg<3> -> i3 + cbit.write %value, %reg : i3, !cbit.reg<3> + return %value : i3 + } + } + )mlir"); + ASSERT_TRUE(moduleOp); + EXPECT_TRUE(succeeded(verify(*moduleOp))); + + size_t loads = 0; + size_t stores = 0; + bool containsRead = false; + bool containsWrite = false; + moduleOp->walk([&](memref::LoadOp) { ++loads; }); + moduleOp->walk([&](memref::StoreOp) { ++stores; }); + moduleOp->walk([&](cbit::ReadOp) { containsRead = true; }); + moduleOp->walk([&](cbit::WriteOp) { containsWrite = true; }); + EXPECT_GT(loads, 0); + EXPECT_GT(stores, 0); + EXPECT_FALSE(containsRead); + EXPECT_FALSE(containsWrite); } TEST_F(CBitToMemRefTest, ConvertsFunctionSignaturesCallsAndReturns) { diff --git a/mlir/unittests/Conversion/JeffRoundTrip/CMakeLists.txt b/mlir/unittests/Conversion/JeffRoundTrip/CMakeLists.txt index 5429ada62c..957c1c7357 100644 --- a/mlir/unittests/Conversion/JeffRoundTrip/CMakeLists.txt +++ b/mlir/unittests/Conversion/JeffRoundTrip/CMakeLists.txt @@ -16,6 +16,7 @@ target_link_libraries( MLIRParser MLIRPass MLIRQCOPrograms + MLIRQCODDFunctionality MLIRQCOProgramBuilder MLIRSupportMQT MLIRTransforms diff --git a/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp b/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp index 6d0dd770c8..97d9dd15d0 100644 --- a/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp +++ b/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp @@ -19,6 +19,7 @@ #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Utils/DDFunctionality.h" #include "mlir/Support/Passes.h" #include "qco_programs.h" @@ -32,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -517,6 +519,184 @@ TEST(JeffRoundTripRegressionTest, ConvertsJeffBitArraysDirectlyToCBit) { EXPECT_FALSE(hasI1Tensor); } +TEST(JeffRoundTripRegressionTest, PreservesLiveOldArrayValues) { + MLIRContext context; + context.loadDialect(); + auto program = parseSourceString(R"mlir(module { + func.func @main() -> !cbit.reg<1> attributes {mqt.entry_point} { + %q = qco.alloc : !qco.qubit + %c = cbit.alloc(#cbit.init) : !cbit.reg<1> + %index = arith.constant 0 : index + %true = arith.constant true + cbit.store %true, %c[%index] : !cbit.reg<1> + qco.sink %q : !qco.qubit + return %c : !cbit.reg<1> + } + })mlir", + &context); + ASSERT_TRUE(program); + ASSERT_TRUE(succeeded(convertQCOToJeff(*program))); + auto main = program->lookupSymbol("main"); + auto returned = cast(main.getBody().front().getTerminator()); + auto original = *main.getOps().begin(); + auto updated = returned.getOperand(0); + returned->setOperands({original.getResult(), updated}); + main.setFunctionType( + FunctionType::get(&context, {}, {updated.getType(), updated.getType()})); + auto bytes = serialize(*program); + program = deserialize(&context, bytes); + ASSERT_TRUE(program); + ASSERT_TRUE(succeeded(convertJeffToQCO(*program))); + ASSERT_TRUE(succeeded(verify(*program))); + auto histogram = + qco::sample(program->lookupSymbol("main"), 1, 1); + ASSERT_TRUE(succeeded(histogram)); + EXPECT_EQ(histogram->at("10"), 1); +} + +TEST(JeffRoundTripRegressionTest, ConvertsSignedIndexComparison) { + MLIRContext context; + context.loadDialect(); + auto program = parseSourceString(R"mlir(module { + func.func @main(%lhs: index, %rhs: index) -> i1 attributes {mqt.entry_point} { + %q = qco.alloc : !qco.qubit + %result = arith.cmpi slt, %lhs, %rhs : index + qco.sink %q : !qco.qubit + return %result : i1 + } + })mlir", + &context); + ASSERT_TRUE(program); + ASSERT_TRUE(succeeded(convertQCOToJeff(*program))); + EXPECT_TRUE(succeeded(verify(*program))); +} + +TEST(JeffRoundTripRegressionTest, PreservesPromotedSignedMinMax) { + MLIRContext context; + context.loadDialect(); + auto program = parseSourceString(R"mlir(module { + func.func @main() -> (!cbit.reg<3>, !cbit.reg<3>) attributes {mqt.entry_point} { + %q = qco.alloc : !qco.qubit + %source = cbit.alloc(#cbit.init) : !cbit.reg<3> + %negative = arith.constant -3 : i3 + cbit.write %negative, %source : i3, !cbit.reg<3> + %value = cbit.read %source : !cbit.reg<3> -> i3 + %positive = arith.constant 2 : i3 + %minimum = arith.minsi %value, %positive : i3 + %maximum = arith.maxsi %value, %positive : i3 + %min = cbit.alloc(#cbit.init) : !cbit.reg<3> + %max = cbit.alloc(#cbit.init) : !cbit.reg<3> + cbit.write %minimum, %min : i3, !cbit.reg<3> + cbit.write %maximum, %max : i3, !cbit.reg<3> + qco.sink %q : !qco.qubit + return %min, %max : !cbit.reg<3>, !cbit.reg<3> + } + })mlir", + &context); + ASSERT_TRUE(program); + ASSERT_TRUE(succeeded(convertQCOToJeff(*program))); + auto bytes = serialize(*program); + program = deserialize(&context, bytes); + ASSERT_TRUE(program); + ASSERT_TRUE(succeeded(convertJeffToQCO(*program))); + auto histogram = + qco::sample(program->lookupSymbol("main"), 1, 1); + ASSERT_TRUE(succeeded(histogram)); + EXPECT_EQ(histogram->at("010101"), 1); +} + +TEST(JeffRoundTripRegressionTest, RejectsPromotedUnsupportedMath) { + MLIRContext context; + context.loadDialect(); + for (const auto* const expression : + {"math.absi %value", "math.ipowi %value, %value"}) { + auto program = parseSourceString( + std::string("module { func.func @main(%value: i3) -> i3 " + "attributes {mqt.entry_point} { %result = ") + + expression + " : i3 return %result : i3 }}", + &context); + ASSERT_TRUE(program); + EXPECT_TRUE(failed(convertQCOToJeff(*program))); + } +} + +TEST(JeffRoundTripRegressionTest, RejectsEffectsInIntegerSelection) { + MLIRContext context; + context.loadDialect(); + auto program = parseSourceString(R"mlir( + module attributes {jeff.entrypoint = 0 : ui16, jeff.strings = ["main"]} { + func.func @main(%select: i1, %value: i8) -> i8 { + %q = jeff.qubit_alloc : !jeff.qubit + %result = jeff.switch (%select, %q, %value) : (i1, !jeff.qubit, i8) -> (i8) + case 0 args(%inner, %integer) { + jeff.qubit_free %inner : !jeff.qubit + jeff.yield %integer : i8 + } + case 1 args(%inner, %integer) { + jeff.qubit_free %inner : !jeff.qubit + jeff.yield %integer : i8 + } + default args(%inner, %integer) { + jeff.qubit_free %inner : !jeff.qubit + jeff.yield %integer : i8 + } + return %result : i8 + } + })mlir", + &context); + ASSERT_TRUE(program); + EXPECT_TRUE(failed(convertJeffToQCO(*program))); +} + +TEST(JeffRoundTripRegressionTest, RejectsLiveOldArrayAcrossSwitchRegions) { + MLIRContext context; + context.loadDialect(); + constexpr llvm::StringLiteral source = R"mlir( + module attributes {jeff.entrypoint = 0 : ui16, jeff.strings = ["main"]} { + func.func @main(%select: i1) -> (tensor<1xi1>, tensor<1xi1>) { + %length = jeff.int_const32(1) : i32 + %index = jeff.int_const32(0) : i32 + %bit = jeff.int_const1(true) : i1 + %old = jeff.int_array_zero(%length) : tensor<1xi1> + %result = jeff.switch (%select, %old, %index, %bit) + : (i1, tensor<1xi1>, i32, i1) -> (tensor<1xi1>) + case 0 args(%array, %idx, %value) { + jeff.yield %array : tensor<1xi1> + } + case 1 args(%array, %idx, %value) { + %new = jeff.int_array_set_index(%idx) %array %value + : i32, tensor<1xi1>, i1 -> tensor<1xi1> + jeff.yield %new : tensor<1xi1> + } + default args(%array, %idx, %value) { + jeff.yield %array : tensor<1xi1> + } + return %old, %result : tensor<1xi1>, tensor<1xi1> + } + })mlir"; + for (const bool oldValueOutside : {true, false}) { + auto program = parseSourceString(source, &context); + ASSERT_TRUE(program); + if (!oldValueOutside) { + auto main = program->lookupSymbol("main"); + auto returned = + cast(main.getBody().front().getTerminator()); + returned->setOperand(0, returned.getOperand(1)); + auto selection = *main.getOps().begin(); + auto& branch = selection.getBranches()[1].front(); + branch.getTerminator()->setOperand(0, branch.getArgument(0)); + } + ASSERT_TRUE(succeeded(verify(*program))); + EXPECT_TRUE(failed(convertJeffToQCO(*program))); + } +} + TEST(JeffRoundTripRegressionTest, RejectsClassicalIfResultsPrecisely) { DialectRegistry registry; registry.insertwalk([&](cbit::CompareOp) { retainsComparison = true; }); + module->walk([&](arith::CmpIOp) { retainsComparison = true; }); EXPECT_FALSE(retainsComparison); } +TEST(QCToQIRAdaptiveNativeTest, RejectsRegisterCallsAtTheSharedBoundary) { + const auto sources = { + R"mlir(module { + func.func private @callee(!cbit.reg<1>) + func.func @main() attributes {mqt.entry_point} { + %c = cbit.alloc(#cbit.init) : !cbit.reg<1> + func.call @callee(%c) : (!cbit.reg<1>) -> () + return + } + })mlir", + R"mlir(module { + func.func private @callee() -> !cbit.reg<1> + func.func @main() -> !cbit.reg<1> attributes {mqt.entry_point} { + %c = func.call @callee() : () -> !cbit.reg<1> + return %c : !cbit.reg<1> + } + })mlir", + R"mlir(module { + func.func private @callee(!cbit.reg<1>) + func.func @main() attributes {mqt.entry_point} { + %callee = func.constant @callee : (!cbit.reg<1>) -> () + %c = cbit.alloc(#cbit.init) : !cbit.reg<1> + func.call_indirect %callee(%c) : (!cbit.reg<1>) -> () + return + } + })mlir", + R"mlir(module { + func.func private @callee() -> !cbit.reg<1> + func.func @main() -> !cbit.reg<1> attributes {mqt.entry_point} { + %callee = func.constant @callee : () -> !cbit.reg<1> + %c = func.call_indirect %callee() : () -> !cbit.reg<1> + return %c : !cbit.reg<1> + } + })mlir"}; + for (const auto* source : sources) { + SCOPED_TRACE(source); + MLIRContext context; + context.loadDialect(); + auto module = parseSourceString(source, &context); + ASSERT_TRUE(module); + bool diagnosed = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + std::string message; + llvm::raw_string_ostream stream(message); + diagnostic.print(stream); + diagnosed |= StringRef(message).contains( + "does not support CBit registers in calls"); + return success(); + }); + EXPECT_TRUE(failed(runQCToQIRAdaptiveConversionSimple(*module))); + EXPECT_TRUE(diagnosed); + } +} + +TEST(QCToQIRAdaptiveNativeTest, PreservesScalarCalls) { + MLIRContext context; + context.loadDialect(); + auto module = parseSourceString(R"mlir(module { + func.func private @callee(i64) -> i64 + func.func @main() attributes {mqt.entry_point} { + %zero = arith.constant 0 : i64 + %callee = func.constant @callee : (i64) -> i64 + %direct = func.call @callee(%zero) : (i64) -> i64 + %indirect = func.call_indirect %callee(%direct) : (i64) -> i64 + return + } + })mlir", + &context); + ASSERT_TRUE(module); + EXPECT_TRUE(succeeded(runQCToQIRAdaptiveConversionSimple(*module))); + EXPECT_TRUE(succeeded(verify(*module))); +} + TEST(QCToQIRAdaptiveNativeTest, RejectsMixedClassicalRegisterRepresentations) { MLIRContext context; context.loadDialect(); auto module = parseSourceString(R"mlir( module { - func.func @main() -> (i1, i1, !cbit.reg<1>) attributes {mqt.entry_point} { + func.func @main() -> (i1, i1, i1, !cbit.reg<1>) + attributes {mqt.entry_point} { %true = arith.constant true %c0 = arith.constant 0 : index %returned = cbit.alloc(#cbit.init) : !cbit.reg<1> @@ -301,8 +377,13 @@ TEST(QCToQIRAdaptiveNativeTest, RejectsMixedClassicalRegisterRepresentations) { scf.yield %local : !cbit.reg<1> } %bit = cbit.load %selected[%c0] : !cbit.reg<1> - %matches = cbit.cmp eq, %selected, 0 : i1 : !cbit.reg<1> - return %bit, %matches, %returned : i1, i1, !cbit.reg<1> + %whole = cbit.read %selected : !cbit.reg<1> -> i1 + %matches_read = cbit.read %selected : !cbit.reg<1> -> i1 + %matches_rhs = arith.constant 0 : i1 + %matches = arith.cmpi eq, %matches_read, %matches_rhs : i1 + cbit.write %true, %selected : i1, !cbit.reg<1> + return %bit, %whole, %matches, %returned + : i1, i1, i1, !cbit.reg<1> } } )mlir", @@ -321,7 +402,7 @@ TEST(QCToQIRAdaptiveNativeTest, RejectsMixedClassicalRegisterRepresentations) { return success(); }); EXPECT_TRUE(failed(runQCToQIRAdaptiveConversionSimple(*module))); - EXPECT_EQ(mixedRepresentationDiagnostics, 2); + EXPECT_EQ(mixedRepresentationDiagnostics, 4); } TEST(QCToQIRAdaptiveNativeTest, LowersReturnedRegisterMerge) { @@ -331,7 +412,7 @@ TEST(QCToQIRAdaptiveNativeTest, LowersReturnedRegisterMerge) { LLVM::LLVMDialect, scf::SCFDialect>(); auto module = parseSourceString(R"mlir( module { - func.func @main() -> (i1, i1, !cbit.reg<1>, !cbit.reg<1>) + func.func @main() -> (i1, i1, i1, !cbit.reg<1>, !cbit.reg<1>) attributes {mqt.entry_point} { %true = arith.constant true %c0 = arith.constant 0 : index @@ -343,9 +424,12 @@ TEST(QCToQIRAdaptiveNativeTest, LowersReturnedRegisterMerge) { scf.yield %second : !cbit.reg<1> } %bit = cbit.load %selected[%c0] : !cbit.reg<1> - %matches = cbit.cmp eq, %selected, 0 : i1 : !cbit.reg<1> - return %bit, %matches, %first, %second - : i1, i1, !cbit.reg<1>, !cbit.reg<1> + %whole = cbit.read %selected : !cbit.reg<1> -> i1 + %matches_read = cbit.read %selected : !cbit.reg<1> -> i1 + %matches_rhs = arith.constant 0 : i1 + %matches = arith.cmpi eq, %matches_read, %matches_rhs : i1 + return %bit, %whole, %matches, %first, %second + : i1, i1, i1, !cbit.reg<1>, !cbit.reg<1> } } )mlir", @@ -358,7 +442,46 @@ TEST(QCToQIRAdaptiveNativeTest, LowersReturnedRegisterMerge) { module->walk([&](LLVM::CallOp call) { resultReads += call.getCallee() == qir::QIR_READ_RESULT; }); - EXPECT_EQ(resultReads, 2); + EXPECT_EQ(resultReads, 3); +} + +TEST(QCToQIRAdaptiveNativeTest, RejectsWriteThroughReturnedRegisterMerge) { + MLIRContext context; + context.loadDialect(); + auto module = parseSourceString(R"mlir( + module { + func.func @main() -> (!cbit.reg<1>, !cbit.reg<1>) + attributes {mqt.entry_point} { + %true = arith.constant true + %first = cbit.alloc(#cbit.init) : !cbit.reg<1> + %second = cbit.alloc(#cbit.init) : !cbit.reg<1> + %selected = scf.if %true -> (!cbit.reg<1>) { + scf.yield %first : !cbit.reg<1> + } else { + scf.yield %second : !cbit.reg<1> + } + cbit.write %true, %selected : i1, !cbit.reg<1> + return %first, %second : !cbit.reg<1>, !cbit.reg<1> + } + } + )mlir", + &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + bool sawExpectedDiagnostic = false; + const ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + std::string message; + llvm::raw_string_ostream stream(message); + diagnostic.print(stream); + sawExpectedDiagnostic |= StringRef(message).contains( + "does not support non-measurement writes to returned CBit registers"); + return success(); + }); + EXPECT_TRUE(failed(runQCToQIRAdaptiveConversionSimple(*module))); + EXPECT_TRUE(sawExpectedDiagnostic); } TEST(QCToQIRAdaptiveNativeTest, RejectsMultipleRegisterDestinations) { diff --git a/mlir/unittests/Conversion/QCToQIR/QCToQIRBase/test_qc_to_qir_base.cpp b/mlir/unittests/Conversion/QCToQIR/QCToQIRBase/test_qc_to_qir_base.cpp index e92df59f35..5b62f07fc5 100644 --- a/mlir/unittests/Conversion/QCToQIR/QCToQIRBase/test_qc_to_qir_base.cpp +++ b/mlir/unittests/Conversion/QCToQIR/QCToQIRBase/test_qc_to_qir_base.cpp @@ -159,32 +159,6 @@ TEST(QCToQIRBaseNativeTest, RejectsMultiBlockEntryFunctionWithoutMutation) { EXPECT_EQ(entryPoint.getBlocks().size(), 2); } -TEST(QCToQIRBaseNativeTest, RejectsClassicalRegisterComparisons) { - MLIRContext context; - context.loadDialect(); - qc::QCProgramBuilder builder(&context); - builder.initialize(); - auto reg = builder.allocClassicalBitRegister(1); - auto rhs = builder.getIntegerAttr(builder.getIntegerType(1), 0); - (void)cbit::CompareOp::create(builder, builder.getI1Type(), - cbit::ComparisonPredicate::Equal, reg, rhs); - auto module = builder.finalize(); - ASSERT_TRUE(module); - - bool sawExpectedDiagnostic = false; - ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { - std::string message; - llvm::raw_string_ostream stream(message); - diagnostic.print(stream); - sawExpectedDiagnostic |= StringRef(message).contains( - "QIR Base Profile does not support classical-register comparisons"); - return success(); - }); - EXPECT_TRUE(failed(runQCToQIRBaseConversion(*module))); - EXPECT_TRUE(sawExpectedDiagnostic); -} - TEST(QCToQIRBaseNativeTest, ControlledBarrierDoesNotControlFollowingGate) { expectFollowingXIsUncontrolled( [](qc::QCProgramBuilder& builder, Value control, Value target) { diff --git a/mlir/unittests/Dialect/CBit/IR/test_cbit_ir.cpp b/mlir/unittests/Dialect/CBit/IR/test_cbit_ir.cpp index 6baa759b85..87163af866 100644 --- a/mlir/unittests/Dialect/CBit/IR/test_cbit_ir.cpp +++ b/mlir/unittests/Dialect/CBit/IR/test_cbit_ir.cpp @@ -66,7 +66,8 @@ TEST_F(CBitIRTest, ParsesAndPrintsRegisterOperations) { %reg = cbit.alloc(#cbit.init) {mqt.register_name = "c"} : !cbit.reg<2> cbit.store %false, %reg[%c0] : !cbit.reg<2> %bit = cbit.load %reg[%c0] : !cbit.reg<2> - %matches = cbit.cmp eq, %reg, 1 : i2 : !cbit.reg<2> + %value = cbit.read %reg : !cbit.reg<2> -> i2 + cbit.write %value, %reg : i2, !cbit.reg<2> return %reg : !cbit.reg<2> } } @@ -85,27 +86,41 @@ TEST_F(CBitIRTest, ParsesAndPrintsRegisterOperations) { EXPECT_NE(printed.find("!cbit.reg<2>"), std::string::npos); EXPECT_NE(printed.find("cbit.store"), std::string::npos); EXPECT_NE(printed.find("cbit.load"), std::string::npos); - EXPECT_NE(printed.find("cbit.cmp eq"), std::string::npos); + EXPECT_NE(printed.find("cbit.read"), std::string::npos); + EXPECT_NE(printed.find("cbit.write"), std::string::npos); } -TEST_F(CBitIRTest, RejectsComparisonWidthMismatch) { +TEST_F(CBitIRTest, RejectsReadWidthMismatch) { EXPECT_FALSE(parse(R"mlir( module { func.func @main() { %reg = cbit.alloc(#cbit.init) : !cbit.reg<2> - %matches = cbit.cmp eq, %reg, 1 : i3 : !cbit.reg<2> + %value = cbit.read %reg : !cbit.reg<2> -> i3 return } } )mlir")); } -TEST_F(CBitIRTest, RejectsUnsupportedComparisonWidth) { +TEST_F(CBitIRTest, RejectsUnsignedIntegerType) { EXPECT_FALSE(parse(R"mlir( module { func.func @main() { - %reg = cbit.alloc(#cbit.init) : !cbit.reg<4294967297> - %matches = cbit.cmp eq, %reg, 0 : i1 : !cbit.reg<4294967297> + %reg = cbit.alloc(#cbit.init) : !cbit.reg<2> + %value = "cbit.read"(%reg) : (!cbit.reg<2>) -> ui2 + return + } + } + )mlir")); +} + +TEST_F(CBitIRTest, RejectsWriteWidthMismatch) { + EXPECT_FALSE(parse(R"mlir( + module { + func.func @main() { + %value = arith.constant 0 : i3 + %reg = cbit.alloc(#cbit.init) : !cbit.reg<2> + cbit.write %value, %reg : i3, !cbit.reg<2> return } } @@ -172,7 +187,8 @@ TEST_F(CBitIRTest, ReportsMemoryEffects) { %reg = cbit.alloc(#cbit.init) : !cbit.reg<1> cbit.store %false, %reg[%c0] : !cbit.reg<1> %bit = cbit.load %reg[%c0] : !cbit.reg<1> - %matches = cbit.cmp eq, %reg, 0 : i1 : !cbit.reg<1> + %value = cbit.read %reg : !cbit.reg<1> -> i1 + cbit.write %value, %reg : i1, !cbit.reg<1> return } } @@ -180,18 +196,21 @@ TEST_F(CBitIRTest, ReportsMemoryEffects) { ASSERT_TRUE(moduleOp); cbit::AllocOp alloc; - cbit::CompareOp compare; cbit::LoadOp load; + cbit::ReadOp read; cbit::StoreOp store; + cbit::WriteOp write; moduleOp->walk([&](cbit::AllocOp op) { alloc = op; }); - moduleOp->walk([&](cbit::CompareOp op) { compare = op; }); moduleOp->walk([&](cbit::LoadOp op) { load = op; }); + moduleOp->walk([&](cbit::ReadOp op) { read = op; }); moduleOp->walk([&](cbit::StoreOp op) { store = op; }); + moduleOp->walk([&](cbit::WriteOp op) { write = op; }); ASSERT_NE(alloc.getOperation(), nullptr); - ASSERT_NE(compare.getOperation(), nullptr); ASSERT_NE(load.getOperation(), nullptr); + ASSERT_NE(read.getOperation(), nullptr); ASSERT_NE(store.getOperation(), nullptr); + ASSERT_NE(write.getOperation(), nullptr); SmallVector effects; alloc.getEffects(effects); @@ -205,31 +224,36 @@ TEST_F(CBitIRTest, ReportsMemoryEffects) { EXPECT_EQ(effects.front().getValue(), load.getReg()); effects.clear(); - compare.getEffects(effects); + read.getEffects(effects); ASSERT_EQ(effects.size(), 1); EXPECT_TRUE(isa(effects.front().getEffect())); - EXPECT_EQ(effects.front().getValue(), compare.getReg()); + EXPECT_EQ(effects.front().getValue(), read.getReg()); effects.clear(); store.getEffects(effects); ASSERT_EQ(effects.size(), 1); EXPECT_TRUE(isa(effects.front().getEffect())); EXPECT_EQ(effects.front().getValue(), store.getReg()); + + effects.clear(); + write.getEffects(effects); + ASSERT_EQ(effects.size(), 1); + EXPECT_TRUE(isa(effects.front().getEffect())); + EXPECT_EQ(effects.front().getValue(), write.getReg()); } TEST_F(CBitIRTest, ForwardsStraightLineStoresAndZeroInitialization) { auto moduleOp = parse(R"mlir( module { - func.func @main() -> (i1, i1, i1) { + func.func @main() -> (i1, i1) { %c0 = arith.constant 0 : index %c1 = arith.constant 1 : index %true = arith.constant true %reg = cbit.alloc(#cbit.init) : !cbit.reg<2> %zero = cbit.load %reg[%c0] : !cbit.reg<2> - %matches = cbit.cmp eq, %reg, 0 : i2 : !cbit.reg<2> cbit.store %true, %reg[%c1] : !cbit.reg<2> %stored = cbit.load %reg[%c1] : !cbit.reg<2> - return %zero, %stored, %matches : i1, i1, i1 + return %zero, %stored : i1, i1 } } )mlir"); @@ -246,16 +270,12 @@ TEST_F(CBitIRTest, ForwardsStraightLineStoresAndZeroInitialization) { moduleOp->print(canonicalizedStream); APInt zero; APInt stored; - APInt matches; EXPECT_TRUE(matchPattern(returnOp.getOperand(0), m_ConstantInt(&zero))) << canonicalized; EXPECT_TRUE(matchPattern(returnOp.getOperand(1), m_ConstantInt(&stored))) << canonicalized; - EXPECT_TRUE(matchPattern(returnOp.getOperand(2), m_ConstantInt(&matches))) - << canonicalized; EXPECT_TRUE(zero.isZero()); EXPECT_TRUE(stored.isOne()); - EXPECT_TRUE(matches.isOne()); } TEST_F(CBitIRTest, DoesNotForwardAcrossAnAmbiguousStore) { @@ -267,7 +287,9 @@ TEST_F(CBitIRTest, DoesNotForwardAcrossAnAmbiguousStore) { %reg = cbit.alloc(#cbit.init) : !cbit.reg<2> cbit.store %true, %reg[%dynamic] : !cbit.reg<2> %value = cbit.load %reg[%c0] : !cbit.reg<2> - %matches = cbit.cmp eq, %reg, 0 : i2 : !cbit.reg<2> + %snapshot = cbit.read %reg : !cbit.reg<2> -> i2 + %expected = arith.constant 0 : i2 + %matches = arith.cmpi eq, %snapshot, %expected : i2 return %value, %matches : i1, i1 } } @@ -281,6 +303,6 @@ TEST_F(CBitIRTest, DoesNotForwardAcrossAnAmbiguousStore) { auto funcOp = *moduleOp->getOps().begin(); auto returnOp = *funcOp.getOps().begin(); EXPECT_TRUE(returnOp.getOperand(0).getDefiningOp()); - EXPECT_TRUE(returnOp.getOperand(1).getDefiningOp()); + EXPECT_TRUE(returnOp.getOperand(1).getDefiningOp()); } } // namespace diff --git a/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp b/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp index f3561243bd..45a34e2f4a 100644 --- a/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp +++ b/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp @@ -622,7 +622,7 @@ enum class ForbiddenModifierBodyOp : std::uint8_t { QubitRegisterLoad, QubitRegisterStore, CBitAlloc, - CBitCompare, + CBitRead, CBitLoad, CBitStore }; @@ -657,8 +657,8 @@ static StringRef forbiddenOperationName(ForbiddenModifierBodyOp kind) { return "qubit-register-store"; case ForbiddenModifierBodyOp::CBitAlloc: return "cbit.alloc"; - case ForbiddenModifierBodyOp::CBitCompare: - return "cbit.cmp"; + case ForbiddenModifierBodyOp::CBitRead: + return "cbit.read"; case ForbiddenModifierBodyOp::CBitLoad: return "cbit.load"; case ForbiddenModifierBodyOp::CBitStore: @@ -696,10 +696,8 @@ static void emitForbiddenModifierBodyOperation(QCProgramBuilder& builder, cbit::RegisterType::get(builder.getContext(), 1), cbit::Initialization::Zero); return; - case ForbiddenModifierBodyOp::CBitCompare: - cbit::CompareOp::create(builder, builder.getI1Type(), - cbit::ComparisonPredicate::Equal, cbitReg, - builder.getIntegerAttr(builder.getI1Type(), 0)); + case ForbiddenModifierBodyOp::CBitRead: + cbit::ReadOp::create(builder, builder.getI1Type(), cbitReg); return; case ForbiddenModifierBodyOp::CBitLoad: cbit::LoadOp::create(builder, builder.getI1Type(), cbitReg, index); @@ -757,7 +755,7 @@ TEST_F(QCTest, ModifiersRecursivelyRejectEveryForbiddenOperation) { ForbiddenModifierBodyOp::QubitRegisterLoad, ForbiddenModifierBodyOp::QubitRegisterStore, ForbiddenModifierBodyOp::CBitAlloc, - ForbiddenModifierBodyOp::CBitCompare, + ForbiddenModifierBodyOp::CBitRead, ForbiddenModifierBodyOp::CBitLoad, ForbiddenModifierBodyOp::CBitStore}; diff --git a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp index 46eac456bd..5b752a3b7b 100644 --- a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp +++ b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp @@ -121,6 +121,28 @@ TEST(OpenQASM3EmissionTest, PreservesMeasurementOrderBeforeDelayedStore) { << *emitted; } +TEST(OpenQASM3EmissionTest, RejectsStaleClassicalSnapshots) { + constexpr llvm::StringLiteral source = R"mlir(module { + func.func @main() -> (!cbit.reg<2>, i1) attributes {mqt.entry_point} { + %one = arith.constant 1 : i2 + %zero = arith.constant 0 : i2 + %bits = cbit.alloc(#cbit.init) {mqt.register_name = "c"} + : !cbit.reg<2> + cbit.write %one, %bits : i2, !cbit.reg<2> + %old = cbit.read %bits : !cbit.reg<2> -> i2 + cbit.write %zero, %bits : i2, !cbit.reg<2> + %condition = arith.cmpi eq, %old, %one : i2 + return %bits, %condition : !cbit.reg<2>, i1 + } + })mlir"; + DialectRegistry registry = emissionDialects(); + MLIRContext context(registry); + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + + EXPECT_TRUE(failed(qc::translateQCToOpenQASM3(*moduleOp))); +} + TEST(OpenQASM3EmissionTest, CanonicalizesFixedAnglesToPortableFloats) { constexpr llvm::StringLiteral source = R"qasm(OPENQASM 3.1; include "stdgates.inc"; @@ -285,7 +307,6 @@ while (c == 1) { ASSERT_TRUE(succeeded(emitted)); EXPECT_NE(emitted->find("while ("), std::string::npos) << *emitted; - EXPECT_NE(emitted->find("c == 1"), std::string::npos) << *emitted; EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict})) << *emitted; @@ -298,12 +319,36 @@ module { %q = qc.alloc : !qc.qubit %c = cbit.alloc(#cbit.init) {mqt.register_name = "c"} : !cbit.reg<3> - %eq = cbit.cmp eq, %c, 5 : i3 : !cbit.reg<3> - %ne = cbit.cmp ne, %c, 5 : i3 : !cbit.reg<3> - %ult = cbit.cmp ult, %c, 5 : i3 : !cbit.reg<3> - %ule = cbit.cmp ule, %c, 5 : i3 : !cbit.reg<3> - %ugt = cbit.cmp ugt, %c, 5 : i3 : !cbit.reg<3> - %uge = cbit.cmp uge, %c, 5 : i3 : !cbit.reg<3> + %eq_read = cbit.read %c : !cbit.reg<3> -> i3 + %eq_rhs = arith.constant 5 : i3 + %eq = arith.cmpi eq, %eq_read, %eq_rhs : i3 + %ne_read = cbit.read %c : !cbit.reg<3> -> i3 + %ne_rhs = arith.constant 5 : i3 + %ne = arith.cmpi ne, %ne_read, %ne_rhs : i3 + %ult_read = cbit.read %c : !cbit.reg<3> -> i3 + %ult_rhs = arith.constant 5 : i3 + %ult = arith.cmpi ult, %ult_read, %ult_rhs : i3 + %ule_read = cbit.read %c : !cbit.reg<3> -> i3 + %ule_rhs = arith.constant 5 : i3 + %ule = arith.cmpi ule, %ule_read, %ule_rhs : i3 + %ugt_read = cbit.read %c : !cbit.reg<3> -> i3 + %ugt_rhs = arith.constant 5 : i3 + %ugt = arith.cmpi ugt, %ugt_read, %ugt_rhs : i3 + %uge_read = cbit.read %c : !cbit.reg<3> -> i3 + %uge_rhs = arith.constant 5 : i3 + %uge = arith.cmpi uge, %uge_read, %uge_rhs : i3 + %slt_read = cbit.read %c : !cbit.reg<3> -> i3 + %slt_rhs = arith.constant -3 : i3 + %slt = arith.cmpi slt, %slt_read, %slt_rhs : i3 + %sle_read = cbit.read %c : !cbit.reg<3> -> i3 + %sle_rhs = arith.constant -3 : i3 + %sle = arith.cmpi sle, %sle_read, %sle_rhs : i3 + %sgt_read = cbit.read %c : !cbit.reg<3> -> i3 + %sgt_rhs = arith.constant -3 : i3 + %sgt = arith.cmpi sgt, %sgt_read, %sgt_rhs : i3 + %sge_read = cbit.read %c : !cbit.reg<3> -> i3 + %sge_rhs = arith.constant -3 : i3 + %sge = arith.cmpi sge, %sge_read, %sge_rhs : i3 scf.if %eq { qc.x %q : !qc.qubit } @@ -322,6 +367,18 @@ module { scf.if %uge { qc.x %q : !qc.qubit } + scf.if %slt { + qc.x %q : !qc.qubit + } + scf.if %sle { + qc.x %q : !qc.qubit + } + scf.if %sgt { + qc.x %q : !qc.qubit + } + scf.if %sge { + qc.x %q : !qc.qubit + } return %c : !cbit.reg<3> } } @@ -334,10 +391,6 @@ module { auto emitted = qc::translateQCToOpenQASM3(*moduleOp); ASSERT_TRUE(succeeded(emitted)); - for (const auto* comparison : - {"c == 5", "c != 5", "c < 5", "c <= 5", "c > 5", "c >= 5"}) { - EXPECT_NE(emitted->find(comparison), std::string::npos) << *emitted; - } EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict})) << *emitted; @@ -352,7 +405,9 @@ module { %q = qc.alloc : !qc.qubit %c = cbit.alloc(#cbit.init) {mqt.register_name = "c"} : !cbit.reg<3> - %condition = cbit.cmp eq, %c, 0 : i3 : !cbit.reg<3> + %condition_read = cbit.read %c : !cbit.reg<3> -> i3 + %condition_rhs = arith.constant 0 : i3 + %condition = arith.cmpi eq, %condition_read, %condition_rhs : i3 %false = arith.constant false %forwarded = arith.xori %condition, %false : i1 cbit.store %true, %c[%zero] : !cbit.reg<3> @@ -371,6 +426,202 @@ module { EXPECT_TRUE(failed(qc::translateQCToOpenQASM3(*moduleOp))); } +TEST(OpenQASM3EmissionTest, EmitsFixedWidthRegisterExpressions) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() -> !cbit.reg<3> attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + %c = cbit.alloc(#cbit.init) {mqt.register_name = "c"} + : !cbit.reg<3> + %value = cbit.read %c : !cbit.reg<3> -> i3 + %four = arith.constant 4 : i3 + %biased = arith.xori %value, %four : i3 + %one = arith.constant 1 : i3 + %distance = arith.andi %value, %one : i3 + %shifted = arith.shli %biased, %distance : i3 + cbit.write %shifted, %c : i3, !cbit.reg<3> + %updated = cbit.read %c : !cbit.reg<3> -> i3 + %three = arith.constant 3 : i3 + %condition = arith.cmpi ult, %updated, %three : i3 + scf.if %condition { + qc.x %q : !qc.qubit + } + return %c : !cbit.reg<3> + } +} +)mlir"; + DialectRegistry registry = emissionDialects(); + MLIRContext context(registry); + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + + auto emitted = qc::translateQCToOpenQASM3(*moduleOp); + + ASSERT_TRUE(succeeded(emitted)); + EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( + *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict})) + << *emitted; +} + +TEST(OpenQASM3EmissionTest, RejectsWideBitRegisterShiftDistance) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() -> !cbit.reg<65> attributes {mqt.entry_point} { + %a = cbit.alloc(#cbit.init) {mqt.register_name = "a"} + : !cbit.reg<65> + %b = cbit.alloc(#cbit.init) {mqt.register_name = "b"} + : !cbit.reg<65> + %av = cbit.read %a : !cbit.reg<65> -> i65 + %bv = cbit.read %b : !cbit.reg<65> -> i65 + %shifted = arith.shli %av, %bv : i65 + cbit.write %shifted, %a : i65, !cbit.reg<65> + return %a : !cbit.reg<65> + } +} +)mlir"; + DialectRegistry registry = emissionDialects(); + MLIRContext context(registry); + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + + EXPECT_TRUE(failed(qc::translateQCToOpenQASM3(*moduleOp))); +} + +TEST(OpenQASM3EmissionTest, RoundTripsExtendedBooleanShiftDistance) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() -> !cbit.reg<64> attributes {mqt.entry_point} { + %zero = arith.constant 0 : index + %c = cbit.alloc(#cbit.init) {mqt.register_name = "c"} + : !cbit.reg<64> + %bit = cbit.load %c[%zero] : !cbit.reg<64> + %distance = arith.extui %bit : i1 to i64 + %value = cbit.read %c : !cbit.reg<64> -> i64 + %shifted = arith.shli %value, %distance : i64 + cbit.write %shifted, %c : i64, !cbit.reg<64> + return %c : !cbit.reg<64> + } +} +)mlir"; + DialectRegistry registry = emissionDialects(); + MLIRContext context(registry); + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + + auto emitted = qc::translateQCToOpenQASM3(*moduleOp); + ASSERT_TRUE(succeeded(emitted)); + EXPECT_TRUE(qc::translateQASM3ToQC(*emitted, &context)) << *emitted; +} + +TEST(OpenQASM3EmissionTest, EmitsScalarWidthOneRegisterWrites) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() -> !cbit.reg<1> attributes {mqt.entry_point} { + %true = arith.constant true + %c = cbit.alloc(#cbit.init) {mqt.register_name = "c"} + : !cbit.reg<1> + cbit.write %true, %c : i1, !cbit.reg<1> + return %c : !cbit.reg<1> + } +} +)mlir"; + DialectRegistry registry = emissionDialects(); + MLIRContext context(registry); + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + + auto emitted = qc::translateQCToOpenQASM3(*moduleOp); + + ASSERT_TRUE(succeeded(emitted)); + EXPECT_TRUE(oq3::frontend::analyzeOpenQASM( + *emitted, {.gatePolicy = oq3::frontend::GatePolicy::Strict})) + << *emitted; +} + +TEST(OpenQASM3EmissionTest, RoundTripsWholeRegisterRotations) { + constexpr llvm::StringLiteral source = R"qasm( +OPENQASM 3.1; +qubit[5] q; +output bit[5] result; +result = measure q; +result = rotl(result, 2); +)qasm"; + MLIRContext context; + auto moduleOp = qc::translateQASM3ToQC(source, &context); + ASSERT_TRUE(moduleOp); + + auto emitted = qc::translateQCToOpenQASM3(*moduleOp); + + ASSERT_TRUE(succeeded(emitted)); + EXPECT_TRUE(qc::translateQASM3ToQC(*emitted, &context)) << *emitted; +} + +TEST(OpenQASM3EmissionTest, RoundTripsWideBitVectorBuiltins) { + constexpr llvm::StringLiteral source = R"qasm(OPENQASM 3.1; +output bit[65] c; +c = 1; +int distance = int(bool(c)); +c = rotl(c, distance); +c = rotr(c, 2); +output uint count; +count = popcount(c); +)qasm"; + MLIRContext context; + auto moduleOp = qc::translateQASM3ToQC(source, &context); + ASSERT_TRUE(moduleOp); + auto emitted = qc::translateQCToOpenQASM3(*moduleOp); + ASSERT_TRUE(succeeded(emitted)); + auto restored = qc::translateQASM3ToQC(*emitted, &context); + ASSERT_TRUE(restored) << *emitted; + EXPECT_TRUE(succeeded(verify(*restored))); +} + +TEST(OpenQASM3EmissionTest, RejectsWideScalarIntegerExpressions) { + constexpr auto expressions = std::to_array({ + R"mlir(%sum = arith.addi %value, %zero : i80 + %condition = arith.cmpi eq, %sum, %zero : i80)mlir", + R"mlir(%condition = arith.cmpi slt, %value, %zero : i80)mlir", + R"mlir(%narrow = arith.trunci %value : i80 to i64 + %zero64 = arith.constant 0 : i64 + %condition = arith.cmpi eq, %narrow, %zero64 : i64)mlir", + }); + DialectRegistry registry = emissionDialects(); + MLIRContext context(registry); + for (const auto expression : expressions) { + SCOPED_TRACE(expression.str()); + const auto source = R"mlir(module { + func.func @main() -> i1 attributes {mqt.entry_point} { + %bits = cbit.alloc(#cbit.init) : !cbit.reg<80> + %value = cbit.read %bits : !cbit.reg<80> -> i80 + %zero = arith.constant 0 : i80 + )mlir" + expression.str() + + R"mlir( + return %condition : i1 + } + })mlir"; + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + EXPECT_TRUE(failed(qc::translateQCToOpenQASM3(*moduleOp))); + } +} + +TEST(OpenQASM3EmissionTest, EmitsIndexArithmetic) { + constexpr llvm::StringLiteral source = R"mlir(module { + func.func @main() -> index attributes {mqt.entry_point} { + %one = arith.constant 1 : index + %sum = arith.addi %one, %one : index + return %sum : index + } + })mlir"; + DialectRegistry registry = emissionDialects(); + MLIRContext context(registry); + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + auto emitted = qc::translateQCToOpenQASM3(*moduleOp); + ASSERT_TRUE(succeeded(emitted)); + EXPECT_TRUE(qc::translateQASM3ToQC(*emitted, &context)) << *emitted; +} + TEST(OpenQASM3EmissionTest, EmitsNativeIndexSwitch) { constexpr llvm::StringLiteral source = R"mlir( module { @@ -528,12 +779,9 @@ module { auto emitted = qc::translateQCToOpenQASM3(*moduleOp); ASSERT_TRUE(succeeded(emitted)); - EXPECT_NE(emitted->find("((1 + 2) / 2)"), std::string::npos); - EXPECT_NE(emitted->find("((1 + 2) >= 2)"), std::string::npos); EXPECT_NE(emitted->find("sin((-0.25))"), std::string::npos); EXPECT_NE(emitted->find("mod(0.25, sin((-0.25)))"), std::string::npos); - EXPECT_NE(emitted->find("int(sin((-0.25)))"), std::string::npos); - EXPECT_NE(emitted->find("(int(sin((-0.25))) != 0)"), std::string::npos); + EXPECT_NE(emitted->find("int[64]("), std::string::npos) << *emitted; } TEST(OpenQASM3EmissionTest, EmitsFloatingRemainderAsStrictOpenQASMMod) { @@ -596,12 +844,7 @@ module { auto emitted = qc::translateQCToOpenQASM3(*moduleOp); ASSERT_TRUE(succeeded(emitted)); - for (const auto* const comparison : - {"(1 == 2)", "(1 != 2)", "(1 < 2)", "(1 <= 2)", "(1 > 2)", "(1 >= 2)", - "(float(1) == 2.0)", "(float(1) != 2.0)", "(float(1) < 2.0)", - "(float(1) <= 2.0)", "(float(1) > 2.0)", "(float(1) >= 2.0)"}) { - EXPECT_NE(emitted->find(comparison), std::string::npos) << comparison; - } + EXPECT_NE(emitted->find("int[64]("), std::string::npos) << *emitted; } TEST(OpenQASM3EmissionTest, EmitsCanonicalConstantRangeBoundaries) { @@ -912,34 +1155,6 @@ TEST(OpenQASM3EmissionTest, RejectsUnsupportedSubsetConcerns) { return %memory : memref<1x!qc.qubit> } })mlir"}, - Fixture{.name = "unsupported-expression-width", .source = R"mlir(module { - func.func @main() { - %one = arith.constant 1 : i32 - %sum = arith.addi %one, %one : i32 - return - } - })mlir"}, - Fixture{.name = "sign-extension", .source = R"mlir(module { - func.func @main() -> i64 { - %value = arith.constant true - %extended = arith.extsi %value : i1 to i64 - return %extended : i64 - } - })mlir"}, - Fixture{.name = "integer-truncation", .source = R"mlir(module { - func.func @main() -> i1 { - %value = arith.constant 2 : i64 - %truncated = arith.trunci %value : i64 to i1 - return %truncated : i1 - } - })mlir"}, - Fixture{.name = "packed-bitwise", .source = R"mlir(module { - func.func @main() -> i64 { - %one = arith.constant 1 : i64 - %value = arith.andi %one, %one : i64 - return %value : i64 - } - })mlir"}, Fixture{.name = "unordered-float-comparison", .source = R"mlir(module { func.func @main() -> i1 { %one = arith.constant 1.0 : f64 @@ -994,35 +1209,6 @@ TEST(OpenQASM3EmissionTest, RejectsUnsupportedSubsetConcerns) { return } })mlir"}, - Fixture{.name = "select", .source = R"mlir(module { - func.func @main() -> i64 { - %condition = arith.constant true - %one = arith.constant 1 : i64 - %value = arith.select %condition, %one, %one : i64 - return %value : i64 - } - })mlir"}, - Fixture{.name = "unsigned-arithmetic", .source = R"mlir(module { - func.func @main() -> i64 { - %one = arith.constant 1 : i64 - %value = arith.divui %one, %one : i64 - return %value : i64 - } - })mlir"}, - Fixture{.name = "unsigned-comparison", .source = R"mlir(module { - func.func @main() -> i1 { - %one = arith.constant 1 : i64 - %value = arith.cmpi ult, %one, %one : i64 - return %value : i1 - } - })mlir"}, - Fixture{.name = "unsigned-cast", .source = R"mlir(module { - func.func @main() -> f64 { - %one = arith.constant 1 : i64 - %value = arith.uitofp %one : i64 to f64 - return %value : f64 - } - })mlir"}, Fixture{.name = "unsupported-output", .source = R"mlir(module { func.func @main() -> f32 { %value = arith.constant 1.0 : f32 diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp index c0e9a8a4f8..f72f0be830 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp @@ -394,7 +394,7 @@ enum class VerifierModifierKind : uint8_t { Inv, Ctrl, Pow }; enum class ForbiddenModifierBodyOp : uint8_t { Measure, CBitAlloc, - CBitCompare, + CBitRead, CBitLoad, CBitStore }; @@ -419,8 +419,8 @@ static StringRef forbiddenOperationName(ForbiddenModifierBodyOp kind) { return "measure"; case ForbiddenModifierBodyOp::CBitAlloc: return "cbit.alloc"; - case ForbiddenModifierBodyOp::CBitCompare: - return "cbit.cmp"; + case ForbiddenModifierBodyOp::CBitRead: + return "cbit.read"; case ForbiddenModifierBodyOp::CBitLoad: return "cbit.load"; case ForbiddenModifierBodyOp::CBitStore: @@ -483,10 +483,8 @@ buildInvalidNestedModifierBody(QCOProgramBuilder& builder, builder, cbit::RegisterType::get(builder.getContext(), 1), cbit::Initialization::Zero); break; - case ForbiddenModifierBodyOp::CBitCompare: - cbit::CompareOp::create( - builder, builder.getI1Type(), cbit::ComparisonPredicate::Equal, - cbitReg, builder.getIntegerAttr(builder.getI1Type(), 0)); + case ForbiddenModifierBodyOp::CBitRead: + cbit::ReadOp::create(builder, builder.getI1Type(), cbitReg); break; case ForbiddenModifierBodyOp::CBitLoad: cbit::LoadOp::create(builder, builder.getI1Type(), cbitReg, @@ -520,7 +518,7 @@ TEST_F(QCOTest, ModifiersRecursivelyRejectNonUnitaryOperations) { VerifierModifierKind::Pow}; constexpr std::array forbiddenOperations{ ForbiddenModifierBodyOp::Measure, ForbiddenModifierBodyOp::CBitAlloc, - ForbiddenModifierBodyOp::CBitCompare, ForbiddenModifierBodyOp::CBitLoad, + ForbiddenModifierBodyOp::CBitRead, ForbiddenModifierBodyOp::CBitLoad, ForbiddenModifierBodyOp::CBitStore}; for (const auto modifier : modifiers) { diff --git a/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp b/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp index d1c1f7a9e5..9701915d9c 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp @@ -800,19 +800,24 @@ TEST_F(QCODDFunctionalityTest, SimulateCBitConditionAndMeasurementUpdate) { TEST_F(QCODDFunctionalityTest, SimulateCBitRegisterComparisons) { constexpr std::array comparisons{ - std::pair{cbit::ComparisonPredicate::Equal, false}, - std::pair{cbit::ComparisonPredicate::NotEqual, true}, - std::pair{cbit::ComparisonPredicate::Less, true}, - std::pair{cbit::ComparisonPredicate::LessEqual, true}, - std::pair{cbit::ComparisonPredicate::Greater, false}, - std::pair{cbit::ComparisonPredicate::GreaterEqual, false}, + std::pair{arith::CmpIPredicate::eq, false}, + std::pair{arith::CmpIPredicate::ne, true}, + std::pair{arith::CmpIPredicate::ult, true}, + std::pair{arith::CmpIPredicate::ule, true}, + std::pair{arith::CmpIPredicate::ugt, false}, + std::pair{arith::CmpIPredicate::uge, false}, + std::pair{arith::CmpIPredicate::slt, false}, + std::pair{arith::CmpIPredicate::sle, false}, + std::pair{arith::CmpIPredicate::sgt, true}, + std::pair{arith::CmpIPredicate::sge, true}, }; for (const auto [predicate, expected] : comparisons) { auto mod = buildModule([&](QCOProgramBuilder& b) { auto reg = b.allocClassicalBitRegister(2, "c"); - auto rhs = b.getIntegerAttr(b.getIntegerType(2), 1); - auto condition = - cbit::CompareOp::create(b, b.getI1Type(), predicate, reg, rhs); + auto rhs = b.getIntegerAttr(b.getIntegerType(2), 3); + auto condition = arith::CmpIOp::create( + b, predicate, cbit::ReadOp::create(b, rhs.getType(), reg), + arith::ConstantOp::create(b, rhs)); auto q = b.staticQubit(0); q = b.qcoIf( condition, q, [&](Value arg) { return b.x(arg); }, @@ -849,8 +854,10 @@ TEST_F(QCODDFunctionalityTest, RejectsUndefinedCBitRegisterComparison) { auto reg = b.allocClassicalBitRegister(1, "c", cbit::Initialization::Undefined); auto rhs = b.getIntegerAttr(b.getIntegerType(1), 0); - auto condition = cbit::CompareOp::create( - b, b.getI1Type(), cbit::ComparisonPredicate::Equal, reg, rhs); + auto condition = + arith::CmpIOp::create(b, arith::CmpIPredicate::eq, + cbit::ReadOp::create(b, rhs.getType(), reg), + arith::ConstantOp::create(b, rhs)); auto q = b.staticQubit(0); q = b.qcoIf( condition, q, [&](Value arg) { return arg; }, @@ -866,6 +873,27 @@ TEST_F(QCODDFunctionalityTest, RejectsUndefinedCBitRegisterComparison) { failed(simulate(mainFunc(*mod), dd::makeZeroState(1, *dd), *dd, rng))); } +TEST_F(QCODDFunctionalityTest, SimulateWholeCBitRegisterReadAndWrite) { + auto mod = buildModule([](QCOProgramBuilder& b) { + auto reg = b.allocClassicalBitRegister(3, "c"); + auto five = arith::ConstantIntOp::create(b, 5, 3).getResult(); + cbit::WriteOp::create(b, five, reg); + auto value = cbit::ReadOp::create(b, b.getIntegerType(3), reg).getResult(); + auto condition = + arith::CmpIOp::create(b, arith::CmpIPredicate::eq, value, five) + .getResult(); + auto q = b.staticQubit(0); + q = b.qcoIf( + condition, q, [&](Value arg) { return b.x(arg); }, + [&](Value arg) { return arg; }); + b.sink(q); + return b.intConstant(0); + }); + ASSERT_TRUE(mod); + + expectSimulatesFromZero(mainFunc(*mod), true); +} + TEST_F(QCODDFunctionalityTest, SimulateMeasureFeedsIndexSwitch) { // |1> → measure → index_castui → index_switch case 1 applies X → |0>. auto mod = buildModule([](QCOProgramBuilder& b) { diff --git a/mlir/unittests/Target/OpenQASM/OpenQASMTestUtils.h b/mlir/unittests/Target/OpenQASM/OpenQASMTestUtils.h index 1868d28a6b..aac0f8bafb 100644 --- a/mlir/unittests/Target/OpenQASM/OpenQASMTestUtils.h +++ b/mlir/unittests/Target/OpenQASM/OpenQASMTestUtils.h @@ -18,7 +18,6 @@ #include #include #include -#include #include #include #include @@ -29,16 +28,11 @@ #include #include #include -#include #include -#include -#include #include #include #include -#include -#include namespace mlir::oq3::test { @@ -173,62 +167,4 @@ inline SmallVector> returnedBitValues(ModuleOp moduleOp) { return values; } -inline std::vector canonicalizedBitOutputs(const StringRef source) { - MLIRContext context; - auto moduleOp = qc::translateQASM3ToQC(source, &context); - if (!moduleOp) { - ADD_FAILURE() // NOLINT(readability-implicit-bool-conversion) - << "translation failed"; - return {}; - } - if (failed(verify(*moduleOp))) { - ADD_FAILURE() // NOLINT(readability-implicit-bool-conversion) - << "translation produced an invalid module"; - return {}; - } - PassManager canonicalizer(&context); - canonicalizer.addPass(createCanonicalizerPass()); - if (failed(canonicalizer.run(*moduleOp))) { - ADD_FAILURE() // NOLINT(readability-implicit-bool-conversion) - << "canonicalization failed"; - return {}; - } - - const auto returned = returnedBitValues(*moduleOp); - std::vector outputs; - outputs.reserve(returned.size()); - for (const auto operand : returned) { - if (!operand) { - outputs.push_back(false); - continue; - } - const auto value = evaluateConstantInteger(*operand); - if (!value) { - std::string description; - llvm::raw_string_ostream stream(description); - operand->print(stream); - ADD_FAILURE() << "canonicalized output is not constant: " << description; - return {}; - } - outputs.push_back(!value->isZero()); - } - return outputs; -} - -inline std::vector rotateBits(const std::array& bits, - const int64_t distance, const bool left) { - constexpr int64_t width = 5; - auto normalized = distance % width; - if (normalized < 0) { - normalized += width; - } - std::vector result(width); - for (int64_t bit = 0; bit < width; ++bit) { - const auto source = - left ? (bit + width - normalized) % width : (bit + normalized) % width; - result[bit] = bits[static_cast(source)]; - } - return result; -} - } // namespace mlir::oq3::test diff --git a/mlir/unittests/Target/OpenQASM/test_openqasm_emitter.cpp b/mlir/unittests/Target/OpenQASM/test_openqasm_emitter.cpp index 841bd0ab70..55e9d4d0ed 100644 --- a/mlir/unittests/Target/OpenQASM/test_openqasm_emitter.cpp +++ b/mlir/unittests/Target/OpenQASM/test_openqasm_emitter.cpp @@ -47,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -931,17 +932,12 @@ if (target[0] || target[1]) { x q[0]; } canonicalizer.addPass(createCanonicalizerPass()); ASSERT_TRUE(succeeded(canonicalizer.run(*moduleOp))); - SmallVector measured; - moduleOp->walk([&](qc::MeasureOp measurement) { - measured.push_back(measurement.getResult()); - }); - const auto returned = returnedBitValues(*moduleOp); - ASSERT_EQ(measured.size(), 2); - ASSERT_EQ(returned.size(), 2); - ASSERT_TRUE(returned[0]); - ASSERT_TRUE(returned[1]); - EXPECT_EQ(*returned[0], measured[0]); - EXPECT_EQ(*returned[1], measured[1]); + SmallVector writes; + moduleOp->walk([&](cbit::WriteOp write) { writes.push_back(write); }); + ASSERT_EQ(writes.size(), 1); + auto read = writes.front().getValue().getDefiningOp(); + ASSERT_TRUE(read); + EXPECT_NE(read.getReg(), writes.front().getReg()); } TEST(OpenQASMTargetTest, LowersTypedBitVectorBuiltins) { @@ -980,110 +976,101 @@ if (count == 3) { x q; } funnelShifts += isa(operation); }); EXPECT_EQ(populationCounts, 1); - // Both rotation distances are constant and therefore only permute SSA values. - EXPECT_EQ(funnelShifts, 0); + EXPECT_EQ(funnelShifts, 1); } -TEST(OpenQASMTargetTest, ReusesPackedNestedDynamicRotations) { +TEST(OpenQASMTargetTest, LowersRuntimeBitRegisterExpressions) { constexpr llvm::StringLiteral source = R"qasm( OPENQASM 3.1; -bit[5] value; -value[0] = true; -value[1] = false; -value[2] = true; -value[3] = false; -value[4] = true; -int distance = -7; -uint count = popcount(rotl(rotr(value, distance), 1)); -qubit q; -if (count == 3) { x q; } +include "stdgates.inc"; +qubit[3] q; +bit[3] value = measure q; +output bit[3] result; +result = (~value & 6) | (value ^ 1); +if ((result >> 1) < (result << 2)) { x q[0]; } )qasm"; + auto analyzed = oq3::frontend::analyzeOpenQASM(source); + ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message; + EXPECT_TRUE(llvm::any_of(analyzed.program->conditions, [](const auto& value) { + return value.kind == oq3::frontend::ConditionKind::BitVectorComparison; + })); + MLIRContext context; auto moduleOp = qc::translateQASM3ToQC(source, &context); ASSERT_TRUE(moduleOp); ASSERT_TRUE(succeeded(verify(*moduleOp))); - - size_t leftShifts = 0; - size_t rightShifts = 0; - size_t populationCounts = 0; - size_t unpackingTruncations = 0; - moduleOp->walk([&](Operation* operation) { - leftShifts += isa(operation); - rightShifts += isa(operation); - populationCounts += isa(operation); - if (auto truncation = dyn_cast(operation); - truncation && truncation.getOut().getType().isInteger(1)) { - ++unpackingTruncations; - } - }); - EXPECT_EQ(leftShifts, 1); - EXPECT_EQ(rightShifts, 1); - EXPECT_EQ(populationCounts, 1); - // The nested packed value reaches popcount without an unpack/repack cycle. - EXPECT_EQ(unpackingTruncations, 0); } -TEST(OpenQASMTargetTest, StoresAtomicRotationsThroughControlFlow) { +TEST(OpenQASMTargetTest, FoldsBitRegisterOvershiftsToZero) { constexpr llvm::StringLiteral source = R"qasm( OPENQASM 3.1; -output bit[5] value; -value[0] = true; -value[1] = false; -value[2] = true; -value[3] = false; -value[4] = true; -qubit q; -bit condition = measure q; -if (condition) { - value = rotl(value, 1); -} +qubit[3] q; +bit[3] value = measure q; +output bit[3] result; +result = value << 3; )qasm"; MLIRContext context; auto moduleOp = qc::translateQASM3ToQC(source, &context); ASSERT_TRUE(moduleOp); ASSERT_TRUE(succeeded(verify(*moduleOp))); - bool storesWholeRegister = false; - moduleOp->walk([&](scf::IfOp conditional) { - size_t stores = 0; - conditional.getThenRegion().walk([&](cbit::StoreOp) { ++stores; }); - storesWholeRegister |= conditional.getNumResults() == 0 && stores == 5; - }); - EXPECT_TRUE(storesWholeRegister); + bool hasLeftShift = false; + moduleOp->walk([&](arith::ShLIOp) { hasLeftShift = true; }); + EXPECT_FALSE(hasLeftShift); + cbit::WriteOp write; + moduleOp->walk([&](cbit::WriteOp candidate) { write = candidate; }); + ASSERT_TRUE(write); + EXPECT_TRUE(matchPattern(write.getValue(), m_Zero())); } -TEST(OpenQASMTargetTest, SelfRotationSnapshotsTheWholeRegister) { +TEST(OpenQASMTargetTest, LowersQiskitCompatibleBitRegisterShiftDistances) { constexpr llvm::StringLiteral source = R"qasm( OPENQASM 3.1; -qubit[5] q; -output bit[5] result; -result = measure q; -result = rotl(result, 2); +qubit[3] q; +bit[3] value = measure q; +if ((value << (value & 1)) == 0) {} )qasm"; MLIRContext context; auto moduleOp = qc::translateQASM3ToQC(source, &context); ASSERT_TRUE(moduleOp); ASSERT_TRUE(succeeded(verify(*moduleOp))); - PassManager canonicalizer(&context); - canonicalizer.addPass(createCanonicalizerPass()); - ASSERT_TRUE(succeeded(canonicalizer.run(*moduleOp))); + bool hasShift = false; + moduleOp->walk([&](arith::ShLIOp) { hasShift = true; }); + EXPECT_TRUE(hasShift); +} - SmallVector measured; - moduleOp->walk([&](qc::MeasureOp measurement) { - measured.push_back(measurement.getResult()); +TEST(OpenQASMTargetTest, WidensSizedIntegerBuiltinAndIndexOperands) { + constexpr auto sources = std::to_array({ + R"qasm(OPENQASM 3.1; +bit[3] c = 1; +int[3] distance = -1; +c = rotl(c, distance); +c = rotr(c, distance); +)qasm", + R"qasm(OPENQASM 3.1; +bit[3] c = 0; +uint[2] positive = 2; +int[2] negative = -1; +c[positive] = true; +c[negative] = false; +)qasm", + R"qasm(OPENQASM 3.1; +qubit q; +int[3] signedExponent = -1; +uint[3] unsignedExponent = 7; +pow(signedExponent) @ x q; +pow(unsignedExponent) @ x q; +)qasm", }); - const auto returned = returnedBitValues(*moduleOp); - ASSERT_EQ(measured.size(), 5); - ASSERT_EQ(returned.size(), 5); - ASSERT_TRUE(llvm::all_of( - returned, [](const auto& value) { return value.has_value(); })); - EXPECT_EQ(*returned[0], measured[3]); - EXPECT_EQ(*returned[1], measured[4]); - EXPECT_EQ(*returned[2], measured[0]); - EXPECT_EQ(*returned[3], measured[1]); - EXPECT_EQ(*returned[4], measured[2]); + for (const auto source : sources) { + SCOPED_TRACE(source.str()); + MLIRContext context; + auto moduleOp = qc::translateQASM3ToQC(source, &context); + ASSERT_TRUE(moduleOp); + EXPECT_TRUE(succeeded(verify(*moduleOp))); + } } TEST(OpenQASMTargetTest, SupportsWidthOneBitVectorBuiltins) { @@ -1152,97 +1139,6 @@ uint count = popcount(value); EXPECT_EQ(narrowedCounts, 1); } -TEST(OpenQASMTargetTest, RotationsProduceSpecifiedBitResults) { - constexpr std::array input{true, false, true, true, false}; - constexpr std::array distances{0, 2, -2, 7, -7}; - std::string source = "OPENQASM 3.1;\n"; - std::vector> expectedResults; - size_t resultIndex = 0; - for (const bool runtime : {false, true}) { - for (const auto distance : distances) { - for (const bool left : {true, false}) { - const auto resultName = "result" + std::to_string(resultIndex); - source += "output bit[5] " + resultName + ";\n"; - for (size_t bit = 0; bit < input.size(); ++bit) { - source += resultName + "[" + std::to_string(bit) + - "] = " + (input[bit] ? "true;\n" : "false;\n"); - } - std::string distanceExpression = std::to_string(distance); - if (runtime) { - const auto distanceName = "distance" + std::to_string(resultIndex); - source.append("int ") - .append(distanceName) - .append(" = ") - .append(distanceExpression) - .append(";\n"); - distanceExpression = distanceName; - } - source.append(resultName) - .append(" = ") - .append(left ? "rotl(" : "rotr(") - .append(resultName) - .append(", ") - .append(distanceExpression) - .append(");\n"); - expectedResults.push_back(rotateBits(input, distance, left)); - ++resultIndex; - } - } - } - - const auto outputs = canonicalizedBitOutputs(source); - ASSERT_EQ(outputs.size(), expectedResults.size() * input.size()); - std::vector> actualResults; - actualResults.reserve(expectedResults.size()); - for (size_t result = 0; result < expectedResults.size(); ++result) { - const auto begin = - outputs.begin() + static_cast(result * input.size()); - actualResults.emplace_back(begin, begin + input.size()); - EXPECT_EQ(actualResults.back(), expectedResults[result]) - << "rotation result " << result; - } - - for (size_t runtime = 0; runtime < 2; ++runtime) { - for (size_t distance = 0; distance < distances.size(); ++distance) { - // libc++ uses a pointer here, whereas MSVC uses an iterator class. - const auto opposite = // NOLINT(readability-qualified-auto) - std::ranges::find(distances, -distances[distance]); - ASSERT_NE(opposite, distances.end()); - const auto oppositeIndex = - static_cast(opposite - distances.begin()); - const auto left = ((runtime * distances.size()) + distance) * 2; - const auto oppositeRight = - (((runtime * distances.size()) + oppositeIndex) * 2) + 1; - EXPECT_EQ(actualResults[left], actualResults[oppositeRight]) - << "rotl(a, n) differs from rotr(a, -n) for n = " - << distances[distance]; - } - } -} - -TEST(OpenQASMTargetTest, PopcountProducesSpecifiedResult) { - constexpr llvm::StringLiteral source = R"qasm( -OPENQASM 3.1; -bit[5] source; -source[0] = true; -source[1] = false; -source[2] = true; -source[3] = true; -source[4] = false; -output bit[6] result; -result[0] = false; -result[1] = false; -result[2] = false; -result[3] = false; -result[4] = false; -result[5] = false; -result[popcount(source)] = true; -)qasm"; - - EXPECT_EQ(canonicalizedBitOutputs(source), - (std::vector{false, false, false, true, false, false})); -} - TEST(OpenQASMTargetTest, SupportsOpenQASM2RegisterConditions) { constexpr llvm::StringLiteral source = R"qasm( OPENQASM 2.0; @@ -1260,7 +1156,7 @@ if (c == 1) x q[0]; size_t conditionals = 0; size_t comparisons = 0; moduleOp->walk([&](scf::IfOp) { ++conditionals; }); - moduleOp->walk([&](cbit::CompareOp) { ++comparisons; }); + moduleOp->walk([&](arith::CmpIOp) { ++comparisons; }); EXPECT_EQ(conditionals, 1); EXPECT_EQ(comparisons, 1); } @@ -1287,12 +1183,20 @@ if (c >= 5) { x q; } ASSERT_TRUE(moduleOp); ASSERT_TRUE(succeeded(verify(*moduleOp))); - std::array predicates{}; - moduleOp->walk([&](cbit::CompareOp comparison) { - predicates.at(static_cast(comparison.getPredicate())) = true; - EXPECT_EQ(comparison.getRhs(), llvm::APInt(3, 5)); + std::vector predicates; + moduleOp->walk([&](arith::CmpIOp comparison) { + predicates.emplace_back(comparison.getPredicate()); + EXPECT_EQ( + cast( + comparison.getRhs().getDefiningOp().getValue()) + .getValue(), + llvm::APInt(3, 5)); }); - EXPECT_TRUE(llvm::all_of(predicates, [](const bool value) { return value; })); + EXPECT_EQ( + predicates, + (std::vector{arith::CmpIPredicate::eq, arith::CmpIPredicate::ne, + arith::CmpIPredicate::ult, arith::CmpIPredicate::ule, + arith::CmpIPredicate::ugt, arith::CmpIPredicate::uge})); } TEST(OpenQASMTargetTest, PreservesWideRegisterComparisons) { @@ -1307,10 +1211,121 @@ if (c == 18446744073709551616) x q[0]; MLIRContext context; auto moduleOp = qc::translateQASM3ToQC(source, &context); ASSERT_TRUE(moduleOp); - cbit::CompareOp comparison; - moduleOp->walk([&](cbit::CompareOp op) { comparison = op; }); + arith::CmpIOp comparison; + moduleOp->walk([&](arith::CmpIOp op) { comparison = op; }); ASSERT_TRUE(comparison); - EXPECT_EQ(comparison.getRhs(), llvm::APInt(65, 1).shl(64)); + EXPECT_EQ( + cast( + comparison.getRhs().getDefiningOp().getValue()) + .getValue(), + llvm::APInt(65, 1).shl(64)); +} + +TEST(OpenQASMTargetTest, EmitsSizedBitRegisterCastCondition) { + constexpr llvm::StringLiteral source = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; + +bit[2] syndrome; +qubit[2] q; + +syndrome[0] = measure q[0]; +syndrome[1] = measure q[1]; + +if (uint[2](syndrome) == 3) { + x q[0]; +} +if (uint[2](syndrome) < 3) { + x q[0]; +} +)qasm"; + + MLIRContext context; + auto moduleOp = qc::translateQASM3ToQC(source, &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + size_t conditionalGates = 0; + moduleOp->walk([&](qc::XOp operation) { + conditionalGates += operation->getParentOfType() != nullptr; + }); + EXPECT_EQ(conditionalGates, 2); + std::vector predicates; + moduleOp->walk([&](arith::CmpIOp comparison) { + predicates.emplace_back(comparison.getPredicate()); + EXPECT_EQ( + cast( + comparison.getRhs().getDefiningOp().getValue()) + .getInt(), + 3); + }); + EXPECT_EQ(predicates, + (std::vector{arith::CmpIPredicate::eq, arith::CmpIPredicate::slt})); +} + +TEST(OpenQASMTargetTest, EmitsSignedBitRegisterCastComparisons) { + constexpr llvm::StringLiteral source = R"qasm(OPENQASM 3.1; +include "stdgates.inc"; + +bit[2] syndrome; +qubit[2] q; + +syndrome[0] = measure q[0]; +syndrome[1] = measure q[1]; + +if (int[2](syndrome) < -1) { x q[0]; } +if (int[2](syndrome) <= -1) { x q[0]; } +if (-1 < int[2](syndrome)) { x q[0]; } +if (-1 <= int[2](syndrome)) { x q[0]; } +)qasm"; + + MLIRContext context; + auto moduleOp = qc::translateQASM3ToQC(source, &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + std::vector predicates; + moduleOp->walk([&](arith::CmpIOp comparison) { + predicates.emplace_back(comparison.getPredicate()); + EXPECT_TRUE(comparison.getLhs().getType().isInteger(64)); + }); + EXPECT_EQ( + predicates, + (std::vector{arith::CmpIPredicate::slt, arith::CmpIPredicate::sle, + arith::CmpIPredicate::slt, arith::CmpIPredicate::sle})); +} + +TEST(OpenQASMTargetTest, PreservesBitRegisterCastSignedness) { + constexpr llvm::StringLiteral source = R"qasm( +OPENQASM 3.1; +bit[3] unsigned_bits; +unsigned_bits[0] = true; +unsigned_bits[1] = true; +unsigned_bits[2] = false; +bit[3] signed_bits; +signed_bits[0] = true; +signed_bits[1] = false; +signed_bits[2] = true; +output uint unsigned_value; +unsigned_value = uint[3](unsigned_bits); +output int signed_value; +signed_value = int[3](signed_bits); +)qasm"; + + MLIRContext context; + auto moduleOp = qc::translateQASM3ToQC(source, &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + func::ReturnOp result; + moduleOp->walk([&](func::ReturnOp operation) { result = operation; }); + ASSERT_TRUE(result); + ASSERT_EQ(result.getNumOperands(), 2); + auto unsignedCast = result.getOperand(0).getDefiningOp(); + auto signedCast = result.getOperand(1).getDefiningOp(); + ASSERT_TRUE(unsignedCast); + ASSERT_TRUE(signedCast); + EXPECT_TRUE(unsignedCast.getIn().getDefiningOp()); + EXPECT_TRUE(signedCast.getIn().getDefiningOp()); } TEST(OpenQASMTargetTest, ZeroInitializesUnmeasuredOpenQASM2Registers) { @@ -1334,13 +1349,11 @@ if (c == 0) x q[0]; ASSERT_EQ(returned.size(), 2); EXPECT_TRUE(llvm::none_of( returned, [](const auto& value) { return value.has_value(); })); - size_t conditionals = 0; size_t xGates = 0; - moduleOp->walk([&](scf::IfOp) { ++conditionals; }); + moduleOp->walk([&](cbit::AllocOp alloc) { + EXPECT_EQ(alloc.getInitialization(), cbit::Initialization::Zero); + }); moduleOp->walk([&](qc::XOp) { ++xGates; }); - // Zero initialization proves the condition true and removes both - // short-circuit conditionals. - EXPECT_EQ(conditionals, 0); EXPECT_EQ(xGates, 1); } @@ -1595,6 +1608,33 @@ switch (selector) { EXPECT_EQ(switches, 1); } +TEST(OpenQASMTargetTest, PreservesNarrowUnsignedSwitchValues) { + constexpr llvm::StringLiteral source = R"qasm(OPENQASM 3.1; +uint[3] selector = 7; +output int result; +result = 0; +switch (selector) { + case 7 { result = 42; } + default { result = -1; } +} +)qasm"; + MLIRContext context; + auto moduleOp = qc::translateQASM3ToQC(source, &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + PassManager canonicalizer(&context); + canonicalizer.addPass(createCanonicalizerPass()); + ASSERT_TRUE(succeeded(canonicalizer.run(*moduleOp))); + + auto function = *moduleOp->getOps().begin(); + auto result = + cast(function.getBody().front().getTerminator()); + ASSERT_EQ(result.getNumOperands(), 1); + APInt value; + ASSERT_TRUE(matchPattern(result.getOperand(0), m_ConstantInt(&value))); + EXPECT_EQ(value.getSExtValue(), 42); +} + TEST(OpenQASMTargetTest, LoadsDynamicQubitMeasurementsDirectly) { constexpr llvm::StringLiteral source = R"qasm( OPENQASM 3.1; @@ -2060,7 +2100,7 @@ for int i in [0:stride:6] { x q[i]; } EXPECT_EQ(text.find("i128"), std::string::npos); } -TEST(OpenQASMTargetTest, LowersCheckedIntegerArithmeticAtQCTarget) { +TEST(OpenQASMTargetTest, LowersIntegerArithmeticAtItsMachineWidth) { constexpr auto sources = std::to_array({ R"qasm( OPENQASM 3.1; @@ -2085,9 +2125,9 @@ if (value + 1 > 0) { x q; })qasm", auto moduleOp = qc::translateQASM3ToQC(source, &context); ASSERT_TRUE(moduleOp); ASSERT_TRUE(succeeded(verify(*moduleOp))); - size_t assertions = 0; - moduleOp->walk([&](cf::AssertOp) { ++assertions; }); - EXPECT_GE(assertions, 1); + moduleOp->walk([&](arith::AddIOp add) { + EXPECT_TRUE(add.getType().isInteger(64) || add.getType().isIndex()); + }); } } @@ -2118,11 +2158,8 @@ unsignedValue = unsignedValue ** unsignedOperand; auto moduleOp = qc::translateQASM3ToQC(source, &context); ASSERT_TRUE(moduleOp); ASSERT_TRUE(succeeded(verify(*moduleOp))); - size_t assertions = 0; size_t powerLoops = 0; - moduleOp->walk([&](cf::AssertOp) { ++assertions; }); moduleOp->walk([&](scf::WhileOp) { ++powerLoops; }); - EXPECT_GE(assertions, 7); EXPECT_EQ(powerLoops, 2); } diff --git a/mlir/unittests/Target/OpenQASM/test_openqasm_parser.cpp b/mlir/unittests/Target/OpenQASM/test_openqasm_parser.cpp index b95bebc929..4f0dc9625f 100644 --- a/mlir/unittests/Target/OpenQASM/test_openqasm_parser.cpp +++ b/mlir/unittests/Target/OpenQASM/test_openqasm_parser.cpp @@ -561,6 +561,9 @@ OPENQASM 3.1; const uint WIDTH = 8; const angle[WIDTH] fixed = angle[WIDTH](pi / 2); angle machine = angle(tau / 4); +bit[2] value; +if (uint[2](value) == 3) {} +if (int[2](value) == -1) {} )qasm"; auto parsed = oq3::frontend::parseOpenQASM(source); diff --git a/mlir/unittests/Target/OpenQASM/test_openqasm_semantics.cpp b/mlir/unittests/Target/OpenQASM/test_openqasm_semantics.cpp index f138e1cee8..351dc68e97 100644 --- a/mlir/unittests/Target/OpenQASM/test_openqasm_semantics.cpp +++ b/mlir/unittests/Target/OpenQASM/test_openqasm_semantics.cpp @@ -56,16 +56,14 @@ x q; EXPECT_TRUE(oq3::frontend::analyzeOpenQASM(v31)); } -TEST(OpenQASMFrontendTest, RejectsUnsupportedIntegerDeclarations) { +TEST(OpenQASMFrontendTest, AcceptsSizedIntegerDeclarations) { constexpr llvm::StringLiteral source = R"qasm( OPENQASM 3.1; -int[32] counter; +int[32] counter = 0; )qasm"; auto analyzed = oq3::frontend::analyzeOpenQASM(source); - ASSERT_FALSE(analyzed); - ASSERT_FALSE(analyzed.diagnostics.empty()); - EXPECT_NE(analyzed.diagnostics.front().message.find("Integer declarations"), - std::string::npos); + ASSERT_TRUE(analyzed); + EXPECT_EQ(analyzed.program->scalars.front().integerWidth, 32); } TEST(OpenQASMFrontendTest, RejectsTooFewVariadicControlOperands) { @@ -153,7 +151,7 @@ TEST(OpenQASMFrontendTest, RejectsUnprovedQuantumIndices) { "cannot prove that qubit index is in bounds"}, {"OPENQASM 3.1; qubit[2] q; bit b = measure q[0]; int i = b; " "x q[i];", - "not a scalar value"}, + "cannot prove that qubit index is in bounds"}, {"OPENQASM 3.1; qubit[16] q; for int i in [0:3] { x q[i * i]; }", "cannot prove that qubit index is in bounds"}, {"OPENQASM 3.1; qubit[4] q; for int i in [0:3] { x q[i / 1]; }", @@ -170,7 +168,7 @@ TEST(OpenQASMFrontendTest, RejectsUnprovedQuantumIndices) { "x q[i - 9223372036854775806]; }", "cannot prove that qubit index is in bounds"}, {"OPENQASM 3.1; qubit[4] q; for int i in [0:1] { x q[i << 1]; }", - "not supported yet"}, + "explicitly sized uint"}, {"OPENQASM 3.1; qubit[4] q; for int i in [3:-1:0] { x q[i]; }", "cannot prove that qubit index is in bounds"}, {"OPENQASM 3.1; qubit[4] q; int i = 4; if (i < 4) { x q[i]; }", @@ -525,6 +523,9 @@ TEST(OpenQASMFrontendTest, RejectsMutableGlobalCapturesInGateBodies) { std::to_array>({ {"mutable-capture", "OPENQASM 3.1; float theta = 0.5; gate g q { rx(theta) q; }"}, + {"bit-register-capture", + "OPENQASM 3.1; bit[2] c; c[0] = true; c[1] = false; " + "gate g q { rz(uint[2](c)) q; }"}, {"declaration", "OPENQASM 3.1; gate g q { int i = 0; }"}, {"measurement", "OPENQASM 3.1; bit c; gate g q { measure q -> c; }"}, {"reset", "OPENQASM 3.1; gate g q { reset q; }"}, @@ -626,6 +627,43 @@ out = true; EXPECT_TRUE(preserved) << preserved.diagnostics.front().message; } +TEST(OpenQASMFrontendTest, InvalidatesBitRegisterCastIndexFactsOnBitMutation) { + constexpr llvm::StringLiteral source = R"qasm( +OPENQASM 3.1; +bit[2] source; +source[0] = true; +source[1] = false; +bit[2] target; +target[uint[2](source)] = true; +source[0] = false; +qubit q; +if (target[uint[2](source)]) { x q; } +output bit out; +out = true; +)qasm"; + + auto analyzed = oq3::frontend::analyzeOpenQASM(source); + ASSERT_FALSE(analyzed); + ASSERT_FALSE(analyzed.diagnostics.empty()); + EXPECT_NE(analyzed.diagnostics.front().message.find("uninitialized bit"), + std::string::npos); + + constexpr llvm::StringLiteral noMutation = R"qasm( +OPENQASM 3.1; +bit[2] source; +source[0] = true; +source[1] = false; +bit[2] target; +target[uint[2](source)] = true; +qubit q; +if (target[uint[2](source)]) { x q; } +output bit out; +out = true; +)qasm"; + auto preserved = oq3::frontend::analyzeOpenQASM(noMutation); + EXPECT_TRUE(preserved) << preserved.diagnostics.front().message; +} + TEST(OpenQASMFrontendTest, RejectsBoolMeasurementTargetsInAllSourceModes) { constexpr auto sourcePrograms = std::to_array({ "qubit q; bool measured = measure q;", @@ -1346,18 +1384,6 @@ TEST(OpenQASMFrontendTest, RejectsInvalidProgramsAcrossSemanticFamilies) { .source = "OPENQASM 3.1; int value = 1; if (value) {}"}, {.name = "bool-compound-assignment", .source = "OPENQASM 3.1; bool value = true; value += false;"}, - {.name = "unsupported-bitwise-not", - .source = "OPENQASM 3.1; int value = ~1;"}, - {.name = "unsupported-bitwise-and", - .source = "OPENQASM 3.1; int value = 1 & 2;"}, - {.name = "unsupported-bitwise-or", - .source = "OPENQASM 3.1; int value = 1 | 2;"}, - {.name = "unsupported-bitwise-xor", - .source = "OPENQASM 3.1; int value = 1 ^ 2;"}, - {.name = "unsupported-shift-left", - .source = "OPENQASM 3.1; int value = 1 << 2;"}, - {.name = "unsupported-shift-right", - .source = "OPENQASM 3.1; int value = 2 >> 1;"}, {.name = "uninitialized-scalar", .source = "OPENQASM 3.1; int x; int y = x + 1;"}, {.name = "self-initialization", .source = "OPENQASM 3.1; int x = x + 1;"}, @@ -1670,6 +1696,93 @@ if(c==1180591620717411303433) x q[0]; })); } +TEST(OpenQASMFrontendTest, PromotesNarrowUnsignedBitRegisterCastToInt) { + constexpr llvm::StringLiteral source = R"qasm( +OPENQASM 3.1; +qubit[2] q; +bit[2] value = measure q; +if (uint[2](value) < -1) {} +)qasm"; + + auto analyzed = oq3::frontend::analyzeOpenQASM(source); + ASSERT_TRUE(analyzed) << analyzed.diagnostics.front().message; + const auto comparison = + llvm::find_if(analyzed.program->conditions, [](const auto& condition) { + return condition.kind == oq3::frontend::ConditionKind::Comparison; + }); + ASSERT_NE(comparison, analyzed.program->conditions.end()); + EXPECT_EQ(analyzed.program->expressions[comparison->comparisonLhs].type, + oq3::frontend::ScalarType::Int); + EXPECT_EQ(analyzed.program->expressions[comparison->comparisonRhs].type, + oq3::frontend::ScalarType::Int); +} + +TEST(OpenQASMFrontendTest, RejectsUnsupportedBitRegisterExpressionOperands) { + struct InvalidExpression { + llvm::StringRef source; + llvm::StringRef diagnostic; + }; + constexpr auto invalidExpressions = std::to_array({ + {.source = "OPENQASM 3.1; qubit[3] q; bit[3] value = measure q; " + "if ((value ^ 8) == 0) {}", + .diagnostic = "fit the operand width"}, + {.source = "OPENQASM 3.1; qubit[3] q; bit[3] value = measure q; " + "int distance = 1; " + "if ((value << distance) == 0) {}", + .diagnostic = "must have unsigned integer type"}, + {.source = "OPENQASM 3.1; qubit[3] q; bit[3] value = measure q; " + "if ((value >> -1) == 0) {}", + .diagnostic = "must be nonnegative"}, + }); + + for (const auto& invalid : invalidExpressions) { + SCOPED_TRACE(invalid.source.str()); + auto analyzed = oq3::frontend::analyzeOpenQASM(invalid.source); + ASSERT_FALSE(analyzed); + ASSERT_FALSE(analyzed.diagnostics.empty()); + EXPECT_NE(analyzed.diagnostics.front().message.find(invalid.diagnostic), + std::string::npos) + << analyzed.diagnostics.front().message; + } +} + +TEST(OpenQASMFrontendTest, RejectsInvalidSizedBitRegisterCasts) { + struct InvalidCast { + llvm::StringRef source; + llvm::StringRef diagnostic; + }; + constexpr auto invalidCasts = std::to_array({ + {.source = "OPENQASM 3.1; qubit[2] q; bit[2] value = measure q; " + "uint result = uint[3](value);", + .diagnostic = "must match the bit-register width"}, + {.source = "OPENQASM 3.1; qubit[65] q; bit[65] value = measure q; " + "uint result = uint[65](value);", + .diagnostic = "supports widths from 1 through 64"}, + {.source = "OPENQASM 3.1; bit[2] value; " + "uint result = uint[2](value);", + .diagnostic = "has not been initialized"}, + {.source = "OPENQASM 3.1; bit[2] value; " + "uint result = uint(value);", + .diagnostic = "has not been initialized"}, + {.source = "OPENQASM 3.1; uint width = 2; bit[2] value; " + "uint result = uint[width](value);", + .diagnostic = "must be a constant integer expression"}, + {.source = "OPENQASM 3.1; bit[2] value; " + "uint result = uint[true](value);", + .diagnostic = "must be an integer expression"}, + }); + + for (const auto& invalid : invalidCasts) { + SCOPED_TRACE(invalid.source.str()); + auto analyzed = oq3::frontend::analyzeOpenQASM(invalid.source); + ASSERT_FALSE(analyzed); + ASSERT_FALSE(analyzed.diagnostics.empty()); + EXPECT_NE(analyzed.diagnostics.front().message.find(invalid.diagnostic), + std::string::npos) + << analyzed.diagnostics.front().message; + } +} + TEST(OpenQASMFrontendTest, AcceptsWideIntegerLiteralWithDigitSeparatorsInOpenQASM2If) { // Same value as above, spelled with grammar-legal digit separators. diff --git a/mlir/unittests/programs/qasm_programs.cpp b/mlir/unittests/programs/qasm_programs.cpp index b853ac22ac..44df3a1d49 100644 --- a/mlir/unittests/programs/qasm_programs.cpp +++ b/mlir/unittests/programs/qasm_programs.cpp @@ -1471,11 +1471,11 @@ llvm::ArrayRef jeffCompatiblePrograms() { llvm::ArrayRef jeffIncompatiblePrograms() { static const std::array programs{ + OpenQASMProgram{.name = "integer-to-floating-gate-parameter", + .source = bitVectorBuiltins}, OpenQASMProgram{.name = "checked-integer-state", .source = checkedIntegerState}, OpenQASMProgram{.name = "dynamic-range", .source = dynamicRange}, - OpenQASMProgram{.name = "bit-vector-builtins", - .source = bitVectorBuiltins}, }; return programs; } diff --git a/test/python/test_mlir_integer_interchange.py b/test/python/test_mlir_integer_interchange.py new file mode 100644 index 0000000000..034ec0d3d3 --- /dev/null +++ b/test/python/test_mlir_integer_interchange.py @@ -0,0 +1,300 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Observable fixed-width integer behavior across compiler format boundaries.""" + +from __future__ import annotations + +import os + +import pytest +import qiskit +from packaging.version import Version + +from mqt.core.mlir import JeffProgram, QCProgram + +supports_qiskit_translation = Version("2.5.0") <= Version(qiskit.__version__) < Version( + "2.6.0" +) or qiskit.__version__ == os.environ.get("MQT_QISKIT_TEST_CANDIDATE_VERSION") + + +def _program(width: int, body: str, result_width: int, initial: int) -> QCProgram: + return QCProgram.from_mlir_str(f""" +module {{ + func.func @main() -> !cbit.reg<{result_width}> attributes {{mqt.entry_point}} {{ + %q = qc.alloc : !qc.qubit + {"qc.x %q : !qc.qubit" if initial & 1 else ""} + %source = cbit.alloc(#cbit.init) : !cbit.reg<{width}> + %initial = arith.constant {initial} : i{width} + cbit.write %initial, %source : i{width}, !cbit.reg<{width}> + %index = arith.constant 0 : index + %bit = qc.measure %q : !qc.qubit -> i1 + cbit.store %bit, %source[%index] : !cbit.reg<{width}> + %value = cbit.read %source : !cbit.reg<{width}> -> i{width} + %output = cbit.alloc(#cbit.init) : !cbit.reg<{result_width}> + {body} + cbit.write %result, %output : i{result_width}, !cbit.reg<{result_width}> + qc.dealloc %q : !qc.qubit + return %output : !cbit.reg<{result_width}> + }} +}} +""") + + +def _observe(program: QCProgram, width: int) -> int: + qco = program.to_qco(copy=True) + try: + counts = qco.sample(shots=1, seed=1) + except ValueError as error: + pytest.fail(f"{error}\n{qco}") + bits = next(iter(counts)).replace(" ", "") + return int(bits[:width], 2) + + +def _check_paths(program: QCProgram, width: int, expected: int) -> None: + assert _observe(program, width) == expected + restored_qasm = QCProgram.from_qasm_str(program.to_openqasm3().source) + assert _observe(restored_qasm, width) == expected + jeff = program.to_qco(copy=True).to_jeff() + restored_jeff = JeffProgram.from_bytes(jeff.to_bytes()).to_qco().to_qc() + assert _observe(restored_jeff, width) == expected + assert _observe(QCProgram.from_qasm_str(restored_jeff.to_openqasm3().source), width) == expected + if supports_qiskit_translation: + for candidate in (program, restored_jeff): + assert _observe(QCProgram.from_qiskit(candidate.to_qiskit()), width) == expected + + +@pytest.mark.parametrize("width", [1, 3, 8, 9, 32, 64]) +@pytest.mark.parametrize("predicate", ["eq", "ne", "ult", "ule", "ugt", "uge", "slt", "sle", "sgt", "sge"]) +def test_integer_comparison_interchange(width: int, predicate: str) -> None: + """Signedness belongs to the operation, including computed operands.""" + high = 1 << (width - 1) + program = _program( + width, + f""" + %zero = arith.constant 0 : i{width} + %computed = arith.xori %value, %zero : i{width} + %mask = arith.constant {(1 << width) - 1} : i{width} + %other = arith.xori %value, %mask : i{width} + %result = arith.cmpi {predicate}, %computed, %other : i{width} + """, + 1, + high, + ) + expected = int(predicate in {"ne", "ugt", "uge", "slt", "sle"}) + _check_paths(program, 1, expected) + program.cleanup() + _check_paths(program, 1, expected) + + +@pytest.mark.parametrize("width", [1, 3, 8, 9, 32, 64]) +def test_modular_arithmetic_and_selection(width: int) -> None: + """Arithmetic and selection preserve the original width after promotion.""" + maximum = (1 << width) - 1 + program = _program( + width, + f""" + %one = arith.constant 1 : i{width} + %sum = arith.addi %value, %one : i{width} + %condition = arith.cmpi ne, %sum, %one : i{width} + %result = arith.select %condition, %sum, %value : i{width} + """, + width, + maximum, + ) + _check_paths(program, width, 0) + + +@pytest.mark.parametrize("width", [1, 3, 8, 9, 32, 64]) +def test_logical_right_shift_high_bit(width: int) -> None: + """The jeff adapter must not interpret a logical shift as signed.""" + high = 1 << (width - 1) + program = _program( + width, + f""" + %distance = arith.constant {width - 1} : i{width} + %result = arith.shrui %value, %distance : i{width} + """, + width, + high, + ) + _check_paths(program, width, 1) + + +@pytest.mark.parametrize(("source_width", "target_width"), [(1, 3), (3, 8), (8, 9), (9, 32), (32, 64), (64, 3)]) +@pytest.mark.parametrize("signed", [False, True]) +def test_integer_width_casts(source_width: int, target_width: int, *, signed: bool) -> None: + """Backend promotion must not change truncation or sign extension.""" + initial = (1 << source_width) - 1 + operation = "arith.trunci" if target_width < source_width else "arith.extsi" if signed else "arith.extui" + program = _program( + source_width, + f""" + %result = {operation} %value : i{source_width} to i{target_width} + """, + target_width, + initial, + ) + expected = (1 << target_width) - 1 if signed or target_width < source_width else initial + _check_paths(program, target_width, expected) + program.cleanup() + _check_paths(program, target_width, expected) + + +@pytest.mark.parametrize("width", [1, 3, 8, 9, 32, 64]) +@pytest.mark.parametrize("runtime", [False, True]) +@pytest.mark.parametrize("direction", ["<<", ">>"]) +def test_source_zero_filling_shifts(width: int, *, runtime: bool, direction: str) -> None: + """Validate the original distance, including values lost when narrowing.""" + initial = 1 if direction == "<<" else 1 << (width - 1) + distances = [0, width - 1, width, 256] + for distance in distances: + amount = "uint[16](distance)" if runtime else str(distance) + program = QCProgram.from_qasm_str(f""" +OPENQASM 3.0; +include "stdgates.inc"; +qubit q; +{"x q;" if distance & 1 else ""} +bit[{width}] source = bit[{width}](uint[{width}]({initial})); +bit[16] distance = bit[16](uint[16]({distance})); +distance[0] = measure q; +bit[{width}] result; +result = source {direction} {amount}; +""") + expected = (initial << distance) & ((1 << width) - 1) if direction == "<<" else initial >> distance + _check_paths(program, width, expected) + program.cleanup() + _check_paths(program, width, expected) + + +@pytest.mark.parametrize("width", [65, 301]) +def test_wide_comparison_snapshot_through_jeff(width: int) -> None: + """Shared comparisons read the old register, even across a later store.""" + program = QCProgram.from_mlir_str(f""" +module {{ + func.func @main() -> !cbit.reg<1> attributes {{mqt.entry_point}} {{ + %q = qc.alloc : !qc.qubit + qc.h %q : !qc.qubit + %source = cbit.alloc(#cbit.init) : !cbit.reg<{width}> + %position = arith.constant {width - 1} : index + %true = arith.constant true + %false = arith.constant false + cbit.store %true, %source[%position] : !cbit.reg<{width}> + %snapshot = cbit.read %source : !cbit.reg<{width}> -> i{width} + cbit.store %false, %source[%position] : !cbit.reg<{width}> + %high = arith.constant {1 << (width - 1)} : i{width} + %zero = arith.constant 0 : i{width} + %equal = arith.cmpi eq, %snapshot, %high : i{width} + %positive = arith.cmpi ult, %zero, %snapshot : i{width} + %result = arith.andi %equal, %positive : i1 + %output = cbit.alloc(#cbit.init) : !cbit.reg<1> + cbit.write %result, %output : i1, !cbit.reg<1> + qc.dealloc %q : !qc.qubit + return %output : !cbit.reg<1> + }} +}} +""") + for cleanup in [False, True]: + if cleanup: + program.cleanup() + assert _observe(program, 1) == 1 + jeff = program.to_qco(copy=True).to_jeff() + restored = JeffProgram.from_bytes(jeff.to_bytes()).to_qco().to_qc() + assert _observe(restored, 1) == 1 + + +@pytest.mark.parametrize("width", [1, 3, 8, 9, 32, 64]) +def test_signed_right_shift_high_bit(width: int) -> None: + """Arithmetic shifts retain the original sign bit in promoted integers.""" + program = _program( + width, + f""" + %distance = arith.constant {width - 1} : i{width} + %result = arith.shrsi %value, %distance : i{width} + """, + width, + 1 << (width - 1), + ) + _check_paths(program, width, (1 << width) - 1) + + +@pytest.mark.parametrize("width", [1, 3, 8, 9, 32, 64]) +@pytest.mark.parametrize("operation", ["popcount", "rotate_left", "rotate_right"]) +def test_integer_intrinsics(width: int, operation: str) -> None: + """Targets without integer intrinsics use the shared arithmetic lowering.""" + high = 1 << (width - 1) + if operation == "popcount": + body = f"%result = math.ctpop %value : i{width}" + _check_paths(_program(width, body, width, (1 << width) - 1), width, width) + return + intrinsic = "fshl" if operation == "rotate_left" else "fshr" + for distance in [0, 1, width]: + body = f""" + %amount = arith.constant {distance} : i{width} + %result = llvm.intr.{intrinsic}(%value, %value, %amount) : (i{width}, i{width}, i{width}) -> i{width} + """ + amount = distance % width + expected = ( + ((high << amount) | (high >> (width - amount))) & ((1 << width) - 1) + if operation == "rotate_left" + else ((high >> amount) | (high << (width - amount))) & ((1 << width) - 1) + ) + _check_paths(_program(width, body, width, high), width, expected) + + +@pytest.mark.parametrize("runtime", [False, True]) +def test_narrow_unsigned_comparison_promotes(*, runtime: bool) -> None: + """Constant analysis and runtime analysis use the same integer promotion.""" + operand = "uint[3](input_bits)" if runtime else "uint[3](7)" + program = QCProgram.from_qasm_str(f""" +OPENQASM 3.0; +include "stdgates.inc"; +qubit q; +h q; +bit[3] input_bits = bit[3](uint[3](7)); +bit[1] result; +result = bit[1](uint[1]({operand} > -1)); +""") + _check_paths(program, 1, 1) + + +@pytest.mark.parametrize("literal", ["255", "-1"]) +def test_constant_integer_casts_truncate(literal: str) -> None: + """Explicit narrowing also applies before any runtime IR is produced.""" + program = QCProgram.from_qasm_str(f""" +OPENQASM 3.0; +include "stdgates.inc"; +qubit q; +h q; +const uint[3] narrowed = uint[3]({literal}); +bit[3] result = bit[3](narrowed); +""") + _check_paths(program, 3, 7) + + +def test_simulator_aliases_survive_value_map_growth() -> None: + """Constant-folded aliases must remain valid when the SSA map reallocates.""" + operations = ["%zero = arith.constant 0 : i8", "%alias0 = arith.ori %value, %zero : i8"] + operations.extend(f"%alias{i} = arith.ori %alias{i - 1}, %zero : i8" for i in range(1, 1024)) + operations.append("%result = arith.ori %alias1023, %zero : i8") + assert _observe(_program(8, "\n".join(operations), 8, 173), 8) == 173 + + +def test_jeff_rejects_wider_general_integer_expressions() -> None: + """The wide comparison fast path must not imply general multiword support.""" + program = _program( + 65, + """ + %mask = arith.constant 3 : i65 + %result = arith.andi %value, %mask : i65 + """, + 65, + 1, + ) + with pytest.raises(RuntimeError, match="MLIR operation failed"): + program.to_qco().to_jeff() diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index 4eea8934c2..3228793d94 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -33,6 +33,7 @@ ParameterVector, PowerModifier, Qubit, + Store, library, ) from qiskit.circuit.classical import expr, types @@ -500,8 +501,8 @@ def test_openqasm2_measurements_export_with_zero_initialized_register() -> None: @pytest.mark.parametrize("late_value", ["false", "true"]) -def test_flat_export_rejects_classical_store_after_quantum_work(late_value: str) -> None: - """Reject constant CBit stores regardless of their position.""" +def test_flat_export_preserves_classical_store_order(late_value: str) -> None: + """Preserve constant CBit stores around quantum work.""" program = QCProgram.from_mlir_str( f"""module {{ func.func @main() -> !cbit.reg<2> attributes {{mqt.entry_point}} {{ @@ -521,8 +522,11 @@ def test_flat_export_rejects_classical_store_after_quantum_work(late_value: str) """ ) - with pytest.raises(RuntimeError, match="does not support non-measurement classical stores"): - program.to_qiskit() + restored = program.to_qiskit() + + assert [instruction.operation.name for instruction in restored.data] == ["store", "x", "store"] + assert not restored.data[0].operation.rvalue.value + assert bool(restored.data[2].operation.rvalue.value) is (late_value == "true") def test_target_compiled_openqasm2_measurements_export() -> None: @@ -594,18 +598,73 @@ def test_openqasm_register_ordering_exports_to_qiskit_expression() -> None: restored = program.to_qiskit() condition = restored.data[2].operation.condition - assert "cbit.cmp uge" in program.ir + assert "arith.cmpi uge" in program.ir assert isinstance(condition, expr.Expr) assert expr.structurally_equivalent(condition, expr.greater_equal(restored.cregs[0], 1)) reimported = QCProgram.from_qiskit(restored) - assert "cbit.cmp uge" in reimported.ir + assert "arith.cmpi uge" in reimported.ir reimported_circuit = reimported.to_qiskit() reimported_condition = reimported_circuit.data[2].operation.condition assert isinstance(reimported_condition, expr.Expr) assert expr.structurally_equivalent(reimported_condition, expr.greater_equal(reimported_circuit.cregs[0], 1)) +def test_openqasm_signed_register_ordering_exports_to_qiskit_uint_expression() -> None: + """Encode signed register ordering with Qiskit's unsigned expressions.""" + program = QCProgram.from_qasm_str( + """OPENQASM 3.1; +include "stdgates.inc"; +qubit[4] q; +bit[3] c; +c[0] = measure q[0]; +c[1] = measure q[1]; +c[2] = measure q[2]; +if (int[3](c) < -1) { x q[3]; } +""" + ) + + restored = program.to_qiskit() + assert "arith.cmpi slt" in program.ir + reimported_program = QCProgram.from_qiskit(restored) + assert reimported_program.is_valid + assert reimported_program.to_qiskit() is not None + + qasm_reimported = QCProgram.from_qasm_str(qiskit.qasm3.dumps(restored)) + assert qasm_reimported.is_valid + + +def test_qiskit_lossless_register_cast_imports_canonically() -> None: + """Canonicalize a lossless Uint widening around a register comparison.""" + circuit = QuantumCircuit(2, 3) + circuit.measure(0, 0) + uint8 = types.Uint(8) + condition = expr.equal(expr.cast(circuit.cregs[0], uint8), expr.lift(3, uint8)) + with circuit.if_test(condition): + circuit.x(1) + + program = QCProgram.from_qiskit(circuit) + program.cleanup() + ir = program.ir + + assert "arith.cmpi eq" in ir + assert "cbit.load" not in ir + + +def test_qiskit_lossy_register_cast_remains_an_expression() -> None: + """Do not treat a truncating Uint cast as a whole-register read.""" + circuit = QuantumCircuit(1, 3) + uint2 = types.Uint(2) + condition = expr.equal(expr.cast(circuit.cregs[0], uint2), expr.lift(1, uint2)) + with circuit.if_test(condition): + circuit.x(0) + + ir = QCProgram.from_qiskit(circuit).ir + + assert "arith.trunci" in ir + assert "cbit.read" in ir + + @pytest.mark.parametrize( ("comparison", "predicate"), [ @@ -627,7 +686,7 @@ def test_qiskit_register_conditions_import_canonically(comparison: str | None, p ir = QCProgram.from_qiskit(circuit).ir - assert f"cbit.cmp {predicate}" in ir + assert f"arith.cmpi {predicate}" in ir assert "cbit.load" not in ir @@ -648,10 +707,10 @@ def test_qiskit_reversed_register_conditions_import_canonically(comparison: str, with circuit.if_test(getattr(expr, comparison)(1, circuit.cregs[0])): circuit.x(0) - ir = QCProgram.from_qiskit(circuit).ir - - assert f"cbit.cmp {predicate}" in ir - assert "cbit.load" not in ir + circuit.measure(0, 0) + histogram = QCProgram.from_qiskit(circuit).to_qco().sample(shots=1, seed=1) + expected = "001" if predicate in {"ne", "ult", "ule"} else "000" + assert histogram == {expected: 1} def test_qiskit_oversized_tuple_condition_is_false() -> None: @@ -663,12 +722,142 @@ def test_qiskit_oversized_tuple_condition_is_false() -> None: program = QCProgram.from_qiskit(circuit) assert "arith.constant false" in program.ir - assert "cbit.cmp" not in program.ir + assert "arith.cmpi" not in program.ir condition = program.to_qiskit().data[0].operation.condition assert isinstance(condition, expr.Value) assert condition.value == 0 +def test_qiskit_store_round_trip_preserves_register_semantics() -> None: + """Import and export bit, register, and runtime-indexed assignments.""" + data = ClassicalRegister(3, "data") + index = ClassicalRegister(2, "index") + circuit = QuantumCircuit(data, index) + circuit.store( + data[1], + True, # ruff: ignore[boolean-positional-value-in-call] Qiskit Store arguments are positional-only. + ) + circuit.store(data, expr.bit_xor(data, 3)) + circuit.store( + expr.index(data, index), + False, # ruff: ignore[boolean-positional-value-in-call] Qiskit Store arguments are positional-only. + ) + + program = QCProgram.from_qiskit(circuit) + restored = program.to_qiskit() + + assert program.ir.count("cbit.store") == 2 + assert "cbit.write" in program.ir + assert "cbit.read" in program.ir + assert "arith.index_castui" in program.ir + assert [instruction.operation.name for instruction in restored.data] == ["store"] * 3 + expected = ( + ( + expr.lift(restored.cregs[0][1]), + expr.lift( + True, # ruff: ignore[boolean-positional-value-in-call] Qiskit expression arguments are positional-only. + ), + ), + (expr.lift(restored.cregs[0]), expr.bit_xor(restored.cregs[0], 3)), + ( + expr.index(restored.cregs[0], restored.cregs[1]), + expr.lift( + False, # ruff: ignore[boolean-positional-value-in-call] Qiskit expression arguments are positional-only. + ), + ), + ) + for instruction, (lvalue, rvalue) in zip(restored.data, expected, strict=True): + assert isinstance(instruction.operation, Store) + assert expr.structurally_equivalent(instruction.operation.lvalue, lvalue) + assert expr.structurally_equivalent(instruction.operation.rvalue, rvalue) + + +def test_qiskit_store_round_trip_inside_control_flow() -> None: + """Keep register metadata when rebasing a control-flow Store body.""" + register = ClassicalRegister(3, "c") + circuit = QuantumCircuit(register) + with circuit.if_test(expr.lift(register[0])): + circuit.store(register, 5) + + restored = QCProgram.from_qiskit(circuit).to_qiskit() + + body = restored.data[0].operation.blocks[0] + assert body.cregs == restored.cregs + assert len(body.data) == 1 + operation = body.data[0].operation + assert isinstance(operation, Store) + assert expr.structurally_equivalent(operation.lvalue, expr.lift(body.cregs[0])) + assert expr.structurally_equivalent(operation.rvalue, expr.lift(5, types.Uint(3))) + + +def test_qiskit_store_preserves_width_one_uint_contexts() -> None: + """Cast i1 values to Uint(1) for register and index operands.""" + program = QCProgram.from_mlir_str( + """module { + func.func @main() -> (!cbit.reg<2>, !cbit.reg<1>) attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + %data = cbit.alloc(#cbit.init) {mqt.register_name = "data"} : !cbit.reg<2> + %index = cbit.alloc(#cbit.init) {mqt.register_name = "index"} : !cbit.reg<1> + %zero = arith.constant 0 : index + %true = arith.constant true + cbit.write %true, %index : i1, !cbit.reg<1> + %bit = cbit.load %index[%zero] : !cbit.reg<1> + %target = arith.index_castui %bit : i1 to index + %false = arith.constant false + cbit.store %false, %data[%target] : !cbit.reg<2> + qc.dealloc %q : !qc.qubit + return %data, %index : !cbit.reg<2>, !cbit.reg<1> + } +} +""" + ) + + register_store, indexed_store = (item.operation for item in program.to_qiskit().data) + + assert isinstance(register_store, Store) + assert register_store.rvalue.type == types.Uint(1) + assert isinstance(indexed_store, Store) + assert indexed_store.lvalue.index.type == types.Uint(1) + + +def test_qiskit_store_rejects_stale_dynamic_index() -> None: + """Reject an index snapshot that Qiskit would re-evaluate after a write.""" + program = QCProgram.from_mlir_str( + """module { + func.func @main() -> (!cbit.reg<4>, !cbit.reg<2>) attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + %data = cbit.alloc(#cbit.init) {mqt.register_name = "data"} : !cbit.reg<4> + %index = cbit.alloc(#cbit.init) {mqt.register_name = "index"} : !cbit.reg<2> + %old_index = cbit.read %index : !cbit.reg<2> -> i2 + %zero = arith.constant 0 : index + %true = arith.constant true + cbit.store %true, %index[%zero] : !cbit.reg<2> + %target = arith.index_castui %old_index : i2 to index + %false = arith.constant false + cbit.store %false, %data[%target] : !cbit.reg<4> + qc.dealloc %q : !qc.qubit + return %data, %index : !cbit.reg<4>, !cbit.reg<2> + } +} +""" + ) + + with pytest.raises(RuntimeError, match="cannot preserve a stale classical snapshot"): + program.to_qiskit() + + +def test_qiskit_custom_gate_named_store_remains_a_gate() -> None: + """Use the Store type, not its public name, to classify assignments.""" + definition = QuantumCircuit(1) + definition.x(0) + gate = Gate("store", 1, []) + gate.definition = definition + circuit = QuantumCircuit(1) + circuit.append(gate, [0]) + + assert "qc.x" in QCProgram.from_qiskit(circuit).ir + + 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( @@ -779,8 +968,8 @@ def test_qiskit_export_rejects_mixed_sentinel_and_cbit_results() -> None: program.to_qiskit() -def test_qiskit_round_trip_preserves_anonymous_clbits() -> None: - """Represent loose Qiskit clbits as one anonymous public CBit register.""" +def test_qiskit_round_trip_groups_anonymous_clbits() -> None: + """Represent loose Qiskit Clbits as one explicit public register.""" circuit = QuantumCircuit(1) circuit.add_bits([Clbit()]) circuit.measure(0, 0) @@ -790,7 +979,7 @@ def test_qiskit_round_trip_preserves_anonymous_clbits() -> None: assert "cbit.alloc(#cbit.init) : !cbit.reg<1>" in program.ir assert restored.num_clbits == 1 - assert restored.cregs == [] + assert [(register.name, len(register)) for register in restored.cregs] == [("_mqt_c0", 1)] assert restored.count_ops() == {"measure": 1} @@ -815,14 +1004,16 @@ def test_qiskit_export_excludes_internal_cbit_registers() -> None: assert [(register.name, len(register)) for register in restored.cregs] == [("output", 1)] -def test_qiskit_export_rejects_duplicate_measurement_destinations() -> None: - """Reject multiple measurements that write the same public bit.""" +def test_qiskit_export_preserves_repeated_measurement_destinations() -> None: + """Preserve sequential measurements that overwrite the same bit.""" circuit = QuantumCircuit(1, 1) circuit.measure(0, 0) circuit.measure(0, 0) - with pytest.raises(RuntimeError, match="duplicate classical destinations"): - QCProgram.from_qiskit(circuit).to_qiskit() + restored = QCProgram.from_qiskit(circuit).to_qiskit() + + assert restored.count_ops() == {"measure": 2} + assert [restored.find_bit(item.clbits[0]).index for item in restored.data] == [0, 0] def test_qiskit_export_rejects_measurement_with_multiple_destinations() -> None: @@ -1230,7 +1421,7 @@ def test_control_flow_and_controlled_unitary_preserve_instruction_order() -> Non @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.""" + """Keep root-register metadata in nested block conditions.""" 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)): @@ -1242,9 +1433,11 @@ def test_root_register_expression_and_nested_condition_preserve_captures(num_clb 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 + body = outer.blocks[0] + assert body.cregs == restored.cregs + inner = body.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) + assert {variable.var for variable in expr.iter_vars(inner.condition)} == {body.cregs[0]} def test_repeated_cbit_uint_expression_falls_back_to_expression_tree() -> None: @@ -1357,15 +1550,8 @@ def test_zero_qubit_cbit_only_control_flow_round_trip() -> None: 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) + assert len(restored.data) == 0 + assert QCProgram.from_qiskit(restored).to_qco().sample(shots=1, seed=1) == {"0": 1} def _single_qubit_program(operations: list[str], *, returns_classical: bool = False) -> QCProgram: @@ -1570,39 +1756,10 @@ def test_shared_expression_dag_expansion_is_bounded() -> None: "%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", "}"]) + operations.extend(f"%value{index} = arith.andi %value{index - 1}, %value{index - 1} : i1" for index in range(1, 15)) + operations.extend(["scf.if %value14 {", " 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"): + with pytest.raises(RuntimeError, match="size limit of 16384 nodes"): program.to_qiskit() @@ -1659,8 +1816,8 @@ def test_export_control_flow_depth_is_bounded() -> None: program.to_qiskit() -def test_nonboolean_result_bearing_if_is_rejected() -> None: - """Reject a result-bearing scf.if whose result is not Boolean.""" +def test_unused_nonboolean_result_bearing_if_is_omitted() -> None: + """Dead classical computations do not restrict circuit export.""" program = _single_qubit_program([ "%condition = arith.constant true", "%result = scf.if %condition -> (i64) {", @@ -1672,8 +1829,7 @@ def test_nonboolean_result_bearing_if_is_rejected() -> None: "}", "qc.x %q : !qc.qubit", ]) - with pytest.raises(RuntimeError, match="canonical short-circuit Boolean SSA result"): - program.to_qiskit() + assert program.to_qiskit().count_ops() == {"x": 1} @pytest.mark.parametrize( @@ -1711,6 +1867,39 @@ def test_stale_classical_snapshot_is_rejected(write_operations: tuple[str, ...]) program.to_qiskit() +@pytest.mark.parametrize( + "overwrite", + [ + "cbit.store %false, %classical[%zero] : !cbit.reg<1>", + "cbit.write %false, %classical : i1, !cbit.reg<1>", + """qc.x %q : !qc.qubit + %next = qc.measure %q : !qc.qubit -> i1 + cbit.store %next, %classical[%zero] : !cbit.reg<1>""", + """scf.if %measured { + cbit.store %false, %classical[%zero] : !cbit.reg<1> + }""", + ], + ids=["bit-store", "register-write", "measurement", "nested-store"], +) +def test_measurement_snapshot_rejects_overwritten_destination(overwrite: str) -> None: + """A measurement SSA value retains its value when its destination changes.""" + program = _single_qubit_program( + [ + '%classical = cbit.alloc(#cbit.init) {mqt.register_name = "c"} : !cbit.reg<1>', + "%zero = arith.constant 0 : index", + "%false = arith.constant false", + "qc.x %q : !qc.qubit", + "%measured = qc.measure %q : !qc.qubit -> i1", + "cbit.store %measured, %classical[%zero] : !cbit.reg<1>", + overwrite, + "scf.if %measured { 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( @@ -1824,12 +2013,10 @@ def test_conditional_measurement_does_not_initialize_returned_cbit() -> None: ("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 : index + %right = arith.constant 1 : index + %condition = arith.cmpi eq, %left, %right : index""", + "integer comparisons require integer operands", ), ( """%left = arith.constant 0 : i65 @@ -1837,12 +2024,6 @@ def test_conditional_measurement_does_not_initialize_returned_cbit() -> None: %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 @@ -1850,7 +2031,7 @@ def test_conditional_measurement_does_not_initialize_returned_cbit() -> None: "floating-point literals must be finite", ), ], - ids=["unsupported-op", "width", "signed-compare", "nonfinite"], + ids=["index", "width", "nonfinite"], ) def test_unsupported_export_expressions_fail_closed(expression: str, error: str) -> None: """Reject unsupported expression forms before modifying the source program.""" @@ -1899,27 +2080,15 @@ def test_qiskit_import_zero_initializes_clbits_before_control_flow() -> None: ) def test_bool_uint_and_float_expressions(condition: expr.Expr, operation: str) -> None: """Round-trip representative Bool, Uint, and Float expressions.""" - circuit = QuantumCircuit(1) + circuit = QuantumCircuit(1, 1) with circuit.if_test(condition): circuit.x(0) - + circuit.measure(0, 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) + restored = QCProgram.from_qiskit(program.to_qiskit()) + expected = "0" if operation in {"scf.if", "arith.xori"} else "1" + assert restored.to_qco().sample(shots=1, seed=1) == {expected: 1} def test_index_expression_export_preserves_low_bit() -> None: @@ -1997,12 +2166,12 @@ def test_uint_register_cast_to_bool_tests_all_bits() -> None: program = QCProgram.from_qiskit(circuit) ir = program.ir - assert "cbit.cmp ne" in 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 "cbit.cmp ne" in round_trip_ir + assert "arith.cmpi ne" in round_trip_ir assert "arith.trunci" not in round_trip_ir @@ -2086,22 +2255,38 @@ def test_classical_expression_clbit_captures_import() -> None: def test_classical_expression_register_captures_round_trip_on_import() -> None: - """Pack a captured register in Qiskit's little-endian bit order.""" - circuit = QuantumCircuit(1, 3) - condition = expr.equal(expr.bit_xor(circuit.cregs[0], 1), 5) + """Preserve a captured register and an in-range runtime shift.""" + circuit = QuantumCircuit(4, 3) + circuit.measure(range(3), range(3)) + distance = expr.bit_and(circuit.cregs[0], 1) + condition = expr.equal(expr.shift_left(expr.bit_xor(circuit.cregs[0], 1), distance), 2) with circuit.if_test(condition): - circuit.x(0) + circuit.x(3) program = QCProgram.from_qiskit(circuit) assert QCProgram.from_mlir_str(program.ir).ir == program.ir + assert QCProgram.from_qasm_str(qiskit.qasm3.dumps(circuit)).is_valid ir = program.ir - assert _cbit_load_indices(ir) == [0, 1, 2] - assert ir.count("arith.shli") == 2 + assert "cbit.read" in ir + assert "cbit.load" not in ir assert "arith.xori" in ir + assert "arith.shli" in ir assert "arith.cmpi eq" in ir +def test_width_one_register_bitwise_expression_round_trips() -> None: + """Keep one-bit register expressions typed as Qiskit Uint values.""" + circuit = QuantumCircuit(2, 1) + circuit.measure(0, 0) + condition = expr.equal(expr.bit_xor(circuit.cregs[0], 1), 0) + with circuit.if_test(condition): + circuit.x(1) + circuit.measure(1, 0) + restored = QCProgram.from_qiskit(QCProgram.from_qiskit(circuit).to_qiskit()) + assert restored.to_qco().sample(shots=1, seed=1) == {"0": 1} + + def test_nested_classical_expression_captures_import() -> None: """Compose nested local capture maps without changing root Clbit identity.""" circuit = QuantumCircuit(1, 3) @@ -2128,7 +2313,8 @@ def test_switch_expression_captures_import() -> None: ir = QCProgram.from_qiskit(circuit).ir - assert _cbit_load_indices(ir) == [0, 1] + assert "cbit.read" in ir + assert "cbit.load" not in ir assert "arith.xori" in ir assert "scf.index_switch" in ir @@ -2169,7 +2355,8 @@ def test_condition_only_switch_expression_imports() -> None: ir = QCProgram.from_qiskit(circuit).ir - assert _cbit_load_indices(ir) == [0, 1] + assert "cbit.read" in ir + assert "cbit.load" not in ir assert "arith.xori" in ir assert "scf.index_switch" in ir @@ -2236,7 +2423,7 @@ def test_excessively_nested_classical_expression_is_rejected() -> None: def test_oversized_classical_expression_is_rejected() -> None: """Bound the total size of a balanced classical expression.""" - level = [expr.equal(1, 1) for _ in range(1025)] + level = [expr.equal(1, 1) for _ in range(4097)] while len(level) > 1: level = [ expr.logic_or(level[index], level[index + 1]) if index + 1 < len(level) else level[index] @@ -2247,7 +2434,7 @@ def test_oversized_classical_expression_is_rejected() -> None: circuit.x(0) source_data = list(circuit.data) - with pytest.raises(RuntimeError, match="expressions exceed the node limit of 4096"): + with pytest.raises(RuntimeError, match="expressions exceed the node limit of 16384"): QCProgram.from_qiskit(circuit) assert list(circuit.data) == source_data