From 0141a0b4f8bbf63608f74fdd5b8608e2f2c40e95 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 28 Aug 2026 13:06:13 +0200 Subject: [PATCH 1/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Harden=20MLIR=20contra?= =?UTF-8?q?cts=20(#2255)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit every MQT-owned pass, verifier, rewrite pattern, conversion target, and registration surface. Harden diagnostics, atomicity, resource bounds, metadata checks, and importer boundaries. Add focused regressions across conversions, QIR, mapping, JEFF, OpenQASM, decision diagrams, and Qiskit interoperability. Assisted-by: GPT-5.6 Sol via Codex --- .agent/plans/audit-mlir-contracts.md | 402 ++++++++++ cmake/ExternalDependencies.cmake | 14 + docs/mlir/OpenQASM.md | 4 + .../mlir/Compiler/JeffDeserializerError.h | 26 + .../mlir/Compiler/JeffFatalErrorRedirect.h | 42 + mlir/include/mlir/Compiler/Programs.h | 10 + .../Conversion/CBitToMemRef/CBitToMemRef.td | 12 +- .../include/mlir/Conversion/ConversionUtils.h | 4 +- .../mlir/Conversion/JeffToQCO/JeffToQCO.td | 6 +- .../mlir/Conversion/QCOToJeff/QCOToJeff.td | 9 +- .../mlir/Conversion/QCOToQC/QCOToQC.td | 11 +- .../mlir/Conversion/QCToQCO/QCToQCO.td | 8 +- .../QCToQIR/QIRAdaptive/QCToQIRAdaptive.td | 21 +- .../Conversion/QCToQIR/QIRBase/QCToQIRBase.td | 26 +- .../Conversion/QCToQIR/QIRCommon/QIRCommon.h | 14 + mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h | 8 +- .../mlir/Dialect/MQT/Transforms/Passes.td | 3 +- mlir/include/mlir/Dialect/MQT/Utils/Angles.h | 10 + .../mlir/Dialect/MQT/Utils/ConstantFolding.h | 8 +- .../mlir/Dialect/MQT/Utils/Modifiers.h | 125 ++- mlir/include/mlir/Dialect/QC/IR/QCOps.td | 4 +- .../mlir/Dialect/QC/Transforms/Passes.td | 1 + mlir/include/mlir/Dialect/QCO/IR/QCOOps.td | 5 +- mlir/include/mlir/Dialect/QCO/QCOUtils.h | 106 ++- .../mlir/Dialect/QCO/Transforms/Passes.td | 71 +- .../mlir/Dialect/QIR/Transforms/Passes.td | 1 + .../mlir/Dialect/QTensor/Transforms/Passes.td | 1 + mlir/include/mlir/Support/OperationUtils.h | 49 ++ mlir/lib/Compiler/CMakeLists.txt | 2 + mlir/lib/Compiler/Programs.cpp | 742 +++++++++++++++++- .../Conversion/CBitToMemRef/CBitToMemRef.cpp | 41 +- .../Conversion/CBitToMemRef/CMakeLists.txt | 1 + mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp | 128 ++- mlir/lib/Conversion/QCOToJeff/CMakeLists.txt | 1 + mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp | 150 +++- mlir/lib/Conversion/QCOToQC/CMakeLists.txt | 2 + mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | 244 +++++- mlir/lib/Conversion/QCToQCO/CMakeLists.txt | 1 + mlir/lib/Conversion/QCToQCO/QCToQCO.cpp | 492 +++++++++--- .../QCToQIR/QIRAdaptive/CMakeLists.txt | 1 + .../QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp | 153 +++- .../Conversion/QCToQIR/QIRBase/CMakeLists.txt | 1 + .../QCToQIR/QIRBase/QCToQIRBase.cpp | 173 +++- .../QCToQIR/QIRCommon/QIRCommon.cpp | 403 +++++++++- mlir/lib/Dialect/MQT/IR/MQTDialect.cpp | 152 ++-- .../MQT/Transforms/NormalizeGlobalPhases.cpp | 438 +++++++---- .../MQT/Transforms/UnrollModifiers.cpp | 28 +- mlir/lib/Dialect/MQT/Utils/Angles.cpp | 48 +- .../lib/Dialect/MQT/Utils/ConstantFolding.cpp | 130 ++- mlir/lib/Dialect/MQT/Utils/Modifiers.cpp | 15 +- mlir/lib/Dialect/QC/IR/Modifiers/CtrlOp.cpp | 17 +- mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp | 22 +- .../Dialect/QC/IR/Modifiers/ModifierUtils.cpp | 81 +- mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp | 64 +- .../QC/IR/QubitManagement/DeallocOp.cpp | 4 +- .../QC/Transforms/ShrinkQubitRegisters.cpp | 6 + .../QC/Translation/TranslateQCToOpenQASM3.cpp | 49 +- mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp | 83 +- mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp | 57 +- .../QCO/IR/Modifiers/ModifierUtils.cpp | 89 ++- .../Dialect/QCO/IR/Modifiers/ModifierUtils.h | 4 + mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp | 137 +++- .../IR/Operations/StandardGates/BarrierOp.cpp | 69 +- .../QCO/IR/Operations/StandardGates/ROp.cpp | 13 +- mlir/lib/Dialect/QCO/IR/QCOUtils.cpp | 142 +++- .../lib/Dialect/QCO/Transforms/CMakeLists.txt | 1 + .../DecomposeMultiControlled.cpp | 33 +- .../QCO/Transforms/Mapping/Mapping.cpp | 471 +++++++++-- .../FuseSingleQubitUnitaryRuns.cpp | 15 + .../NativeSynthesis/TargetSynthesis.cpp | 175 ++++- .../Optimizations/HadamardLifting.cpp | 45 +- .../Optimizations/MeasurementLifting.cpp | 55 +- .../MergeSingleQubitRotationGates.cpp | 66 +- .../Optimizations/PauliTwirling.cpp | 22 +- .../Optimizations/QuantumLoopUnroll.cpp | 261 ++++-- .../Optimizations/RemoveDeadGates.cpp | 7 + .../ReplaceClassicalControls.cpp | 31 +- .../Transforms/Optimizations/ReuseQubits.cpp | 7 + .../lib/Dialect/QCO/Utils/DDFunctionality.cpp | 264 +++++-- mlir/lib/Dialect/QCO/Utils/WireIterator.cpp | 17 +- .../QIR/Transforms/AttachQIRAttributes.cpp | 544 +++++++++---- .../lib/Dialect/QIR/Transforms/QIRCleanup.cpp | 100 ++- mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp | 23 +- .../QTensor/Transforms/ShrinkRegisters.cpp | 192 ++--- mlir/lib/Support/Passes.cpp | 4 +- mlir/tools/mqt-cc/mqt-cc.cpp | 27 +- mlir/unittests/Compiler/CMakeLists.txt | 2 + .../Compiler/mqt-cc/verify_invalid_mlir.cmake | 7 + .../Compiler/test_compiler_pipeline.cpp | 271 ++++++- .../CBitToMemRef/test_cbit_to_memref.cpp | 82 +- .../JeffRoundTrip/test_jeff_round_trip.cpp | 266 ++++++- .../Conversion/QCOToQC/test_qco_to_qc.cpp | 333 ++++++++ .../Conversion/QCToQCO/CMakeLists.txt | 3 +- .../Conversion/QCToQCO/test_qc_to_qco.cpp | 277 +++++++ .../test_qc_to_qir_adaptive.cpp | 643 +++++++++++++++ .../QCToQIR/QCToQIRBase/CMakeLists.txt | 6 +- .../QCToQIRBase/test_qc_to_qir_base.cpp | 573 +++++++++++++- mlir/unittests/Dialect/MQT/IR/CMakeLists.txt | 1 + mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp | 110 ++- .../test_global_phase_normalization.cpp | 212 ++++- .../Dialect/MQT/Utils/CMakeLists.txt | 5 +- .../MQT/Utils/test_constant_folding.cpp | 25 +- mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp | 456 ++++++++++- .../Translation/test_openqasm3_emission.cpp | 92 +++ mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp | 718 ++++++++++++++++- .../Dialect/QCO/IR/test_qco_ir_matrix.cpp | 373 ++++++++- .../test_euler_decomposition.cpp | 72 ++ .../test_multi_controlled_decomposition.cpp | 73 ++ .../QCO/Transforms/Mapping/CMakeLists.txt | 2 + .../QCO/Transforms/Mapping/test_mapping.cpp | 522 +++++++++++- .../NativeSynthesis/test_target_synthesis.cpp | 156 ++++ .../test_qco_hadamard_lifting.cpp | 93 ++- .../test_qco_measurement_lifting.cpp | 112 +++ .../test_qco_merge_single_qubit_rotation.cpp | 133 +++- .../Optimizations/test_qco_pauli_twirling.cpp | 54 +- .../test_qco_replace_classical_controls.cpp | 217 +++++ .../test_quantum_loop_unroll.cpp | 268 +++++++ .../QCO/Utils/test_dd_functionality.cpp | 277 +++++++ .../Dialect/QCO/Utils/test_wireiterator.cpp | 30 + mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp | 708 ++++++++++++++++- .../Transforms/test_qtensor_transforms.cpp | 79 ++ test/python/test_mlir_qiskit_translation.py | 16 +- 122 files changed, 13296 insertions(+), 1515 deletions(-) create mode 100644 .agent/plans/audit-mlir-contracts.md create mode 100644 mlir/include/mlir/Compiler/JeffDeserializerError.h create mode 100644 mlir/include/mlir/Compiler/JeffFatalErrorRedirect.h create mode 100644 mlir/include/mlir/Support/OperationUtils.h diff --git a/.agent/plans/audit-mlir-contracts.md b/.agent/plans/audit-mlir-contracts.md new file mode 100644 index 0000000000..1e43b6b9af --- /dev/null +++ b/.agent/plans/audit-mlir-contracts.md @@ -0,0 +1,402 @@ +# Audit and harden MLIR contracts + +This ExecPlan is a living document. The sections Progress, Surprises & +Discoveries, Decision Log, and Outcomes & Retrospective are updated while the +work proceeds. This plan is maintained in accordance with .agent/PLANS.md. + +## Purpose / Big Picture + +Complete GitHub issue #2255 as one reviewable change. Every MQT-owned MLIR pass, +handwritten verifier entrypoint, rewrite/conversion pattern, and dialect +registration surface is audited. Unsupported but structurally valid input must +produce a diagnostic rather than crash or partially mutate the IR; successful +passes must return verifier-valid and semantically faithful IR; verifiers must +own only local invariants; patterns must report match success and failure +truthfully; and every dialect a pass can create must be registered. + +The observable result is a focused regression for each confirmed objective +violation, plus a green complete MLIR test label and repository lint checks. + +## Progress + +- [x] (2026-08-27 22:27Z) Read the repository guidance, MLIR development policy, + issue #2255, and ExecPlan requirements; created an isolated worktree at + origin/main commit baecdc55f130a26a21222d6fe5c613db7eee3633. +- [x] (2026-08-28 00:36Z) Completed the exhaustive inventory: 27 passes, 26 + verifier entrypoints, 235 rewrite/conversion patterns, and seven + registration surfaces. +- [x] (2026-08-28 00:36Z) Reduced search candidates to reachable contract + violations by constructing verifier-valid or locally accepted inputs. +- [x] (2026-08-28 05:00Z) Hardened pass anchors and registrations, preflighted + unsupported input before mutation, made conversions atomic, repaired local + verifier ownership, bounded recursive/resource-sensitive processing, and + corrected rewrite, metadata, numeric, mapping, iterator, and lifetime + cases. +- [x] (2026-08-28 05:20Z) Ran the focused suites for QC IR, QCO IR, QIR + metadata, QIR Base, JeFF round-trip, QCO-to-QC, QC-to-QCO, mapping, QCO + utilities, and optimizations; all were green before the final focused + review. +- [x] (2026-08-28 05:45Z) Resolved the final focused-review findings: + invalidated ancestor QTensor caches after nested stores, diagnosed + duplicate-static aliases after lifetime end, and made captured-register + lifetime failures diagnostic and pattern-atomic. +- [x] (2026-08-28 08:01Z) Completed the initial release build, all 3,075 + mqt-mlir-unittests, repository lint, diff checks, and changed-file audit. + Standalone C++ lint cannot start on this host because clang-tidy 22 is not + installed; Nox reports that exact environment limitation. +- [x] (2026-08-28 09:09Z) Closed the final QIR Base ordering interaction: + preserved per-qubit irreversible ordering, kept independent operations + legal, and made Mapping retain terminal measurement/reset suffixes after + routing swaps. Rebuilt the release tree and passed all 3,086 + mqt-mlir-unittests. +- [x] (2026-08-28 10:45Z) Completed three independent boundary reviews and + resolved their remaining findings: accepted LLVM function-interface entry + points, preserved Qiskit's parameter-vector shrink semantics, routed every + JeFF file/byte/CLI import through one bounded preflight, capped aggregate + OpenQASM classical width, and enforced QCO linearity before DD + construction. +- [x] (2026-08-28 11:00Z) Corrected JeFF entry-point metadata to use the + schema-defined function index, contained recoverable jeff-mlir fatal + reports, and covered framing, incomplete structures, declarations, + undefined values, invalid reconstructed IR, and assertion-prone shapes + through byte, file, and command-line imports. +- [x] (2026-08-28 11:05Z) Completed the pre-rebase release build, all 3,102 + mqt-mlir-unittests, five focused Python/Qiskit metadata regressions, the + full repository lint session, and diff checks. Standalone C++ lint still + cannot start because this host does not provide clang-tidy 22. +- [x] (2026-08-31) Rebased the audit onto origin/main commit + 35d3dc2cb87dc9ed4904e9db7eb43257ad3d4527 and re-audited the newly landed + static-qubit canonicalizer, QCO DD sampling/runtime paths, and partial + Layout/Mapping APIs. The refreshed census is 27 passes, 26 verifier + entrypoints, 236 patterns (124 conversion and 112 + canonicalization/optimization), and seven registration surfaces. +- [x] (2026-08-31) Prevented HoistStaticQubit from moving values across a nested + IsolatedFromAbove boundary, bounded QCO DD call and region nesting at 64 + with a shared 10,000-step analysis/execution budget, and enforced one + program-wide identity per static-qubit index with no reacquisition after + deallocation. +- [x] (2026-08-31) Completed the refreshed release build and all 3,133 tests in + the mqt-mlir-unittests label. +- [x] (2026-08-31) Passed the six QCO DD Python regressions, five focused + Python/Qiskit metadata regressions, repository lint, and diff checks. + Reconfirmed that standalone C++ lint cannot start because clang-tidy 22 is + unavailable on this host. + +## Surprises & Discoveries + +- Observation: several conversions diagnosed unsupported input only after + mutating the source module. Clone/verify/commit or complete read-only + preflight was necessary to make failure atomic. +- Observation: QIR aggregate metadata cannot count every pointer-bearing store. + Only aggregate roots reachable from qubit-bearing QIS operands represent qubit + capacity; unrelated result arrays otherwise inflate the count. +- Observation: QCO QTensor insert was previously treated as identity even when + it changed a slot. Correct lowering requires a memref.store and conservative + cache invalidation when aliasing cannot be disproved. +- Observation: current `main` defines each static-qubit index as one physical + identity for the whole program. Repeated references are canonicalized to one + entry-block root; any later use after deallocation, including a syntactic + reacquisition of the same index, must fail cleanly and atomically. +- Observation: HoistStaticQubit may move a zero-operand static root to the + function entry only when the function is its nearest IsolatedFromAbove + ancestor. Crossing a nested isolation boundary makes the retained users + capture a value from above and invalidates otherwise verified IR. +- Observation: loop-unroll verification of a temporary clone must retain its + parent module so sibling symbol references resolve, while verifying only the + transformed operation because the temporary module is intentionally partial. +- Observation: recursive walkers and expansion-producing passes needed explicit + depth, iteration, or resource budgets. Deep but valid nested regions also + required iterative traversal tests. +- Observation: QCO DD sampling performs a recursive interprocedural analysis + before execution, so bounding only the runtime walker is insufficient. The + sampling analysis and runtime now share the 64-call/region-nesting policy and + a 10,000-step work budget. +- Observation: angle normalization documentation used the wrong half-open + interval; known NaN/Inf expressions and integer size products required + explicit checks before conversion or allocation arithmetic. +- Observation: MLIR canonicalizer maxIterations is a best-effort production + bound. Non-convergence becomes a failure only under a test-only convergence + option that GreedyRewriteConfig does not expose, so the production cleanup + pass cannot truthfully promise a synthetic failure regression. +- Observation: region presence alone is not the QIR Base boundary. Region-based + control flow is unsupported except qc.ctrl; preserved non-control region ops + such as tensor.generate remain legal, including in helper functions. +- Observation: QIR Base's irreversible ordering is a per-qubit constraint, not a + global operation-order constraint. Independent quantum operations and a + zero-target global phase may follow a measurement or reset, while aliases of + the same static qubit must still be rejected. +- Observation: Mapping's wire driver advances through terminal measurements and + resets before routing. Inserting a swap at the old terminal iterator position + put that swap after the irreversible operation; moving the terminal suffix + across the swap through the existing SSA rewiring preserves logical state and + keeps the irreversible operation terminal. +- Observation: module-wide MQT metadata validation must inspect + FunctionOpInterface rather than only func.func. LLVM functions are a supported + compiler checkpoint and must retain the same uniqueness and definition + requirements; source-level QC/QCO transformation helpers remain intentionally + typed to func.func. +- Observation: a Qiskit ParameterVectorElement may legally retain an index at or + beyond the vector's current shrunken size. Group identity/name/size must be + consistent, but the element index is not bounded by that current size. +- Observation: the JeFF schema defines entrypoint as a function-list index, but + both MQT conversions had treated it as a string-table index. Programs with + auxiliary strings exposed the mismatch only after a binary round trip. +- Observation: jeff-mlir treats several recoverable data errors as LLVM fatal + errors and assumes particular operation/region arities before verification. A + shared MQT preflight must therefore validate version, value indices, every + instruction family, callable signatures, control-flow shapes, nesting, and + aggregate sizes before invoking it; its remaining fatal reports must be + translated back into diagnostics at the same boundary. +- Observation: OpenQASM output growth is governed by aggregate classical width, + not only per-register widths. DD construction likewise needs QCO linearity + verification in the shared preparation path, not only at selected callers. + +## Decision Log + +- Decision: complete all of #2255 in one PR, while keeping each regression and + source fix narrow. Rationale: the user explicitly requested one comprehensive + audit; separable tests keep the large scope reviewable. Date/Author: + 2026-08-27, Codex. +- Decision: reject unsupported semantic shapes during a read-only preflight + unless the target dialect can preserve them. Rationale: a diagnostic is safer + than a partial or silently lossy conversion and does not expand this audit + into feature work. Date/Author: 2026-08-28, Codex. +- Decision: use clone/verify/commit where a conversion has many independently + fallible stages and a complete preflight would duplicate the lowering. + Rationale: it provides module atomicity with existing MLIR APIs and no new + framework. Date/Author: 2026-08-28, Codex. +- Decision: move non-local MQT metadata invariants out of dialect callbacks and + into the explicit program validator. Rationale: operation verification must + not depend on unrelated siblings, while compiler boundaries still enforce + whole-program uniqueness. Date/Author: 2026-08-28, Codex. +- Decision: define the QIR Base region boundary with RegionBranchOpInterface, + exempt qc.ctrl, and inspect convertible QC/CBit/MemRef operations nested in + entry-function non-control containers. Rationale: this rejects unsupported + helper/entry control flow without rejecting preserved tensor regions. + Date/Author: 2026-08-28, Codex. +- Decision: enforce QIR Base ordering by canonical qubit identity and repair the + mapper's insertion point for terminal irreversible suffixes. Rationale: this + retains the profile contract without rejecting valid commuting operations or + lowering target-mapped programs into post-measurement gates. Date/Author: + 2026-08-28, Codex. +- Decision: use one JeFF byte deserializer for the typed API, file API, and CLI; + preflight its complete supported 0.3.0 shape and compile only the third-party + translation object with a narrow fatal-to-exception redirect caught by that + boundary. Rationale: jeff-mlir's API otherwise terminates the process after + semantic or MLIR verification failures, while duplicating its full lowering + would create a second implementation. Date/Author: 2026-08-28, Codex. +- Decision: cap emitted OpenQASM classical storage at 1,048,576 aggregate bits + and run verifyLinearity in the common DD preparation helper. Rationale: both + checks belong at their shared resource-owning boundary and cover every public + caller without duplicated policy. Date/Author: 2026-08-28, Codex. +- Decision: hoist qc.static only within its nearest isolation scope and treat a + static index as one program-wide physical identity that cannot be reacquired + after deallocation. Rationale: canonicalization must preserve SSA isolation, + while conversion and runtime lifetime checks must agree on static-qubit + identity. Date/Author: 2026-08-31, Codex. +- Decision: apply the same finite limits to QCO DD sampling analysis and + execution: 64 nested calls/regions and 10,000 shared work/steps. Rationale: + both phases traverse user-controlled interprocedural control flow and must + fail or conservatively select dynamic sampling before exhausting host + resources. Date/Author: 2026-08-31, Codex. + +## Outcomes & Retrospective + +The refreshed census and implementation are complete: 27 pass implementations, +26 handwritten verifier entrypoints, 236 patterns (124 conversion and 112 +canonicalization/optimization), and seven registration surfaces. Confirmed +defects covered valid-input crashes, partial mutation, invalid or lossy +successful output, non-local verification, undeclared dialects, +recursion/resource exhaustion, false rewrite results, isolation-breaking motion, +missed folded constants, numeric overflow/non-finite values, and duplicated or +stale conversion state. + +After rebasing onto origin/main at 35d3dc2cb87dc9ed4904e9db7eb43257ad3d4527, the +complete release build and all 3,133 tests in the mqt-mlir-unittests label pass. +Current focused checks are green at QC IR 347/347, QC-to-QCO 176/176, QCO-to-QC +147/147, QCO utilities 149/149, and JeFF round-trip 152/152. The six QCO DD +Python 3.13 tests, five focused Python/Qiskit parameter-vector tests, repository +lint, and git diff checks also pass. The only unavailable check is standalone +C++ lint: `uvx nox -s cpp-lint` aborts before analysis with +`clang-tidy 22 is required` because that binary is absent from the host. +Pre-rebase focused-suite counts remain recorded below as historical checkpoints +rather than current per-suite totals. + +## Context and Orientation + +Pass declarations live below mlir/include/mlir in TableGen files and their +implementations below mlir/lib. Operation and attribute definitions live below +mlir/include/mlir/Dialect, handwritten verifiers and canonicalizers below +mlir/lib/Dialect, conversions below mlir/lib/Conversion, and direct GoogleTest +coverage in mirrored paths below mlir/unittests. + +The 27-pass census is: CBit-to-MemRef, JeFF-to-QCO, QCO-to-JeFF, QCO-to-QC, +QC-to-QCO, QIR Adaptive, QIR Base, NormalizeGlobalPhases, UnrollModifiers, +ShrinkQubitRegisters, DecomposeMultiControlled, Mapping, +FuseSingleQubitUnitaryRuns, FuseTwoQubitUnitaryRuns, TargetNativeGates, +VerifyTargetGates, HadamardLifting, MeasurementLifting, +MergeSingleQubitRotationGates, PauliTwirling, QuantumLoopUnroll, +RemoveDeadGates, ReplaceClassicalControls, ReuseQubits, QIR attribute +attachment, QIR cleanup, and QTensor shrinking. + +The 26 verifier entrypoints comprise 19 operation verifiers plus CBit +RegisterType, the QC and QCO unitary interfaces, three MQT dialect attribute +callbacks, and the program validator. The 236-pattern census comprises 124 +conversion patterns and 112 canonicalization/optimization patterns. Conversion +distribution is CBit-to-MemRef 3, JeFF-to-QCO 25, QCO-to-JeFF 27, QCO-to-QC 23, +QC-to-QCO 22, QIR Adaptive 10, QIR Base 8, and QIR Common 6. + +The seven registration surfaces are the five dialect initializers for CBit, MQT, +QC, QCO, and QTensor, plus the program compiler registry in Programs.cpp and +command-line compiler registry in mqt-cc.cpp. The audit checks both dialect +initialization and every pass getDependentDialects declaration. + +The normative contract rules are in docs/mlir/development.md. The audit adds no +lit/FileCheck infrastructure, runtime dependency, public API migration, or +style-only cleanup. + +## Milestones + +### Milestone 1: Complete and disposition the census + +Inventory every pass root and dependency declaration, verifier entrypoint, +pattern implementation, and registration site. Search for fatal errors, +assertions, unchecked casts/indexing, recursive walks, hand-matched constants, +and fallible work after mutation. The milestone is complete when every item is +recorded as clean, affected directly, or affected through a shared fix. + +### Milestone 2: Prove objective violations + +For each candidate, construct the smallest verified or locally accepted IR that +reaches it. Discard structurally unreachable findings. Keep valid-input crashes, +invalid/lossy successful output, non-local verifier assumptions, partial +mutation, false rewrite contracts, missing registrations, and unbounded work. +The milestone is complete when each retained case has a failing baseline +reproducer and a named ownership boundary for the fix. + +### Milestone 3: Harden contracts with focused regressions + +Put validation before the first mutation or lower a clone and commit only after +verification. Replace process termination with operation/pass diagnostics, make +verifier checks local, register produced dialects, bound recursive or expanding +work, and use MLIR constant matching rather than producer-specific casts. Add +direct GoogleTests beside existing coverage. The milestone is complete when +every retained case passes and successful output verifies. + +### Milestone 4: Whole-suite closure and PR handoff + +Build the final tree, run all mqt-mlir-unittests, run both lint sessions and +diff checks, then inspect status/name/stat output for unrelated or generated +files. Record exact results here. The milestone is complete when the branch is +PR-ready and no known #2255 contract defect remains. + +## Plan of Work + +Maintain the complete census while inspecting implementation and declaration +pairs. Validate candidates with direct tests rather than changing code from +search results alone. Apply the smallest fix at the owning boundary and keep +failure atomic. Re-run the narrow binary immediately after each cluster, then +perform a second read-only review of high-risk conversion state and semantic +preflights. Finish with the full labeled suite, lint, and diff audit. + +## Concrete Steps + +Run all commands from the repository root. + + rg -n 'def .*: Pass|runOnOperation|getDependentDialects' mlir/include mlir/lib + rg -n 'LogicalResult .*verify|::verify\(' mlir/lib/Dialect + rg -n 'RewritePattern|ConversionPattern|matchAndRewrite' mlir/lib + rg -n 'assert\(|reportFatal|llvm_unreachable|\.front\(\)|cast<' mlir/lib + +Configure and build: + + cmake --preset release + cmake --build build/release --parallel 8 + +Run the complete checks after focused suites are green: + + ctest --test-dir build/release -L mqt-mlir-unittests --output-on-failure --parallel 8 + uvx nox -s cpp-lint + uvx nox -s lint + git diff --check + +## Validation and Acceptance + +Acceptance requires a named, fully dispositioned census; a regression for every +confirmed objective violation; no process termination or partial mutation for +unsupported valid input; verifier-valid successful output; truthful pattern +results; complete dialect registration; bounded recursion/resource use; and a +green full mqt-mlir-unittests label. Lint results and any environment-only +limitation must be recorded exactly. No generated, unrelated, or style-only file +may remain in the diff. + +## Idempotence and Recovery + +Inventory searches and tests are repeatable. Source edits use focused patches. +If a test exposes partial mutation, move validation ahead of the first mutation +or use clone/verify/commit rather than rollback. The isolated worktree protects +the user's existing checkout. If Ninja reports a corrupt log after interruption, +run ninja -C build/release -t recompact and rebuild; do not clean unrelated +state. + +## Artifacts and Notes + +Initial baseline: baecdc55f130a26a21222d6fe5c613db7eee3633 from origin/main. + +Refreshed base: 35d3dc2cb87dc9ed4904e9db7eb43257ad3d4527 from origin/main. + +Pre-rebase focused checkpoint: + + QC IR: 345/345 + QCO IR: 510/510 + QIR metadata: 133/133 + QIR Base: 144/144 + QIR Adaptive: 159/159 + JeFF round-trip: 151/151 + QCO-to-QC: 147/147 + QC-to-QCO: 176/176 + Mapping: 97/97 + QCO utilities: 121/121 + Optimizations: 219/219 + Decomposition and native synthesis: 242/242 + QTensor transforms: 3/3 + +Latest full closure transcript: + + cmake --build build/release --parallel 8 + # completed successfully + + ctest --test-dir build/release -L mqt-mlir-unittests --output-on-failure --parallel 8 + 100% tests passed, 0 tests failed out of 3133 + + uvx nox -s tests-3.13 -- test/python/test_qco_dd.py + 6 passed + + uvx nox -s tests-3.13 -- test/python/test_mlir_qiskit_translation.py -k \ + 'parameter_vector_element_outside_current_size_round_trips or parameter_vector_metadata_is_preflighted' + 5 passed + + uvx nox -s lint + nox > Session lint was successful + + uvx nox -s cpp-lint + nox > Session cpp-lint aborted: clang-tidy 22 is required. + + git diff --check + # no output + +## Interfaces and Dependencies + +Use existing typed TableGen pass anchors, signalPassFailure, operation +diagnostics, dialect registries, RegionBranchOpInterface, m_Constant, +GreedyRewriteConfig, and direct GoogleTest/CTest targets. Add no runtime or test +dependency. The Python API is unchanged. C++ API additions are limited to shared +helper declarations; no existing public API is removed or changed. + +Revision note: expanded the initial QIR Base investigation into the requested +complete #2255 contract audit; replaced the stale mid-audit disposition with the +full pass, verifier, pattern, and registration census; the final review also +closed the QIR Base/Mapping irreversible-ordering interaction. The origin/main +refresh added one canonicalization pattern to the census, closed +HoistStaticQubit's isolation boundary, aligned program-wide static-qubit +identity and DD analysis/runtime bounds, and refreshed the full-suite evidence. diff --git a/cmake/ExternalDependencies.cmake b/cmake/ExternalDependencies.cmake index 1142f236f3..2207dfd472 100644 --- a/cmake/ExternalDependencies.cmake +++ b/cmake/ExternalDependencies.cmake @@ -38,6 +38,20 @@ function(_mqt_core_make_jeff_available) endfunction() _mqt_core_make_jeff_available() +# jeff-mlir currently reports malformed serialized input through LLVM's fatal error API. Compile its +# translation boundary with exceptions and redirect those reports so the public MQT importer can +# diagnose them without exiting. +if(MSVC) + target_compile_options( + obj.MLIRJeffTranslation + PRIVATE /EHsc "/FI${PROJECT_SOURCE_DIR}/mlir/include/mlir/Compiler/JeffFatalErrorRedirect.h") +else() + target_compile_options( + obj.MLIRJeffTranslation + PRIVATE -fexceptions -include + "${PROJECT_SOURCE_DIR}/mlir/include/mlir/Compiler/JeffFatalErrorRedirect.h") +endif() + set(JSON_VERSION 3.12.0 CACHE STRING "nlohmann_json version") diff --git a/docs/mlir/OpenQASM.md b/docs/mlir/OpenQASM.md index 21020cd48a..303f205f75 100644 --- a/docs/mlir/OpenQASM.md +++ b/docs/mlir/OpenQASM.md @@ -203,6 +203,10 @@ 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. +Export accepts at most 64 region-owning operations along any IR path, an +expression nesting depth of 256, and an expansion budget of 4,096 values per +expression. The total width of classical registers is 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 diff --git a/mlir/include/mlir/Compiler/JeffDeserializerError.h b/mlir/include/mlir/Compiler/JeffDeserializerError.h new file mode 100644 index 0000000000..6fe3a027b2 --- /dev/null +++ b/mlir/include/mlir/Compiler/JeffDeserializerError.h @@ -0,0 +1,26 @@ +/* + * 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 + +namespace mlir::detail { + +/// Recoverable failure reported by the third-party JEFF deserializer. +class JeffDeserializerError final : public std::runtime_error { +public: + explicit JeffDeserializerError(std::string message) + : std::runtime_error(std::move(message)) {} +}; + +} // namespace mlir::detail diff --git a/mlir/include/mlir/Compiler/JeffFatalErrorRedirect.h b/mlir/include/mlir/Compiler/JeffFatalErrorRedirect.h new file mode 100644 index 0000000000..8b972f33a2 --- /dev/null +++ b/mlir/include/mlir/Compiler/JeffFatalErrorRedirect.h @@ -0,0 +1,42 @@ +/* + * 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 "mlir/Compiler/JeffDeserializerError.h" + +#include +#include +#include + +namespace llvm { + +[[noreturn]] inline void throwJeffDeserializerError(const char* reason, + bool = true) { + throw mlir::detail::JeffDeserializerError(reason); +} + +[[noreturn]] inline void throwJeffDeserializerError(const StringRef reason, + bool = true) { + throw mlir::detail::JeffDeserializerError(reason.str()); +} + +[[noreturn]] inline void throwJeffDeserializerError(const Twine& reason, + bool = true) { + throw mlir::detail::JeffDeserializerError(reason.str()); +} + +} // namespace llvm + +// jeff-mlir currently reports invalid serialized input through LLVM's fatal +// error API. Redirect only its translation target to the recoverable exception +// above; the public MQT import boundary preflights assertion-prone shapes, then +// catches and diagnoses any remaining dependency failures. +#define report_fatal_error throwJeffDeserializerError diff --git a/mlir/include/mlir/Compiler/Programs.h b/mlir/include/mlir/Compiler/Programs.h index 95e4bc7efd..7d06fa55cf 100644 --- a/mlir/include/mlir/Compiler/Programs.h +++ b/mlir/include/mlir/Compiler/Programs.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -35,6 +36,15 @@ class OpenQASMProgram; class QIRProgram; class CompilerTarget; +namespace detail { + +/// Deserialize the JEFF subset supported by the compiler without allowing the +/// dependency to terminate on unsupported declarations. +[[nodiscard]] FailureOr> +deserializeJeffFile(MLIRContext* context, const std::filesystem::path& path); + +} // namespace detail + /** * @brief The QIR profile represented by a QIR program. */ diff --git a/mlir/include/mlir/Conversion/CBitToMemRef/CBitToMemRef.td b/mlir/include/mlir/Conversion/CBitToMemRef/CBitToMemRef.td index c03deb739e..ebc3f4b15e 100644 --- a/mlir/include/mlir/Conversion/CBitToMemRef/CBitToMemRef.td +++ b/mlir/include/mlir/Conversion/CBitToMemRef/CBitToMemRef.td @@ -13,13 +13,19 @@ def ConvertCBitToMemRef : Pass<"convert-cbit-to-memref", "mlir::ModuleOp"> { let description = [{ This pass explicitly lowers CBit register types and operations to memrefs. - A zero-initialized register becomes an allocation followed by one false - store for each element. An undefined register becomes an allocation only. + A zero-initialized register becomes an allocation followed by a loop that + stores false to each element. An undefined register becomes an allocation + only. The pass also converts function signatures, calls, returns, branches, and structural SCF operation types. It does not infer CBit registers from existing memrefs. + + Input may contain at most 64 region-owning operations along any IR path. }]; let dependentDialects = ["mlir::arith::ArithDialect", - "mlir::memref::MemRefDialect"]; + "mlir::cf::ControlFlowDialect", + "mlir::func::FuncDialect", + "mlir::memref::MemRefDialect", + "mlir::scf::SCFDialect"]; } diff --git a/mlir/include/mlir/Conversion/ConversionUtils.h b/mlir/include/mlir/Conversion/ConversionUtils.h index 1144ba66d0..d1b16d3e92 100644 --- a/mlir/include/mlir/Conversion/ConversionUtils.h +++ b/mlir/include/mlir/Conversion/ConversionUtils.h @@ -35,13 +35,13 @@ namespace mlir { inline LogicalResult moveRegion(Region& source, Region& dest, ConversionPatternRewriter& rewriter, const TypeConverter* typeConverter) { - rewriter.inlineRegionBefore(source, dest, dest.end()); - auto* block = &dest.front(); + auto* block = &source.front(); TypeConverter::SignatureConversion sc(block->getNumArguments()); if (failed( typeConverter->convertSignatureArgs(block->getArgumentTypes(), sc))) { return failure(); } + rewriter.inlineRegionBefore(source, dest, dest.end()); rewriter.applySignatureConversion(block, sc); return success(); } diff --git a/mlir/include/mlir/Conversion/JeffToQCO/JeffToQCO.td b/mlir/include/mlir/Conversion/JeffToQCO/JeffToQCO.td index b5143e0f6e..c89c9d94e4 100644 --- a/mlir/include/mlir/Conversion/JeffToQCO/JeffToQCO.td +++ b/mlir/include/mlir/Conversion/JeffToQCO/JeffToQCO.td @@ -14,11 +14,14 @@ def JeffToQCO : Pass<"jeff-to-qco", "mlir::ModuleOp"> { let description = [{ This pass converts all operations from the `jeff` dialect to their equivalent operations in the QCO dialect. It ensures that the returned module is a valid QCO module. + Input may contain at most 64 region-owning operations along any IR path. Note that this pass is still in development as QCO and `jeff` do not have full feature parity yet. Known limitations: + - `jeff.entrypoint` must select a string in `jeff.strings` that names a + defined, single-block `func.func` ending in `func.return` - Only specific `CustomOp`s are currently supported - Only specific `PPROp`s are currently supported - Support for multiple functions is currently limited @@ -26,7 +29,8 @@ def JeffToQCO : Pass<"jeff-to-qco", "mlir::ModuleOp"> { let dependentDialects = ["mlir::arith::ArithDialect", "mlir::cbit::CBitDialect", "mlir::math::MathDialect", - "mlir::mqt::MQTDialect", "mlir::scf::SCFDialect", + "mlir::func::FuncDialect", "mlir::mqt::MQTDialect", + "mlir::scf::SCFDialect", "mlir::tensor::TensorDialect", "mlir::qco::QCODialect", "mlir::qtensor::QTensorDialect", diff --git a/mlir/include/mlir/Conversion/QCOToJeff/QCOToJeff.td b/mlir/include/mlir/Conversion/QCOToJeff/QCOToJeff.td index 58ea959982..a3e28a2acb 100644 --- a/mlir/include/mlir/Conversion/QCOToJeff/QCOToJeff.td +++ b/mlir/include/mlir/Conversion/QCOToJeff/QCOToJeff.td @@ -14,12 +14,19 @@ def QCOToJeff : Pass<"qco-to-jeff", "mlir::ModuleOp"> { let description = [{ This pass converts all operations from the QCO dialect to their equivalent operations in the `jeff` dialect. It ensures that the returned module is a valid `jeff` module that can be serialized. + Input may contain at most 64 region-owning operations along any IR path. Note that this pass is still in development as QCO and `jeff` do not have full feature parity yet. + The module must contain exactly one defined, single-block function marked + with `mqt.entry_point`, and static and dynamic allocation modes must not be + mixed. Static qubit indices must be unique. + `qco.static` is currently converted to `jeff.qubit_alloc` because `jeff` does not yet represent static (index-based) qubits. 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::arith::ArithDialect", + "mlir::func::FuncDialect", "mlir::jeff::JeffDialect", + "mlir::qc::QCDialect", "mlir::qco::QCODialect"]; } diff --git a/mlir/include/mlir/Conversion/QCOToQC/QCOToQC.td b/mlir/include/mlir/Conversion/QCOToQC/QCOToQC.td index 18fdf68fa7..6fe32130af 100644 --- a/mlir/include/mlir/Conversion/QCOToQC/QCOToQC.td +++ b/mlir/include/mlir/Conversion/QCOToQC/QCOToQC.td @@ -14,8 +14,15 @@ def QCOToQC : Pass<"qco-to-qc", "mlir::ModuleOp"> { let description = [{ This pass converts all operations from the QCO dialect to their equivalent operations in the QC dialect. It handles the transformation of qubit values in QCO to qubit references in QC, ensuring that the semantics of quantum operations are preserved during the conversion process. + + Every physical static-qubit identity must have at most one live QCO wire. + A static index may be reacquired after its previous wire is explicitly sunk + earlier in the same block. + Input may contain at most 64 region-owning operations along any IR path. }]; - let dependentDialects = ["mlir::memref::MemRefDialect", - "mlir::qc::QCDialect"]; + let dependentDialects = ["mlir::cf::ControlFlowDialect", + "mlir::func::FuncDialect", + "mlir::memref::MemRefDialect", "mlir::qc::QCDialect", + "mlir::scf::SCFDialect"]; } diff --git a/mlir/include/mlir/Conversion/QCToQCO/QCToQCO.td b/mlir/include/mlir/Conversion/QCToQCO/QCToQCO.td index 6b1c2d8868..f6b10d885b 100644 --- a/mlir/include/mlir/Conversion/QCToQCO/QCToQCO.td +++ b/mlir/include/mlir/Conversion/QCToQCO/QCToQCO.td @@ -14,8 +14,12 @@ def QCToQCO : Pass<"qc-to-qco", "mlir::ModuleOp"> { let description = [{ This pass converts all operations from the QC dialect to their equivalent operations in the QCO dialect. It handles the transformation of qubit references in QC to qubit values in QCO, ensuring that the semantics of quantum operations are preserved during the conversion process. + Input may contain at most 64 region-owning operations along any IR path. }]; - let dependentDialects = ["mlir::arith::ArithDialect", "mlir::qco::QCODialect", - "mlir::qtensor::QTensorDialect"]; + let dependentDialects = ["mlir::arith::ArithDialect", + "mlir::cf::ControlFlowDialect", + "mlir::func::FuncDialect", "mlir::qco::QCODialect", + "mlir::qtensor::QTensorDialect", + "mlir::scf::SCFDialect"]; } diff --git a/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td b/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td index 7bc2a0079f..73dad042b3 100644 --- a/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td +++ b/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td @@ -20,20 +20,33 @@ def QCToQIRAdaptive : Pass<"qc-to-qir-adaptive", "mlir::ModuleOp"> { - Input is a valid module in the QC dialect. - The entry function must be marked with `mqt.entry_point`. + - The entry function must be defined and have no arguments. + - QC, CBit, and MemRef operations must occur only in the entry function. + - Input may contain at most 64 region-owning operations along any IR path. + - The total width of all `cbit.alloc` operations must not exceed 1,048,576 + (`1 << 20`) classical result slots per module. + - Existing `__quantum__` runtime symbols must be Func or LLVM function + declarations. + - Static and dynamic qubit allocation modes must not be mixed. + - Qubit MemRef registers and loads must be one-dimensional. Behavior: - Each QC quantum operation is replaced by a call to the corresponding QIR function in the LLVM dialect. - Operations from the scf dialect are lowered into the cf dialect before they are lowered to the LLVM dialect. - - Required QIR module flags are attached to the MLIR module. - - Required attributes are attached to the entry function. + - QIR module flags and entry-point passthrough metadata are attached by the + separate QIR metadata pass, normally through the QIR cleanup pipeline. - The pass extends the existing block structure of the entry function with an entry block and an epilogue block: 0. Initialization block: Sets up the execution environment and performs required runtime initialization. 1. Epilogue block: Records measurement results and returns from the entry function. Any blocks in-between have no restrictions regarding their operations as long as they are supported. - Measurement results may be used as classical values to drive conditional branches. - - Non-quantum dialects are lowered via MLIR's built-in conversions. + - Supported Func, SCF, Arith, CF, and Math operations are lowered via + MLIR's built-in conversions; other dialects are preserved. }]; - let dependentDialects = ["mlir::LLVM::LLVMDialect"]; + let dependentDialects = ["mlir::arith::ArithDialect", + "mlir::cf::ControlFlowDialect", + "mlir::LLVM::LLVMDialect", "mlir::qc::QCDialect", + "mlir::qco::QCODialect"]; } diff --git a/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td b/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td index 041dc983c3..4c63833a65 100644 --- a/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td +++ b/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td @@ -20,22 +20,42 @@ def QCToQIRBase : Pass<"qc-to-qir-base", "mlir::ModuleOp"> { - Input is a valid module in the QC dialect. - The entry function must be marked with `mqt.entry_point`. + - The entry function must be defined and have no arguments. - The input entry function must consist of a single block. Multi-block input functions are currently not supported. + - QC, CBit, and MemRef operations must occur only in the entry function. + - Input may contain at most 64 region-owning operations along any IR path. + - The total width of all `cbit.alloc` operations must not exceed 1,048,576 + (`1 << 20`) classical result slots per module. + - Existing `__quantum__` runtime symbols must be Func or LLVM function + declarations. + - Qubit memrefs must be direct, statically sized, one-dimensional + allocations accessed with constant in-bounds indices. - The program must have straight-line control flow (i.e., Base Profile QIR). + - Operations with observable effects or non-speculatable behavior must + precede every `qc.measure` and `qc.reset` unless they are independent + quantum operations on qubits that have not yet been measured or reset. + This lets the block split commute independent quantum operations while + preserving each qubit's order. Semantically inert no-op/resource + plumbing and pure, speculatable operations may also follow irreversible + operations. Behavior: - Each QC quantum operation is replaced by a call to the corresponding QIR function in the LLVM dialect. - - Required QIR module flags are attached as attributes to the entry function. + - QIR module flags and entry-point passthrough metadata are attached by the + separate QIR metadata pass, normally through the QIR cleanup pipeline. - The pass transforms the single-block entry function into four blocks to satisfy QIR Base Profile constraints: 0. Initialization block: Sets up the execution environment and performs required runtime initialization. 1. Reversible operations block: Contains only void-returning calls to reversible quantum operations. 2. Irreversible operations block: Contains only void-returning calls to operations marked irreversible (e.g., `__quantum__qis__mz__body` and `__quantum__qis__reset__body`). 3. Epilogue block: Records measurement results and returns from the entry function. - Blocks are connected via unconditional branches in the order listed above. - - Non-quantum dialects are lowered via MLIR's built-in conversions. + - Supported Func, Arith, CF, and Math operations are lowered via MLIR's + built-in conversions; other dialects are preserved. }]; - let dependentDialects = ["mlir::LLVM::LLVMDialect"]; + let dependentDialects = ["mlir::arith::ArithDialect", + "mlir::LLVM::LLVMDialect", "mlir::qc::QCDialect", + "mlir::qco::QCODialect"]; } diff --git a/mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h b/mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h index 34fb68d5f1..97f42a6f3a 100644 --- a/mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h +++ b/mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h @@ -43,6 +43,12 @@ struct LoweringState { /// Cache static qubit pointers for reuse DenseMap staticQubits; + /// Cache Base Profile qubit-register elements by source register and index. + DenseMap> staticQubitRegisterElements; + + /// Next physical qubit index after all explicitly referenced static qubits. + int64_t nextStaticQubitIndex = 0; + /// Cache qubit register sizes for reuse DenseMap qregSizes; @@ -90,6 +96,14 @@ struct QCToQIRTypeConverter final : LLVMTypeConverter { explicit QCToQIRTypeConverter(MLIRContext* ctx); }; +/** + * Validate the module-wide assumptions shared by the QIR conversions before + * either pass mutates the input module. + */ +[[nodiscard]] LogicalResult validateQIRConversionInput(ModuleOp moduleOp, + bool requireSingleBlock, + LoweringState& state); + /** * @brief Base class for conversion patterns that need access to lowering state * diff --git a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h index 579dfec92a..c6c7536168 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h @@ -15,6 +15,7 @@ #include #include #include +#include //===----------------------------------------------------------------------===// // Dialect @@ -23,7 +24,7 @@ #include "mlir/Dialect/MQT/IR/MQTDialect.h.inc" // IWYU pragma: export namespace mlir::mqt { -/// Return whether an operation is the program entry point. +/// Return whether an operation has the program entry-point marker. [[nodiscard]] inline bool isEntryPoint(Operation* operation) { return operation != nullptr && operation->hasAttr(MQTDialect::EntryPointAttrHelper::getNameStr()); @@ -35,7 +36,7 @@ void setEntryPoint(Operation* operation); /// Remove the program entry-point marker from an operation. void removeEntryPoint(Operation* operation); -/// Return the program entry point, or null if the module has none. +/// Return the source-level func.func entry point, or null if there is none. [[nodiscard]] inline func::FuncOp getEntryPoint(ModuleOp moduleOp) { for (auto function : moduleOp.getOps()) { if (isEntryPoint(function)) { @@ -44,4 +45,7 @@ void removeEntryPoint(Operation* operation); } return nullptr; } + +/// Verify metadata invariants that involve more than one operation. +[[nodiscard]] LogicalResult verifyProgramMetadata(ModuleOp moduleOp); } // namespace mlir::mqt diff --git a/mlir/include/mlir/Dialect/MQT/Transforms/Passes.td b/mlir/include/mlir/Dialect/MQT/Transforms/Passes.td index 5b4685d8b4..29715c5d9e 100644 --- a/mlir/include/mlir/Dialect/MQT/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/MQT/Transforms/Passes.td @@ -51,7 +51,8 @@ def UnrollModifiers : Pass<"unroll-modifiers", "mlir::ModuleOp"> { them. Modifiers nested in a `pow` body are unrolled either way. If a modifier cannot be unrolled, the modifier is left untouched. Such - modifiers are skipped silently; the pass never fails. + modifiers are skipped silently. The pass fails before mutation when QCO + linearity is violated. }]; } diff --git a/mlir/include/mlir/Dialect/MQT/Utils/Angles.h b/mlir/include/mlir/Dialect/MQT/Utils/Angles.h index d9e941aa8d..150bdaa9bc 100644 --- a/mlir/include/mlir/Dialect/MQT/Utils/Angles.h +++ b/mlir/include/mlir/Dialect/MQT/Utils/Angles.h @@ -11,6 +11,7 @@ #pragma once #include +#include #include #include @@ -22,6 +23,15 @@ inline constexpr double MAX_GLOBAL_PHASE_ANGLE = 1.0e4; /// Normalize an angle to (-pi, pi]. [[nodiscard]] double normalizeAngle(double theta); +/// Normalize a finite runtime angle to (-pi, pi] without overflowing an +/// intermediate computation. +[[nodiscard]] Value normalizeAngle(RewriterBase& rewriter, Location loc, + Value theta); + +/// Scale an angle by a finite, exactly integral floating-point factor without +/// overflowing, and normalize the result to (-pi, pi]. +[[nodiscard]] double scaleAngleByInteger(double theta, double factor); + /// Check the compiler-wide global-phase angle contract. [[nodiscard]] bool isValidGlobalPhaseAngle(double theta); diff --git a/mlir/include/mlir/Dialect/MQT/Utils/ConstantFolding.h b/mlir/include/mlir/Dialect/MQT/Utils/ConstantFolding.h index 52187ad578..4b79c9a7d8 100644 --- a/mlir/include/mlir/Dialect/MQT/Utils/ConstantFolding.h +++ b/mlir/include/mlir/Dialect/MQT/Utils/ConstantFolding.h @@ -21,11 +21,11 @@ namespace mlir::mqt { /// Convert a floating-point or integer attribute to a double. [[nodiscard]] std::optional attributeToDouble(Attribute attr); -/// Convert a direct arithmetic constant to a double. +/// Convert a direct constant-like value to a double. [[nodiscard]] std::optional valueToDouble(Value value); /** - * Recursively constant-fold a pure SSA expression DAG to an attribute. + * Iteratively constant-fold a pure SSA expression DAG to an attribute. * * The cache memoizes successful and failed evaluations so shared operands are * resolved once. @@ -37,10 +37,10 @@ namespace mlir::mqt { valueToConstantAttr(Value value, DenseMap>& cache); -/// Recursively constant-fold a pure SSA expression DAG to an attribute. +/// Iteratively constant-fold a pure SSA expression DAG to an attribute. [[nodiscard]] std::optional valueToConstantAttr(Value value); -/// Recursively constant-fold a pure SSA expression DAG to a double. +/// Iteratively constant-fold a pure SSA expression DAG to a double. [[nodiscard]] std::optional valueToConstantDouble(Value value); } // namespace mlir::mqt diff --git a/mlir/include/mlir/Dialect/MQT/Utils/Modifiers.h b/mlir/include/mlir/Dialect/MQT/Utils/Modifiers.h index 61e13953c8..159f1d74c8 100644 --- a/mlir/include/mlir/Dialect/MQT/Utils/Modifiers.h +++ b/mlir/include/mlir/Dialect/MQT/Utils/Modifiers.h @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -98,32 +99,128 @@ template const size_t index) { auto unitaries = llvm::make_filter_range( block, [](Operation& op) { return isa(op); }); - auto it = std::next(unitaries.begin(), static_cast(index)); + auto it = unitaries.begin(); + for (size_t i = 0; i < index && it != unitaries.end(); ++i) { + ++it; + } if (it == unitaries.end()) { llvm::reportFatalUsageError("Unitary index out of bounds"); } return cast(*it); } -/// Return the sole body unitary, or a null interface if there is not one. template -[[nodiscard]] UnitaryInterface getSoleBodyUnitary(Block& block) { - auto unitaries = llvm::make_filter_range( - block, [](Operation& op) { return isa(op); }); - auto it = unitaries.begin(); - if (it == unitaries.end()) { - return {}; +[[nodiscard]] bool containsUnitaryOperation(Operation* operation) { + SmallVector worklist{operation}; + while (!worklist.empty()) { + Operation* current = worklist.pop_back_val(); + if (isa(current)) { + return true; + } + for (Region& region : current->getRegions()) { + for (Block& block : region) { + for (Operation& nested : block) { + worklist.push_back(&nested); + } + } + } } - auto unitary = cast(*it); - if (++it != unitaries.end()) { - return {}; + return false; +} + +/// Return whether @p operation or one of its nested operations is neither a +/// unitary nor a terminator. +template +[[nodiscard]] bool containsSupportingOperation(Operation* operation) { + SmallVector worklist{operation}; + while (!worklist.empty()) { + Operation* current = worklist.pop_back_val(); + if (!isa(current) && + !current->hasTrait()) { + return true; + } + for (Region& region : current->getRegions()) { + for (Block& block : region) { + for (Operation& nested : block) { + worklist.push_back(&nested); + } + } + } + } + return false; +} + +/// Return whether top-level supporting operations may move before a modifier +/// without moving an impure operation across a unitary. +template +[[nodiscard]] bool canHoistSupportingOps(Block& body) { + bool sawUnitary = false; + for (Operation& operation : body) { + if (isa(operation)) { + if (containsSupportingOperation(&operation)) { + return false; + } + sawUnitary = true; + continue; + } + if (operation.hasTrait()) { + continue; + } + if (containsUnitaryOperation(&operation) || + (sawUnitary && !isPure(&operation))) { + return false; + } + } + return true; +} + +/// Move top-level non-unitary operations before @p target. Fail without +/// mutation if doing so could move an impure operation across a unitary. +template +[[nodiscard]] LogicalResult hoistSupportingOpsBefore(Block& body, + Operation* target, + RewriterBase& rewriter) { + const auto isSupporting = [](Operation& operation) { + return !isa(operation) && + !operation.hasTrait(); + }; + if (!canHoistSupportingOps(body)) { + return failure(); + } + for (Operation& operation : llvm::make_early_inc_range(body)) { + if (isSupporting(operation)) { + rewriter.moveOpBefore(&operation, target); + } + } + return success(); +} + +/// Return the sole top-level body unitary, or a null interface if another +/// top-level operation contains nested unitaries. +template +[[nodiscard]] UnitaryInterface getSoleBodyUnitary(Block& block) { + UnitaryInterface unitary; + for (Operation& operation : block) { + if (auto candidate = dyn_cast(operation)) { + if (unitary) { + return {}; + } + unitary = candidate; + continue; + } + if (containsUnitaryOperation(&operation)) { + return {}; + } } return unitary; } -/// Move a modifier body's support operations before @p target. -void hoistSupportingOpsBefore(Block& body, Operation* keep, Operation* target, - RewriterBase& rewriter); +/// Move a modifier body's support operations before @p target. Fail without +/// mutation if an impure support operation follows @p keep. +[[nodiscard]] LogicalResult hoistSupportingOpsBefore(Block& body, + Operation* keep, + Operation* target, + RewriterBase& rewriter); /// Inline a modifier body and replace the modifier with the yielded values. void inlineModifierBody(Operation* operation, Block& body, diff --git a/mlir/include/mlir/Dialect/QC/IR/QCOps.td b/mlir/include/mlir/Dialect/QC/IR/QCOps.td index e8a1c4cb1a..e6ae0712c7 100644 --- a/mlir/include/mlir/Dialect/QC/IR/QCOps.td +++ b/mlir/include/mlir/Dialect/QC/IR/QCOps.td @@ -961,7 +961,9 @@ def BarrierOp : QCOp<"barrier", traits = [UnitaryOpInterface]> { // Modifiers //===----------------------------------------------------------------------===// -def YieldOp : QCOp<"yield", traits = [Terminator]> { +def YieldOp + : QCOp<"yield", + traits = [Terminator, ParentOneOf<["CtrlOp", "InvOp", "PowOp"]>]> { let summary = "Yield from a modifier region"; let description = [{ Terminates a modifier region, yielding control back to the enclosing operation. diff --git a/mlir/include/mlir/Dialect/QC/Transforms/Passes.td b/mlir/include/mlir/Dialect/QC/Transforms/Passes.td index 4dedaf1342..eb9450a36c 100644 --- a/mlir/include/mlir/Dialect/QC/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QC/Transforms/Passes.td @@ -19,6 +19,7 @@ def ShrinkQubitRegistersPass "Shrink static `qc::QubitType` MemRef registers to accessed indices."; let description = [{ Shrinks one-dimensional static MemRef registers with element type `!qc.qubit` by removing never-read indices and remapping `memref.load` users accordingly. + Input may contain at most 64 region-owning operations along any IR path. }]; } diff --git a/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td b/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td index e4d9dff00a..9267050d2a 100644 --- a/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td +++ b/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td @@ -1125,13 +1125,14 @@ def BarrierOp : QCOOp<"barrier", traits = [UnitaryOpInterface, Pure]> { static Value getParameter(size_t i) { llvm::reportFatalUsageError("BarrierOp has no parameters"); } static OperandRange getParameters() { return {nullptr, 0}; } [[nodiscard]] static StringRef getBaseSymbol() { return "barrier"; } - [[nodiscard]] bool hasCompileTimeKnownUnitaryMatrix() const { return true; } - [[nodiscard]] DynamicMatrix getUnitaryMatrix(); + [[nodiscard]] bool hasCompileTimeKnownUnitaryMatrix(); + [[nodiscard]] std::optional getUnitaryMatrix(); }]; let builders = [OpBuilder<(ins "ValueRange":$qubits)>]; let hasCanonicalizer = 1; + let hasVerifier = 1; } //===----------------------------------------------------------------------===// diff --git a/mlir/include/mlir/Dialect/QCO/QCOUtils.h b/mlir/include/mlir/Dialect/QCO/QCOUtils.h index d3b9091fdd..7067e845e5 100644 --- a/mlir/include/mlir/Dialect/QCO/QCOUtils.h +++ b/mlir/include/mlir/Dialect/QCO/QCOUtils.h @@ -22,11 +22,21 @@ #include #include +#include #include #include namespace mlir::qco { +/// Return false when both parameters fold and their sum is non-finite. +/// Dynamic parameters remain valid SSA values and may be merged at runtime. +[[nodiscard]] inline bool constantParameterSumIsFinite(Value lhs, Value rhs) { + const auto lhsConstant = mqt::valueToConstantDouble(lhs); + const auto rhsConstant = mqt::valueToConstantDouble(rhs); + return !lhsConstant || !rhsConstant || + std::isfinite(*lhsConstant + *rhsConstant); +} + /** * @brief Check if given quantum operation is unused (i.e., only used by sinks * or resets and has no memory effects). @@ -60,10 +70,16 @@ inline bool checkDeadGate(Operation* op) { /// the entry block. [[nodiscard]] LogicalResult verifyLinearity(Operation* root); -/// Maximum number of modifier targets supported by @ref -/// composeBodyMatrix. +/// Maximum number of qubits supported by dense modifier matrix queries. inline constexpr size_t kMaxModifierTargetQubits = 10; +/// Return whether a dense modifier matrix fits the supported qubit bound. +[[nodiscard]] constexpr bool +isModifierMatrixSizeSupported(size_t numTargets, size_t numControls = 0) { + return numTargets <= kMaxModifierTargetQubits && + numControls <= kMaxModifierTargetQubits - numTargets; +} + /** * @brief Composes compile-time unitaries in a modifier body on @p numTargets * wires. @@ -75,6 +91,10 @@ inline constexpr size_t kMaxModifierTargetQubits = 10; [[nodiscard]] std::optional composeBodyMatrix(Block& block, size_t numTargets); +/// Return whether @p block has a compile-time-known matrix that +/// @ref composeBodyMatrix can construct without allocating it. +[[nodiscard]] bool hasComposableBodyMatrix(Block& block, size_t numTargets); + /** * @brief Check whether two parameter values match. * @@ -108,8 +128,13 @@ static bool valuesMatchWithinTolerance(Value lhs, Value rhs) { template LogicalResult removeInversePairOneTargetZeroParameter(OpType op, PatternRewriter& rewriter) { + auto output = op.getOutputQubit(0); + if (!output.hasOneUse()) { + return failure(); + } + // Check if the successor is the inverse operation - auto nextOp = dyn_cast(*op.getOutputQubit(0).user_begin()); + auto nextOp = dyn_cast(*output.user_begin()); if (!nextOp) { return failure(); } @@ -138,6 +163,10 @@ removeInversePairTwoTargetZeroParameter(OpType op, PatternRewriter& rewriter, bool symmetric = false, bool swappedTargets = false) { auto output0 = op.getOutputQubit(0); + auto output1 = op.getOutputQubit(1); + if (!output0.hasOneUse() || !output1.hasOneUse()) { + return failure(); + } // Check if the successor is the inverse operation auto nextOp = dyn_cast(*output0.user_begin()); @@ -146,7 +175,7 @@ removeInversePairTwoTargetZeroParameter(OpType op, PatternRewriter& rewriter, } // Both qubits have to point to the same successor - auto nextOp2 = *op.getOutputQubit(1).user_begin(); + auto nextOp2 = *output1.user_begin(); if (nextOp2 != nextOp) { return failure(); } @@ -173,6 +202,11 @@ template LogicalResult removeInversePairThreeTargetZeroParameter(OpType op, PatternRewriter& rewriter) { + if (!llvm::all_of(op.getOutputQubits(), + [](Value output) { return output.hasOneUse(); })) { + return failure(); + } + auto nextOp = dyn_cast(*op.getOutputQubit(0).user_begin()); if (!nextOp || op.getOutputQubits() != nextOp.getInputQubits()) { return failure(); @@ -199,8 +233,13 @@ removeInversePairThreeTargetZeroParameter(OpType op, template LogicalResult mergeOneTargetZeroParameter(OpType op, PatternRewriter& rewriter) { + auto output = op.getOutputQubit(0); + if (!output.hasOneUse()) { + return failure(); + } + // Check if the successor is the same operation - auto nextOp = dyn_cast(*op.getOutputQubit(0).user_begin()); + auto nextOp = dyn_cast(*output.user_begin()); if (!nextOp) { return failure(); } @@ -228,16 +267,28 @@ LogicalResult mergeOneTargetZeroParameter(OpType op, */ template LogicalResult mergeOneTargetOneParameter(OpType op, PatternRewriter& rewriter) { + auto output = op.getOutputQubit(0); + if (!output.hasOneUse()) { + return failure(); + } + // Check if the successor is the same operation - auto nextOp = dyn_cast(*op.getOutputQubit(0).user_begin()); - if (!nextOp) { + auto nextOp = dyn_cast(*output.user_begin()); + if (!nextOp || op->getBlock() != nextOp->getBlock()) { + return failure(); + } + if (!constantParameterSumIsFinite(op.getOperand(1), nextOp.getOperand(1))) { return failure(); } - // Compute and set the new parameter + // Compute the new parameter where both operands dominate, then move the + // merged gate behind it. + rewriter.setInsertionPoint(nextOp); auto newParameter = arith::AddFOp::create( rewriter, op.getLoc(), op.getOperand(1), nextOp.getOperand(1)); - op->setOperand(1, newParameter.getResult()); + rewriter.modifyOpInPlace( + op, [&] { op->setOperand(1, newParameter.getResult()); }); + rewriter.moveOpBefore(op, nextOp); // Replace the second operation with the result of the first operation rewriter.replaceOp(nextOp, op.getResult()); @@ -260,19 +311,34 @@ template static LogicalResult mergeTwoTargetOneParameterImpl(OpType op, OpType nextOp, PatternRewriter& rewriter, bool symmetric = false) { + if (op->getBlock() != nextOp->getBlock()) { + return failure(); + } + + auto output0 = op.getOutputQubit(0); + auto output1 = op.getOutputQubit(1); + if (!output0.hasOneUse() || !output1.hasOneUse()) { + return failure(); + } // Both qubits have to point to the same successor - auto nextOp2 = *op.getOutputQubit(1).user_begin(); + auto nextOp2 = *output1.user_begin(); if (nextOp2 != nextOp) { return failure(); } - auto output0 = op.getOutputQubit(0); if (symmetric || output0 == nextOp.getInputQubit(0)) { - // Compute and set the new parameter + if (!constantParameterSumIsFinite(op.getOperand(2), nextOp.getOperand(2))) { + return failure(); + } + // Compute the new parameter where both operands dominate, then move the + // merged gate behind it. + rewriter.setInsertionPoint(nextOp); auto newParameter = arith::AddFOp::create( rewriter, op.getLoc(), op.getOperand(2), nextOp.getOperand(2)); - op->setOperand(2, newParameter.getResult()); + rewriter.modifyOpInPlace( + op, [&] { op->setOperand(2, newParameter.getResult()); }); + rewriter.moveOpBefore(op, nextOp); rewriter.replaceOp(nextOp, nextOp.getInputQubits()); return success(); } @@ -292,8 +358,13 @@ static LogicalResult mergeTwoTargetOneParameterImpl(OpType op, OpType nextOp, template LogicalResult mergeTwoTargetOneParameter(OpType op, PatternRewriter& rewriter, bool symmetric = false) { + auto output = op.getOutputQubit(0); + if (!output.hasOneUse()) { + return failure(); + } + // Check if the successor is the same operation - auto nextOp = dyn_cast(*op.getOutputQubit(0).user_begin()); + auto nextOp = dyn_cast(*output.user_begin()); if (!nextOp) { return failure(); } @@ -313,8 +384,13 @@ LogicalResult mergeTwoTargetOneParameter(OpType op, PatternRewriter& rewriter, */ template LogicalResult mergeXXPlusMinusYY(OpType op, PatternRewriter& rewriter) { + auto output = op.getOutputQubit(0); + if (!output.hasOneUse()) { + return failure(); + } + // Check if the successor is the same operation - auto nextOp = dyn_cast(*op.getOutputQubit(0).user_begin()); + auto nextOp = dyn_cast(*output.user_begin()); if (!nextOp) { return failure(); } diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 5faad215e5..de78e82735 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -13,7 +13,7 @@ include "mlir/Pass/PassBase.td" def MergeSingleQubitRotationGates : Pass<"merge-single-qubit-rotation-gates", "mlir::ModuleOp"> { - let dependentDialects = ["mlir::qco::QCODialect", + let dependentDialects = ["mlir::qc::QCDialect", "mlir::qco::QCODialect", "::mlir::arith::ArithDialect", "::mlir::math::MathDialect"]; let summary = "Merge single-qubit gates using quaternion-based fusion"; @@ -49,7 +49,7 @@ def MergeSingleQubitRotationGates parameters at run time. The emitted `UOp` is defined by $U = \exp [i (\phi + \lambda) / 2] R_z (\phi) R_y (\theta) R_z (\lambda)$. - Normalizing either extracted Z angle into $[-\pi, \pi)$ by $\pm 2\pi$ flips + Normalizing either extracted Z angle into $(-\pi, \pi]$ by $\pm 2\pi$ flips the $\mathrm{SU}(2)$ representative; half of each removed angle is returned as an Euler-wrap phase correction. Each merge therefore computes $\mathrm{inputPhase} - (\phi + \lambda) / 2 + \mathrm{eulerPhase}$, which @@ -60,12 +60,14 @@ def MergeSingleQubitRotationGates the implementation directly invokes the shared global-phase normalization utility to combine, normalize, and remove trivial corrections in their respective scopes. + + Input may contain at most 64 region-owning operations along any IR path. }]; } def FuseSingleQubitUnitaryRuns : Pass<"fuse-single-qubit-unitary-runs", "mlir::ModuleOp"> { - let dependentDialects = ["mlir::qco::QCODialect", + let dependentDialects = ["mlir::qc::QCDialect", "mlir::qco::QCODialect", "::mlir::arith::ArithDialect", "::mlir::math::MathDialect", "::mlir::qtensor::QTensorDialect"]; @@ -86,6 +88,8 @@ def FuseSingleQubitUnitaryRuns The pass also composes supported named gates with dynamic `f64` parameters and emits conservative runtime sequences in the requested basis. Dynamic `pow` and arbitrary dynamic unitaries remain unchanged. + + Input may contain at most 64 region-owning operations along any IR path. }]; let options = [Option< "basis", "basis", "std::string", "\"zyz\"", @@ -96,18 +100,22 @@ def FuseSingleQubitUnitaryRuns def QuantumLoopUnroll : InterfacePass<"quantum-loop-unroll", "FunctionOpInterface"> { - let dependentDialects = ["mlir::qco::QCODialect", "mlir::scf::SCFDialect"]; + let dependentDialects = ["mlir::arith::ArithDialect", "mlir::qco::QCODialect", + "mlir::scf::SCFDialect"]; let summary = "Unroll bounded loops containing quantum operations"; let description = [{ This pass unrolls `scf.for` operations which contain quantum operations. Classical loops are not unrolled. Because the pass uses the `mlir::loopUnrollByFactor` utility internally, the same assumptions and restrictions hold for both. See the [MLIR documentation](https://mlir.llvm.org/doxygen/include_2mlir_2Dialect_2SCF_2Utils_2Utils_8h.html) for more information. + + Input may contain at most 64 region-owning operations along any IR path. }]; let options = [Option<"unrollFactor", "unroll-factor", "int64_t", /*default=*/"-1", - "Use this unroll factor for all loops being unrolled, " - "set it to -1, and it will fully unroll.">]; + "Use this unroll factor (maximum 4096) for all loops " + "being unrolled; -1 fully unrolls static loops within " + "the same limit.">]; } //===----------------------------------------------------------------------===// @@ -115,7 +123,8 @@ def QuantumLoopUnroll //===----------------------------------------------------------------------===// def MappingPass : Pass<"place-and-route", "mlir::ModuleOp"> { - let dependentDialects = ["mlir::qco::QCODialect"]; + let dependentDialects = ["mlir::qc::QCDialect", "mlir::qco::QCODialect", + "mlir::scf::SCFDialect"]; let summary = "This pass ensures that a program meets the connectivity " "constraints of a target architecture."; let description = [{ @@ -133,6 +142,8 @@ def MappingPass : Pass<"place-and-route", "mlir::ModuleOp"> { as routing workspace are materialized. Then, the pass traverses the circuit and inserts `qco.swap` operations to ensure that all two-qubit operations are executable on the target architecture, a process known as routing. + Routing keeps terminal measurements and resets after any inserted swaps so + that irreversible operations remain terminal on each logical qubit. For routing, the pass first divides the circuit into layers. A layer is a set of independently executable (sequences or blocks of) two-qubit operations. Subsequently, the pass performs an A* search for each layer to find and insert a @@ -159,21 +170,25 @@ def MappingPass : Pass<"place-and-route", "mlir::ModuleOp"> { potentially find a more optimal solution. This is behavior is controlled by the `niterations` parameter. The pass option `ntrials` determines how many random initial layouts the pass explores. If compiled with multi-threading on, these trials will be executed in parallel. + + Within the entry-point body, any IR path may contain at most 64 + region-owning operations. }]; - let options = [Option<"nlookahead", "nlookahead", "std::size_t", "1", - "The number of lookahead steps.">, - Option<"alpha", "alpha", "float", "1.0F", - "The alpha factor in the cost function. Must be > 0.">, - Option<"lambda", "lambda", "float", "0.5F", - "The lambda factor in the cost function.">, - Option<"niterations", "niterations", "std::size_t", "1", - "The number of forwards and backwards traversal to " - "improve the initial layout. Must be > 0.">, - Option<"ntrials", "ntrials", "std::size_t", "4", - "The number of (possibly parallel) random trials of " - "the forwards and backwards mechanism. Must be > 0.">, - Option<"seed", "seed", "std::size_t", "42", - "A seed used for randomization.">]; + let options = + [Option<"nlookahead", "nlookahead", "std::size_t", "1", + "The number of lookahead steps. Must be <= 4096.">, + Option<"alpha", "alpha", "float", "1.0F", + "The alpha factor in the cost function. Must be finite and > 0.">, + Option<"lambda", "lambda", "float", "0.5F", + "The lambda factor in the cost function. Must be finite.">, + Option<"niterations", "niterations", "std::size_t", "1", + "The number of forwards and backwards traversal to " + "improve the initial layout. Must be in [1, 4096].">, + Option<"ntrials", "ntrials", "std::size_t", "4", + "The number of (possibly parallel) random trials of " + "the forwards and backwards mechanism. Must be in [1, 4096].">, + Option<"seed", "seed", "std::size_t", "42", + "A seed used for randomization.">]; let statistics = [Statistic<"numSwaps", "num-inserted-swaps", "The number of inserted SWAPs">]; } @@ -201,7 +216,8 @@ def PauliTwirl2QGates : Pass<"pauli-twirl-2q-gates", "mlir::ModuleOp"> { } def HadamardLifting : Pass<"hadamard-lifting", "mlir::ModuleOp"> { - let dependentDialects = ["mlir::qco::QCODialect"]; + let dependentDialects = ["mlir::qc::QCDialect", "mlir::qco::QCODialect", + "::mlir::arith::ArithDialect"]; let summary = "This pass attempts to move Hadamard gates as far away from " "measurements as possible by commuting them " "with Pauli gates. This is done in order to apply measurement " @@ -258,6 +274,7 @@ def HadamardLifting : Pass<"hadamard-lifting", "mlir::ModuleOp"> { Afterward the measurement lifting routine could transform the CNOT into a classically controlled Pauli-X. + Input may contain at most 64 region-owning operations along any IR path. }]; } @@ -275,6 +292,8 @@ def MeasurementLifting : Pass<"measurement-lifting", "mlir::ModuleOp"> { also move before `x` and `y`, with the classical result inverted. Earlier measurements enable qubit reuse and subsequent optimizations. + + Input may contain at most 64 region-owning operations along any IR path. }]; } @@ -302,6 +321,8 @@ def ReplaceClassicalControls Replacing quantum controls with classical conditions reduces quantum interactions and can enable qubit reuse and subsequent optimizations. + + Input may contain at most 64 region-owning operations along any IR path. }]; } @@ -323,6 +344,8 @@ def ReuseQubits : Pass<"reuse-qubits", "mlir::ModuleOp"> { Only single-qubit allocations are considered for reuse, no `qtensor` allocations. Use the `mqt-qubit-reuse` pipeline to run the preparation passes followed by this pass. + + Input may contain at most 64 region-owning operations along any IR path. }]; } @@ -339,6 +362,8 @@ def RemoveDeadGates : Pass<"remove-dead-gates", "mlir::ModuleOp"> { measurement-free programs may intentionally represent state preparation or larger quantum building blocks. The `mqt-qubit-reuse` pipeline runs this pass before attempting to reuse qubits. + + Input may contain at most 64 region-owning operations along any IR path. }]; } @@ -382,6 +407,8 @@ def DecomposeMultiControlled Intermediate building blocks may be left as `qco.ctrl` / `qco.rccx` when `min-qubits` keeps them; the greedy rewriter lowers further when the threshold allows. + + Input may contain at most 64 region-owning operations along any IR path. }]; let options = [Option< "minQubits", "min-qubits", "uint64_t", "3", diff --git a/mlir/include/mlir/Dialect/QIR/Transforms/Passes.td b/mlir/include/mlir/Dialect/QIR/Transforms/Passes.td index d88f5d79fa..140e839d38 100644 --- a/mlir/include/mlir/Dialect/QIR/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QIR/Transforms/Passes.td @@ -26,6 +26,7 @@ def QIRCleanupPass : Pass<"qir-cleanup", "mlir::ModuleOp"> { let description = [{ Removes redundant QIR runtime qubit-array allocation/release pairs that do not contribute to observable behavior, and keeps QIR modules compact. + Input may contain at most 64 region-owning operations along any IR path. }]; } diff --git a/mlir/include/mlir/Dialect/QTensor/Transforms/Passes.td b/mlir/include/mlir/Dialect/QTensor/Transforms/Passes.td index cfc322d3cf..6ec4913444 100644 --- a/mlir/include/mlir/Dialect/QTensor/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QTensor/Transforms/Passes.td @@ -19,6 +19,7 @@ def ShrinkQTensorToFitPass : Pass<"qtensor-shrink-to-fit", "mlir::ModuleOp"> { Shrinks one-dimensional static qtensors by tracing linear tensor chains from `qtensor.dealloc` to `qtensor.alloc` and rebuilding the chain on a compact allocation that only keeps accessed indices. + Input may contain at most 64 region-owning operations along any IR path. }]; } diff --git a/mlir/include/mlir/Support/OperationUtils.h b/mlir/include/mlir/Support/OperationUtils.h new file mode 100644 index 0000000000..430584d29d --- /dev/null +++ b/mlir/include/mlir/Support/OperationUtils.h @@ -0,0 +1,49 @@ +/* + * 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 +#include + +#include +#include + +namespace mlir { + +/// Verify that no path below `root` exceeds `maximumDepth` region-owning ops. +[[nodiscard]] inline LogicalResult +verifyRegionNestingDepth(Operation* root, size_t maximumDepth) { + SmallVector> worklist{{root, 0}}; + while (!worklist.empty()) { + auto [operation, parentDepth] = worklist.pop_back_val(); + const size_t childDepth = + parentDepth + static_cast(operation->getNumRegions() != 0); + if (childDepth > maximumDepth) { + return operation->emitError() + << "operation nesting exceeds the supported maximum of " + << maximumDepth << " operations with regions"; + } + for (Region& region : operation->getRegions()) { + for (Block& block : region) { + for (Operation& nested : block) { + worklist.emplace_back(&nested, childDepth); + } + } + } + } + return success(); +} + +} // namespace mlir diff --git a/mlir/lib/Compiler/CMakeLists.txt b/mlir/lib/Compiler/CMakeLists.txt index 08fa49cf9e..c14e87cda3 100644 --- a/mlir/lib/Compiler/CMakeLists.txt +++ b/mlir/lib/Compiler/CMakeLists.txt @@ -46,6 +46,7 @@ target_sources( ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler/QDMIAdapter.h) # Build the compiler pipeline library +set(LLVM_REQUIRES_EH ON) add_mlir_library( MQTCompilerPipeline PARTIAL_SOURCES_INTENDED @@ -81,6 +82,7 @@ add_mlir_library( MQT::MLIRSupport) mqt_mlir_target_use_project_options(MQTCompilerPipeline) +unset(LLVM_REQUIRES_EH) # collect header files file(GLOB_RECURSE COMPILER_HEADERS_SOURCE "${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler/*.h") diff --git a/mlir/lib/Compiler/Programs.cpp b/mlir/lib/Compiler/Programs.cpp index 06425cb3d6..bfbeefe5c4 100644 --- a/mlir/lib/Compiler/Programs.cpp +++ b/mlir/lib/Compiler/Programs.cpp @@ -10,6 +10,7 @@ #include "mlir/Compiler/Programs.h" +#include "mlir/Compiler/JeffDeserializerError.h" #include "mlir/Compiler/TargetCompilation.h" #include "mlir/Conversion/JeffToQCO/JeffToQCO.h" #include "mlir/Conversion/QCOToJeff/QCOToJeff.h" @@ -34,12 +35,17 @@ #include "mlir/Support/Passes.h" #include +#include +#include #include #include #include #include +#include #include +#include #include +#include #include #include #include @@ -74,6 +80,7 @@ #include #include #include +#include #include #include #include @@ -87,6 +94,17 @@ namespace mlir { +static void pushNestedOperations(Operation* operation, + SmallVectorImpl& worklist) { + for (Region& region : operation->getRegions()) { + for (Block& block : region) { + for (Operation& nested : block) { + worklist.push_back(&nested); + } + } + } +} + [[nodiscard]] static std::shared_ptr createCompilerContext() { DialectRegistry registry; registry.insertwalk([&](Operation* operation) { - found |= operation->getDialect()->getNamespace() == dialect; - }); - return found; + SmallVector worklist{mod}; + while (!worklist.empty()) { + Operation* operation = worklist.pop_back_val(); + if (operation->getDialect()->getNamespace() == dialect) { + return true; + } + pushNestedOperations(operation, worklist); + } + return false; } template @@ -168,6 +190,9 @@ runPasses(ModuleOp mod, const llvm::function_ref populatePasses, const StringRef failureMessage, const bool enableTiming = false, const bool enableStatistics = false) { + if (failed(mqt::verifyProgramMetadata(mod))) { + return failure(); + } PassManager pm(mod.getContext()); if (enableTiming) { pm.enableTiming(); @@ -179,7 +204,7 @@ runPasses(ModuleOp mod, if (failed(pm.run(mod))) { return mod.emitError(failureMessage); } - return success(); + return mqt::verifyProgramMetadata(mod); } [[nodiscard]] static LogicalResult runQCOTransformPasses( @@ -323,7 +348,8 @@ QCProgram::fromModule(std::shared_ptr context, "cannot construct a QC program with a different MLIR context"); return std::nullopt; } - if (failed(verify(*storage.mod))) { + if (failed(verify(*storage.mod)) || + failed(mqt::verifyProgramMetadata(*storage.mod))) { return std::nullopt; } if (!moduleUsesDialect(*storage.mod, "qc")) { @@ -341,7 +367,9 @@ bool QCProgram::cleanup() { } bool QCProgram::normalizeGlobalPhases() { - return succeeded(mqt::normalizeGlobalPhases(mod())); + return succeeded(mqt::verifyProgramMetadata(mod())) && + succeeded(mqt::normalizeGlobalPhases(mod())) && + succeeded(mqt::verifyProgramMetadata(mod())); } std::optional QCProgram::toOpenQASM3() const { @@ -392,13 +420,23 @@ std::optional QCProgram::intoQIR(const QIRProfile profile) && { static size_t countGatesIf(ModuleOp moduleOp, const llvm::function_ref predicate) { - size_t count = 0; auto entryPoint = mqt::getEntryPoint(moduleOp); - entryPoint.walk([&](qc::UnitaryOpInterface op) { - count += !isa(op) && predicate(op); - return isa(op) ? WalkResult::skip() - : WalkResult::advance(); - }); + if (!entryPoint) { + return 0; + } + size_t count = 0; + SmallVector worklist{entryPoint}; + while (!worklist.empty()) { + Operation* operation = worklist.pop_back_val(); + auto unitary = dyn_cast(operation); + if (unitary) { + count += !isa(unitary) && predicate(unitary); + if (isa(unitary)) { + continue; + } + } + pushNestedOperations(operation, worklist); + } return count; } @@ -454,7 +492,8 @@ QCOProgram::fromModule(std::shared_ptr context, "cannot construct a QCO program with a different MLIR context"); return std::nullopt; } - if (failed(verify(*storage.mod))) { + if (failed(verify(*storage.mod)) || + failed(mqt::verifyProgramMetadata(*storage.mod))) { return std::nullopt; } if (!moduleUsesDialect(*storage.mod, "qco")) { @@ -480,21 +519,24 @@ bool QCOProgram::cleanup() { } bool QCOProgram::normalizeGlobalPhases() { - if (!hasValidLinearity()) { + if (!hasValidLinearity() || failed(mqt::verifyProgramMetadata(mod()))) { return false; } - return succeeded(mqt::normalizeGlobalPhases(mod())) && hasValidLinearity(); + return succeeded(mqt::normalizeGlobalPhases(mod())) && + succeeded(mqt::verifyProgramMetadata(mod())) && hasValidLinearity(); } bool QCOProgram::runPassPipeline(const std::string_view pipeline, const bool enableTiming, const bool enableStatistics) { - if (!hasValidLinearity()) { + if (!hasValidLinearity() || failed(mqt::verifyProgramMetadata(mod()))) { + return false; + } + if (failed( + ::runPassPipeline(mod(), pipeline, enableTiming, enableStatistics))) { return false; } - return succeeded(::runPassPipeline(mod(), pipeline, enableTiming, - enableStatistics)) && - hasValidLinearity(); + return succeeded(mqt::verifyProgramMetadata(mod())) && hasValidLinearity(); } bool QCOProgram::mergeSingleQubitRotationGates() { @@ -594,38 +636,650 @@ std::optional QCOProgram::intoJeff() && { // JeffProgram //===----------------------------------------------------------------------===// -std::optional -JeffProgram::fromBytes(const std::span bytes) { +namespace { + +class JeffDeserializerInputVerifier { +public: + JeffDeserializerInputVerifier(MLIRContext* context, + const ::jeff::Module::Reader module) + : context(context), module(module), functions(module.getFunctions()), + stringsSize(module.getStrings().size()) {} + + [[nodiscard]] LogicalResult verify() { + if (module.getVersion() != 0 || module.getVersionMinor() != 3 || + module.getVersionPatch() != 0) { + return reject("unsupported jeff version; expected 0.3.0"); + } + if (stringsSize > maxContainerSize) { + return reject("jeff module contains too many strings"); + } + + for (uint64_t functionIndex = 0; functionIndex < functions.size(); + ++functionIndex) { + const auto function = functions[functionIndex]; + if (function.getName() >= stringsSize) { + return reject("jeff function name does not refer to the string table"); + } + if (function.isDeclaration()) { + return reject("jeff function declarations are not supported"); + } + const auto definition = function.getDefinition(); + if (!definition.hasBody()) { + return reject("jeff function definition must contain a body"); + } + if (!definition.getBody().hasOperations()) { + return reject("jeff function body must contain an operations list"); + } + const auto values = definition.getValues(); + if (values.size() > maxContainerSize) { + return reject("jeff function contains too many values"); + } + for (const auto value : values) { + if (failed(verifyType(value.getType()))) { + return failure(); + } + } + this->values = values; + currentFunctionIndex = functionIndex; + if (failed(verifyRegion(definition.getBody(), std::nullopt, std::nullopt, + 0))) { + return failure(); + } + } + return success(); + } + +private: + static constexpr uint64_t maxContainerSize = 1U << 20; + static constexpr uint64_t maxRegionDepth = 64; + + [[nodiscard]] LogicalResult reject(const Twine& message) const { + return emitError(UnknownLoc::get(context)) << message; + } + + [[nodiscard]] static bool isSupportedIntegerWidth(const uint8_t width) { + return width == 1 || width == 8 || width == 16 || width == 32 || + width == 64; + } + + [[nodiscard]] static bool + isSupportedFloatPrecision(const ::jeff::FloatPrecision precision) { + return precision == ::jeff::FloatPrecision::FLOAT32 || + precision == ::jeff::FloatPrecision::FLOAT64; + } + + [[nodiscard]] LogicalResult + verifyType(const ::jeff::Type::Reader type) const { + switch (type.which()) { + case ::jeff::Type::QUBIT: + case ::jeff::Type::QUREG: + return success(); + case ::jeff::Type::INT: + if (isSupportedIntegerWidth(type.getInt())) { + return success(); + } + return reject("jeff integer type has an unsupported bit width"); + case ::jeff::Type::INT_ARRAY: + if (isSupportedIntegerWidth(type.getIntArray().getBitwidth())) { + return success(); + } + return reject("jeff integer-array type has an unsupported bit width"); + case ::jeff::Type::FLOAT: + if (isSupportedFloatPrecision(type.getFloat())) { + return success(); + } + return reject("jeff floating-point type has an unsupported precision"); + case ::jeff::Type::FLOAT_ARRAY: + if (isSupportedFloatPrecision(type.getFloatArray().getPrecision())) { + return success(); + } + return reject( + "jeff floating-point-array type has an unsupported precision"); + default: + return reject("jeff value has an unknown type"); + } + } + + [[nodiscard]] LogicalResult verifyArity(const ::jeff::Op::Reader operation, + const uint64_t inputs, + const uint64_t outputs) const { + if (operation.getInputs().size() == inputs && + operation.getOutputs().size() == outputs) { + return success(); + } + return reject(Twine("jeff instruction expects ") + Twine(inputs) + + " inputs and " + Twine(outputs) + " outputs"); + } + + [[nodiscard]] LogicalResult + verifyValueIndices(const ::jeff::Op::Reader operation) const { + for (const auto input : operation.getInputs()) { + if (input >= values.size()) { + return reject("jeff instruction input is outside the value table"); + } + } + for (const auto output : operation.getOutputs()) { + if (output >= values.size()) { + return reject("jeff instruction output is outside the value table"); + } + } + return success(); + } + + [[nodiscard]] LogicalResult + verifyRegion(const ::jeff::Region::Reader region, + const std::optional expectedSources, + const std::optional expectedTargets, + const uint64_t depth) { + if (depth > maxRegionDepth) { + return reject("jeff structured control flow exceeds the nesting limit"); + } + if (expectedSources && region.getSources().size() != *expectedSources) { + return reject("jeff region source count does not match its operation"); + } + if (expectedTargets && region.getTargets().size() != *expectedTargets) { + return reject("jeff region target count does not match its operation"); + } + for (const auto source : region.getSources()) { + if (source >= values.size()) { + return reject("jeff region source is outside the value table"); + } + } + for (const auto target : region.getTargets()) { + if (target >= values.size()) { + return reject("jeff region target is outside the value table"); + } + } + if (!region.hasOperations()) { + return reject("jeff region must contain an operations list"); + } + const auto operations = region.getOperations(); + if (operations.size() > maxContainerSize - totalOperations) { + return reject("jeff module contains too many operations"); + } + totalOperations += operations.size(); + for (const auto operation : operations) { + if (failed(verifyOperation(operation, depth))) { + return failure(); + } + } + return success(); + } + + [[nodiscard]] LogicalResult + verifyWellKnownGate(const ::jeff::Op::Reader operation, + const ::jeff::QubitGate::Reader gate) const { + const uint64_t controls = gate.getControlQubits(); + uint64_t targets = 0; + uint64_t parameters = 0; + switch (gate.getWellKnown()) { + case ::jeff::WellKnownGate::X: + case ::jeff::WellKnownGate::Y: + case ::jeff::WellKnownGate::Z: + case ::jeff::WellKnownGate::S: + case ::jeff::WellKnownGate::T: + case ::jeff::WellKnownGate::H: + case ::jeff::WellKnownGate::I: + targets = 1; + break; + case ::jeff::WellKnownGate::R1: + case ::jeff::WellKnownGate::RX: + case ::jeff::WellKnownGate::RY: + case ::jeff::WellKnownGate::RZ: + targets = 1; + parameters = 1; + break; + case ::jeff::WellKnownGate::U: + targets = 1; + parameters = 3; + break; + case ::jeff::WellKnownGate::SWAP: + targets = 2; + break; + case ::jeff::WellKnownGate::GPHASE: + parameters = 1; + break; + default: + return reject("jeff instruction names an unknown well-known gate"); + } + return verifyArity(operation, targets + controls + parameters, + targets + controls); + } + + [[nodiscard]] LogicalResult + verifyGate(const ::jeff::Op::Reader operation, + const ::jeff::QubitGate::Reader gate) const { + const uint64_t controls = gate.getControlQubits(); + switch (gate.which()) { + case ::jeff::QubitGate::WELL_KNOWN: + return verifyWellKnownGate(operation, gate); + case ::jeff::QubitGate::CUSTOM: { + const auto custom = gate.getCustom(); + if (custom.getName() >= stringsSize) { + return reject("jeff custom-gate name is outside the string table"); + } + const uint64_t targets = custom.getNumQubits(); + const uint64_t parameters = custom.getNumParams(); + return verifyArity(operation, targets + controls + parameters, + targets + controls); + } + case ::jeff::QubitGate::PPR: { + const uint64_t targets = gate.getPpr().getPauliString().size(); + return verifyArity(operation, targets + controls + 1, targets + controls); + } + default: + return reject("jeff instruction contains an unknown gate kind"); + } + } + + [[nodiscard]] LogicalResult + verifyQubitOperation(const ::jeff::Op::Reader operation) const { + const auto instruction = operation.getInstruction().getQubit(); + switch (instruction.which()) { + case ::jeff::QubitOp::ALLOC: + return verifyArity(operation, 0, 1); + case ::jeff::QubitOp::FREE: + case ::jeff::QubitOp::FREE_ZERO: + return verifyArity(operation, 1, 0); + case ::jeff::QubitOp::MEASURE: + case ::jeff::QubitOp::RESET: + return verifyArity(operation, 1, 1); + case ::jeff::QubitOp::MEASURE_ND: + return verifyArity(operation, 1, 2); + case ::jeff::QubitOp::GATE: + return verifyGate(operation, instruction.getGate()); + default: + return reject("jeff instruction contains an unknown qubit operation"); + } + } + + [[nodiscard]] LogicalResult + verifyQuregOperation(const ::jeff::Op::Reader operation) const { + switch (operation.getInstruction().getQureg().which()) { + case ::jeff::QuregOp::ALLOC: + return verifyArity(operation, 1, 1); + case ::jeff::QuregOp::FREE: + case ::jeff::QuregOp::FREE_ZERO: + return verifyArity(operation, 1, 0); + case ::jeff::QuregOp::EXTRACT_INDEX: + return verifyArity(operation, 2, 2); + case ::jeff::QuregOp::INSERT_INDEX: + case ::jeff::QuregOp::INSERT_SLICE: + return verifyArity(operation, 3, 1); + case ::jeff::QuregOp::EXTRACT_SLICE: + return verifyArity(operation, 3, 2); + case ::jeff::QuregOp::LENGTH: + return verifyArity(operation, 1, 2); + case ::jeff::QuregOp::SPLIT: + return verifyArity(operation, 2, 2); + case ::jeff::QuregOp::JOIN: + return verifyArity(operation, 2, 1); + case ::jeff::QuregOp::CREATE: + return verifyArity(operation, operation.getInputs().size(), 1); + default: + return reject("jeff instruction contains an unknown qureg operation"); + } + } + + [[nodiscard]] LogicalResult + verifyIntegerOperation(const ::jeff::Op::Reader operation) const { + switch (operation.getInstruction().getInt().which()) { + case ::jeff::IntOp::CONST1: + case ::jeff::IntOp::CONST8: + case ::jeff::IntOp::CONST16: + case ::jeff::IntOp::CONST32: + case ::jeff::IntOp::CONST64: + return verifyArity(operation, 0, 1); + case ::jeff::IntOp::NOT: + case ::jeff::IntOp::ABS: + return verifyArity(operation, 1, 1); + case ::jeff::IntOp::ADD: + case ::jeff::IntOp::SUB: + case ::jeff::IntOp::MUL: + case ::jeff::IntOp::DIV_S: + case ::jeff::IntOp::DIV_U: + case ::jeff::IntOp::POW: + case ::jeff::IntOp::AND: + case ::jeff::IntOp::OR: + case ::jeff::IntOp::XOR: + case ::jeff::IntOp::MIN_S: + case ::jeff::IntOp::MIN_U: + case ::jeff::IntOp::MAX_S: + case ::jeff::IntOp::MAX_U: + case ::jeff::IntOp::REM_S: + case ::jeff::IntOp::REM_U: + case ::jeff::IntOp::SHL: + case ::jeff::IntOp::SHR: + case ::jeff::IntOp::EQ: + case ::jeff::IntOp::LT_S: + case ::jeff::IntOp::LTE_S: + case ::jeff::IntOp::LT_U: + case ::jeff::IntOp::LTE_U: + return verifyArity(operation, 2, 1); + default: + return reject("jeff instruction contains an unknown integer operation"); + } + } + + [[nodiscard]] LogicalResult + verifyIntegerArrayOperation(const ::jeff::Op::Reader operation) const { + const auto instruction = operation.getInstruction().getIntArray(); + switch (instruction.which()) { + case ::jeff::IntArrayOp::CONST1: + case ::jeff::IntArrayOp::CONST8: + case ::jeff::IntArrayOp::CONST16: + case ::jeff::IntArrayOp::CONST32: + case ::jeff::IntArrayOp::CONST64: + return verifyArity(operation, 0, 1); + case ::jeff::IntArrayOp::ZERO: + if (!isSupportedIntegerWidth(instruction.getZero())) { + return reject("jeff integer-array zero has an unsupported bit width"); + } + return verifyArity(operation, 1, 1); + case ::jeff::IntArrayOp::GET_INDEX: + if (failed(verifyArity(operation, 2, 1))) { + return failure(); + } + if (values[operation.getInputs()[0]].getType().which() != + ::jeff::Type::INT_ARRAY) { + return reject("jeff integer-array get requires an integer-array input"); + } + return success(); + case ::jeff::IntArrayOp::SET_INDEX: + return verifyArity(operation, 3, 1); + case ::jeff::IntArrayOp::LENGTH: + return verifyArity(operation, 1, 1); + case ::jeff::IntArrayOp::CREATE: + if (operation.getInputs().size() == 0) { + return reject("jeff integer-array create requires at least one input"); + } + return verifyArity(operation, operation.getInputs().size(), 1); + default: + return reject( + "jeff instruction contains an unknown integer-array operation"); + } + } + + [[nodiscard]] LogicalResult + verifyFloatOperation(const ::jeff::Op::Reader operation) const { + switch (operation.getInstruction().getFloat().which()) { + case ::jeff::FloatOp::CONST32: + case ::jeff::FloatOp::CONST64: + return verifyArity(operation, 0, 1); + case ::jeff::FloatOp::SQRT: + case ::jeff::FloatOp::ABS: + case ::jeff::FloatOp::CEIL: + case ::jeff::FloatOp::FLOOR: + case ::jeff::FloatOp::EXP: + case ::jeff::FloatOp::LOG: + case ::jeff::FloatOp::SIN: + case ::jeff::FloatOp::COS: + case ::jeff::FloatOp::TAN: + case ::jeff::FloatOp::ASIN: + case ::jeff::FloatOp::ACOS: + case ::jeff::FloatOp::ATAN: + case ::jeff::FloatOp::SINH: + case ::jeff::FloatOp::COSH: + case ::jeff::FloatOp::TANH: + case ::jeff::FloatOp::ASINH: + case ::jeff::FloatOp::ACOSH: + case ::jeff::FloatOp::ATANH: + case ::jeff::FloatOp::IS_NAN: + case ::jeff::FloatOp::IS_INF: + return verifyArity(operation, 1, 1); + case ::jeff::FloatOp::ADD: + case ::jeff::FloatOp::SUB: + case ::jeff::FloatOp::MUL: + case ::jeff::FloatOp::POW: + case ::jeff::FloatOp::ATAN2: + case ::jeff::FloatOp::MAX: + case ::jeff::FloatOp::MIN: + case ::jeff::FloatOp::EQ: + case ::jeff::FloatOp::LT: + case ::jeff::FloatOp::LTE: + return verifyArity(operation, 2, 1); + default: + return reject( + "jeff instruction contains an unknown floating-point operation"); + } + } + + [[nodiscard]] LogicalResult + verifyFloatArrayOperation(const ::jeff::Op::Reader operation) const { + const auto instruction = operation.getInstruction().getFloatArray(); + switch (instruction.which()) { + case ::jeff::FloatArrayOp::CONST32: + case ::jeff::FloatArrayOp::CONST64: + return verifyArity(operation, 0, 1); + case ::jeff::FloatArrayOp::ZERO: + if (!isSupportedFloatPrecision(instruction.getZero())) { + return reject( + "jeff floating-point-array zero has an unsupported precision"); + } + return verifyArity(operation, 1, 1); + case ::jeff::FloatArrayOp::GET_INDEX: + if (failed(verifyArity(operation, 2, 1))) { + return failure(); + } + if (values[operation.getInputs()[0]].getType().which() != + ::jeff::Type::FLOAT_ARRAY) { + return reject("jeff floating-point-array get requires a " + "floating-point-array input"); + } + return success(); + case ::jeff::FloatArrayOp::SET_INDEX: + return verifyArity(operation, 3, 1); + case ::jeff::FloatArrayOp::LENGTH: + return verifyArity(operation, 1, 1); + case ::jeff::FloatArrayOp::CREATE: + if (operation.getInputs().size() == 0) { + return reject( + "jeff floating-point-array create requires at least one input"); + } + return verifyArity(operation, operation.getInputs().size(), 1); + default: + return reject("jeff instruction contains an unknown " + "floating-point-array operation"); + } + } + + [[nodiscard]] LogicalResult + verifyStructuredControlFlow(const ::jeff::Op::Reader operation, + const uint64_t depth) { + const auto instruction = operation.getInstruction().getScf(); + const uint64_t inputs = operation.getInputs().size(); + const uint64_t outputs = operation.getOutputs().size(); + switch (instruction.which()) { + case ::jeff::ScfOp::SWITCH: { + if (inputs == 0 || outputs != inputs - 1) { + return reject("jeff switch shape is unsupported by the deserializer"); + } + const auto switchInstruction = instruction.getSwitch(); + for (const auto branch : switchInstruction.getBranches()) { + if (failed(verifyRegion(branch, inputs - 1, outputs, depth + 1))) { + return failure(); + } + } + if (switchInstruction.hasDefault() && + failed(verifyRegion(switchInstruction.getDefault(), inputs - 1, + outputs, depth + 1))) { + return failure(); + } + return success(); + } + case ::jeff::ScfOp::FOR: + if (inputs < 3 || outputs != inputs - 3) { + return reject("jeff for-loop shape is unsupported by the deserializer"); + } + return verifyRegion(instruction.getFor(), outputs + 1, outputs, + depth + 1); + case ::jeff::ScfOp::WHILE: + if (outputs != inputs) { + return reject( + "jeff while-loop shape is unsupported by the deserializer"); + } + if (failed(verifyRegion(instruction.getWhile().getBefore(), inputs, + outputs + 1, depth + 1))) { + return failure(); + } + return verifyRegion(instruction.getWhile().getAfter(), outputs, inputs, + depth + 1); + default: + return reject( + "jeff instruction contains an unknown structured-control-flow " + "operation"); + } + } + + [[nodiscard]] LogicalResult + verifyFunctionCall(const ::jeff::Op::Reader operation) const { + const uint64_t callee = operation.getInstruction().getFunc().getFuncCall(); + if (callee >= functions.size()) { + return reject("jeff call refers to an unknown function"); + } + if (callee > currentFunctionIndex) { + return reject("jeff forward function calls are unsupported"); + } + const auto body = functions[callee].getDefinition().getBody(); + return verifyArity(operation, body.getSources().size(), + body.getTargets().size()); + } + + [[nodiscard]] LogicalResult + verifyOperation(const ::jeff::Op::Reader operation, const uint64_t depth) { + if (failed(verifyValueIndices(operation))) { + return failure(); + } + switch (operation.getInstruction().which()) { + case ::jeff::Op::Instruction::QUBIT: + return verifyQubitOperation(operation); + case ::jeff::Op::Instruction::QUREG: + return verifyQuregOperation(operation); + case ::jeff::Op::Instruction::INT: + return verifyIntegerOperation(operation); + case ::jeff::Op::Instruction::INT_ARRAY: + return verifyIntegerArrayOperation(operation); + case ::jeff::Op::Instruction::FLOAT: + return verifyFloatOperation(operation); + case ::jeff::Op::Instruction::FLOAT_ARRAY: + return verifyFloatArrayOperation(operation); + case ::jeff::Op::Instruction::SCF: + return verifyStructuredControlFlow(operation, depth); + case ::jeff::Op::Instruction::FUNC: + return verifyFunctionCall(operation); + default: + return reject("jeff instruction has an unknown kind"); + } + } + + MLIRContext* context; + ::jeff::Module::Reader module; + capnp::List<::jeff::Function>::Reader functions; + uint64_t stringsSize; + capnp::List<::jeff::Value>::Reader values; + uint64_t currentFunctionIndex = 0; + uint64_t totalOperations = 0; +}; + +} // namespace + +[[nodiscard]] static LogicalResult +verifyJeffDeserializerInput(MLIRContext* context, + kj::ArrayPtr words) { + capnp::FlatArrayMessageReader message(words); + const auto module = message.getRoot<::jeff::Module>(); + if (!module.hasFunctions()) { + return emitError(UnknownLoc::get(context), + "jeff module must contain a functions list"); + } + const auto functions = module.getFunctions(); + if (functions.size() == 0) { + return emitError(UnknownLoc::get(context), + "jeff module must contain at least one function"); + } + constexpr uint64_t maxFunctions = + static_cast(std::numeric_limits::max()) + 1; + if (functions.size() > maxFunctions) { + return emitError(UnknownLoc::get(context)) + << "jeff module contains more than " << maxFunctions << " functions"; + } + if (module.getEntrypoint() >= functions.size()) { + return emitError(UnknownLoc::get(context), + "jeff entry point does not refer to a function"); + } + + return JeffDeserializerInputVerifier(context, module).verify(); +} + +[[nodiscard]] static FailureOr> +deserializeJeffBytes(MLIRContext* context, + const std::span bytes) { + if (bytes.empty()) { + return emitError(UnknownLoc::get(context), "jeff data must not be empty"); + } if (bytes.size() % sizeof(capnp::word) != 0U) { - auto context = createCompilerContext(); - emitError(UnknownLoc::get(context.get()), - "jeff data size must be a multiple of the Cap'n Proto word size"); - return std::nullopt; + return emitError( + UnknownLoc::get(context), + "jeff data size must be a multiple of the Cap'n Proto word size"); } auto words = kj::heapArray(bytes.size() / sizeof(capnp::word)); std::memcpy(words.begin(), bytes.data(), bytes.size()); + try { + if (failed(verifyJeffDeserializerInput(context, words.asPtr()))) { + return failure(); + } + auto mod = deserialize(context, words.asPtr()); + if (!mod) { + return emitError(UnknownLoc::get(context), + "failed to deserialize jeff bytes"); + } + return mod; + } catch (const kj::Exception& exception) { + return emitError(UnknownLoc::get(context)) + << "failed to parse jeff data: " + << exception.getDescription().cStr(); + } catch (const detail::JeffDeserializerError& exception) { + return emitError(UnknownLoc::get(context)) + << "failed to deserialize jeff data: " << exception.what(); + } +} + +FailureOr> +detail::deserializeJeffFile(MLIRContext* context, + const std::filesystem::path& path) { + std::string errorMessage; + auto file = openInputFile(path.string(), &errorMessage); + if (!file) { + return emitError(UnknownLoc::get(context)) + << "failed to load jeff file '" << path.string() + << "': " << errorMessage; + } + + const auto buffer = file->getBuffer(); + return deserializeJeffBytes( + context, std::as_bytes(std::span(buffer.data(), buffer.size()))); +} + +std::optional +JeffProgram::fromBytes(const std::span bytes) { auto context = createCompilerContext(); - auto mod = deserialize(context.get(), words.asPtr()); - if (!mod) { - emitError(UnknownLoc::get(context.get()), - "failed to deserialize jeff bytes"); + auto mod = deserializeJeffBytes(context.get(), bytes); + if (failed(mod)) { return std::nullopt; } - return JeffProgram({.context = std::move(context), .mod = std::move(mod)}); + return JeffProgram({.context = std::move(context), .mod = std::move(*mod)}); } std::optional JeffProgram::fromFile(const std::filesystem::path& path) { auto context = createCompilerContext(); - auto mod = deserializeFromFile(context.get(), path.string()); - if (!mod) { - emitError(UnknownLoc::get(context.get())) - << "failed to deserialize jeff file '" << path.string() << "'"; + auto mod = detail::deserializeJeffFile(context.get(), path); + if (failed(mod)) { return std::nullopt; } - return JeffProgram({.context = std::move(context), .mod = std::move(mod)}); + return JeffProgram({.context = std::move(context), .mod = std::move(*mod)}); } JeffProgram JeffProgram::copy() const { return JeffProgram(cloneStorage()); } @@ -754,6 +1408,20 @@ runDefaultPipeline(CompilerInput&& program, const ProgramFormat output, const CompilerTarget* const target, const std::string_view qcoPipeline, const bool enableTiming, const bool enableStatistics) { + const bool hasValidInput = std::visit( + [](T& value) { + using ProgramType = std::remove_cvref_t; + if constexpr (std::is_same_v) { + return true; + } else { + return succeeded(verify(value.module())) && + succeeded(mqt::verifyProgramMetadata(value.module())); + } + }, + program); + if (!hasValidInput) { + return std::nullopt; + } if (target != nullptr && (output == ProgramFormat::QCImport || output == ProgramFormat::QCO || output == ProgramFormat::Jeff)) { diff --git a/mlir/lib/Conversion/CBitToMemRef/CBitToMemRef.cpp b/mlir/lib/Conversion/CBitToMemRef/CBitToMemRef.cpp index 0f7854e830..01011d77b1 100644 --- a/mlir/lib/Conversion/CBitToMemRef/CBitToMemRef.cpp +++ b/mlir/lib/Conversion/CBitToMemRef/CBitToMemRef.cpp @@ -13,17 +13,24 @@ #include "mlir/Dialect/CBit/IR/CBitAttributes.h" #include "mlir/Dialect/CBit/IR/CBitDialect.h" #include "mlir/Dialect/CBit/IR/CBitOps.h" +#include "mlir/Support/OperationUtils.h" #include +#include #include #include #include +#include #include +#include #include +#include +#include #include #include #include +#include #include #include @@ -58,13 +65,17 @@ struct ConvertAllocOp final : OpConversionPattern { if (op.getInitialization() == cbit::Initialization::Zero) { auto zero = arith::ConstantOp::create(rewriter, op.getLoc(), rewriter.getBoolAttr(false)); - for (int64_t index = 0; index < type.getDimSize(0); ++index) { - auto indexValue = - arith::ConstantIndexOp::create(rewriter, op.getLoc(), index); - memref::StoreOp::create(rewriter, op.getLoc(), zero.getResult(), - allocation.getResult(), - ValueRange{indexValue.getResult()}); - } + auto lower = arith::ConstantIndexOp::create(rewriter, op.getLoc(), 0); + auto upper = arith::ConstantIndexOp::create(rewriter, op.getLoc(), + type.getDimSize(0)); + auto step = arith::ConstantIndexOp::create(rewriter, op.getLoc(), 1); + scf::ForOp::create( + rewriter, op.getLoc(), lower, upper, step, ValueRange{}, + [&](OpBuilder& builder, Location location, Value index, ValueRange) { + memref::StoreOp::create(builder, location, zero.getResult(), + allocation.getResult(), ValueRange{index}); + scf::YieldOp::create(builder, location); + }); } rewriter.replaceOp(op, allocation.getResult()); @@ -103,7 +114,14 @@ struct ConvertCBitToMemRef final protected: void runOnOperation() override { MLIRContext* context = &getContext(); - auto moduleOp = getOperation(); + auto original = getOperation(); + constexpr size_t maxRegionNesting = 64; + if (failed(verifyRegionNestingDepth(original, maxRegionNesting))) { + signalPassFailure(); + return; + } + OwningOpRef converted(original.clone()); + auto moduleOp = *converted; CBitTypeConverter typeConverter; ConversionTarget target(*context); RewritePatternSet patterns(context); @@ -131,7 +149,14 @@ struct ConvertCBitToMemRef final if (failed(applyPartialConversion(moduleOp, target, std::move(patterns)))) { signalPassFailure(); + return; + } + if (failed(verify(moduleOp))) { + signalPassFailure(); + return; } + original->setAttrs(moduleOp->getAttrDictionary()); + original.getBodyRegion().takeBody(moduleOp.getBodyRegion()); } }; } // namespace diff --git a/mlir/lib/Conversion/CBitToMemRef/CMakeLists.txt b/mlir/lib/Conversion/CBitToMemRef/CMakeLists.txt index 5ac2c0a5df..0af348a6f7 100644 --- a/mlir/lib/Conversion/CBitToMemRef/CMakeLists.txt +++ b/mlir/lib/Conversion/CBitToMemRef/CMakeLists.txt @@ -14,6 +14,7 @@ add_mlir_conversion_library( LINK_LIBS MLIRArithDialect MLIRCBitDialect + MLIRControlFlowDialect MLIRFuncDialect MLIRFuncTransforms MLIRMemRefDialect diff --git a/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp b/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp index eb983470fc..d815989527 100644 --- a/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp +++ b/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp @@ -16,6 +16,7 @@ #include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" @@ -37,12 +38,15 @@ #include #include #include +#include #include #include #include #include +#include #include #include +#include #include #include @@ -201,30 +205,63 @@ static void createBarrierOp(jeff::CustomOp& op, jeff::CustomOpAdaptor& adaptor, } /** - * @brief Gets the name of the entry point from the module attributes + * @brief Validates and returns the entry-point name encoded by jeff metadata. */ -static FailureOr getEntryPointName(ModuleOp moduleOp) { - auto entryPointAttr = moduleOp->getAttrOfType("jeff.entrypoint"); - if (!entryPointAttr || !entryPointAttr.getType().isUnsignedInteger()) { - return moduleOp.emitError( - "requires an unsigned integer 'jeff.entrypoint' attribute"); +static FailureOr validateJeffEntryPoint(ModuleOp module) { + const auto entryPoint = module->getAttrOfType("jeff.entrypoint"); + if (!entryPoint || !entryPoint.getType().isUnsignedInteger()) { + module.emitError( + "jeff-to-qco requires an unsigned integer 'jeff.entrypoint' " + "attribute"); + return failure(); } - auto entryPoint = entryPointAttr.getUInt(); - - auto stringsAttr = moduleOp->getAttrOfType("jeff.strings"); - if (!stringsAttr) { - return moduleOp.emitError("requires an array 'jeff.strings' attribute"); + const auto strings = module->getAttrOfType("jeff.strings"); + if (!strings) { + module.emitError( + "jeff-to-qco requires an array 'jeff.strings' module attribute"); + return failure(); } - - if (entryPoint >= stringsAttr.size()) { - return moduleOp.emitError("'jeff.entrypoint' index is out of bounds"); + if (entryPoint.getValue().isNegative() || + entryPoint.getValue().getActiveBits() > 64) { + module.emitError("'jeff.entrypoint' must be a nonnegative function index"); + return failure(); } - - auto name = dyn_cast(stringsAttr[entryPoint]); - if (!name) { - return moduleOp.emitError("'jeff.entrypoint' must index a string"); + const auto index = entryPoint.getValue().getZExtValue(); + if (!llvm::all_of(strings, + [](Attribute value) { return isa(value); })) { + module.emitError("'jeff.entrypoint' must index a string and all entries in " + "'jeff.strings' must be strings"); + return failure(); + } + func::FuncOp function; + uint64_t functionIndex = 0; + for (auto candidate : module.getOps()) { + if (functionIndex++ == index) { + function = candidate; + break; + } + } + if (!function) { + module.emitError( + "'jeff.entrypoint' index is out of bounds for the function list"); + return failure(); + } + const auto name = function.getSymNameAttr(); + if (!llvm::any_of(strings, [name](Attribute value) { + return cast(value) == name; + })) { + module.emitError("the jeff entry-point function name is absent from " + "'jeff.strings'"); + return failure(); + } + if (function.isExternal() || !function.getBody().hasOneBlock() || + !isa(function.getBody().front().getTerminator())) { + function.emitError( + "jeff-to-qco requires a defined, single-block entry function ending " + "in func.return"); + return failure(); } - return name.getValue(); + return name; } /** @@ -1168,12 +1205,15 @@ struct ConvertJeffYieldOpToQCO final : OpConversionPattern { * ``` */ struct ConvertJeffMainToQCO final : OpConversionPattern { - using OpConversionPattern::OpConversionPattern; + ConvertJeffMainToQCO(TypeConverter& typeConverter, MLIRContext* context, + StringAttr entryPointName) + : OpConversionPattern(typeConverter, context, PatternBenefit(2)), + entryPointName(entryPointName) {} LogicalResult matchAndRewrite(func::FuncOp op, OpAdaptor /*adaptor*/, ConversionPatternRewriter& rewriter) const override { - if (op.getSymName() != getEntryPointName(op->getParentOfType())) { + if (op.getSymNameAttr() != entryPointName) { return failure(); } @@ -1219,6 +1259,9 @@ struct ConvertJeffMainToQCO final : OpConversionPattern { return success(); } + +private: + StringAttr entryPointName; }; /** @@ -1260,8 +1303,15 @@ struct JeffToQCO final : impl::JeffToQCOBase { protected: void runOnOperation() override { MLIRContext* context = &getContext(); - auto moduleOp = getOperation(); - auto entryPointName = getEntryPointName(moduleOp); + auto original = getOperation(); + constexpr size_t maxRegionNesting = 64; + if (failed(verifyRegionNestingDepth(original, maxRegionNesting))) { + signalPassFailure(); + return; + } + OwningOpRef converted(original.clone()); + auto module = *converted; + const auto entryPointName = validateJeffEntryPoint(module); if (failed(entryPointName)) { signalPassFailure(); return; @@ -1279,7 +1329,8 @@ struct JeffToQCO final : impl::JeffToQCOBase { tensor::TensorDialect, scf::SCFDialect>(); target.addDynamicallyLegalOp([&](func::FuncOp op) { - return (op.getSymName() != *entryPointName || mqt::isEntryPoint(op)) && + return (op.getSymNameAttr() != *entryPointName || + mqt::isEntryPoint(op)) && typeConverter.isSignatureLegal(op.getFunctionType()) && typeConverter.isLegal(&op.getBody()); }); @@ -1293,8 +1344,9 @@ struct JeffToQCO final : impl::JeffToQCOBase { populateReturnOpTypeConversionPattern(patterns, typeConverter); patterns.add( - typeConverter, context, PatternBenefit(2)); + ConvertJeffIntArrayGetIndexOpToCBit>(typeConverter, context, + PatternBenefit(2)); + patterns.add(typeConverter, context, *entryPointName); patterns.add< ConvertJeffQuregAllocOpToQCO, ConvertJeffQuregExtractIndexOpToQCO, ConvertJeffQuregInsertIndexOpToQCO, ConvertJeffQuregFreeZeroOpToQCO, @@ -1319,19 +1371,25 @@ struct JeffToQCO final : impl::JeffToQCOBase { context); // Apply the conversion - if (applyPartialConversion(moduleOp, target, std::move(patterns)) - .failed()) { + if (applyPartialConversion(module, target, std::move(patterns)).failed()) { signalPassFailure(); return; } - moduleOp->removeAttr("jeff.entrypoint"); - moduleOp->removeAttr("jeff.strings"); - moduleOp->removeAttr("jeff.tool"); - moduleOp->removeAttr("jeff.toolVersion"); - moduleOp->removeAttr("jeff.version"); - moduleOp->removeAttr("jeff.versionMinor"); - moduleOp->removeAttr("jeff.versionPatch"); + module->removeAttr("jeff.entrypoint"); + module->removeAttr("jeff.strings"); + module->removeAttr("jeff.tool"); + module->removeAttr("jeff.toolVersion"); + module->removeAttr("jeff.version"); + module->removeAttr("jeff.versionMinor"); + module->removeAttr("jeff.versionPatch"); + if (failed(verify(module)) || failed(mqt::verifyProgramMetadata(module)) || + failed(qco::verifyLinearity(module))) { + signalPassFailure(); + return; + } + original->setAttrs(module->getAttrDictionary()); + original.getBodyRegion().takeBody(module.getBodyRegion()); } }; diff --git a/mlir/lib/Conversion/QCOToJeff/CMakeLists.txt b/mlir/lib/Conversion/QCOToJeff/CMakeLists.txt index 9028a06963..b2c06b8411 100644 --- a/mlir/lib/Conversion/QCOToJeff/CMakeLists.txt +++ b/mlir/lib/Conversion/QCOToJeff/CMakeLists.txt @@ -21,6 +21,7 @@ add_mlir_conversion_library( MLIRMQTDialect MLIRMQTTransforms MLIRMQTUtils + MLIRQCDialect MLIRQCODialect MLIRTransforms) diff --git a/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp b/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp index 505c54cd5c..ffd3e9d18a 100644 --- a/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp +++ b/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp @@ -15,8 +15,10 @@ #include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Transforms/GlobalPhaseNormalization.h" #include "mlir/Dialect/MQT/Utils/GatePowering.h" +#include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" @@ -24,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -38,13 +41,16 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include +#include #include #include @@ -54,6 +60,7 @@ #include #include #include +#include #include #include #include @@ -203,6 +210,106 @@ struct LoweringState { } }; +[[nodiscard]] static LogicalResult validateQCOToJeffInput(ModuleOp module) { + if (failed(mqt::verifyProgramMetadata(module)) || + failed(qco::verifyLinearity(module))) { + return failure(); + } + func::FuncOp entryPoint; + for (auto function : module.getOps()) { + if (!mqt::isEntryPoint(function)) { + continue; + } + if (entryPoint) { + module.emitError( + "qco-to-jeff requires exactly one program entry function"); + return failure(); + } + entryPoint = function; + } + if (!entryPoint) { + module.emitError( + "qco-to-jeff requires a program entry function marked with " + "mqt.entry_point"); + return failure(); + } + if (entryPoint.isExternal() || !entryPoint.getBody().hasOneBlock() || + !isa(entryPoint.getBody().front().getTerminator())) { + entryPoint.emitError( + "qco-to-jeff requires a defined, single-block entry function ending " + "in func.return"); + return failure(); + } + + Operation* staticAllocation = nullptr; + Operation* dynamicAllocation = nullptr; + DenseSet staticIndices; + bool invalid = false; + const auto validateType = [&](Type type, Operation* owner) { + const auto tensor = dyn_cast(type); + if (tensor && isa(tensor.getElementType()) && + tensor.getRank() != 1) { + owner->emitError("qco-to-jeff only supports rank-one qco.qubit tensors"); + invalid = true; + } + }; + SmallVector worklist{module.getOperation()}; + while (!worklist.empty()) { + Operation* operation = worklist.pop_back_val(); + if (auto staticOp = dyn_cast(operation)) { + staticAllocation = operation; + if (!staticIndices.insert(staticOp.getIndex()).second) { + staticOp.emitError( + "qco-to-jeff cannot preserve duplicate static qubit index ") + << staticOp.getIndex(); + invalid = true; + } + } else if (isa(operation)) { + dynamicAllocation = operation; + } + if (auto control = dyn_cast(operation); + control && + control.getNumControls() > std::numeric_limits::max()) { + control.emitError( + "qco-to-jeff supports at most 255 controls on one operation"); + invalid = true; + } + for (const Type type : operation->getOperandTypes()) { + validateType(type, operation); + } + for (const Type type : operation->getResultTypes()) { + validateType(type, operation); + } + if (auto function = dyn_cast(operation)) { + for (const Type type : function.getArgumentTypes()) { + validateType(type, operation); + } + for (const Type type : function.getResultTypes()) { + validateType(type, operation); + } + } + for (Region& region : operation->getRegions()) { + for (Block& block : region) { + for (BlockArgument argument : block.getArguments()) { + validateType(argument.getType(), operation); + } + for (Operation& nested : block) { + worklist.push_back(&nested); + } + } + } + } + if (invalid) { + return failure(); + } + if (staticAllocation && dynamicAllocation) { + dynamicAllocation->emitError( + "qco-to-jeff cannot mix static and dynamic qubit allocations"); + return failure(); + } + return success(); +} + /** * @brief Base class for conversion patterns that need access to the * LoweringState @@ -469,25 +576,27 @@ static LogicalResult cleanUp(ModuleOp moduleOp, LoweringState& state) { return failure(); } + std::optional entryPoint; + uint64_t functionIndex = 0; for (auto funcOp : moduleOp.getOps()) { + if (funcOp.getSymName() == state.entryPointName) { + if (functionIndex > std::numeric_limits::max()) { + return failure(); + } + entryPoint = static_cast(functionIndex); + } state.strings.emplace_back(funcOp.getSymName()); + ++functionIndex; } - - auto* const it = llvm::find(state.strings, state.entryPointName); - if (it == state.strings.end()) { - return failure(); - } - const auto distance = std::distance(state.strings.begin(), it); - if (std::cmp_greater(distance, std::numeric_limits::max())) { + if (!entryPoint) { return failure(); } - const auto entryPoint = static_cast(distance); OpBuilder builder(moduleOp.getContext()); auto uint16Type = builder.getIntegerType(16, false); moduleOp->setAttr("jeff.entrypoint", - builder.getIntegerAttr(uint16Type, entryPoint)); + builder.getIntegerAttr(uint16Type, *entryPoint)); SmallVector stringRefs; stringRefs.reserve(state.strings.size()); @@ -1849,7 +1958,18 @@ struct QCOToJeff final : impl::QCOToJeffBase { protected: void runOnOperation() override { MLIRContext* context = &getContext(); - auto moduleOp = getOperation(); + auto original = getOperation(); + constexpr size_t maxRegionNesting = 64; + if (failed(verifyRegionNestingDepth(original, maxRegionNesting))) { + signalPassFailure(); + return; + } + OwningOpRef converted(original.clone()); + auto moduleOp = *converted; + if (failed(validateQCOToJeffInput(moduleOp))) { + signalPassFailure(); + return; + } if (failed(mqt::normalizeGlobalPhases(moduleOp))) { signalPassFailure(); return; @@ -1860,7 +1980,7 @@ struct QCOToJeff final : impl::QCOToJeffBase { QCOToJeffTypeConverter typeConverter(context); LoweringState state; - state.cbitState.recordRegisterUses(moduleOp); + state.cbitState.recordRegisterUses(moduleOp.getOperation()); // Configure conversion target target.addIllegalDialect { if (cleanUp(moduleOp, state).failed()) { signalPassFailure(); + return; + } + if (failed(verify(moduleOp)) || + failed(mqt::verifyProgramMetadata(moduleOp))) { + signalPassFailure(); + return; } + original->setAttrs(moduleOp->getAttrDictionary()); + original.getBodyRegion().takeBody(moduleOp.getBodyRegion()); } }; diff --git a/mlir/lib/Conversion/QCOToQC/CMakeLists.txt b/mlir/lib/Conversion/QCOToQC/CMakeLists.txt index 2fad50932a..56a0a4707e 100644 --- a/mlir/lib/Conversion/QCOToQC/CMakeLists.txt +++ b/mlir/lib/Conversion/QCOToQC/CMakeLists.txt @@ -15,8 +15,10 @@ add_mlir_conversion_library( QCOToQCIncGen LINK_LIBS MLIRCBitDialect + MLIRControlFlowDialect MLIRQCDialect MLIRQCODialect + MLIRQCOUtils MLIRQTensorDialect MLIRArithDialect MLIRFuncDialect diff --git a/mlir/lib/Conversion/QCOToQC/QCOToQC.cpp b/mlir/lib/Conversion/QCOToQC/QCOToQC.cpp index 9d96069e38..e272788be0 100644 --- a/mlir/lib/Conversion/QCOToQC/QCOToQC.cpp +++ b/mlir/lib/Conversion/QCOToQC/QCOToQC.cpp @@ -15,12 +15,15 @@ #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/IR/QCOps.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" #include #include +#include #include #include #include @@ -29,15 +32,20 @@ #include #include #include +#include #include #include #include +#include #include +#include #include #include #include #include +#include +#include #include namespace mlir { @@ -87,6 +95,58 @@ struct LoweringState { } }; +/** Invalidates cached slots for the same memref in enclosing regions. */ +static void invalidateAncestorQTensorCaches(LoweringState& state, + Region* region, Value memref) { + for (auto* current = region->getParentRegion(); current != nullptr; + current = current->getParentRegion()) { + if (auto it = state.extractedIndices.find(current); + it != state.extractedIndices.end()) { + it->second.erase(memref); + } + if (auto it = state.qubitValues.find(current); + it != state.qubitValues.end()) { + it->second.erase(memref); + } + } +} + +template +[[nodiscard]] static LogicalResult visitOperations(Operation* root, + Callback callback) { + SmallVector operations{root}; + for (size_t next = 0; next < operations.size(); ++next) { + auto* operation = operations[next]; + if (failed(callback(operation))) { + return failure(); + } + for (Region& region : operation->getRegions()) { + for (Block& block : region) { + for (auto& nested : block) { + operations.push_back(&nested); + } + } + } + } + return success(); +} + +[[nodiscard]] static LogicalResult +validateAllocationContracts(Operation* root, LoweringState& state) { + return visitOperations(root, [&](Operation* op) { + std::optional mode; + if (isa(op)) { + mode = AllocationMode::Static; + } else if (isa(op)) { + mode = AllocationMode::Dynamic; + } + if (mode && failed(state.ensureAllocationMode(*mode, op))) { + return failure(); + } + return success(); + }); +} + /** * @brief Base class for conversion patterns that need access to lowering state * @@ -149,6 +209,122 @@ static void inlineRegion(Region& sourceRegion, Region& targetRegion, return memref && isa(memref.getElementType()); } +/// Trace a value-semantics quantum value to its reference-semantics input. +[[nodiscard]] static Value traceReferenceInput(Value value) { + while (auto result = dyn_cast(value)) { + Operation* op = result.getOwner(); + if (auto unitary = dyn_cast(op)) { + value = unitary.getInputForOutput(value); + continue; + } + if (auto measure = dyn_cast(op)) { + value = measure.getQubitIn(); + continue; + } + if (auto reset = dyn_cast(op)) { + value = reset.getQubitIn(); + continue; + } + if (auto extract = dyn_cast(op)) { + if (value != extract.getOutTensor()) { + return {}; + } + value = extract.getTensor(); + continue; + } + if (auto insert = dyn_cast(op)) { + value = insert.getDest(); + continue; + } + if (auto forOp = dyn_cast(op)) { + value = forOp.getTiedLoopInit(result)->get(); + continue; + } + if (auto whileOp = dyn_cast(op)) { + value = whileOp.getInits()[result.getResultNumber()]; + continue; + } + if (auto ifOp = dyn_cast(op)) { + auto* input = ifOp.getTiedQubit(result); + if (input == nullptr) { + return {}; + } + value = input->get(); + continue; + } + if (auto switchOp = dyn_cast(op)) { + auto* input = switchOp.getTiedTarget(result); + if (input == nullptr) { + return {}; + } + value = input->get(); + continue; + } + return {}; + } + return value; +} + +[[nodiscard]] static LogicalResult +validateReferencePreservingYield(Operation* terminator, ValueRange yielded, + ValueRange inputs) { + SmallVector quantumYields; + SmallVector quantumInputs; + llvm::copy_if(yielded, std::back_inserter(quantumYields), [](Value value) { + return isQuantumStateType(value.getType()); + }); + llvm::copy_if(inputs, std::back_inserter(quantumInputs), [](Value value) { + return isQuantumStateType(value.getType()); + }); + if (quantumYields.size() != quantumInputs.size()) { + return terminator->emitOpError( + "QCO-to-QC conversion requires quantum region results to match the " + "region inputs"); + } + for (auto [yieldedValue, input] : + llvm::zip_equal(quantumYields, quantumInputs)) { + if (traceReferenceInput(yieldedValue) != input) { + return terminator->emitOpError( + "QCO-to-QC conversion requires quantum region results to preserve " + "input order"); + } + } + return success(); +} + +[[nodiscard]] static LogicalResult +validateReferencePreservingYields(Operation* root) { + return visitOperations(root, [&](Operation* op) { + ValueRange yielded; + if (auto yieldOp = dyn_cast(op)) { + yielded = yieldOp.getTargets(); + } else if (auto yieldOp = dyn_cast(op)) { + if (!isa(op->getParentOp())) { + if (llvm::none_of(yieldOp.getResults(), [](Value value) { + return isQuantumStateType(value.getType()); + })) { + return success(); + } + yieldOp.emitOpError( + "QCO-to-QC conversion supports quantum state in scf.yield only " + "for scf.for and scf.while"); + return failure(); + } + yielded = yieldOp.getResults(); + } else if (auto conditionOp = dyn_cast(op)) { + yielded = conditionOp.getArgs(); + } else { + return success(); + } + + SmallVector inputs(op->getBlock()->getArguments()); + if (failed(validateReferencePreservingYield(op, yielded, inputs))) { + return failure(); + } + return success(); + }); +} + [[nodiscard]] static SmallVector selectConvertedState(ValueRange originalValues, ValueRange convertedValues, const bool selectQuantum) { @@ -337,15 +513,38 @@ struct ConvertQTensorExtractOp final } }; -/** - * @brief Removes qtensor.insert operations - */ -struct ConvertQTensorInsertOp final : OpConversionPattern { - using OpConversionPattern::OpConversionPattern; +/** Converts qtensor.insert to an in-place memref.store. */ +struct ConvertQTensorInsertOp final + : StatefulOpConversionPattern { + using StatefulOpConversionPattern::StatefulOpConversionPattern; LogicalResult matchAndRewrite(qtensor::InsertOp op, OpAdaptor adaptor, ConversionPatternRewriter& rewriter) const override { + auto& state = getState(); + auto* region = op->getParentRegion(); + auto dest = adaptor.getDest(); + auto index = adaptor.getIndex(); + auto scalar = adaptor.getScalar(); + auto& extractedIndices = state.extractedIndices[region][dest]; + auto& qubitValues = state.qubitValues[region][dest]; + if (extractedIndices.contains(index) && + qubitValues.lookup(index) == scalar) { + rewriter.replaceOp(op, dest); + return success(); + } + + memref::StoreOp::create(rewriter, op.getLoc(), scalar, dest, + ValueRange{index}); + invalidateAncestorQTensorCaches(state, region, dest); + + // A dynamic index may alias any previously observed slot. Rebuild the + // cache conservatively, retaining only the value established by this + // store. + extractedIndices.clear(); + qubitValues.clear(); + extractedIndices.insert(index); + qubitValues[index] = scalar; rewriter.replaceOp(op, adaptor.getDest()); return success(); } @@ -1174,11 +1373,25 @@ struct QCOToQC final : impl::QCOToQCBase { protected: void runOnOperation() override { MLIRContext* context = &getContext(); - auto moduleOp = getOperation(); + auto original = getOperation(); + constexpr size_t maxRegionNesting = 64; + if (failed(verifyRegionNestingDepth(original, maxRegionNesting))) { + signalPassFailure(); + return; + } + OwningOpRef converted(original.clone()); + auto module = *converted; // Create state object to track the qubit addressing mode LoweringState state; + if (failed(qco::verifyLinearity(module)) || + failed(validateAllocationContracts(module, state)) || + failed(validateReferencePreservingYields(module))) { + signalPassFailure(); + return; + } + ConversionTarget target(*context); RewritePatternSet patterns(context); QCOToQCTypeConverter typeConverter(context); @@ -1207,8 +1420,8 @@ struct QCOToQC final : impl::QCOToQCBase { // Register operation conversion patterns that do not need state tracking patterns - .add>( typeConverter, context); @@ -1224,9 +1437,9 @@ struct QCOToQC final : impl::QCOToQCBase { ConvertQCOSCFForOp>(typeConverter, context); // Register operation conversion patterns that need state tracking - patterns.add( - typeConverter, context, &state); + patterns.add(typeConverter, context, &state); // Conversion of qco types in func.func signatures // Note: This currently has limitations with signature changes @@ -1251,9 +1464,16 @@ struct QCOToQC final : impl::QCOToQCBase { populateBranchOpInterfaceTypeConversionPattern(patterns, typeConverter); // Apply the conversion - if (failed(applyPartialConversion(moduleOp, target, std::move(patterns)))) { + if (failed(applyPartialConversion(module, target, std::move(patterns)))) { + signalPassFailure(); + return; + } + if (failed(verify(module))) { signalPassFailure(); + return; } + original->setAttrs(module->getAttrDictionary()); + original.getBodyRegion().takeBody(module.getBodyRegion()); } }; diff --git a/mlir/lib/Conversion/QCToQCO/CMakeLists.txt b/mlir/lib/Conversion/QCToQCO/CMakeLists.txt index c4c998c5c0..0e7cefa8ab 100644 --- a/mlir/lib/Conversion/QCToQCO/CMakeLists.txt +++ b/mlir/lib/Conversion/QCToQCO/CMakeLists.txt @@ -15,6 +15,7 @@ add_mlir_conversion_library( QCToQCOIncGen LINK_LIBS MLIRCBitDialect + MLIRControlFlowDialect MLIRQCDialect MLIRQCODialect MLIRQTensorDialect diff --git a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp index f00d37b32c..226a5cd962 100644 --- a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp +++ b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp @@ -18,12 +18,16 @@ #include "mlir/Dialect/QC/IR/QCOps.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" +#include #include #include +#include #include +#include #include #include #include @@ -35,13 +39,16 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include +#include #include #include #include @@ -78,10 +85,100 @@ struct RegisterAccess { /** @brief Indices already used for one register by a quantum operation. */ struct SeenRegisterIndices { - DenseMap constants; - llvm::SmallDenseSet dynamicValues; + SmallVector values; }; +/** @brief A register index represented as one SSA value plus a constant. */ +struct AffineRegisterIndex { + Value base; + int64_t offset; +}; + +/** @brief Peels constant additions and subtractions from a register index. */ +[[nodiscard]] static AffineRegisterIndex decomposeRegisterIndex(Value index) { + if (const auto constant = getConstantIntValue(index)) { + return {.base = {}, .offset = *constant}; + } + + int64_t offset = 0; + while (true) { + Value next; + int64_t nextOffset = 0; + if (auto add = index.getDefiningOp()) { + if (const auto lhs = getConstantIntValue(add.getLhs())) { + next = add.getRhs(); + if (llvm::AddOverflow(offset, *lhs, nextOffset)) { + break; + } + } else if (const auto rhs = getConstantIntValue(add.getRhs())) { + next = add.getLhs(); + if (llvm::AddOverflow(offset, *rhs, nextOffset)) { + break; + } + } else { + break; + } + } else if (auto sub = index.getDefiningOp()) { + const auto rhs = getConstantIntValue(sub.getRhs()); + if (!rhs || llvm::SubOverflow(offset, *rhs, nextOffset)) { + break; + } + next = sub.getLhs(); + } else { + break; + } + index = next; + offset = nextOffset; + } + return {.base = index, .offset = offset}; +} + +/** @brief Checks whether a constant is outside a constant-bounded loop IV. */ +[[nodiscard]] static bool +isOutsideForInductionRange(int64_t constant, const AffineRegisterIndex& index) { + auto argument = dyn_cast(index.base); + if (!argument) { + return false; + } + auto forOp = dyn_cast_or_null(argument.getOwner()->getParentOp()); + if (!forOp || forOp.getInductionVar() != argument) { + return false; + } + + const auto lower = getConstantIntValue(forOp.getLowerBound()); + const auto upper = getConstantIntValue(forOp.getUpperBound()); + const auto step = getConstantIntValue(forOp.getStep()); + if (!lower || !upper || !step || *step <= 0) { + return false; + } + + int64_t adjustedLower = 0; + int64_t adjustedUpper = 0; + if (llvm::AddOverflow(*lower, index.offset, adjustedLower) || + llvm::AddOverflow(*upper, index.offset, adjustedUpper)) { + return false; + } + return adjustedLower >= adjustedUpper || constant < adjustedLower || + constant >= adjustedUpper; +} + +/** @brief Proves two supported register-index expressions are distinct. */ +[[nodiscard]] static bool areProvablyDistinctRegisterIndices(Value lhs, + Value rhs) { + const auto lhsIndex = decomposeRegisterIndex(lhs); + const auto rhsIndex = decomposeRegisterIndex(rhs); + if (lhsIndex.base == rhsIndex.base) { + return lhsIndex.offset != rhsIndex.offset; + } + if (!lhsIndex.base) { + return isOutsideForInductionRange(lhsIndex.offset, rhsIndex); + } + if (!rhsIndex.base) { + return isOutsideForInductionRange(rhsIndex.offset, lhsIndex); + } + return false; +} + /** @brief Qubit allocation mode */ enum class AllocationMode : std::uint8_t { Unset, //!< No allocation mode has been established yet. @@ -130,6 +227,9 @@ struct LoweringState { /// (typically a `func.func` body or a modifier region). DenseMap> qubitMap; + /// Per-region canonical QC reference for each physical static-qubit index. + DenseMap> staticQubitKeys; + /// Per-region map from stable register identifiers to their latest QTensor /// SSA values. DenseMap> tensorMap; @@ -176,6 +276,26 @@ struct LoweringState { } }; +[[nodiscard]] static LogicalResult +validateAllocationMode(Operation* root, LoweringState& state) { + const auto result = root->walk([&](Operation* op) { + std::optional mode; + if (auto staticOp = dyn_cast(op)) { + mode = AllocationMode::Static; + } else if (isa(op)) { + mode = AllocationMode::Dynamic; + } else if (auto alloc = dyn_cast(op); + alloc && isa(alloc.getType().getElementType())) { + mode = AllocationMode::Dynamic; + } + if (mode && failed(state.ensureAllocationMode(*mode, op))) { + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + return success(!result.wasInterrupted()); +} + /** * @brief Base class for conversion patterns that need access to lowering state * @@ -208,7 +328,7 @@ class StatefulOpConversionPattern : public OpConversionPattern { } // namespace /** @brief Returns whether a type is ranked or unranked QC qubit storage. */ -[[nodiscard]] static bool isQubitMemrefType(const Type type) { +[[nodiscard]] static bool isQubitMemrefType(Type type) { const auto memref = dyn_cast(type); return memref && isa(memref.getElementType()); } @@ -256,23 +376,26 @@ findRegionLocalMap(DenseMap>& map, } /** @brief Resolves the latest QCO SSA value for a QC qubit reference. */ -[[nodiscard]] static Value lookupMappedQubit(LoweringState& state, - Operation* anchor, Value qcQubit) { +[[nodiscard]] static FailureOr +lookupMappedQubit(LoweringState& state, Operation* anchor, Value qcQubit) { qcQubit = canonicalQubitKey(state, qcQubit); const auto& [qubitMap, qubitValue] = findRegionLocalMap(state.qubitMap, anchor, qcQubit); - assert(qubitMap != nullptr && qubitValue != nullptr && "QC qubit not found"); + if (qubitMap == nullptr || qubitValue == nullptr) { + return anchor->emitOpError("references a qubit that has no live QCO value"); + } return *qubitValue; } /** @brief Resolves the latest QTensor SSA value for a QC register. */ -[[nodiscard]] static Value lookupMappedTensor(LoweringState& state, - Operation* anchor, - const RegisterId reg) { +[[nodiscard]] static FailureOr +lookupMappedTensor(LoweringState& state, Operation* anchor, RegisterId reg) { const auto& [tensorMap, tensorValue] = findRegionLocalMap(state.tensorMap, anchor, reg); - assert(tensorMap != nullptr && tensorValue != nullptr && - "QC register not found"); + if (tensorMap == nullptr || tensorValue == nullptr) { + return anchor->emitOpError( + "references a qubit register that has no live QTensor value"); + } return *tensorValue; } @@ -313,22 +436,34 @@ static void assignMappedTensor(LoweringState& state, Operation* anchor, /** @brief Resolves a range of QC qubits to their latest QCO values. */ template -[[nodiscard]] static SmallVector +[[nodiscard]] static FailureOr> resolveMappedQubits(LoweringState& state, Operation* anchor, const Range& qcQubits) { - return llvm::to_vector(llvm::map_range(qcQubits, [&](Value qcQubit) { - return lookupMappedQubit(state, anchor, qcQubit); - })); + SmallVector qcoQubits; + for (auto qcQubit : qcQubits) { + auto qcoQubit = lookupMappedQubit(state, anchor, qcQubit); + if (failed(qcoQubit)) { + return failure(); + } + qcoQubits.push_back(*qcoQubit); + } + return qcoQubits; } /** @brief Resolves a range of QC memrefs to their latest QTensor values. */ template -[[nodiscard]] static SmallVector +[[nodiscard]] static FailureOr> resolveMappedTensors(LoweringState& state, Operation* anchor, const Range& registers) { - return llvm::to_vector(llvm::map_range(registers, [&](RegisterId reg) { - return lookupMappedTensor(state, anchor, reg); - })); + SmallVector tensors; + for (const RegisterId reg : registers) { + auto tensor = lookupMappedTensor(state, anchor, reg); + if (failed(tensor)) { + return failure(); + } + tensors.push_back(*tensor); + } + return tensors; } /** @brief Updates mappings for matching QC and QCO qubit ranges. */ @@ -385,43 +520,82 @@ struct MaterializedQubits { /** * @brief Materializes register-backed qubits immediately before a quantum op. */ -[[nodiscard]] static MaterializedQubits +[[nodiscard]] static FailureOr materializeQubits(LoweringState& state, Operation* anchor, ValueRange qcQubits, PatternRewriter& rewriter) { MaterializedQubits materialized; materialized.values.reserve(qcQubits.size()); materialized.accesses.reserve(qcQubits.size()); + SmallVector standaloneValues(qcQubits.size()); + DenseMap currentTensors; - for (auto qcQubit : qcQubits) { + for (auto [position, qcQubit] : llvm::enumerate(qcQubits)) { const auto accessIt = state.registerAccesses.find(qcQubit); if (accessIt == state.registerAccesses.end()) { - materialized.values.push_back(lookupMappedQubit(state, anchor, qcQubit)); + auto qcoQubit = lookupMappedQubit(state, anchor, qcQubit); + if (failed(qcoQubit)) { + return failure(); + } + standaloneValues[position] = *qcoQubit; materialized.accesses.emplace_back(); continue; } const auto access = accessIt->second; - auto tensor = lookupMappedTensor(state, anchor, access.reg); - auto extract = qtensor::ExtractOp::create(rewriter, anchor->getLoc(), - tensor, access.index); - assignMappedTensor(state, anchor, access.reg, extract.getOutTensor()); - materialized.values.push_back(extract.getResult()); + if (!currentTensors.contains(access.reg)) { + auto tensor = lookupMappedTensor(state, anchor, access.reg); + if (failed(tensor)) { + return failure(); + } + currentTensors.try_emplace(access.reg, *tensor); + } materialized.accesses.emplace_back(access); } + for (auto [position, access] : llvm::enumerate(materialized.accesses)) { + if (!access) { + materialized.values.push_back(standaloneValues[position]); + continue; + } + + auto extract = qtensor::ExtractOp::create( + rewriter, anchor->getLoc(), currentTensors[access->reg], access->index); + currentTensors[access->reg] = extract.getOutTensor(); + materialized.values.push_back(extract.getResult()); + } + for (const auto& [reg, tensor] : currentTensors) { + assignMappedTensor(state, anchor, reg, tensor); + } + return materialized; } /** * @brief Commits quantum-operation results to standalone mappings or QTensor. */ -static void commitQubits(LoweringState& state, Operation* anchor, - ValueRange qcQubits, ValueRange qcoQubits, - const MaterializedQubits& materialized, - PatternRewriter& rewriter) { +[[nodiscard]] static LogicalResult +commitQubits(LoweringState& state, Operation* anchor, ValueRange qcQubits, + ValueRange qcoQubits, const MaterializedQubits& materialized, + PatternRewriter& rewriter) { assert(qcQubits.size() == qcoQubits.size()); assert(qcQubits.size() == materialized.accesses.size()); + DenseMap currentTensors; + // A successful materializeQubits call republished every register referenced + // below, so these lookups cannot fail on a reachable pattern path. Resolve + // them all before creating inserts to keep this helper failure-atomic even + // if its internal state is inconsistent. + for (const auto& access : materialized.accesses) { + if (!access || currentTensors.contains(access->reg)) { + continue; + } + auto tensor = lookupMappedTensor(state, anchor, access->reg); + if (failed(tensor)) { + return failure(); + } + currentTensors.try_emplace(access->reg, *tensor); + } + for (std::size_t i = qcQubits.size(); i > 0; --i) { const auto position = i - 1; const auto& access = materialized.accesses[position]; @@ -430,16 +604,20 @@ static void commitQubits(LoweringState& state, Operation* anchor, continue; } - auto tensor = lookupMappedTensor(state, anchor, access->reg); auto insert = qtensor::InsertOp::create( - rewriter, anchor->getLoc(), qcoQubits[position], tensor, access->index); - assignMappedTensor(state, anchor, access->reg, insert.getResult()); + rewriter, anchor->getLoc(), qcoQubits[position], + currentTensors[access->reg], access->index); + currentTensors[access->reg] = insert.getResult(); } + for (const auto& [reg, tensor] : currentTensors) { + assignMappedTensor(state, anchor, reg, tensor); + } + return success(); } /** @brief Resolves all structured QC state to QCO and QTensor values. */ -[[nodiscard]] static SmallVector resolveAllValues(LoweringState& state, - Operation* anchor) { +[[nodiscard]] static FailureOr> +resolveAllValues(LoweringState& state, Operation* anchor) { SmallVector registers; SmallVector qcQubits; auto* owner = structuredValueOwner(anchor); @@ -454,9 +632,16 @@ static void commitQubits(LoweringState& state, Operation* anchor, SmallVector qcoTargets; qcoTargets.reserve(registers.size() + qcQubits.size()); - llvm::append_range(qcoTargets, - resolveMappedTensors(state, anchor, registers)); - llvm::append_range(qcoTargets, resolveMappedQubits(state, anchor, qcQubits)); + auto tensors = resolveMappedTensors(state, anchor, registers); + if (failed(tensors)) { + return failure(); + } + llvm::append_range(qcoTargets, *tensors); + auto qcoQubits = resolveMappedQubits(state, anchor, qcQubits); + if (failed(qcoQubits)) { + return failure(); + } + llvm::append_range(qcoTargets, *qcoQubits); return qcoTargets; } @@ -607,25 +792,32 @@ collectRegisterAccesses(Operation* root, LoweringState& state) { } auto& seen = registerIndices[access->second.reg]; - if (const auto constant = getConstantIntValue(access->second.index)) { - const auto [it, inserted] = - seen.constants.try_emplace(*constant, access->second.index); - if (!inserted && - isEqualConstantIntOrValue(it->second, access->second.index)) { + for (auto previous : seen.values) { + if (areProvablyDistinctRegisterIndices(previous, + access->second.index)) { + continue; + } + const auto previousConstant = getConstantIntValue(previous); + const auto currentConstant = getConstantIntValue(access->second.index); + if (previousConstant && currentConstant && + *previousConstant == *currentConstant) { operation->emitOpError( "requires distinct qubit operands; register-backed operands " "have the same constant index"); return WalkResult::interrupt(); } - continue; - } - - if (!seen.dynamicValues.insert(access->second.index).second) { + if (previous == access->second.index) { + operation->emitOpError( + "requires distinct qubit operands; register-backed operands " + "use the same dynamic index"); + return WalkResult::interrupt(); + } operation->emitOpError( - "requires distinct qubit operands; register-backed operands use " - "the same dynamic index"); + "requires distinct qubit operands; register-backed indices are " + "not provably distinct"); return WalkResult::interrupt(); } + seen.values.push_back(access->second.index); } return WalkResult::advance(); }); @@ -775,7 +967,8 @@ struct ConvertFuncReturnOp final : StatefulOpConversionPattern { DenseSet liveQubits; for (auto [qcOperand, adaptorOperand] : llvm::zip_equal(op.getOperands(), adaptor.getOperands())) { - if (auto it = map.find(qcOperand); it != map.end()) { + const auto key = canonicalQubitKey(state, qcOperand); + if (auto it = map.find(key); it != map.end()) { auto latest = it->second; returnValues.emplace_back(latest); liveQubits.insert(latest); @@ -934,9 +1127,12 @@ struct ConvertMemRefDeallocOp final auto& tensorMap = state.tensorMap[op->getParentRegion()]; const auto reg = lookupRegisterId(state, memref); auto qtensor = lookupMappedTensor(state, op.getOperation(), reg); + if (failed(qtensor)) { + return failure(); + } tensorMap.erase(reg); - rewriter.replaceOpWithNewOp(op, qtensor); + rewriter.replaceOpWithNewOp(op, *qtensor); return success(); } }; @@ -998,17 +1194,32 @@ struct ConvertQCDeallocOp final : StatefulOpConversionPattern { matchAndRewrite(DeallocOp op, OpAdaptor /*adaptor*/, ConversionPatternRewriter& rewriter) const override { auto& state = getState(); - auto& qubitMap = state.qubitMap[op->getParentRegion()]; + auto* region = op->getParentRegion(); + auto& qubitMap = state.qubitMap[region]; auto* operation = op.getOperation(); - auto qcQubit = op.getQubit(); + auto qcQubit = canonicalQubitKey(state, op.getQubit()); auto qcoQubit = lookupMappedQubit(state, operation, qcQubit); + if (failed(qcoQubit)) { + return failure(); + } // Create the sink operation - rewriter.replaceOpWithNewOp(op, qcoQubit); + rewriter.replaceOpWithNewOp(op, *qcoQubit); // Remove from state as qubit is no longer in use qubitMap.erase(qcQubit); + if (auto staticKeysIt = state.staticQubitKeys.find(region); + staticKeysIt != state.staticQubitKeys.end()) { + auto& staticKeys = staticKeysIt->second; + const auto staticKey = + llvm::find_if(staticKeys, [qcQubit](const auto& entry) { + return entry.second == qcQubit; + }); + if (staticKey != staticKeys.end()) { + staticKeys.erase(staticKey); + } + } return success(); } @@ -1041,6 +1252,19 @@ struct ConvertQCStaticOp final : StatefulOpConversionPattern { return failure(); } auto qcQubit = op.getQubit(); + auto& staticKeys = state.staticQubitKeys[op->getParentRegion()]; + const auto [keyIt, inserted] = + staticKeys.try_emplace(op.getIndex(), qcQubit); + if (!inserted) { + auto qcoQubit = + lookupMappedQubit(state, op.getOperation(), keyIt->second); + if (failed(qcoQubit)) { + return failure(); + } + state.convertedQubitAliases[qcQubit] = keyIt->second; + rewriter.replaceOp(op, *qcoQubit); + return success(); + } auto qcoOp = rewriter.replaceOpWithNewOp(op, op.getIndex()); assignMappedQubit(state, qcoOp, qcQubit, qcoOp.getQubit()); @@ -1081,13 +1305,19 @@ struct ConvertQCMeasureOp final : StatefulOpConversionPattern { auto qcQubit = op.getQubit(); const SmallVector qcQubits{qcQubit}; auto materialized = materializeQubits(state, operation, qcQubits, rewriter); + if (failed(materialized)) { + return failure(); + } // Create qco.measure (returns both output qubit and bit result) auto qcoOp = - qco::MeasureOp::create(rewriter, op.getLoc(), materialized.values[0]); + qco::MeasureOp::create(rewriter, op.getLoc(), materialized->values[0]); const SmallVector qcoQubits{qcoOp.getQubitOut()}; - commitQubits(state, operation, qcQubits, qcoQubits, materialized, rewriter); + if (failed(commitQubits(state, operation, qcQubits, qcoQubits, + *materialized, rewriter))) { + return failure(); + } // Replace the QC operation's bit result with the QCO bit result rewriter.replaceOp(op, qcoOp.getResult()); @@ -1127,13 +1357,19 @@ struct ConvertQCResetOp final : StatefulOpConversionPattern { auto qcQubit = op.getQubit(); const SmallVector qcQubits{qcQubit}; auto materialized = materializeQubits(state, operation, qcQubits, rewriter); + if (failed(materialized)) { + return failure(); + } // Create qco.reset (consumes input, produces output) auto qcoOp = - qco::ResetOp::create(rewriter, op.getLoc(), materialized.values[0]); + qco::ResetOp::create(rewriter, op.getLoc(), materialized->values[0]); const SmallVector qcoQubits{qcoOp.getQubitOut()}; - commitQubits(state, operation, qcQubits, qcoQubits, materialized, rewriter); + if (failed(commitQubits(state, operation, qcQubits, qcoQubits, + *materialized, rewriter))) { + return failure(); + } // Erase the old (it has no results to replace) rewriter.eraseOp(op); @@ -1164,12 +1400,17 @@ struct ConvertQCGateToQCO final : StatefulOpConversionPattern { auto& state = this->getState(); auto qcTargets = op.getTargets(); auto materialized = materializeQubits(state, op, qcTargets, rewriter); - auto qcoOp = createGate(rewriter, op, materialized.values, + if (failed(materialized)) { + return failure(); + } + auto qcoOp = createGate(rewriter, op, materialized->values, std::make_index_sequence{}, std::make_index_sequence{}); - commitQubits(state, op, qcTargets, qcoOp.getOutputTargets(), materialized, - rewriter); + if (failed(commitQubits(state, op, qcTargets, qcoOp.getOutputTargets(), + *materialized, rewriter))) { + return failure(); + } rewriter.eraseOp(op); @@ -1188,11 +1429,16 @@ struct ConvertQCUnitaryOp final : StatefulOpConversionPattern { auto* operation = op.getOperation(); auto qcQubits = op.getQubits(); auto materialized = materializeQubits(state, operation, qcQubits, rewriter); + if (failed(materialized)) { + return failure(); + } auto qcoOp = qco::UnitaryOp::create(rewriter, op.getLoc(), - materialized.values, op.getMatrix()); + materialized->values, op.getMatrix()); - commitQubits(state, operation, qcQubits, qcoOp.getQubitsOut(), materialized, - rewriter); + if (failed(commitQubits(state, operation, qcQubits, qcoOp.getQubitsOut(), + *materialized, rewriter))) { + return failure(); + } rewriter.eraseOp(op); return success(); } @@ -1221,13 +1467,18 @@ struct ConvertQCBarrierOp final : StatefulOpConversionPattern { auto* operation = op.getOperation(); auto qcQubits = op.getQubits(); auto materialized = materializeQubits(state, operation, qcQubits, rewriter); + if (failed(materialized)) { + return failure(); + } // Create qco.barrier auto qcoOp = - qco::BarrierOp::create(rewriter, op.getLoc(), materialized.values); + qco::BarrierOp::create(rewriter, op.getLoc(), materialized->values); - commitQubits(state, operation, qcQubits, qcoOp.getQubitsOut(), materialized, - rewriter); + if (failed(commitQubits(state, operation, qcQubits, qcoOp.getQubitsOut(), + *materialized, rewriter))) { + return failure(); + } rewriter.eraseOp(op); return success(); @@ -1262,10 +1513,13 @@ struct ConvertQCCtrlOp final : StatefulOpConversionPattern { auto qcControls = op.getControls(); auto qcQubits = op.getQubits(); auto materialized = materializeQubits(state, operation, qcQubits, rewriter); + if (failed(materialized)) { + return failure(); + } auto qcoControls = - ValueRange(materialized.values).take_front(qcControls.size()); + ValueRange(materialized->values).take_front(qcControls.size()); auto qcoTargets = - ValueRange(materialized.values).drop_front(qcControls.size()); + ValueRange(materialized->values).drop_front(qcControls.size()); // Create qco.ctrl auto qcoOp = @@ -1274,7 +1528,10 @@ struct ConvertQCCtrlOp final : StatefulOpConversionPattern { SmallVector qcoQubits; llvm::append_range(qcoQubits, qcoOp.getControlsOut()); llvm::append_range(qcoQubits, qcoOp.getTargetsOut()); - commitQubits(state, operation, qcQubits, qcoQubits, materialized, rewriter); + if (failed(commitQubits(state, operation, qcQubits, qcoQubits, + *materialized, rewriter))) { + return failure(); + } const SmallVector sourceArguments( op.getRegion().front().getArguments()); @@ -1320,12 +1577,19 @@ struct ConvertQCInvOp final : StatefulOpConversionPattern { auto qcTargets = op.getTargets(); auto materialized = materializeQubits(state, operation, qcTargets, rewriter); + if (failed(materialized)) { + return failure(); + } // Create qco.inv - auto qcoOp = qco::InvOp::create(rewriter, op.getLoc(), materialized.values); + auto qcoOp = + qco::InvOp::create(rewriter, op.getLoc(), materialized->values); - commitQubits(state, operation, qcTargets, qcoOp.getOutputTargets(), - materialized, rewriter); + if (failed(commitQubits(state, operation, qcTargets, + qcoOp.getOutputTargets(), *materialized, + rewriter))) { + return failure(); + } const SmallVector sourceArguments( op.getRegion().front().getArguments()); @@ -1371,13 +1635,18 @@ struct ConvertQCPowOp final : StatefulOpConversionPattern { auto qcTargets = op.getTargets(); auto materialized = materializeQubits(state, operation, qcTargets, rewriter); + if (failed(materialized)) { + return failure(); + } // Create qco.pow with exponent. - auto qcoOp = qco::PowOp::create(rewriter, op.getLoc(), materialized.values, + auto qcoOp = qco::PowOp::create(rewriter, op.getLoc(), materialized->values, op.getExponent()); - commitQubits(state, operation, qcTargets, qcoOp.getQubitsOut(), - materialized, rewriter); + if (failed(commitQubits(state, operation, qcTargets, qcoOp.getQubitsOut(), + *materialized, rewriter))) { + return failure(); + } const SmallVector sourceArguments( op.getRegion().front().getArguments()); @@ -1422,7 +1691,10 @@ struct ConvertQCYieldOp final : StatefulOpConversionPattern { } auto targets = resolveMappedQubits(state, operation, frame->second); - rewriter.replaceOpWithNewOp(op, targets); + if (failed(targets)) { + return failure(); + } + rewriter.replaceOpWithNewOp(op, *targets); state.qubitMap.erase(region); state.modifierRegionQubits.erase(frame); return success(); @@ -1464,7 +1736,11 @@ struct ConvertSCFForOp final : StatefulOpConversionPattern { const auto numRegisters = registerMap.size(); const auto numQubits = qubitMap.size(); - auto qcoTargets = resolveAllValues(state, operation); + auto qcoTargetsOr = resolveAllValues(state, operation); + if (failed(qcoTargetsOr)) { + return failure(); + } + auto& qcoTargets = *qcoTargetsOr; const auto numOriginalResults = op.getNumResults(); SmallVector initArgs(op.getInitArgs()); llvm::append_range(initArgs, qcoTargets); @@ -1548,7 +1824,11 @@ struct ConvertSCFWhileOp final : StatefulOpConversionPattern { const auto numRegisters = registerMap.size(); const auto numQubits = qubitMap.size(); - auto qcoTargets = resolveAllValues(state, operation); + auto qcoTargetsOr = resolveAllValues(state, operation); + if (failed(qcoTargetsOr)) { + return failure(); + } + auto& qcoTargets = *qcoTargetsOr; const auto numOriginalInits = op.getInits().size(); const auto numOriginalResults = op.getNumResults(); SmallVector initArgs(op.getInits()); @@ -1653,7 +1933,11 @@ struct ConvertSCFIfOp final : StatefulOpConversionPattern { const auto numRegisters = registerMap.size(); const auto numQubits = qubitMap.size(); - auto qcoTargets = resolveAllValues(state, operation); + auto qcoTargetsOr = resolveAllValues(state, operation); + if (failed(qcoTargetsOr)) { + return failure(); + } + auto& qcoTargets = *qcoTargetsOr; // Create the new IfOp auto newIfOp = IfOp::create(rewriter, op.getLoc(), op.getResultTypes(), @@ -1747,7 +2031,11 @@ struct ConvertSCFIndexSwitchOp final const auto numRegisters = registerMap.size(); const auto numQubits = qubitMap.size(); - const auto targets = resolveAllValues(state, operation); + auto targetsOr = resolveAllValues(state, operation); + if (failed(targetsOr)) { + return failure(); + } + auto& targets = *targetsOr; const auto linearResultTypes = ValueRange(targets).getTypes(); const SmallVector locs(targets.size(), op.getLoc()); @@ -1813,7 +2101,11 @@ struct ConvertSCFYieldOp final : StatefulOpConversionPattern { auto* operation = op.getOperation(); SmallVector targets(op.getResults()); - llvm::append_range(targets, resolveAllValues(state, operation)); + auto qcoTargets = resolveAllValues(state, operation); + if (failed(qcoTargets)) { + return failure(); + } + llvm::append_range(targets, *qcoTargets); if (isa(op->getParentOp())) { rewriter.replaceOpWithNewOp(op, targets); @@ -1849,7 +2141,11 @@ struct ConvertSCFConditionOp final auto* operation = op.getOperation(); SmallVector targets(op.getArgs()); - llvm::append_range(targets, resolveAllValues(state, operation)); + auto qcoTargets = resolveAllValues(state, operation); + if (failed(qcoTargets)) { + return failure(); + } + llvm::append_range(targets, *qcoTargets); rewriter.replaceOpWithNewOp(op, op.getCondition(), targets); @@ -1881,17 +2177,21 @@ struct QCToQCO final : impl::QCToQCOBase { protected: void runOnOperation() override { MLIRContext* context = &getContext(); - auto moduleOp = getOperation(); - - LoweringState preflightState; - if (failed(validateModifierBodies(moduleOp)) || - failed(validateQuantumValueSources(moduleOp)) || - failed(collectRegisterAccesses(moduleOp, preflightState))) { + auto original = getOperation(); + constexpr size_t maxRegionNesting = 64; + if (failed(verifyRegionNestingDepth(original, maxRegionNesting))) { signalPassFailure(); return; } + OwningOpRef converted(original.clone()); + auto moduleOp = *converted; - if (failed(normalizeStaticQubits(moduleOp))) { + LoweringState preflightState; + if (failed(validateAllocationMode(moduleOp, preflightState)) || + failed(validateModifierBodies(moduleOp)) || + failed(validateQuantumValueSources(moduleOp)) || + failed(collectRegisterAccesses(moduleOp, preflightState)) || + failed(normalizeStaticQubits(moduleOp))) { signalPassFailure(); return; } @@ -1903,7 +2203,8 @@ struct QCToQCO final : impl::QCToQCOBase { RewritePatternSet patterns(context); QCToQCOTypeConverter typeConverter(context); - if (failed(collectRegisterAccesses(moduleOp, state))) { + if (failed(validateAllocationMode(moduleOp, state)) || + failed(collectRegisterAccesses(moduleOp, state))) { signalPassFailure(); return; } @@ -2026,7 +2327,14 @@ struct QCToQCO final : impl::QCToQCOBase { if (failed(applyPartialConversion(moduleOp, terminatorTarget, std::move(terminatorPatterns)))) { signalPassFailure(); + return; + } + if (failed(verify(moduleOp)) || failed(qco::verifyLinearity(moduleOp))) { + signalPassFailure(); + return; } + original->setAttrs(moduleOp->getAttrDictionary()); + original.getBodyRegion().takeBody(moduleOp.getBodyRegion()); } }; diff --git a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/CMakeLists.txt b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/CMakeLists.txt index f1d10d5746..1f6dfa4809 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/CMakeLists.txt +++ b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/CMakeLists.txt @@ -21,6 +21,7 @@ add_mlir_conversion_library( MLIRQIRUtils MLIRLLVMDialect MLIRQCDialect + MLIRQCODialect MLIRArithDialect MLIRTransforms MLIRFuncDialect diff --git a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp index c2953e5955..371b277d40 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp @@ -14,11 +14,14 @@ #include "mlir/Dialect/CBit/IR/CBitAttributes.h" #include "mlir/Dialect/CBit/IR/CBitDialect.h" #include "mlir/Dialect/CBit/IR/CBitOps.h" +#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Transforms/GlobalPhaseNormalization.h" #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/IR/QCOps.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" +#include #include #include #include @@ -35,14 +38,19 @@ #include #include #include +#include #include +#include #include #include +#include #include #include +#include #include #include +#include #include #include @@ -297,6 +305,31 @@ struct ConvertMemRefLoadOp final : StatefulOpConversionPattern { } }; +static bool canReleaseInOutputBlock(Operation* release, + const LoweringState& state) { + Block* releaseBlock = release->getBlock(); + SmallVector worklist; + for (Block* successor : releaseBlock->getSuccessors()) { + worklist.push_back(successor); + } + SmallPtrSet visited; + while (!worklist.empty()) { + Block* block = worklist.pop_back_val(); + if (block == releaseBlock) { + return false; + } + if (!visited.insert(block).second) { + continue; + } + for (Block* successor : block->getSuccessors()) { + worklist.push_back(successor); + } + } + + const DominanceInfo dominance(state.outputBlock->getParentOp()); + return dominance.dominates(release, state.outputBlock->getTerminator()); +} + /** * @brief Converts memref.dealloc to QIR qubit-array release * @@ -322,31 +355,30 @@ struct ConvertMemRefDeallocOp final op, "Only one-dimensional registers are supported"); } auto& state = getState(); + auto size = state.qregSizes.lookup(op.getMemref()); + if (!size) { + return rewriter.notifyMatchFailure(op, "unknown qubit register"); + } auto* ctx = getContext(); auto i64Type = rewriter.getI64Type(); auto ptrType = LLVM::LLVMPointerType::get(ctx); - // Save current insertion point - const OpBuilder::InsertionGuard guard(rewriter); - - // Release resources in output block - rewriter.setInsertionPoint(state.outputBlock->getTerminator()); - auto fnSig = LLVM::LLVMFunctionType::get(LLVM::LLVMVoidType::get(ctx), {i64Type, ptrType}); auto fnDec = getOrCreateFunctionDeclaration(rewriter, op, QIR_QUBIT_ARRAY_RELEASE, fnSig); - auto size = state.qregSizes.lookup(op.getMemref()); - if (!size) { - return rewriter.notifyMatchFailure(op, "unknown qubit register"); + if (canReleaseInOutputBlock(op, state)) { + const OpBuilder::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(state.outputBlock->getTerminator()); + LLVM::CallOp::create(rewriter, op.getLoc(), fnDec, + ValueRange{size, adaptor.getMemref()}); + rewriter.eraseOp(op); + } else { + rewriter.replaceOpWithNewOp( + op, fnDec, ValueRange{size, adaptor.getMemref()}); } - // Create the release call - LLVM::CallOp::create(rewriter, op.getLoc(), fnDec, - ValueRange{size, adaptor.getMemref()}); - rewriter.eraseOp(op); - return success(); } }; @@ -410,22 +442,26 @@ struct ConvertQCDeallocOp final : StatefulOpConversionPattern { matchAndRewrite(DeallocOp op, OpAdaptor adaptor, ConversionPatternRewriter& rewriter) const override { auto& state = getState(); + if (state.allocationMode == AllocationMode::Static) { + rewriter.eraseOp(op); + return success(); + } auto* ctx = getContext(); auto ptrType = LLVM::LLVMPointerType::get(ctx); - // Save current insertion point - const OpBuilder::InsertionGuard guard(rewriter); - - // Release resources in output block - rewriter.setInsertionPoint(state.outputBlock->getTerminator()); - auto fnSig = LLVM::LLVMFunctionType::get(LLVM::LLVMVoidType::get(ctx), {ptrType}); auto fnDec = getOrCreateFunctionDeclaration(rewriter, op, QIR_QUBIT_RELEASE, fnSig); - LLVM::CallOp::create(rewriter, op.getLoc(), fnDec, adaptor.getQubit()); - rewriter.eraseOp(op); + if (canReleaseInOutputBlock(op, state)) { + const OpBuilder::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(state.outputBlock->getTerminator()); + LLVM::CallOp::create(rewriter, op.getLoc(), fnDec, adaptor.getQubit()); + rewriter.eraseOp(op); + } else { + rewriter.replaceOpWithNewOp(op, fnDec, adaptor.getQubit()); + } return success(); } @@ -558,13 +594,12 @@ namespace { * * Conversion stages: * 1. Convert scf dialect to cf - * 2. Cpmvert func dialect to LLVM + * 2. Convert func dialect to LLVM * 3. Ensure proper block structure for QIR Adaptive Profile * 4. Add QIR initialization call * 5. Convert QC and memref operations to QIR calls - * 6. Set QIR metadata attributes - * 7. Convert arith and cf dialects to LLVM - * 8. Reconcile unrealized casts + * 6. Convert arith, cf, and math dialects to LLVM + * 7. Reconcile unrealized casts */ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { using QCToQIRAdaptiveBase::QCToQIRAdaptiveBase; @@ -578,8 +613,7 @@ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { * 1. **Entry block**: Contains constant operations and initialization * 2. **Intermediate blocks**: Original function structure containing * quantum operations - * 3. **Output block**: Contains output recording calls and qubit release - * calls + * 3. **Output block**: Contains output recording and result-release calls * * @param main The main LLVM function to restructure * @param state The LoweringState of the conversion pass @@ -587,7 +621,6 @@ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { static void ensureBlocks(LLVM::LLVMFuncOp& main, LoweringState& state) { OpBuilder builder(main.getBody()); auto* firstBlock = &main.front(); - auto* lastBlock = &main.back(); auto* entryBlock = builder.createBlock(&main.getBody()); main.getBlocks().splice(Region::iterator(firstBlock), main.getBlocks(), @@ -599,11 +632,36 @@ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { builder.setInsertionPointToEnd(entryBlock); LLVM::BrOp::create(builder, main->getLoc(), firstBlock); - auto* terminatorOp = lastBlock->getTerminator(); - terminatorOp->moveBefore(outputBlock, outputBlock->end()); - builder.setInsertionPointToEnd(lastBlock); - LLVM::BrOp::create(builder, main->getLoc(), outputBlock); + SmallVector returns; + for (auto& block : main.getBody()) { + if (!block.empty()) { + if (auto returnOp = dyn_cast(block.back())) { + returns.push_back(returnOp); + } + } + } + if (returns.size() == 1) { + auto returnOp = returns.front(); + auto* returnBlock = returnOp->getBlock(); + returnOp->moveBefore(outputBlock, outputBlock->end()); + builder.setInsertionPointToEnd(returnBlock); + LLVM::BrOp::create(builder, main.getLoc(), outputBlock); + } else { + const auto returnType = main.getFunctionType().getReturnType(); + if (!isa(returnType)) { + outputBlock->addArgument(returnType, main.getLoc()); + } + for (auto returnOp : returns) { + builder.setInsertionPoint(returnOp); + LLVM::BrOp::create(builder, returnOp.getLoc(), returnOp.getOperands(), + outputBlock); + returnOp.erase(); + } + builder.setInsertionPointToEnd(outputBlock); + LLVM::ReturnOp::create(builder, main.getLoc(), + outputBlock->getArguments()); + } // Move up all constants to the beginning auto& entryOps = entryBlock->getOperations(); @@ -684,15 +742,26 @@ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { */ void runOnOperation() override { MLIRContext* ctx = &getContext(); - auto moduleOp = getOperation(); + auto original = getOperation(); + constexpr size_t maxRegionNesting = 64; + if (failed(verifyRegionNestingDepth(original, maxRegionNesting))) { + signalPassFailure(); + return; + } + OwningOpRef converted(original.clone()); + auto moduleOp = *converted; + LoweringState state; + if (failed(validateQIRConversionInput( + moduleOp, /*requireSingleBlock=*/false, state))) { + signalPassFailure(); + return; + } if (failed(mqt::normalizeGlobalPhases(moduleOp))) { signalPassFailure(); return; } ConversionTarget target(*ctx); QCToQIRTypeConverter typeConverter(ctx); - LoweringState state; - target.addLegalDialect(); // Stage 1: Convert scf dialect to cf @@ -710,7 +779,7 @@ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { } // Stage 2.0: Prepare classical result registers - if (failed(prepareClassicalResults(moduleOp, state))) { + if (failed(prepareClassicalResults(moduleOp.getOperation(), state))) { signalPassFailure(); return; } @@ -730,7 +799,7 @@ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { auto main = getMainFunction(moduleOp); if (!main) { - moduleOp->emitError("no main function with mqt.entry_point found"); + moduleOp.emitError("no main function with mqt.entry_point found"); signalPassFailure(); return; } @@ -784,7 +853,15 @@ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { passManager.addPass(createReconcileUnrealizedCastsPass()); if (passManager.run(moduleOp).failed()) { signalPassFailure(); + return; + } + if (failed(verify(moduleOp)) || + failed(mqt::verifyProgramMetadata(moduleOp))) { + signalPassFailure(); + return; } + original->setAttrs(moduleOp->getAttrDictionary()); + original.getBodyRegion().takeBody(moduleOp.getBodyRegion()); } }; diff --git a/mlir/lib/Conversion/QCToQIR/QIRBase/CMakeLists.txt b/mlir/lib/Conversion/QCToQIR/QIRBase/CMakeLists.txt index c5afadd429..276188e4de 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRBase/CMakeLists.txt +++ b/mlir/lib/Conversion/QCToQIR/QIRBase/CMakeLists.txt @@ -21,6 +21,7 @@ add_mlir_conversion_library( MLIRMQTTransforms MLIRLLVMDialect MLIRQCDialect + MLIRQCODialect MLIRArithDialect MLIRTransforms MLIRFuncDialect diff --git a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp index 641018356e..0d4a518302 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp @@ -17,6 +17,7 @@ #include "mlir/Dialect/MQT/Transforms/GlobalPhaseNormalization.h" #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/IR/QCOps.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" #include @@ -37,12 +38,16 @@ #include #include #include +#include #include #include #include +#include +#include #include #include #include +#include #include #include @@ -189,10 +194,21 @@ struct ConvertMemRefLoadOp final : StatefulOpConversionPattern { // Switch to entry block rewriter.setInsertionPoint(state.entryBlock->getTerminator()); - auto nqubits = state.staticQubits.size(); - auto qubit = createPointerFromIndex(rewriter, op.getLoc(), - static_cast(nqubits)); - state.staticQubits.try_emplace(static_cast(nqubits), qubit); + const auto index = getConstantIntValue(op.getIndices().front()); + if (!index) { + return rewriter.notifyMatchFailure( + op, "expected a prevalidated direct, constant-index register load"); + } + auto& registerElements = state.staticQubitRegisterElements[op.getMemref()]; + if (const auto it = registerElements.find(*index); + it != registerElements.end()) { + rewriter.replaceOp(op, it->second); + return success(); + } + const auto physicalIndex = state.nextStaticQubitIndex++; + auto qubit = createPointerFromIndex(rewriter, op.getLoc(), physicalIndex); + state.staticQubits.try_emplace(physicalIndex, qubit); + registerElements.try_emplace(*index, qubit); rewriter.replaceOp(op, qubit); return success(); @@ -239,15 +255,13 @@ struct ConvertQCAllocOp final : StatefulOpConversionPattern { matchAndRewrite(AllocOp op, OpAdaptor /*adaptor*/, ConversionPatternRewriter& rewriter) const override { auto& state = getState(); - const OpBuilder::InsertionGuard guard(rewriter); rewriter.setInsertionPoint(state.entryBlock->getTerminator()); - const auto nqubits = state.staticQubits.size(); - auto qubit = createPointerFromIndex(rewriter, op.getLoc(), - static_cast(nqubits)); - state.staticQubits.try_emplace(static_cast(nqubits), qubit); + const auto physicalIndex = state.nextStaticQubitIndex++; + auto qubit = createPointerFromIndex(rewriter, op.getLoc(), physicalIndex); + state.staticQubits.try_emplace(physicalIndex, qubit); rewriter.replaceOp(op, qubit); return success(); @@ -321,6 +335,28 @@ struct ConvertQCMeasureOp final : StatefulOpConversionPattern { return success(); } }; + +/** Convert qc.reset to an irreversible QIR reset call. */ +struct ConvertQCResetOp final : StatefulOpConversionPattern { + using StatefulOpConversionPattern::StatefulOpConversionPattern; + + LogicalResult + matchAndRewrite(ResetOp op, OpAdaptor adaptor, + ConversionPatternRewriter& rewriter) const override { + auto& state = getState(); + auto* ctx = getContext(); + const OpBuilder::InsertionGuard guard(rewriter); + + rewriter.setInsertionPoint(state.measurementsBlock->getTerminator()); + auto fnType = LLVM::LLVMFunctionType::get(LLVM::LLVMVoidType::get(ctx), + LLVM::LLVMPointerType::get(ctx)); + auto fnDecl = + getOrCreateFunctionDeclaration(rewriter, op, QIR_RESET, fnType); + LLVM::CallOp::create(rewriter, op.getLoc(), fnDecl, adaptor.getOperands()); + rewriter.eraseOp(op); + return success(); + } +}; } // namespace /** @@ -333,11 +369,87 @@ static void populateQCToQIRBasePatterns(RewritePatternSet& patterns, populateQCToQIRPatterns(patterns, typeConverter, ctx, state); patterns.add(typeConverter, ctx, &state); + ConvertQCResetOp, ConvertQCDeallocOp>(typeConverter, ctx, + &state); patterns.add(typeConverter, ctx); } namespace { + +/// Returns whether two QC references lower to the same Base Profile qubit. +static bool referencesSameQubit(Value lhs, Value rhs) { + if (lhs == rhs) { + return true; + } + auto lhsStatic = lhs.getDefiningOp(); + auto rhsStatic = rhs.getDefiningOp(); + if (lhsStatic && rhsStatic) { + return lhsStatic.getIndex() == rhsStatic.getIndex(); + } + auto lhsLoad = lhs.getDefiningOp(); + auto rhsLoad = rhs.getDefiningOp(); + if (!lhsLoad || !rhsLoad || lhsLoad.getMemref() != rhsLoad.getMemref()) { + return false; + } + return getConstantIntValue(lhsLoad.getIndices().front()) == + getConstantIntValue(rhsLoad.getIndices().front()); +} + +/// Returns whether a unitary touches a qubit already measured or reset. +static bool touchesIrreversibleQubit(UnitaryOpInterface unitary, + ArrayRef irreversibleQubits) { + for (auto qubit : unitary.getQubits()) { + for (auto irreversibleQubit : irreversibleQubits) { + if (referencesSameQubit(qubit, irreversibleQubit)) { + return true; + } + } + } + return false; +} + +/// Reject input whose operation order cannot be preserved by the Base block +/// split. +static LogicalResult validateBaseOperationOrder(ModuleOp module) { + auto entryPoint = mqt::getEntryPoint(module); + bool sawIrreversibleOperation = false; + SmallVector irreversibleQubits; + for (Operation& operation : entryPoint.getBody().front()) { + if (auto measure = dyn_cast(operation)) { + if (!measure.getResult().use_empty()) { + return measure.emitError( + "QIR Base Profile only supports measurement results returned " + "directly or stored directly in returned CBit registers"); + } + sawIrreversibleOperation = true; + irreversibleQubits.push_back(measure.getQubit()); + continue; + } + if (auto reset = dyn_cast(operation)) { + sawIrreversibleOperation = true; + irreversibleQubits.push_back(reset.getQubit()); + continue; + } + if (!sawIrreversibleOperation || + operation.hasTrait() || + isa(operation) || + (isPure(&operation) && isSpeculatable(&operation))) { + continue; + } + if (auto unitary = dyn_cast(operation); + unitary && !touchesIrreversibleQubit(unitary, irreversibleQubits)) { + continue; + } + operation.emitError( + "QIR Base Profile requires operations with observable effects or " + "non-speculatable behavior to precede all measurements and resets") + << "; offending operation is '" << operation.getName() << "'"; + return failure(); + } + return success(); +} + /** * @brief Pass for converting QC dialect operations to QIR * @@ -456,11 +568,24 @@ struct QCToQIRBase final : impl::QCToQIRBaseBase { */ void runOnOperation() override { MLIRContext* ctx = &getContext(); - auto moduleOp = getOperation(); - auto entryPoint = mqt::getEntryPoint(moduleOp); - if (entryPoint && !entryPoint.getBody().hasOneBlock()) { - entryPoint.emitError( - "QIR Base Profile requires a single-block entry function"); + auto original = getOperation(); + constexpr size_t maxRegionNesting = 64; + if (failed(verifyRegionNestingDepth(original, maxRegionNesting))) { + signalPassFailure(); + return; + } + OwningOpRef converted(original.clone()); + auto moduleOp = *converted; + LoweringState state; + if (failed(validateQIRConversionInput(moduleOp, + /*requireSingleBlock=*/true, + state))) { + signalPassFailure(); + return; + } + // Result preparation removes the supported direct uses of measurements. + if (failed(prepareClassicalResults(moduleOp.getOperation(), state)) || + failed(validateBaseOperationOrder(moduleOp))) { signalPassFailure(); return; } @@ -473,14 +598,6 @@ struct QCToQIRBase final : impl::QCToQIRBaseBase { target.addLegalDialect(); - LoweringState state; - - // Stage 1.0: Prepare classical result registers - if (failed(prepareClassicalResults(moduleOp, state))) { - signalPassFailure(); - return; - } - // Stage 1.1: Convert func dialect to LLVM { RewritePatternSet funcPatterns(ctx); @@ -496,7 +613,7 @@ struct QCToQIRBase final : impl::QCToQIRBaseBase { auto main = getMainFunction(moduleOp); if (!main) { - moduleOp->emitError("no main function with mqt.entry_point found"); + moduleOp.emitError("no main function with mqt.entry_point found"); signalPassFailure(); return; } @@ -549,7 +666,15 @@ struct QCToQIRBase final : impl::QCToQIRBaseBase { passManager.addPass(createReconcileUnrealizedCastsPass()); if (passManager.run(moduleOp).failed()) { signalPassFailure(); + return; + } + if (failed(verify(moduleOp)) || + failed(mqt::verifyProgramMetadata(moduleOp))) { + signalPassFailure(); + return; } + original->setAttrs(moduleOp->getAttrDictionary()); + original.getBodyRegion().takeBody(moduleOp.getBodyRegion()); } }; diff --git a/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp b/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp index bd8dc30da8..215ca1150a 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp @@ -17,6 +17,7 @@ #include "mlir/Dialect/QC/IR/QCOps.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" +#include #include #include #include @@ -34,16 +35,20 @@ #include #include #include +#include #include #include #include +#include #include #include #include +#include #include #include #include +#include #include #include @@ -51,6 +56,22 @@ namespace mlir { using namespace qc; using namespace qir; +template +static void walkOperationsIteratively(Operation* root, Callback&& callback) { + SmallVector worklist{root}; + while (!worklist.empty()) { + Operation* operation = worklist.pop_back_val(); + callback(operation); + for (Region& region : operation->getRegions()) { + for (Block& block : region) { + for (Operation& nested : block) { + worklist.push_back(&nested); + } + } + } + } +} + LogicalResult LoweringState::ensureAllocationMode(AllocationMode requested, Operation* op) { if (allocationMode == AllocationMode::Unset) { @@ -430,27 +451,292 @@ Value getResultPtr(LoweringState& state, Operation* op, return result; } -LogicalResult prepareClassicalResults(Operation* moduleOp, - LoweringState& state) { - bool hasInvalidMemory = false; - SmallVector consumedStores; - moduleOp->walk([&](func::FuncOp funcOp) { - if (!mqt::isEntryPoint(funcOp)) { +LogicalResult validateQIRConversionInput(ModuleOp moduleOp, + bool requireSingleBlock, + LoweringState& state) { + if (failed(mqt::verifyProgramMetadata(moduleOp))) { + return failure(); + } + for (Operation& operation : moduleOp.getBody()->getOperations()) { + const auto symbol = SymbolTable::getSymbolName(&operation); + if (!symbol || !symbol.getValue().starts_with("__quantum__")) { + continue; + } + if (auto function = dyn_cast(operation); + function && function.isExternal()) { + continue; + } + if (auto function = dyn_cast(operation); + function && function.isExternal()) { + continue; + } + return operation.emitError() + << "QIR conversion reserves runtime symbol " << symbol.getValue() + << " for a function declaration"; + } + func::FuncOp entryPoint; + for (auto function : moduleOp.getOps()) { + if (!mqt::isEntryPoint(function)) { + continue; + } + if (entryPoint) { + moduleOp.emitError("QIR conversion requires exactly one program entry " + "function marked with mqt.entry_point"); + return failure(); + } + entryPoint = function; + } + if (!entryPoint) { + moduleOp.emitError( + "QIR conversion requires a program entry function marked with " + "mqt.entry_point"); + return failure(); + } + if (entryPoint.isExternal()) { + entryPoint.emitError("QIR conversion requires a defined entry function"); + return failure(); + } + if (entryPoint.getNumArguments() != 0) { + entryPoint.emitError( + "QIR conversion does not support entry-function arguments"); + return failure(); + } + if (requireSingleBlock && !entryPoint.getBody().hasOneBlock()) { + entryPoint.emitError( + "QIR Base Profile conversion requires a single-block entry function"); + return failure(); + } + if (requireSingleBlock && + !isa(entryPoint.getBody().front().getTerminator())) { + entryPoint.emitError( + "QIR Base Profile conversion requires straight-line control flow " + "ending in func.return"); + return failure(); + } + + bool invalid = false; + walkOperationsIteratively(moduleOp, [&](Operation* operation) { + if (invalid || operation == moduleOp || operation == entryPoint) { + return; + } + const auto dialect = operation->getName().getDialectNamespace(); + if (dialect != qc::QCDialect::getDialectNamespace() && + dialect != cbit::CBitDialect::getDialectNamespace() && + dialect != memref::MemRefDialect::getDialectNamespace()) { return; } + if (operation->getParentOfType() != entryPoint) { + operation->emitError( + "QIR conversion only supports QC, CBit, and MemRef operations in " + "the program entry function"); + invalid = true; + } + }); + if (invalid) { + return failure(); + } + + if (requireSingleBlock) { + walkOperationsIteratively(moduleOp, [&](Operation* operation) { + if (invalid || operation == moduleOp || isa(operation) || + !isa(operation)) { + return; + } + operation->emitError( + "QIR Base Profile conversion does not support region-based control " + "flow other than qc.ctrl"); + invalid = true; + }); + if (invalid) { + return failure(); + } - funcOp.walk([&](memref::AllocOp allocOp) { - const auto type = allocOp.getType(); + walkOperationsIteratively(entryPoint, [&](Operation* operation) { + if (invalid || operation == entryPoint) { + return; + } + const auto dialect = operation->getName().getDialectNamespace(); + if (dialect != qc::QCDialect::getDialectNamespace() && + dialect != cbit::CBitDialect::getDialectNamespace() && + dialect != memref::MemRefDialect::getDialectNamespace()) { + return; + } + for (Operation* parent = operation->getParentOp(); + parent && parent != entryPoint; parent = parent->getParentOp()) { + if (parent->getNumRegions() == 0 || isa(parent)) { + continue; + } + operation->emitError( + "QIR Base Profile conversion does not support QC, CBit, or " + "MemRef operations nested in preserved region operations"); + invalid = true; + return; + } + }); + if (invalid) { + return failure(); + } + } + + walkOperationsIteratively(entryPoint, [&](Operation* operation) { + if (auto op = dyn_cast(operation)) { + const auto type = op.getType(); if (type.getRank() != 1 || !isa(type.getElementType())) { - allocOp.emitError( - "QIR conversion only supports generic memrefs for " - "one-dimensional qc.qubit registers; use CBit for classical " - "registers"); - hasInvalidMemory = true; + op.emitError("QIR conversion only supports generic memrefs for " + "one-dimensional qc.qubit registers; use CBit for " + "classical registers"); + invalid = true; + } + } + if (auto op = dyn_cast(operation)) { + const auto type = cast(op.getMemref().getType()); + if (type.getRank() != 1 || op.getIndices().size() != 1 || + !isa(type.getElementType())) { + op.emitError("QIR conversion only supports one-dimensional qubit " + "register loads with exactly one index"); + invalid = true; + } + } + }); + if (invalid) { + return failure(); + } + + if (!requireSingleBlock) { + Operation* staticAllocation = nullptr; + Operation* dynamicAllocation = nullptr; + walkOperationsIteratively(entryPoint, [&](Operation* operation) { + if (isa(operation)) { + staticAllocation = operation; + } else if (isa(operation)) { + dynamicAllocation = operation; } }); + if (staticAllocation && dynamicAllocation) { + dynamicAllocation->emitError( + "QIR Adaptive Profile conversion cannot mix static and dynamic " + "qubit allocations"); + return failure(); + } + return success(); + } + + SmallVector> loadedRegisterElements; + uint64_t freshStaticQubitIds = 0; + walkOperationsIteratively(entryPoint, [&](Operation* operation) { + if (auto op = dyn_cast(operation)) { + const auto rawIndex = op.getIndex(); + if (rawIndex >= + static_cast(std::numeric_limits::max())) { + op.emitError("static qubit index exceeds the supported QIR range"); + invalid = true; + return; + } + const auto index = static_cast(rawIndex); + state.nextStaticQubitIndex = + std::max(state.nextStaticQubitIndex, index + 1); + return; + } + if (auto op = dyn_cast(operation)) { + ++freshStaticQubitIds; + return; + } + if (auto op = dyn_cast(operation)) { + const auto type = op.getType(); + if (type.isDynamicDim(0)) { + op.emitError("QIR Base Profile conversion requires statically sized " + "one-dimensional qc.qubit memrefs"); + invalid = true; + } + return; + } + auto op = dyn_cast(operation); + if (!op) { + return; + } + auto allocation = op.getMemref().getDefiningOp(); + const auto index = getConstantIntValue(op.getIndices().front()); + if (!allocation || !index || *index < 0 || + *index >= allocation.getType().getDimSize(0)) { + op.emitError("QIR Base Profile conversion requires a constant, " + "in-bounds index into a direct qubit-register allocation"); + invalid = true; + return; + } + const std::pair element{op.getMemref(), *index}; + if (!llvm::is_contained(loadedRegisterElements, element)) { + loadedRegisterElements.push_back(element); + ++freshStaticQubitIds; + } + }); + if (invalid) { + return failure(); + } + const auto availableStaticQubitIds = static_cast( + std::numeric_limits::max() - state.nextStaticQubitIndex); + if (freshStaticQubitIds > availableStaticQubitIds) { + entryPoint.emitError( + "QIR Base Profile conversion exhausts the supported static qubit " + "index range"); + return failure(); + } + return success(); +} + +LogicalResult prepareClassicalResults(Operation* moduleOp, + LoweringState& state) { + constexpr uint64_t maxClassicalResultSlots = 1U << 20; + uint64_t numClassicalResultSlots = 0; + bool exceedsResultLimit = false; + walkOperationsIteratively(moduleOp, [&](Operation* operation) { + auto allocOp = dyn_cast(operation); + if (!allocOp || exceedsResultLimit) { + return; + } + const auto width = allocOp.getResult().getType().getWidth(); + if (width <= 0 || static_cast(width) > + maxClassicalResultSlots - numClassicalResultSlots) { + allocOp.emitError() << "QIR conversion supports at most " + << maxClassicalResultSlots + << " classical result slots per module"; + exceedsResultLimit = true; + return; + } + numClassicalResultSlots += static_cast(width); + }); + if (exceedsResultLimit) { + return failure(); + } + + struct ReturnRewrite { + func::FuncOp function; + func::ReturnOp returnOp; + SmallVector keptOperands; + SmallVector keptTypes; + bool needsStatusResult = false; + bool recordsClassicalOutput = false; + }; - funcOp.walk([&](cbit::AllocOp allocOp) { + bool hasInvalidMemory = false; + SmallVector consumedStores; + SmallVector returnRewrites; + SmallVector entryPoints; + walkOperationsIteratively(moduleOp, [&](Operation* operation) { + if (auto function = dyn_cast(operation); + function && mqt::isEntryPoint(function)) { + entryPoints.push_back(function); + } + }); + for (auto funcOp : entryPoints) { + + walkOperationsIteratively(funcOp, [&](Operation* operation) { + if (operation->getParentOfType() != funcOp) { + return; + } + auto allocOp = dyn_cast(operation); + if (!allocOp) { + return; + } const auto [it, inserted] = state.cregIndices.try_emplace( allocOp.getOperation(), state.cregs.size()); if (inserted) { @@ -467,7 +753,7 @@ LogicalResult prepareClassicalResults(Operation* moduleOp, reg.results.assign(static_cast(size), Value{}); }); - const auto markRegisterForRecording = [&](const size_t registerIndex) { + const auto markRegisterForRecording = [&](size_t registerIndex) { auto& reg = state.cregs[registerIndex]; if (reg.record) { return; @@ -479,40 +765,58 @@ LogicalResult prepareClassicalResults(Operation* moduleOp, state.returnedCregs.push_back(registerIndex); }; - funcOp.walk([&](func::ReturnOp returnOp) { - SmallVector keptOperands; - SmallVector keptReturnTypes; + walkOperationsIteratively(funcOp, [&](Operation* operation) { + if (operation->getParentOfType() != funcOp) { + return; + } + auto returnOp = dyn_cast(operation); + if (!returnOp) { + return; + } + ReturnRewrite rewrite{.function = funcOp, .returnOp = returnOp}; for (auto operand : returnOp.getOperands()) { if (auto measureOp = operand.getDefiningOp()) { state.returnedStaticResults.insert(measureOp.getOperation()); + rewrite.recordsClassicalOutput = true; } else if (auto allocOp = operand.getDefiningOp(); allocOp && state.cregIndices.contains(allocOp.getOperation())) { markRegisterForRecording( state.cregIndices.at(allocOp.getOperation())); + rewrite.recordsClassicalOutput = true; } else { - keptOperands.push_back(operand); - keptReturnTypes.push_back(operand.getType()); + rewrite.keptOperands.push_back(operand); + rewrite.keptTypes.push_back(operand.getType()); } } - if (keptOperands.empty() && !returnOp.getOperands().empty()) { - OpBuilder builder(returnOp); - auto zero = - arith::ConstantIntOp::create(builder, returnOp.getLoc(), 0, 64); - keptOperands.push_back(zero); - keptReturnTypes.push_back(zero.getType()); - } - - returnOp.getOperandsMutable().assign(keptOperands); - - funcOp.setFunctionType(FunctionType::get( - funcOp.getContext(), funcOp.getFunctionType().getInputs(), - keptReturnTypes)); + rewrite.needsStatusResult = + rewrite.keptOperands.empty() && !returnOp.getOperands().empty(); + returnRewrites.push_back(std::move(rewrite)); }); - funcOp.walk([&](cbit::StoreOp storeOp) { + if (returnRewrites.size() > 1) { + auto recordedReturn = + llvm::find_if(returnRewrites, [](const ReturnRewrite& rewrite) { + return rewrite.recordsClassicalOutput; + }); + if (recordedReturn != returnRewrites.end()) { + recordedReturn->returnOp.emitError( + "QIR conversion requires a single entry-function return when " + "recording classical outputs"); + return failure(); + } + } + + walkOperationsIteratively(funcOp, [&](Operation* operation) { + if (operation->getParentOfType() != funcOp) { + return; + } + auto storeOp = dyn_cast(operation); + if (!storeOp) { + return; + } auto allocOp = storeOp.getReg().getDefiningOp(); if (!allocOp || !state.cregIndices.contains(allocOp.getOperation())) { storeOp.emitError( @@ -544,10 +848,39 @@ LogicalResult prepareClassicalResults(Operation* moduleOp, } consumedStores.push_back(storeOp); }); - }); + } if (hasInvalidMemory) { return failure(); } + + DenseMap> loweredReturnTypes; + for (auto& rewrite : returnRewrites) { + if (rewrite.needsStatusResult) { + rewrite.keptTypes.push_back( + IntegerType::get(rewrite.function.getContext(), 64)); + } + const auto [it, inserted] = loweredReturnTypes.try_emplace( + rewrite.function.getOperation(), rewrite.keptTypes); + if (!inserted && it->second != rewrite.keptTypes) { + rewrite.returnOp.emitError( + "QIR conversion requires every entry-function return to have the " + "same lowered result types"); + return failure(); + } + } + + for (auto& rewrite : returnRewrites) { + if (rewrite.needsStatusResult) { + OpBuilder builder(rewrite.returnOp); + auto zero = arith::ConstantIntOp::create( + builder, rewrite.returnOp.getLoc(), 0, 64); + rewrite.keptOperands.push_back(zero); + } + rewrite.returnOp.getOperandsMutable().assign(rewrite.keptOperands); + rewrite.function.setFunctionType(FunctionType::get( + rewrite.function.getContext(), + rewrite.function.getFunctionType().getInputs(), rewrite.keptTypes)); + } for (auto storeOp : consumedStores) { storeOp.erase(); } diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index 756e204499..08dd046671 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -15,6 +15,9 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" +#include +#include +#include #include #include #include @@ -37,8 +40,8 @@ using namespace mlir::mqt; void MQTDialect::initialize() {} -[[nodiscard]] static LogicalResult -verifyEntryPoint(Operation* operation, const NamedAttribute attribute) { +[[nodiscard]] static LogicalResult verifyEntryPoint(Operation* operation, + NamedAttribute attribute) { if (!isa(attribute.getValue())) { return operation->emitError() << "attribute '" << attribute.getName().getValue() @@ -55,17 +58,11 @@ verifyEntryPoint(Operation* operation, const NamedAttribute attribute) { << "' requires a defined module-level function"; } - for (Operation& candidate : moduleOp.getBody()->getOperations()) { - if (&candidate != operation && isEntryPoint(&candidate)) { - return operation->emitError() - << "module must contain at most one program entry point"; - } - } return success(); } [[nodiscard]] static LogicalResult verifyName(Operation* operation, - const NamedAttribute attribute) { + NamedAttribute attribute) { const auto name = dyn_cast(attribute.getValue()); if (!name) { return operation->emitError() @@ -85,8 +82,8 @@ verifyEntryPoint(Operation* operation, const NamedAttribute attribute) { return success(); } -[[nodiscard]] static LogicalResult -verifyParameterGroup(Operation* operation, const Attribute attribute) { +[[nodiscard]] static LogicalResult verifyParameterGroup(Operation* operation, + Attribute attribute) { const auto group = dyn_cast(attribute); const auto identity = group ? group.getAs("identity") : nullptr; const auto groupName = group ? group.getAs("name") : nullptr; @@ -115,7 +112,7 @@ verifyParameterGroup(Operation* operation, const Attribute attribute) { [[nodiscard]] static LogicalResult verifyInputGroup(FunctionOpInterface function, Operation* operation, - const unsigned argIndex, const Attribute attribute) { + unsigned argIndex, Attribute attribute) { const auto inputName = function.getArgAttrOfType( argIndex, MQTDialect::InputNameAttrHelper::getNameStr()); if (!inputName) { @@ -155,7 +152,7 @@ verifyInputGroup(FunctionOpInterface function, Operation* operation, } [[nodiscard]] static LogicalResult -verifyRegisterName(Operation* operation, const NamedAttribute attribute) { +verifyRegisterName(Operation* operation, NamedAttribute attribute) { if (failed(verifyName(operation, attribute))) { return failure(); } @@ -173,30 +170,11 @@ verifyRegisterName(Operation* operation, const NamedAttribute attribute) { << "' requires an allocation in a function entry block"; } - const auto name = cast(attribute.getValue()); - for (unsigned index = 0; index < function.getNumArguments(); ++index) { - if (function.getArgAttrOfType( - index, MQTDialect::InputNameAttrHelper::getNameStr()) == name) { - return operation->emitError() - << "duplicate program name '" << name.getValue() << "'"; - } - } - for (Operation& candidate : function.getFunctionBody().front()) { - if (&candidate == operation) { - continue; - } - if (candidate.getAttrOfType( - MQTDialect::RegisterNameAttrHelper::getNameStr()) == name) { - return operation->emitError() - << "duplicate program name '" << name.getValue() << "'"; - } - } return success(); } -LogicalResult -MQTDialect::verifyOperationAttribute(Operation* operation, - const NamedAttribute attribute) { +LogicalResult MQTDialect::verifyOperationAttribute(Operation* operation, + NamedAttribute attribute) { if (attribute.getName() == EntryPointAttrHelper::getNameStr()) { return verifyEntryPoint(operation, attribute); } @@ -220,9 +198,10 @@ MQTDialect::verifyOperationAttribute(Operation* operation, << "unknown MQT attribute '" << attribute.getName().getValue() << "'"; } -LogicalResult MQTDialect::verifyRegionArgAttribute( - Operation* operation, const unsigned regionIndex, const unsigned argIndex, - const NamedAttribute attribute) { +LogicalResult MQTDialect::verifyRegionArgAttribute(Operation* operation, + unsigned regionIndex, + unsigned argIndex, + NamedAttribute attribute) { const auto attributeName = attribute.getName(); if (attributeName != InputNameAttrHelper::getNameStr() && attributeName != ParameterGroupAttrHelper::getNameStr()) { @@ -245,36 +224,97 @@ LogicalResult MQTDialect::verifyRegionArgAttribute( if (failed(verifyName(operation, attribute))) { return failure(); } + return success(); +} + +LogicalResult MQTDialect::verifyRegionResultAttribute( + Operation* operation, unsigned /*regionIndex*/, unsigned /*resultIndex*/, + NamedAttribute attribute) { + return operation->emitError() + << "attribute '" << attribute.getName().getValue() + << "' is not valid on a region result"; +} - const auto name = cast(attribute.getValue()); +[[nodiscard]] static LogicalResult +verifyProgramNames(FunctionOpInterface function) { + llvm::StringSet<> names; + DenseMap> groupDescriptions; for (unsigned index = 0; index < function.getNumArguments(); ++index) { - if (index == argIndex) { + const auto name = function.getArgAttrOfType( + index, MQTDialect::InputNameAttrHelper::getNameStr()); + if (name && !names.insert(name.getValue()).second) { + return function.emitError() + << "duplicate program name '" << name.getValue() << "'"; + } + const auto group = function.getArgAttrOfType( + index, MQTDialect::ParameterGroupAttrHelper::getNameStr()); + if (!group) { continue; } - if (function.getArgAttrOfType(index, attribute.getName()) == - name) { - return operation->emitError() - << "duplicate program name '" << name.getValue() << "'"; + if (failed(verifyInputGroup(function, function.getOperation(), index, + group))) { + return failure(); + } + const auto identity = group.getAs("identity"); + const auto groupName = group.getAs("name"); + const auto groupSize = group.getAs("size").getInt(); + const auto [description, inserted] = groupDescriptions.try_emplace( + identity, std::pair{groupName, groupSize}); + if (!inserted && description->second != + std::pair{groupName, groupSize}) { + return function.emitError() + << "parameter-group identity '" << identity.getValue() + << "' has inconsistent name or size"; } } - if (!function.getFunctionBody().empty()) { - for (Operation& candidate : function.getFunctionBody().front()) { - if (candidate.getAttrOfType( - RegisterNameAttrHelper::getNameStr()) == name) { - return operation->emitError() - << "duplicate program name '" << name.getValue() << "'"; - } + if (function.getFunctionBody().empty()) { + return success(); + } + for (Operation& operation : function.getFunctionBody().front()) { + const auto name = operation.getAttrOfType( + MQTDialect::RegisterNameAttrHelper::getNameStr()); + if (name && !names.insert(name.getValue()).second) { + return operation.emitError() + << "duplicate program name '" << name.getValue() << "'"; } } return success(); } -LogicalResult MQTDialect::verifyRegionResultAttribute( - Operation* operation, unsigned /*regionIndex*/, unsigned /*resultIndex*/, - const NamedAttribute attribute) { - return operation->emitError() - << "attribute '" << attribute.getName().getValue() - << "' is not valid on a region result"; +LogicalResult mlir::mqt::verifyProgramMetadata(ModuleOp moduleOp) { + Operation* entryPoint = nullptr; + SmallVector worklist; + for (Operation& operation : moduleOp.getBody()->getOperations()) { + worklist.push_back(&operation); + } + while (!worklist.empty()) { + Operation* operation = worklist.pop_back_val(); + if (isEntryPoint(operation)) { + auto function = dyn_cast(operation); + if (!function || operation->getParentOp() != moduleOp.getOperation() || + function.getFunctionBody().empty()) { + return operation->emitError( + "program entry point must be a defined module-level function"); + } + if (entryPoint != nullptr) { + return operation->emitError( + "module must contain at most one program entry point"); + } + entryPoint = operation; + } + if (auto function = dyn_cast(operation); + function && failed(verifyProgramNames(function))) { + return failure(); + } + for (Region& region : operation->getRegions()) { + for (Block& block : region) { + for (Operation& nested : block) { + worklist.push_back(&nested); + } + } + } + } + return success(); } void mlir::mqt::setEntryPoint(Operation* operation) { diff --git a/mlir/lib/Dialect/MQT/Transforms/NormalizeGlobalPhases.cpp b/mlir/lib/Dialect/MQT/Transforms/NormalizeGlobalPhases.cpp index ac28e2d7d9..21401288b0 100644 --- a/mlir/lib/Dialect/MQT/Transforms/NormalizeGlobalPhases.cpp +++ b/mlir/lib/Dialect/MQT/Transforms/NormalizeGlobalPhases.cpp @@ -8,6 +8,7 @@ * Licensed under the MIT License */ +#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Transforms/GlobalPhaseNormalization.h" #include "mlir/Dialect/MQT/Transforms/Passes.h" #include "mlir/Dialect/MQT/Utils/Angles.h" @@ -16,7 +17,9 @@ #include "mlir/Dialect/MQT/Utils/Parameters.h" #include "mlir/Dialect/QC/IR/QCOps.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/QCOUtils.h" +#include #include #include #include @@ -33,7 +36,9 @@ #include #include +#include #include +#include #include #include #include @@ -75,6 +80,8 @@ class PhaseExpression final { return constant && *constant == 0.0; } + [[nodiscard]] bool isConstant() const { return getConstant().has_value(); } + void add(PhaseExpression&& other) { if (isZero()) { *this = std::move(other); @@ -117,7 +124,7 @@ class PhaseExpression final { return; } if (const auto constant = getConstant()) { - instructions.front() = normalizeAngle(*constant * factor); + instructions.front() = scaleAngleByInteger(*constant, factor); return; } instructions.emplace_back(Scale{factor}); @@ -138,26 +145,29 @@ class PhaseExpression final { continue; } if (const auto* value = std::get_if(&instruction)) { - stack.push_back(*value); + stack.push_back(normalizeAngle(rewriter, loc, *value)); continue; } if (std::holds_alternative(instruction)) { assert(stack.size() >= 2); auto rhs = stack.pop_back_val(); auto lhs = stack.pop_back_val(); - stack.push_back(rewriter.createOrFold(loc, lhs, rhs)); + auto sum = rewriter.createOrFold(loc, lhs, rhs); + stack.push_back(normalizeAngle(rewriter, loc, sum)); continue; } assert(!stack.empty()); auto operand = stack.pop_back_val(); if (std::holds_alternative(instruction)) { - stack.push_back(rewriter.createOrFold(loc, operand)); + auto negated = rewriter.createOrFold(loc, operand); + stack.push_back(normalizeAngle(rewriter, loc, negated)); continue; } const auto factor = std::get(instruction).factor; auto factorValue = constantFromScalar(rewriter, loc, factor); - stack.push_back( - rewriter.createOrFold(loc, factorValue, operand)); + auto scaled = + rewriter.createOrFold(loc, factorValue, operand); + stack.push_back(normalizeAngle(rewriter, loc, scaled)); } assert(stack.size() == 1); Value result = stack.front(); @@ -166,7 +176,7 @@ class PhaseExpression final { if (const auto constant = valueToConstantDouble(result)) { return constantFromScalar(rewriter, loc, normalizeAngle(*constant)); } - return result; + return normalizeAngle(rewriter, loc, result); } private: @@ -189,62 +199,63 @@ struct PhaseContribution final { PhaseDialect dialect; Location loc; PhaseExpression expression; - - void add(PhaseContribution other) { - assert(dialect == other.dialect && - "QC and QCO operations cannot occur in the same program"); - expression.add(std::move(other.expression)); - } }; -} // namespace +using PhaseContributions = std::array, 2>; -/// Collect a pure, body-local dependency slice in topological order. -static bool collectHoistableSlice(Value value, Block& body, - SmallPtrSetImpl& visiting, - SmallPtrSetImpl& collected, - SmallVectorImpl& ordered) { - if (auto blockArg = dyn_cast(value)) { - return blockArg.getOwner() != &body; - } +[[nodiscard]] static constexpr std::size_t +getDialectIndex(PhaseDialect dialect) { + return static_cast(dialect); +} - auto* definingOp = value.getDefiningOp(); - if (definingOp == nullptr || definingOp->getBlock() != &body) { - return true; - } - if (collected.contains(definingOp)) { - return true; - } - if (!visiting.insert(definingOp).second || definingOp->getNumRegions() != 0 || - !isPure(definingOp) || !isSpeculatable(definingOp)) { - return false; - } - for (auto operand : definingOp->getOperands()) { - if (!collectHoistableSlice(operand, body, visiting, collected, ordered)) { - return false; - } +static void addContribution(PhaseContributions& contributions, + PhaseContribution contribution) { + auto& aggregate = contributions[getDialectIndex(contribution.dialect)]; + if (aggregate) { + aggregate->expression.add(std::move(contribution.expression)); + return; } - visiting.erase(definingOp); - collected.insert(definingOp); - ordered.push_back(definingOp); - return true; + aggregate = std::move(contribution); } +} // namespace + /// Make all dynamic leaves of @p expression available before @p modifier. static bool hoistExpressionBefore(const PhaseExpression& expression, Block& body, Operation* modifier, RewriterBase& rewriter) { - SmallPtrSet visiting; SmallPtrSet collected; + SmallVector worklist; SmallVector ordered; bool hoistable = true; - expression.forEachValue([&](Value value) { - if (hoistable && - !collectHoistableSlice(value, body, visiting, collected, ordered)) { + expression.forEachValue([&](Value value) { worklist.push_back(value); }); + while (hoistable && !worklist.empty()) { + auto value = worklist.pop_back_val(); + if (auto blockArg = dyn_cast(value)) { + if (blockArg.getOwner() == &body) { + hoistable = false; + } + continue; + } + + auto* definingOp = value.getDefiningOp(); + if (definingOp == nullptr || definingOp->getBlock() != &body || + !collected.insert(definingOp).second) { + continue; + } + if (definingOp->getNumRegions() != 0 || !isPure(definingOp) || + !isSpeculatable(definingOp)) { hoistable = false; + continue; } - }); + llvm::append_range(worklist, definingOp->getOperands()); + } if (hoistable) { + for (auto& op : body) { + if (collected.contains(&op)) { + ordered.push_back(&op); + } + } for (auto* op : ordered) { rewriter.moveOpBefore(op, modifier); } @@ -258,107 +269,114 @@ class GlobalPhaseNormalizer final { public: explicit GlobalPhaseNormalizer(MLIRContext* context) : rewriter(context) {} - void normalize(Region& region) { normalizeRegion(region); } + void normalize(Region& root) { + struct RegionWorkItem { + Region* region; + Operation* extractionBoundary; + }; + struct BlockWorkItem { + Block* block; + Operation* extractionBoundary; + }; + + SmallVector regionWorklist{{&root, nullptr}}; + SmallVector blocks; + while (!regionWorklist.empty()) { + auto [region, extractionBoundary] = regionWorklist.pop_back_val(); + for (auto& block : *region) { + blocks.push_back({&block, extractionBoundary}); + for (auto& op : block) { + auto* nestedBoundary = getExtractionBoundary(&op); + for (auto& nested : op.getRegions()) { + regionWorklist.push_back({&nested, nestedBoundary}); + } + } + } + } -private: - [[nodiscard]] std::optional - normalizeOperation(Operation* op) { - if (auto inv = dyn_cast(op)) { - return factorInverse(inv); + for (auto [block, extractionBoundary] : llvm::reverse(blocks)) { + auto contributions = normalizeBlock(*block, extractionBoundary); + if (extractionBoundary != nullptr) { + applyExtractionBoundary(extractionBoundary, std::move(contributions)); + } } - if (auto inv = dyn_cast(op)) { - return factorInverse(inv); + } + +private: + [[nodiscard]] static Operation* getExtractionBoundary(Operation* op) { + if (isa(op)) { + return op; } if (auto pow = dyn_cast(op)) { - return factorPower(pow); + const auto exponent = pow.getExponentValue(); + return exponent && isIntegerExponent(*exponent) ? op : nullptr; } if (auto pow = dyn_cast(op)) { - return factorPower(pow); + const auto exponent = pow.getExponentValue(); + return exponent && isIntegerExponent(*exponent) ? op : nullptr; } - if (auto ctrl = dyn_cast(op)) { - return factorControl(ctrl); - } - if (auto ctrl = dyn_cast(op)) { - return factorControl(ctrl); - } - for (auto& nested : op->getRegions()) { - normalizeRegion(nested); - } - return std::nullopt; + return nullptr; } - template - [[nodiscard]] std::optional factorInverse(InvOp op) { - auto phase = normalizeBlock(*op.getBody(), op); - if (phase) { - phase->expression.negate(); + static bool canExtract(Operation* boundary, PhaseDialect dialect) { + if (isa(boundary)) { + return dialect == PhaseDialect::QC; + } + if (isa(boundary)) { + return dialect == PhaseDialect::QCO; } - return phase; + return true; } - template - [[nodiscard]] std::optional factorPower(PowOp op) { - const auto exponent = op.getExponentValue(); - if (!exponent || !isIntegerExponent(*exponent)) { - normalizeRegion(op->getRegion(0)); - return std::nullopt; - } - auto phase = normalizeBlock(*op.getBody(), op); - if (phase) { - phase->expression.scale(*exponent); + static bool canExtractExpression(Operation* boundary, + const PhaseExpression& expression) { + std::optional exponent; + if (auto pow = dyn_cast(boundary)) { + exponent = pow.getExponentValue(); + } else if (auto pow = dyn_cast(boundary)) { + exponent = pow.getExponentValue(); } - return phase; + return !exponent || expression.isConstant() || std::abs(*exponent) <= 1.0; } - [[nodiscard]] std::optional factorControl(qc::CtrlOp op) { - auto phase = normalizeBlock(*op.getBody(), op); - if (!phase || op.getNumControls() == 0) { - return phase; - } - if (phase->expression.isZero()) { - return std::nullopt; + void factorControl(qc::CtrlOp op, PhaseContribution phase) { + if (phase.expression.isZero()) { + return; } - rewriter.setInsertionPoint(op); - auto angle = phase->expression.materialize(rewriter, phase->loc); + auto angle = phase.expression.materialize(rewriter, phase.loc); rewriter.setInsertionPointAfter(op); if (op.getNumControls() == 1) { - qc::POp::create(rewriter, phase->loc, op.getControl(0), angle); - return std::nullopt; + qc::POp::create(rewriter, phase.loc, op.getControl(0), angle); + return; } auto controls = op.getControls(); - qc::CtrlOp::create(rewriter, phase->loc, controls.drop_back(), + qc::CtrlOp::create(rewriter, phase.loc, controls.drop_back(), controls.back(), [&](Value target) { - qc::POp::create(rewriter, phase->loc, target, angle); + qc::POp::create(rewriter, phase.loc, target, angle); }); - return std::nullopt; } - [[nodiscard]] std::optional factorControl(qco::CtrlOp op) { - auto phase = normalizeBlock(*op.getBody(), op); - if (!phase || op.getNumControls() == 0) { - return phase; - } - if (phase->expression.isZero()) { - return std::nullopt; + void factorControl(qco::CtrlOp op, PhaseContribution phase) { + if (phase.expression.isZero()) { + return; } - rewriter.setInsertionPoint(op); - auto angle = phase->expression.materialize(rewriter, phase->loc); + auto angle = phase.expression.materialize(rewriter, phase.loc); rewriter.setInsertionPointAfter(op); SmallVector oldControls(op.getOutputControls()); SmallVector newControls; Operation* relativePhase = nullptr; if (op.getNumControls() == 1) { auto p = - qco::POp::create(rewriter, phase->loc, oldControls.front(), angle); + qco::POp::create(rewriter, phase.loc, oldControls.front(), angle); newControls.push_back(p.getOutputTarget(0)); relativePhase = p; } else { auto relative = qco::CtrlOp::create( - rewriter, phase->loc, ValueRange(oldControls).drop_back(), + rewriter, phase.loc, ValueRange(oldControls).drop_back(), oldControls.back(), [&](Value target) { - return qco::POp::create(rewriter, phase->loc, target, angle) + return qco::POp::create(rewriter, phase.loc, target, angle) .getOutputTarget(0); }); llvm::append_range(newControls, relative.getOutputQubits()); @@ -369,88 +387,168 @@ class GlobalPhaseNormalizer final { llvm::zip_equal(oldControls, newControls)) { rewriter.replaceAllUsesExcept(oldControl, newControl, relativePhase); } - return std::nullopt; } - void normalizeRegion(Region& region) { - for (auto& block : region) { - static_cast(normalizeBlock(block, nullptr)); + void recordContributions(Operation* op, PhaseContributions contributions) { + auto& recorded = contributionsByOperation[op]; + for (auto& contribution : contributions) { + if (contribution) { + addContribution(recorded, std::move(*contribution)); + } + } + } + + void applyExtractionBoundary(Operation* op, + PhaseContributions contributions) { + if (isa(op)) { + for (auto& contribution : contributions) { + if (contribution) { + contribution->expression.negate(); + } + } + recordContributions(op, std::move(contributions)); + return; + } + + std::optional exponent; + if (auto pow = dyn_cast(op)) { + exponent = pow.getExponentValue(); + } else if (auto pow = dyn_cast(op)) { + exponent = pow.getExponentValue(); + } + if (exponent) { + for (auto& contribution : contributions) { + if (contribution) { + contribution->expression.scale(*exponent); + } + } + recordContributions(op, std::move(contributions)); + return; + } + + if (auto ctrl = dyn_cast(op)) { + if (ctrl.getNumControls() == 0) { + recordContributions(op, std::move(contributions)); + return; + } + auto& phase = contributions[getDialectIndex(PhaseDialect::QC)]; + if (phase) { + factorControl(ctrl, std::move(*phase)); + phase.reset(); + } + } else if (auto ctrl = dyn_cast(op)) { + if (ctrl.getNumControls() == 0) { + recordContributions(op, std::move(contributions)); + return; + } + auto& phase = contributions[getDialectIndex(PhaseDialect::QCO)]; + if (phase) { + factorControl(ctrl, std::move(*phase)); + phase.reset(); + } + } + recordContributions(op, std::move(contributions)); + } + + [[nodiscard]] static bool isAtBlockExit(Operation* phase, + Operation* terminator) { + for (auto* next = phase->getNextNode(); next != terminator; + next = next->getNextNode()) { + if (next == nullptr || !isa(next)) { + return false; + } } + return true; } - [[nodiscard]] std::optional + [[nodiscard]] PhaseContributions normalizeBlock(Block& block, Operation* extractionBoundary) { - std::optional aggregate; - SmallVector directPhases; - bool hasNestedContribution = false; + PhaseContributions aggregates; + PhaseContributions extracted; + std::array, 2> directPhases; + std::array hasNestedContribution{}; + Operation* terminator = + block.mightHaveTerminator() ? block.getTerminator() : nullptr; for (auto& op : llvm::make_early_inc_range(block.without_terminator())) { - std::optional phase; if (auto gphase = dyn_cast(&op)) { - phase.emplace(PhaseDialect::QC, gphase.getLoc(), - PhaseExpression(gphase.getTheta())); - directPhases.push_back(gphase); + addContribution(aggregates, {PhaseDialect::QC, gphase.getLoc(), + PhaseExpression(gphase.getTheta())}); + directPhases[getDialectIndex(PhaseDialect::QC)].push_back(gphase); } else if (auto gphase = dyn_cast(&op)) { - phase.emplace(PhaseDialect::QCO, gphase.getLoc(), - PhaseExpression(gphase.getTheta())); - directPhases.push_back(gphase); + addContribution(aggregates, {PhaseDialect::QCO, gphase.getLoc(), + PhaseExpression(gphase.getTheta())}); + directPhases[getDialectIndex(PhaseDialect::QCO)].push_back(gphase); + } else if (auto it = contributionsByOperation.find(&op); + it != contributionsByOperation.end()) { + for (std::size_t i = 0; i < it->second.size(); ++i) { + auto& contribution = it->second[i]; + if (contribution) { + hasNestedContribution[i] = true; + addContribution(aggregates, std::move(*contribution)); + } + } + contributionsByOperation.erase(it); } else { - phase = normalizeOperation(&op); - hasNestedContribution |= phase.has_value(); + continue; } - if (!phase) { + } + + for (std::size_t i = 0; i < aggregates.size(); ++i) { + auto& aggregate = aggregates[i]; + if (!aggregate) { continue; } - if (aggregate) { - aggregate->add(std::move(*phase)); - } else { - aggregate = std::move(phase); + if (extractionBoundary != nullptr && + canExtract(extractionBoundary, aggregate->dialect) && + canExtractExpression(extractionBoundary, aggregate->expression) && + hoistExpressionBefore(aggregate->expression, block, + extractionBoundary, rewriter)) { + for (auto* phase : directPhases[i]) { + rewriter.eraseOp(phase); + } + extracted[i] = std::move(aggregate); + continue; } - } - if (!aggregate) { - return std::nullopt; - } - if (extractionBoundary != nullptr && - hoistExpressionBefore(aggregate->expression, block, extractionBoundary, - rewriter)) { - for (auto* phase : directPhases) { + // Preserve already-normalized exit phases, including dynamic angles. + if (extractionBoundary == nullptr && !hasNestedContribution[i] && + directPhases[i].size() == 1 && + isAtBlockExit(directPhases[i].front(), terminator)) { + auto* phase = directPhases[i].front(); + auto angle = dyn_cast(phase) + ? cast(phase).getTheta() + : cast(phase).getTheta(); + const auto constant = valueToConstantDouble(angle); + if (!constant || + (normalizeAngle(*constant) == *constant && *constant != 0.0)) { + continue; + } + } + + for (auto* phase : directPhases[i]) { rewriter.eraseOp(phase); } - return aggregate; - } - - // Preserve already-normalized exit phases, including dynamic angles. - if (extractionBoundary == nullptr && !hasNestedContribution && - directPhases.size() == 1 && - directPhases.front()->getNextNode() == block.getTerminator()) { - auto angle = dyn_cast(directPhases.front()) - ? cast(directPhases.front()).getTheta() - : cast(directPhases.front()).getTheta(); - const auto constant = valueToConstantDouble(angle); - if (!constant || - (normalizeAngle(*constant) == *constant && *constant != 0.0)) { - return std::nullopt; + if (aggregate->expression.isZero()) { + continue; + } + if (terminator != nullptr) { + rewriter.setInsertionPoint(terminator); + } else { + rewriter.setInsertionPointToEnd(&block); + } + auto angle = aggregate->expression.materialize(rewriter, aggregate->loc); + if (aggregate->dialect == PhaseDialect::QC) { + qc::GPhaseOp::create(rewriter, aggregate->loc, angle); + } else { + qco::GPhaseOp::create(rewriter, aggregate->loc, angle); } } - - for (auto* phase : directPhases) { - rewriter.eraseOp(phase); - } - if (aggregate->expression.isZero()) { - return std::nullopt; - } - rewriter.setInsertionPoint(block.getTerminator()); - auto angle = aggregate->expression.materialize(rewriter, aggregate->loc); - if (aggregate->dialect == PhaseDialect::QC) { - qc::GPhaseOp::create(rewriter, aggregate->loc, angle); - } else { - qco::GPhaseOp::create(rewriter, aggregate->loc, angle); - } - return std::nullopt; + return extracted; } IRRewriter rewriter; + DenseMap contributionsByOperation; }; struct NormalizeGlobalPhases final @@ -468,6 +566,10 @@ struct NormalizeGlobalPhases final } // namespace LogicalResult normalizeGlobalPhases(ModuleOp moduleOp) { + if (failed(verifyProgramMetadata(moduleOp)) || + failed(qco::verifyLinearity(moduleOp))) { + return failure(); + } GlobalPhaseNormalizer normalizer(moduleOp.getContext()); normalizer.normalize(moduleOp.getRegion()); return success(); diff --git a/mlir/lib/Dialect/MQT/Transforms/UnrollModifiers.cpp b/mlir/lib/Dialect/MQT/Transforms/UnrollModifiers.cpp index cd3a63767b..a1a9b2910d 100644 --- a/mlir/lib/Dialect/MQT/Transforms/UnrollModifiers.cpp +++ b/mlir/lib/Dialect/MQT/Transforms/UnrollModifiers.cpp @@ -15,6 +15,7 @@ #include "mlir/Dialect/QC/IR/QCOps.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/QCOUtils.h" #include #include @@ -336,16 +337,35 @@ namespace { struct UnrollModifiers final : impl::UnrollModifiersBase { protected: void runOnOperation() override { + if (failed(qco::verifyLinearity(getOperation()))) { + signalPassFailure(); + return; + } + + SmallVector reversePostorder; + SmallVector worklist{getOperation()}; + while (!worklist.empty()) { + Operation* op = worklist.pop_back_val(); + reversePostorder.push_back(op); + for (Region& region : op->getRegions()) { + for (Block& block : region) { + for (Operation& nested : block) { + worklist.push_back(&nested); + } + } + } + } + SmallVector modifiers; - getOperation()->walk([&](Operation* op) { + for (Operation* op : llvm::reverse(reversePostorder)) { if (isa(op)) { modifiers.push_back(op); } - }); + } - // The walk visits nested modifiers before their parents, so unrolling the - // collected modifiers in order reaches a fixpoint in a single sweep. + // Unrolling nested modifiers before their parents reaches a fixpoint in a + // single sweep. IRRewriter rewriter(&getContext()); for (auto* modifier : modifiers) { llvm::TypeSwitch(modifier) diff --git a/mlir/lib/Dialect/MQT/Utils/Angles.cpp b/mlir/lib/Dialect/MQT/Utils/Angles.cpp index aef5707c78..bb127fc0d1 100644 --- a/mlir/lib/Dialect/MQT/Utils/Angles.cpp +++ b/mlir/lib/Dialect/MQT/Utils/Angles.cpp @@ -11,11 +11,15 @@ #include "mlir/Dialect/MQT/Utils/Angles.h" #include "mlir/Dialect/MQT/Utils/ConstantFolding.h" +#include "mlir/Dialect/MQT/Utils/Parameters.h" +#include #include +#include #include #include +#include #include #include @@ -33,15 +37,57 @@ double normalizeAngle(double theta) { return theta; } +Value normalizeAngle(RewriterBase& rewriter, Location loc, Value theta) { + if (const auto constant = valueToConstantDouble(theta)) { + return constantFromScalar(rewriter, loc, normalizeAngle(*constant)); + } + + const auto pi = constantFromScalar(rewriter, loc, std::numbers::pi); + const auto negativePi = constantFromScalar(rewriter, loc, -std::numbers::pi); + const auto twoPi = constantFromScalar(rewriter, loc, 2.0 * std::numbers::pi); + auto remainder = arith::RemFOp::create(rewriter, loc, theta, twoPi); + auto abovePi = arith::CmpFOp::create(rewriter, loc, arith::CmpFPredicate::OGT, + remainder, pi); + auto belowOrAtNegativePi = arith::CmpFOp::create( + rewriter, loc, arith::CmpFPredicate::OLE, remainder, negativePi); + auto subtractTurn = arith::SubFOp::create(rewriter, loc, remainder, twoPi); + auto addTurn = arith::AddFOp::create(rewriter, loc, remainder, twoPi); + auto upperBounded = + arith::SelectOp::create(rewriter, loc, abovePi, subtractTurn, remainder); + return arith::SelectOp::create(rewriter, loc, belowOrAtNegativePi, addTurn, + upperBounded) + .getResult(); +} + +double scaleAngleByInteger(double theta, double factor) { + assert(std::isfinite(factor) && factor == std::floor(factor)); + double remaining = std::abs(factor); + double multiple = normalizeAngle(theta); + double result = 0.0; + while (remaining >= 1.0) { + if (std::fmod(remaining, 2.0) == 1.0) { + result = normalizeAngle(result + multiple); + } + remaining = std::floor(remaining / 2.0); + if (remaining >= 1.0) { + multiple = normalizeAngle(multiple + multiple); + } + } + return factor < 0.0 ? normalizeAngle(-result) : result; +} + bool isValidGlobalPhaseAngle(const double theta) { return std::isfinite(theta) && std::abs(theta) <= MAX_GLOBAL_PHASE_ANGLE; } LogicalResult verifyGlobalPhaseAngle(Operation* operation, Value angle) { const auto constant = valueToConstantDouble(angle); - if (!constant || !std::isfinite(*constant)) { + if (!constant) { return success(); } + if (!std::isfinite(*constant)) { + return operation->emitOpError("constant angle must be finite"); + } if (!isValidGlobalPhaseAngle(*constant)) { return operation->emitOpError() << "constant angle must have magnitude at most " diff --git a/mlir/lib/Dialect/MQT/Utils/ConstantFolding.cpp b/mlir/lib/Dialect/MQT/Utils/ConstantFolding.cpp index b9a446ec3c..b67496dee1 100644 --- a/mlir/lib/Dialect/MQT/Utils/ConstantFolding.cpp +++ b/mlir/lib/Dialect/MQT/Utils/ConstantFolding.cpp @@ -10,8 +10,8 @@ #include "mlir/Dialect/MQT/Utils/ConstantFolding.h" +#include #include -#include #include #include #include @@ -41,11 +41,11 @@ std::optional attributeToDouble(Attribute attr) { } std::optional valueToDouble(Value value) { - auto constantOp = value.getDefiningOp(); - if (!constantOp) { + Attribute attr; + if (!matchPattern(value, m_Constant(&attr))) { return std::nullopt; } - return attributeToDouble(constantOp.getValue()); + return attributeToDouble(attr); } std::optional @@ -55,39 +55,105 @@ valueToConstantAttr(Value value, return it->second; } - Attribute attr; - if (matchPattern(value, m_Constant(&attr))) { - return cache[value] = attr; - } + struct Frame { + Value value; + Operation* operation; + unsigned nextOperand = 0; + }; - Operation* operation = value.getDefiningOp(); - if (operation == nullptr || operation->getNumRegions() != 0 || - !isPure(operation)) { - return cache[value] = std::nullopt; - } + SmallVector worklist; + llvm::SmallDenseSet active; + const auto schedule = [&](Value candidate) { + if (cache.contains(candidate)) { + return; + } + Attribute attr; + if (matchPattern(candidate, m_Constant(&attr))) { + cache[candidate] = attr; + return; + } + Operation* operation = candidate.getDefiningOp(); + if (operation == nullptr || operation->getNumRegions() != 0 || + !isPure(operation)) { + cache[candidate] = std::nullopt; + return; + } + active.insert(candidate); + worklist.push_back({candidate, operation}); + }; - SmallVector operands; - operands.reserve(operation->getNumOperands()); - for (Value operand : operation->getOperands()) { - const auto folded = valueToConstantAttr(operand, cache); - if (!folded) { - return cache[value] = std::nullopt; + schedule(value); + while (!worklist.empty()) { + auto& frame = worklist.back(); + bool scheduledOperand = false; + while (frame.nextOperand < frame.operation->getNumOperands()) { + Value operand = frame.operation->getOperand(frame.nextOperand++); + if (cache.contains(operand)) { + continue; + } + if (active.contains(operand)) { + cache[operand] = std::nullopt; + continue; + } + schedule(operand); + scheduledOperand = true; + break; + } + if (scheduledOperand) { + continue; } - operands.push_back(*folded); - } - SmallVector results; - if (failed(operation->fold(operands, results)) || results.size() != 1) { - return cache[value] = std::nullopt; - } - std::optional folded; - if (auto resultAttr = dyn_cast_if_present(results.front())) { - folded = resultAttr; - } else if (auto resultValue = dyn_cast_if_present(results.front())) { - /* Identity-style folds can return an existing SSA value. */ - folded = valueToConstantAttr(resultValue, cache); + SmallVector operands; + operands.reserve(frame.operation->getNumOperands()); + bool failedOperand = false; + for (Value operand : frame.operation->getOperands()) { + const auto it = cache.find(operand); + if (it == cache.end() || !it->second) { + failedOperand = true; + break; + } + operands.push_back(*it->second); + } + if (failedOperand) { + active.erase(frame.value); + cache[frame.value] = std::nullopt; + worklist.pop_back(); + continue; + } + + SmallVector results; + if (failed(frame.operation->fold(operands, results)) || + results.size() != 1) { + active.erase(frame.value); + cache[frame.value] = std::nullopt; + worklist.pop_back(); + continue; + } + if (auto resultAttr = dyn_cast_if_present(results.front())) { + active.erase(frame.value); + cache[frame.value] = resultAttr; + worklist.pop_back(); + continue; + } + + auto resultValue = dyn_cast_if_present(results.front()); + if (!resultValue || resultValue == frame.value || + active.contains(resultValue)) { + active.erase(frame.value); + cache[frame.value] = std::nullopt; + worklist.pop_back(); + continue; + } + if (!cache.contains(resultValue)) { + schedule(resultValue); + continue; + } + active.erase(frame.value); + cache[frame.value] = cache.lookup(resultValue); + worklist.pop_back(); } - return cache[value] = folded; + + return cache.lookup(value); } std::optional valueToConstantAttr(Value value) { diff --git a/mlir/lib/Dialect/MQT/Utils/Modifiers.cpp b/mlir/lib/Dialect/MQT/Utils/Modifiers.cpp index e5a299bcda..3376c9a660 100644 --- a/mlir/lib/Dialect/MQT/Utils/Modifiers.cpp +++ b/mlir/lib/Dialect/MQT/Utils/Modifiers.cpp @@ -58,13 +58,24 @@ Value getValueFromBlockArgument(Value qubit, ValueRange qubits) { return qubit; } -void hoistSupportingOpsBefore(Block& body, Operation* keep, Operation* target, - RewriterBase& rewriter) { +LogicalResult hoistSupportingOpsBefore(Block& body, Operation* keep, + Operation* target, + RewriterBase& rewriter) { + bool sawKeep = false; + for (Operation& bodyOp : body) { + if (&bodyOp == keep) { + sawKeep = true; + } else if (sawKeep && !bodyOp.hasTrait() && + !isPure(&bodyOp)) { + return failure(); + } + } for (auto& bodyOp : llvm::make_early_inc_range(body)) { if (&bodyOp != keep && !bodyOp.hasTrait()) { rewriter.moveOpBefore(&bodyOp, target); } } + return success(); } void inlineModifierBody(Operation* operation, Block& body, diff --git a/mlir/lib/Dialect/QC/IR/Modifiers/CtrlOp.cpp b/mlir/lib/Dialect/QC/IR/Modifiers/CtrlOp.cpp index 9154b415f3..819411d96c 100644 --- a/mlir/lib/Dialect/QC/IR/Modifiers/CtrlOp.cpp +++ b/mlir/lib/Dialect/QC/IR/Modifiers/CtrlOp.cpp @@ -133,11 +133,12 @@ struct ReduceCtrl final : OpRewritePattern { op->getAttrOfType(opSegmentsAttrName); auto newSegments = DenseI32ArrayAttr::get( rewriter.getContext(), {segmentsAttr[0] - 1, segmentsAttr[1] + 1}); - op->setAttr(opSegmentsAttrName, newSegments); - - // Add a block argument for the target qubit - auto arg = op.getBody()->addArgument(QubitType::get(rewriter.getContext()), - op.getLoc()); + rewriter.modifyOpInPlace(op, [&] { + op->setAttr(opSegmentsAttrName, newSegments); + op.getBody()->addArgument(QubitType::get(rewriter.getContext()), + op.getLoc()); + }); + auto arg = op.getBody()->getArguments().back(); // Replace the current GPhaseOp with a PhaseOp const OpBuilder::InsertionGuard guard(rewriter); @@ -156,11 +157,13 @@ struct EraseEmptyCtrl final : OpRewritePattern { using OpRewritePattern::OpRewritePattern; LogicalResult matchAndRewrite(CtrlOp op, PatternRewriter& rewriter) const override { - if (op.getNumBodyUnitaries() != 0) { + if (llvm::any_of(*op.getBody(), [](Operation& operation) { + return mqt::containsUnitaryOperation(&operation); + })) { return failure(); } - rewriter.eraseOp(op); + mqt::inlineModifierBody(op, *op.getBody(), op.getTargets(), rewriter); return success(); } }; diff --git a/mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp b/mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp index b0af22ea2f..f49b63dcac 100644 --- a/mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp +++ b/mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp @@ -14,6 +14,7 @@ #include "mlir/Dialect/QC/IR/QCInterfaces.h" #include "mlir/Dialect/QC/IR/QCOps.h" +#include #include #include #include @@ -67,6 +68,11 @@ struct MoveCtrlOutsideInv final : OpRewritePattern { return mqt::getValueFromBlockArgument(t, outerQubits); }); + if (failed(mqt::hoistSupportingOpsBefore(*op.getBody(), innerCtrlOp, op, + rewriter))) { + return failure(); + } + rewriter.replaceOpWithNewOp( op, controls, targets, [&](ValueRange targetArgs) { InvOp::create(rewriter, op.getLoc(), targetArgs, @@ -103,8 +109,10 @@ struct InvPowToNegPow final : OpRewritePattern { // Move supporting ops (constants, arithmetic) out of the body so their // Values are accessible from outside and survive InvOp erasure. - mqt::hoistSupportingOpsBefore(*invOp.getBody(), innerPow.getOperation(), - invOp, rewriter); + if (failed(mqt::hoistSupportingOpsBefore( + *invOp.getBody(), innerPow.getOperation(), invOp, rewriter))) { + return failure(); + } Value negExponent = arith::NegFOp::create(rewriter, invOp.getLoc(), innerPow.getExponent()); // The inner pow's operands alias the inv's block args; translate them back @@ -323,6 +331,10 @@ struct CancelNestedInv final : OpRewritePattern { llvm::map_to_vector(innerInvOp.getQubits(), [&](Value q) { return mqt::getValueFromBlockArgument(q, op.getQubits()); }); + if (failed(mqt::hoistSupportingOpsBefore(*op.getBody(), innerInvOp, op, + rewriter))) { + return failure(); + } mqt::inlineModifierBody(op, *innerInvOp.getBody(), replacements, rewriter); return success(); } @@ -335,11 +347,13 @@ struct EraseEmptyInv final : OpRewritePattern { using OpRewritePattern::OpRewritePattern; LogicalResult matchAndRewrite(InvOp op, PatternRewriter& rewriter) const override { - if (op.getNumBodyUnitaries() != 0) { + if (llvm::any_of(*op.getBody(), [](Operation& operation) { + return mqt::containsUnitaryOperation(&operation); + })) { return failure(); } - rewriter.eraseOp(op); + mqt::inlineModifierBody(op, *op.getBody(), op.getQubits(), rewriter); return success(); } }; diff --git a/mlir/lib/Dialect/QC/IR/Modifiers/ModifierUtils.cpp b/mlir/lib/Dialect/QC/IR/Modifiers/ModifierUtils.cpp index fd8d89c8e7..b99a9cc2c2 100644 --- a/mlir/lib/Dialect/QC/IR/Modifiers/ModifierUtils.cpp +++ b/mlir/lib/Dialect/QC/IR/Modifiers/ModifierUtils.cpp @@ -10,52 +10,101 @@ #include "ModifierUtils.h" -#include "mlir/Dialect/CBit/IR/CBitOps.h" #include "mlir/Dialect/MQT/Utils/Modifiers.h" -#include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/IR/QCOps.h" #include +#include #include -#include #include +#include #include #include #include +#include +#include #include #include -#include #include #include namespace mlir::qc::detail { +[[nodiscard]] static bool containsQubit(Type type) { + if (isa(type)) { + return true; + } + const auto shapedType = dyn_cast(type); + return shapedType && isa(shapedType.getElementType()); +} + +[[nodiscard]] static bool +isForbiddenModifierBodyOperation(Operation* operation) { + const auto carriesQubit = + llvm::any_of(operation->getOperandTypes(), containsQubit) || + llvm::any_of(operation->getResultTypes(), containsQubit); + if (isa(operation) || + operation->hasTrait() || + operation->hasTrait()) { + return false; + } + if (isa(operation)) { + return carriesQubit; + } + if (!isMemoryEffectFree(operation)) { + return true; + } + return carriesQubit; +} + LogicalResult verifyModifierBody(Operation* modifierOp, Block& body) { - const auto hasNonUnitaryOperation = - body.walk([](Operation* operation) { - return isa(operation) - ? WalkResult::interrupt() - : WalkResult::advance(); - }) - .wasInterrupted(); - if (hasNonUnitaryOperation) { + auto targets = cast(modifierOp).getTargets(); + if (body.getNumArguments() != targets.size()) { return modifierOp->emitOpError( - "body must not contain non-unitary operations or access registers"); + "number of block arguments must match the number of targets"); + } + SmallPtrSet uniqueTargets; + for (auto [index, argument, target] : + llvm::enumerate(body.getArguments(), targets)) { + if (argument.getType() != target.getType()) { + return modifierOp->emitOpError("block argument type at index ") + << index << " does not match target type"; + } + if (!uniqueTargets.insert(target).second) { + return modifierOp->emitOpError("duplicate target qubit found"); + } } SetVector captures; getUsedValuesDefinedAbove(modifierOp->getRegions(), captures); if (llvm::any_of(captures, [](Value value) { - return isa(value.getType()); + return containsQubit(value.getType()); })) { return modifierOp->emitOpError( "body must not capture qubits from above; use only its aliased block " "arguments"); } + SmallVector worklist; + for (Operation& operation : body) { + worklist.push_back(&operation); + } + while (!worklist.empty()) { + Operation* operation = worklist.pop_back_val(); + if (isForbiddenModifierBodyOperation(operation)) { + return modifierOp->emitOpError( + "body must not contain non-unitary operations or access registers"); + } + for (Region& region : operation->getRegions()) { + for (Block& block : region) { + for (Operation& nested : block) { + worklist.push_back(&nested); + } + } + } + } + return success(); } diff --git a/mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp b/mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp index 863bc25466..b99deebd9a 100644 --- a/mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp +++ b/mlir/lib/Dialect/QC/IR/Modifiers/PowOp.cpp @@ -18,6 +18,7 @@ #include "mlir/Dialect/QC/IR/QCInterfaces.h" #include "mlir/Dialect/QC/IR/QCOps.h" +#include #include #include #include @@ -117,6 +118,12 @@ static Value scaleByExponent(Value param, PowOp op, PatternRewriter& rewriter) { return arith::MulFOp::create(rewriter, op.getLoc(), op.getExponent(), param); } +[[nodiscard]] static bool constantScaleIsFinite(Value parameter, + double factor) { + const auto constant = valueToConstantDouble(parameter); + return constant ? std::isfinite(*constant * factor) : std::abs(factor) <= 1.0; +} + namespace { /// pow(1.0) { U } => U @@ -143,6 +150,10 @@ struct ErasePow0 final : OpRewritePattern { if (!exponent || std::abs(*exponent) > PARAMETER_COMPARISON_TOLERANCE) { return failure(); } + if (failed(mqt::hoistSupportingOpsBefore( + *op.getBody(), op, rewriter))) { + return failure(); + } rewriter.eraseOp(op); return success(); } @@ -195,6 +206,14 @@ struct MergeNestedPow final : OpRewritePattern { if (!innerPow) { return failure(); } + if (const auto innerExponent = + valueToConstantDouble(innerPow.getExponent())) { + if (!std::isfinite(*innerExponent * *outerExponent)) { + return failure(); + } + } else if (std::abs(*outerExponent) > 1.0) { + return failure(); + } // The inner pow's operands alias the outer pow's block args, possibly in a // different order / subset. Translate them back to the outer pow's operands // so the merged pow's footprint matches the inner pow positionally. @@ -204,8 +223,10 @@ struct MergeNestedPow final : OpRewritePattern { }); // Move supporting ops (constants, arithmetic) out of the body so their // Values are accessible from outside and survive PowOp erasure. - mqt::hoistSupportingOpsBefore(*op.getBody(), innerPow.getOperation(), op, - rewriter); + if (failed(mqt::hoistSupportingOpsBefore( + *op.getBody(), innerPow.getOperation(), op, rewriter))) { + return failure(); + } auto merged = scaleByExponent(innerPow.getExponent(), op, rewriter); rewriter.replaceOpWithNewOp( op, merged, qubits, [&](ValueRange powArgs) { @@ -246,6 +267,11 @@ struct MoveCtrlOutsidePow final : OpRewritePattern { return mqt::getValueFromBlockArgument(t, outerQubits); }); + if (failed(mqt::hoistSupportingOpsBefore(*op.getBody(), innerCtrlOp, op, + rewriter))) { + return failure(); + } + rewriter.replaceOpWithNewOp( op, controls, targets, [&](ValueRange targetArgs) { PowOp::create(rewriter, op.getLoc(), op.getExponent(), targetArgs, @@ -324,9 +350,35 @@ struct FoldPowIntoGate final : OpRewritePattern { return failure(); } + if (auto gate = dyn_cast(innerOp)) { + if (const auto angle = valueToConstantDouble(gate.getTheta()); + angle && !isValidGlobalPhaseAngle(*angle * r)) { + return failure(); + } + if (!valueToConstantDouble(gate.getTheta()) && std::abs(r) > 1.0) { + return failure(); + } + } else if (isa(innerOp)) { + if (!constantScaleIsFinite(inner.getParameter(0), r)) { + return failure(); + } + } + if (isa( + innerOp)) { + const double fullTurnScale = r * std::numbers::pi; + if (!std::isfinite(fullTurnScale) || + std::abs(fullTurnScale) > MAX_GLOBAL_PHASE_ANGLE) { + return failure(); + } + } + // Move supporting ops (constants, arithmetic) out of the body so their // Values are accessible from outside and survive PowOp erasure. - mqt::hoistSupportingOpsBefore(*op.getBody(), innerOp, op, rewriter); + if (failed(mqt::hoistSupportingOpsBefore(*op.getBody(), innerOp, op, + rewriter))) { + return failure(); + } return TypeSwitch(innerOp) // --- Rotation gates: multiply angle by exponent --- @@ -621,11 +673,13 @@ struct EraseEmptyPow final : OpRewritePattern { using OpRewritePattern::OpRewritePattern; LogicalResult matchAndRewrite(PowOp op, PatternRewriter& rewriter) const override { - if (op.getNumBodyUnitaries() != 0) { + if (llvm::any_of(*op.getBody(), [](Operation& operation) { + return mqt::containsUnitaryOperation(&operation); + })) { return failure(); } - rewriter.eraseOp(op); + mqt::inlineModifierBody(op, *op.getBody(), op.getQubits(), rewriter); return success(); } }; diff --git a/mlir/lib/Dialect/QC/IR/QubitManagement/DeallocOp.cpp b/mlir/lib/Dialect/QC/IR/QubitManagement/DeallocOp.cpp index 26b6f7b6f6..b8559de464 100644 --- a/mlir/lib/Dialect/QC/IR/QubitManagement/DeallocOp.cpp +++ b/mlir/lib/Dialect/QC/IR/QubitManagement/DeallocOp.cpp @@ -53,7 +53,9 @@ struct HoistStaticQubit final : OpRewritePattern { LogicalResult matchAndRewrite(StaticOp op, PatternRewriter& rewriter) const override { auto funcOp = op->getParentOfType(); - if (!funcOp || op->getBlock() == &funcOp.getBody().front()) { + if (!funcOp || + op->getParentWithTrait() != funcOp || + op->getBlock() == &funcOp.getBody().front()) { return failure(); } rewriter.moveOpBefore(op, &funcOp.getBody().front(), diff --git a/mlir/lib/Dialect/QC/Transforms/ShrinkQubitRegisters.cpp b/mlir/lib/Dialect/QC/Transforms/ShrinkQubitRegisters.cpp index c4f09ee4a8..65843e6b1a 100644 --- a/mlir/lib/Dialect/QC/Transforms/ShrinkQubitRegisters.cpp +++ b/mlir/lib/Dialect/QC/Transforms/ShrinkQubitRegisters.cpp @@ -10,6 +10,7 @@ #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/Transforms/Passes.h" +#include "mlir/Support/OperationUtils.h" #include #include @@ -156,6 +157,11 @@ struct ShrinkQubitRegistersPass final : impl::ShrinkQubitRegistersPassBase { protected: void runOnOperation() override { + constexpr size_t maxRegionNesting = 64; + if (failed(verifyRegionNestingDepth(getOperation(), maxRegionNesting))) { + signalPassFailure(); + return; + } RewritePatternSet patterns(&getContext()); patterns.add(&getContext()); diff --git a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp index d20cdf93b4..4541f0f597 100644 --- a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp +++ b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp @@ -17,11 +17,13 @@ #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/IR/QCInterfaces.h" #include "mlir/Dialect/QC/IR/QCOps.h" +#include "mlir/Support/OperationUtils.h" #include "mlir/Target/OpenQASM/GateCatalog.h" #include #include #include +#include #include #include #include @@ -134,7 +136,8 @@ class OpenQASMEmitter { explicit OpenQASMEmitter(ModuleOp moduleOp) : moduleOp(moduleOp) {} [[nodiscard]] FailureOr emit() { - if (failed(verify(moduleOp)) || failed(preflight()) || + if (failed(verifyRegionNestingDepth(moduleOp, maxRegionNesting)) || + failed(verify(moduleOp)) || failed(preflight()) || failed(collectProgramShape())) { return failure(); } @@ -179,6 +182,14 @@ class OpenQASMEmitter { size_t nextScalar = 0; size_t nextLoop = 0; size_t nextHelper = 0; + size_t expressionNesting = 0; + size_t expressionWork = 0; + size_t numClassicalBits = 0; + + static constexpr size_t maxRegionNesting = 64; + static constexpr size_t maxExpressionNesting = 256; + static constexpr size_t maxExpressionWork = 4096; + static constexpr size_t maxClassicalBits = 1U << 20; [[nodiscard]] static LogicalResult fail(Operation* operation, const Twine& message) { @@ -294,6 +305,14 @@ class OpenQASMEmitter { } if (auto alloc = dyn_cast(&operation)) { const auto type = alloc.getResult().getType(); + const auto width = type.getWidth(); + if (width <= 0 || static_cast(width) > + maxClassicalBits - numClassicalBits) { + return fail(alloc, "total classical register width exceeds the " + "supported limit of " + + Twine(maxClassicalBits) + " bits"); + } + numClassicalBits += static_cast(width); const bool isOutput = returnedRegisters.contains(alloc.getResult()); const auto name = alloc->getAttrOfType( mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()); @@ -301,7 +320,7 @@ class OpenQASMEmitter { Resource resource{.kind = ResourceKind::Bit, .name = isOutput ? outputName(requested) : uniqueName("c", nextBit), - .width = type.getWidth(), + .width = width, .output = isOutput, .initialization = alloc.getInitialization()}; resources.try_emplace(alloc.getResult(), std::move(resource)); @@ -550,6 +569,22 @@ class OpenQASMEmitter { } [[nodiscard]] FailureOr emitExpression(Value value) { + if (expressionNesting == 0) { + expressionWork = 0; + } + ++expressionNesting; + ++expressionWork; + auto depthGuard = llvm::make_scope_exit([&] { --expressionNesting; }); + if (expressionNesting > maxExpressionNesting) { + return failExpression(value, "expression nesting exceeds the supported " + "maximum of " + + Twine(maxExpressionNesting)); + } + if (expressionWork > maxExpressionWork) { + return failExpression(value, "expression expansion exceeds the supported " + "maximum of " + + Twine(maxExpressionWork) + " values"); + } if (const auto found = valueNames.find(value); found != valueNames.end()) { return found->second; } @@ -583,8 +618,11 @@ class OpenQASMEmitter { const auto name = operation->getName().getStringRef(); if (name == "arith.remf") { auto lhs = emitExpression(operation->getOperand(0)); + if (failed(lhs)) { + return failure(); + } auto rhs = emitExpression(operation->getOperand(1)); - if (failed(lhs) || failed(rhs)) { + if (failed(rhs)) { return failure(); } return (Twine("mod(") + *lhs + ", " + *rhs + ")").str(); @@ -679,8 +717,11 @@ class OpenQASMEmitter { [[nodiscard]] FailureOr emitBinary(Value lhsValue, const StringRef operation, Value rhsValue) { auto lhs = emitExpression(lhsValue); + if (failed(lhs)) { + return failure(); + } auto rhs = emitExpression(rhsValue); - if (failed(lhs) || failed(rhs)) { + if (failed(rhs)) { return failure(); } return (Twine("(") + *lhs + " " + operation + " " + *rhs + ")").str(); diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp index db164ff35a..6f24c9cabb 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp @@ -40,6 +40,19 @@ using namespace mlir::qco; namespace { +static void inlineCtrlBody(CtrlOp op, PatternRewriter& rewriter) { + auto* body = op.getBody(); + auto* terminator = body->getTerminator(); + SmallVector outputs(op.getControlsIn()); + for (Value yielded : terminator->getOperands()) { + outputs.push_back( + mqt::getValueFromBlockArgument(yielded, op.getTargetsIn())); + } + rewriter.inlineBlockBefore(body, op, op.getTargetsIn()); + rewriter.eraseOp(terminator); + rewriter.replaceOp(op, outputs); +} + /** * @brief Merge nested control modifiers into a single one. */ @@ -130,15 +143,7 @@ struct ReduceCtrl final : OpRewritePattern { // Inline ops from empty control modifiers, IdOp and BarrierOp if (op.getNumControls() == 0 || isa(innerOp)) { - auto* body = op.getBody(); - auto* terminator = body->getTerminator(); - // Controls are pass-through results outside the body yield, so the - // generic inlineModifierBody result mapping does not apply here. - SmallVector outputs(op.getControlsIn()); - llvm::append_range(outputs, terminator->getOperands()); - rewriter.inlineBlockBefore(body, op, op.getTargetsIn()); - rewriter.eraseOp(terminator); - rewriter.replaceOp(op, outputs); + inlineCtrlBody(op, rewriter); return success(); } @@ -171,13 +176,14 @@ struct ReduceCtrl final : OpRewritePattern { op->getAttrOfType(opSegmentsAttrName); auto newSegments = DenseI32ArrayAttr::get( rewriter.getContext(), {segmentsAttr[0] - 1, segmentsAttr[1] + 1}); - op->setAttr(opSegmentsAttrName, newSegments); const auto opResultSegmentsAttrName = CtrlOp::getResultSegmentSizeAttr(); - op->setAttr(opResultSegmentsAttrName, newSegments); - - // Add a block argument for the target qubit - auto arg = op.getBody()->addArgument(QubitType::get(rewriter.getContext()), - op.getLoc()); + rewriter.modifyOpInPlace(op, [&] { + op->setAttr(opSegmentsAttrName, newSegments); + op->setAttr(opResultSegmentsAttrName, newSegments); + op.getBody()->addArgument(QubitType::get(rewriter.getContext()), + op.getLoc()); + }); + auto arg = op.getBody()->getArguments().back(); // Replace the current GPhaseOp with a PhaseOp const OpBuilder::InsertionGuard guard(rewriter); @@ -187,7 +193,8 @@ struct ReduceCtrl final : OpRewritePattern { // Add the results of the POp to the yield operation auto yieldOp = cast(op.getBody()->back()); - yieldOp->setOperands(pOp->getResults()); + rewriter.modifyOpInPlace(yieldOp, + [&] { yieldOp->setOperands(pOp->getResults()); }); // Erase the GPhaseOp rewriter.eraseOp(gPhaseOp); @@ -203,11 +210,13 @@ struct EraseEmptyCtrl final : OpRewritePattern { using OpRewritePattern::OpRewritePattern; LogicalResult matchAndRewrite(CtrlOp op, PatternRewriter& rewriter) const override { - if (op.getNumBodyUnitaries() != 0) { + if (llvm::any_of(*op.getBody(), [](Operation& operation) { + return mqt::containsUnitaryOperation(&operation); + })) { return failure(); } - rewriter.replaceOp(op, op.getOperands()); + inlineCtrlBody(op, rewriter); return success(); } }; @@ -302,6 +311,15 @@ void CtrlOp::build(OpBuilder& odsBuilder, OperationState& odsState, } LogicalResult CtrlOp::verify() { + if (getControlsIn().size() != getControlsOut().size()) { + return emitOpError( + "number of input controls must match the number of output controls"); + } + if (getTargetsIn().size() != getTargetsOut().size()) { + return emitOpError( + "number of input targets must match the number of output targets"); + } + auto& block = *getBody(); if (failed(detail::verifyModifierBody(getOperation(), block))) { return failure(); @@ -356,17 +374,17 @@ void CtrlOp::getCanonicalizationPatterns(RewritePatternSet& results, } bool CtrlOp::hasCompileTimeKnownUnitaryMatrix() { - return all_of(getBody()->getOps(), - [](UnitaryOpInterface op) { - return op.hasCompileTimeKnownUnitaryMatrix(); - }); + if (!isModifierMatrixSizeSupported(getNumTargets(), getNumControls()) || + !detail::isModifierMatrixNestingSupported(getOperation())) { + return false; + } + return hasComposableBodyMatrix(*getBody(), getNumTargets()); } std::optional CtrlOp::getUnitaryMatrix() { - if (getNumControls() >= 32) { - llvm::reportFatalUsageError( - "Creating the unitary matrix for a CtrlOp with more than 31 controls " - "is not supported due to memory constraints."); + if (!isModifierMatrixSizeSupported(getNumTargets(), getNumControls()) || + !detail::isModifierMatrixNestingSupported(getOperation())) { + return std::nullopt; } const auto numControls = getNumControls(); @@ -382,18 +400,7 @@ std::optional CtrlOp::getUnitaryMatrix() { return matrix; }; - // Single inner unitary (e.g. `ctrl { h }`, `ctrl { cx }`). - if (auto bodyUnitary = - mqt::getSoleBodyUnitary(*getBody())) { - if (const auto targetMatrix = - bodyUnitary.getUnitaryMatrix()) { - assert(targetMatrix->cols() == targetMatrix->rows()); - return controlledMatrix(targetMatrix->cols(), *targetMatrix); - } - return std::nullopt; - } - - // Composed body (e.g., `ctrl { h; x }` or `ctrl { swap; ry }`) + // Compose the complete body so pass-through targets are represented too. if (const auto composed = composeBodyMatrix(*getBody(), getNumTargets())) { return controlledMatrix(composed->rows(), *composed); } diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp index a702deb803..31090730a7 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp @@ -80,6 +80,11 @@ struct MoveCtrlOutsideInv final : OpRewritePattern { return mqt::getValueFromBlockArgument(t, outerQubits); }); + if (failed(mqt::hoistSupportingOpsBefore(*op.getBody(), innerCtrlOp, op, + rewriter))) { + return failure(); + } + auto newCtrl = CtrlOp::create(rewriter, op.getLoc(), controls, targets, [&](ValueRange targetArgs) -> SmallVector { @@ -132,8 +137,10 @@ struct InvPowToNegPow final : OpRewritePattern { // Move supporting ops (constants, arithmetic) out of the body so their // Values are accessible from outside and survive InvOp erasure. - mqt::hoistSupportingOpsBefore(*invOp.getBody(), innerPow.getOperation(), - invOp, rewriter); + if (failed(mqt::hoistSupportingOpsBefore( + *invOp.getBody(), innerPow.getOperation(), invOp, rewriter))) { + return failure(); + } Value negExponent = arith::NegFOp::create(rewriter, invOp.getLoc(), innerPow.getExponent()); // The inner pow's operands alias the inv's block args; translate them back @@ -376,6 +383,10 @@ struct CancelNestedInv final : OpRewritePattern { llvm::map_to_vector(innerInvOp.getInputQubits(), [&](Value q) { return mqt::getValueFromBlockArgument(q, op.getInputQubits()); }); + if (failed(mqt::hoistSupportingOpsBefore(*op.getBody(), innerInvOp, op, + rewriter))) { + return failure(); + } mqt::inlineModifierBody(op, *innerInvOp.getBody(), replacements, rewriter); return success(); } @@ -388,11 +399,13 @@ struct EraseEmptyInv final : OpRewritePattern { using OpRewritePattern::OpRewritePattern; LogicalResult matchAndRewrite(InvOp op, PatternRewriter& rewriter) const override { - if (op.getNumBodyUnitaries() != 0) { + if (llvm::any_of(*op.getBody(), [](Operation& operation) { + return mqt::containsUnitaryOperation(&operation); + })) { return failure(); } - rewriter.replaceOp(op, op.getOperands()); + mqt::inlineModifierBody(op, *op.getBody(), op.getInputQubits(), rewriter); return success(); } }; @@ -472,6 +485,10 @@ void InvOp::build(OpBuilder& odsBuilder, OperationState& odsState, Value qubit, } LogicalResult InvOp::verify() { + if (getQubitsIn().size() != getQubitsOut().size()) { + return emitOpError( + "number of input qubits must match the number of output qubits"); + } auto& block = *getBody(); if (failed(detail::verifyModifierBody(getOperation(), block))) { return failure(); @@ -503,6 +520,13 @@ LogicalResult InvOp::verify() { } } + SmallPtrSet uniqueQubitsOut; + for (Value target : blockTerminator->getOperands()) { + if (!uniqueQubitsOut.insert(target).second) { + return emitOpError("duplicate yielded qubit found"); + } + } + return success(); } @@ -514,28 +538,19 @@ void InvOp::getCanonicalizationPatterns(RewritePatternSet& results, } bool InvOp::hasCompileTimeKnownUnitaryMatrix() { - return all_of(getBody()->getOps(), - [](UnitaryOpInterface op) { - return op.hasCompileTimeKnownUnitaryMatrix(); - }); + if (!isModifierMatrixSizeSupported(getNumTargets()) || + !detail::isModifierMatrixNestingSupported(getOperation())) { + return false; + } + return hasComposableBodyMatrix(*getBody(), getNumTargets()); } std::optional InvOp::getUnitaryMatrix() { - if (getNumBodyUnitaries() == 0) { - return DynamicMatrix::identity(1LL << getNumTargets()); - } - - // Single inner unitary (e.g. `inv { h }`, `inv { cx }`). - if (auto bodyUnitary = - mqt::getSoleBodyUnitary(*getBody())) { - if (const auto targetMatrix = - bodyUnitary.getUnitaryMatrix()) { - return targetMatrix->adjoint(); - } + if (!isModifierMatrixSizeSupported(getNumTargets()) || + !detail::isModifierMatrixNestingSupported(getOperation())) { return std::nullopt; } - - // Composed body (e.g., `ctrl { h; x }` or `ctrl { swap; ry }`) + // Compose the complete body so pass-through targets are represented too. if (const auto composed = composeBodyMatrix(*getBody(), getNumTargets())) { return composed->adjoint(); } diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.cpp index cd4a4cf30e..22d4b448b3 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.cpp @@ -10,52 +10,105 @@ #include "ModifierUtils.h" -#include "mlir/Dialect/CBit/IR/CBitOps.h" #include "mlir/Dialect/MQT/Utils/Modifiers.h" -#include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include #include -#include #include +#include #include #include #include +#include +#include #include #include -#include #include #include +#include namespace mlir::qco::detail { -LogicalResult verifyModifierBody(Operation* modifierOp, Block& body) { - const auto hasNonUnitaryOperation = - body.walk([](Operation* operation) { - return isa(operation) - ? WalkResult::interrupt() - : WalkResult::advance(); - }) - .wasInterrupted(); - if (hasNonUnitaryOperation) { - return modifierOp->emitOpError( - "body must not contain non-unitary operations or access registers"); +bool isModifierMatrixNestingSupported(Operation* modifierOp) { + constexpr size_t maxModifierNesting = 64; + SmallVector> worklist{{modifierOp, 0}}; + while (!worklist.empty()) { + auto [operation, parentDepth] = worklist.pop_back_val(); + const size_t depth = + parentDepth + static_cast(isa(operation)); + if (depth > maxModifierNesting) { + return false; + } + for (Region& region : operation->getRegions()) { + for (Block& block : region) { + for (Operation& nested : block) { + worklist.emplace_back(&nested, depth); + } + } + } + } + return true; +} + +[[nodiscard]] static bool containsQubit(Type type) { + if (isa(type)) { + return true; } + const auto shapedType = dyn_cast(type); + return shapedType && isa(shapedType.getElementType()); +} +[[nodiscard]] static bool +isForbiddenModifierBodyOperation(Operation* operation) { + const auto carriesQubit = + llvm::any_of(operation->getOperandTypes(), containsQubit) || + llvm::any_of(operation->getResultTypes(), containsQubit); + if (isa(operation) || + operation->hasTrait() || + operation->hasTrait()) { + return false; + } + if (isa(operation)) { + return carriesQubit; + } + if (!isMemoryEffectFree(operation)) { + return true; + } + return carriesQubit; +} + +LogicalResult verifyModifierBody(Operation* modifierOp, Block& body) { SetVector captures; getUsedValuesDefinedAbove(modifierOp->getRegions(), captures); if (llvm::any_of(captures, [](Value value) { - return isa(value.getType()); + return containsQubit(value.getType()); })) { return modifierOp->emitOpError( "body must not capture qubits from above; use only its aliased block " "arguments"); } + SmallVector worklist; + for (Operation& operation : body) { + worklist.push_back(&operation); + } + while (!worklist.empty()) { + Operation* operation = worklist.pop_back_val(); + if (isForbiddenModifierBodyOperation(operation)) { + return modifierOp->emitOpError( + "body must not contain non-unitary operations or access registers"); + } + for (Region& region : operation->getRegions()) { + for (Block& block : region) { + for (Operation& nested : block) { + worklist.push_back(&nested); + } + } + } + } + return success(); } diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.h b/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.h index 31a28044f3..7a3c7209fa 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.h +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.h @@ -31,6 +31,10 @@ namespace qco::detail { [[nodiscard]] LogicalResult verifyModifierBody(Operation* modifierOp, Block& body); +/** @brief Return whether matrix queries may safely recurse through a modifier. + */ +[[nodiscard]] bool isModifierMatrixNestingSupported(Operation* modifierOp); + /** * @brief Return the positions of the qubits that the body of a modifier uses. * diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp index 010d07e506..12cc1401da 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp @@ -37,6 +37,7 @@ #include #include +#include #include #include #include @@ -136,6 +137,12 @@ static Value scaleByExponent(auto param, PowOp op, PatternRewriter& rewriter) { return arith::MulFOp::create(rewriter, op.getLoc(), op.getExponent(), param); } +[[nodiscard]] static bool constantScaleIsFinite(Value parameter, + double factor) { + const auto constant = valueToConstantDouble(parameter); + return constant ? std::isfinite(*constant * factor) : std::abs(factor) <= 1.0; +} + namespace { /// pow(1.0) { U } => inline U @@ -164,6 +171,10 @@ struct ErasePow0 final : OpRewritePattern { if (!exponent || std::abs(*exponent) > PARAMETER_COMPARISON_TOLERANCE) { return failure(); } + if (failed(mqt::hoistSupportingOpsBefore( + *op.getBody(), op, rewriter))) { + return failure(); + } // pow(0) is the identity: thread the input qubits straight to the results. rewriter.replaceOp(op, op.getQubitsIn()); @@ -228,6 +239,14 @@ struct MergeNestedPow final : OpRewritePattern { if (innerPow.getNumQubits() != op.getNumQubits()) { return failure(); } + if (const auto innerExponent = + valueToConstantDouble(innerPow.getExponent())) { + if (!std::isfinite(*innerExponent * *outerExponent)) { + return failure(); + } + } else if (std::abs(*outerExponent) > 1.0) { + return failure(); + } // The inner pow's operands alias the outer pow's block args, possibly in a // different order / subset. Translate them back to the outer pow's operands @@ -239,8 +258,10 @@ struct MergeNestedPow final : OpRewritePattern { }); // Move supporting ops (constants, arithmetic) out of the body so their // Values are accessible from outside and survive PowOp erasure. - mqt::hoistSupportingOpsBefore(*op.getBody(), innerPow.getOperation(), op, - rewriter); + if (failed(mqt::hoistSupportingOpsBefore( + *op.getBody(), innerPow.getOperation(), op, rewriter))) { + return failure(); + } Value merged = scaleByExponent(innerPow.getExponent(), op, rewriter); auto newPow = PowOp::create(rewriter, op.getLoc(), qubits, merged, @@ -298,6 +319,11 @@ struct MoveCtrlOutsidePow final : OpRewritePattern { return mqt::getValueFromBlockArgument(t, outerQubits); }); + if (failed(mqt::hoistSupportingOpsBefore(*op.getBody(), innerCtrlOp, op, + rewriter))) { + return failure(); + } + auto newCtrl = CtrlOp::create( rewriter, op.getLoc(), controls, targets, [&](ValueRange targetArgs) -> SmallVector { @@ -391,9 +417,35 @@ struct FoldPowIntoGate final : OpRewritePattern { return failure(); } + if (auto gate = dyn_cast(innerOp)) { + if (const auto angle = valueToConstantDouble(gate.getTheta()); + angle && !isValidGlobalPhaseAngle(*angle * r)) { + return failure(); + } + if (!valueToConstantDouble(gate.getTheta()) && std::abs(r) > 1.0) { + return failure(); + } + } else if (isa(innerOp)) { + if (!constantScaleIsFinite(bodyUnitary.getParameter(0), r)) { + return failure(); + } + } + if (isa( + innerOp)) { + const double fullTurnScale = r * std::numbers::pi; + if (!std::isfinite(fullTurnScale) || + std::abs(fullTurnScale) > MAX_GLOBAL_PHASE_ANGLE) { + return failure(); + } + } + // Move supporting ops (constants, arithmetic) out of the body so their // Values are accessible from outside and survive PowOp erasure. - mqt::hoistSupportingOpsBefore(*op.getBody(), innerOp, op, rewriter); + if (failed(mqt::hoistSupportingOpsBefore(*op.getBody(), innerOp, op, + rewriter))) { + return failure(); + } const LogicalResult result = TypeSwitch(innerOp) @@ -714,11 +766,13 @@ struct EraseEmptyPow final : OpRewritePattern { using OpRewritePattern::OpRewritePattern; LogicalResult matchAndRewrite(PowOp op, PatternRewriter& rewriter) const override { - if (op.getNumBodyUnitaries() != 0) { + if (llvm::any_of(*op.getBody(), [](Operation& operation) { + return mqt::containsUnitaryOperation(&operation); + })) { return failure(); } - rewriter.replaceOp(op, op.getInputQubits()); + mqt::inlineModifierBody(op, *op.getBody(), op.getInputQubits(), rewriter); return success(); } }; @@ -833,6 +887,10 @@ void PowOp::build(OpBuilder& odsBuilder, OperationState& odsState, Value qubit, } LogicalResult PowOp::verify() { + if (getQubitsIn().size() != getQubitsOut().size()) { + return emitOpError( + "number of input qubits must match the number of output qubits"); + } auto& block = *getBody(); if (failed(detail::verifyModifierBody(getOperation(), block))) { return failure(); @@ -863,6 +921,13 @@ LogicalResult PowOp::verify() { } } + SmallPtrSet uniqueQubitsOut; + for (Value target : blockTerminator->getOperands()) { + if (!uniqueQubitsOut.insert(target).second) { + return emitOpError("duplicate yielded qubit found"); + } + } + return success(); } @@ -874,15 +939,14 @@ void PowOp::getCanonicalizationPatterns(RewritePatternSet& results, } // This structural query deliberately avoids constructing the body matrix or -// running the eigensolver. A true result means all inputs needed to attempt the -// computation are known; getUnitaryMatrix() can still fail for unsupported -// bodies or numerical reasons. +// running the eigensolver. bool PowOp::hasCompileTimeKnownUnitaryMatrix() { + if (!isModifierMatrixSizeSupported(getNumTargets()) || + !detail::isModifierMatrixNestingSupported(getOperation())) { + return false; + } return getExponentValue().has_value() && - all_of(getBody()->getOps(), - [](UnitaryOpInterface op) { - return op.hasCompileTimeKnownUnitaryMatrix(); - }); + hasComposableBodyMatrix(*getBody(), getNumTargets()); } /** @@ -894,14 +958,17 @@ bool PowOp::hasCompileTimeKnownUnitaryMatrix() { * `V` is unitary and `V^{-1} = V^\dagger`; this is verified before use because * the eigensolver does not orthogonalize degenerate eigenspaces. * - * The body matrix `U` comes either from a single inner unitary (e.g. - * `pow(p) { h }`) or, for a composed body (e.g. `pow(p) { h; x }`), from - * @ref composeBodyMatrix over all targets. + * The body matrix `U` comes from @ref composeBodyMatrix over all targets, + * including pass-through targets and exact-spanning inner unitaries. * * @return `U^p`, or `std::nullopt` if the exponent is non-constant, the body is * not fully compile-time known, or `V` is not unitary. */ std::optional PowOp::getUnitaryMatrix() { + if (!isModifierMatrixSizeSupported(getNumTargets()) || + !detail::isModifierMatrixNestingSupported(getOperation())) { + return std::nullopt; + } const auto exponent = getExponentValue(); if (!exponent) { return std::nullopt; @@ -943,23 +1010,37 @@ std::optional PowOp::getUnitaryMatrix() { // Build D^p by raising each eigenvalue to the power p (principal branch). DynamicMatrix powDiagonal(dim); for (std::int64_t i = 0; i < dim; ++i) { - powDiagonal(i, i) = std::pow(eigenvalues[static_cast(i)], p); + const auto eigenvalue = eigenvalues[static_cast(i)]; + Complex powered; + if (isIntegerExponent(p)) { + const auto magnitude = std::abs(eigenvalue); + if (!std::isfinite(magnitude) || + magnitude <= PARAMETER_COMPARISON_TOLERANCE) { + return std::nullopt; + } + powered = std::polar(1.0, scaleAngleByInteger(std::arg(eigenvalue), p)); + } else { + powered = std::pow(eigenvalue, p); + } + if (!std::isfinite(powered.real()) || !std::isfinite(powered.imag())) { + return std::nullopt; + } + powDiagonal(i, i) = powered; } - return v * powDiagonal * v.adjoint(); - }; - - // Single inner unitary (e.g. `pow(p) { h }`, `pow(p) { rz(theta) }`). - if (auto bodyUnitary = - mqt::getSoleBodyUnitary(*getBody())) { - if (const auto targetMatrix = - bodyUnitary.getUnitaryMatrix()) { - return raiseToPow(*targetMatrix); + auto result = v * powDiagonal * v.adjoint(); + for (std::int64_t row = 0; row < dim; ++row) { + for (std::int64_t col = 0; col < dim; ++col) { + const auto value = result(row, col); + if (!std::isfinite(value.real()) || !std::isfinite(value.imag())) { + return std::nullopt; + } + } } - return std::nullopt; - } + return result; + }; - // Composed body (e.g., `pow(p) { h; x }`). + // Compose the complete body so pass-through targets are represented too. if (const auto composed = composeBodyMatrix(*getBody(), getNumTargets())) { return raiseToPow(*composed); } diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp index fa38a0fe89..bccf67bfcc 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp @@ -9,9 +9,9 @@ */ #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" -#include #include #include #include @@ -23,6 +23,8 @@ #include #include +#include +#include using namespace mlir; using namespace mlir::qco; @@ -37,48 +39,43 @@ struct MergeSubsequentBarrier final : OpRewritePattern { LogicalResult matchAndRewrite(BarrierOp op, PatternRewriter& rewriter) const override { - auto qubitsIn = op.getQubitsIn(); - - auto anythingToMerge = false; - DenseMap newQubitsOutMap; - - SmallVector newQubitsIn; - SmallVector indicesToFill; - - for (size_t i = 0; i < qubitsIn.size(); ++i) { - if (isa( - *op.getOutputForInput(qubitsIn[i]).getUsers().begin())) { - anythingToMerge = true; - newQubitsOutMap[i] = qubitsIn[i]; - } else { - newQubitsIn.push_back(qubitsIn[i]); - indicesToFill.push_back(i); - } - } - - if (!anythingToMerge) { + auto outputs = op.getQubitsOut(); + if (outputs.empty()) { return failure(); } - auto newBarrier = BarrierOp::create(rewriter, op.getLoc(), newQubitsIn); - - for (size_t i = 0; i < indicesToFill.size(); ++i) { - newQubitsOutMap[indicesToFill[i]] = newBarrier.getQubitsOut()[i]; + BarrierOp nextBarrier; + for (Value output : outputs) { + if (!output.hasOneUse()) { + return failure(); + } + auto user = dyn_cast(*output.getUsers().begin()); + if (!user || user->getBlock() != op->getBlock() || + (nextBarrier && user != nextBarrier)) { + return failure(); + } + nextBarrier = user; } - SmallVector newQubitsOut; - newQubitsOut.reserve(op.getQubitsIn().size()); - for (size_t i = 0; i < op.getQubitsIn().size(); ++i) { - newQubitsOut.push_back(newQubitsOutMap[i]); + if (nextBarrier.getNumTargets() != outputs.size()) { + return failure(); } - rewriter.replaceOp(op, newQubitsOut); + rewriter.replaceOp(op, op.getQubitsIn()); return success(); } }; } // namespace +LogicalResult BarrierOp::verify() { + if (getQubitsIn().size() != getQubitsOut().size()) { + return emitOpError( + "number of input qubits must match the number of output qubits"); + } + return success(); +} + Value BarrierOp::getInputForOutput(Value output) { if (auto result = dyn_cast(output); result && result.getOwner() == getOperation()) { @@ -111,7 +108,13 @@ void BarrierOp::getCanonicalizationPatterns(RewritePatternSet& results, results.add(context); } -DynamicMatrix BarrierOp::getUnitaryMatrix() { - const auto numQubits = getQubitsIn().size(); - return DynamicMatrix::identity(1LL << numQubits); +bool BarrierOp::hasCompileTimeKnownUnitaryMatrix() { + return isModifierMatrixSizeSupported(getNumTargets()); +} + +std::optional BarrierOp::getUnitaryMatrix() { + if (!hasCompileTimeKnownUnitaryMatrix()) { + return std::nullopt; + } + return DynamicMatrix::identity(static_cast(1ULL << getNumTargets())); } diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp index 7663079248..7478a439cb 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp @@ -77,18 +77,27 @@ struct MergeSubsequentR final : OpRewritePattern { LogicalResult matchAndRewrite(ROp op, PatternRewriter& rewriter) const override { + if (!op.getOutputQubit(0).hasOneUse()) { + return failure(); + } auto nextOp = dyn_cast(*op.getOutputQubit(0).user_begin()); - if (!nextOp) { + if (!nextOp || op->getBlock() != nextOp->getBlock()) { return failure(); } if (!valuesMatchWithinTolerance(op.getPhi(), nextOp.getPhi())) { return failure(); } + if (!constantParameterSumIsFinite(op.getTheta(), nextOp.getTheta())) { + return failure(); + } + rewriter.setInsertionPoint(nextOp); auto newParameter = arith::AddFOp::create(rewriter, op.getLoc(), op.getTheta(), nextOp.getTheta()); - op->setOperand(1, newParameter.getResult()); + rewriter.modifyOpInPlace( + op, [&] { op->setOperand(1, newParameter.getResult()); }); + rewriter.moveOpBefore(op, nextOp); rewriter.replaceOp(nextOp, op.getResult()); return success(); } diff --git a/mlir/lib/Dialect/QCO/IR/QCOUtils.cpp b/mlir/lib/Dialect/QCO/IR/QCOUtils.cpp index 65c395afe2..ef235be81f 100644 --- a/mlir/lib/Dialect/QCO/IR/QCOUtils.cpp +++ b/mlir/lib/Dialect/QCO/IR/QCOUtils.cpp @@ -10,6 +10,7 @@ #include "mlir/Dialect/QCO/QCOUtils.h" +#include "mlir/Dialect/MQT/Utils/Modifiers.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" @@ -17,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -26,13 +28,12 @@ #include #include #include -#include #include -#include #include #include #include +#include namespace mlir::qco { @@ -52,7 +53,9 @@ LogicalResult verifyLinearity(Operation* root) { } DenseSet staticIndices; - const auto walkResult = root->walk([&](Operation* op) { + SmallVector operations{root}; + for (size_t next = 0; next < operations.size(); ++next) { + auto* op = operations[next]; if (auto staticOp = dyn_cast(op)) { if (entryPoint && (entryPoint.isDeclaration() || @@ -61,33 +64,35 @@ LogicalResult verifyLinearity(Operation* root) { << "expected static qubits in the entry block of program entry " "function @" << entryPoint.getSymName(); - return WalkResult::interrupt(); + return failure(); } if (!staticIndices.insert(staticOp.getIndex()).second) { staticOp.emitError() << "expected each static qubit index to identify one linear " "value, but found duplicate index " << staticOp.getIndex(); - return WalkResult::interrupt(); + return failure(); } } for (auto result : op->getResults()) { if (failed(verifyLinearValue(result))) { - return WalkResult::interrupt(); + return failure(); } } for (Region& region : op->getRegions()) { for (Block& block : region) { for (auto argument : block.getArguments()) { if (failed(verifyLinearValue(argument))) { - return WalkResult::interrupt(); + return failure(); } } + for (auto& nestedOp : block) { + operations.push_back(&nestedOp); + } } } - return WalkResult::advance(); - }); - return walkResult.wasInterrupted() ? failure() : success(); + } + return success(); } /// Returns the wire index for @p wire in @p wireIds, or `std::nullopt` if @@ -145,16 +150,111 @@ embedUnitaryInBody(UnitaryOpInterface unitary, size_t numTargets, return matrix->embedInNqubit(numTargets, *q0, *q1); } +bool hasComposableBodyMatrix(Block& block, size_t numTargets) { + if (!isModifierMatrixSizeSupported(numTargets) || + block.getNumArguments() != numTargets || + block.getTerminator()->getNumOperands() != numTargets) { + return false; + } + + if (auto sole = mqt::getSoleBodyUnitary(block); + sole && sole.getNumQubits() > 2) { + if (sole.getNumQubits() != numTargets || + !sole.hasCompileTimeKnownUnitaryMatrix()) { + return false; + } + const auto inputsMatch = + llvm::all_of(llvm::enumerate(sole.getInputQubits()), [&](auto indexed) { + return indexed.value() == block.getArgument(indexed.index()); + }); + const auto outputsMatch = llvm::all_of( + llvm::zip_equal(sole.getOutputQubits(), + block.getTerminator()->getOperands()), + [](auto pair) { return std::get<0>(pair) == std::get<1>(pair); }); + return inputsMatch && outputsMatch; + } + + DenseMap wireIds; + for (size_t i = 0; i < numTargets; ++i) { + wireIds[block.getArgument(i)] = i; + } + + for (Operation& op : block.without_terminator()) { + const bool handled = + TypeSwitch(&op) + .Case([&](BarrierOp barrier) { + propagateWireIds(barrier, wireIds); + return true; + }) + .Case([](GPhaseOp gphase) { + return cast(gphase.getOperation()) + .hasCompileTimeKnownUnitaryMatrix(); + }) + .Case([&](UnitaryOpInterface unitary) { + if (unitary.getNumQubits() == 0 || unitary.getNumQubits() > 2 || + !unitary.hasCompileTimeKnownUnitaryMatrix() || + llvm::any_of(unitary.getInputQubits(), [&](Value input) { + return !wireIds.contains(input); + })) { + return false; + } + propagateWireIds(unitary, wireIds); + return true; + }) + .Default([&](Operation* unknown) { + const auto usesQubit = [](Value value) { + return isLinearQubitType(value.getType()); + }; + return !mqt::containsUnitaryOperation( + unknown) && + !llvm::any_of(unknown->getOperands(), usesQubit) && + !llvm::any_of(unknown->getResults(), usesQubit); + }); + if (!handled) { + return false; + } + } + + for (auto [index, yielded] : + llvm::enumerate(block.getTerminator()->getOperands())) { + const auto wire = lookupWireId(wireIds, yielded); + if (!wire.has_value() || *wire != index) { + return false; + } + } + return true; +} + std::optional composeBodyMatrix(Block& block, size_t numTargets) { - if (numTargets == 0 || numTargets > kMaxModifierTargetQubits || - block.getNumArguments() != numTargets) { + if (!hasComposableBodyMatrix(block, numTargets)) { return std::nullopt; } + if (auto sole = mqt::getSoleBodyUnitary(block); + sole && sole.getNumQubits() > 2 && sole.getNumQubits() == numTargets) { + const auto inputsMatch = + llvm::all_of(llvm::enumerate(sole.getInputQubits()), [&](auto indexed) { + return indexed.value() == block.getArgument(indexed.index()); + }); + const auto outputsMatch = llvm::all_of( + llvm::zip_equal(sole.getOutputQubits(), + block.getTerminator()->getOperands()), + [](auto pair) { return std::get<0>(pair) == std::get<1>(pair); }); + if (!inputsMatch || !outputsMatch) { + return std::nullopt; + } + auto matrix = sole.getUnitaryMatrix(); + const auto expectedDim = static_cast(1ULL << numTargets); + if (!matrix || matrix->rows() != expectedDim || + matrix->cols() != expectedDim) { + return std::nullopt; + } + return matrix; + } + std::optional acc; Complex global{1.0, 0.0}; - bool found = false; DenseMap wireIds; for (size_t i = 0; i < numTargets; ++i) { @@ -174,7 +274,6 @@ std::optional composeBodyMatrix(Block& block, return false; } global *= matrix->value; - found = true; return true; }) .Case([&](UnitaryOpInterface unitary) { @@ -187,15 +286,16 @@ std::optional composeBodyMatrix(Block& block, } else { acc->premultiplyBy(*embedded); } - found = true; propagateWireIds(unitary, wireIds); return true; }) .Default([&](Operation* unknown) { const auto usesQubit = [](Value value) { - return isa(value.getType()); + return isLinearQubitType(value.getType()); }; - return !llvm::any_of(unknown->getOperands(), usesQubit) && + return !mqt::containsUnitaryOperation( + unknown) && + !llvm::any_of(unknown->getOperands(), usesQubit) && !llvm::any_of(unknown->getResults(), usesQubit); }); @@ -204,8 +304,12 @@ std::optional composeBodyMatrix(Block& block, } } - if (!found) { - return std::nullopt; + for (auto [index, yielded] : + llvm::enumerate(block.getTerminator()->getOperands())) { + const auto wire = lookupWireId(wireIds, yielded); + if (!wire.has_value() || *wire != index) { + return std::nullopt; + } } if (!acc.has_value()) { acc = DynamicMatrix::identity(static_cast(1ULL << numTargets)); diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index 621df3444e..6b23549f1d 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -15,6 +15,7 @@ add_mlir_library( PUBLIC MQTCompilerTarget PRIVATE + MLIRQCDialect MLIRQCODialect MLIRQCOUtils MLIRQTensorUtils diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp index a8e26f1e34..949ce1f76e 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp @@ -14,6 +14,7 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Support/OperationUtils.h" #include #include // IWYU pragma: keep (Passes.h.inc) @@ -1163,15 +1164,15 @@ static void appendSp22PRx(CircuitPlan& plan, size_t m, double sign) { /// SP22 Theorem 2: expand `Q_m` into single-controlled CRX only. static CircuitPlan buildSp22Q(size_t m) { CircuitPlan q; - if (m < 2) { - return q; // Q_1 = Q_0 = I - } - appendSp22PRx(q, m - 1, 1.0); - q.append({.kind = PlanOpKind::CRX, - .wires = {0, m - 1}, - .angle = std::ldexp(K_PI, -static_cast(m - 2))}); - appendPlanOps(q, buildSp22Q(m - 1)); - appendSp22PRx(q, m - 1, -1.0); + for (size_t level = m; level > 1; --level) { + appendSp22PRx(q, level - 1, 1.0); + q.append({.kind = PlanOpKind::CRX, + .wires = {0, level - 1}, + .angle = std::ldexp(K_PI, -static_cast(level - 2))}); + } + for (size_t level = 2; level <= m; ++level) { + appendSp22PRx(q, level - 1, -1.0); + } return q; } @@ -1335,6 +1336,10 @@ struct DecomposeControlledGatePattern final : OpRewritePattern { // MCSWAP(C, a, b) = CX(a,b) · MCX(C ∪ {b}, a) · CX(a,b). if (op.getNumTargets() == 2 && isa(inner.getOperation())) { + if (failed(mqt::hoistSupportingOpsBefore( + *op.getBody(), inner.getOperation(), op, rewriter))) { + return failure(); + } rewriter.setInsertionPoint(op); rewriter.replaceOp(op, synthesizeControlledSwap( rewriter, op.getLoc(), op.getControlsIn(), @@ -1350,6 +1355,10 @@ struct DecomposeControlledGatePattern final : OpRewritePattern { return failure(); } + if (failed(mqt::hoistSupportingOpsBefore( + *op.getBody(), inner.getOperation(), op, rewriter))) { + return failure(); + } ControlledTarget gate = spec->gate; // A compile-time phase of +/- pi is exactly Z; route it through the // multi-controlled-Z path (elementary at 3–4 qubits, relative-phase / Vale @@ -1424,6 +1433,12 @@ struct DecomposeMultiControlled final return; } + constexpr size_t maxRegionNesting = 64; + if (failed(verifyRegionNestingDepth(getOperation(), maxRegionNesting))) { + signalPassFailure(); + return; + } + RewritePatternSet patterns(&getContext()); patterns.add( &getContext(), minQubits); diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 4863597595..59c4f32dd0 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -12,15 +12,18 @@ #include "mlir/Compiler/Target.h" #include "mlir/Dialect/MQT/IR/MQTDialect.h" +#include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QCO/Utils/Drivers.h" #include "mlir/Dialect/QCO/Utils/Graph.h" #include "mlir/Dialect/QCO/Utils/Layout.h" #include "mlir/Dialect/QCO/Utils/WireIterator.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" #include "mlir/Dialect/QTensor/Utils/TensorIterator.h" +#include "mlir/Support/OperationUtils.h" #include #include @@ -37,11 +40,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -49,6 +54,7 @@ #include #include #include +#include #include #include #include @@ -346,36 +352,85 @@ struct MappingPass : impl::MappingPassBase { protected: void runOnOperation() override { - assert(alpha > 0 && "expected alpha > 0"); - assert(niterations > 0 && "expected niterations > 0"); - assert(ntrials > 0 && "expected ntrials > 0"); + constexpr size_t maxSearchOption = 4096; + auto mod = getOperation(); + if (!std::isfinite(alpha) || !(alpha > 0)) { + mod.emitError() << "requires finite alpha > 0"; + signalPassFailure(); + return; + } + if (!std::isfinite(lambda)) { + mod.emitError() << "requires finite lambda"; + signalPassFailure(); + return; + } + if (nlookahead > maxSearchOption) { + mod.emitError() << "requires nlookahead <= " << maxSearchOption; + signalPassFailure(); + return; + } + if (niterations == 0 || niterations > maxSearchOption) { + mod.emitError() << "requires 0 < niterations <= " << maxSearchOption; + signalPassFailure(); + return; + } + if (ntrials == 0 || ntrials > maxSearchOption) { + mod.emitError() << "requires 0 < ntrials <= " << maxSearchOption; + signalPassFailure(); + return; + } if (!target) { - llvm::reportFatalUsageError("No compiler target specified!"); + mod.emitError() << "requires a compiler target"; + signalPassFailure(); + return; } IRRewriter rewriter(&getContext()); - auto mod = getOperation(); - auto func = mqt::getEntryPoint(mod); - if (!func) { + if (failed(mqt::verifyProgramMetadata(mod))) { + signalPassFailure(); + return; + } + + auto entryPoint = mqt::getEntryPoint(mod); + if (!entryPoint) { mod.emitError() << "does not contain an entry point function"; signalPassFailure(); return; } - auto comp = discoverComputation(func); + if (failed(qco::verifyLinearity(entryPoint)) || + failed(validateMappingInput(entryPoint))) { + signalPassFailure(); + return; + } + + // Include the module and function operations in the shared depth bound. + if (failed(verifyRegionNestingDepth(mod, maxRegionNesting + 2))) { + signalPassFailure(); + return; + } + + OwningOpRef transformedModule(mod.clone()); + auto transformedFunc = mqt::getEntryPoint(*transformedModule); + auto comp = discoverComputation(transformedFunc); if (failed(comp)) { signalPassFailure(); return; } - auto& body = func.getFunctionBody(); + // A classical-only entry point has nothing to place or route. + if (comp->wires.empty()) { + return; + } + + auto& body = transformedFunc.getFunctionBody(); auto& wires = comp->wires; auto& infos = comp->infos; if (wires.size() > target->numQubits()) { - func.emitError() + transformedFunc.emitError() << "requires " + Twine(wires.size()) + " qubits. However, the architecture only supports " + Twine(target->numQubits()) + " qubits."; @@ -385,7 +440,7 @@ struct MappingPass : impl::MappingPassBase { auto layout = generateLayout(wires, infos); if (failed(layout)) { - func->emitError() << "failed to refine random initial layouts."; + transformedFunc.emitError() << "failed to refine random initial layouts."; signalPassFailure(); return; } @@ -399,20 +454,308 @@ struct MappingPass : impl::MappingPassBase { const auto routeRes = route(bundle, &rewriter); if (failed(routeRes)) { - func.emitError() << "failed to map the function"; + transformedFunc.emitError() << "failed to map the function"; signalPassFailure(); return; } - // Collect statistics. - const auto stats = *routeRes; - numSwaps += stats.nswaps; - // Fix SSA Dominance issues. llvm::for_each(body.getBlocks(), [](Block& b) { sortTopologically(&b); }); + + if (failed(verify(transformedFunc)) || + failed(qco::verifyLinearity(transformedFunc))) { + transformedFunc.emitError() << "target mapping produced invalid IR"; + signalPassFailure(); + return; + } + + entryPoint.getFunctionBody().takeBody(transformedFunc.getFunctionBody()); + numSwaps += routeRes->nswaps; } private: + static constexpr size_t maxStructuredNesting = 64; + static constexpr size_t maxRegionNesting = 64; + + /// Return whether a type carries value-semantics quantum state. + static bool isQuantumType(Type type) { + if (isa(type)) { + return true; + } + const auto shaped = dyn_cast(type); + return shaped && isa(shaped.getElementType()); + } + + /// Return whether a type is a tensor carrying value-semantics quantum state. + static bool isQuantumTensorType(Type type) { + const auto tensor = dyn_cast(type); + return tensor && isa(tensor.getElementType()); + } + + struct WhileTensorTrace { + Value conditionArgument; + Value result; + }; + + /// Follow one tensor init through a while's before region to its result. + static FailureOr traceWhileTensorInit(scf::WhileOp whileOp, + size_t initIndex, + size_t nesting = 0) { + if (nesting > maxStructuredNesting) { + whileOp.emitError() << "target mapping supports at most " + << maxStructuredNesting + << " nested quantum structured operations"; + return failure(); + } + + Block* beforeBody = whileOp.getBeforeBody(); + if (initIndex >= beforeBody->getNumArguments()) { + whileOp.emitError( + "target mapping cannot match an scf.while tensor init to its " + "before-region argument"); + return failure(); + } + + Value current = beforeBody->getArgument(initIndex); + DenseSet visited; + while (visited.insert(current).second) { + if (!current.hasOneUse()) { + whileOp.emitError( + "target mapping requires linear scf.while tensor flow"); + return failure(); + } + + OpOperand& use = *current.use_begin(); + Operation* user = use.getOwner(); + if (auto condition = dyn_cast(user)) { + if (condition->getParentOp() != whileOp) { + whileOp.emitError( + "target mapping requires every quantum tensor scf.while init " + "to reach its condition"); + return failure(); + } + + auto conditionArgs = condition.getArgs(); + const auto conditionIt = llvm::find(conditionArgs, current); + if (conditionIt == conditionArgs.end()) { + whileOp.emitError( + "target mapping cannot match an scf.while tensor value to a " + "condition result"); + return failure(); + } + const auto resultIndex = static_cast( + std::distance(conditionArgs.begin(), conditionIt)); + if (resultIndex >= whileOp.getNumResults() || + !isQuantumTensorType(whileOp.getResult(resultIndex).getType())) { + whileOp.emitError( + "target mapping cannot match an scf.while tensor value to a " + "quantum tensor result"); + return failure(); + } + return WhileTensorTrace{current, whileOp.getResult(resultIndex)}; + } + + Value next; + if (auto extract = dyn_cast(user)) { + next = extract.getOutTensor(); + } else if (auto insert = dyn_cast(user)) { + next = insert.getResult(); + } else if (auto forOp = dyn_cast(user)) { + next = forOp.getTiedLoopResult(&use); + } else if (auto nestedWhile = dyn_cast(user)) { + auto nestedTrace = traceWhileTensorInit( + nestedWhile, use.getOperandNumber(), nesting + 1); + if (failed(nestedTrace)) { + return failure(); + } + next = nestedTrace->result; + } else if (auto ifOp = dyn_cast(user)) { + next = ifOp.getTiedResult(&use); + } else if (auto switchOp = dyn_cast(user)) { + next = switchOp.getTiedResult(&use); + } + + if (!next || !isQuantumTensorType(next.getType())) { + whileOp.emitError( + "target mapping requires every quantum tensor scf.while init to " + "reach its condition"); + return failure(); + } + current = next; + } + + whileOp.emitError("target mapping found a cyclic scf.while tensor flow"); + return failure(); + } + + /// Validate the assumptions used by TensorIterator for scf.while tensors. + static LogicalResult validateWhileTensorFlow(scf::WhileOp whileOp) { + auto inits = whileOp.getInits(); + auto results = whileOp.getResults(); + const auto numValues = std::max(inits.size(), results.size()); + for (size_t index = 0; index < numValues; ++index) { + const bool initIsQubit = + index < inits.size() && isa(inits[index].getType()); + const bool resultIsQubit = + index < results.size() && isa(results[index].getType()); + if (initIsQubit != resultIsQubit) { + return whileOp.emitError( + "target mapping requires positional scalar-qubit scf.while " + "inputs and results"); + } + } + + auto condition = + dyn_cast(whileOp.getBeforeBody()->getTerminator()); + if (!condition) { + return whileOp.emitError( + "target mapping requires scf.while before regions to terminate with " + "scf.condition"); + } + + DenseSet conditionTensors; + for (Value argument : condition.getArgs()) { + if (isQuantumTensorType(argument.getType())) { + conditionTensors.insert(argument); + } + } + + DenseSet reachedConditionTensors; + for (auto [index, init] : llvm::enumerate(whileOp.getInits())) { + if (!isQuantumTensorType(init.getType())) { + continue; + } + auto trace = traceWhileTensorInit(whileOp, index); + if (failed(trace)) { + return failure(); + } + if (!conditionTensors.contains(trace->conditionArgument) || + !reachedConditionTensors.insert(trace->conditionArgument).second) { + return whileOp.emitError( + "target mapping requires one-to-one scf.while tensor flow"); + } + } + + if (reachedConditionTensors.size() != conditionTensors.size()) { + return whileOp.emitError( + "target mapping requires every quantum tensor scf.while condition " + "result to originate from an init"); + } + return success(); + } + + /// Return whether Mapping knows how to follow quantum values through an op. + static bool isSupportedQuantumCarrier(Operation* op) { + return isa(op) || + isa(op); + } + + /// Return whether Mapping recursively routes an operation's regions. + static bool isSupportedStructuredOperation(Operation* op) { + return isa(op); + } + + /// Validate carrier support and bound recursive structured-region routing. + static LogicalResult validateMappingInput(func::FuncOp func) { + if (!func.getBody().hasOneBlock()) { + return func.emitError( + "target mapping supports only single-block entry functions"); + } + if (llvm::any_of(func.getArgumentTypes(), isQuantumType) || + llvm::any_of(func.getFunctionType().getResults(), isQuantumType)) { + return func.emitError( + "target mapping does not support quantum function arguments or " + "results; allocate qubits in the entry function body"); + } + + struct PendingOperation { + Operation* op; + size_t structuredNesting; + size_t regionNesting; + Operation* unsupportedRegion; + }; + + SmallVector pending; + for (Block& block : func.getBody()) { + for (Operation& op : block) { + pending.push_back({&op, 0, 0, nullptr}); + } + } + + while (!pending.empty()) { + auto [op, structuredNesting, regionNesting, unsupportedRegion] = + pending.pop_back_val(); + if (isa(op)) { + return op->emitError() + << "target mapping requires dynamically allocated qubits; " + "static qubits are already placed"; + } + const bool carriesQuantum = + llvm::any_of(op->getOperandTypes(), isQuantumType) || + llvm::any_of(op->getResultTypes(), isQuantumType); + if (carriesQuantum && !isSupportedQuantumCarrier(op)) { + return op->emitError() + << "target mapping does not support quantum values carried by " + << op->getName(); + } + + if (unsupportedRegion && carriesQuantum) { + return unsupportedRegion->emitError() + << "target mapping does not support quantum operations nested " + "in " + << unsupportedRegion->getName(); + } + + if (auto whileOp = dyn_cast(op); + whileOp && failed(validateWhileTensorFlow(whileOp))) { + return failure(); + } + + const bool structured = + carriesQuantum && isSupportedStructuredOperation(op); + const size_t childStructuredNesting = + structuredNesting + static_cast(structured); + if (childStructuredNesting > maxStructuredNesting) { + return op->emitError() + << "target mapping supports at most " << maxStructuredNesting + << " nested quantum structured operations"; + } + const size_t childRegionNesting = + regionNesting + static_cast(op->getNumRegions() != 0); + if (childRegionNesting > maxRegionNesting) { + return op->emitError() + << "target mapping supports at most " << maxRegionNesting + << " nested operations with regions"; + } + + // Mapping treats a unitary, including a modifier, as one routing node. + // Its region is the implementation of that node rather than nested + // structured control flow to route independently. + if (isa(op)) { + continue; + } + + Operation* childUnsupportedRegion = unsupportedRegion; + if (!childUnsupportedRegion && op->getNumRegions() != 0 && + !isSupportedStructuredOperation(op) && !isa(op)) { + childUnsupportedRegion = op; + } + for (Region& region : op->getRegions()) { + for (Block& block : region) { + for (Operation& nested : block) { + pending.push_back({&nested, childStructuredNesting, + childRegionNesting, childUnsupportedRegion}); + } + } + } + } + + return success(); + } + /// Return the qubit values in `values`, preserving their relative order. static SmallVector getQubitValues(ValueRange values) { return to_vector(llvm::make_filter_range( @@ -569,26 +912,34 @@ struct MappingPass : impl::MappingPassBase { static FailureOr discoverComputation(func::FuncOp func) { Computation computation; - const auto discovery = func.walk([&](Operation* op) { + SmallVector operations; + for (Block& block : func.getBody()) { + for (Operation& operation : block) { + operations.push_back(&operation); + } + } + for (size_t next = 0; next < operations.size(); ++next) { + Operation* op = operations[next]; if (auto unitary = dyn_cast(op)) { - if (isa(op)) { - return WalkResult::advance(); - } - if (unitary.getNumQubits() > 2) { + if (!isa(op) && unitary.getNumQubits() > 2) { unitary.emitError() << "cannot route an operation acting on " << unitary.getNumQubits() << " qubits; decompose it to one- and two-qubit operations " "first"; - return WalkResult::interrupt(); + return failure(); } - computation.hasTwoQubitOperations |= unitary.getNumQubits() == 2; + computation.hasTwoQubitOperations |= + !isa(op) && unitary.getNumQubits() == 2; } - if (!isa(op)) { - return WalkResult::advance(); - } - if (op->getParentRegion() == &func.getFunctionBody()) { + if (isa(op)) { + if (op->getParentRegion() != &func.getFunctionBody()) { + op->emitError() + << "target mapping requires dynamic qubit allocations in the " + "entry function body"; + return failure(); + } TypeSwitch(op) .Case([&](AllocOp alloc) { computation.scalarAllocations.emplace_back(alloc); @@ -597,17 +948,15 @@ struct MappingPass : impl::MappingPassBase { computation.tensorAllocations.emplace_back( TensorAllocation{.allocation = alloc}); }); - return WalkResult::advance(); } - op->emitError() - << "target mapping requires dynamic qubit allocations in the entry " - "function body"; - return WalkResult::interrupt(); - }); - - if (discovery.wasInterrupted()) { - return failure(); + for (Region& region : op->getRegions()) { + for (Block& block : region) { + for (Operation& nested : block) { + operations.push_back(&nested); + } + } + } } for (auto alloc : computation.scalarAllocations) { @@ -1172,8 +1521,7 @@ struct MappingPass : impl::MappingPassBase { continue; } - if (op->getNumRegions() > 0 && visited.insert(op).second) { - assert((isa(op))); + if (isSupportedStructuredOperation(op) && visited.insert(op).second) { composites.emplace_back(op, indices); continue; } @@ -1203,9 +1551,9 @@ struct MappingPass : impl::MappingPassBase { /// adding operands for indices not in the composite's index set. Returns a /// patch with the updated wire mapping which preserves the parent's wire /// infos and layout. - RoutingBundle::Patch place(CompositeUnitary& composite, - const RoutingBundle& parent, - IRRewriter& rewriter) { + FailureOr place(CompositeUnitary& composite, + const RoutingBundle& parent, + IRRewriter& rewriter) { DenseSet included; // Already included indices. included.reserve(composite.indices.size()); @@ -1232,16 +1580,18 @@ struct MappingPass : impl::MappingPassBase { return it.qubit(); })); - composite = CompositeUnitary{ - .op = TypeSwitch(composite.op) - .Case( - [&](auto cfOp) { return extend(cfOp, addons, rewriter); }) - .Default([](Operation* op) { - report_fatal_error("place: unhandled op: " + - op->getName().getStringRef()); - return nullptr; - }), - .indices = allIndices}; + Operation* originalOp = composite.op; + Operation* extendedOp = + TypeSwitch(originalOp) + .Case( + [&](auto cfOp) { return extend(cfOp, addons, rewriter); }) + .Default([](Operation*) { return nullptr; }); + if (extendedOp == nullptr) { + originalOp->emitError( + "target mapping cannot place this region operation"); + return failure(); + } + composite = CompositeUnitary{.op = extendedOp, .indices = allIndices}; auto results = composite.op->getResults(); @@ -1563,7 +1913,10 @@ struct MappingPass : impl::MappingPassBase { for (auto& composite : composites) { if constexpr (Mode == RoutingMode::Hot) { auto patch = place(composite, bundle, *rewriter); - bundle.applyPatch(std::move(patch)); + if (failed(patch)) { + return failure(); + } + bundle.applyPatch(std::move(*patch)); } auto res = dispatch(composite, bundle, rewriter); @@ -1602,7 +1955,19 @@ struct MappingPass : impl::MappingPassBase { // must ensure the insertion point is before the multi-qubit gates. for (auto& it : wires) { - std::advance(it, it == std::default_sentinel ? -2 : -1); + if (it != std::default_sentinel) { + std::advance(it, -1); + continue; + } + std::advance(it, -2); + // Keep a terminal irreversible suffix after routing SWAPs. Moving a + // logical state through a SWAP and then measuring/resetting that + // state is equivalent, while routing the collapsed/reset state would + // introduce a mid-circuit irreversible operation unnecessarily. + while (it.operation() != nullptr && + isa(it.operation())) { + std::advance(it, -1); + } } } diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index 8c0e80d7b1..d475e9200d 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -8,13 +8,17 @@ * Licensed under the MIT License */ +#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Transforms/GlobalPhaseNormalization.h" +#include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include "mlir/Dialect/QCO/Utils/WireIterator.h" +#include "mlir/Support/OperationUtils.h" #include // IWYU pragma: keep (Passes.h.inc) #include // IWYU pragma: keep (Passes.h.inc) @@ -201,6 +205,17 @@ struct FuseSingleQubitUnitaryRunsPass final return; } + constexpr size_t maxRegionNesting = 64; + if (failed(verifyRegionNestingDepth(moduleOp, maxRegionNesting))) { + signalPassFailure(); + return; + } + if (failed(mqt::verifyProgramMetadata(moduleOp)) || + failed(qco::verifyLinearity(moduleOp))) { + signalPassFailure(); + return; + } + RewritePatternSet compositionPatterns(&getContext()); decomposition::populateParameterizedSingleQubitRunCompositionPatterns( compositionPatterns, *parsed); diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index 0503a95462..5fd9c1ca97 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -9,7 +9,10 @@ */ #include "mlir/Compiler/Target.h" +#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Transforms/GlobalPhaseNormalization.h" +#include "mlir/Dialect/MQT/Utils/Modifiers.h" +#include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" @@ -18,11 +21,14 @@ #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" +#include "mlir/Support/OperationUtils.h" #include +#include #include #include // IWYU pragma: keep (Passes.h.inc) #include +#include #include #include #include @@ -35,7 +41,6 @@ #include #include #include -#include #include #include @@ -60,6 +65,21 @@ struct FusableTwoQubitRun { } // namespace +static Block* getModifierBody(Operation* operation) { + if (!isa(operation)) { + return nullptr; + } + return &operation->getRegion(0).front(); +} + +static bool hasModifierSupportingOps(Operation* operation) { + Block* body = getModifierBody(operation); + return body != nullptr && + llvm::any_of(body->without_terminator(), [](Operation& bodyOp) { + return mqt::containsSupportingOperation(&bodyOp); + }); +} + // --- Run membership ------------------------------------------------------- // /// Whether `op` is nested under a modifier body. Such unitaries are handled @@ -97,7 +117,8 @@ static bool assignTwoQubitOpMatrix(Operation* op, Matrix4x4& matrix) { static std::optional oneQubitRunMemberMatrix(UnitaryOpInterface unitary) { if (!unitary || !unitary.isSingleQubit() || - !isWalkableUnitaryShell(unitary.getOperation())) { + !isWalkableUnitaryShell(unitary.getOperation()) || + hasModifierSupportingOps(unitary.getOperation())) { return std::nullopt; } Matrix2x2 matrix; @@ -111,7 +132,8 @@ oneQubitRunMemberMatrix(UnitaryOpInterface unitary) { static std::optional twoQubitRunMemberMatrix(UnitaryOpInterface unitary) { if (!unitary || !unitary.isTwoQubit() || - !isWalkableUnitaryShell(unitary.getOperation())) { + !isWalkableUnitaryShell(unitary.getOperation()) || + hasModifierSupportingOps(unitary.getOperation())) { return std::nullopt; } Matrix4x4 matrix; @@ -126,8 +148,9 @@ twoQubitRunMemberMatrix(UnitaryOpInterface unitary) { /// The sole walkable one- or two-qubit consumer of `wire`, or a null interface. /// `wire` is single-use by qubit linearity. static UnitaryOpInterface uniqueUnitaryUser(Value wire) { - assert(wire.hasOneUse() && - "qubit values are single-use, so a run tail has exactly one user"); + if (!wire.hasOneUse()) { + return {}; + } auto unitary = dyn_cast(*wire.user_begin()); if (!unitary || !isWalkableUnitaryShell(unitary.getOperation()) || (!unitary.isSingleQubit() && !unitary.isTwoQubit())) { @@ -161,9 +184,9 @@ static Operation* twoQubitGateAtEndOfOneQChain(Value wire) { static bool feedsFromSameTwoQubitRun(UnitaryOpInterface op) { Value in0 = op.getInputQubit(0); Value in1 = op.getInputQubit(1); - assert(in0.hasOneUse() && in1.hasOneUse() && - "qubit values are single-use, so a run member consumes each input " - "exactly once"); + if (!in0.hasOneUse() || !in1.hasOneUse()) { + return false; + } Operation* gate0 = twoQubitGateAtEndOfOneQChain(in0); Operation* gate1 = twoQubitGateAtEndOfOneQChain(in1); return gate0 != nullptr && gate0 == gate1; @@ -270,7 +293,7 @@ static void eraseFusableRun(RewriterBase& rewriter, /// its two-qubit operation count. static bool fuseTwoQubitGateRun(IRRewriter& rewriter, UnitaryOpInterface head, const Matrix4x4& headMatrix, - const CompilerTarget::SynthesisBasis basis) { + CompilerTarget::SynthesisBasis basis) { FusableTwoQubitRun run = scanFusableTwoQubitRun(head, headMatrix); if (run.ops.size() < 2) { return false; @@ -304,57 +327,89 @@ namespace { struct SynthesisPlan { Operation* firstNeed = nullptr; Operation* matrixUnavailable = nullptr; + Operation* supportNotHoistable = nullptr; SmallVector operations; }; } // namespace +static SmallVector collectOperationsPostorder(Operation* root) { + SmallVector worklist{root}; + SmallVector reversePostorder; + while (!worklist.empty()) { + Operation* operation = worklist.pop_back_val(); + reversePostorder.push_back(operation); + for (Region& region : operation->getRegions()) { + for (Block& block : region) { + for (Operation& nested : block) { + worklist.push_back(&nested); + } + } + } + } + return llvm::to_vector(llvm::reverse(reversePostorder)); +} + static SynthesisPlan planTargetSynthesis(Operation* root, const CompilerTarget& target) { SynthesisPlan plan; - root->walk([&](Operation* operation) { + for (Operation* operation : collectOperationsPostorder(root)) { auto unitary = dyn_cast(operation); if (!unitary || !isWalkableUnitaryShell(operation) || (unitary.getNumQubits() != 1 && unitary.getNumQubits() != 2)) { - return WalkResult::advance(); + continue; } if (!requiresTargetSynthesis(operation, target)) { - return WalkResult::advance(); + continue; } if (plan.firstNeed == nullptr) { plan.firstNeed = operation; } + if (Block* body = getModifierBody(operation); + body != nullptr && + !mqt::canHoistSupportingOps(*body)) { + plan.supportNotHoistable = operation; + break; + } + if (unitary.isSingleQubit()) { Matrix2x2 matrix; if (unitary.getUnitaryMatrix2x2(matrix) || decomposition::canSynthesizeParameterizedUnitary1Q(operation)) { plan.operations.emplace_back(operation); - return WalkResult::advance(); + continue; } } else { Matrix4x4 matrix; if (assignTwoQubitOpMatrix(operation, matrix)) { plan.operations.emplace_back(operation); - return WalkResult::advance(); + continue; } } plan.matrixUnavailable = operation; - return WalkResult::interrupt(); - }); + break; + } return plan; } -static void lowerTargetOperation(IRRewriter& rewriter, UnitaryOpInterface op, - const CompilerTarget::SynthesisBasis basis) { - Operation* const operation = op.getOperation(); +static LogicalResult +lowerTargetOperation(IRRewriter& rewriter, UnitaryOpInterface op, + CompilerTarget::SynthesisBasis basis) { + Operation* operation = op.getOperation(); + if (Block* body = getModifierBody(operation); + body != nullptr && + failed(mqt::hoistSupportingOpsBefore(*body, operation, + rewriter))) { + return failure(); + } rewriter.setInsertionPoint(operation); if (op.isSingleQubit()) { Matrix2x2 matrix; if (!op.getUnitaryMatrix2x2(matrix)) { decomposition::synthesizeParameterizedUnitary1Q(rewriter, operation, basis.singleQubit); - return; + return success(); } const auto synthesized = decomposition::synthesizeUnitary1QEuler( rewriter, operation->getLoc(), op.getInputQubit(0), matrix, @@ -366,7 +421,7 @@ static void lowerTargetOperation(IRRewriter& rewriter, UnitaryOpInterface op, decomposition::emitGPhaseIfNeeded(rewriter, operation->getLoc(), synthesized->globalPhase); rewriter.replaceOp(operation, synthesized->qubit); - return; + return success(); } Matrix4x4 matrix; @@ -388,6 +443,7 @@ static void lowerTargetOperation(IRRewriter& rewriter, UnitaryOpInterface op, synthesized.globalPhase); rewriter.replaceOp(operation, ValueRange{synthesized.qubit0, synthesized.qubit1}); + return success(); } static LogicalResult fuseTwoQubitGates(ModuleOp moduleOp) { @@ -396,13 +452,13 @@ static LogicalResult fuseTwoQubitGates(ModuleOp moduleOp) { .entangler = CompilerTarget::GateKind::CZ}; SmallVector runHeads; - moduleOp.walk([&](Operation* operation) { + for (Operation* operation : collectOperationsPostorder(moduleOp)) { auto unitary = dyn_cast(operation); const auto matrix = twoQubitRunMemberMatrix(unitary); if (matrix && !feedsFromSameTwoQubitRun(unitary)) { runHeads.emplace_back(operation); } - }); + } bool changed = false; IRRewriter rewriter(moduleOp.getContext()); @@ -426,12 +482,22 @@ struct FuseTwoQubitGatesPass final MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(FuseTwoQubitGatesPass) void getDependentDialects(DialectRegistry& registry) const override { - registry.insert(); + registry.insert(); } protected: void runOnOperation() override { ModuleOp moduleOp = getOperation(); + constexpr size_t maxRegionNesting = 64; + if (failed(verifyRegionNestingDepth(moduleOp, maxRegionNesting))) { + signalPassFailure(); + return; + } + if (failed(mqt::verifyProgramMetadata(moduleOp)) || + failed(qco::verifyLinearity(moduleOp))) { + signalPassFailure(); + return; + } if (failed(fuseTwoQubitGates(moduleOp))) { signalPassFailure(); } @@ -446,7 +512,8 @@ struct TargetNativeSynthesisPass final : target(targetIn) {} void getDependentDialects(DialectRegistry& registry) const override { - registry.insert(); + registry.insert(); } protected: @@ -455,6 +522,16 @@ struct TargetNativeSynthesisPass final return; } ModuleOp moduleOp = getOperation(); + constexpr size_t maxRegionNesting = 64; + if (failed(verifyRegionNestingDepth(moduleOp, maxRegionNesting))) { + signalPassFailure(); + return; + } + if (failed(mqt::verifyProgramMetadata(moduleOp)) || + failed(qco::verifyLinearity(moduleOp))) { + signalPassFailure(); + return; + } const auto plan = planTargetSynthesis(moduleOp, target); if (plan.firstNeed == nullptr) { return; @@ -469,6 +546,14 @@ struct TargetNativeSynthesisPass final signalPassFailure(); return; } + if (plan.supportNotHoistable != nullptr) { + plan.supportNotHoistable->emitError() + << "target-native synthesis cannot lower modifier because an " + "operation in its body cannot move across the unitary " + "operations without changing semantics"; + signalPassFailure(); + return; + } if (plan.matrixUnavailable != nullptr) { plan.matrixUnavailable->emitError() << "target-native synthesis cannot lower operation '" @@ -480,8 +565,14 @@ struct TargetNativeSynthesisPass final IRRewriter rewriter(&getContext()); for (Operation* operation : plan.operations) { - lowerTargetOperation(rewriter, cast(operation), - *targetBasis); + if (failed(lowerTargetOperation( + rewriter, cast(operation), *targetBasis))) { + operation->emitError() + << "target-native synthesis cannot hoist modifier support " + "operations without changing semantics"; + signalPassFailure(); + return; + } } if (failed(mlir::mqt::normalizeGlobalPhases(moduleOp))) { signalPassFailure(); @@ -500,7 +591,7 @@ struct VerifyTargetConformancePass final protected: void runOnOperation() override { - WalkResult result = getOperation()->walk([&](Operation* operation) { + for (Operation* operation : collectOperationsPostorder(getOperation())) { if (auto function = dyn_cast(operation); function && llvm::any_of(function.getArgumentTypes(), [](const auto type) { @@ -513,48 +604,48 @@ struct VerifyTargetConformancePass final function.emitError() << "target conformance requires quantum function inputs to be " "assigned to qco.static target sites"; - return WalkResult::interrupt(); + signalPassFailure(); + return; } if (auto staticOp = dyn_cast(operation)) { const auto site = static_cast(staticOp.getIndex()); if (target.vertexForSite(site)) { - return WalkResult::advance(); + continue; } staticOp.emitError() << "target does not contain static site " << site; - return WalkResult::interrupt(); + signalPassFailure(); + return; } if (isa(operation)) { operation->emitError() << "target conformance requires qubits to be assigned to " "qco.static target sites"; - return WalkResult::interrupt(); + signalPassFailure(); + return; } size_t arity = 1; size_t parameterCount = 0; if (auto unitary = dyn_cast(operation)) { if (isExcludedFromTopLevelUnitaryWalk(operation)) { - return WalkResult::advance(); + continue; } arity = unitary.getNumQubits(); parameterCount = unitary.getNumParams(); } else if (!isa(operation)) { - return WalkResult::advance(); + continue; } if (target.supports(operation)) { - return WalkResult::advance(); + continue; } - auto diagnostic = operation->emitError() - << "target does not support operation '" - << operation->getName() << "' with arity " << arity - << " and " << parameterCount << " parameter(s)"; - return WalkResult::interrupt(); - }); - if (result.wasInterrupted()) { + operation->emitError() << "target does not support operation '" + << operation->getName() << "' with arity " << arity + << " and " << parameterCount << " parameter(s)"; signalPassFailure(); + return; } } diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/HadamardLifting.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/HadamardLifting.cpp index 49ac6f7a9b..08df085728 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/HadamardLifting.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/HadamardLifting.cpp @@ -8,11 +8,14 @@ * Licensed under the MIT License */ +#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Transforms/GlobalPhaseNormalization.h" #include "mlir/Dialect/MQT/Utils/Modifiers.h" +#include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Support/OperationUtils.h" #include #include @@ -22,7 +25,9 @@ #include #include +#include #include +#include #include namespace mlir::qco { @@ -104,8 +109,12 @@ struct LiftHadamardsAbovePauliGatesPattern final return failure(); } - // op needs to be in front of a Hadamard gate - auto hadamardGate = dyn_cast(*op->getUsers().begin()); + // op needs to be directly in front of a Hadamard gate + auto output = op.getOutputQubit(0); + if (!output.hasOneUse()) { + return failure(); + } + auto hadamardGate = dyn_cast(*output.getUsers().begin()); if (!hadamardGate) { return failure(); @@ -153,7 +162,7 @@ struct LiftHadamardAboveCNOTPattern final : OpRewritePattern { auto qubitInMeasurement = op.getQubitIn(); auto* predecessor = qubitInMeasurement.getDefiningOp(); auto hadamardGate = dyn_cast(predecessor); - if (!hadamardGate) { + if (!hadamardGate || !hadamardGate.getOutputQubit(0).hasOneUse()) { return failure(); } @@ -161,7 +170,7 @@ struct LiftHadamardAboveCNOTPattern final : OpRewritePattern { auto inQubitHadamard = hadamardGate.getInputQubit(0); predecessor = inQubitHadamard.getDefiningOp(); auto cnotGate = dyn_cast(predecessor); - if (!cnotGate) { + if (!cnotGate || !inQubitHadamard.hasOneUse()) { return failure(); } if (auto innerUnitary = @@ -173,22 +182,22 @@ struct LiftHadamardAboveCNOTPattern final : OpRewritePattern { // Find a control qubit not followed by a measurement. // If there is no such control, the transformation cannot be applied. - unsigned int controlIndex = 0; + std::optional controlIndex; for (unsigned int i = 0; i < cnotGate.getNumControls(); i++) { - if (isa(*cnotGate.getOutputControl(i).getUsers().begin())) { - if (i == cnotGate.getNumControls() - 1) { - return failure(); - } - } else { + auto output = cnotGate.getOutputControl(i); + if (output.hasOneUse() && !isa(*output.getUsers().begin())) { controlIndex = i; break; } } + if (!controlIndex) { + return failure(); + } // Save all SSA values that will be needed after in-place modifications. Value origTgtIn = cnotGate.getInputTarget(0); - Value origCtrlIn = cnotGate.getInputControl(controlIndex); - Value origCtrlOut = cnotGate.getOutputControl(controlIndex); + Value origCtrlIn = cnotGate.getInputControl(*controlIndex); + Value origCtrlOut = cnotGate.getOutputControl(*controlIndex); // Add Hadamard gates before the CNOT. rewriter.setInsertionPoint(cnotGate); @@ -197,7 +206,7 @@ struct LiftHadamardAboveCNOTPattern final : OpRewritePattern { // Rewire the CNOT operands in-place so that the roles are swapped rewriter.modifyOpInPlace(cnotGate, [&]() { - cnotGate->setOperand(controlIndex, h1.getOutputTarget(0)); + cnotGate->setOperand(*controlIndex, h1.getOutputTarget(0)); cnotGate->setOperand(cnotGate.getNumControls(), h2.getOutputTarget(0)); }); @@ -228,6 +237,16 @@ struct HadamardLifting final : impl::HadamardLiftingBase { void runOnOperation() override { auto op = getOperation(); auto* ctx = &getContext(); + constexpr size_t maxRegionNesting = 64; + if (failed(verifyRegionNestingDepth(op, maxRegionNesting))) { + signalPassFailure(); + return; + } + if (failed(mqt::verifyProgramMetadata(op)) || + failed(qco::verifyLinearity(op))) { + signalPassFailure(); + return; + } // Define the set of patterns to use. RewritePatternSet patterns(ctx); diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MeasurementLifting.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MeasurementLifting.cpp index fc4e571bf7..b002c6f54e 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MeasurementLifting.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MeasurementLifting.cpp @@ -11,9 +11,12 @@ #include "mlir/Dialect/MQT/Utils/Modifiers.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Support/OperationUtils.h" #include +#include #include #include #include @@ -21,6 +24,7 @@ #include #include +#include #include namespace mlir::qco { @@ -41,14 +45,33 @@ static bool isInverting(Operation* op) { return isa(op); } * @return True if the operation is a diagonal gate, false otherwise. */ static bool isDiagonal(Operation* op) { - if (op == nullptr) { - return false; + while (op != nullptr && isa(op)) { + op = mqt::getSoleBodyUnitary( + *op->getRegion(0).getBlocks().begin()); } - if (isa(op)) { - return isDiagonal(mqt::getSoleBodyUnitary( - *op->getRegion(0).getBlocks().begin())); + return op != nullptr && isa(op); +} + +/// Return whether nested modifier bodies contain only unitaries and yields. +static bool hasOnlyUnitaryBodyOperations(Operation* root) { + SmallVector worklist{root}; + while (!worklist.empty()) { + Operation* operation = worklist.pop_back_val(); + for (Region& region : operation->getRegions()) { + for (Block& block : region) { + for (Operation& nested : block) { + if (isa(nested)) { + continue; + } + if (!isa(nested)) { + return false; + } + worklist.push_back(&nested); + } + } + } } - return isa(op); + return true; } /** @@ -110,6 +133,12 @@ struct LiftMeasurementsAbovePhaseGatesPattern final return mlir::failure(); } + // Erasing or moving the modifier must not discard or reorder its support + // operations relative to the measurement. + if (!hasOnlyUnitaryBodyOperations(predecessor)) { + return mlir::failure(); + } + if (predecessorUnitary.isSingleQubit()) { rewriter.replaceOp(predecessor, predecessorUnitary.getInputQubits()); return mlir::success(); @@ -184,6 +213,10 @@ struct LiftMeasurementsAboveControlsPattern final return mlir::failure(); } + if (!hasOnlyUnitaryBodyOperations(predecessor)) { + return mlir::failure(); + } + if (llvm::find(predecessorCtrl.getControlsOut(), qubitVariable) == predecessorCtrl.getControlsOut().end()) { // The measured qubit is a target, not a control of the gate. @@ -208,6 +241,16 @@ struct MeasurementLifting final auto op = getOperation(); auto* ctx = &getContext(); + if (failed(qco::verifyLinearity(op))) { + signalPassFailure(); + return; + } + constexpr size_t maxRegionNesting = 64; + if (failed(verifyRegionNestingDepth(op, maxRegionNesting))) { + signalPassFailure(); + return; + } + // Define the set of patterns to use. RewritePatternSet patterns(ctx); patterns.add #include @@ -280,10 +285,13 @@ static ScalarConsts makeConsts(RewriterBase& rewriter, Location loc) { * normalize(a) = a - floor((a + Ï€) / 2Ï€) * 2Ï€ */ template -static Val wrapToPi(Val angle, const ScalarConsts& c) { - const auto twoPi = c.two * c.pi; - const auto floored = ((angle + c.pi) / twoPi).floor(); - return angle - (floored * twoPi); +static Val wrapToPi(Val angle, const ScalarConsts& /*c*/) { + if constexpr (std::is_same_v) { + return {mqt::normalizeAngle(angle.v), angle.rewriter, angle.loc}; + } else { + return {mqt::normalizeAngle(*angle.rewriter, angle.loc, angle.v), + angle.rewriter, angle.loc}; + } } /** @@ -555,7 +563,7 @@ static FailureOr> globalPhaseOf(UnitaryOpInterface op, if (!phi || !lambda) { return failure(); } - return (*phi + *lambda) / c.two; + return (*phi / c.two) + (*lambda / c.two); }) .Default([](auto) -> FailureOr> { return failure(); }); } @@ -691,6 +699,7 @@ static Val sumAngles(Val lhs, Val rhs) { static void emitParameterizedGPhaseIfNeeded(RewriterBase& rewriter, Location loc, Val phase) { + phase.v = mqt::normalizeAngle(rewriter, loc, phase.v); if (!isConstantAngle(phase)) { GPhaseOp::create(rewriter, loc, phase.v); } @@ -814,7 +823,7 @@ directZYZAnglesFromGate(UnitaryOpInterface op, RewriterBase& rewriter, return {.theta = halfPi, .phi = phi, .lambda = lambda, - .phase = sumAngles(phi, lambda) / consts.two}; + .phase = (phi / consts.two) + (lambda / consts.two)}; } const auto theta = parameter(0); @@ -995,14 +1004,26 @@ struct MergeSingleQubitRotationGatesPattern final if (failed(phase)) { return failure(); } - phaseAccum = phaseAccum + *phase; + if (!std::isfinite(phase->v)) { + return failure(); + } + phaseAccum.v = mqt::normalizeAngle(mqt::normalizeAngle(phaseAccum.v) + + mqt::normalizeAngle(phase->v)); qAccum = qAccum ? hamiltonProduct(*qi, *qAccum) : *qi; + if (!std::isfinite(qAccum->w.v) || !std::isfinite(qAccum->x.v) || + !std::isfinite(qAccum->y.v) || !std::isfinite(qAccum->z.v)) { + return failure(); + } } const auto [theta, phi, lambda, eulerPhase] = anglesFromQuaternion(*qAccum, consts); - const auto correction = - phaseAccum - ((phi + lambda) / consts.two) + eulerPhase; + auto correction = phaseAccum - ((phi + lambda) / consts.two) + eulerPhase; + if (!std::isfinite(theta.v) || !std::isfinite(phi.v) || + !std::isfinite(lambda.v) || !std::isfinite(correction.v)) { + return failure(); + } + correction.v = mqt::normalizeAngle(correction.v); for (auto chainOp : llvm::drop_begin(chain)) { rewriter.replaceOp(chainOp, chainOp.getInputQubit(0)); @@ -1047,7 +1068,8 @@ struct MergeSingleQubitRotationGatesPattern final return failure(); } qAccum = qAccum ? hamiltonProduct(*qi, *qAccum) : *qi; - phaseAccum = phaseAccum + *phase; + const auto boundedPhase = wrapToPi(*phase, consts); + phaseAccum = wrapToPi(phaseAccum + boundedPhase, consts); } for (auto chainOp : llvm::drop_begin(chain)) { @@ -1070,10 +1092,11 @@ struct MergeSingleQubitRotationGatesPattern final lambda = lambda + consts.pi; eulerPhase = eulerPhase + consts.pi; } - const RuntimeEulerAngles angles{.theta = theta, - .phi = phi, - .lambda = lambda, - .phase = phaseAccum + eulerPhase}; + const RuntimeEulerAngles angles{ + .theta = theta, + .phi = phi, + .lambda = lambda, + .phase = wrapToPi(phaseAccum + eulerPhase, consts)}; Value qubit = emitRuntimeEulerAngles( rewriter, loc, chain.front().getInputQubit(0), angles, basis, consts); rewriter.replaceOp(chain.front(), qubit); @@ -1104,8 +1127,8 @@ struct MergeSingleQubitRotationGatesPattern final return failure(); } - if (succeeded(tryMergeStaticChain(chain, rewriter))) { - return success(); + if (!hasDynamicParameter(chain)) { + return tryMergeStaticChain(chain, rewriter); } return mergeDynamicChain(chain, rewriter); } @@ -1125,6 +1148,17 @@ struct MergeSingleQubitRotationGates final auto op = getOperation(); auto* ctx = &getContext(); + constexpr size_t maxRegionNesting = 64; + if (failed(verifyRegionNestingDepth(op, maxRegionNesting))) { + signalPassFailure(); + return; + } + if (failed(mqt::verifyProgramMetadata(op)) || + failed(qco::verifyLinearity(op))) { + signalPassFailure(); + return; + } + RewritePatternSet patterns(ctx); patterns.add(patterns.getContext()); diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/PauliTwirling.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/PauliTwirling.cpp index 1199af6577..394787f1b2 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/PauliTwirling.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/PauliTwirling.cpp @@ -19,9 +19,7 @@ #include #include #include -#include #include -#include #include #include @@ -219,13 +217,25 @@ struct PauliTwirl2QGates final const std::array* table; }; SmallVector gates; - getOperation().walk([&](Operation* op) { + SmallVector worklist{getOperation()}; + while (!worklist.empty()) { + Operation* op = worklist.pop_back_val(); if (const auto* table = getTwirlTable(op)) { gates.push_back({.gate = cast(op), .table = table}); } - return isa(op) ? WalkResult::skip() - : WalkResult::advance(); - }); + if (isa(op)) { + continue; + } + SmallVector nested; + for (Region& region : op->getRegions()) { + for (Block& block : region) { + for (Operation& operation : block) { + nested.push_back(&operation); + } + } + } + worklist.append(nested.rbegin(), nested.rend()); + } IRRewriter rewriter(&getContext()); std::mt19937_64 rng(seed); diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/QuantumLoopUnroll.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/QuantumLoopUnroll.cpp index e381e1ff86..26a11dccdb 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/QuantumLoopUnroll.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/QuantumLoopUnroll.cpp @@ -9,13 +9,18 @@ */ #include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Support/OperationUtils.h" #include #include #include +#include #include +#include #include +#include #include #include #include @@ -26,6 +31,11 @@ namespace mlir::qco { #define GEN_PASS_DEF_QUANTUMLOOPUNROLL #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" +/// Keep verifier-valid loop bounds from turning this optimization into an +/// unbounded allocation request. +static constexpr uint64_t kMaxQuantumLoopUnrollFactor = 4096; +static constexpr uint64_t kMaxQuantumLoopExpandedOperations = 100000; + /** * @brief Predicate for quantum loops. * @details A quantum loop is a `scf.for` operation that has at least one qubit @@ -52,14 +62,168 @@ static bool isQuantumLoop(scf::ForOp loop) { */ static SmallVector collectQuantumLoops(FunctionOpInterface func) { SmallVector loops; - func.walk([&](scf::ForOp loop) { - if (isQuantumLoop(loop)) { - loops.emplace_back(loop); + SmallVector> worklist; + worklist.emplace_back(func.getOperation(), false); + while (!worklist.empty()) { + const auto [operation, visited] = worklist.pop_back_val(); + if (visited) { + if (auto loop = dyn_cast(operation); + loop && isQuantumLoop(loop)) { + loops.emplace_back(loop); + } + continue; } - }); + worklist.emplace_back(operation, true); + for (Region& region : operation->getRegions()) { + for (Block& block : region) { + for (Operation& nested : block) { + worklist.emplace_back(&nested, false); + } + } + } + } return loops; } +/** @brief Whether a loop body only yields its iteration arguments unchanged. */ +static bool hasIdentityYieldOnlyBody(scf::ForOp loop) { + if (!llvm::hasSingleElement(loop.getBody()->getOperations())) { + return false; + } + auto yield = dyn_cast(loop.getBody()->getTerminator()); + return yield && llvm::equal(yield.getResults(), loop.getRegionIterArgs()); +} + +/** @brief Check the projected unrolled IR size before cloning or rewriting. */ +static LogicalResult verifyUnrollExpansionBudget(FunctionOpInterface func, + int64_t unrollFactor) { + uint64_t projectedOperations = 0; + SmallVector> worklist; + worklist.emplace_back(func.getOperation(), 1); + + while (!worklist.empty()) { + const auto [operation, multiplier] = worklist.pop_back_val(); + if (multiplier > kMaxQuantumLoopExpandedOperations - projectedOperations) { + return operation->emitError() + << "quantum loop unrolling would exceed the limit of " + << kMaxQuantumLoopExpandedOperations << " projected operations"; + } + projectedOperations += multiplier; + + uint64_t nestedMultiplier = multiplier; + if (auto loop = dyn_cast(operation); + loop && isQuantumLoop(loop)) { + if (hasIdentityYieldOnlyBody(loop)) { + nestedMultiplier = 0; + } else { + uint64_t factor = 0; + if (unrollFactor == -1) { + const auto tripCount = loop.getStaticTripCount(); + if (!tripCount) { + // Nested bounds may become constant after an enclosing loop is + // unrolled. Account for their current body once and recheck the + // complete budget before every subsequent unrolling round. + factor = 1; + } else { + factor = + tripCount->getLimitedValue(kMaxQuantumLoopUnrollFactor + 1); + } + } else { + factor = static_cast(unrollFactor); + } + if (factor > kMaxQuantumLoopUnrollFactor) { + return loop.emitError() + << "quantum loop unroll factor " << factor + << " exceeds the limit of " << kMaxQuantumLoopUnrollFactor; + } + if (factor != 0 && + nestedMultiplier > kMaxQuantumLoopExpandedOperations / factor) { + return loop.emitError() + << "quantum loop unrolling would exceed the limit of " + << kMaxQuantumLoopExpandedOperations + << " projected operations"; + } + nestedMultiplier *= factor; + } + } + + if (nestedMultiplier == 0) { + continue; + } + for (Region& region : operation->getRegions()) { + for (Block& block : region) { + for (Operation& nested : block) { + worklist.emplace_back(&nested, nestedMultiplier); + } + } + } + } + return success(); +} + +/** @brief Unroll all selected loops in @p func. */ +static LogicalResult unrollQuantumLoops(FunctionOpInterface func, + int64_t unrollFactor) { + if (unrollFactor == -1) { + while (true) { + if (failed(verifyUnrollExpansionBudget(func, unrollFactor))) { + return failure(); + } + auto loops = collectQuantumLoops(func); + if (loops.empty()) { + return success(); + } + + bool changed = false; + for (auto loop : loops) { + if (hasIdentityYieldOnlyBody(loop)) { + loop.replaceAllUsesWith(loop.getInitArgs()); + loop.erase(); + changed = true; + continue; + } + + const auto tripCount = loop.getStaticTripCount(); + if (!tripCount) { + continue; + } + if (tripCount->isZero()) { + loop.replaceAllUsesWith(loop.getInitArgs()); + loop.erase(); + changed = true; + continue; + } + + if (failed(loopUnrollFull(loop))) { + loop.emitError() << "failed to fully unroll"; + return failure(); + } + changed = true; + } + + if (!changed) { + loops.front().emitError() + << "cannot fully unroll a quantum loop without a static trip " + "count"; + return failure(); + } + + if (failed(applyPatternsGreedily(func, + RewritePatternSet(func.getContext())))) { + return failure(); + } + } + } + + for (auto loop : collectQuantumLoops(func)) { + if (failed(loopUnrollByFactor(loop, unrollFactor))) { + loop.emitError() << "failed to unroll with factor " + Twine(unrollFactor); + return failure(); + } + } + return success(); +} + namespace { /** @@ -77,6 +241,13 @@ struct QuantumLoopUnroll final signalPassFailure(); return; } + if (unrollFactor > static_cast(kMaxQuantumLoopUnrollFactor)) { + getOperation()->emitError() + << "quantum loop unroll factor " << Twine(unrollFactor) + << " exceeds the limit of " << kMaxQuantumLoopUnrollFactor; + signalPassFailure(); + return; + } // Note that the built-in loop-unrolling utilities initialize // `IRRewriter`s using the context of the loop operation and automatically @@ -87,57 +258,45 @@ struct QuantumLoopUnroll final return; } - // If the unroll factor is -1, fully unroll all loops. - if (unrollFactor == -1) { - while (true) { - auto loops = collectQuantumLoops(getOperation()); - if (loops.empty()) { - return; - } - - bool changed = false; - for (auto loop : loops) { - const auto tripCount = loop.getStaticTripCount(); - if (!tripCount) { - continue; - } - if (tripCount->isZero() || - llvm::hasSingleElement(loop.getBody()->getOperations())) { - loop.replaceAllUsesWith(loop.getInitArgs()); - loop.erase(); - changed = true; - continue; - } - - if (failed(loopUnrollFull(loop))) { - loop.emitError() << "failed to fully unroll"; - signalPassFailure(); - return; - } - changed = true; - } + if (collectQuantumLoops(getOperation()).empty()) { + return; + } - if (!changed) { - loops.front().emitError() << "failed to fully unroll"; - signalPassFailure(); - return; - } + constexpr size_t maxRegionNesting = 64; + if (failed(verifyRegionNestingDepth(getOperation(), maxRegionNesting))) { + signalPassFailure(); + return; + } + if (failed(verifyUnrollExpansionBudget(getOperation(), unrollFactor))) { + signalPassFailure(); + return; + } - if (failed(applyPatternsGreedily(getOperation(), - RewritePatternSet(&getContext())))) { - signalPassFailure(); - return; - } - } + // Perform the transformation on a clone first. Besides validating all + // selected loop bounds, this keeps the source function untouched if any + // loop cannot be unrolled. + OwningOpRef transformedModule = + ModuleOp::create(getOperation()->getLoc()); + Operation* transformedOperation = getOperation()->clone(); + transformedModule->push_back(transformedOperation); + if (failed(unrollQuantumLoops( + cast(transformedOperation), unrollFactor))) { + signalPassFailure(); + return; + } + // Symbol references remain unchanged, so verify the function without + // requiring sibling symbols to be present in the temporary module. + if (failed(verify(transformedOperation)) || + failed(qco::verifyLinearity(transformedOperation))) { + getOperation()->emitError("quantum loop unrolling produced invalid IR"); + signalPassFailure(); + return; } - for (auto loop : collectQuantumLoops(getOperation())) { - if (failed(loopUnrollByFactor(loop, unrollFactor))) { - loop.emitError() << "failed to unroll with factor " + - Twine(unrollFactor); - signalPassFailure(); - return; - } + for (auto [originalRegion, transformedRegion] : + llvm::zip_equal(getOperation()->getRegions(), + transformedOperation->getRegions())) { + originalRegion.takeBody(transformedRegion); } } }; diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/RemoveDeadGates.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/RemoveDeadGates.cpp index 7b945835dc..b7c3ffb23f 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/RemoveDeadGates.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/RemoveDeadGates.cpp @@ -11,11 +11,13 @@ #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Support/OperationUtils.h" #include #include #include +#include #include namespace mlir::qco { @@ -54,6 +56,11 @@ struct RemoveDeadGates final : impl::RemoveDeadGatesBase { protected: void runOnOperation() override { + constexpr size_t maxRegionNesting = 64; + if (failed(verifyRegionNestingDepth(getOperation(), maxRegionNesting))) { + signalPassFailure(); + return; + } RewritePatternSet patterns(&getContext()); patterns.add( &getContext()); diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ReplaceClassicalControls.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ReplaceClassicalControls.cpp index 476d3bb926..d97bbed3b1 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ReplaceClassicalControls.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ReplaceClassicalControls.cpp @@ -13,6 +13,7 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Support/OperationUtils.h" #include #include @@ -195,11 +196,18 @@ static LogicalResult tryReplaceMeasuredRZTarget(CtrlOp op, RZOp rzOp, return failure(); } if (areAllMeasured(op.getControlsIn())) { + if (failed( + mqt::hoistSupportingOpsBefore(*op.getBody(), rzOp, op, rewriter))) { + return failure(); + } rewriter.replaceOp(op, op.getInputQubits()); return success(); } - mqt::hoistSupportingOpsBefore(*op.getBody(), rzOp, op, rewriter); + if (failed( + mqt::hoistSupportingOpsBefore(*op.getBody(), rzOp, op, rewriter))) { + return failure(); + } rewriter.setInsertionPoint(op); Value phase = selectScaledAngle(rewriter, op.getLoc(), rzOp.getTheta(), outcome, 0.5, -0.5); @@ -238,12 +246,19 @@ static LogicalResult tryReplaceMeasuredRZZTarget(CtrlOp op, RZZOp rzzOp, return failure(); } if (bothTargetsMeasured && areAllMeasured(op.getControlsIn())) { + if (failed(mqt::hoistSupportingOpsBefore(*op.getBody(), rzzOp, op, + rewriter))) { + return failure(); + } replaceRZZCtrlOp(op, *targetResultOrder, op.getControlsIn(), op.getTargetsIn(), rewriter); return success(); } - mqt::hoistSupportingOpsBefore(*op.getBody(), rzzOp, op, rewriter); + if (failed( + mqt::hoistSupportingOpsBefore(*op.getBody(), rzzOp, op, rewriter))) { + return failure(); + } rewriter.setInsertionPoint(op); SmallVector controls(op.getControlsIn()); SmallVector targets(op.getTargetsIn()); @@ -294,6 +309,10 @@ static void trySwapControlAndTargetOfPhaseGate(CtrlOp op, Value controlOut = op.getControlsOut()[controlIndex]; Value targetOut = op.getTargetsOut()[0]; + if (!controlOut.hasOneUse() || !targetOut.hasOneUse()) { + ++controlIndex; + continue; + } rewriter.modifyOpInPlace(op, [&]() { op.getTargetsInMutable()[0].set(control); @@ -323,6 +342,9 @@ struct ReplaceBasisStateControlsWithIfPattern final LogicalResult matchAndRewrite(MeasureOp measure, PatternRewriter& rewriter) const override { + if (!measure.getQubitOut().hasOneUse()) { + return failure(); + } auto ctrlOp = dyn_cast(*measure.getQubitOut().getUsers().begin()); if (!ctrlOp) { return failure(); @@ -410,6 +432,11 @@ struct ReplaceClassicalControls final void runOnOperation() override { auto op = getOperation(); auto* ctx = &getContext(); + constexpr size_t maxRegionNesting = 64; + if (failed(verifyRegionNestingDepth(op, maxRegionNesting))) { + signalPassFailure(); + return; + } // Define the set of patterns to use. RewritePatternSet patterns(ctx); diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ReuseQubits.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ReuseQubits.cpp index f12c5fac88..09d0e97ede 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ReuseQubits.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ReuseQubits.cpp @@ -10,6 +10,7 @@ #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Support/OperationUtils.h" #include #include @@ -20,6 +21,7 @@ #include #include +#include #include #include @@ -155,6 +157,11 @@ struct ReuseQubits final : impl::ReuseQubitsBase { void runOnOperation() override { auto op = getOperation(); auto* ctx = &getContext(); + constexpr size_t maxRegionNesting = 64; + if (failed(verifyRegionNestingDepth(op, maxRegionNesting))) { + signalPassFailure(); + return; + } // Define the set of patterns to use. RewritePatternSet patterns(ctx); diff --git a/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp b/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp index a9e52d540e..ad6135af83 100644 --- a/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp +++ b/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp @@ -28,13 +28,18 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" +#include "mlir/Support/OperationUtils.h" #include #include +#include #include #include +#include #include +#include #include #include #include @@ -52,6 +57,7 @@ #include #include #include +#include #include #include #include @@ -64,6 +70,10 @@ namespace mlir::qco { namespace { +constexpr size_t maxCallNesting = 64; +constexpr size_t maxExecutionSteps = 10'000; +constexpr size_t maxRegionNesting = 64; + struct QubitMap { DenseMap qubits; size_t numQubits = 0; @@ -118,6 +128,7 @@ struct ClassicalEnv { DenseMap deferredMeasurements; /// Shared storage preserves CBit register identity across `func.call`. DenseMap> registers; + size_t allocatedRegisterBits = 0; }; struct DecodedGate { @@ -131,7 +142,7 @@ struct WalkState { dd::Package* dd; std::mt19937_64* rng = nullptr; const DenseSet* deferredMeasurements = nullptr; - size_t remainingExecutionSteps = 10'000; + size_t remainingExecutionSteps = maxExecutionSteps; DenseSet activeCalls; }; struct LoopRange { @@ -201,35 +212,66 @@ decodeStandardGate(UnitaryOpInterface unitary) { static dd::mCachedEdge buildEmbeddedLocalDD(dd::Package& dd, const DynamicMatrix& local, const DenseMap& operandForWire, - size_t numOperands, int64_t level, size_t row, - size_t col) { - if (level < 0) { - return dd::mCachedEdge::terminal( - local(static_cast(row), static_cast(col))); - } - const auto wire = static_cast(level); - const auto operand = operandForWire.find(wire); - if (operand == operandForWire.end()) { - const auto child = buildEmbeddedLocalDD(dd, local, operandForWire, - numOperands, level - 1, row, col); - return dd.makeDDNode( - wire, {child, dd::mCachedEdge::zero(), dd::mCachedEdge::zero(), child}); - } - - const size_t operandMask = size_t{1} << (numOperands - 1 - operand->second); - const auto edge00 = buildEmbeddedLocalDD(dd, local, operandForWire, - numOperands, level - 1, row, col); - const auto edge01 = - buildEmbeddedLocalDD(dd, local, operandForWire, numOperands, level - 1, - row, col | operandMask); - const auto edge10 = - buildEmbeddedLocalDD(dd, local, operandForWire, numOperands, level - 1, - row | operandMask, col); - const auto edge11 = - buildEmbeddedLocalDD(dd, local, operandForWire, numOperands, level - 1, - row | operandMask, col | operandMask); - return dd.makeDDNode( - wire, {edge00, edge01, edge10, edge11}); + size_t numOperands, int64_t rootLevel, size_t rootRow, + size_t rootCol) { + struct Frame { + int64_t level; + size_t row; + size_t col; + SmallVector children; + }; + + SmallVector frames{ + {.level = rootLevel, .row = rootRow, .col = rootCol, .children = {}}}; + while (!frames.empty()) { + Frame& frame = frames.back(); + std::optional completed; + if (frame.level < 0) { + completed = dd::mCachedEdge::terminal(local( + static_cast(frame.row), static_cast(frame.col))); + } else { + const auto wire = static_cast(frame.level); + const auto operand = operandForWire.find(wire); + const size_t childCount = operand == operandForWire.end() ? 1 : 4; + if (frame.children.size() < childCount) { + size_t childRow = frame.row; + size_t childCol = frame.col; + if (operand != operandForWire.end()) { + const size_t operandMask = size_t{1} + << (numOperands - 1 - operand->second); + const size_t child = frame.children.size(); + if (child >= 2) { + childRow |= operandMask; + } + if ((child & 1U) != 0U) { + childCol |= operandMask; + } + } + frames.push_back({.level = frame.level - 1, + .row = childRow, + .col = childCol, + .children = {}}); + continue; + } + if (operand == operandForWire.end()) { + const auto child = frame.children.front(); + completed = dd.makeDDNode( + wire, + {child, dd::mCachedEdge::zero(), dd::mCachedEdge::zero(), child}); + } else { + completed = dd.makeDDNode( + wire, {frame.children[0], frame.children[1], frame.children[2], + frame.children[3]}); + } + } + + frames.pop_back(); + if (frames.empty()) { + return *completed; + } + frames.back().children.push_back(*completed); + } + llvm_unreachable("embedded DD construction starts with one frame"); } static dd::MatrixDD makeEmbeddedLocalDD(dd::Package& dd, @@ -415,8 +457,18 @@ static LogicalResult applyUnsignedIndexCast(Value in, Value out, Operation* op, static LogicalResult allocateRegister(cbit::AllocOp alloc, ClassicalEnv& classical) { - const auto width = - static_cast(alloc.getResult().getType().getWidth()); + constexpr size_t maxClassicalRegisterBits = 1U << 20; + const auto rawWidth = alloc.getResult().getType().getWidth(); + if (rawWidth <= 0 || + classical.allocatedRegisterBits > maxClassicalRegisterBits || + static_cast(rawWidth) > + maxClassicalRegisterBits - classical.allocatedRegisterBits) { + return alloc.emitError() + << "QCO DD simulation supports at most " << maxClassicalRegisterBits + << " classical register bits per module"; + } + const auto width = static_cast(rawWidth); + classical.allocatedRegisterBits += width; ClassicalEnv::RegisterBit initialValue; if (alloc.getInitialization() == cbit::Initialization::Zero) { initialValue.value = false; @@ -610,30 +662,43 @@ resolveLoop(scf::ForOp forOp, ClassicalEnv& classical, size_t remainingSteps) { static LogicalResult bindValuePairs(ValueRange sources, ValueRange dests, WalkState& walk, Operation* op) { + if (sources.size() != dests.size()) { + return op->emitError() + << "source and destination arity mismatch in QCO DD simulation"; + } const QubitMap sourceQubits = *walk.qubits; const ClassicalEnv sourceClassical = *walk.classical; + + // Validate the complete transfer before updating either environment. for (auto [src, dest] : llvm::zip_equal(sources, dests)) { + if (src.getType() != dest.getType()) { + return op->emitError() + << "source and destination type mismatch in QCO DD simulation: " + << src.getType() << " versus " << dest.getType(); + } if (isa(dest.getType())) { - const auto q = sourceQubits.lookup(src); - if (!q) { + if (!sourceQubits.lookup(src)) { return op->emitError() << "qubit SSA value is not mapped for QCO DD construction"; } - walk.qubits->bind(dest, *q); } else if (isa(dest.getType())) { - const auto it = sourceClassical.registers.find(src); - if (it == sourceClassical.registers.end()) { + if (!sourceClassical.registers.contains(src)) { return op->emitError() << "CBit register is not mapped for QCO DD simulation"; } - walk.classical->registers[dest] = it->second; + } else if (!sourceClassical.scalars.contains(src)) { + return op->emitError() + << "classical SSA value is not mapped for QCO DD simulation"; + } + } + + for (auto [src, dest] : llvm::zip_equal(sources, dests)) { + if (isa(dest.getType())) { + walk.qubits->bind(dest, *sourceQubits.lookup(src)); + } else if (isa(dest.getType())) { + walk.classical->registers[dest] = sourceClassical.registers.at(src); } else { - const auto value = sourceClassical.scalars.find(src); - if (value == sourceClassical.scalars.end()) { - return op->emitError() - << "classical SSA value is not mapped for QCO DD simulation"; - } - walk.classical->scalars[dest] = value->second; + walk.classical->scalars[dest] = sourceClassical.scalars.at(src); } } return success(); @@ -835,12 +900,26 @@ static LogicalResult applyOp(Operation& op, WalkState& walk, StateDD& state) { .template Case([&](func::CallOp call) -> LogicalResult { auto callee = SymbolTable::lookupNearestSymbolFrom( call, call.getCalleeAttr()); + if (!callee) { + return call.emitError() << "func.call callee @" << call.getCallee() + << " could not be resolved"; + } if (!callee.getBody().hasOneBlock()) { return call.emitError() << "func.call callee must have a single-block body"; } - auto returnOp = - cast(callee.getBody().front().getTerminator()); + Block& calleeBody = callee.getBody().front(); + if (calleeBody.empty()) { + return call.emitError() << "func.call callee must end in func.return"; + } + auto returnOp = dyn_cast(calleeBody.getTerminator()); + if (!returnOp) { + return call.emitError() << "func.call callee must end in func.return"; + } + if (failed(verifyLinearity(callee)) || + failed(verifyRegionNestingDepth(callee, maxRegionNesting))) { + return failure(); + } Operation* calleeOp = callee.getOperation(); if (!walk.activeCalls.insert(calleeOp).second) { return call.emitError() @@ -849,13 +928,23 @@ static LogicalResult applyOp(Operation& op, WalkState& walk, StateDD& state) { } const auto guard = llvm::make_scope_exit([&] { walk.activeCalls.erase(calleeOp); }); + if (walk.activeCalls.size() > maxCallNesting) { + return call.emitError() + << "func.call nesting exceeds the limit of " << maxCallNesting; + } + if (walk.remainingExecutionSteps == 0) { + return call.emitError( + "QCO DD execution exceeds the limit of 10000 control-flow " + "steps"); + } + --walk.remainingExecutionSteps; if (failed(bindValuePairs(call.getArgOperands(), callee.getArguments(), walk, call))) { return failure(); } - if (failed(walkBlock(callee.getBody().front(), walk, state))) { + if (failed(walkBlock(calleeBody, walk, state))) { return failure(); } return bindValuePairs(returnOp.getOperands(), call.getResults(), walk, @@ -920,14 +1009,33 @@ static LogicalResult walkFunction(func::FuncOp func, WalkState& walkState, } static FailureOr prepare(func::FuncOp func, const dd::Package& dd) { + if (failed(verifyLinearity(func)) || + failed(verifyRegionNestingDepth(func, maxRegionNesting))) { + return failure(); + } if (!func.getBody().hasOneBlock()) { return func.emitError() << "QCO DD construction expects a single-block function body"; } + Block& body = func.getBody().front(); + if (body.empty() || !isa(body.getTerminator())) { + return func.emitError() + << "QCO DD construction expects the function to end in func.return"; + } QubitMap qubits; - for (StaticOp staticOp : func.getBody().front().getOps()) { - const auto q = static_cast(staticOp.getIndex()); + DenseSet staticIndices; + for (StaticOp staticOp : body.getOps()) { + const auto index = staticOp.getIndex(); + if (index > std::numeric_limits::max()) { + return staticOp.emitError() << "static qubit index " << index + << " exceeds the DD qubit-index capacity"; + } + const auto q = static_cast(index); + if (!staticIndices.insert(q).second) { + return staticOp.emitError() << "duplicate static qubit index " << index + << " is ambiguous for QCO DD construction"; + } qubits.bind(staticOp.getQubit(), q); qubits.numQubits = std::max(qubits.numQubits, static_cast(q) + 1); } @@ -941,7 +1049,7 @@ static FailureOr prepare(func::FuncOp func, const dd::Package& dd) { } qubits.numQubits = next; } - for (AllocOp alloc : func.getBody().front().getOps()) { + for (AllocOp alloc : body.getOps()) { qubits.bind(alloc.getResult(), static_cast(qubits.numQubits++)); } if (dd.qubits() < qubits.numQubits) { @@ -1041,31 +1149,52 @@ static bool isDeferrableMeasurement(MeasureOp measure, Block* entry, static void analyzeSampling(func::FuncOp func, Block* entry, ArrayRef outputs, - DenseSet& active, SamplingPlan& plan) { + DenseSet& active, SamplingPlan& plan, + size_t& remainingSteps) { Operation* funcOp = func.getOperation(); - if (!active.insert(funcOp).second) { + if (remainingSteps == 0 || active.size() > maxCallNesting || + !active.insert(funcOp).second) { plan.dynamic = true; return; } - func.getBody().walk([&](Operation* op) { - if (isa(op)) { - plan.dynamic = true; - } else if (auto measure = dyn_cast(op)) { - if (isDeferrableMeasurement(measure, entry, outputs)) { - plan.deferredMeasurements.insert(op); - } else { + SmallVector blocks; + for (Block& block : func.getBody()) { + blocks.push_back(&block); + } + while (!blocks.empty()) { + Block* block = blocks.pop_back_val(); + for (Operation& operation : *block) { + if (remainingSteps == 0) { plan.dynamic = true; + active.erase(funcOp); + return; } - } else if (auto call = dyn_cast(op)) { - auto callee = SymbolTable::lookupNearestSymbolFrom( - call, call.getCalleeAttr()); - if (!callee.getBody().hasOneBlock()) { + --remainingSteps; + Operation* op = &operation; + if (isa(op)) { plan.dynamic = true; - } else { - analyzeSampling(callee, entry, outputs, active, plan); + } else if (auto measure = dyn_cast(op)) { + if (isDeferrableMeasurement(measure, entry, outputs)) { + plan.deferredMeasurements.insert(op); + } else { + plan.dynamic = true; + } + } else if (auto call = dyn_cast(op)) { + auto callee = SymbolTable::lookupNearestSymbolFrom( + call, call.getCalleeAttr()); + if (!callee || !callee.getBody().hasOneBlock()) { + plan.dynamic = true; + } else { + analyzeSampling(callee, entry, outputs, active, plan, remainingSteps); + } + } + for (Region& region : op->getRegions()) { + for (Block& nested : region) { + blocks.push_back(&nested); + } } } - }); + } active.erase(funcOp); } @@ -1088,7 +1217,8 @@ static FailureOr getSamplingPlan(func::FuncOp func) { } DenseSet active; - analyzeSampling(func, &entry, plan.outputs, active, plan); + size_t remainingSteps = maxExecutionSteps; + analyzeSampling(func, &entry, plan.outputs, active, plan, remainingSteps); return plan; } diff --git a/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp b/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp index b8e883b422..bf019e4aa4 100644 --- a/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp +++ b/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp @@ -90,10 +90,7 @@ void WireIterator::forward() { .Case([&](IndexSwitchOp op) { qubit_ = op.getTiedResult(&(*qubit_.use_begin())); }) - .Default([&](Operation* op) { - llvm::reportFatalInternalError("unknown op in def-use chain: " + - op->getName().getStringRef()); - }); + .Default([&](Operation*) { isFinal_ = true; }); } } @@ -125,6 +122,7 @@ void WireIterator::backward() { } // Find the input from the output qubit SSA value. + bool reachedBoundary = false; TypeSwitch(op_) .Case( [&](UnitaryOpInterface op) { qubit_ = op.getInputForOutput(qubit_); }) @@ -163,10 +161,13 @@ void WireIterator::backward() { } llvm::reportFatalInternalError("expected result lookup"); }) - .Default([&](Operation* op) { - llvm::reportFatalInternalError("unknown op in def-use chain: " + - op->getName().getStringRef()); - }); + .Default([&](Operation*) { reachedBoundary = true; }); + + if (reachedBoundary) { + op_ = nullptr; + isFinal_ = false; + return; + } // Get the operation that produces the qubit value. // If the current qubit SSA value is a BlockArgument (no defining op), the diff --git a/mlir/lib/Dialect/QIR/Transforms/AttachQIRAttributes.cpp b/mlir/lib/Dialect/QIR/Transforms/AttachQIRAttributes.cpp index 249ec57829..16a6b3a876 100644 --- a/mlir/lib/Dialect/QIR/Transforms/AttachQIRAttributes.cpp +++ b/mlir/lib/Dialect/QIR/Transforms/AttachQIRAttributes.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -28,10 +29,12 @@ #include #include #include +#include -#include +#include #include #include +#include #include #include #include @@ -64,6 +67,31 @@ struct Metadata { bool usesMultipleReturnPoints{false}; }; +template +static void walkQIRAttributeOperationsIteratively(Operation* root, + Callback&& callback) { + SmallVector worklist{root}; + while (!worklist.empty()) { + Operation* operation = worklist.pop_back_val(); + callback(operation); + for (Region& region : operation->getRegions()) { + for (Block& block : region) { + for (Operation& nested : block) { + worklist.push_back(&nested); + } + } + } + } +} + +[[nodiscard]] static bool hasQIREntryPointAttribute(LLVM::LLVMFuncOp function) { + const auto passthrough = function->getAttrOfType("passthrough"); + return passthrough && llvm::any_of(passthrough, [](Attribute attribute) { + const auto name = dyn_cast(attribute); + return name && name.getValue() == StringRef(::qir::ENTRY_POINT_ATTR); + }); +} + /** * @brief Attaches the required attributes to the function marked as * entry_point. @@ -74,15 +102,47 @@ struct QIRSetAttributesAndMetadata final protected: void runOnOperation() override { - IRRewriter rewriter(&getContext()); - auto main = getMainFunction(getOperation()); - if (!main) { + SmallVector entryPoints; + for (auto function : getOperation().getOps()) { + if (mqt::isEntryPoint(function) || hasQIREntryPointAttribute(function)) { + entryPoints.push_back(function); + } + } + if (entryPoints.size() != 1) { + getOperation().emitError() + << "QIR metadata attachment requires exactly one entry point, but " + "found " + << entryPoints.size(); + signalPassFailure(); return; } - Metadata metadata = useAdaptive ? getAdaptive(main) : getBase(main); + + auto main = entryPoints.front(); + auto module = getOperation(); + const auto [useDynamicQubit, useDynamicResult, useArrays] = + usesDynamic(module); + if (!useAdaptive && (useDynamicQubit || useDynamicResult)) { + module.emitError() + << "QIR base profile does not support dynamic resource management"; + signalPassFailure(); + return; + } + + auto numQubits = getNumQubits(module, !useDynamicQubit); + auto numResults = getNumResults(module, !useDynamicResult); + if (failed(numQubits) || failed(numResults)) { + signalPassFailure(); + return; + } + + Metadata metadata = + useAdaptive ? getAdaptive(main, *numQubits, *numResults, + useDynamicQubit, useDynamicResult, useArrays) + : getBase(*numQubits, *numResults); if (useAdaptive) { - collectOptionalFeatures(getOperation(), main, metadata); + collectOptionalFeatures(module, main, metadata); } + IRRewriter rewriter(&getContext()); setMetadata(main, metadata, rewriter); } @@ -119,30 +179,55 @@ struct QIRSetAttributesAndMetadata final return createFlag(behavior, name, rewriter.getBoolAttr(value)); }; - const SmallVector attributes{ - rewriter.getStringAttr(::qir::ENTRY_POINT_ATTR), - rewriter.getStrArrayAttr( - {::qir::OUTPUT_LABELING_SCHEMA_ATTR, ::qir::LABELED_SCHEMA}), - rewriter.getStrArrayAttr( - {::qir::QIR_PROFILES_ATTR, - useAdaptive ? ::qir::ADAPTIVE_PROFILE : ::qir::BASE_PROFILE}), - rewriter.getStrArrayAttr( - {"required_num_qubits", std::to_string(metadata.numQubits)}), - rewriter.getStrArrayAttr( - {"required_num_results", std::to_string(metadata.numResults)})}; + const auto isQIRFunctionAttribute = [](Attribute attribute) { + if (const auto name = dyn_cast(attribute)) { + return name.getValue() == StringRef(::qir::ENTRY_POINT_ATTR); + } + const auto pair = dyn_cast(attribute); + const auto key = pair && pair.size() == 2 ? dyn_cast(pair[0]) + : StringAttr{}; + return key && + (key.getValue() == StringRef(::qir::OUTPUT_LABELING_SCHEMA_ATTR) || + key.getValue() == StringRef(::qir::QIR_PROFILES_ATTR) || + key.getValue() == "required_num_qubits" || + key.getValue() == "required_num_results"); + }; + SmallVector attributes; + if (const auto passthrough = + main->getAttrOfType("passthrough")) { + llvm::copy_if(passthrough, std::back_inserter(attributes), + [&](Attribute attribute) { + return !isQIRFunctionAttribute(attribute); + }); + } + attributes.append( + {rewriter.getStringAttr(::qir::ENTRY_POINT_ATTR), + rewriter.getStrArrayAttr( + {::qir::OUTPUT_LABELING_SCHEMA_ATTR, ::qir::LABELED_SCHEMA}), + rewriter.getStrArrayAttr( + {::qir::QIR_PROFILES_ATTR, + useAdaptive ? ::qir::ADAPTIVE_PROFILE : ::qir::BASE_PROFILE}), + rewriter.getStrArrayAttr( + {"required_num_qubits", std::to_string(metadata.numQubits)}), + rewriter.getStrArrayAttr( + {"required_num_results", std::to_string(metadata.numResults)})}); main->setAttr("passthrough", rewriter.getArrayAttr(attributes)); mqt::removeEntryPoint(main); rewriter.setInsertionPointToEnd(m.getBody()); - SmallVector flags{ - createI32Flag(LLVM::ModFlagBehavior::Error, "qir_major_version", 2), - createI32Flag(LLVM::ModFlagBehavior::Max, "qir_minor_version", 1), - createBoolFlag(LLVM::ModFlagBehavior::Error, "dynamic_qubit_management", - metadata.useDynamicQubit), - createBoolFlag(LLVM::ModFlagBehavior::Error, - "dynamic_result_management", metadata.useDynamicResult)}; + SmallVector flags = collectUnrelatedModuleFlags(m, rewriter); + flags.emplace_back( + createI32Flag(LLVM::ModFlagBehavior::Error, "qir_major_version", 2)); + flags.emplace_back( + createI32Flag(LLVM::ModFlagBehavior::Max, "qir_minor_version", 1)); + flags.emplace_back(createBoolFlag(LLVM::ModFlagBehavior::Error, + "dynamic_qubit_management", + metadata.useDynamicQubit)); + flags.emplace_back(createBoolFlag(LLVM::ModFlagBehavior::Error, + "dynamic_result_management", + metadata.useDynamicResult)); if (useAdaptive) { flags.emplace_back( @@ -165,7 +250,6 @@ struct QIRSetAttributesAndMetadata final } } - removeExistingModuleFlags(m, rewriter); const auto setTypes = [&](const StringRef name, const llvm::SmallSet& types) { if (types.empty()) { @@ -182,152 +266,266 @@ struct QIRSetAttributesAndMetadata final rewriter.getArrayAttr(flags)); } - /// Remove existing module flag operations from module. - /// Note that this might also erase non-QIR module flag operations, but for - /// now, we assume that there are no others. - static void removeExistingModuleFlags(ModuleOp m, IRRewriter& rewriter) { - SmallVector flagOps; - m->walk([&](LLVM::ModuleFlagsOp op) { flagOps.emplace_back(op); }); - llvm::for_each(flagOps, [&](Operation* op) { rewriter.eraseOp(op); }); + static bool isQIRModuleFlag(StringRef key) { + return key == "qir_major_version" || key == "qir_minor_version" || + key == "dynamic_qubit_management" || + key == "dynamic_result_management" || key == "backwards_branching" || + key == "arrays" || key == "ir_functions" || + key == "multiple_target_branching" || + key == "multiple_return_points" || key == "int_computations" || + key == "float_computations"; + } + + /// Remove existing top-level QIR module flags and return every unrelated + /// flag unchanged. + static SmallVector + collectUnrelatedModuleFlags(ModuleOp m, IRRewriter& rewriter) { + SmallVector preserved; + for (auto flagsOp : + llvm::make_early_inc_range(m.getOps())) { + for (const auto flag : + flagsOp.getFlags().getAsRange()) { + if (!isQIRModuleFlag(flag.getKey().getValue())) { + preserved.emplace_back(flag); + } + } + rewriter.eraseOp(flagsOp); + } + return preserved; } - /// Count the number of uniquely indexed qubit pointers. + /// Return one past the greatest indexed qubit pointer. /// Assumes that qubits are constant integers that are converted to /// an integer pointer and then used in (at least) one quantum instruction. - static size_t getNumQubits(LLVM::LLVMFuncOp& main) { - static constexpr StringRef QIS_PREFIX = "__quantum__qis"; - - DenseSet seen; - main->walk([&](LLVM::ConstantOp constOp) { - if (constOp.use_empty()) { - return; + static LogicalResult + includeStaticPointer(Value pointer, StringRef resource, size_t& capacity, + ModuleOp module, bool requireStatic, + SmallPtrSetImpl& resolving, + SmallPtrSetImpl* aggregates = nullptr) { + auto toPtrOp = pointer.getDefiningOp(); + if (toPtrOp) { + auto constOp = toPtrOp.getArg().getDefiningOp(); + if (!constOp) { + return toPtrOp.emitError() + << "statically addressed QIR " << resource + << " must be converted from an integer constant"; } - const auto intAttr = dyn_cast(constOp.getValue()); - if (!intAttr) { - return; + if (!intAttr || !intAttr.getType().isInteger()) { + return constOp.emitError() + << "QIR " << resource << " index must be an integer constant"; } - - if (!intAttr.getType().isInteger()) { // Not a ": index". - return; + const auto index = intAttr.getValue(); + if (index.isNegative() || index.getActiveBits() >= sizeof(size_t) * 8) { + return constOp.emitError() + << "QIR " << resource + << " index must be non-negative and representable as a host " + "size"; } - - const auto userIt = - llvm::find_if(constOp->getUsers(), [](Operation* user) { - return isa(user); - }); - if (userIt == constOp->user_end()) { - return; + capacity = + std::max(capacity, static_cast(index.getZExtValue()) + 1); + return success(); + } + if (pointer.getDefiningOp()) { + capacity = std::max(capacity, size_t{1}); + return success(); + } + if (auto call = pointer.getDefiningOp(); + call && call.getCallee() && + (*call.getCallee() == QIR_ARRAY_CREATE || + *call.getCallee() == QIR_TUPLE_CREATE)) { + // Generic controlled QIS calls receive aggregate pointers. Their static + // qubit constituents are counted from the stores that populate them. + if (aggregates) { + aggregates->insert(pointer); } + return success(); + } - auto toPtrOp = cast(*userIt); - const auto callIt = - llvm::find_if(toPtrOp->getUses(), [](OpOperand& operand) { - auto callOp = dyn_cast(operand.getOwner()); - if (!callOp) { - return false; - } + auto blockArgument = dyn_cast(pointer); + if (blockArgument) { + if (aggregates) { + aggregates->insert(pointer); + } + Operation* anchor = blockArgument.getOwner()->getParentOp(); + if (!resolving.insert(pointer).second) { + return anchor->emitError() + << "cannot determine a static QIR " << resource + << " index through recursive function arguments"; + } - auto callee = callOp.getCallee(); - if (!callee.has_value()) { - return false; - } + auto function = dyn_cast(anchor); + bool sawDirectCall = false; + LogicalResult status = success(); + if (function && !function.isExternal() && + blockArgument.getOwner() == &function.getBody().front()) { + walkQIRAttributeOperationsIteratively( + module, [&](Operation* operation) { + if (failed(status)) { + return; + } + auto call = dyn_cast(operation); + if (!call || !call.getCallee() || + *call.getCallee() != function.getSymName() || + blockArgument.getArgNumber() >= call.getNumOperands()) { + return; + } + sawDirectCall = true; + status = includeStaticPointer( + call.getOperand(blockArgument.getArgNumber()), resource, + capacity, module, requireStatic, resolving, aggregates); + }); + } + resolving.erase(pointer); + if (failed(status)) { + return failure(); + } + if (sawDirectCall) { + return success(); + } + } - if (*callee == QIR_MEASURE) { + if (!requireStatic) { + return success(); + } + Operation* anchor = pointer.getDefiningOp(); + if (!anchor) { + anchor = cast(pointer).getOwner()->getParentOp(); + } + return anchor->emitError() << "cannot determine the static QIR " << resource + << " index from pointer provenance"; + } - // The following assumes that the first argument of a - // measurement call is the qubit. This may (or may not) hold in - // the future. + [[nodiscard]] static Value getQIRResourceAggregate(Value address) { + if (auto call = address.getDefiningOp()) { + if (call.getCallee() && *call.getCallee() == QIR_ARRAY_ELEMENT && + call.getNumOperands() >= 1) { + return call.getOperand(0); + } + return {}; + } + auto gep = address.getDefiningOp(); + return gep ? gep.getBase() : Value{}; + } - return operand.getOperandNumber() == 0; - } + static FailureOr getNumQubits(ModuleOp scope, bool requireStatic) { + static constexpr StringRef QIS_PREFIX = "__quantum__qis"; - return callee->starts_with(QIS_PREFIX); - }); - if (callIt == toPtrOp->use_end()) { + size_t requiredQubits = 0; + LogicalResult status = success(); + SmallPtrSet qubitAggregates; + SmallVector, 8> aggregateStores; + const auto includePointer = [&](Value pointer) { + SmallPtrSet resolving; + status = includeStaticPointer(pointer, "qubit", requiredQubits, scope, + requireStatic, resolving, &qubitAggregates); + }; + walkQIRAttributeOperationsIteratively(scope, [&](Operation* operation) { + if (failed(status)) { return; } - - // The set ensures that we don't insert the same index multiple times. - seen.insert(intAttr.getValue()); + if (auto store = dyn_cast(operation); + store && isa(store.getValue().getType())) { + if (Value aggregate = getQIRResourceAggregate(store.getAddr())) { + aggregateStores.emplace_back(store, aggregate); + } + return; + } + auto callOp = dyn_cast(operation); + if (!callOp || !callOp.getCallee() || + !callOp.getCallee()->starts_with(QIS_PREFIX)) { + return; + } + for (OpOperand& operand : callOp->getOpOperands()) { + if (*callOp.getCallee() == QIR_MEASURE && + operand.getOperandNumber() != 0) { + continue; + } + if (!isa(operand.get().getType())) { + continue; + } + includePointer(operand.get()); + if (failed(status)) { + return; + } + } }); + if (failed(status)) { + return failure(); + } - return seen.size(); + // Follow only aggregate stores reachable from a qubit-bearing QIS operand. + // The runtime uses opaque pointers for both qubits and results, so scanning + // every QIR array or tuple would misclassify unrelated result aggregates. + SmallPtrSet processedStores; + bool processedStore = false; + do { + processedStore = false; + for (auto& [store, aggregate] : aggregateStores) { + if (!qubitAggregates.contains(aggregate) || + !processedStores.insert(store.getOperation()).second) { + continue; + } + processedStore = true; + includePointer(store.getValue()); + if (failed(status)) { + return failure(); + } + } + } while (processedStore); + return requiredQubits; } - /// Count the number of uniquely indexed result_record_output statements. - static size_t getNumResults(LLVM::LLVMFuncOp& main) { - DenseSet seen; - main->walk([&](LLVM::CallOp callOp) { - if (!callOp.getCallee()) { - return; - } + /// Return the capacity required by all statically indexed result pointers. + static FailureOr getNumResults(ModuleOp scope, bool requireStatic) { + size_t requiredResults = 0; + LogicalResult status = success(); + const auto includePointer = [&](Value pointer) { + SmallPtrSet resolving; + status = includeStaticPointer(pointer, "result", requiredResults, scope, + requireStatic, resolving); + }; - if (*callOp.getCallee() != QIR_RECORD_OUTPUT) { + walkQIRAttributeOperationsIteratively(scope, [&](Operation* operation) { + if (failed(status)) { return; } - - auto operand = callOp->getOperand(0); - auto toPtrOp = dyn_cast(operand.getDefiningOp()); - if (!toPtrOp) { + auto callOp = dyn_cast(operation); + if (!callOp) { return; } - - auto arg = toPtrOp.getArg(); - auto constOp = dyn_cast(arg.getDefiningOp()); - if (!constOp) { + const auto callee = callOp.getCallee(); + if (!callee) { return; } - - const auto intAttr = dyn_cast(constOp.getValue()); - if (!intAttr) { - return; + if (*callee == QIR_MEASURE && callOp.getNumOperands() >= 2) { + includePointer(callOp.getOperand(1)); + } else if ((*callee == QIR_RECORD_OUTPUT || *callee == QIR_READ_RESULT) && + callOp.getNumOperands() >= 1) { + includePointer(callOp.getOperand(0)); } - - // The set ensures that we don't insert the same index multiple times. - seen.insert(intAttr.getValue()); }); - - return seen.size(); + if (failed(status)) { + return failure(); + } + return requiredResults; } /// Determine whether a loop (as a set of blocks) is an iterative loop (true) /// or a conditionally terminated loop (false). static bool classifyLoop(const SmallPtrSet& loop) { + bool hasConditionalTermination = false; for (Block* block : loop) { - Operation* terminator = block->getTerminator(); - assert(terminator != nullptr); - - if (auto condBrOp = dyn_cast(terminator)) { - auto condition = condBrOp.getCondition(); - - if (isa(condition)) { // Ensure that there is a def-op. - return true; - } - - auto callOp = dyn_cast(condition.getDefiningOp()); - - // If the condition is not produced by a measurement call, we - // consider it a basic loop. - if (!callOp || !callOp.getCallee()) { - return true; - } - - // If the condition has been produced by a measurement call - // (e.g. a until-zero-measurement loop), and breaks outside the loop, - // we found a "conditionally terminating loop". - if (*callOp.getCallee() == QIR_READ_RESULT && - (!loop.contains(condBrOp.getTrueDest()) || - !loop.contains(condBrOp.getFalseDest()))) { - return false; - } - - // Unseen edge case (so far): The condition of the terminator - // operation is produced by a function call, which isn't a - // measurement. - return true; + auto condBrOp = dyn_cast_or_null(block->getTerminator()); + if (!condBrOp || (loop.contains(condBrOp.getTrueDest()) && + loop.contains(condBrOp.getFalseDest()))) { + continue; } + auto callOp = condBrOp.getCondition().getDefiningOp(); + hasConditionalTermination |= callOp && callOp.getCallee() && + *callOp.getCallee() == QIR_READ_RESULT; } + return !hasConditionalTermination; } /// Return pair of booleans, indicating whether the entry point uses @@ -346,7 +544,7 @@ struct QIRSetAttributesAndMetadata final Block* tail = █ SmallPtrSet loop{header}; - if (header != tail) { + if (loop.insert(tail).second) { worklist.push_back(tail); } @@ -375,12 +573,16 @@ struct QIRSetAttributesAndMetadata final /// Return triple of booleans, indicating whether the entry point uses /// dynamic qubits = [0], dynamic results = [1], or dynamic arrays = [2]. - static std::tuple usesDynamic(LLVM::LLVMFuncOp& main) { + static std::tuple usesDynamic(Operation* scope) { bool useDynamicQubit{false}; bool useDynamicResult{false}; bool useArrays{false}; - main->walk([&](LLVM::CallOp callOp) { + walkQIRAttributeOperationsIteratively(scope, [&](Operation* operation) { + auto callOp = dyn_cast(operation); + if (!callOp) { + return; + } if (!callOp.getCallee()) { return; } @@ -396,6 +598,12 @@ struct QIRSetAttributesAndMetadata final } else if (name == QIR_RESULT_ARRAY_ALLOC) { useDynamicResult = true; useArrays = true; + } else if (name == QIR_ARRAY_CREATE || name == QIR_ARRAY_ELEMENT || + name == QIR_ARRAY_RELEASE || name == QIR_ARRAY_RECORD_OUTPUT || + name == QIR_RESULT_ARRAY_RECORD_OUTPUT || + name == QIR_QUBIT_ARRAY_RELEASE || + name == QIR_RESULT_ARRAY_RELEASE) { + useArrays = true; } }); @@ -405,7 +613,7 @@ struct QIRSetAttributesAndMetadata final static void collectOptionalFeatures(ModuleOp moduleOp, LLVM::LLVMFuncOp entryPoint, Metadata& metadata) { - const auto recordType = [&](const Type type) { + const auto recordType = [&](Type type) { if (const auto integer = dyn_cast(type); integer && integer.getWidth() > 1) { metadata.integerTypes.insert("i" + std::to_string(integer.getWidth())); @@ -418,9 +626,15 @@ struct QIRSetAttributesAndMetadata final } }; - moduleOp.walk([&](LLVM::LLVMFuncOp function) { + SmallVector functions; + walkQIRAttributeOperationsIteratively(moduleOp, [&](Operation* operation) { + if (auto function = dyn_cast(operation)) { + functions.emplace_back(function); + } + }); + for (auto function : functions) { if (function.isExternal()) { - return; + continue; } metadata.usesIRFunctions |= function != entryPoint; if (function != entryPoint) { @@ -430,30 +644,32 @@ struct QIRSetAttributesAndMetadata final llvm::for_each(block.getArgumentTypes(), recordType); } size_t returnCount = 0; - function.walk([&](Operation* operation) { - returnCount += isa(operation); - metadata.usesMultipleTargetBranching |= isa(operation); - if (operation->hasTrait()) { - return; - } - const auto hasScalarResult = - llvm::any_of(operation->getResultTypes(), [](const Type type) { - return isa(type) || type.isF16() || type.isF32() || - type.isF64(); - }); - if (hasScalarResult && !isa(operation)) { - llvm::for_each(operation->getOperandTypes(), recordType); - } - llvm::for_each(operation->getResultTypes(), recordType); - }); + walkQIRAttributeOperationsIteratively( + function, [&](Operation* operation) { + returnCount += isa(operation); + metadata.usesMultipleTargetBranching |= + isa(operation); + if (operation->hasTrait()) { + return; + } + const auto hasScalarResult = + llvm::any_of(operation->getResultTypes(), [](Type type) { + return isa(type) || type.isF16() || + type.isF32() || type.isF64(); + }); + if (hasScalarResult && !isa(operation)) { + llvm::for_each(operation->getOperandTypes(), recordType); + } + llvm::for_each(operation->getResultTypes(), recordType); + }); metadata.usesMultipleReturnPoints |= returnCount > 1; - }); + } } /// Return the metadata for a QIR base profile compliant program. - static Metadata getBase(LLVM::LLVMFuncOp& main) { - return {.numQubits = getNumQubits(main), - .numResults = getNumResults(main), + static Metadata getBase(size_t numQubits, size_t numResults) { + return {.numQubits = numQubits, + .numResults = numResults, .useDynamicQubit = false, .useDynamicResult = false, .useArrays = false, @@ -461,12 +677,12 @@ struct QIRSetAttributesAndMetadata final } /// Return the metadata for a QIR adaptive profile compliant program. - Metadata getAdaptive(LLVM::LLVMFuncOp& main) { + Metadata getAdaptive(LLVM::LLVMFuncOp& main, size_t numQubits, + size_t numResults, bool useDynamicQubit, + bool useDynamicResult, bool useArrays) { const auto& domInfo = getAnalysis(); const auto [useIteration, useCondTerm] = usesBackwardsBranching(main, domInfo); - const auto [useDynamicQubit, useDynamicResult, useArrays] = - usesDynamic(main); Metadata md; md.useDynamicQubit = useDynamicQubit; @@ -474,11 +690,11 @@ struct QIRSetAttributesAndMetadata final md.useArrays = useArrays; if (!useDynamicQubit) { - md.numQubits = getNumQubits(main); + md.numQubits = numQubits; } if (!useDynamicResult) { - md.numResults = getNumResults(main); + md.numResults = numResults; } if (useIteration) { diff --git a/mlir/lib/Dialect/QIR/Transforms/QIRCleanup.cpp b/mlir/lib/Dialect/QIR/Transforms/QIRCleanup.cpp index 64e3d1dd13..5710045fca 100644 --- a/mlir/lib/Dialect/QIR/Transforms/QIRCleanup.cpp +++ b/mlir/lib/Dialect/QIR/Transforms/QIRCleanup.cpp @@ -10,6 +10,7 @@ #include "mlir/Dialect/QIR/Transforms/Passes.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" +#include "mlir/Support/OperationUtils.h" #include #include @@ -24,6 +25,7 @@ #include #include +#include #include namespace mlir::qir { @@ -31,6 +33,23 @@ namespace mlir::qir { #define GEN_PASS_DEF_QIRCLEANUPPASS #include "mlir/Dialect/QIR/Transforms/Passes.h.inc" +template +static void walkQIRCleanupOperationsIteratively(Operation* root, + Callback&& callback) { + SmallVector worklist{root}; + while (!worklist.empty()) { + Operation* operation = worklist.pop_back_val(); + callback(operation); + for (Region& region : operation->getRegions()) { + for (Block& block : region) { + for (Operation& nested : block) { + worklist.push_back(&nested); + } + } + } + } +} + [[nodiscard]] static StringAttr getMetadataKey(const Attribute attr) { auto pair = dyn_cast(attr); if (!pair || pair.size() != 2) { @@ -53,17 +72,29 @@ namespace mlir::qir { } [[nodiscard]] static bool moduleHasDynamicQubitRuntimeCalls(ModuleOp module) { - return llvm::any_of(module.getOps(), [](LLVM::CallOp callOp) { + bool found = false; + walkQIRCleanupOperationsIteratively(module, [&](Operation* operation) { + auto callOp = dyn_cast(operation); + if (!callOp) { + return; + } const auto callee = getCalleeName(callOp); - return callee == QIR_QUBIT_ALLOC || callee == QIR_QUBIT_ARRAY_ALLOC; + found |= callee == QIR_QUBIT_ALLOC || callee == QIR_QUBIT_ARRAY_ALLOC; }); + return found; } [[nodiscard]] static bool moduleHasDynamicResultRuntimeCalls(ModuleOp module) { - return llvm::any_of(module.getOps(), [](LLVM::CallOp callOp) { + bool found = false; + walkQIRCleanupOperationsIteratively(module, [&](Operation* operation) { + auto callOp = dyn_cast(operation); + if (!callOp) { + return; + } const auto callee = getCalleeName(callOp); - return callee == QIR_RESULT_ALLOC || callee == QIR_RESULT_ARRAY_ALLOC; + found |= callee == QIR_RESULT_ALLOC || callee == QIR_RESULT_ARRAY_ALLOC; }); + return found; } static void dropUnusedExternalDeclarations(ModuleOp module) { @@ -96,23 +127,9 @@ static void normalizeQIRMetadata(ModuleOp module) { return; } - ArrayAttr requiredNumQubitsAttr = nullptr; - ArrayAttr requiredNumResultsAttr = nullptr; - for (const auto attr : passthroughAttr) { - const auto key = getMetadataKey(attr); - if (!key) { - continue; - } - if (key.getValue() == "required_num_qubits") { - requiredNumQubitsAttr = cast(attr); - } else if (key.getValue() == "required_num_results") { - requiredNumResultsAttr = cast(attr); - } - } - OpBuilder builder(module.getContext()); SmallVector updatedMetadata; - updatedMetadata.reserve(passthroughAttr.size() + 2); + updatedMetadata.reserve(passthroughAttr.size()); for (const auto attr : passthroughAttr) { const auto key = getMetadataKey(attr); @@ -122,15 +139,9 @@ static void normalizeQIRMetadata(ModuleOp module) { } if (key.getValue() == "dynamic_qubit_management" && !hasDynamicQubit) { - if (requiredNumQubitsAttr) { - updatedMetadata.push_back(requiredNumQubitsAttr); - } continue; } if (key.getValue() == "dynamic_result_management" && !hasDynamicResult) { - if (requiredNumResultsAttr) { - updatedMetadata.push_back(requiredNumResultsAttr); - } continue; } @@ -151,10 +162,26 @@ namespace { struct RemoveDeadQubitArrayPair final : OpRewritePattern { using OpRewritePattern::OpRewritePattern; + [[nodiscard]] static bool haveEqualIntegerValues(Value lhs, Value rhs) { + if (lhs == rhs) { + return true; + } + auto lhsConstant = lhs.getDefiningOp(); + auto rhsConstant = rhs.getDefiningOp(); + if (!lhsConstant || !rhsConstant) { + return false; + } + const auto lhsValue = dyn_cast(lhsConstant.getValue()); + const auto rhsValue = dyn_cast(rhsConstant.getValue()); + return lhsValue && rhsValue && lhsValue == rhsValue; + } + LogicalResult matchAndRewrite(LLVM::CallOp releaseCall, PatternRewriter& rewriter) const override { if (getCalleeName(releaseCall) != QIR_QUBIT_ARRAY_RELEASE || - releaseCall.getNumOperands() < 2) { + releaseCall.getNumOperands() != 2 || releaseCall.getNumResults() != 0 || + !releaseCall.getOperand(0).getType().isInteger(64) || + !isa(releaseCall.getOperand(1).getType())) { return failure(); } @@ -175,8 +202,12 @@ struct RemoveDeadQubitArrayPair final : OpRewritePattern { } if (getCalleeName(callOp) != QIR_QUBIT_ARRAY_ALLOC || - callOp.getNumOperands() < 2 || - callOp.getOperand(1) != allocaOp.getResult()) { + callOp.getNumOperands() != 3 || callOp.getNumResults() != 0 || + !callOp.getOperand(0).getType().isInteger(64) || + !isa(callOp.getOperand(1).getType()) || + !isa(callOp.getOperand(2).getType()) || + callOp.getOperand(1) != allocaOp.getResult() || + !callOp.getOperand(2).getDefiningOp()) { return failure(); } if (allocCall != nullptr) { @@ -188,6 +219,14 @@ struct RemoveDeadQubitArrayPair final : OpRewritePattern { if (!allocCall) { return failure(); } + if (allocCall->getBlock() != releaseCall->getBlock() || + !allocCall->isBeforeInBlock(releaseCall) || + !haveEqualIntegerValues(allocCall.getOperand(0), + releaseCall.getOperand(0)) || + !haveEqualIntegerValues(allocCall.getOperand(0), + allocaOp.getArraySize())) { + return failure(); + } rewriter.eraseOp(releaseCall); rewriter.eraseOp(allocCall); @@ -207,6 +246,11 @@ struct QIRCleanupPass final : impl::QIRCleanupPassBase { protected: void runOnOperation() override { auto module = getOperation(); + constexpr size_t maxRegionNesting = 64; + if (failed(verifyRegionNestingDepth(module, maxRegionNesting))) { + signalPassFailure(); + return; + } RewritePatternSet patterns(&getContext()); patterns.add(&getContext()); diff --git a/mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp b/mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp index 5929e072c5..c315130ee7 100644 --- a/mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp +++ b/mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp @@ -282,15 +282,26 @@ LLVM::LLVMFuncOp getOrCreateFunctionDeclaration(OpBuilder& builder, builder.setInsertionPointToEnd(moduleOp.getBody()); fnDecl = LLVM::LLVMFuncOp::create(builder, op->getLoc(), fnName, fnType); + } - // Add irreversible attribute to irreversible quantum operations - if (fnName == QIR_MEASURE || fnName == QIR_RESET) { - fnDecl->setAttr("passthrough", - builder.getStrArrayAttr({::qir::IRREVERSIBLE_ATTR})); - } + auto function = cast(fnDecl); + if (fnName != QIR_MEASURE && fnName != QIR_RESET) { + return function; } - return cast(fnDecl); + const auto irreversible = builder.getStringAttr(::qir::IRREVERSIBLE_ATTR); + const auto passthrough = function->getAttrOfType("passthrough"); + if (passthrough && llvm::is_contained(passthrough, irreversible)) { + return function; + } + + SmallVector entries; + if (passthrough) { + entries.append(passthrough.begin(), passthrough.end()); + } + entries.push_back(irreversible); + function->setAttr("passthrough", builder.getArrayAttr(entries)); + return function; } LLVM::AddressOfOp createResultLabel(OpBuilder& builder, Operation* op, diff --git a/mlir/lib/Dialect/QTensor/Transforms/ShrinkRegisters.cpp b/mlir/lib/Dialect/QTensor/Transforms/ShrinkRegisters.cpp index cba3aba988..366bdb4b2a 100644 --- a/mlir/lib/Dialect/QTensor/Transforms/ShrinkRegisters.cpp +++ b/mlir/lib/Dialect/QTensor/Transforms/ShrinkRegisters.cpp @@ -10,7 +10,10 @@ #include "mlir/Dialect/QTensor/IR/QTensorOps.h" #include "mlir/Dialect/QTensor/Transforms/Passes.h" +#include "mlir/Support/OperationUtils.h" +#include +#include #include #include #include @@ -21,7 +24,6 @@ #include #include -#include #include #include #include @@ -31,63 +33,36 @@ namespace mlir::qtensor { #define GEN_PASS_DEF_SHRINKQTENSORTOFITPASS #include "mlir/Dialect/QTensor/Transforms/Passes.h.inc" -/** - * @brief Return the unique user of a linear qtensor value. - */ -[[nodiscard]] static Operation* getLinearTensorUser(Value tensor) { - assert(tensor.hasOneUse() && "Expected a linear tensor with exactly one use"); - return *tensor.getUsers().begin(); -} - /** * @brief Mark a single live index. */ -[[nodiscard]] static LogicalResult markLiveIndex(const int64_t index, - BitVector& liveIndices) { - if (index < 0 || std::cmp_greater_equal(index, liveIndices.size())) { +[[nodiscard]] static LogicalResult +markLiveIndex(int64_t index, int64_t tensorSize, + llvm::SmallDenseSet& liveIndices) { + if (index < 0 || index >= tensorSize) { return failure(); } - liveIndices.set(static_cast(index)); + liveIndices.insert(index); return success(); } -/** - * @brief Redirect the tensor operand from @p from to @p to. - */ -[[nodiscard]] static LogicalResult remapTensorOperand(Operation* op, Value from, - Value to) { - if (auto extractOp = dyn_cast(op)) { - if (extractOp.getTensor() != from) { - return failure(); - } - extractOp->setOperand(0, to); - return success(); - } - if (auto insertOp = dyn_cast(op)) { - if (insertOp.getDest() != from) { - return failure(); - } - insertOp->setOperand(1, to); - return success(); - } - if (auto deallocOp = dyn_cast(op)) { - if (deallocOp.getTensor() != from) { - return failure(); - } - deallocOp->setOperand(0, to); - return success(); - } - return failure(); -} +struct TensorAccess { + Operation* operation; + int64_t index; +}; /** - * @brief Walk alloc->dealloc and collect all touched indices. + * @brief Walk alloc->dealloc and plan all accesses without changing the IR. */ -[[nodiscard]] static LogicalResult -collectLiveIndices(AllocOp allocOp, BitVector& live, DeallocOp& deallocOp) { +[[nodiscard]] static LogicalResult collectTensorChain( + AllocOp allocOp, int64_t tensorSize, llvm::SmallDenseSet& live, + SmallVectorImpl& accesses, DeallocOp& deallocOp) { auto tensor = allocOp.getResult(); while (true) { - auto* user = getLinearTensorUser(tensor); + if (!tensor.hasOneUse()) { + return failure(); + } + auto* user = *tensor.getUsers().begin(); if (auto currentDealloc = dyn_cast(user)) { if (currentDealloc.getTensor() != tensor) { @@ -102,9 +77,10 @@ collectLiveIndices(AllocOp allocOp, BitVector& live, DeallocOp& deallocOp) { return failure(); } auto index = getConstantIntValue(extractOp.getIndex()); - if (!index || failed(markLiveIndex(*index, live))) { + if (!index || failed(markLiveIndex(*index, tensorSize, live))) { return failure(); } + accesses.push_back({extractOp, *index}); tensor = extractOp.getOutTensor(); continue; } @@ -114,9 +90,10 @@ collectLiveIndices(AllocOp allocOp, BitVector& live, DeallocOp& deallocOp) { return failure(); } auto index = getConstantIntValue(insertOp.getIndex()); - if (!index || failed(markLiveIndex(*index, live))) { + if (!index || failed(markLiveIndex(*index, tensorSize, live))) { return failure(); } + accesses.push_back({insertOp, *index}); tensor = insertOp.getResult(); continue; } @@ -141,9 +118,11 @@ struct ShrinkStaticQTensor final : OpRewritePattern { return failure(); } - BitVector live(static_cast(*oldSize), false); + llvm::SmallDenseSet live; + SmallVector accesses; DeallocOp oldDeallocOp{}; - if (failed(collectLiveIndices(allocOp, live, oldDeallocOp))) { + if (failed(collectTensorChain(allocOp, *oldSize, live, accesses, + oldDeallocOp))) { return failure(); } @@ -151,57 +130,41 @@ struct ShrinkStaticQTensor final : OpRewritePattern { return failure(); } - SmallVector newIndexByOldIndex(static_cast(*oldSize), -1); - int64_t newSize = 0; - for (int64_t index = 0; index < *oldSize; ++index) { - if (live.test(static_cast(index))) { - newIndexByOldIndex[static_cast(index)] = newSize++; - } + SmallVector liveIndices(live.begin(), live.end()); + llvm::sort(liveIndices); + const auto newSize = static_cast(liveIndices.size()); + DenseMap newIndexByOldIndex; + for (auto [newIndex, oldIndex] : llvm::enumerate(liveIndices)) { + newIndexByOldIndex.try_emplace(oldIndex, static_cast(newIndex)); } if (newSize <= 0 || newSize == *oldSize) { return failure(); } + SmallVector mappedIndices; + mappedIndices.reserve(accesses.size()); + for (const auto& access : accesses) { + const auto mapped = newIndexByOldIndex.find(access.index); + if (mapped == newIndexByOldIndex.end()) { + return failure(); + } + mappedIndices.push_back(mapped->second); + } + rewriter.setInsertionPoint(allocOp); auto size = arith::ConstantIndexOp::create(rewriter, allocOp.getLoc(), newSize); auto newAlloc = AllocOp::create(rewriter, allocOp.getLoc(), size.getResult()); - newAlloc->setDiscardableAttrs(allocOp->getDiscardableAttrDictionary()); + rewriter.modifyOpInPlace(newAlloc, [&] { + newAlloc->setDiscardableAttrs(allocOp->getDiscardableAttrDictionary()); + }); - auto oldTensor = allocOp.getResult(); auto currentTensor = newAlloc.getResult(); - while (true) { - Operation* currentOp = getLinearTensorUser(oldTensor); - - if (auto deallocOp = dyn_cast(currentOp)) { - if (deallocOp != oldDeallocOp || deallocOp.getTensor() != oldTensor) { - return failure(); - } - rewriter.setInsertionPoint(deallocOp); - DeallocOp::create(rewriter, deallocOp.getLoc(), currentTensor); - rewriter.eraseOp(deallocOp); - break; - } - - if (auto extractOp = dyn_cast(currentOp)) { - if (extractOp.getTensor() != oldTensor) { - return failure(); - } - const auto oldIndex = *getConstantIntValue(extractOp.getIndex()); - if (oldIndex < 0 || - std::cmp_greater_equal(oldIndex, newIndexByOldIndex.size())) { - return failure(); - } - const auto mappedIndex = - newIndexByOldIndex[static_cast(oldIndex)]; - if (mappedIndex < 0) { - return failure(); - } - auto oldOutTensor = extractOp.getOutTensor(); - auto* nextOp = getLinearTensorUser(oldOutTensor); - + for (const auto [access, mappedIndex] : + llvm::zip_equal(accesses, mappedIndices)) { + if (auto extractOp = dyn_cast(access.operation)) { rewriter.setInsertionPoint(extractOp); auto index = arith::ConstantIndexOp::create( rewriter, extractOp.getLoc(), mappedIndex); @@ -209,50 +172,28 @@ struct ShrinkStaticQTensor final : OpRewritePattern { currentTensor, index.getResult()); rewriter.replaceAllUsesWith(extractOp.getResult(), newExtract.getResult()); - currentTensor = newExtract.getOutTensor(); - if (failed(remapTensorOperand(nextOp, oldOutTensor, oldTensor))) { - return failure(); - } - rewriter.eraseOp(extractOp); continue; } - if (auto insertOp = dyn_cast(currentOp)) { - if (insertOp.getDest() != oldTensor) { - return failure(); - } - const auto oldIndex = *getConstantIntValue(insertOp.getIndex()); - if (oldIndex < 0 || - std::cmp_greater_equal(oldIndex, newIndexByOldIndex.size())) { - return failure(); - } - const auto mappedIndex = - newIndexByOldIndex[static_cast(oldIndex)]; - if (mappedIndex < 0) { - return failure(); - } - auto oldResultTensor = insertOp.getResult(); - auto* nextOp = getLinearTensorUser(oldResultTensor); + auto insertOp = cast(access.operation); + rewriter.setInsertionPoint(insertOp); + auto index = arith::ConstantIndexOp::create(rewriter, insertOp.getLoc(), + mappedIndex); + auto newInsert = + InsertOp::create(rewriter, insertOp.getLoc(), insertOp.getScalar(), + currentTensor, index.getResult()); - rewriter.setInsertionPoint(insertOp); - auto index = arith::ConstantIndexOp::create(rewriter, insertOp.getLoc(), - mappedIndex); - auto newInsert = - InsertOp::create(rewriter, insertOp.getLoc(), insertOp.getScalar(), - currentTensor, index.getResult()); + currentTensor = newInsert.getResult(); + } - currentTensor = newInsert.getResult(); - if (failed(remapTensorOperand(nextOp, oldResultTensor, oldTensor))) { - return failure(); - } - rewriter.eraseOp(insertOp); - continue; - } + rewriter.setInsertionPoint(oldDeallocOp); + DeallocOp::create(rewriter, oldDeallocOp.getLoc(), currentTensor); - return failure(); + rewriter.eraseOp(oldDeallocOp); + for (const auto& access : llvm::reverse(accesses)) { + rewriter.eraseOp(access.operation); } - rewriter.eraseOp(allocOp); return success(); } @@ -262,6 +203,11 @@ struct ShrinkQTensorToFitPass final : impl::ShrinkQTensorToFitPassBase { protected: void runOnOperation() override { + constexpr size_t maxRegionNesting = 64; + if (failed(verifyRegionNestingDepth(getOperation(), maxRegionNesting))) { + signalPassFailure(); + return; + } RewritePatternSet patterns(&getContext()); patterns.add(&getContext()); diff --git a/mlir/lib/Support/Passes.cpp b/mlir/lib/Support/Passes.cpp index 8c1302a361..b8e2290aab 100644 --- a/mlir/lib/Support/Passes.cpp +++ b/mlir/lib/Support/Passes.cpp @@ -125,8 +125,8 @@ void populateQCCleanupPipeline(OpPassManager& pm) { } void populateQCOCleanupPipeline(OpPassManager& pm) { - pm.addPass(createCanonicalizerPass( - GreedyRewriteConfig{}.setMaxIterations(GreedyRewriteConfig::kNoLimit))); + pm.addPass( + createCanonicalizerPass(GreedyRewriteConfig{}.setMaxIterations(64))); pm.addPass(mlir::mqt::createNormalizeGlobalPhases()); pm.addPass(createCSEPass()); pm.addPass(qtensor::createShrinkQTensorToFitPass()); diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index fb88e19425..46ba31daa5 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -8,6 +8,7 @@ * Licensed under the MIT License */ +#include "mlir/Compiler/Programs.h" #include "mlir/Compiler/QDMIAdapter.h" #include "mlir/Compiler/TargetCompilation.h" #include "mlir/Conversion/JeffToQCO/JeffToQCO.h" @@ -29,7 +30,6 @@ #include "mlir/Support/Passes.h" #include -#include #include #include #include @@ -66,6 +66,7 @@ #include #include +#include #include #include #include @@ -300,26 +301,20 @@ static ParsedProgram loadJeffFile(const StringRef filename, return {}; } - std::string errorMessage; - if (!openInputFile(filename, &errorMessage)) { - llvm::errs() << "Failed to load file '" << filename << "': '" - << errorMessage << "'\n"; - return {}; - } - - auto mod = deserializeFromFile(context, filename); - if (!mod) { + auto mod = detail::deserializeJeffFile(context, + std::filesystem::path(filename.str())); + if (failed(mod)) { llvm::errs() << "Failed to deserialize jeff file '" << filename << "'.\n"; return {}; } PassManager pm(context); pm.addPass(createJeffToQCO()); - if (pm.run(*mod).failed()) { + if (pm.run(**mod).failed()) { llvm::errs() << "Failed to convert jeff input to QCO.\n"; return {}; } - return {.mod = std::move(mod), .dialect = InputDialect::QCO}; + return {.mod = std::move(*mod), .dialect = InputDialect::QCO}; } /** @@ -483,6 +478,9 @@ static int runCompiler(int argc, char** argv) { if (!program.mod) { return 1; } + if (failed(mqt::verifyProgramMetadata(*program.mod))) { + return 1; + } if (*parsedOutputFormat == OutputFormat::QCImport && program.dialect != InputDialect::QC) { @@ -514,7 +512,10 @@ static int runCompiler(int argc, char** argv) { if (failed(populate(pm))) { return failure(); } - return pm.run(*program.mod); + if (failed(pm.run(*program.mod))) { + return failure(); + } + return mqt::verifyProgramMetadata(*program.mod); }; if (*parsedOutputFormat != OutputFormat::QCImport && diff --git a/mlir/unittests/Compiler/CMakeLists.txt b/mlir/unittests/Compiler/CMakeLists.txt index 122f4ff745..b4a5d5d673 100644 --- a/mlir/unittests/Compiler/CMakeLists.txt +++ b/mlir/unittests/Compiler/CMakeLists.txt @@ -31,10 +31,12 @@ mqt_copy_qdmi_runtime(mqt-core-mlir-unittests-compiler MQT::CoreQDMIScDevice target_compile_definitions( mqt-core-mlir-unittests-compiler PRIVATE + MQT_CORE_MLIR_MQT_CC="$" MQT_CORE_MLIR_HETEROGENEOUS_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/heterogeneous-sc.json" MQT_CORE_MLIR_DIRECTIONAL_ONE_WAY_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/directional-one-way-sc.json" MQT_CORE_MLIR_DIRECTIONAL_TWO_WAY_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/directional-two-way-sc.json" ) +add_dependencies(mqt-core-mlir-unittests-compiler mqt-cc) mqt_mlir_configure_unittest_target(mqt-core-mlir-unittests-compiler REQUIRES_EH) diff --git a/mlir/unittests/Compiler/mqt-cc/verify_invalid_mlir.cmake b/mlir/unittests/Compiler/mqt-cc/verify_invalid_mlir.cmake index 06e577ae1c..0d0e8c43ed 100644 --- a/mlir/unittests/Compiler/mqt-cc/verify_invalid_mlir.cmake +++ b/mlir/unittests/Compiler/mqt-cc/verify_invalid_mlir.cmake @@ -19,7 +19,14 @@ endfunction() file(MAKE_DIRECTORY "${OUTPUT_DIR}") set(input_file "${OUTPUT_DIR}/invalid.mlir") file(WRITE "${input_file}" "module {\n") +set(duplicate_entry_points "${OUTPUT_DIR}/duplicate-entry-points.mlir") +file( + WRITE "${duplicate_entry_points}" + "module {\n func.func @first() attributes {mqt.entry_point} { return }\n func.func @second() attributes {mqt.entry_point} { return }\n}\n" +) require_failure("invalid MLIR" "expected operation name" "${MQT_CC}" "${input_file}") +require_failure("duplicate entry points" "module must contain at most one program entry point" + "${MQT_CC}" "${duplicate_entry_points}" "--emit=qc-import") require_failure("nonlinear QCO" "expected linear QCO value to have exactly one use" "${MQT_CC}" "${NONLINEAR_QCO_INPUT}" "--emit=qco") diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index ecf0abe6e4..7d3e5a70e0 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -30,11 +30,15 @@ #include "qco_programs.h" #include "qir_programs.h" +#include +#include #include +#include #include #include #include #include +#include #include #include #include @@ -58,10 +62,12 @@ #include #include +#include #include #include #include #include +#include #include #include #include @@ -124,7 +130,7 @@ class CompilerPipelineTest qtensor::QTensorDialect, arith::ArithDialect, cf::ControlFlowDialect, func::FuncDialect, math::MathDialect, memref::MemRefDialect, scf::SCFDialect, - LLVM::LLVMDialect, jeff::JeffDialect>(); + LLVM::LLVMDialect, mlir::jeff::JeffDialect>(); context = std::make_unique(); context->appendDialectRegistry(registry); context->loadAllAvailableDialects(); @@ -302,6 +308,57 @@ TEST(CompilerProgramOwnershipTest, ValidatesAndOwnsExistingQCModules) { EXPECT_FALSE( QCProgram::fromModule(otherContext, std::move(mismatchedModule))); } + +TEST(CompilerProgramOwnershipTest, + EnforcesProgramMetadataAtImportAndPassBoundaries) { + constexpr llvm::StringLiteral validSource = R"mlir(module { + func.func @main() attributes {mqt.entry_point} { + %qubit = qc.alloc : !qc.qubit + qc.dealloc %qubit : !qc.qubit + return + } + func.func @helper() { return } + })mlir"; + constexpr llvm::StringLiteral duplicateEntryPoints = R"mlir(module { + func.func @main() attributes {mqt.entry_point} { + %qubit = qc.alloc : !qc.qubit + qc.dealloc %qubit : !qc.qubit + return + } + func.func @other() attributes {mqt.entry_point} { return } + })mlir"; + + auto program = QCProgram::fromMLIRString(validSource); + ASSERT_TRUE(program); + auto helper = program->module().lookupSymbol("helper"); + ASSERT_TRUE(helper); + mlir::mqt::setEntryPoint(helper); + EXPECT_FALSE(program->cleanup()); + EXPECT_FALSE(program->normalizeGlobalPhases()); + EXPECT_FALSE(runDefaultPipeline(CompilerInput{std::move(*program)}, + ProgramFormat::QCImport)); + + EXPECT_FALSE(QCProgram::fromMLIRString(duplicateEntryPoints)); + + constexpr llvm::StringLiteral validQCOSource = R"mlir(module { + func.func @main() attributes {mqt.entry_point} { + %qubit = qco.alloc : !qco.qubit + qco.sink %qubit : !qco.qubit + return + } + func.func @helper() { return } + })mlir"; + auto qcoProgram = QCOProgram::fromMLIRString(validQCOSource); + ASSERT_TRUE(qcoProgram); + helper = qcoProgram->module().lookupSymbol("helper"); + ASSERT_TRUE(helper); + mlir::mqt::setEntryPoint(helper); + EXPECT_FALSE(qcoProgram->runPassPipeline("canonicalize")); + EXPECT_FALSE(qcoProgram->normalizeGlobalPhases()); + EXPECT_FALSE(runDefaultPipeline(CompilerInput{std::move(*qcoProgram)}, + ProgramFormat::QCO)); +} + TEST(CompilerProgramOwnershipTest, EnforcesQCOLinearityAtPublicBoundaries) { DialectRegistry registry; registry.insert(); + ub::UBDialect, LLVM::LLVMDialect, mlir::jeff::JeffDialect>(); MLIRContext context(registry); context.loadAllAvailableDialects(); auto moduleOp = parseSourceString(ir, &context); @@ -1202,6 +1259,65 @@ TEST_F(CompilerPipelineTest, TypedProgramsNormalizeGlobalPhases) { EXPECT_EQ(StringRef(textual->str()).count("qco.gphase"), 1); } +[[nodiscard]] static ::jeff::Module::Builder +initializeCurrentJeffModule(capnp::MallocMessageBuilder& message) { + auto module = message.initRoot<::jeff::Module>(); + module.setVersion(0); + module.setVersionMinor(3); + module.setVersionPatch(0); + return module; +} + +[[nodiscard]] static std::vector +serializeJeffMessage(capnp::MessageBuilder& message) { + const auto words = capnp::messageToFlatArray(message); + const auto serialized = words.asBytes(); + std::vector bytes(serialized.size()); + std::memcpy(bytes.data(), serialized.begin(), serialized.size()); + return bytes; +} + +static void expectJeffImportFailure(const std::span bytes, + const StringRef stem, + const StringRef expectedDiagnostic) { + EXPECT_FALSE(JeffProgram::fromBytes(bytes)); + + const auto path = + std::filesystem::path(testing::TempDir()) / (stem + ".jeff").str(); + std::ofstream output(path, std::ios::binary); + if (!bytes.empty()) { + output.write(reinterpret_cast(bytes.data()), + static_cast(bytes.size())); + } + output.close(); + ASSERT_TRUE(output.good()); + EXPECT_FALSE(JeffProgram::fromFile(path)); + + const auto errorPath = + std::filesystem::path(testing::TempDir()) / (stem + ".stderr").str(); + const auto executable = StringRef(MQT_CORE_MLIR_MQT_CC); + const auto pathString = path.string(); + const auto errorPathString = errorPath.string(); + const SmallVector arguments{executable, pathString, + "--input-format=jeff", "--emit=qco"}; + const std::array, 3> redirects{ + std::nullopt, std::nullopt, StringRef(errorPathString)}; + std::string executionError; + bool executionFailed = false; + EXPECT_EQ(llvm::sys::ExecuteAndWait(executable, arguments, std::nullopt, + redirects, 10, 0, &executionError, + &executionFailed), + 1); + EXPECT_FALSE(executionFailed) << executionError; + + std::ifstream errorOutput(errorPath); + ASSERT_TRUE(errorOutput.good()); + const std::string errorText((std::istreambuf_iterator(errorOutput)), + std::istreambuf_iterator()); + EXPECT_NE(errorText.find(expectedDiagnostic.str()), std::string::npos) + << errorText; +} + /** * @brief Test: jeff programs round-trip through their binary APIs */ @@ -1245,6 +1361,142 @@ x q; EXPECT_FALSE(jeff.write(path.parent_path() / "missing" / "output.jeff")); } +/** + * @brief Test: unsupported jeff declarations fail at public import boundaries + */ +TEST_F(CompilerPipelineTest, JeffFunctionDeclarationsAreRejected) { + capnp::MallocMessageBuilder message; + auto module = initializeCurrentJeffModule(message); + auto strings = module.initStrings(2); + strings.set(0, "main"); + strings.set(1, "external"); + + auto functions = module.initFunctions(2); + functions[0].setName(0); + auto definition = functions[0].initDefinition(); + definition.initValues(0); + auto body = definition.initBody(); + body.initSources(0); + body.initTargets(0); + body.initOperations(0); + + functions[1].setName(1); + auto declaration = functions[1].initDeclaration(); + declaration.initInputs(0); + declaration.initOutputs(0); + module.setEntrypoint(0); + + expectJeffImportFailure(serializeJeffMessage(message), + "unsupported_declaration", + "jeff function declarations are not supported"); +} + +TEST_F(CompilerPipelineTest, MalformedJeffStructuresAreRejected) { + expectJeffImportFailure({}, "empty_jeff", "jeff data must not be empty"); + const std::vector malformed(sizeof(capnp::word)); + expectJeffImportFailure(malformed, "malformed_jeff", + "failed to parse jeff data"); + + { + capnp::MallocMessageBuilder message; + std::ignore = initializeCurrentJeffModule(message); + expectJeffImportFailure(serializeJeffMessage(message), "missing_functions", + "jeff module must contain a functions list"); + } + { + capnp::MallocMessageBuilder message; + auto module = initializeCurrentJeffModule(message); + module.initStrings(1).set(0, "main"); + auto function = module.initFunctions(1)[0]; + function.setName(0); + std::ignore = function.initDefinition(); + module.setEntrypoint(0); + expectJeffImportFailure(serializeJeffMessage(message), "missing_body", + "jeff function definition must contain a body"); + } + { + capnp::MallocMessageBuilder message; + auto module = initializeCurrentJeffModule(message); + module.initStrings(1).set(0, "main"); + auto function = module.initFunctions(1)[0]; + function.setName(0); + auto definition = function.initDefinition(); + definition.initValues(0); + auto body = definition.initBody(); + body.initSources(0); + body.initTargets(0); + module.setEntrypoint(0); + expectJeffImportFailure( + serializeJeffMessage(message), "missing_operations", + "jeff function body must contain an operations list"); + } +} + +TEST_F(CompilerPipelineTest, InvalidJeffSemanticsAreRejectedWithoutExiting) { + { + capnp::MallocMessageBuilder message; + auto module = initializeCurrentJeffModule(message); + module.initStrings(1).set(0, "main"); + auto function = module.initFunctions(1)[0]; + function.setName(0); + auto definition = function.initDefinition(); + definition.initValues(1)[0].initType().setQubit(); + auto body = definition.initBody(); + body.initSources(0); + body.initTargets(0); + auto operation = body.initOperations(1)[0]; + operation.initInputs(1).set(0, 0); + operation.initOutputs(0); + operation.initInstruction().initQubit().setFree(); + module.setEntrypoint(0); + expectJeffImportFailure(serializeJeffMessage(message), "undefined_value", + "failed to deserialize jeff data: Value not found"); + } + { + capnp::MallocMessageBuilder message; + auto module = initializeCurrentJeffModule(message); + module.initStrings(1).set(0, "main"); + auto functions = module.initFunctions(2); + for (auto function : functions) { + function.setName(0); + auto definition = function.initDefinition(); + definition.initValues(0); + auto body = definition.initBody(); + body.initSources(0); + body.initTargets(0); + body.initOperations(0); + } + module.setEntrypoint(0); + expectJeffImportFailure( + serializeJeffMessage(message), "duplicate_function", + "failed to deserialize jeff data: Verification of MLIR module failed"); + } + { + capnp::MallocMessageBuilder message; + auto module = initializeCurrentJeffModule(message); + module.initStrings(1).set(0, "main"); + auto function = module.initFunctions(1)[0]; + function.setName(0); + auto definition = function.initDefinition(); + auto values = definition.initValues(2); + values[0].initType().setInt(32); + values[1].initType().setInt(32); + auto body = definition.initBody(); + body.initSources(1).set(0, 0); + body.initTargets(1).set(0, 1); + auto operation = body.initOperations(1)[0]; + auto inputs = operation.initInputs(2); + inputs.set(0, 0); + inputs.set(1, 0); + operation.initOutputs(1).set(0, 1); + operation.initInstruction().initIntArray().setGetIndex(); + module.setEntrypoint(0); + expectJeffImportFailure( + serializeJeffMessage(message), "invalid_array_type", + "jeff integer-array get requires an integer-array input"); + } +} + /** * @brief Test: QCO and QIR typed programs retain their respective semantics */ @@ -1940,6 +2192,21 @@ barrier q[0], q[1]; EXPECT_EQ(qc->numTwoQubitGates(), 3); } +TEST_F(CompilerPipelineTest, QCProgramCountGatesWithoutEntryPoint) { + constexpr llvm::StringLiteral source = R"mlir(module { + func.func @helper() { + %qubit = qc.alloc : !qc.qubit + qc.dealloc %qubit : !qc.qubit + return + } + })mlir"; + auto qc = QCProgram::fromMLIRString(source); + ASSERT_TRUE(qc); + EXPECT_EQ(qc->numGates(), 0); + EXPECT_EQ(qc->numSingleQubitGates(), 0); + EXPECT_EQ(qc->numTwoQubitGates(), 0); +} + /** * @brief Test: gate counting includes each structured control-flow region once. */ diff --git a/mlir/unittests/Conversion/CBitToMemRef/test_cbit_to_memref.cpp b/mlir/unittests/Conversion/CBitToMemRef/test_cbit_to_memref.cpp index 39723e68f7..9e4b56ef5f 100644 --- a/mlir/unittests/Conversion/CBitToMemRef/test_cbit_to_memref.cpp +++ b/mlir/unittests/Conversion/CBitToMemRef/test_cbit_to_memref.cpp @@ -21,13 +21,16 @@ #include #include #include +#include #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -38,6 +41,7 @@ #include #include +#include using namespace mlir; @@ -68,6 +72,58 @@ class CBitToMemRefTest : public ::testing::Test { } }; +TEST(CBitToMemRefPassContract, IsModuleAnchoredAndDeclaresCreatedDialects) { + auto pass = createConvertCBitToMemRef(); + ASSERT_TRUE(pass->getOpName()); + EXPECT_EQ(*pass->getOpName(), ModuleOp::getOperationName()); + + DialectRegistry registry; + pass->getDependentDialects(registry); + EXPECT_TRUE( + registry.getDialectAllocator(arith::ArithDialect::getDialectNamespace())); + EXPECT_TRUE(registry.getDialectAllocator( + cf::ControlFlowDialect::getDialectNamespace())); + EXPECT_TRUE( + registry.getDialectAllocator(func::FuncDialect::getDialectNamespace())); + EXPECT_TRUE(registry.getDialectAllocator( + memref::MemRefDialect::getDialectNamespace())); + EXPECT_TRUE( + registry.getDialectAllocator(scf::SCFDialect::getDialectNamespace())); +} + +TEST_F(CBitToMemRefTest, ConversionFailureDoesNotMutateInput) { + constexpr size_t depth = 64; + std::string source = R"mlir( + module { + func.func @main() { + )mlir"; + for (size_t i = 0; i < depth; ++i) { + source += " scf.execute_region {\n"; + } + source += R"mlir( + %reg = cbit.alloc(#cbit.init) : !cbit.reg<1> + )mlir"; + for (size_t i = 0; i < depth; ++i) { + source += " scf.yield\n }\n"; + } + source += R"mlir( + return + } + } + )mlir"; + auto moduleOp = parseSourceString(source, context.get()); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + OwningOpRef original(moduleOp->clone()); + + PassManager manager(context.get()); + manager.addPass(createConvertCBitToMemRef()); + EXPECT_TRUE(failed(manager.run(*moduleOp))); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + moduleOp->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + TEST_F(CBitToMemRefTest, LowersInitializationLoadsAndStores) { auto moduleOp = convert(R"mlir( module { @@ -103,12 +159,36 @@ TEST_F(CBitToMemRefTest, LowersInitializationLoadsAndStores) { moduleOp->walk([&](memref::StoreOp) { ++stores; }); moduleOp->walk([&](memref::LoadOp) { ++loads; }); EXPECT_EQ(allocations, 2); - EXPECT_EQ(stores, 3); + EXPECT_EQ(stores, 2); EXPECT_EQ(loads, 1); ASSERT_TRUE(registerName); EXPECT_EQ(registerName.getValue(), "result"); } +TEST_F(CBitToMemRefTest, LargeZeroInitializationProducesBoundedIR) { + auto moduleOp = convert(R"mlir( + module { + func.func @main() { + %reg = cbit.alloc(#cbit.init) : !cbit.reg<1000000000> + return + } + } + )mlir"); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + SmallVector loops; + moduleOp->walk([&](scf::ForOp loop) { loops.emplace_back(loop); }); + ASSERT_EQ(loops.size(), 1); + EXPECT_EQ(getConstantIntValue(loops.front().getLowerBound()), 0); + EXPECT_EQ(getConstantIntValue(loops.front().getUpperBound()), 1000000000); + EXPECT_EQ(getConstantIntValue(loops.front().getStep()), 1); + + size_t stores = 0; + loops.front().getBody()->walk([&](memref::StoreOp) { ++stores; }); + EXPECT_EQ(stores, 1); +} + TEST_F(CBitToMemRefTest, ConvertsFunctionSignaturesCallsAndReturns) { auto moduleOp = convert(R"mlir( module { diff --git a/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp b/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp index 6d0dd770c8..e52f884681 100644 --- a/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp +++ b/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp @@ -16,9 +16,11 @@ #include "mlir/Dialect/CBit/IR/CBitOps.h" #include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Transforms/Passes.h" +#include "mlir/Dialect/QC/IR/QCDialect.h" #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/QCOUtils.h" #include "mlir/Support/Passes.h" #include "qco_programs.h" @@ -40,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -377,6 +380,7 @@ TEST(JeffRoundTripRegressionTest, RejectsInvalidJeffModuleMetadata) { const auto rejects = [&](const ArrayRef attributes, const StringRef expected) { + SCOPED_TRACE(expected.str()); auto moduleOp = ModuleOp::create(builder.getUnknownLoc()); moduleOp->setAttrs(builder.getDictionaryAttr(attributes)); bool sawExpectedDiagnostic = false; @@ -403,7 +407,7 @@ TEST(JeffRoundTripRegressionTest, RejectsInvalidJeffModuleMetadata) { rejects( {builder.getNamedAttr("jeff.entrypoint", builder.getI16IntegerAttr(0))}, "requires an unsigned integer 'jeff.entrypoint' attribute"); - rejects({entryPoint}, "requires an array 'jeff.strings' attribute"); + rejects({entryPoint}, "requires an array 'jeff.strings' module attribute"); rejects({builder.getNamedAttr("jeff.entrypoint", builder.getIntegerAttr(uint16Type, 1)), strings}, @@ -415,6 +419,252 @@ TEST(JeffRoundTripRegressionTest, RejectsInvalidJeffModuleMetadata) { "'jeff.entrypoint' must index a string"); } +static LogicalResult convertQCOToJeffDirect(ModuleOp module) { + PassManager pm(module.getContext()); + pm.addPass(createQCOToJeff()); + return pm.run(module); +} + +static bool isEquivalentToClone(ModuleOp module, ModuleOp clone) { + return OperationEquivalence::isEquivalentTo( + module.getOperation(), clone.getOperation(), + OperationEquivalence::Flags::None); +} + +static ModuleOp createJeffMetadataModule(MLIRContext& context, + Attribute entryPoint, + Attribute strings) { + OpBuilder builder(&context); + auto module = ModuleOp::create(builder.getUnknownLoc()); + if (entryPoint) { + module->setAttr("jeff.entrypoint", entryPoint); + } + if (strings) { + module->setAttr("jeff.strings", strings); + } + builder.setInsertionPointToStart(module.getBody()); + auto main = func::FuncOp::create(builder, builder.getUnknownLoc(), "main", + builder.getFunctionType({}, {})); + auto* block = main.addEntryBlock(); + builder.setInsertionPointToEnd(block); + func::ReturnOp::create(builder, builder.getUnknownLoc()); + return module; +} + +TEST(JeffRoundTripRegressionTest, ConversionPassesDeclareModuleContracts) { + auto toQCO = createJeffToQCO(); + ASSERT_TRUE(toQCO->getOpName()); + EXPECT_EQ(*toQCO->getOpName(), ModuleOp::getOperationName()); + + auto toJeff = createQCOToJeff(); + ASSERT_TRUE(toJeff->getOpName()); + EXPECT_EQ(*toJeff->getOpName(), ModuleOp::getOperationName()); + DialectRegistry toQCORegistry; + toQCO->getDependentDialects(toQCORegistry); + EXPECT_TRUE(toQCORegistry.getDialectAllocator( + func::FuncDialect::getDialectNamespace())); + EXPECT_TRUE(toQCORegistry.getDialectAllocator( + qco::QCODialect::getDialectNamespace())); + + DialectRegistry registry; + toJeff->getDependentDialects(registry); + EXPECT_TRUE( + registry.getDialectAllocator(arith::ArithDialect::getDialectNamespace())); + EXPECT_TRUE( + registry.getDialectAllocator(qc::QCDialect::getDialectNamespace())); + EXPECT_TRUE( + registry.getDialectAllocator(qco::QCODialect::getDialectNamespace())); +} + +TEST(JeffRoundTripRegressionTest, RejectsMalformedJeffMetadataBeforeMutation) { + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + OpBuilder builder(&context); + const auto validIndex = builder.getI16IntegerAttr(0); + const auto validStrings = + builder.getArrayAttr({builder.getStringAttr("main")}); + + const auto expectRejected = [&](Attribute entryPoint, Attribute strings) { + auto module = createJeffMetadataModule(context, entryPoint, strings); + auto before = module.clone(); + EXPECT_TRUE(failed(convertJeffToQCO(module))); + EXPECT_TRUE(isEquivalentToClone(module, before)); + }; + + expectRejected({}, validStrings); + expectRejected(builder.getStringAttr("main"), validStrings); + expectRejected(validIndex, {}); + expectRejected(validIndex, builder.getStringAttr("main")); + expectRejected(builder.getI16IntegerAttr(1), validStrings); + expectRejected(validIndex, + builder.getArrayAttr({builder.getI16IntegerAttr(0)})); + expectRejected(validIndex, + builder.getArrayAttr({builder.getStringAttr("missing")})); +} + +TEST(JeffRoundTripRegressionTest, + RejectsUnsupportedQCOEntryShapeBeforeMutation) { + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + OpBuilder builder(&context); + auto module = ModuleOp::create(builder.getUnknownLoc()); + builder.setInsertionPointToStart(module.getBody()); + auto main = func::FuncOp::create(builder, builder.getUnknownLoc(), "main", + builder.getFunctionType({}, {})); + mlir::mqt::setEntryPoint(main); + auto* entry = main.addEntryBlock(); + auto* second = main.addBlock(); + builder.setInsertionPointToEnd(entry); + func::ReturnOp::create(builder, builder.getUnknownLoc()); + builder.setInsertionPointToEnd(second); + func::ReturnOp::create(builder, builder.getUnknownLoc()); + ASSERT_TRUE(succeeded(verify(module))); + auto before = module.clone(); + + EXPECT_TRUE(failed(convertQCOToJeffDirect(module))); + EXPECT_TRUE(isEquivalentToClone(module, before)); +} + +TEST(JeffRoundTripRegressionTest, + RejectsMixedQCOAllocationModesBeforeMutation) { + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + OpBuilder builder(&context); + auto module = ModuleOp::create(builder.getUnknownLoc()); + builder.setInsertionPointToStart(module.getBody()); + auto main = func::FuncOp::create(builder, builder.getUnknownLoc(), "main", + builder.getFunctionType({}, {})); + mlir::mqt::setEntryPoint(main); + auto* block = main.addEntryBlock(); + builder.setInsertionPointToEnd(block); + qco::AllocOp::create(builder, builder.getUnknownLoc()); + qco::StaticOp::create(builder, builder.getUnknownLoc(), 0); + func::ReturnOp::create(builder, builder.getUnknownLoc()); + ASSERT_TRUE(succeeded(verify(module))); + auto before = module.clone(); + + EXPECT_TRUE(failed(convertQCOToJeffDirect(module))); + EXPECT_TRUE(isEquivalentToClone(module, before)); +} + +TEST(JeffRoundTripRegressionTest, + RejectsDuplicateStaticQubitIndicesBeforeMutation) { + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() attributes {mqt.entry_point} { + %first = qco.static 0 : !qco.qubit + %second = qco.static 0 : !qco.qubit + qco.sink %first : !qco.qubit + qco.sink %second : !qco.qubit + return + } +} +)mlir"; + auto module = parseSourceString(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + auto before = module->clone(); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + std::string message; + llvm::raw_string_ostream stream(message); + diagnostic.print(stream); + sawExpectedDiagnostic |= + StringRef(message).contains("found duplicate index 0"); + return success(); + }); + EXPECT_TRUE(failed(convertQCOToJeffDirect(*module))); + EXPECT_TRUE(sawExpectedDiagnostic); + EXPECT_TRUE(isEquivalentToClone(*module, before)); +} + +TEST(JeffRoundTripRegressionTest, RejectsRankZeroQubitTensorBeforeMutation) { + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main(%arg: tensor) -> tensor + attributes {mqt.entry_point} { + return %arg : tensor + } +} +)mlir"; + auto module = parseSourceString(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + auto before = module->clone(); + + EXPECT_TRUE(failed(convertQCOToJeffDirect(*module))); + EXPECT_TRUE(isEquivalentToClone(*module, before)); +} + +TEST(JeffRoundTripRegressionTest, RejectsMoreThan255ControlsBeforeMutation) { + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + auto module = qco::QCOProgramBuilder::build( + &context, [](qco::QCOProgramBuilder& builder) { + auto reg = builder.allocQubitRegister(257); + SmallVector controls(reg.qubits.begin(), + std::prev(reg.qubits.end())); + builder.ctrl(controls, reg.qubits.back(), + [&](Value target) { return builder.x(target); }); + return builder.intConstant(0); + }); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + auto before = module->clone(); + + EXPECT_TRUE(failed(convertQCOToJeffDirect(*module))); + EXPECT_TRUE(isEquivalentToClone(*module, before)); +} + +TEST(JeffRoundTripRegressionTest, + RejectsStaleEntryPointMetadataAfterJeffConversion) { + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + OpBuilder builder(&context); + auto module = createJeffMetadataModule( + context, builder.getI16IntegerAttr(0), + builder.getArrayAttr({builder.getStringAttr("main")})); + builder.setInsertionPointToEnd(module.getBody()); + auto helper = func::FuncOp::create(builder, builder.getUnknownLoc(), "helper", + builder.getFunctionType({}, {})); + mlir::mqt::setEntryPoint(helper); + auto* block = helper.addEntryBlock(); + builder.setInsertionPointToEnd(block); + func::ReturnOp::create(builder, builder.getUnknownLoc()); + ASSERT_TRUE(succeeded(verify(module))); + ASSERT_TRUE(succeeded(mlir::mqt::verifyProgramMetadata(module))); + auto before = module.clone(); + + EXPECT_TRUE(failed(convertJeffToQCO(module))); + EXPECT_TRUE(isEquivalentToClone(module, before)); +} + TEST(JeffRoundTripRegressionTest, RestoresStatusResultAtEndOfEntryPoint) { DialectRegistry registry; registry.insertsetAttr("jeff.strings", - builder.getArrayAttr({builder.getStringAttr("main")})); + builder.getArrayAttr({builder.getStringAttr("auxiliary"), + builder.getStringAttr("main")})); auto main = func::FuncOp::create(builder, loc, "main", builder.getFunctionType({}, {})); @@ -441,9 +692,12 @@ TEST(JeffRoundTripRegressionTest, RestoresStatusResultAtEndOfEntryPoint) { ASSERT_TRUE(succeeded(convertJeffToQCO(program))); EXPECT_TRUE(succeeded(verify(program))); - EXPECT_EQ(main.getFunctionType(), + auto convertedMain = program.lookupSymbol("main"); + ASSERT_TRUE(convertedMain); + EXPECT_EQ(convertedMain.getFunctionType(), builder.getFunctionType({}, {builder.getI64Type()})); - auto returnOp = cast(block->getTerminator()); + auto returnOp = + cast(convertedMain.getBody().front().getTerminator()); ASSERT_EQ(returnOp.getNumOperands(), 1); EXPECT_TRUE(returnOp.getOperand(0).getType().isInteger(64)); } @@ -547,6 +801,7 @@ module { auto module = parseSourceString(source, &context); ASSERT_TRUE(module); ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef before(module->clone()); bool sawExpectedDiagnostic = false; ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { @@ -560,6 +815,9 @@ module { }); EXPECT_TRUE(failed(convertQCOToJeff(*module))); EXPECT_TRUE(sawExpectedDiagnostic); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), before->getOperation(), + OperationEquivalence::Flags::None)); } TEST(JeffRoundTripRegressionTest, RejectsLegacyClassicalMemref) { diff --git a/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp b/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp index 83bed48f62..7cca6c9f49 100644 --- a/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp +++ b/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp @@ -14,6 +14,7 @@ #include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" #include "mlir/Dialect/QC/IR/QCDialect.h" +#include "mlir/Dialect/QC/IR/QCOps.h" #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" @@ -23,12 +24,15 @@ #include #include +#include #include #include #include #include +#include #include #include +#include #include #include #include @@ -86,6 +90,335 @@ static LogicalResult runQCOToQCConversion(ModuleOp module) { return pm.run(module); } +TEST(QCOToQCPassContract, IsModuleAnchoredAndDeclaresCreatedDialects) { + auto pass = createQCOToQC(); + ASSERT_TRUE(pass->getOpName()); + EXPECT_EQ(*pass->getOpName(), ModuleOp::getOperationName()); + + DialectRegistry registry; + pass->getDependentDialects(registry); + EXPECT_TRUE( + registry.getDialectAllocator(func::FuncDialect::getDialectNamespace())); + EXPECT_TRUE(registry.getDialectAllocator( + cf::ControlFlowDialect::getDialectNamespace())); + EXPECT_TRUE( + registry.getDialectAllocator(scf::SCFDialect::getDialectNamespace())); +} + +TEST(QCOToQCRegressionTest, RejectsYieldPermutationWithoutMutation) { + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() attributes {mqt.entry_point} { + %q0 = qco.alloc : !qco.qubit + %q1 = qco.alloc : !qco.qubit + %lb = arith.constant 0 : index + %ub = arith.constant 1 : index + %step = arith.constant 1 : index + %out0, %out1 = scf.for %iv = %lb to %ub step %step + iter_args(%left = %q0, %right = %q1) + -> (!qco.qubit, !qco.qubit) { + scf.yield %right, %left : !qco.qubit, !qco.qubit + } + qco.sink %out0 : !qco.qubit + qco.sink %out1 : !qco.qubit + return + } +} +)mlir"; + + auto module = parseSourceString(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + sawExpectedDiagnostic |= + StringRef(diagnostic.str()).contains("preserve input order"); + return success(); + }); + EXPECT_TRUE(failed(runQCOToQCConversion(*module))); + EXPECT_TRUE(sawExpectedDiagnostic); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST(QCOToQCRegressionTest, RejectsMixedAllocationModesWithoutMutation) { + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() attributes {mqt.entry_point} { + %static = qco.static 0 : !qco.qubit + %dynamic = qco.alloc : !qco.qubit + qco.sink %static : !qco.qubit + qco.sink %dynamic : !qco.qubit + return + } +} +)mlir"; + + auto module = parseSourceString(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + sawExpectedDiagnostic |= + StringRef(diagnostic.str()).contains("cannot mix static and dynamic"); + return success(); + }); + EXPECT_TRUE(failed(runQCOToQCConversion(*module))); + EXPECT_TRUE(sawExpectedDiagnostic); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST(QCOToQCRegressionTest, RejectsDuplicateStaticIndicesWithoutMutation) { + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() attributes {mqt.entry_point} { + %first = qco.static 7 : !qco.qubit + %second = qco.static 7 : !qco.qubit + qco.sink %first : !qco.qubit + qco.sink %second : !qco.qubit + return + } +} +)mlir"; + + auto module = parseSourceString(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + sawExpectedDiagnostic |= + StringRef(diagnostic.str()).contains("found duplicate index 7"); + return success(); + }); + EXPECT_TRUE(failed(runQCOToQCConversion(*module))); + EXPECT_TRUE(sawExpectedDiagnostic); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST(QCOToQCRegressionTest, + RejectsStaticIndexReacquisitionAfterSinkWithoutMutation) { + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() attributes {mqt.entry_point} { + %first = qco.static 7 : !qco.qubit + %after_h = qco.h %first : !qco.qubit -> !qco.qubit + qco.sink %after_h : !qco.qubit + %second = qco.static 7 : !qco.qubit + %after_x = qco.x %second : !qco.qubit -> !qco.qubit + qco.sink %after_x : !qco.qubit + return + } +} +)mlir"; + + auto module = parseSourceString(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + sawExpectedDiagnostic |= + StringRef(diagnostic.str()).contains("found duplicate index 7"); + return success(); + }); + EXPECT_TRUE(failed(runQCOToQCConversion(*module))); + EXPECT_TRUE(sawExpectedDiagnostic); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST(QCOToQCRegressionTest, RejectsNonlinearQubitsWithoutMutation) { + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() attributes {mqt.entry_point} { + %q0 = qco.alloc : !qco.qubit + %q1 = qco.h %q0 : !qco.qubit -> !qco.qubit + %q2 = qco.x %q0 : !qco.qubit -> !qco.qubit + qco.sink %q1 : !qco.qubit + qco.sink %q2 : !qco.qubit + return + } +} +)mlir"; + + auto module = parseSourceString(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + sawExpectedDiagnostic |= + StringRef(diagnostic.str()).contains("exactly one use"); + return success(); + }); + EXPECT_TRUE(failed(runQCOToQCConversion(*module))); + EXPECT_TRUE(sawExpectedDiagnostic); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST(QCOToQCRegressionTest, PreservesQTensorInsertSlotUpdates) { + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() attributes {mqt.entry_point} { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c2 = arith.constant 2 : index + %tensor0 = qtensor.alloc(%c2) : tensor<2x!qco.qubit> + %tensor1, %q0 = qtensor.extract %tensor0[%c0] : tensor<2x!qco.qubit> + %tensor2, %q1 = qtensor.extract %tensor1[%c1] : tensor<2x!qco.qubit> + %tensor3 = qtensor.insert %q0 into %tensor2[%c1] : tensor<2x!qco.qubit> + %tensor4 = qtensor.insert %q1 into %tensor3[%c0] : tensor<2x!qco.qubit> + %tensor5, %at0 = qtensor.extract %tensor4[%c0] : tensor<2x!qco.qubit> + %tensor6, %at1 = qtensor.extract %tensor5[%c1] : tensor<2x!qco.qubit> + qco.sink %at0 : !qco.qubit + qco.sink %at1 : !qco.qubit + qtensor.dealloc %tensor6 : tensor<2x!qco.qubit> + return + } +} +)mlir"; + + auto module = parseSourceString(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + ASSERT_TRUE(succeeded(runQCOToQCConversion(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + + SmallVector stores; + module->walk([&](memref::StoreOp store) { stores.push_back(store); }); + ASSERT_EQ(stores.size(), 2U); + EXPECT_NE(stores[0].getValue(), stores[1].getValue()); + EXPECT_EQ(stores[0].getMemref(), stores[1].getMemref()); + + SmallVector loads; + module->walk([&](memref::LoadOp load) { loads.push_back(load); }); + ASSERT_EQ(loads.size(), 3U); + EXPECT_TRUE(stores[1]->isBeforeInBlock(loads[2])); + + bool containsQTensorOperations = false; + module->walk([&](Operation* operation) { + containsQTensorOperations |= + operation->getDialect() == + context.getLoadedDialect(); + }); + EXPECT_FALSE(containsQTensorOperations); +} + +TEST(QCOToQCRegressionTest, InvalidatesQTensorCacheAcrossLoopSlotSwap) { + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() attributes {mqt.entry_point} { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c2 = arith.constant 2 : index + %tensor0 = qtensor.alloc(%c2) : tensor<2x!qco.qubit> + %tensor1, %before = qtensor.extract %tensor0[%c0] : tensor<2x!qco.qubit> + %tensor2 = qtensor.insert %before into %tensor1[%c0] : tensor<2x!qco.qubit> + %tensor3 = scf.for %iv = %c0 to %c1 step %c1 + iter_args(%tensor = %tensor2) -> (tensor<2x!qco.qubit>) { + %tensor4, %left = qtensor.extract %tensor[%c0] : tensor<2x!qco.qubit> + %tensor5, %right = qtensor.extract %tensor4[%c1] : tensor<2x!qco.qubit> + %tensor6 = qtensor.insert %left into %tensor5[%c1] : tensor<2x!qco.qubit> + %tensor7 = qtensor.insert %right into %tensor6[%c0] : tensor<2x!qco.qubit> + scf.yield %tensor7 : tensor<2x!qco.qubit> + } + %tensor8, %at0 = qtensor.extract %tensor3[%c0] : tensor<2x!qco.qubit> + %tensor9, %at1 = qtensor.extract %tensor8[%c1] : tensor<2x!qco.qubit> + qco.sink %at0 : !qco.qubit + qco.sink %at1 : !qco.qubit + qtensor.dealloc %tensor9 : tensor<2x!qco.qubit> + return + } +} +)mlir"; + + auto module = parseSourceString(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + ASSERT_TRUE(succeeded(runQCOToQCConversion(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + + auto function = *module->getOps().begin(); + auto loops = llvm::to_vector(function.getBody().getOps()); + ASSERT_EQ(loops.size(), 1U); + EXPECT_EQ(llvm::range_size(loops[0].getBody()->getOps()), + 2U); + + SmallVector loadsBeforeLoop; + SmallVector loadsAfterLoop; + for (auto load : function.getBody().front().getOps()) { + (load->isBeforeInBlock(loops[0]) ? loadsBeforeLoop : loadsAfterLoop) + .push_back(load); + } + EXPECT_EQ(loadsBeforeLoop.size(), 1U); + EXPECT_EQ(loadsAfterLoop.size(), 2U); +} + TEST(QCOToQCRegressionTest, RetainsQubitRegisterName) { DialectRegistry registry; registry.insert #include #include +#include #include #include #include @@ -45,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -52,6 +55,7 @@ #include #include #include +#include #include #include @@ -109,6 +113,21 @@ static LogicalResult runQCToQCOConversion(ModuleOp module) { return pm.run(module); } +TEST(QCToQCOPassContract, IsModuleAnchoredAndDeclaresCreatedDialects) { + auto pass = createQCToQCO(); + ASSERT_TRUE(pass->getOpName()); + EXPECT_EQ(*pass->getOpName(), ModuleOp::getOperationName()); + + DialectRegistry registry; + pass->getDependentDialects(registry); + EXPECT_TRUE( + registry.getDialectAllocator(func::FuncDialect::getDialectNamespace())); + EXPECT_TRUE(registry.getDialectAllocator( + cf::ControlFlowDialect::getDialectNamespace())); + EXPECT_TRUE( + registry.getDialectAllocator(scf::SCFDialect::getDialectNamespace())); +} + namespace { class QCToQCORegressionTest : public testing::Test { @@ -220,8 +239,232 @@ class QCToQCORegressionTest : public testing::Test { } }; +class RejectingRegionMovePattern final + : public OpConversionPattern { +public: + RejectingRegionMovePattern(TypeConverter& typeConverter, MLIRContext* context, + bool& sourcePreserved) + : OpConversionPattern(typeConverter, context), + sourcePreserved(sourcePreserved) {} + + LogicalResult + matchAndRewrite(func::FuncOp op, OpAdaptor /*adaptor*/, + ConversionPatternRewriter& rewriter) const override { + if (!op->hasAttr("test.reject_region_move")) { + return failure(); + } + auto module = op->getParentOfType(); + auto destination = module.lookupSymbol("destination"); + if (!destination) { + return failure(); + } + + const auto result = moveRegion(op.getBody(), destination.getBody(), + rewriter, getTypeConverter()); + if (failed(result)) { + sourcePreserved = !op.getBody().empty() && destination.getBody().empty(); + } + return result; + } + +private: + bool& sourcePreserved; +}; + } // namespace +TEST_F(QCToQCORegressionTest, + DuplicateStaticReferencesShareOneEvolvingQCOValue) { + auto module = parseSourceString(R"mlir( + module { + func.func @main() { + %a = qc.static 0 : !qc.qubit + %b = qc.static 0 : !qc.qubit + qc.x %a : !qc.qubit + qc.h %b : !qc.qubit + return + } + } + )mlir", + &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + ASSERT_TRUE(succeeded(runQCToQCOConversion(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + auto function = *module->getOps().begin(); + EXPECT_EQ(llvm::range_size(function.getBody().getOps()), 1U); + auto x = *function.getBody().getOps().begin(); + auto h = *function.getBody().getOps().begin(); + EXPECT_EQ(h.getInputTarget(0), x.getOutputTarget(0)); +} + +TEST_F(QCToQCORegressionTest, + RejectsStaticUseAfterDeallocationWithoutMutation) { + constexpr auto sources = std::to_array({ + R"mlir( +module { + func.func @main() { + %a = qc.static 0 : !qc.qubit + qc.dealloc %a : !qc.qubit + %b = qc.static 0 : !qc.qubit + qc.x %b : !qc.qubit + return + } +} +)mlir", + R"mlir( +module { + func.func @main() { + %a = qc.static 0 : !qc.qubit + %b = qc.static 0 : !qc.qubit + qc.dealloc %a : !qc.qubit + qc.dealloc %b : !qc.qubit + return + } +} +)mlir", + R"mlir( +module { + func.func @main() { + %a = qc.static 0 : !qc.qubit + %b = qc.static 0 : !qc.qubit + qc.dealloc %b : !qc.qubit + qc.x %a : !qc.qubit + return + } +} +)mlir", + }); + + for (const auto source : sources) { + SCOPED_TRACE(source.str()); + auto module = parseSourceString(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + sawExpectedDiagnostic |= + StringRef(diagnostic.str()).contains("has no live QCO value"); + return success(); + }); + EXPECT_TRUE(failed(runQCToQCOConversion(*module))); + EXPECT_TRUE(sawExpectedDiagnostic); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); + } +} + +TEST_F(QCToQCORegressionTest, RejectedRegionMovePreservesSource) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func private @destination() + func.func @source(%arg: index) attributes {test.reject_region_move} { + return + } +} +)mlir"; + + auto module = parseSourceString(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + TypeConverter typeConverter; + typeConverter.addConversion([](Type type) -> std::optional { + if (isa(type)) { + return std::nullopt; + } + return type; + }); + ConversionTarget target(context); + target.markUnknownOpDynamicallyLegal([](Operation*) { return true; }); + target.addDynamicallyLegalOp( + [](func::FuncOp op) { return !op->hasAttr("test.reject_region_move"); }); + + bool sourcePreserved = false; + RewritePatternSet patterns(&context); + patterns.add(typeConverter, &context, + sourcePreserved); + ScopedDiagnosticHandler handler( + &context, [](Diagnostic& /*diagnostic*/) { return success(); }); + EXPECT_TRUE( + failed(applyPartialConversion(*module, target, std::move(patterns)))); + EXPECT_TRUE(sourcePreserved); + + auto sourceFunc = module->lookupSymbol("source"); + auto destination = module->lookupSymbol("destination"); + ASSERT_TRUE(sourceFunc); + ASSERT_TRUE(destination); + EXPECT_FALSE(sourceFunc.getBody().empty()); + EXPECT_TRUE(destination.getBody().empty()); +} + +TEST_F(QCToQCORegressionTest, + RejectsPossiblyAliasedDynamicIndicesWithoutMutation) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main(%i: index, %j: index) attributes {mqt.entry_point} { + %reg = memref.alloc() : memref<2x!qc.qubit> + %q0 = memref.load %reg[%i] : memref<2x!qc.qubit> + %q1 = memref.load %reg[%j] : memref<2x!qc.qubit> + qc.swap %q0, %q1 : !qc.qubit, !qc.qubit + memref.dealloc %reg : memref<2x!qc.qubit> + return + } +} +)mlir"; + + auto module = parseSourceString(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + sawExpectedDiagnostic |= + StringRef(diagnostic.str()).contains("not provably distinct"); + return success(); + }); + EXPECT_TRUE(failed(runQCToQCOConversion(*module))); + EXPECT_TRUE(sawExpectedDiagnostic); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST_F(QCToQCORegressionTest, RejectsMixedAllocationModesWithoutMutation) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() attributes {mqt.entry_point} { + %static = qc.static 0 : !qc.qubit + %dynamic = qc.alloc : !qc.qubit + qc.dealloc %dynamic : !qc.qubit + return + } +} +)mlir"; + + auto module = parseSourceString(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + sawExpectedDiagnostic |= + StringRef(diagnostic.str()).contains("cannot mix static and dynamic"); + return success(); + }); + EXPECT_TRUE(failed(runQCToQCOConversion(*module))); + EXPECT_TRUE(sawExpectedDiagnostic); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + TEST_F(QCToQCORegressionTest, PreservesForResultsWithQuantumState) { constexpr llvm::StringLiteral source = R"mlir( module { @@ -1423,6 +1666,40 @@ module { expectNoQCOperations(*moduleOp); } +TEST_F(QCToQCORegressionTest, + RejectsConditionallyDeallocatedCapturedRegisterWithoutMutation) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main(%condition: i1) attributes {mqt.entry_point} { + %reg = memref.alloc() : memref<1x!qc.qubit> + scf.if %condition { + memref.dealloc %reg : memref<1x!qc.qubit> + } + return + } +} +)mlir"; + + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + OwningOpRef original(moduleOp->clone()); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + sawExpectedDiagnostic |= + StringRef(diagnostic.str()) + .contains("references a qubit register that has no live QTensor " + "value"); + return success(); + }); + EXPECT_TRUE(failed(runQCToQCOConversion(*moduleOp))); + EXPECT_TRUE(sawExpectedDiagnostic); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + moduleOp->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + TEST_F(QCToQCORegressionTest, RejectsSameDynamicRegisterIndexWithinOneOperation) { constexpr llvm::StringLiteral source = R"mlir( diff --git a/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp b/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp index 8dc77847ac..a4f7bc80e1 100644 --- a/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp +++ b/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp @@ -11,9 +11,12 @@ #include "Support/IRVerification.h" #include "TestCaseUtils.h" #include "mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.h" +#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Transforms/Passes.h" #include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" #include "mlir/Dialect/QC/IR/QCDialect.h" +#include "mlir/Dialect/QC/IR/QCOps.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QIR/Builder/QIRProgramBuilder.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" #include "mlir/Support/Passes.h" @@ -30,10 +33,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -41,6 +46,7 @@ #include #include +#include #include #include #include @@ -98,6 +104,605 @@ static LogicalResult runQCToQIRAdaptiveConversionSimple(ModuleOp moduleOp) { return pm.run(moduleOp); } +static bool isEquivalentToClone(ModuleOp module, ModuleOp clone) { + return OperationEquivalence::isEquivalentTo( + module.getOperation(), clone.getOperation(), + OperationEquivalence::Flags::None); +} + +TEST(QCToQIRAdaptiveNativeTest, DeclaresModuleRootAndProducedDialects) { + auto pass = createQCToQIRAdaptive(); + ASSERT_TRUE(pass->getOpName()); + EXPECT_EQ(*pass->getOpName(), ModuleOp::getOperationName()); + + DialectRegistry registry; + pass->getDependentDialects(registry); + EXPECT_TRUE( + registry.getDialectAllocator(arith::ArithDialect::getDialectNamespace())); + EXPECT_TRUE(registry.getDialectAllocator( + cf::ControlFlowDialect::getDialectNamespace())); + EXPECT_TRUE( + registry.getDialectAllocator(qc::QCDialect::getDialectNamespace())); + EXPECT_TRUE( + registry.getDialectAllocator(qco::QCODialect::getDialectNamespace())); +} + +TEST(QCToQIRAdaptiveNativeTest, + RejectsExcessiveClassicalResultCapacityAtomically) { + MLIRContext context; + context.loadDialect(); + auto module = + qc::QCProgramBuilder::build(&context, [](qc::QCProgramBuilder& builder) { + builder.allocClassicalBitRegister(1LL << 30); + return builder.intConstant(0); + }); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + auto before = module->clone(); + + EXPECT_TRUE(failed(runQCToQIRAdaptiveConversionSimple(*module))); + EXPECT_TRUE(isEquivalentToClone(*module, before)); +} + +TEST(QCToQIRAdaptiveNativeTest, RejectsMissingEntryBeforeMutation) { + MLIRContext context; + context.loadDialect(); + OpBuilder builder(&context); + auto module = ModuleOp::create(builder.getUnknownLoc()); + builder.setInsertionPointToStart(module.getBody()); + auto helper = func::FuncOp::create(builder, builder.getUnknownLoc(), "helper", + builder.getFunctionType({}, {})); + auto* block = helper.addEntryBlock(); + builder.setInsertionPointToEnd(block); + func::ReturnOp::create(builder, builder.getUnknownLoc()); + ASSERT_TRUE(succeeded(verify(module))); + auto before = module.clone(); + + EXPECT_TRUE(failed(runQCToQIRAdaptiveConversionSimple(module))); + EXPECT_TRUE(isEquivalentToClone(module, before)); +} + +TEST(QCToQIRAdaptiveNativeTest, + RejectsNonFunctionReservedRuntimeSymbolAtomically) { + MLIRContext context; + context.loadDialect(); + OpBuilder builder(&context); + const auto loc = builder.getUnknownLoc(); + auto module = ModuleOp::create(loc); + builder.setInsertionPointToStart(module.getBody()); + LLVM::GlobalOp::create(builder, loc, builder.getI8Type(), + /*isConstant=*/true, LLVM::Linkage::Internal, + builder.getStringAttr(qir::QIR_RESET), + builder.getI8IntegerAttr(0)); + auto main = func::FuncOp::create(builder, loc, "main", + builder.getFunctionType({}, {})); + mlir::mqt::setEntryPoint(main); + auto* block = main.addEntryBlock(); + builder.setInsertionPointToEnd(block); + auto qubit = qc::AllocOp::create(builder, loc); + qc::ResetOp::create(builder, loc, qubit); + qc::DeallocOp::create(builder, loc, qubit); + func::ReturnOp::create(builder, loc); + ASSERT_TRUE(succeeded(verify(module))); + auto before = module.clone(); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + const auto message = diagnostic.str(); + sawExpectedDiagnostic |= + StringRef(message).contains("reserves runtime symbol") && + StringRef(message).contains(qir::QIR_RESET); + return success(); + }); + EXPECT_TRUE(failed(runQCToQIRAdaptiveConversionSimple(module))); + EXPECT_TRUE(sawExpectedDiagnostic); + EXPECT_TRUE(isEquivalentToClone(module, before)); +} + +TEST(QCToQIRAdaptiveNativeTest, + RejectsReservedRuntimeFunctionDefinitionAtomically) { + MLIRContext context; + context.loadDialect(); + OpBuilder builder(&context); + auto loc = builder.getUnknownLoc(); + auto module = ModuleOp::create(loc); + builder.setInsertionPointToStart(module.getBody()); + auto runtimeDefinition = LLVM::LLVMFuncOp::create( + builder, loc, qir::QIR_RESET, + LLVM::LLVMFunctionType::get(LLVM::LLVMVoidType::get(&context), + {LLVM::LLVMPointerType::get(&context)})); + auto* runtimeEntry = runtimeDefinition.addEntryBlock(builder); + builder.setInsertionPointToEnd(runtimeEntry); + LLVM::ReturnOp::create(builder, loc, ValueRange{}); + + builder.setInsertionPointToEnd(module.getBody()); + auto main = func::FuncOp::create(builder, loc, "main", + builder.getFunctionType({}, {})); + mlir::mqt::setEntryPoint(main); + auto* entry = main.addEntryBlock(); + builder.setInsertionPointToEnd(entry); + auto qubit = qc::AllocOp::create(builder, loc); + qc::ResetOp::create(builder, loc, qubit); + qc::DeallocOp::create(builder, loc, qubit); + func::ReturnOp::create(builder, loc); + ASSERT_TRUE(succeeded(verify(module))); + auto before = module.clone(); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + auto message = diagnostic.str(); + sawExpectedDiagnostic |= + StringRef(message).contains("reserves runtime symbol") && + StringRef(message).contains(qir::QIR_RESET) && + StringRef(message).contains("function declaration"); + return success(); + }); + EXPECT_TRUE(failed(runQCToQIRAdaptiveConversionSimple(module))); + EXPECT_TRUE(sawExpectedDiagnostic); + EXPECT_TRUE(isEquivalentToClone(module, before)); +} + +TEST(QCToQIRAdaptiveNativeTest, DoesNotReleaseStaticQubits) { + MLIRContext context; + context.loadDialect(); + OpBuilder builder(&context); + auto loc = builder.getUnknownLoc(); + auto module = ModuleOp::create(loc); + builder.setInsertionPointToStart(module.getBody()); + auto main = func::FuncOp::create(builder, loc, "main", + builder.getFunctionType({}, {})); + mlir::mqt::setEntryPoint(main); + auto* entry = main.addEntryBlock(); + builder.setInsertionPointToEnd(entry); + auto qubit = qc::StaticOp::create(builder, loc, 0); + qc::DeallocOp::create(builder, loc, qubit); + func::ReturnOp::create(builder, loc); + ASSERT_TRUE(succeeded(verify(module))); + + ASSERT_TRUE(succeeded(runQCToQIRAdaptiveConversionSimple(module))); + ASSERT_TRUE(succeeded(verify(module))); + EXPECT_FALSE(module.lookupSymbol(qir::QIR_QUBIT_RELEASE)); +} + +TEST(QCToQIRAdaptiveNativeTest, RoutesEveryReturnThroughOneEpilogue) { + MLIRContext context; + context.loadDialect(); + OpBuilder builder(&context); + auto module = ModuleOp::create(builder.getUnknownLoc()); + builder.setInsertionPointToStart(module.getBody()); + auto main = func::FuncOp::create(builder, builder.getUnknownLoc(), "main", + builder.getFunctionType({}, {})); + mlir::mqt::setEntryPoint(main); + auto* entry = main.addEntryBlock(); + auto* thenBlock = main.addBlock(); + auto* elseBlock = main.addBlock(); + builder.setInsertionPointToEnd(entry); + auto condition = + arith::ConstantIntOp::create(builder, builder.getUnknownLoc(), 1, 1); + cf::CondBranchOp::create(builder, builder.getUnknownLoc(), condition, + thenBlock, ValueRange{}, elseBlock, ValueRange{}); + builder.setInsertionPointToEnd(thenBlock); + func::ReturnOp::create(builder, builder.getUnknownLoc()); + builder.setInsertionPointToEnd(elseBlock); + func::ReturnOp::create(builder, builder.getUnknownLoc()); + ASSERT_TRUE(succeeded(verify(module))); + + ASSERT_TRUE(succeeded(runQCToQIRAdaptiveConversionSimple(module))); + ASSERT_TRUE(succeeded(verify(module))); + size_t returns = 0; + auto loweredMain = qir::getMainFunction(module); + ASSERT_TRUE(loweredMain); + loweredMain.walk([&](LLVM::ReturnOp) { ++returns; }); + EXPECT_EQ(returns, 1U); + EXPECT_EQ(static_cast( + std::distance(loweredMain.getBody().back().pred_begin(), + loweredMain.getBody().back().pred_end())), + 2U); +} + +TEST(QCToQIRAdaptiveNativeTest, LeavesNestedLLVMReturnsUntouched) { + MLIRContext context; + context.loadDialect(); + OpBuilder builder(&context); + const auto loc = builder.getUnknownLoc(); + auto module = ModuleOp::create(loc); + builder.setInsertionPointToStart(module.getBody()); + auto main = func::FuncOp::create(builder, loc, "main", + builder.getFunctionType({}, {})); + mlir::mqt::setEntryPoint(main); + auto* entry = main.addEntryBlock(); + builder.setInsertionPointToEnd(entry); + + auto nestedModule = ModuleOp::create(loc); + builder.insert(nestedModule.getOperation()); + builder.setInsertionPointToStart(nestedModule.getBody()); + auto nestedFunction = LLVM::LLVMFuncOp::create( + builder, loc, "nested", + LLVM::LLVMFunctionType::get(builder.getI32Type(), {})); + auto* nestedEntry = nestedFunction.addEntryBlock(builder); + builder.setInsertionPointToEnd(nestedEntry); + auto value = + LLVM::ConstantOp::create(builder, loc, builder.getI32IntegerAttr(0)); + LLVM::ReturnOp::create(builder, loc, value.getResult()); + + builder.setInsertionPointToEnd(entry); + func::ReturnOp::create(builder, loc); + ASSERT_TRUE(succeeded(verify(module))); + + ASSERT_TRUE(succeeded(runQCToQIRAdaptiveConversionSimple(module))); + ASSERT_TRUE(succeeded(verify(module))); + auto loweredMain = qir::getMainFunction(module); + ASSERT_TRUE(loweredMain); + size_t directReturns = 0; + size_t nestedReturns = 0; + loweredMain.walk([&](LLVM::ReturnOp returnOp) { + if (returnOp->getParentOp() == loweredMain.getOperation()) { + ++directReturns; + } else { + ++nestedReturns; + } + }); + EXPECT_EQ(directReturns, 1U); + EXPECT_EQ(nestedReturns, 1U); +} + +TEST(QCToQIRAdaptiveNativeTest, PreservesNestedFuncReturnType) { + MLIRContext context; + context.loadDialect(); + OpBuilder builder(&context); + auto loc = builder.getUnknownLoc(); + auto module = ModuleOp::create(loc); + builder.setInsertionPointToStart(module.getBody()); + auto main = func::FuncOp::create(builder, loc, "main", + builder.getFunctionType({}, {})); + mlir::mqt::setEntryPoint(main); + auto* entry = main.addEntryBlock(); + builder.setInsertionPointToEnd(entry); + + auto nestedModule = ModuleOp::create(loc); + builder.insert(nestedModule.getOperation()); + builder.setInsertionPointToStart(nestedModule.getBody()); + auto nestedFunction = + func::FuncOp::create(builder, loc, "nested", + builder.getFunctionType({}, {builder.getI32Type()})); + auto* nestedEntry = nestedFunction.addEntryBlock(); + builder.setInsertionPointToEnd(nestedEntry); + auto value = arith::ConstantIntOp::create(builder, loc, 0, 32); + func::ReturnOp::create(builder, loc, value.getResult()); + + builder.setInsertionPointToEnd(entry); + func::ReturnOp::create(builder, loc); + ASSERT_TRUE(succeeded(verify(module))); + + ASSERT_TRUE(succeeded(runQCToQIRAdaptiveConversionSimple(module))); + ASSERT_TRUE(succeeded(verify(module))); + LLVM::LLVMFuncOp loweredNested; + module.walk([&](LLVM::LLVMFuncOp function) { + if (function.getName() == "nested") { + loweredNested = function; + } + }); + ASSERT_TRUE(loweredNested); + EXPECT_TRUE(loweredNested.getFunctionType().getReturnType().isInteger(32)); +} + +TEST(QCToQIRAdaptiveNativeTest, KeepsDynamicReleasesInTheirControlFlowBlock) { + MLIRContext context; + context.loadDialect(); + OpBuilder builder(&context); + const auto loc = builder.getUnknownLoc(); + auto module = ModuleOp::create(loc); + builder.setInsertionPointToStart(module.getBody()); + auto main = func::FuncOp::create(builder, loc, "main", + builder.getFunctionType({}, {})); + mlir::mqt::setEntryPoint(main); + auto* entry = main.addEntryBlock(); + auto* allocate = main.addBlock(); + auto* skip = main.addBlock(); + auto* exit = main.addBlock(); + builder.setInsertionPointToEnd(entry); + auto condition = arith::ConstantIntOp::create(builder, loc, 1, 1); + cf::CondBranchOp::create(builder, loc, condition, allocate, ValueRange{}, + skip, ValueRange{}); + + builder.setInsertionPointToEnd(allocate); + auto qubit = qc::AllocOp::create(builder, loc); + qc::DeallocOp::create(builder, loc, qubit); + const auto registerType = MemRefType::get({2}, qc::QubitType::get(&context)); + auto qubitRegister = + memref::AllocOp::create(builder, loc, registerType, ValueRange{}); + memref::DeallocOp::create(builder, loc, qubitRegister.getResult()); + cf::BranchOp::create(builder, loc, exit); + + builder.setInsertionPointToEnd(skip); + cf::BranchOp::create(builder, loc, exit); + builder.setInsertionPointToEnd(exit); + func::ReturnOp::create(builder, loc); + ASSERT_TRUE(succeeded(verify(module))); + + ASSERT_TRUE(succeeded(runQCToQIRAdaptiveConversionSimple(module))); + ASSERT_TRUE(succeeded(verify(module))); + auto loweredMain = qir::getMainFunction(module); + ASSERT_TRUE(loweredMain); + Block* qubitAllocationBlock = nullptr; + Block* qubitReleaseBlock = nullptr; + Block* arrayAllocationBlock = nullptr; + Block* arrayReleaseBlock = nullptr; + loweredMain.walk([&](LLVM::CallOp call) { + if (call.getCallee() == qir::QIR_QUBIT_ALLOC) { + qubitAllocationBlock = call->getBlock(); + } else if (call.getCallee() == qir::QIR_QUBIT_RELEASE) { + qubitReleaseBlock = call->getBlock(); + } else if (call.getCallee() == qir::QIR_QUBIT_ARRAY_ALLOC) { + arrayAllocationBlock = call->getBlock(); + } else if (call.getCallee() == qir::QIR_QUBIT_ARRAY_RELEASE) { + arrayReleaseBlock = call->getBlock(); + } + }); + ASSERT_NE(qubitAllocationBlock, nullptr); + ASSERT_NE(qubitReleaseBlock, nullptr); + ASSERT_NE(arrayAllocationBlock, nullptr); + ASSERT_NE(arrayReleaseBlock, nullptr); + EXPECT_EQ(qubitReleaseBlock, qubitAllocationBlock); + EXPECT_EQ(arrayReleaseBlock, arrayAllocationBlock); +} + +TEST(QCToQIRAdaptiveNativeTest, + KeepsConditionallyExecutedReleasesInTheirControlFlowBlock) { + MLIRContext context; + context.loadDialect(); + OpBuilder builder(&context); + const auto loc = builder.getUnknownLoc(); + auto module = ModuleOp::create(loc); + builder.setInsertionPointToStart(module.getBody()); + auto main = func::FuncOp::create(builder, loc, "main", + builder.getFunctionType({}, {})); + mlir::mqt::setEntryPoint(main); + auto* entry = main.addEntryBlock(); + auto* release = main.addBlock(); + auto* skip = main.addBlock(); + auto* exit = main.addBlock(); + builder.setInsertionPointToEnd(entry); + auto qubit = qc::AllocOp::create(builder, loc); + const auto registerType = MemRefType::get({2}, qc::QubitType::get(&context)); + auto qubitRegister = + memref::AllocOp::create(builder, loc, registerType, ValueRange{}); + auto condition = arith::ConstantIntOp::create(builder, loc, 1, 1); + cf::CondBranchOp::create(builder, loc, condition, release, ValueRange{}, skip, + ValueRange{}); + + builder.setInsertionPointToEnd(release); + qc::DeallocOp::create(builder, loc, qubit); + memref::DeallocOp::create(builder, loc, qubitRegister.getResult()); + cf::BranchOp::create(builder, loc, exit); + + builder.setInsertionPointToEnd(skip); + cf::BranchOp::create(builder, loc, exit); + builder.setInsertionPointToEnd(exit); + func::ReturnOp::create(builder, loc); + ASSERT_TRUE(succeeded(verify(module))); + + ASSERT_TRUE(succeeded(runQCToQIRAdaptiveConversionSimple(module))); + ASSERT_TRUE(succeeded(verify(module))); + auto loweredMain = qir::getMainFunction(module); + ASSERT_TRUE(loweredMain); + Block* qubitReleaseBlock = nullptr; + Block* arrayReleaseBlock = nullptr; + loweredMain.walk([&](LLVM::CallOp call) { + if (call.getCallee() == qir::QIR_QUBIT_RELEASE) { + qubitReleaseBlock = call->getBlock(); + } else if (call.getCallee() == qir::QIR_QUBIT_ARRAY_RELEASE) { + arrayReleaseBlock = call->getBlock(); + } + }); + ASSERT_NE(qubitReleaseBlock, nullptr); + ASSERT_NE(arrayReleaseBlock, nullptr); + EXPECT_EQ(qubitReleaseBlock, arrayReleaseBlock); + EXPECT_NE(qubitReleaseBlock, &loweredMain.getBody().back()); +} + +TEST(QCToQIRAdaptiveNativeTest, KeepsRepeatedReleasesInTheirControlFlowBlock) { + MLIRContext context; + context.loadDialect(); + OpBuilder builder(&context); + const auto loc = builder.getUnknownLoc(); + auto module = ModuleOp::create(loc); + builder.setInsertionPointToStart(module.getBody()); + auto main = func::FuncOp::create(builder, loc, "main", + builder.getFunctionType({}, {})); + mlir::mqt::setEntryPoint(main); + auto* entry = main.addEntryBlock(); + auto* loop = main.addBlock(); + auto* exit = main.addBlock(); + loop->addArgument(builder.getI1Type(), loc); + + builder.setInsertionPointToEnd(entry); + auto firstIteration = arith::ConstantIntOp::create(builder, loc, 1, 1); + auto lastIteration = arith::ConstantIntOp::create(builder, loc, 0, 1); + cf::BranchOp::create(builder, loc, loop, + ValueRange{firstIteration.getResult()}); + + builder.setInsertionPointToEnd(loop); + auto qubit = qc::AllocOp::create(builder, loc); + qc::DeallocOp::create(builder, loc, qubit); + const auto registerType = MemRefType::get({2}, qc::QubitType::get(&context)); + auto qubitRegister = + memref::AllocOp::create(builder, loc, registerType, ValueRange{}); + memref::DeallocOp::create(builder, loc, qubitRegister.getResult()); + cf::CondBranchOp::create(builder, loc, loop->getArgument(0), loop, + ValueRange{lastIteration.getResult()}, exit, + ValueRange{}); + + builder.setInsertionPointToEnd(exit); + func::ReturnOp::create(builder, loc); + ASSERT_TRUE(succeeded(verify(module))); + + ASSERT_TRUE(succeeded(runQCToQIRAdaptiveConversionSimple(module))); + ASSERT_TRUE(succeeded(verify(module))); + auto loweredMain = qir::getMainFunction(module); + ASSERT_TRUE(loweredMain); + Block* qubitAllocationBlock = nullptr; + Block* qubitReleaseBlock = nullptr; + Block* arrayAllocationBlock = nullptr; + Block* arrayReleaseBlock = nullptr; + loweredMain.walk([&](LLVM::CallOp call) { + if (call.getCallee() == qir::QIR_QUBIT_ALLOC) { + qubitAllocationBlock = call->getBlock(); + } else if (call.getCallee() == qir::QIR_QUBIT_RELEASE) { + qubitReleaseBlock = call->getBlock(); + } else if (call.getCallee() == qir::QIR_QUBIT_ARRAY_ALLOC) { + arrayAllocationBlock = call->getBlock(); + } else if (call.getCallee() == qir::QIR_QUBIT_ARRAY_RELEASE) { + arrayReleaseBlock = call->getBlock(); + } + }); + ASSERT_NE(qubitAllocationBlock, nullptr); + ASSERT_NE(qubitReleaseBlock, nullptr); + ASSERT_NE(arrayAllocationBlock, nullptr); + ASSERT_NE(arrayReleaseBlock, nullptr); + EXPECT_EQ(qubitReleaseBlock, qubitAllocationBlock); + EXPECT_EQ(arrayReleaseBlock, arrayAllocationBlock); +} + +TEST(QCToQIRAdaptiveNativeTest, + RejectsInconsistentLoweredReturnTypesBeforeMutation) { + MLIRContext context; + context.loadDialect(); + OpBuilder builder(&context); + auto module = ModuleOp::create(builder.getUnknownLoc()); + builder.setInsertionPointToStart(module.getBody()); + auto main = + func::FuncOp::create(builder, builder.getUnknownLoc(), "main", + builder.getFunctionType({}, {builder.getI1Type()})); + mlir::mqt::setEntryPoint(main); + auto* entry = main.addEntryBlock(); + auto* measured = main.addBlock(); + auto* ordinary = main.addBlock(); + const auto loc = builder.getUnknownLoc(); + builder.setInsertionPointToEnd(entry); + auto qubit = qc::StaticOp::create(builder, loc, 0); + auto condition = arith::ConstantIntOp::create(builder, loc, 1, 1); + cf::CondBranchOp::create(builder, loc, condition, measured, ValueRange{}, + ordinary, ValueRange{}); + builder.setInsertionPointToEnd(measured); + auto measurement = qc::MeasureOp::create(builder, loc, qubit.getQubit()); + func::ReturnOp::create(builder, loc, measurement.getResult()); + builder.setInsertionPointToEnd(ordinary); + auto zero = arith::ConstantIntOp::create(builder, loc, 0, 1); + func::ReturnOp::create(builder, loc, zero.getResult()); + ASSERT_TRUE(succeeded(verify(module))); + auto before = module.clone(); + + EXPECT_TRUE(failed(runQCToQIRAdaptiveConversionSimple(module))); + EXPECT_TRUE(isEquivalentToClone(module, before)); +} + +TEST(QCToQIRAdaptiveNativeTest, + RejectsPathDependentClassicalOutputsBeforeMutation) { + MLIRContext context; + context.loadDialect(); + OpBuilder builder(&context); + auto loc = builder.getUnknownLoc(); + auto module = ModuleOp::create(loc); + builder.setInsertionPointToStart(module.getBody()); + auto main = func::FuncOp::create( + builder, loc, "main", builder.getFunctionType({}, {builder.getI1Type()})); + mlir::mqt::setEntryPoint(main); + auto* entry = main.addEntryBlock(); + auto* measureFirst = main.addBlock(); + auto* measureSecond = main.addBlock(); + builder.setInsertionPointToEnd(entry); + auto firstQubit = qc::StaticOp::create(builder, loc, 0); + auto secondQubit = qc::StaticOp::create(builder, loc, 1); + auto condition = arith::ConstantIntOp::create(builder, loc, 1, 1); + cf::CondBranchOp::create(builder, loc, condition, measureFirst, ValueRange{}, + measureSecond, ValueRange{}); + builder.setInsertionPointToEnd(measureFirst); + auto firstMeasurement = + qc::MeasureOp::create(builder, loc, firstQubit.getQubit()); + func::ReturnOp::create(builder, loc, firstMeasurement.getResult()); + builder.setInsertionPointToEnd(measureSecond); + auto secondMeasurement = + qc::MeasureOp::create(builder, loc, secondQubit.getQubit()); + func::ReturnOp::create(builder, loc, secondMeasurement.getResult()); + ASSERT_TRUE(succeeded(verify(module))); + auto before = module.clone(); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + sawExpectedDiagnostic |= + StringRef(diagnostic.str()).contains("single entry-function return"); + return success(); + }); + EXPECT_TRUE(failed(runQCToQIRAdaptiveConversionSimple(module))); + EXPECT_TRUE(sawExpectedDiagnostic); + EXPECT_TRUE(isEquivalentToClone(module, before)); +} + +TEST(QCToQIRAdaptiveNativeTest, RejectsQCInHelperBeforeMutation) { + MLIRContext context; + context.loadDialect(); + qc::QCProgramBuilder programBuilder(&context); + programBuilder.initialize(); + auto module = programBuilder.finalize(); + ASSERT_TRUE(module); + + OpBuilder builder(&context); + builder.setInsertionPointToStart(module->getBody()); + auto helper = func::FuncOp::create(builder, builder.getUnknownLoc(), "helper", + builder.getFunctionType({}, {})); + auto* block = helper.addEntryBlock(); + builder.setInsertionPointToEnd(block); + auto qubit = qc::AllocOp::create(builder, builder.getUnknownLoc()); + qc::DeallocOp::create(builder, builder.getUnknownLoc(), qubit); + func::ReturnOp::create(builder, builder.getUnknownLoc()); + ASSERT_TRUE(succeeded(verify(*module))); + auto before = module->clone(); + + EXPECT_TRUE(failed(runQCToQIRAdaptiveConversionSimple(*module))); + EXPECT_TRUE(isEquivalentToClone(*module, before)); +} + +TEST(QCToQIRAdaptiveNativeTest, RejectsMixedAllocationModesBeforeMutation) { + MLIRContext context; + context.loadDialect(); + OpBuilder builder(&context); + auto module = ModuleOp::create(builder.getUnknownLoc()); + builder.setInsertionPointToStart(module.getBody()); + auto main = func::FuncOp::create(builder, builder.getUnknownLoc(), "main", + builder.getFunctionType({}, {})); + mlir::mqt::setEntryPoint(main); + auto* block = main.addEntryBlock(); + builder.setInsertionPointToEnd(block); + qc::StaticOp::create(builder, builder.getUnknownLoc(), 0); + qc::AllocOp::create(builder, builder.getUnknownLoc()); + func::ReturnOp::create(builder, builder.getUnknownLoc()); + ASSERT_TRUE(succeeded(verify(module))); + auto before = module.clone(); + + EXPECT_TRUE(failed(runQCToQIRAdaptiveConversionSimple(module))); + EXPECT_TRUE(isEquivalentToClone(module, before)); +} + TEST(QCToQIRAdaptiveNativeTest, NormalizesFactorableControlledGlobalPhaseBeforeLowering) { MLIRContext context; @@ -140,6 +745,7 @@ TEST(QCToQIRAdaptiveNativeTest, RejectsControlledPhaseWithNonHoistableAngle) { moduleBuilder.getFunctionType({}, {moduleBuilder.getF64Type()})); angleFunction.setPrivate(); ASSERT_TRUE(succeeded(verify(*moduleOp))); + auto before = moduleOp->clone(); bool sawExpectedDiagnostic = false; ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { @@ -152,6 +758,7 @@ TEST(QCToQIRAdaptiveNativeTest, RejectsControlledPhaseWithNonHoistableAngle) { }); EXPECT_TRUE(failed(runQCToQIRAdaptiveConversion(*moduleOp))); EXPECT_TRUE(sawExpectedDiagnostic); + EXPECT_TRUE(isEquivalentToClone(*moduleOp, before)); } TEST(QCToQIRAdaptiveNativeTest, LowersControlFlowAssertions) { @@ -306,6 +913,7 @@ TEST(QCToQIRAdaptiveNativeTest, RejectsNonMeasurementClassicalStore) { builder.retype(c.getType()); auto module = builder.finalize(c); ASSERT_TRUE(module); + auto before = module->clone(); bool sawExpectedDiagnostic = false; ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { @@ -318,6 +926,7 @@ TEST(QCToQIRAdaptiveNativeTest, RejectsNonMeasurementClassicalStore) { }); EXPECT_TRUE(failed(runQCToQIRAdaptiveConversionSimple(*module))); EXPECT_TRUE(sawExpectedDiagnostic); + EXPECT_TRUE(isEquivalentToClone(*module, before)); } TEST(QCToQIRAdaptiveNativeTest, AcceptsZeroInitializedClassicalRegister) { @@ -433,6 +1042,40 @@ TEST(QCToQIRAdaptiveNativeTest, RejectsUnsupportedIntegerMemref) { EXPECT_TRUE(sawExpectedDiagnostic); } +TEST(QCToQIRAdaptiveNativeTest, RejectsRankZeroLoadBeforeMutation) { + MLIRContext context; + context.loadDialect(); + OpBuilder builder(&context); + const auto loc = builder.getUnknownLoc(); + auto module = ModuleOp::create(loc); + builder.setInsertionPointToStart(module.getBody()); + auto main = func::FuncOp::create(builder, loc, "main", + builder.getFunctionType({}, {})); + mlir::mqt::setEntryPoint(main); + auto* block = main.addEntryBlock(); + builder.setInsertionPointToEnd(block); + const auto type = MemRefType::get({}, qc::QubitType::get(&context)); + auto storage = memref::AllocaOp::create(builder, loc, type); + memref::LoadOp::create(builder, loc, storage.getResult(), ValueRange{}); + func::ReturnOp::create(builder, loc); + ASSERT_TRUE(succeeded(verify(module))); + auto before = module.clone(); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + std::string message; + llvm::raw_string_ostream(message) << diagnostic; + sawExpectedDiagnostic |= StringRef(message).contains( + "only supports one-dimensional qubit register loads with exactly " + "one index"); + return success(); + }); + EXPECT_TRUE(failed(runQCToQIRAdaptiveConversionSimple(module))); + EXPECT_TRUE(sawExpectedDiagnostic); + EXPECT_TRUE(isEquivalentToClone(module, before)); +} + TEST_P(QCToQIRAdaptiveTest, ProgramEquivalence) { const auto& [_, programBuilder, referenceBuilder] = GetParam(); const auto name = " (" + GetParam().name + ")"; diff --git a/mlir/unittests/Conversion/QCToQIR/QCToQIRBase/CMakeLists.txt b/mlir/unittests/Conversion/QCToQIR/QCToQIRBase/CMakeLists.txt index 9e5524b354..6c79bd1f41 100644 --- a/mlir/unittests/Conversion/QCToQIR/QCToQIRBase/CMakeLists.txt +++ b/mlir/unittests/Conversion/QCToQIR/QCToQIRBase/CMakeLists.txt @@ -11,7 +11,8 @@ add_executable(${target_name} test_qc_to_qir_base.cpp) target_link_libraries( ${target_name} - PRIVATE MLIRParser + PRIVATE MLIRAffineDialect + MLIRParser MLIRSupportMQT GTest::gtest_main MLIRQCProgramBuilder @@ -19,7 +20,8 @@ target_link_libraries( MLIRQCPrograms MLIRQIRPrograms MLIRQCToQIRBase - MLIRMQTTransforms) + MLIRMQTTransforms + MLIRTensorDialect) mqt_mlir_configure_unittest_target(${target_name}) 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 41bfced7c1..20ff89e041 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 @@ -11,9 +11,12 @@ #include "Support/IRVerification.h" #include "TestCaseUtils.h" #include "mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.h" +#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Transforms/Passes.h" #include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" #include "mlir/Dialect/QC/IR/QCDialect.h" +#include "mlir/Dialect/QC/IR/QCOps.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QIR/Builder/QIRProgramBuilder.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" #include "mlir/Support/Passes.h" @@ -22,6 +25,7 @@ #include #include +#include #include #include #include @@ -31,17 +35,22 @@ #include #include #include +#include +#include #include #include #include #include +#include #include +#include #include #include #include #include #include +#include #include #include #include @@ -91,6 +100,18 @@ static LogicalResult runQCToQIRBaseConversion(ModuleOp moduleOp) { return pm.run(moduleOp); } +static LogicalResult runQCToQIRBasePass(ModuleOp module) { + PassManager pm(module.getContext()); + pm.addPass(createQCToQIRBase()); + return pm.run(module); +} + +static bool isEquivalentToClone(ModuleOp module, ModuleOp clone) { + return OperationEquivalence::isEquivalentTo( + module.getOperation(), clone.getOperation(), + OperationEquivalence::Flags::None); +} + static void expectFollowingXIsUncontrolled( const function_ref buildModifier) { @@ -126,35 +147,565 @@ TEST(QCToQIRBaseNativeTest, EmptyCtrlDoesNotControlFollowingGate) { }); } +TEST(QCToQIRBaseNativeTest, LowersResetInIrreversibleBlock) { + MLIRContext context; + context.loadDialect(); + qc::QCProgramBuilder builder(&context); + builder.initialize(); + auto qubit = builder.allocQubit(); + builder.reset(qubit); + auto module = builder.finalize(); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + ASSERT_TRUE(succeeded(runQCToQIRBasePass(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + + auto main = qir::getMainFunction(*module); + ASSERT_TRUE(main); + SmallVector blocks; + for (Block& block : main.getBody()) { + blocks.push_back(&block); + } + ASSERT_EQ(blocks.size(), 4U); + + LLVM::CallOp resetCall; + main.walk([&](LLVM::CallOp call) { + if (call.getCallee() == qir::QIR_RESET) { + resetCall = call; + } + }); + ASSERT_TRUE(resetCall); + EXPECT_EQ(resetCall->getBlock(), blocks[2]); +} + +TEST(QCToQIRBaseNativeTest, + RejectsReversibleOperationAfterIrreversibleWithoutMutation) { + for (const bool useMeasurement : {false, true}) { + SCOPED_TRACE(useMeasurement ? "measurement" : "reset"); + MLIRContext context; + context.loadDialect(); + qc::QCProgramBuilder builder(&context); + builder.initialize(); + auto qubit = builder.allocQubit(); + if (useMeasurement) { + (void)builder.measure(qubit); + } else { + builder.reset(qubit); + } + builder.h(qubit); + auto module = builder.finalize(); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + auto before = module->clone(); + + std::string diagnostics; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + return success(); + }); + EXPECT_TRUE(failed(runQCToQIRBasePass(*module))); + EXPECT_TRUE(StringRef(diagnostics) + .contains("requires operations with observable effects or " + "non-speculatable behavior to precede all " + "measurements and resets")); + EXPECT_TRUE(isEquivalentToClone(*module, before)); + } +} + +TEST(QCToQIRBaseNativeTest, + AllowsIndependentUnitaryAndGlobalPhaseAfterIrreversible) { + for (const bool useMeasurement : {false, true}) { + SCOPED_TRACE(useMeasurement ? "measurement" : "reset"); + MLIRContext context; + context.loadDialect(); + qc::QCProgramBuilder builder(&context); + builder.initialize(); + auto first = builder.staticQubit(0); + auto second = builder.staticQubit(1); + if (useMeasurement) { + (void)builder.measure(first); + } else { + builder.reset(first); + } + builder.h(second); + builder.gphase(0.25); + auto module = builder.finalize(); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + EXPECT_TRUE(succeeded(runQCToQIRBasePass(*module))); + EXPECT_TRUE(succeeded(verify(*module))); + } +} + +TEST(QCToQIRBaseNativeTest, + RejectsAliasedStaticQubitAfterIrreversibleWithoutMutation) { + MLIRContext context; + context.loadDialect(); + qc::QCProgramBuilder builder(&context); + builder.initialize(); + (void)builder.measure(builder.staticQubit(7)); + builder.h(builder.staticQubit(7)); + auto module = builder.finalize(); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + auto before = module->clone(); + + EXPECT_TRUE(failed(runQCToQIRBasePass(*module))); + EXPECT_TRUE(isEquivalentToClone(*module, before)); +} + +TEST(QCToQIRBaseNativeTest, RejectsSideEffectAfterIrreversibleWithoutMutation) { + for (const bool useMeasurement : {false, true}) { + SCOPED_TRACE(useMeasurement ? "measurement" : "reset"); + MLIRContext context; + context.loadDialect(); + qc::QCProgramBuilder builder(&context); + builder.initialize(); + auto qubit = builder.allocQubit(); + if (useMeasurement) { + (void)builder.measure(qubit); + } else { + builder.reset(qubit); + } + func::CallOp::create(builder, builder.getLoc(), "side_effect", TypeRange{}, + ValueRange{}); + auto module = builder.finalize(); + ASSERT_TRUE(module); + OpBuilder moduleBuilder(&context); + moduleBuilder.setInsertionPointToStart(module->getBody()); + auto callee = + func::FuncOp::create(moduleBuilder, module->getLoc(), "side_effect", + moduleBuilder.getFunctionType({}, {})); + callee.setPrivate(); + ASSERT_TRUE(succeeded(verify(*module))); + auto before = module->clone(); + + std::string diagnostics; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + return success(); + }); + EXPECT_TRUE(failed(runQCToQIRBasePass(*module))); + EXPECT_TRUE(StringRef(diagnostics) + .contains("requires operations with observable effects or " + "non-speculatable behavior to precede all " + "measurements and resets")); + EXPECT_TRUE(isEquivalentToClone(*module, before)); + } +} + +TEST(QCToQIRBaseNativeTest, RejectsLiveMeasurementResultUseWithoutMutation) { + MLIRContext context; + context.loadDialect(); + qc::QCProgramBuilder builder(&context); + builder.initialize(); + auto qubit = builder.allocQubit(); + auto result = builder.measure(qubit); + auto extended = arith::ExtUIOp::create(builder, builder.getI64Type(), result); + auto module = builder.finalize(extended.getResult()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + auto before = module->clone(); + + std::string diagnostics; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + return success(); + }); + EXPECT_TRUE(failed(runQCToQIRBasePass(*module))); + EXPECT_TRUE(StringRef(diagnostics) + .contains("only supports measurement results returned " + "directly or stored directly in returned CBit " + "registers")); + EXPECT_TRUE(isEquivalentToClone(*module, before)); +} + +TEST(QCToQIRBaseNativeTest, DeclaresModuleRootAndProducedDialects) { + auto pass = createQCToQIRBase(); + ASSERT_TRUE(pass->getOpName()); + EXPECT_EQ(*pass->getOpName(), ModuleOp::getOperationName()); + + DialectRegistry registry; + pass->getDependentDialects(registry); + EXPECT_TRUE( + registry.getDialectAllocator(arith::ArithDialect::getDialectNamespace())); + EXPECT_TRUE( + registry.getDialectAllocator(qc::QCDialect::getDialectNamespace())); + EXPECT_TRUE( + registry.getDialectAllocator(qco::QCODialect::getDialectNamespace())); +} + +TEST(QCToQIRBaseNativeTest, RejectsExcessiveClassicalResultCapacityAtomically) { + MLIRContext context; + context.loadDialect(); + auto module = + qc::QCProgramBuilder::build(&context, [](qc::QCProgramBuilder& builder) { + builder.allocClassicalBitRegister(1LL << 30); + return builder.intConstant(0); + }); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + auto before = module->clone(); + + EXPECT_TRUE(failed(runQCToQIRBasePass(*module))); + EXPECT_TRUE(isEquivalentToClone(*module, before)); +} + +TEST(QCToQIRBaseNativeTest, RejectsMultiBlockEntryBeforeMutation) { + MLIRContext context; + context.loadDialect(); + OpBuilder builder(&context); + auto module = ModuleOp::create(builder.getUnknownLoc()); + builder.setInsertionPointToStart(module.getBody()); + auto main = func::FuncOp::create(builder, builder.getUnknownLoc(), "main", + builder.getFunctionType({}, {})); + mlir::mqt::setEntryPoint(main); + auto* entry = main.addEntryBlock(); + auto* exit = main.addBlock(); + builder.setInsertionPointToEnd(entry); + cf::BranchOp::create(builder, builder.getUnknownLoc(), exit); + builder.setInsertionPointToEnd(exit); + func::ReturnOp::create(builder, builder.getUnknownLoc()); + ASSERT_TRUE(succeeded(verify(module))); + auto before = module.clone(); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + std::string message; + llvm::raw_string_ostream(message) << diagnostic; + sawExpectedDiagnostic |= + StringRef(message).contains("requires a single-block entry function"); + return success(); + }); + EXPECT_TRUE(failed(runQCToQIRBasePass(module))); + EXPECT_TRUE(sawExpectedDiagnostic); + EXPECT_TRUE(isEquivalentToClone(module, before)); +} + TEST(QCToQIRBaseNativeTest, RejectsMultiBlockEntryFunctionWithoutMutation) { MLIRContext context; context.loadDialect(); qc::QCProgramBuilder builder(&context); builder.initialize(); - auto moduleOp = builder.finalize(); - ASSERT_TRUE(moduleOp); - auto entryPoint = moduleOp->lookupSymbol("main"); + auto module = builder.finalize(); + ASSERT_TRUE(module); + auto entryPoint = module->lookupSymbol("main"); ASSERT_TRUE(entryPoint); auto* extraBlock = &entryPoint.getBody().emplaceBlock(); builder.setInsertionPointToEnd(extraBlock); auto status = arith::ConstantIntOp::create(builder, builder.getUnknownLoc(), 0, 64); func::ReturnOp::create(builder, builder.getUnknownLoc(), status.getResult()); - ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(succeeded(verify(*module))); + auto before = module->clone(); + + EXPECT_TRUE(failed(runQCToQIRBaseConversion(*module))); + EXPECT_TRUE(isEquivalentToClone(*module, before)); +} + +TEST(QCToQIRBaseNativeTest, RejectsSingleBlockBackedgeBeforeMutation) { + MLIRContext context; + context.loadDialect(); + OpBuilder builder(&context); + auto module = ModuleOp::create(builder.getUnknownLoc()); + builder.setInsertionPointToStart(module.getBody()); + auto main = func::FuncOp::create(builder, builder.getUnknownLoc(), "main", + builder.getFunctionType({}, {})); + mlir::mqt::setEntryPoint(main); + auto* entry = main.addEntryBlock(); + builder.setInsertionPointToEnd(entry); + cf::BranchOp::create(builder, builder.getUnknownLoc(), entry); + auto before = module.clone(); + + EXPECT_TRUE(failed(runQCToQIRBasePass(module))); + EXPECT_TRUE(isEquivalentToClone(module, before)); +} + +TEST(QCToQIRBaseNativeTest, RejectsStructuredControlFlowBeforeMutation) { + MLIRContext context; + context.loadDialect(); + OpBuilder builder(&context); + auto module = ModuleOp::create(builder.getUnknownLoc()); + builder.setInsertionPointToStart(module.getBody()); + auto main = func::FuncOp::create(builder, builder.getUnknownLoc(), "main", + builder.getFunctionType({}, {})); + mlir::mqt::setEntryPoint(main); + auto* entry = main.addEntryBlock(); + builder.setInsertionPointToEnd(entry); + const auto loc = builder.getUnknownLoc(); + auto condition = arith::ConstantIntOp::create(builder, loc, 1, 1); + scf::IfOp::create(builder, loc, TypeRange{}, condition, false); + func::ReturnOp::create(builder, builder.getUnknownLoc()); + ASSERT_TRUE(succeeded(verify(module))); + auto before = module.clone(); + + EXPECT_TRUE(failed(runQCToQIRBasePass(module))); + EXPECT_TRUE(isEquivalentToClone(module, before)); +} + +TEST(QCToQIRBaseNativeTest, RejectsAffineIfMeasurementBeforeMutation) { + MLIRContext context; + context + .loadDialect(); + auto module = parseSourceString(R"mlir( + module { + func.func @main() attributes {mqt.entry_point} { + %qubit = qc.static 0 : !qc.qubit + %zero = arith.constant 0 : index + affine.if affine_set<(d0) : (d0 >= 0)>(%zero) { + %result = qc.measure %qubit : !qc.qubit -> i1 + } + return + } + } + )mlir", + &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + auto before = module->clone(); 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 requires a single-block entry function"); + llvm::raw_string_ostream(message) << diagnostic; + sawExpectedDiagnostic |= + StringRef(message).contains("does not support region-based control"); return success(); }); - EXPECT_TRUE(failed(runQCToQIRBaseConversion(*moduleOp))); + EXPECT_TRUE(failed(runQCToQIRBasePass(*module))); EXPECT_TRUE(sawExpectedDiagnostic); - EXPECT_EQ(entryPoint.getBlocks().size(), 2); + EXPECT_TRUE(isEquivalentToClone(*module, before)); + EXPECT_TRUE(succeeded(verify(*module))); +} + +TEST(QCToQIRBaseNativeTest, RejectsControlFlowInHelperBeforeMutation) { + MLIRContext context; + context.loadDialect(); + auto module = parseSourceString(R"mlir( + module { + func.func private @helper(%condition: i1) { + scf.if %condition { + } + return + } + func.func @main() attributes {mqt.entry_point} { + %condition = arith.constant true + func.call @helper(%condition) : (i1) -> () + return + } + } + )mlir", + &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + auto before = module->clone(); + + EXPECT_TRUE(failed(runQCToQIRBasePass(*module))); + EXPECT_TRUE(isEquivalentToClone(*module, before)); + EXPECT_TRUE(succeeded(verify(*module))); +} + +TEST(QCToQIRBaseNativeTest, PreservesNonControlRegionOperations) { + MLIRContext context; + context.loadDialect(); + auto module = parseSourceString(R"mlir( + module { + func.func @main() attributes {mqt.entry_point} { + %generated = tensor.generate { + ^bb0(%index: index): + %value = arith.constant 7 : i64 + tensor.yield %value : i64 + } : tensor<1xi64> + return + } + } + )mlir", + &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + ASSERT_TRUE(succeeded(runQCToQIRBasePass(*module))); + EXPECT_TRUE(succeeded(verify(*module))); + size_t generated = 0; + module->walk([&](tensor::GenerateOp) { ++generated; }); + EXPECT_EQ(generated, 1U); +} + +TEST(QCToQIRBaseNativeTest, RejectsEntryArgumentsBeforeMutation) { + MLIRContext context; + context.loadDialect(); + OpBuilder builder(&context); + auto module = ModuleOp::create(builder.getUnknownLoc()); + builder.setInsertionPointToStart(module.getBody()); + auto main = + func::FuncOp::create(builder, builder.getUnknownLoc(), "main", + builder.getFunctionType({builder.getI64Type()}, {})); + mlir::mqt::setEntryPoint(main); + auto* entry = main.addEntryBlock(); + builder.setInsertionPointToEnd(entry); + func::ReturnOp::create(builder, builder.getUnknownLoc()); + ASSERT_TRUE(succeeded(verify(module))); + auto before = module.clone(); + + EXPECT_TRUE(failed(runQCToQIRBasePass(module))); + EXPECT_TRUE(isEquivalentToClone(module, before)); +} + +TEST(QCToQIRBaseNativeTest, ReusesRepeatedRegisterLoads) { + MLIRContext context; + context.loadDialect(); + qc::QCProgramBuilder builder(&context); + builder.initialize(); + auto reg = builder.allocQubitRegisterStorage(2); + auto index = arith::ConstantIndexOp::create(builder, 0); + auto first = builder.loadQubit(reg, index); + builder.h(first); + auto second = builder.loadQubit(reg, index); + builder.x(second); + auto module = builder.finalize(); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(runQCToQIRBasePass(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + + Value hQubit; + Value xQubit; + module->walk([&](LLVM::CallOp call) { + if (call.getCallee() == qir::QIR_H) { + hQubit = call.getOperand(0); + } else if (call.getCallee() == qir::QIR_X) { + xQubit = call.getOperand(0); + } + }); + ASSERT_TRUE(hQubit); + ASSERT_TRUE(xQubit); + EXPECT_EQ(hQubit, xQubit); +} + +TEST(QCToQIRBaseNativeTest, AllocatesAfterSparseStaticQubitIds) { + MLIRContext context; + context.loadDialect(); + OpBuilder builder(&context); + auto module = ModuleOp::create(builder.getUnknownLoc()); + builder.setInsertionPointToStart(module.getBody()); + auto main = func::FuncOp::create(builder, builder.getUnknownLoc(), "main", + builder.getFunctionType({}, {})); + mlir::mqt::setEntryPoint(main); + auto* block = main.addEntryBlock(); + builder.setInsertionPointToEnd(block); + auto fixed = qc::StaticOp::create(builder, builder.getUnknownLoc(), 2); + auto allocated = qc::AllocOp::create(builder, builder.getUnknownLoc()); + qc::HOp::create(builder, builder.getUnknownLoc(), fixed); + qc::HOp::create(builder, builder.getUnknownLoc(), allocated); + qc::DeallocOp::create(builder, builder.getUnknownLoc(), allocated); + func::ReturnOp::create(builder, builder.getUnknownLoc()); + ASSERT_TRUE(succeeded(verify(module))); + + ASSERT_TRUE(succeeded(runQCToQIRBasePass(module))); + ASSERT_TRUE(succeeded(verify(module))); + SmallVector qubitIds; + module.walk([&](LLVM::CallOp call) { + if (call.getCallee() != qir::QIR_H) { + return; + } + auto pointer = call.getOperand(0).getDefiningOp(); + ASSERT_TRUE(pointer); + auto index = pointer.getArg().getDefiningOp(); + ASSERT_TRUE(index); + qubitIds.push_back(cast(index.getValue()).getInt()); + }); + llvm::sort(qubitIds); + EXPECT_EQ(qubitIds, SmallVector({2, 3})); +} + +TEST(QCToQIRBaseNativeTest, RejectsDynamicRegisterIndexBeforeMutation) { + MLIRContext context; + context.loadDialect(); + qc::QCProgramBuilder builder(&context); + builder.initialize(); + auto reg = builder.allocQubitRegisterStorage(2); + auto unknown = LLVM::UndefOp::create(builder, builder.getI64Type()); + auto index = arith::IndexCastOp::create(builder, builder.getIndexType(), + unknown.getResult()); + builder.h(builder.loadQubit(reg, index)); + auto module = builder.finalize(); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + auto before = module->clone(); + + EXPECT_TRUE(failed(runQCToQIRBasePass(*module))); + EXPECT_TRUE(isEquivalentToClone(*module, before)); +} + +TEST(QCToQIRBaseNativeTest, RejectsRankZeroLoadBeforeMutation) { + MLIRContext context; + context.loadDialect(); + OpBuilder builder(&context); + const auto loc = builder.getUnknownLoc(); + auto module = ModuleOp::create(loc); + builder.setInsertionPointToStart(module.getBody()); + auto main = func::FuncOp::create(builder, loc, "main", + builder.getFunctionType({}, {})); + mlir::mqt::setEntryPoint(main); + auto* block = main.addEntryBlock(); + builder.setInsertionPointToEnd(block); + const auto type = MemRefType::get({}, qc::QubitType::get(&context)); + auto storage = memref::AllocaOp::create(builder, loc, type); + memref::LoadOp::create(builder, loc, storage, ValueRange{}); + func::ReturnOp::create(builder, loc); + ASSERT_TRUE(succeeded(verify(module))); + auto before = module.clone(); + + EXPECT_TRUE(failed(runQCToQIRBasePass(module))); + EXPECT_TRUE(isEquivalentToClone(module, before)); +} + +TEST(QCToQIRBaseNativeTest, RejectsStaticQubitIndexExhaustionBeforeMutation) { + MLIRContext context; + context + .loadDialect(); + OpBuilder builder(&context); + const auto loc = builder.getUnknownLoc(); + auto module = ModuleOp::create(loc); + builder.setInsertionPointToStart(module.getBody()); + auto main = func::FuncOp::create(builder, loc, "main", + builder.getFunctionType({}, {})); + mlir::mqt::setEntryPoint(main); + auto* block = main.addEntryBlock(); + builder.setInsertionPointToEnd(block); + auto fixed = qc::StaticOp::create( + builder, loc, + static_cast(std::numeric_limits::max() - 1)); + qc::HOp::create(builder, loc, fixed.getQubit()); + auto allocated = qc::AllocOp::create(builder, loc); + qc::DeallocOp::create(builder, loc, allocated.getResult()); + func::ReturnOp::create(builder, loc); + ASSERT_TRUE(succeeded(verify(module))); + auto before = module.clone(); + + EXPECT_TRUE(failed(runQCToQIRBasePass(module))); + EXPECT_TRUE(isEquivalentToClone(module, before)); } TEST(QCToQIRBaseNativeTest, ControlledBarrierDoesNotControlFollowingGate) { @@ -308,6 +859,7 @@ TEST(QCToQIRBaseNativeTest, RejectsNonMeasurementClassicalStore) { builder.retype(c.getType()); auto module = builder.finalize(c); ASSERT_TRUE(module); + auto before = module->clone(); bool sawExpectedDiagnostic = false; ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { @@ -320,6 +872,7 @@ TEST(QCToQIRBaseNativeTest, RejectsNonMeasurementClassicalStore) { }); EXPECT_TRUE(failed(runQCToQIRBaseConversion(*module))); EXPECT_TRUE(sawExpectedDiagnostic); + EXPECT_TRUE(isEquivalentToClone(*module, before)); } TEST(QCToQIRBaseNativeTest, AcceptsZeroInitializedClassicalRegister) { diff --git a/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt b/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt index cf0a00d560..339f06c992 100644 --- a/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt +++ b/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt @@ -14,6 +14,7 @@ target_link_libraries( MLIRArithDialect MLIRCBitDialect MLIRFuncDialect + MLIRLLVMDialect MLIRMemRefDialect MLIRMQTDialect MLIRParser diff --git a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp index 363efcbd48..ea554a58a2 100644 --- a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp +++ b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -42,8 +43,8 @@ class MQTIRTest : public ::testing::Test { void SetUp() override { DialectRegistry registry; registry.insert(); + LLVM::LLVMDialect, memref::MemRefDialect, mqt::MQTDialect, + qc::QCDialect, qco::QCODialect, qtensor::QTensorDialect>(); context = std::make_unique(registry); context->loadAllAvailableDialects(); } @@ -54,7 +55,7 @@ class MQTIRTest : public ::testing::Test { }; TEST_F(MQTIRTest, AcceptsProgramInputAndRegisterNames) { - EXPECT_TRUE(parse(R"mlir( + auto moduleOp = parse(R"mlir( module { func.func @qc(%theta: f64 {mqt.input_name = "theta[2]", mqt.parameter_group = {identity = "group-id", name = "theta", @@ -82,7 +83,9 @@ TEST_F(MQTIRTest, AcceptsProgramInputAndRegisterNames) { return } } - )mlir")); + )mlir"); + ASSERT_TRUE(moduleOp); + EXPECT_TRUE(succeeded(mqt::verifyProgramMetadata(*moduleOp))); } TEST_F(MQTIRTest, ManagesAndFindsEntryPoint) { @@ -120,12 +123,6 @@ TEST_F(MQTIRTest, RejectsInvalidEntryPoints) { func.func private @main() attributes {mqt.entry_point} } )mlir")); - EXPECT_FALSE(parse(R"mlir( - module { - func.func @first() attributes {mqt.entry_point} { return } - func.func @second() attributes {mqt.entry_point} { return } - } - )mlir")); EXPECT_FALSE(parse(R"mlir( module { func.func @main() { @@ -137,6 +134,42 @@ TEST_F(MQTIRTest, RejectsInvalidEntryPoints) { )mlir")); } +TEST_F(MQTIRTest, AcceptsDefinedLLVMEntryPoint) { + auto llvmEntryPoint = parse(R"mlir( + module { + llvm.func @main() attributes {mqt.entry_point} { + llvm.return + } + } + )mlir"); + ASSERT_TRUE(llvmEntryPoint); + EXPECT_TRUE(succeeded(mqt::verifyProgramMetadata(*llvmEntryPoint))); +} + +TEST_F(MQTIRTest, ProgramMetadataRejectsDuplicateEntryPoints) { + auto moduleOp = parse(R"mlir( + module { + func.func @first() attributes {mqt.entry_point} { return } + func.func @second() attributes {mqt.entry_point} { return } + } + )mlir"); + ASSERT_TRUE(moduleOp); + EXPECT_TRUE(failed(mqt::verifyProgramMetadata(*moduleOp))); +} + +TEST_F(MQTIRTest, ProgramMetadataRejectsNonFuncEntryPoint) { + auto moduleOp = parse(R"mlir( + module { + memref.global "private" @storage : memref<1xi8> + } + )mlir"); + ASSERT_TRUE(moduleOp); + auto global = moduleOp->lookupSymbol("storage"); + ASSERT_TRUE(global); + mqt::setEntryPoint(global); + EXPECT_TRUE(failed(mqt::verifyProgramMetadata(*moduleOp))); +} + TEST_F(MQTIRTest, RejectsInvalidInputNames) { EXPECT_FALSE(parse(R"mlir( module { @@ -155,15 +188,17 @@ TEST_F(MQTIRTest, RejectsInvalidInputNames) { )mlir")); } -TEST_F(MQTIRTest, RejectsDuplicateInputNames) { - EXPECT_FALSE(parse(R"mlir( +TEST_F(MQTIRTest, ProgramMetadataRejectsDuplicateInputNames) { + auto module = parse(R"mlir( module { func.func @main(%lhs: f64 {mqt.input_name = "theta"}, %rhs: i1 {mqt.input_name = "theta"}) { return } } - )mlir")); + )mlir"); + ASSERT_TRUE(module); + EXPECT_TRUE(failed(mqt::verifyProgramMetadata(*module))); } TEST_F(MQTIRTest, RejectsInvalidInputGroups) { @@ -203,6 +238,42 @@ TEST_F(MQTIRTest, RejectsInvalidInputGroups) { )mlir")); } +TEST_F(MQTIRTest, AcceptsParameterGroupsOutsideCurrentVectorSize) { + auto moduleOp = parse(R"mlir( + module { + func.func @main( + %empty: f64 {mqt.input_name = "theta[0]", + mqt.parameter_group = {identity = "empty-vector", name = "theta", + index = 0 : i64, size = 0 : i64}}, + %shrunk: f64 {mqt.input_name = "phi[1]", + mqt.parameter_group = {identity = "shrunk-vector", name = "phi", + index = 1 : i64, size = 1 : i64}}) { + return + } + } + )mlir"); + ASSERT_TRUE(moduleOp); + EXPECT_TRUE(succeeded(mqt::verifyProgramMetadata(*moduleOp))); +} + +TEST_F(MQTIRTest, ProgramMetadataRejectsInconsistentParameterGroups) { + auto moduleOp = parse(R"mlir( + module { + func.func @main( + %lhs: f64 {mqt.input_name = "theta[0]", + mqt.parameter_group = {identity = "group", name = "theta", + index = 0 : i64, size = 2 : i64}}, + %rhs: f64 {mqt.input_name = "phi[1]", + mqt.parameter_group = {identity = "group", name = "phi", + index = 1 : i64, size = 3 : i64}}) { + return + } + } + )mlir"); + ASSERT_TRUE(moduleOp); + EXPECT_TRUE(failed(mqt::verifyProgramMetadata(*moduleOp))); +} + TEST_F(MQTIRTest, RejectsInputMetadataOnOperations) { EXPECT_FALSE(parse(R"mlir( module { @@ -255,7 +326,7 @@ TEST_F(MQTIRTest, RejectsInvalidRegisterNamesAndOwners) { } TEST_F(MQTIRTest, RejectsDuplicateProgramNames) { - EXPECT_FALSE(parse(R"mlir( + auto duplicateRegisters = parse(R"mlir( module { func.func @main() { %lhs = memref.alloc() {mqt.register_name = "state"} @@ -265,8 +336,11 @@ TEST_F(MQTIRTest, RejectsDuplicateProgramNames) { return } } - )mlir")); - EXPECT_FALSE(parse(R"mlir( + )mlir"); + ASSERT_TRUE(duplicateRegisters); + EXPECT_TRUE(failed(mqt::verifyProgramMetadata(*duplicateRegisters))); + + auto duplicateInputAndRegister = parse(R"mlir( module { func.func @main(%arg: f64 {mqt.input_name = "state"}) { %reg = cbit.alloc(#cbit.init) {mqt.register_name = "state"} @@ -274,7 +348,9 @@ TEST_F(MQTIRTest, RejectsDuplicateProgramNames) { return } } - )mlir")); + )mlir"); + ASSERT_TRUE(duplicateInputAndRegister); + EXPECT_TRUE(failed(mqt::verifyProgramMetadata(*duplicateInputAndRegister))); } TEST_F(MQTIRTest, RejectsUnknownMQTAttributes) { diff --git a/mlir/unittests/Dialect/MQT/Transforms/test_global_phase_normalization.cpp b/mlir/unittests/Dialect/MQT/Transforms/test_global_phase_normalization.cpp index 255357696f..f699ee517f 100644 --- a/mlir/unittests/Dialect/MQT/Transforms/test_global_phase_normalization.cpp +++ b/mlir/unittests/Dialect/MQT/Transforms/test_global_phase_normalization.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -106,6 +107,26 @@ class GlobalPhaseNormalizationTest : public testing::Test { } // namespace +TEST_F(GlobalPhaseNormalizationTest, + CombinesModuleLevelPhasesWithoutTerminator) { + OwningOpRef moduleOp = ModuleOp::create(UnknownLoc::get(context.get())); + OpBuilder builder(context.get()); + builder.setInsertionPointToStart(moduleOp->getBody()); + const auto loc = moduleOp->getLoc(); + auto first = mlir::mqt::constantFromScalar(builder, loc, 0.25); + qco::GPhaseOp::create(builder, loc, first); + auto second = mlir::mqt::constantFromScalar(builder, loc, 0.5); + qco::GPhaseOp::create(builder, loc, second); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + ASSERT_TRUE(mlir::mqt::normalizeGlobalPhases(*moduleOp).succeeded()); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + auto phases = llvm::to_vector(moduleOp->getBody()->getOps()); + ASSERT_EQ(phases.size(), 1); + EXPECT_EQ(phases.front()->getNextNode(), nullptr); + expectFoldableGlobalPhase(phases.front().getTheta(), 0.75); +} + TEST_F(GlobalPhaseNormalizationTest, CombinesQCOConstantsAtBlockExit) { auto moduleOp = parse(R"mlir( module { @@ -130,6 +151,67 @@ TEST_F(GlobalPhaseNormalizationTest, CombinesQCOConstantsAtBlockExit) { expectFoldableGlobalPhase(phases.front().getTheta(), 0.75); } +TEST_F(GlobalPhaseNormalizationTest, RejectsNonlinearQCOInputWithoutMutation) { + auto moduleOp = parse(R"mlir( + module { + func.func @test(%q: !qco.qubit) { + %phase = arith.constant 0.25 : f64 + qco.gphase(%phase) + %x = qco.x %q : !qco.qubit -> !qco.qubit + %h = qco.h %q : !qco.qubit -> !qco.qubit + qco.sink %x : !qco.qubit + qco.sink %h : !qco.qubit + return + } + } + )mlir"); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + OwningOpRef before(cast((*moduleOp)->clone())); + + EXPECT_TRUE(failed(mlir::mqt::normalizeGlobalPhases(*moduleOp))); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + moduleOp->getOperation(), before->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST_F(GlobalPhaseNormalizationTest, CombinesQCAndQCOPhasesIndependently) { + auto moduleOp = parse(R"mlir( + module { + func.func @test() { + %qc0 = arith.constant 0.25 : f64 + qc.gphase(%qc0) + %qco0 = arith.constant 1.0 : f64 + qco.gphase(%qco0) + %qc1 = arith.constant 0.5 : f64 + qc.gphase(%qc1) + %qco1 = arith.constant -0.25 : f64 + qco.gphase(%qco1) + return + } + } + )mlir"); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(mlir::mqt::normalizeGlobalPhases(*moduleOp).succeeded()); + ASSERT_TRUE(verify(*moduleOp).succeeded()); + + auto function = *moduleOp->getOps().begin(); + auto qcPhases = + llvm::to_vector(function.getBody().getOps()); + auto qcoPhases = + llvm::to_vector(function.getBody().getOps()); + ASSERT_EQ(qcPhases.size(), 1); + ASSERT_EQ(qcoPhases.size(), 1); + expectFoldableGlobalPhase(qcPhases.front().getTheta(), 0.75); + expectFoldableGlobalPhase(qcoPhases.front().getTheta(), 0.75); + + ASSERT_TRUE(mlir::mqt::normalizeGlobalPhases(*moduleOp).succeeded()); + EXPECT_EQ(llvm::range_size(function.getBody().getOps()), + 1); + EXPECT_EQ(llvm::range_size(function.getBody().getOps()), + 1); +} + TEST_F(GlobalPhaseNormalizationTest, FoldsMulDerivedPhasesWithinPracticalAngleLimit) { // Many arith.mulf-derived gphase angles used to be treated as dynamic and @@ -304,6 +386,53 @@ TEST_F(GlobalPhaseNormalizationTest, EXPECT_LE(countOperations(), firstRunOperationCount); } +TEST_F(GlobalPhaseNormalizationTest, + HandlesDeepModifierAndAngleChainsIteratively) { + OwningOpRef moduleOp = ModuleOp::create(UnknownLoc::get(context.get())); + OpBuilder builder(context.get()); + builder.setInsertionPointToStart(moduleOp->getBody()); + const auto loc = moduleOp->getLoc(); + const auto qubitType = qco::QubitType::get(context.get()); + auto function = func::FuncOp::create( + builder, loc, "test", + builder.getFunctionType({qubitType, builder.getF64Type()}, {qubitType})); + auto* entry = function.addEntryBlock(); + builder.setInsertionPointToStart(entry); + + constexpr std::size_t modifierDepth = 512; + constexpr std::size_t angleDepth = 512; + auto outer = + qco::InvOp::create(builder, loc, ValueRange{function.getArgument(0)}); + auto* body = &outer.getBodyRegion().emplaceBlock(); + auto currentQubit = body->addArgument(qubitType, loc); + for (std::size_t i = 1; i < modifierDepth; ++i) { + builder.setInsertionPointToEnd(body); + auto inner = qco::InvOp::create(builder, loc, ValueRange{currentQubit}); + qco::YieldOp::create(builder, loc, inner.getQubitsOut()); + body = &inner.getBodyRegion().emplaceBlock(); + currentQubit = body->addArgument(qubitType, loc); + } + + builder.setInsertionPointToEnd(body); + Value angle = function.getArgument(1); + for (std::size_t i = 0; i < angleDepth; ++i) { + auto zero = + arith::ConstantOp::create(builder, loc, builder.getF64FloatAttr(0.0)); + angle = arith::AddFOp::create(builder, loc, angle, zero); + } + auto z = qco::ZOp::create(builder, loc, currentQubit); + qco::GPhaseOp::create(builder, loc, angle); + qco::YieldOp::create(builder, loc, ValueRange{z.getOutputTarget(0)}); + + builder.setInsertionPointToEnd(entry); + func::ReturnOp::create(builder, loc, outer.getQubitsOut()); + + ASSERT_TRUE(mlir::mqt::normalizeGlobalPhases(*moduleOp).succeeded()); + auto phases = llvm::to_vector(function.getBody().getOps()); + ASSERT_EQ(phases.size(), 1); + EXPECT_EQ(phases.front().getTheta().getParentBlock(), entry); +} + TEST_F(GlobalPhaseNormalizationTest, KeepsSCFStyleRegionsIndependent) { auto moduleOp = parse(R"mlir( module { @@ -388,6 +517,35 @@ TEST_F(GlobalPhaseNormalizationTest, FractionalPowerRemainsBoundary) { EXPECT_EQ(llvm::range_size(pow.getBody()->getOps()), 1); } +TEST_F(GlobalPhaseNormalizationTest, + IntegralPowerExtractionDoesNotOverflowGlobalPhase) { + auto moduleOp = parse(R"mlir( + module { + func.func @test(%q: !qco.qubit) -> !qco.qubit { + %exponent = arith.constant 1.7976931348623157E+308 : f64 + %phase = arith.constant 1.0 : f64 + %out = qco.pow(%exponent) (%arg = %q) { + qco.gphase(%phase) + qco.yield %arg : !qco.qubit + } : {!qco.qubit} -> {!qco.qubit} + return %out : !qco.qubit + } + } + )mlir"); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + ASSERT_TRUE(succeeded(mlir::mqt::normalizeGlobalPhases(*moduleOp))); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + auto function = *moduleOp->getOps().begin(); + auto phases = llvm::to_vector(function.getBody().getOps()); + ASSERT_EQ(phases.size(), 1U); + const auto phase = + mlir::mqt::valueToConstantDouble(phases.front().getTheta()); + ASSERT_TRUE(phase); + EXPECT_TRUE(std::isfinite(*phase)); +} + TEST_F(GlobalPhaseNormalizationTest, DynamicPowerRemainsBoundary) { auto moduleOp = parse(R"mlir( module { @@ -411,6 +569,47 @@ TEST_F(GlobalPhaseNormalizationTest, DynamicPowerRemainsBoundary) { EXPECT_TRUE(func.getBody().getOps().empty()); } +TEST_F(GlobalPhaseNormalizationTest, DynamicPhasesUseBoundedRuntimeArithmetic) { + auto moduleOp = parse(R"mlir( + module { + func.func @combine(%a: f64, %b: f64) { + qco.gphase(%a) + qco.gphase(%b) + return + } + func.func @power(%q: !qco.qubit, %phase: f64) -> !qco.qubit { + %two = arith.constant 2.0 : f64 + %out = qco.pow(%two) (%arg = %q) { + qco.gphase(%phase) + qco.yield %arg : !qco.qubit + } : {!qco.qubit} -> {!qco.qubit} + return %out : !qco.qubit + } + } + )mlir"); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + ASSERT_TRUE(succeeded(mlir::mqt::normalizeGlobalPhases(*moduleOp))); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + auto combine = moduleOp->lookupSymbol("combine"); + ASSERT_TRUE(combine); + EXPECT_EQ(llvm::range_size(combine.getBody().getOps()), 1U); + size_t combineRemainders = 0; + combine.walk([&](arith::RemFOp) { ++combineRemainders; }); + EXPECT_GT(combineRemainders, 0U); + + auto power = moduleOp->lookupSymbol("power"); + ASSERT_TRUE(power); + auto pow = *power.getBody().getOps().begin(); + EXPECT_EQ(llvm::range_size(pow.getBody()->getOps()), 1U); + EXPECT_TRUE(power.getBody().getOps().empty()); + size_t powerRemainders = 0; + pow.walk([&](arith::RemFOp) { ++powerRemainders; }); + EXPECT_GT(powerRemainders, 0U); +} + TEST_F(GlobalPhaseNormalizationTest, FactorsControlledPhaseOntoControl) { auto moduleOp = parse(R"mlir( module { @@ -649,15 +848,13 @@ TEST_F(GlobalPhaseNormalizationTest, ZeroControlsReleaseAnUnchangedPhase) { EXPECT_EQ(llvm::range_size(function.getBody().getOps()), 1); } -TEST_F(GlobalPhaseNormalizationTest, - MemoryDependentAngleRemainsInsideModifier) { +TEST_F(GlobalPhaseNormalizationTest, ImpureAngleRemainsInsideModifier) { auto moduleOp = parse(R"mlir( module { - func.func @test(%q: !qco.qubit, %angles: memref<1xf64>) - -> !qco.qubit { - %c0 = arith.constant 0 : index + func.func private @get_angle() -> f64 + func.func @test(%q: !qco.qubit) -> !qco.qubit { %out = qco.inv (%arg = %q) { - %phase = memref.load %angles[%c0] : memref<1xf64> + %phase = func.call @get_angle() : () -> f64 %x = qco.x %arg : !qco.qubit -> !qco.qubit qco.gphase(%phase) qco.yield %x : !qco.qubit @@ -670,7 +867,8 @@ TEST_F(GlobalPhaseNormalizationTest, ASSERT_TRUE(mlir::mqt::normalizeGlobalPhases(*moduleOp).succeeded()); ASSERT_TRUE(verify(*moduleOp).succeeded()); - auto func = *moduleOp->getOps().begin(); + auto func = moduleOp->lookupSymbol("test"); + ASSERT_TRUE(func); auto inv = *func.getBody().getOps().begin(); EXPECT_EQ(llvm::range_size(inv.getBody()->getOps()), 1); EXPECT_TRUE(func.getBody().getOps().empty()); diff --git a/mlir/unittests/Dialect/MQT/Utils/CMakeLists.txt b/mlir/unittests/Dialect/MQT/Utils/CMakeLists.txt index 3096dc4013..592bf561a2 100644 --- a/mlir/unittests/Dialect/MQT/Utils/CMakeLists.txt +++ b/mlir/unittests/Dialect/MQT/Utils/CMakeLists.txt @@ -8,8 +8,9 @@ set(mqt_utils_target mqt-core-mlir-unittests-mqt-utils) add_executable(${mqt_utils_target} test_constant_folding.cpp test_gate_powering.cpp) -target_link_libraries(${mqt_utils_target} PRIVATE GTest::gtest_main MLIRArithDialect - MLIRFuncDialect MLIRIR MLIRMQTUtils) +target_link_libraries( + ${mqt_utils_target} PRIVATE GTest::gtest_main MLIRArithDialect MLIRFuncDialect MLIRIndexDialect + MLIRIR MLIRMQTUtils) mqt_mlir_configure_unittest_target(${mqt_utils_target}) gtest_discover_tests(${mqt_utils_target} PROPERTIES LABELS mqt-mlir-unittests DISCOVERY_TIMEOUT 60) diff --git a/mlir/unittests/Dialect/MQT/Utils/test_constant_folding.cpp b/mlir/unittests/Dialect/MQT/Utils/test_constant_folding.cpp index 3ccde2b38b..cb1f0554a4 100644 --- a/mlir/unittests/Dialect/MQT/Utils/test_constant_folding.cpp +++ b/mlir/unittests/Dialect/MQT/Utils/test_constant_folding.cpp @@ -13,6 +13,8 @@ #include #include #include +#include +#include #include #include #include @@ -39,7 +41,8 @@ class ConstantFoldingTest : public ::testing::Test { std::unique_ptr builder; void SetUp() override { - context.loadDialect(); + context.loadDialect(); auto loc = FileLineColLoc::get(&context, "", 1, 1); module = ModuleOp::create(loc); @@ -66,6 +69,13 @@ TEST_F(ConstantFoldingTest, valueToDoubleCastFromInteger) { EXPECT_DOUBLE_EQ(*stdValue, 42.0); } +TEST_F(ConstantFoldingTest, valueToDoubleConstantLike) { + auto op = index::ConstantOp::create(*builder, 42); + const auto stdValue = mlir::mqt::valueToDouble(op.getResult()); + ASSERT_TRUE(stdValue.has_value()); + EXPECT_DOUBLE_EQ(*stdValue, 42.0); +} + TEST_F(ConstantFoldingTest, valueToDoubleCastFromNegativeInteger) { auto op = arith::ConstantOp::create(*builder, builder->getSI32IntegerAttr(-123)); @@ -251,3 +261,16 @@ TEST_F(ConstantFoldingTest, valueToConstantDoubleSharedOperandsFailure) { EXPECT_FALSE(it->second.has_value()); } } + +TEST_F(ConstantFoldingTest, valueToConstantAttrHandlesDeepExpressions) { + constexpr int depth = 10000; + Value value = arith::ConstantIntOp::create(*builder, 1, 64); + Value zero = arith::ConstantIntOp::create(*builder, 0, 64); + for (int i = 0; i < depth; ++i) { + value = arith::AddIOp::create(*builder, value, zero); + } + + const auto folded = mlir::mqt::valueToConstantAttr(value); + ASSERT_TRUE(folded); + EXPECT_EQ(cast(*folded).getInt(), 1); +} diff --git a/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp b/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp index daaceed3fa..ad1728b9f8 100644 --- a/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp +++ b/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp @@ -45,6 +45,7 @@ #include #include #include +#include #include #include @@ -181,6 +182,30 @@ TEST_F(QCTest, CleanupHoistsAndCoalescesStaticQubits) { EXPECT_EQ(staticOps, 2U); } +TEST_F(QCTest, CleanupDoesNotHoistStaticQubitsAcrossIsolationBoundaries) { + auto module = parseSourceString(R"mlir( + module { + func.func @main() { + "builtin.module"() ({ + %q = qc.static 0 : !qc.qubit + qc.x %q : !qc.qubit + }) : () -> () + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + ASSERT_TRUE(succeeded(runQCCleanupPipeline(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + + StaticOp staticOp; + module->walk([&](StaticOp op) { staticOp = op; }); + ASSERT_TRUE(staticOp); + EXPECT_TRUE(isa(staticOp->getParentOp())); +} + TEST_F(QCTest, BuilderRejectsMixedStaticAndDynamicQubitAllocationModes) { EXPECT_DEATH( { @@ -405,6 +430,63 @@ TEST_F(QCTest, UnitaryVerifierRejectsNonFiniteConstantParameters) { } } +TEST_F(QCTest, GlobalPhaseVerifierRejectsDirectAndFoldedNonFiniteAngles) { + constexpr std::array invalidPrograms{ + R"mlir( + module { + func.func @main() { + %infinity = arith.constant 0x7FF0000000000000 : f64 + qc.gphase(%infinity) + return + } + } + )mlir", + R"mlir( + module { + func.func @main() { + %nan = arith.constant 0x7FF8000000000000 : f64 + qc.gphase(%nan) + return + } + } + )mlir", + R"mlir( + module { + func.func @main() { + %max = arith.constant 1.7976931348623157E+308 : f64 + %infinity = arith.addf %max, %max : f64 + qc.gphase(%infinity) + return + } + } + )mlir", + R"mlir( + module { + func.func @main() { + %zero = arith.constant 0.0 : f64 + %nan = arith.divf %zero, %zero : f64 + qc.gphase(%nan) + return + } + } + )mlir"}; + + for (const auto source : invalidPrograms) { + bool sawExpectedDiagnostic = false; + std::string diagnostics; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + sawExpectedDiagnostic |= + StringRef(diagnostic.str()) + .contains( + "constant parameter expression at index 0 must be finite"); + return success(); + }); + EXPECT_FALSE(parseSourceString(source, context.get())); + EXPECT_TRUE(sawExpectedDiagnostic) << diagnostics; + } +} + TEST_F(QCTest, DenseUnitaryBuilderVerifiesAndCanonicalizesIdentity) { const auto matrixType = RankedTensorType::get( {2, 2}, ComplexType::get(Float64Type::get(context.get()))); @@ -595,6 +677,8 @@ enum class ForbiddenModifierBodyOp : std::uint8_t { Dealloc, Measure, Reset, + QubitRegisterAlloc, + QubitRegisterDealloc, QubitRegisterLoad, QubitRegisterStore, CBitAlloc, @@ -626,6 +710,10 @@ static StringRef forbiddenOperationName(ForbiddenModifierBodyOp kind) { return "measure"; case ForbiddenModifierBodyOp::Reset: return "reset"; + case ForbiddenModifierBodyOp::QubitRegisterAlloc: + return "qubit-register-alloc"; + case ForbiddenModifierBodyOp::QubitRegisterDealloc: + return "qubit-register-dealloc"; case ForbiddenModifierBodyOp::QubitRegisterLoad: return "qubit-register-load"; case ForbiddenModifierBodyOp::QubitRegisterStore: @@ -658,6 +746,13 @@ static void emitForbiddenModifierBodyOperation(QCProgramBuilder& builder, case ForbiddenModifierBodyOp::Reset: ResetOp::create(builder, argument); return; + case ForbiddenModifierBodyOp::QubitRegisterAlloc: + memref::AllocOp::create( + builder, MemRefType::get({1}, QubitType::get(builder.getContext()))); + return; + case ForbiddenModifierBodyOp::QubitRegisterDealloc: + memref::DeallocOp::create(builder, qubitReg); + return; case ForbiddenModifierBodyOp::QubitRegisterLoad: memref::LoadOp::create(builder, qubitReg, index); return; @@ -681,7 +776,7 @@ static void emitForbiddenModifierBodyOperation(QCProgramBuilder& builder, static OwningOpRef buildInvalidNestedModifierProgram(MLIRContext* context, - const VerifierModifierKind modifier, + VerifierModifierKind modifier, ForbiddenModifierBodyOp forbiddenOperation) { QCProgramBuilder builder(context); builder.initialize(); @@ -722,6 +817,8 @@ TEST_F(QCTest, ModifiersRecursivelyRejectEveryForbiddenOperation) { ForbiddenModifierBodyOp::Dealloc, ForbiddenModifierBodyOp::Measure, ForbiddenModifierBodyOp::Reset, + ForbiddenModifierBodyOp::QubitRegisterAlloc, + ForbiddenModifierBodyOp::QubitRegisterDealloc, ForbiddenModifierBodyOp::QubitRegisterLoad, ForbiddenModifierBodyOp::QubitRegisterStore, ForbiddenModifierBodyOp::CBitAlloc, @@ -741,10 +838,20 @@ TEST_F(QCTest, ModifiersRecursivelyRejectEveryForbiddenOperation) { bool sawExpectedDiagnostic = false; ScopedDiagnosticHandler handler( context.get(), [&](Diagnostic& diagnostic) { + const bool capturesQubitRegister = + forbiddenOperation == + ForbiddenModifierBodyOp::QubitRegisterDealloc || + forbiddenOperation == + ForbiddenModifierBodyOp::QubitRegisterLoad || + forbiddenOperation == + ForbiddenModifierBodyOp::QubitRegisterStore; + const StringRef expected = + capturesQubitRegister + ? "body must not capture qubits from above" + : "body must not contain non-unitary operations or access " + "registers"; sawExpectedDiagnostic |= - StringRef(diagnostic.str()) - .contains("body must not contain non-unitary operations or " - "access registers"); + StringRef(diagnostic.str()).contains(expected); return success(); }); EXPECT_TRUE(failed(verify(*moduleOp))); @@ -755,8 +862,7 @@ TEST_F(QCTest, ModifiersRecursivelyRejectEveryForbiddenOperation) { static OwningOpRef buildInvalidModifierCaptureProgram(MLIRContext* context, - const VerifierModifierKind modifier, - const bool nested) { + VerifierModifierKind modifier, bool nested) { QCProgramBuilder builder(context); builder.initialize(); auto target = builder.allocQubit(); @@ -813,6 +919,238 @@ TEST_F(QCTest, ModifiersRejectDirectAndNestedQubitCaptures) { } } +TEST_F(QCTest, ModifiersRejectCapturedQubitRegisters) { + constexpr std::array modifiers{VerifierModifierKind::Inv, + VerifierModifierKind::Ctrl, + VerifierModifierKind::Pow}; + for (const auto modifier : modifiers) { + SCOPED_TRACE(testing::Message() + << "modifier=" << modifierName(modifier).str()); + QCProgramBuilder builder(context.get()); + builder.initialize(); + auto captured = builder.allocQubitRegisterStorage(1); + auto target = builder.allocQubit(); + auto control = builder.allocQubit(); + const auto body = [&](Value) { + memref::DeallocOp::create(builder, captured); + }; + Operation* modifierOp = nullptr; + switch (modifier) { + case VerifierModifierKind::Inv: + modifierOp = InvOp::create(builder, target, body).getOperation(); + break; + case VerifierModifierKind::Ctrl: + modifierOp = + CtrlOp::create(builder, control, target, body).getOperation(); + break; + case VerifierModifierKind::Pow: + modifierOp = PowOp::create(builder, 2.0, target, body).getOperation(); + break; + } + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + sawExpectedDiagnostic |= + StringRef(diagnostic.str()) + .contains("body must not capture qubits from above; use " + "only its aliased block arguments"); + return success(); + }); + EXPECT_TRUE(failed(verify(modifierOp))); + EXPECT_TRUE(sawExpectedDiagnostic); + } +} + +TEST_F(QCTest, ModifierCanonicalizersPreserveClassicalCalls) { + auto module = parseSourceString(R"mlir( + module { + func.func private @observe() + + func.func @empty_inv() { + %q = qc.alloc : !qc.qubit + qc.inv (%arg = %q) { + func.call @observe() : () -> () + qc.yield + } : !qc.qubit + qc.dealloc %q : !qc.qubit + return + } + + func.func @empty_ctrl() { + %control = qc.alloc : !qc.qubit + %target = qc.alloc : !qc.qubit + qc.ctrl(%control) targets(%arg = %target) { + func.call @observe() : () -> () + qc.yield + } : {!qc.qubit}, {!qc.qubit} + qc.dealloc %control : !qc.qubit + qc.dealloc %target : !qc.qubit + return + } + + func.func @empty_pow() { + %q = qc.alloc : !qc.qubit + %two = arith.constant 2.0 : f64 + qc.pow(%two) (%arg = %q) { + func.call @observe() : () -> () + qc.yield + } : !qc.qubit + qc.dealloc %q : !qc.qubit + return + } + + func.func @zero_pow() { + %q = qc.alloc : !qc.qubit + %zero = arith.constant 0.0 : f64 + qc.pow(%zero) (%arg = %q) { + func.call @observe() : () -> () + qc.x %arg : !qc.qubit + qc.yield + } : !qc.qubit + qc.dealloc %q : !qc.qubit + return + } + + func.func @move_inv_ctrl() { + %control = qc.alloc : !qc.qubit + %target = qc.alloc : !qc.qubit + qc.inv (%outer_control = %control, %outer_target = %target) { + func.call @observe() : () -> () + qc.ctrl(%outer_control) targets(%inner_target = %outer_target) { + qc.x %inner_target : !qc.qubit + qc.yield + } : {!qc.qubit}, {!qc.qubit} + qc.yield + } : !qc.qubit, !qc.qubit + qc.dealloc %control : !qc.qubit + qc.dealloc %target : !qc.qubit + return + } + + func.func @nested_unitary(%condition: i1) { + %q = qc.alloc : !qc.qubit + qc.inv (%arg = %q) { + scf.if %condition { + qc.x %arg : !qc.qubit + } + qc.yield + } : !qc.qubit + qc.dealloc %q : !qc.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + PassManager manager(context.get()); + manager.addPass(createCanonicalizerPass()); + ASSERT_TRUE(succeeded(manager.run(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + + size_t calls = 0; + module->walk([&](func::CallOp) { ++calls; }); + EXPECT_EQ(calls, 5U); + + auto nestedUnitary = module->lookupSymbol("nested_unitary"); + ASSERT_TRUE(nestedUnitary); + EXPECT_EQ(range_size(nestedUnitary.getOps()), 1U); + size_t nestedXOps = 0; + nestedUnitary.walk([&](XOp) { ++nestedXOps; }); + EXPECT_EQ(nestedXOps, 1U); +} + +static Operation* createEmptyModifier(QCProgramBuilder& builder, + VerifierModifierKind modifier, + Value control, Value target) { + switch (modifier) { + case VerifierModifierKind::Inv: + return InvOp::create(builder, target, [](Value) {}).getOperation(); + case VerifierModifierKind::Ctrl: + return CtrlOp::create(builder, control, target, [](Value) {}) + .getOperation(); + case VerifierModifierKind::Pow: + return PowOp::create(builder, 2.0, target, [](Value) {}).getOperation(); + } + llvm_unreachable("unknown modifier"); +} + +TEST_F(QCTest, ModifiersRejectMismatchedBodyArguments) { + constexpr std::array modifiers{VerifierModifierKind::Inv, + VerifierModifierKind::Ctrl, + VerifierModifierKind::Pow}; + for (const auto modifier : modifiers) { + for (const bool wrongType : {false, true}) { + SCOPED_TRACE(testing::Message() + << "modifier=" << modifierName(modifier).str() + << ", wrongType=" << wrongType); + QCProgramBuilder builder(context.get()); + builder.initialize(); + auto target = builder.allocQubit(); + auto control = builder.allocQubit(); + Operation* modifierOp = + createEmptyModifier(builder, modifier, control, target); + Block& body = modifierOp->getRegion(0).front(); + if (wrongType) { + body.getArgument(0).setType(builder.getI1Type()); + } else { + body.addArgument(QubitType::get(context.get()), builder.getLoc()); + } + + ScopedDiagnosticHandler handler(context.get(), + [](Diagnostic&) { return success(); }); + EXPECT_TRUE(failed(verify(modifierOp))); + } + } +} + +TEST_F(QCTest, ModifiersRejectDuplicateTargets) { + constexpr std::array modifiers{VerifierModifierKind::Inv, + VerifierModifierKind::Ctrl, + VerifierModifierKind::Pow}; + for (const auto modifier : modifiers) { + SCOPED_TRACE(testing::Message() + << "modifier=" << modifierName(modifier).str()); + QCProgramBuilder builder(context.get()); + builder.initialize(); + auto target = builder.allocQubit(); + auto control = builder.allocQubit(); + const SmallVector targets{target, target}; + Operation* modifierOp = nullptr; + switch (modifier) { + case VerifierModifierKind::Inv: + modifierOp = + InvOp::create(builder, targets, [](ValueRange) {}).getOperation(); + break; + case VerifierModifierKind::Ctrl: + modifierOp = + CtrlOp::create(builder, ValueRange{control}, targets, [](ValueRange) { + }).getOperation(); + break; + case VerifierModifierKind::Pow: + modifierOp = PowOp::create(builder, 2.0, targets, [](ValueRange) { + }).getOperation(); + break; + } + + ScopedDiagnosticHandler handler(context.get(), + [](Diagnostic&) { return success(); }); + EXPECT_TRUE(failed(verify(modifierOp))); + } +} + +TEST_F(QCTest, YieldRejectsNonModifierParent) { + QCProgramBuilder builder(context.get()); + builder.initialize(); + auto yield = YieldOp::create(builder); + + ScopedDiagnosticHandler handler(context.get(), + [](Diagnostic&) { return success(); }); + EXPECT_TRUE(failed(verify(yield))); + yield.erase(); +} + /// \name QC/Modifiers/CtrlOp.cpp /// @{ INSTANTIATE_TEST_SUITE_P( @@ -903,6 +1241,112 @@ TEST_F(QCTest, PowExponentIsUnitaryParameter) { EXPECT_EQ(unitary.getParameters().front(), powOp.getExponent()); } +TEST_F(QCTest, OverflowingFinitePowFoldsLeaveVerifiedModifiers) { + const auto build = [&](bool nested) { + return QCProgramBuilder::build(context.get(), [&](auto& builder) { + auto qubits = builder.allocQubitRegister(1); + if (nested) { + builder.pow(2.0, qubits[0], [&](Value outer) { + builder.pow(std::numeric_limits::max(), outer, + [&](Value inner) { builder.x(inner); }); + }); + } else { + builder.pow(std::numeric_limits::max(), qubits[0], + [&](Value target) { builder.x(target); }); + } + return builder.measure(qubits[0]); + }); + }; + + for (const bool nested : {false, true}) { + auto program = build(nested); + ASSERT_TRUE(program); + ASSERT_TRUE(succeeded(verify(*program))); + ASSERT_TRUE(succeeded(runQCCleanupPipeline(program.get()))); + ASSERT_TRUE(succeeded(verify(*program))); + size_t powCount = 0; + program->walk([&](PowOp) { ++powCount; }); + EXPECT_EQ(powCount, nested ? 2U : 1U); + } + + auto foldedParameter = + QCProgramBuilder::build(context.get(), [](auto& builder) { + auto qubits = builder.allocQubitRegister(1); + builder.pow(2.0, qubits[0], [&](Value target) { + auto half = arith::ConstantOp::create( + builder, builder.getUnknownLoc(), + builder.getF64FloatAttr(std::numeric_limits::max() / + 2.0)); + auto theta = + arith::AddFOp::create(builder, builder.getUnknownLoc(), + half.getResult(), half.getResult()); + builder.rx(theta.getResult(), target); + }); + return builder.measure(qubits[0]); + }); + ASSERT_TRUE(foldedParameter); + ASSERT_TRUE(succeeded(verify(*foldedParameter))); + ASSERT_TRUE(succeeded(runQCCleanupPipeline(foldedParameter.get()))); + ASSERT_TRUE(succeeded(verify(*foldedParameter))); + size_t foldedPowCount = 0; + foldedParameter->walk([&](PowOp) { ++foldedPowCount; }); + EXPECT_EQ(foldedPowCount, 1U); +} + +TEST_F(QCTest, DynamicPowScalingDoesNotIntroduceRuntimeOverflow) { + auto program = parseSourceString(R"mlir( + module { + func.func @dynamic_parameter(%theta: f64) { + %two = arith.constant 2.0 : f64 + %q = qc.alloc : !qc.qubit + qc.pow(%two) (%arg = %q) { + qc.rx(%theta) %arg : !qc.qubit + qc.yield + } : !qc.qubit + qc.dealloc %q : !qc.qubit + return + } + func.func @dynamic_nested(%inner_exponent: f64) { + %two = arith.constant 2.0 : f64 + %q = qc.alloc : !qc.qubit + qc.pow(%two) (%outer = %q) { + qc.pow(%inner_exponent) (%inner = %outer) { + qc.x %inner : !qc.qubit + qc.yield + } : !qc.qubit + qc.yield + } : !qc.qubit + qc.dealloc %q : !qc.qubit + return + } + func.func @foldable_nested() { + %two = arith.constant 2.0 : f64 + %q = qc.alloc : !qc.qubit + qc.pow(%two) (%outer = %q) { + %half = arith.constant 8.988465674311579E+307 : f64 + %inner_exponent = arith.addf %half, %half : f64 + qc.pow(%inner_exponent) (%inner = %outer) { + qc.x %inner : !qc.qubit + qc.yield + } : !qc.qubit + qc.yield + } : !qc.qubit + qc.dealloc %q : !qc.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(program); + ASSERT_TRUE(succeeded(verify(*program))); + ASSERT_TRUE(succeeded(runQCCleanupPipeline(program.get()))); + ASSERT_TRUE(succeeded(verify(*program))); + + size_t powCount = 0; + program->walk([&](PowOp) { ++powCount; }); + EXPECT_EQ(powCount, 5U); +} + TEST_F(QCTest, PositiveIntegralPowUCanonicalizes) { for (const double exponent : {2.0, 3.0, 17.0}) { auto program = QCProgramBuilder::build(context.get(), [&](auto& builder) { diff --git a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp index 2609aa9deb..4daaca5a6e 100644 --- a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp +++ b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp @@ -27,12 +27,15 @@ #include #include #include +#include #include #include +#include #include #include #include +#include #include #include @@ -645,6 +648,95 @@ TEST(OpenQASM3EmissionTest, LeavesDestinationEmptyOnFailure) { EXPECT_TRUE(output.empty()); } +TEST(OpenQASM3EmissionTest, RejectsExcessiveExpressionNesting) { + DialectRegistry registry = emissionDialects(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + OpBuilder builder(&context); + const auto location = builder.getUnknownLoc(); + auto moduleOp = ModuleOp::create(location); + auto function = + func::FuncOp::create(builder, location, "main", + builder.getFunctionType({}, {builder.getI64Type()})); + moduleOp.push_back(function); + Block* body = function.addEntryBlock(); + builder.setInsertionPointToStart(body); + Value value = arith::ConstantOp::create(builder, location, + builder.getI64IntegerAttr(1)); + for (size_t i = 0; i < 256; ++i) { + value = arith::AddIOp::create(builder, location, value, value); + } + func::ReturnOp::create(builder, location, value); + ASSERT_TRUE(succeeded(verify(moduleOp))); + + EXPECT_TRUE(failed(qc::translateQCToOpenQASM3(moduleOp))); +} + +TEST(OpenQASM3EmissionTest, RejectsExcessiveExpressionExpansion) { + DialectRegistry registry = emissionDialects(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + OpBuilder builder(&context); + const auto location = builder.getUnknownLoc(); + auto moduleOp = ModuleOp::create(location); + auto function = + func::FuncOp::create(builder, location, "main", + builder.getFunctionType({}, {builder.getI64Type()})); + moduleOp.push_back(function); + Block* body = function.addEntryBlock(); + builder.setInsertionPointToStart(body); + Value value = arith::ConstantOp::create(builder, location, + builder.getI64IntegerAttr(1)); + for (size_t i = 0; i < 16; ++i) { + value = arith::AddIOp::create(builder, location, value, value); + } + func::ReturnOp::create(builder, location, value); + ASSERT_TRUE(succeeded(verify(moduleOp))); + + EXPECT_TRUE(failed(qc::translateQCToOpenQASM3(moduleOp))); +} + +TEST(OpenQASM3EmissionTest, RejectsExcessiveClassicalRegisterWidth) { + DialectRegistry registry = emissionDialects(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + constexpr StringLiteral source = R"mlir(module { + func.func @main() { + %bits = cbit.alloc(#cbit.init) : !cbit.reg<1073741824> + return + } + })mlir"; + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + EXPECT_TRUE(failed(qc::translateQCToOpenQASM3(*moduleOp))); +} + +TEST(OpenQASM3EmissionTest, RejectsExcessiveRegionNesting) { + std::string source = "module { func.func @main() { "; + for (size_t i = 0; i < 64; ++i) { + source += "scf.execute_region { "; + } + source += "scf.yield "; + for (size_t i = 0; i < 64; ++i) { + source += "} "; + if (i + 1 < 64) { + source += "scf.yield "; + } + } + source += "return } }"; + + DialectRegistry registry = emissionDialects(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + EXPECT_TRUE(failed(qc::translateQCToOpenQASM3(*moduleOp))); +} + TEST(OpenQASM3EmissionTest, RejectsInvalidModifierBodies) { DialectRegistry registry = emissionDialects(); MLIRContext context(registry); diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp index 2ef1cf24e3..afb08815dc 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp @@ -54,6 +54,7 @@ #include #include #include +#include #include #include #include @@ -346,6 +347,257 @@ TEST_F(QCOTest, DirectSingleQubitPowBuilder) { EXPECT_TRUE(pow.verify().succeeded()); } +TEST_F(QCOTest, BarrierRejectsMismatchedInputAndOutputArity) { + EXPECT_FALSE(parseSourceString(R"mlir( + module { + func.func @main() { + %qubit = qco.alloc : !qco.qubit + "qco.barrier"(%qubit) : (!qco.qubit) -> () + return + } + } + )mlir", + context.get())); +} + +TEST_F(QCOTest, BarrierCanonicalizationHandlesUnusedOutput) { + auto module = parseSourceString(R"mlir( + module { + func.func @main() { + %qubit = qco.static 0 : !qco.qubit + %unused = qco.barrier %qubit : !qco.qubit -> !qco.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + PassManager manager(context.get()); + manager.addPass(createCanonicalizerPass()); + EXPECT_TRUE(succeeded(manager.run(*module))); + EXPECT_TRUE(succeeded(verify(*module))); + auto function = *module->getOps().begin(); + EXPECT_EQ(range_size(function.getOps()), 0U); +} + +TEST_F(QCOTest, BarrierCanonicalizationPreservesPartialOverlap) { + auto module = parseSourceString(R"mlir( + module { + func.func @main() { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %q2 = qco.static 2 : !qco.qubit + %q3, %q4 = qco.barrier %q0, %q1 + : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit + %q5, %q6 = qco.barrier %q3, %q2 + : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit + qco.sink %q5 : !qco.qubit + qco.sink %q4 : !qco.qubit + qco.sink %q6 : !qco.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + PassManager manager(context.get()); + manager.addPass(createCanonicalizerPass()); + ASSERT_TRUE(succeeded(manager.run(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + + auto function = *module->getOps().begin(); + SmallVector barriers; + function.walk([&](BarrierOp barrier) { barriers.push_back(barrier); }); + ASSERT_EQ(barriers.size(), 2U); + EXPECT_EQ(barriers[1].getQubitsIn()[0], barriers[0].getQubitsOut()[0]); +} + +TEST_F(QCOTest, BarrierCanonicalizationMergesIdenticalSuccessors) { + auto module = parseSourceString(R"mlir( + module { + func.func @main() { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %q2, %q3 = qco.barrier %q0, %q1 + : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit + %q4, %q5 = qco.barrier %q2, %q3 + : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit + qco.sink %q4 : !qco.qubit + qco.sink %q5 : !qco.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + PassManager manager(context.get()); + manager.addPass(createCanonicalizerPass()); + ASSERT_TRUE(succeeded(manager.run(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + + auto function = *module->getOps().begin(); + auto statics = llvm::to_vector(function.getOps()); + auto barriers = llvm::to_vector(function.getOps()); + ASSERT_EQ(statics.size(), 2U); + ASSERT_EQ(barriers.size(), 1U); + EXPECT_EQ(barriers[0].getQubitsIn()[0], statics[0].getQubit()); + EXPECT_EQ(barriers[0].getQubitsIn()[1], statics[1].getQubit()); +} + +TEST_F(QCOTest, GateCanonicalizationHandlesUnusedOutput) { + auto module = parseSourceString(R"mlir( + module { + func.func @main() { + %qubit = qco.static 0 : !qco.qubit + %theta = arith.constant 0.25 : f64 + %unused = qco.rz(%theta) %qubit : !qco.qubit -> !qco.qubit + + %r_qubit = qco.static 1 : !qco.qubit + %phi = arith.constant 0.25 : f64 + %unused_r = qco.r(%theta, %phi) %r_qubit + : !qco.qubit -> !qco.qubit + + %rxx_q0 = qco.static 2 : !qco.qubit + %rxx_q1 = qco.static 3 : !qco.qubit + %unused_rxx0, %unused_rxx1 = qco.rxx(%theta) %rxx_q0, %rxx_q1 + : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit + + %rccx_q0 = qco.static 4 : !qco.qubit + %rccx_q1 = qco.static 5 : !qco.qubit + %rccx_q2 = qco.static 6 : !qco.qubit + %unused_rccx0, %unused_rccx1, %unused_rccx2 = + qco.rccx %rccx_q0, %rccx_q1, %rccx_q2 + : !qco.qubit, !qco.qubit, !qco.qubit + -> !qco.qubit, !qco.qubit, !qco.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + PassManager manager(context.get()); + manager.addPass(createCanonicalizerPass()); + EXPECT_TRUE(succeeded(manager.run(*module))); + EXPECT_TRUE(succeeded(verify(*module))); + auto function = *module->getOps().begin(); + EXPECT_EQ(range_size(function.getOps()), 0U); + EXPECT_EQ(range_size(function.getOps()), 0U); + EXPECT_EQ(range_size(function.getOps()), 0U); + EXPECT_EQ(range_size(function.getOps()), 0U); +} + +TEST_F(QCOTest, CtrlRejectsMismatchedInputAndOutputArity) { + for (const bool mismatchControls : {false, true}) { + SCOPED_TRACE(testing::Message() << "mismatchControls=" << mismatchControls); + QCOProgramBuilder builder(context.get()); + builder.initialize(); + auto control = builder.allocQubit(); + auto target = builder.allocQubit(); + const SmallVector controlsOut = + mismatchControls ? SmallVector{} + : SmallVector{control.getType()}; + const SmallVector targetsOut = + mismatchControls ? SmallVector{target.getType()} + : SmallVector{}; + OperationState state(builder.getLoc(), CtrlOp::getOperationName()); + CtrlOp::build(builder, state, controlsOut, targetsOut, ValueRange{control}, + ValueRange{target}); + Block& body = state.regions.front()->emplaceBlock(); + body.addArgument(QubitType::get(context.get()), builder.getLoc()); + { + const OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPointToStart(&body); + YieldOp::create(builder, body.getArgument(0)); + } + auto ctrl = cast(builder.create(state)); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + const StringRef expected = + mismatchControls + ? "number of input controls must match the number of output " + "controls" + : "number of input targets must match the number of output " + "targets"; + sawExpectedDiagnostic |= StringRef(diagnostic.str()).contains(expected); + return success(); + }); + EXPECT_TRUE(failed(ctrl.verify())); + EXPECT_TRUE(sawExpectedDiagnostic); + } +} + +TEST_F(QCOTest, InvAndPowRejectMismatchedInputAndOutputArity) { + for (const bool isPower : {false, true}) { + SCOPED_TRACE(testing::Message() << "isPower=" << isPower); + QCOProgramBuilder builder(context.get()); + builder.initialize(); + auto target = builder.allocQubit(); + OperationState state(builder.getLoc(), isPower ? PowOp::getOperationName() + : InvOp::getOperationName()); + if (isPower) { + auto exponent = builder.floatConstant(2.0); + PowOp::build(builder, state, TypeRange{}, exponent, ValueRange{target}); + } else { + InvOp::build(builder, state, TypeRange{}, ValueRange{target}); + } + Block& body = state.regions.front()->emplaceBlock(); + body.addArgument(QubitType::get(context.get()), builder.getLoc()); + { + const OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPointToStart(&body); + YieldOp::create(builder, body.getArgument(0)); + } + auto* modifier = builder.create(state); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + sawExpectedDiagnostic |= + StringRef(diagnostic.str()) + .contains("number of input qubits must match the number of " + "output qubits"); + return success(); + }); + EXPECT_TRUE(failed(verify(modifier))); + EXPECT_TRUE(sawExpectedDiagnostic); + } +} + +TEST_F(QCOTest, InvAndPowRejectDuplicateYieldedQubits) { + for (const bool isPower : {false, true}) { + SCOPED_TRACE(testing::Message() << "isPower=" << isPower); + QCOProgramBuilder builder(context.get()); + builder.initialize(); + auto qubits = builder.allocQubitRegister(2); + Operation* modifier = nullptr; + const auto duplicateYield = [](ValueRange args) { + return SmallVector{args[0], args[0]}; + }; + if (isPower) { + modifier = PowOp::create(builder, qubits.qubits, 2.0, duplicateYield); + } else { + modifier = InvOp::create(builder, qubits.qubits, duplicateYield); + } + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + sawExpectedDiagnostic |= + StringRef(diagnostic.str()).contains("duplicate yielded qubit"); + return success(); + }); + EXPECT_TRUE(failed(verify(modifier))); + EXPECT_TRUE(sawExpectedDiagnostic); + } +} + TEST_F(QCOTest, UnitaryVerifierRejectsNonFiniteConstantParameters) { constexpr std::array invalidPrograms{ R"mlir( @@ -388,11 +640,70 @@ TEST_F(QCOTest, UnitaryVerifierRejectsNonFiniteConstantParameters) { } } +TEST_F(QCOTest, GlobalPhaseVerifierRejectsDirectAndFoldedNonFiniteAngles) { + constexpr std::array invalidPrograms{ + R"mlir( + module { + func.func @main() { + %infinity = arith.constant 0x7FF0000000000000 : f64 + qco.gphase(%infinity) + return + } + } + )mlir", + R"mlir( + module { + func.func @main() { + %nan = arith.constant 0x7FF8000000000000 : f64 + qco.gphase(%nan) + return + } + } + )mlir", + R"mlir( + module { + func.func @main() { + %max = arith.constant 1.7976931348623157E+308 : f64 + %infinity = arith.addf %max, %max : f64 + qco.gphase(%infinity) + return + } + } + )mlir", + R"mlir( + module { + func.func @main() { + %zero = arith.constant 0.0 : f64 + %nan = arith.divf %zero, %zero : f64 + qco.gphase(%nan) + return + } + } + )mlir"}; + + for (const auto source : invalidPrograms) { + bool sawExpectedDiagnostic = false; + std::string diagnostics; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + sawExpectedDiagnostic |= + StringRef(diagnostic.str()) + .contains( + "constant parameter expression at index 0 must be finite"); + return success(); + }); + EXPECT_FALSE(parseSourceString(source, context.get())); + EXPECT_TRUE(sawExpectedDiagnostic) << diagnostics; + } +} + namespace { enum class VerifierModifierKind : uint8_t { Inv, Ctrl, Pow }; enum class ForbiddenModifierBodyOp : uint8_t { Measure, + QTensorAlloc, + QTensorFromElements, CBitAlloc, CBitLoad, CBitStore @@ -416,6 +727,10 @@ static StringRef forbiddenOperationName(ForbiddenModifierBodyOp kind) { switch (kind) { case ForbiddenModifierBodyOp::Measure: return "measure"; + case ForbiddenModifierBodyOp::QTensorAlloc: + return "qtensor.alloc"; + case ForbiddenModifierBodyOp::QTensorFromElements: + return "qtensor.from_elements"; case ForbiddenModifierBodyOp::CBitAlloc: return "cbit.alloc"; case ForbiddenModifierBodyOp::CBitLoad: @@ -469,12 +784,20 @@ buildInvalidNestedModifierBody(QCOProgramBuilder& builder, auto condition = builder.boolConstant(true); auto cbitReg = builder.allocClassicalBitRegister(1); auto index = arith::ConstantIndexOp::create(builder, 0); + auto one = arith::ConstantIndexOp::create(builder, 1); const auto modifierBody = [&](Value argument) -> Value { auto ifOp = IfOp::create( builder, condition, argument, [&](Value nestedArgument) -> Value { switch (forbiddenOperation) { case ForbiddenModifierBodyOp::Measure: return MeasureOp::create(builder, nestedArgument).getQubitOut(); + case ForbiddenModifierBodyOp::QTensorAlloc: + qtensor::AllocOp::create(builder, one); + break; + case ForbiddenModifierBodyOp::QTensorFromElements: + qtensor::FromElementsOp::create(builder, + ValueRange{nestedArgument}); + break; case ForbiddenModifierBodyOp::CBitAlloc: cbit::AllocOp::create( builder, cbit::RegisterType::get(builder.getContext(), 1), @@ -511,8 +834,12 @@ TEST_F(QCOTest, ModifiersRecursivelyRejectNonUnitaryOperations) { VerifierModifierKind::Ctrl, VerifierModifierKind::Pow}; constexpr std::array forbiddenOperations{ - ForbiddenModifierBodyOp::Measure, ForbiddenModifierBodyOp::CBitAlloc, - ForbiddenModifierBodyOp::CBitLoad, ForbiddenModifierBodyOp::CBitStore}; + ForbiddenModifierBodyOp::Measure, + ForbiddenModifierBodyOp::QTensorAlloc, + ForbiddenModifierBodyOp::QTensorFromElements, + ForbiddenModifierBodyOp::CBitAlloc, + ForbiddenModifierBodyOp::CBitLoad, + ForbiddenModifierBodyOp::CBitStore}; for (const auto modifier : modifiers) { for (const auto forbiddenOperation : forbiddenOperations) { @@ -567,6 +894,160 @@ TEST_F(QCOTest, ModifiersRejectDirectAndNestedQubitCaptures) { } } +TEST_F(QCOTest, ModifiersRejectCapturedQubitTensors) { + constexpr std::array modifiers{VerifierModifierKind::Inv, + VerifierModifierKind::Ctrl, + VerifierModifierKind::Pow}; + for (const auto modifier : modifiers) { + SCOPED_TRACE(testing::Message() + << "modifier=" << modifierName(modifier).str()); + QCOProgramBuilder builder(context.get()); + builder.initialize(); + auto one = arith::ConstantIndexOp::create(builder, 1); + auto captured = qtensor::AllocOp::create(builder, one); + auto target = builder.allocQubit(); + auto control = builder.allocQubit(); + const auto body = [&](Value argument) { + qtensor::DeallocOp::create(builder, captured); + return argument; + }; + Operation* modifierOp = nullptr; + switch (modifier) { + case VerifierModifierKind::Inv: + modifierOp = InvOp::create(builder, target, body).getOperation(); + break; + case VerifierModifierKind::Ctrl: + modifierOp = + CtrlOp::create(builder, control, target, body).getOperation(); + break; + case VerifierModifierKind::Pow: + modifierOp = PowOp::create(builder, target, 2.0, body).getOperation(); + break; + } + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + sawExpectedDiagnostic |= + StringRef(diagnostic.str()) + .contains("body must not capture qubits from above; use " + "only its aliased block arguments"); + return success(); + }); + EXPECT_TRUE(failed(verify(modifierOp))); + EXPECT_TRUE(sawExpectedDiagnostic); + } +} + +TEST_F(QCOTest, ModifierCanonicalizersPreserveClassicalCalls) { + auto module = parseSourceString(R"mlir( + module { + func.func private @observe() + + func.func @empty_inv() { + %q = qco.alloc : !qco.qubit + %out = qco.inv (%arg = %q) { + func.call @observe() : () -> () + qco.yield %arg : !qco.qubit + } : {!qco.qubit} -> {!qco.qubit} + qco.sink %out : !qco.qubit + return + } + + func.func @empty_ctrl() { + %control = qco.alloc : !qco.qubit + %target = qco.alloc : !qco.qubit + %control_out, %target_out = qco.ctrl(%control) + targets(%arg = %target) { + func.call @observe() : () -> () + qco.yield %arg : !qco.qubit + } : ({!qco.qubit}, {!qco.qubit}) + -> ({!qco.qubit}, {!qco.qubit}) + qco.sink %control_out : !qco.qubit + qco.sink %target_out : !qco.qubit + return + } + + func.func @empty_pow() { + %q = qco.alloc : !qco.qubit + %two = arith.constant 2.0 : f64 + %out = qco.pow(%two) (%arg = %q) { + func.call @observe() : () -> () + qco.yield %arg : !qco.qubit + } : {!qco.qubit} -> {!qco.qubit} + qco.sink %out : !qco.qubit + return + } + + func.func @zero_pow() { + %q = qco.alloc : !qco.qubit + %zero = arith.constant 0.0 : f64 + %out = qco.pow(%zero) (%arg = %q) { + func.call @observe() : () -> () + %body = qco.x %arg : !qco.qubit -> !qco.qubit + qco.yield %body : !qco.qubit + } : {!qco.qubit} -> {!qco.qubit} + qco.sink %out : !qco.qubit + return + } + + func.func @move_inv_ctrl() { + %control = qco.alloc : !qco.qubit + %target = qco.alloc : !qco.qubit + %control_out, %target_out = qco.inv + (%outer_control = %control, %outer_target = %target) { + func.call @observe() : () -> () + %inner_control_out, %inner_target_out = qco.ctrl(%outer_control) + targets(%inner_target = %outer_target) { + %body = qco.x %inner_target : !qco.qubit -> !qco.qubit + qco.yield %body : !qco.qubit + } : ({!qco.qubit}, {!qco.qubit}) + -> ({!qco.qubit}, {!qco.qubit}) + qco.yield %inner_control_out, %inner_target_out + : !qco.qubit, !qco.qubit + } : {!qco.qubit, !qco.qubit} -> {!qco.qubit, !qco.qubit} + qco.sink %control_out : !qco.qubit + qco.sink %target_out : !qco.qubit + return + } + + func.func @nested_unitary(%condition: i1) { + %q = qco.alloc : !qco.qubit + %out = qco.inv (%arg = %q) { + %branch = qco.if %condition args(%nested_arg = %arg) + -> (!qco.qubit) { + %nested = qco.x %nested_arg : !qco.qubit -> !qco.qubit + qco.yield %nested : !qco.qubit + } else args(%nested_arg = %arg) { + qco.yield %nested_arg : !qco.qubit + } + qco.yield %branch : !qco.qubit + } : {!qco.qubit} -> {!qco.qubit} + qco.sink %out : !qco.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + PassManager manager(context.get()); + manager.addPass(createCanonicalizerPass()); + ASSERT_TRUE(succeeded(manager.run(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + + size_t calls = 0; + module->walk([&](func::CallOp) { ++calls; }); + EXPECT_EQ(calls, 5U); + + auto nestedUnitary = module->lookupSymbol("nested_unitary"); + ASSERT_TRUE(nestedUnitary); + EXPECT_EQ(range_size(nestedUnitary.getOps()), 1U); + size_t nestedXOps = 0; + nestedUnitary.walk([&](XOp) { ++nestedXOps; }); + EXPECT_EQ(nestedXOps, 1U); +} + TEST_F(QCOTest, DirectIfBuilder) { QCOProgramBuilder builder(context.get()); auto cbitType = cbit::RegisterType::get(context.get(), 1); @@ -1643,6 +2124,212 @@ TEST_F(QCOTest, PowExponentIsUnitaryParameter) { EXPECT_EQ(unitary.getParameters().front(), powOp.getExponent()); } +TEST_F(QCOTest, OverflowingFinitePowFoldsLeaveVerifiedModifiers) { + const auto build = [&](bool nested) { + return QCOProgramBuilder::build(context.get(), [&](auto& builder) { + auto qubit = builder.allocQubit(); + if (nested) { + qubit = builder.pow(2.0, qubit, [&](Value outer) { + return builder.pow(std::numeric_limits::max(), outer, + [&](Value inner) { return builder.x(inner); }); + }); + } else { + qubit = builder.pow(std::numeric_limits::max(), qubit, + [&](Value target) { return builder.x(target); }); + } + return builder.measure(qubit).second; + }); + }; + + for (const bool nested : {false, true}) { + auto program = build(nested); + ASSERT_TRUE(program); + ASSERT_TRUE(succeeded(verify(*program))); + ASSERT_TRUE(succeeded(runQCOCleanupPipeline(program.get()))); + ASSERT_TRUE(succeeded(verify(*program))); + EXPECT_EQ(llvm::range_size(program->getOps()), 0U); + size_t powCount = 0; + program->walk([&](PowOp) { ++powCount; }); + EXPECT_EQ(powCount, nested ? 2U : 1U); + } + + auto foldedParameter = + QCOProgramBuilder::build(context.get(), [](auto& builder) { + auto qubit = builder.allocQubit(); + qubit = builder.pow(2.0, qubit, [&](Value target) { + auto half = + builder.floatConstant(std::numeric_limits::max() / 2.0); + auto theta = arith::AddFOp::create(builder, builder.getUnknownLoc(), + half, half); + return builder.rx(theta.getResult(), target); + }); + return builder.measure(qubit).second; + }); + ASSERT_TRUE(foldedParameter); + ASSERT_TRUE(succeeded(verify(*foldedParameter))); + ASSERT_TRUE(succeeded(runQCOCleanupPipeline(foldedParameter.get()))); + ASSERT_TRUE(succeeded(verify(*foldedParameter))); + EXPECT_EQ(llvm::range_size(foldedParameter->getOps()), 0U); + size_t foldedPowCount = 0; + foldedParameter->walk([&](PowOp) { ++foldedPowCount; }); + EXPECT_EQ(foldedPowCount, 1U); +} + +TEST_F(QCOTest, DynamicPowScalingDoesNotIntroduceRuntimeOverflow) { + auto program = parseSourceString(R"mlir( + module { + func.func @dynamic_parameter(%theta: f64) { + %two = arith.constant 2.0 : f64 + %q = qco.alloc : !qco.qubit + %out = qco.pow(%two) (%arg = %q) { + %rotated = qco.rx(%theta) %arg : !qco.qubit -> !qco.qubit + qco.yield %rotated : !qco.qubit + } : {!qco.qubit} -> {!qco.qubit} + qco.sink %out : !qco.qubit + return + } + func.func @dynamic_nested(%inner_exponent: f64) { + %two = arith.constant 2.0 : f64 + %q = qco.alloc : !qco.qubit + %out = qco.pow(%two) (%outer = %q) { + %inner_out = qco.pow(%inner_exponent) (%inner = %outer) { + %x = qco.x %inner : !qco.qubit -> !qco.qubit + qco.yield %x : !qco.qubit + } : {!qco.qubit} -> {!qco.qubit} + qco.yield %inner_out : !qco.qubit + } : {!qco.qubit} -> {!qco.qubit} + qco.sink %out : !qco.qubit + return + } + func.func @foldable_nested() { + %two = arith.constant 2.0 : f64 + %q = qco.alloc : !qco.qubit + %out = qco.pow(%two) (%outer = %q) { + %half = arith.constant 8.988465674311579E+307 : f64 + %inner_exponent = arith.addf %half, %half : f64 + %inner_out = qco.pow(%inner_exponent) (%inner = %outer) { + %x = qco.x %inner : !qco.qubit -> !qco.qubit + qco.yield %x : !qco.qubit + } : {!qco.qubit} -> {!qco.qubit} + qco.yield %inner_out : !qco.qubit + } : {!qco.qubit} -> {!qco.qubit} + qco.sink %out : !qco.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(program); + ASSERT_TRUE(succeeded(verify(*program))); + ASSERT_TRUE(succeeded(runQCOCleanupPipeline(program.get()))); + ASSERT_TRUE(succeeded(verify(*program))); + + size_t powCount = 0; + program->walk([&](PowOp) { ++powCount; }); + EXPECT_EQ(powCount, 5U); +} + +TEST_F(QCOTest, OverflowingFiniteGateMergesLeaveVerifiedChains) { + const auto check = [&](OwningOpRef program, StringRef name) { + SCOPED_TRACE(name.str()); + ASSERT_TRUE(program); + ASSERT_TRUE(succeeded(verify(*program))); + ASSERT_TRUE(succeeded(runQCOCleanupPipeline(program.get()))); + ASSERT_TRUE(succeeded(verify(*program))); + size_t count = 0; + program->walk([&](Operation* operation) { + count += operation->getName().getStringRef() == name; + }); + EXPECT_EQ(count, 2U); + }; + + check(QCOProgramBuilder::build( + context.get(), + [](auto& builder) { + auto qubit = builder.allocQubit(); + qubit = builder.rx(std::numeric_limits::max(), qubit); + qubit = builder.rx(std::numeric_limits::max(), qubit); + return builder.measure(qubit).second; + }), + RXOp::getOperationName()); + check(QCOProgramBuilder::build( + context.get(), + [](auto& builder) { + auto qubit = builder.allocQubit(); + qubit = + builder.r(std::numeric_limits::max(), 0.25, qubit); + qubit = + builder.r(std::numeric_limits::max(), 0.25, qubit); + return builder.measure(qubit).second; + }), + ROp::getOperationName()); + check(QCOProgramBuilder::build( + context.get(), + [](auto& builder) { + auto qubits = builder.allocQubitRegister(2); + std::tie(qubits[0], qubits[1]) = builder.rxx( + std::numeric_limits::max(), qubits[0], qubits[1]); + std::tie(qubits[0], qubits[1]) = builder.rxx( + std::numeric_limits::max(), qubits[0], qubits[1]); + return measureRegister(builder, qubits.qubits); + }), + RXXOp::getOperationName()); +} + +TEST_F(QCOTest, DynamicGateParametersMergeIntoVerifiedSums) { + auto program = parseSourceString(R"mlir( + module { + func.func @rx(%a: f64, %b: f64) { + %q0 = qco.alloc : !qco.qubit + %q1 = qco.rx(%a) %q0 : !qco.qubit -> !qco.qubit + %later = arith.mulf %b, %b : f64 + %q2 = qco.rx(%later) %q1 : !qco.qubit -> !qco.qubit + qco.sink %q2 : !qco.qubit + return + } + func.func @r(%a: f64, %b: f64) { + %phi = arith.constant 0.25 : f64 + %q0 = qco.alloc : !qco.qubit + %q1 = qco.r(%a, %phi) %q0 : !qco.qubit -> !qco.qubit + %later = arith.mulf %b, %b : f64 + %q2 = qco.r(%later, %phi) %q1 : !qco.qubit -> !qco.qubit + qco.sink %q2 : !qco.qubit + return + } + func.func @rxx(%a: f64, %b: f64) { + %q0 = qco.alloc : !qco.qubit + %q1 = qco.alloc : !qco.qubit + %q2, %q3 = qco.rxx(%a) %q0, %q1 : !qco.qubit, !qco.qubit + -> !qco.qubit, !qco.qubit + %later = arith.mulf %b, %b : f64 + %q4, %q5 = qco.rxx(%later) %q2, %q3 : !qco.qubit, !qco.qubit + -> !qco.qubit, !qco.qubit + qco.sink %q4 : !qco.qubit + qco.sink %q5 : !qco.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(program); + ASSERT_TRUE(succeeded(verify(*program))); + ASSERT_TRUE(succeeded(runQCOCleanupPipeline(program.get()))); + ASSERT_TRUE(succeeded(verify(*program))); + + size_t rxCount = 0; + size_t rCount = 0; + size_t rxxCount = 0; + program->walk([&](RXOp) { ++rxCount; }); + program->walk([&](ROp) { ++rCount; }); + program->walk([&](RXXOp) { ++rxxCount; }); + EXPECT_EQ(rxCount, 1U); + EXPECT_EQ(rCount, 1U); + EXPECT_EQ(rxxCount, 1U); + size_t addCount = 0; + program->walk([&](arith::AddFOp) { ++addCount; }); + EXPECT_EQ(addCount, 3U); +} + TEST_F(QCOTest, NestedPowAcrossBranchCutDoesNotMerge) { auto program = ::mqt::test::buildMLIRProgram( context.get(), MQT_NAMED_BUILDER(nestedPowBranchCut)); @@ -2875,6 +3562,33 @@ static LogicalResult runUnrollModifiers(ModuleOp moduleOp) { return pm.run(moduleOp); } +TEST_F(QCOTest, UnrollModifiersRejectsNonLinearBodyAtomically) { + auto module = parseSourceString(R"mlir( + module { + func.func @main() { + %qubit = qco.alloc : !qco.qubit + %out = qco.inv (%arg = %qubit) { + %first = qco.x %arg : !qco.qubit -> !qco.qubit + %unused = qco.h %first : !qco.qubit -> !qco.qubit + qco.yield %first : !qco.qubit + } : {!qco.qubit} -> {!qco.qubit} + qco.sink %out : !qco.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + std::string before; + llvm::raw_string_ostream(before) << *module; + + EXPECT_TRUE(failed(runUnrollModifiers(*module))); + std::string after; + llvm::raw_string_ostream(after) << *module; + EXPECT_EQ(after, before); +} + /// Unrolls @p program and checks that it matches @p reference. static void expectUnrollsTo(MLIRContext* context, diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp index 2fd4c3f762..ea09e7289e 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -100,6 +101,11 @@ expectedMatrixFromComputation(const Fn& build, const size_t numQubits = 2) { return *funcOp.getBody().getOps().begin(); } +[[nodiscard]] static BarrierOp firstBarrierOp(ModuleOp module) { + auto funcOp = cast(module.getBody()->front()); + return *funcOp.getBody().getOps().begin(); +} + static void makePowExponentDynamic(ModuleOp module) { auto funcOp = cast(module.getBody()->front()); funcOp.insertArgument(0, Float64Type::get(module.getContext()), {}, @@ -134,6 +140,30 @@ static Value composedBodyWithNestedPow(QCOProgramBuilder& b) { return b.measure(powOut).second; } +static Value buildAlternatingModifierNesting(QCOProgramBuilder& builder, + Value qubit, size_t depth) { + if (depth == 0) { + return builder.x(qubit); + } + if (depth % 3 == 0) { + return builder + .ctrl(ValueRange{}, qubit, + [&](Value argument) { + return buildAlternatingModifierNesting(builder, argument, + depth - 1); + }) + .second; + } + if (depth % 3 == 1) { + return builder.inv(qubit, [&](Value argument) { + return buildAlternatingModifierNesting(builder, argument, depth - 1); + }); + } + return builder.pow(1.0, qubit, [&](Value argument) { + return buildAlternatingModifierNesting(builder, argument, depth - 1); + }); +} + template static void assertCanonicalizedPowMatrixMatches(MLIRContext* context, Builder&& build) { @@ -199,7 +229,7 @@ class QCOMatrixTest : public testing::TestWithParam { void SetUp() override { DialectRegistry registry; registry.insert(); + memref::MemRefDialect, scf::SCFDialect>(); context = std::make_unique(); context->appendDialectRegistry(registry); context->loadAllAvailableDialects(); @@ -387,10 +417,285 @@ TEST_F(QCOMatrixTest, DenseUnitaryComposesThroughModifiers) { EXPECT_TRUE(poweredMatrix->isApprox( DynamicMatrix(SOp::getUnitaryMatrix().adjoint()))); } + +TEST_F(QCOMatrixTest, DeeplyNestedModifierMatrixQueriesFailSafely) { + constexpr size_t nestingDepth = 67; + auto module = QCOProgramBuilder::build( + context.get(), [&](QCOProgramBuilder& builder) -> Value { + auto qubit = buildAlternatingModifierNesting( + builder, builder.allocQubit(), nestingDepth); + return builder.measure(qubit).second; + }); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + auto inverse = firstInvOp(*module); + auto controls = inverse.getBody()->getOps(); + ASSERT_FALSE(controls.empty()); + auto control = *controls.begin(); + auto powers = control.getBody()->getOps(); + ASSERT_FALSE(powers.empty()); + auto power = *powers.begin(); + + EXPECT_FALSE(inverse.hasCompileTimeKnownUnitaryMatrix()); + EXPECT_FALSE(inverse.getUnitaryMatrix()); + EXPECT_FALSE(control.hasCompileTimeKnownUnitaryMatrix()); + EXPECT_FALSE(control.getUnitaryMatrix()); + EXPECT_FALSE(power.hasCompileTimeKnownUnitaryMatrix()); + EXPECT_FALSE(power.getUnitaryMatrix()); +} /// @} /// \name QCO/Modifiers/CtrlOp.cpp /// @{ +TEST_F(QCOMatrixTest, TooManyControlMatrixQueryFailsSafely) { + auto module = QCOProgramBuilder::build( + context.get(), [](QCOProgramBuilder& builder) -> Value { + SmallVector controls; + for (size_t i = 0; i < 32; ++i) { + controls.push_back(builder.allocQubit()); + } + auto target = builder.allocQubit(); + auto [controlsOut, targetOut] = + builder.ctrl(controls, target, + [&](Value argument) { return builder.x(argument); }); + for (Value control : controlsOut) { + builder.sink(control); + } + return builder.measure(targetOut).second; + }); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + auto control = firstCtrlOp(*module); + EXPECT_FALSE(control.hasCompileTimeKnownUnitaryMatrix()); + EXPECT_FALSE(control.getUnitaryMatrix()); +} + +TEST_F(QCOMatrixTest, ModifierMatricesIncludePassThroughTargets) { + auto inverse = + QCOProgramBuilder::build(context.get(), [](QCOProgramBuilder& builder) { + auto qubits = builder.allocQubitRegister(2); + auto outputs = builder.inv(qubits.qubits, [&](ValueRange args) { + return SmallVector{builder.x(args[0]), args[1]}; + }); + builder.sink(outputs[1]); + return builder.measure(outputs[0]).second; + }); + ASSERT_TRUE(inverse); + const auto inverseMatrix = firstInvOp(*inverse).getUnitaryMatrix(); + ASSERT_TRUE(inverseMatrix); + const DynamicMatrix xOnFirst = XOp::getUnitaryMatrix().embedInNqubit(2, 0); + EXPECT_TRUE(inverseMatrix->isApprox(xOnFirst)); + + auto powered = + QCOProgramBuilder::build(context.get(), [](QCOProgramBuilder& builder) { + auto qubits = builder.allocQubitRegister(2); + auto outputs = builder.pow(1.0, qubits.qubits, [&](ValueRange args) { + return SmallVector{builder.x(args[0]), args[1]}; + }); + builder.sink(outputs[1]); + return builder.measure(outputs[0]).second; + }); + ASSERT_TRUE(powered); + const auto poweredMatrix = firstPowOp(*powered).getUnitaryMatrix(); + ASSERT_TRUE(poweredMatrix); + EXPECT_TRUE(poweredMatrix->isApprox(xOnFirst)); + + auto controlled = + QCOProgramBuilder::build(context.get(), [](QCOProgramBuilder& builder) { + auto qubits = builder.allocQubitRegister(3); + auto [controls, targets] = builder.ctrl( + ValueRange{qubits[0]}, ValueRange{qubits[1], qubits[2]}, + [&](ValueRange args) { + return SmallVector{builder.x(args[0]), args[1]}; + }); + builder.sink(controls[0]); + builder.sink(targets[1]); + return builder.measure(targets[0]).second; + }); + ASSERT_TRUE(controlled); + const auto controlledMatrix = firstCtrlOp(*controlled).getUnitaryMatrix(); + ASSERT_TRUE(controlledMatrix); + DynamicMatrix expectedControlled = DynamicMatrix::identity(8); + expectedControlled.setBottomRightCorner(xOnFirst); + EXPECT_TRUE(controlledMatrix->isApprox(expectedControlled)); +} + +TEST_F(QCOMatrixTest, EmptyModifierMatricesRequireIdentityYieldMapping) { + auto emptyInverse = QCOProgramBuilder::build(context.get(), emptyInv); + ASSERT_TRUE(emptyInverse); + const auto identity = firstInvOp(*emptyInverse).getUnitaryMatrix(); + ASSERT_TRUE(identity); + EXPECT_TRUE(identity->isApprox(DynamicMatrix::identity(4))); + + const auto buildPermutation = [](QCOProgramBuilder& builder, + const auto& buildModifier) { + auto qubits = builder.allocQubitRegister(2); + auto outputs = buildModifier(builder, qubits.qubits); + builder.sink(outputs[1]); + return builder.measure(outputs[0]).second; + }; + auto inverse = + QCOProgramBuilder::build(context.get(), [&](QCOProgramBuilder& builder) { + return buildPermutation( + builder, [](QCOProgramBuilder& inner, ValueRange qubits) { + return inner.inv(qubits, [](ValueRange args) { + return SmallVector{args[1], args[0]}; + }); + }); + }); + ASSERT_TRUE(inverse); + EXPECT_FALSE(firstInvOp(*inverse).hasCompileTimeKnownUnitaryMatrix()); + EXPECT_FALSE(firstInvOp(*inverse).getUnitaryMatrix()); + + auto power = + QCOProgramBuilder::build(context.get(), [&](QCOProgramBuilder& builder) { + return buildPermutation( + builder, [](QCOProgramBuilder& inner, ValueRange qubits) { + return inner.pow(1.0, qubits, [](ValueRange args) { + return SmallVector{args[1], args[0]}; + }); + }); + }); + ASSERT_TRUE(power); + EXPECT_FALSE(firstPowOp(*power).hasCompileTimeKnownUnitaryMatrix()); + EXPECT_FALSE(firstPowOp(*power).getUnitaryMatrix()); + + auto controlled = + QCOProgramBuilder::build(context.get(), [](QCOProgramBuilder& builder) { + auto qubits = builder.allocQubitRegister(3); + auto [controls, targets] = + builder.ctrl(ValueRange{qubits[0]}, + ValueRange{qubits[1], qubits[2]}, [](ValueRange args) { + return SmallVector{args[1], args[0]}; + }); + builder.sink(controls[0]); + builder.sink(targets[1]); + return builder.measure(targets[0]).second; + }); + ASSERT_TRUE(controlled); + EXPECT_FALSE(firstCtrlOp(*controlled).hasCompileTimeKnownUnitaryMatrix()); + EXPECT_FALSE(firstCtrlOp(*controlled).getUnitaryMatrix()); +} + +TEST_F(QCOMatrixTest, ComposedWideBodyIsNotReportedAsKnown) { + const auto wideBody = [](QCOProgramBuilder& builder, ValueRange args) { + auto [first, second, third] = builder.rccx(args[0], args[1], args[2]); + first = builder.x(first); + return SmallVector{first, second, third}; + }; + auto inverse = + QCOProgramBuilder::build(context.get(), [&](QCOProgramBuilder& builder) { + auto qubits = builder.allocQubitRegister(3); + auto outputs = builder.inv(qubits.qubits, [&](ValueRange args) { + return wideBody(builder, args); + }); + builder.sink(outputs[1]); + builder.sink(outputs[2]); + return builder.measure(outputs[0]).second; + }); + ASSERT_TRUE(inverse); + EXPECT_FALSE(firstInvOp(*inverse).hasCompileTimeKnownUnitaryMatrix()); + EXPECT_FALSE(firstInvOp(*inverse).getUnitaryMatrix()); + + auto power = + QCOProgramBuilder::build(context.get(), [&](QCOProgramBuilder& builder) { + auto qubits = builder.allocQubitRegister(3); + auto outputs = builder.pow(2.0, qubits.qubits, [&](ValueRange args) { + return wideBody(builder, args); + }); + builder.sink(outputs[1]); + builder.sink(outputs[2]); + return builder.measure(outputs[0]).second; + }); + ASSERT_TRUE(power); + EXPECT_FALSE(firstPowOp(*power).hasCompileTimeKnownUnitaryMatrix()); + EXPECT_FALSE(firstPowOp(*power).getUnitaryMatrix()); + + auto controlled = + QCOProgramBuilder::build(context.get(), [&](QCOProgramBuilder& builder) { + auto qubits = builder.allocQubitRegister(4); + auto [controls, targets] = builder.ctrl( + ValueRange{qubits[0]}, ValueRange(qubits.qubits).drop_front(), + [&](ValueRange args) { return wideBody(builder, args); }); + builder.sink(controls[0]); + builder.sink(targets[1]); + builder.sink(targets[2]); + return builder.measure(targets[0]).second; + }); + ASSERT_TRUE(controlled); + EXPECT_FALSE(firstCtrlOp(*controlled).hasCompileTimeKnownUnitaryMatrix()); + EXPECT_FALSE(firstCtrlOp(*controlled).getUnitaryMatrix()); +} + +TEST_F(QCOMatrixTest, WideDenseMatrixQueriesFailSafely) { + auto wideInverse = + QCOProgramBuilder::build(context.get(), [](QCOProgramBuilder& builder) { + auto qubits = builder.allocQubitRegister(kMaxModifierTargetQubits + 1); + auto outputs = builder.inv(qubits.qubits, [](ValueRange args) { + return llvm::to_vector(args); + }); + for (Value output : outputs) { + builder.sink(output); + } + return builder.intConstant(0); + }); + ASSERT_TRUE(wideInverse); + auto inverse = firstInvOp(*wideInverse); + EXPECT_FALSE(inverse.hasCompileTimeKnownUnitaryMatrix()); + EXPECT_FALSE(inverse.getUnitaryMatrix()); + + auto widePower = + QCOProgramBuilder::build(context.get(), [](QCOProgramBuilder& builder) { + auto qubits = builder.allocQubitRegister(kMaxModifierTargetQubits + 1); + auto outputs = builder.pow(1.0, qubits.qubits, [](ValueRange args) { + return llvm::to_vector(args); + }); + for (Value output : outputs) { + builder.sink(output); + } + return builder.intConstant(0); + }); + ASSERT_TRUE(widePower); + auto power = firstPowOp(*widePower); + EXPECT_FALSE(power.hasCompileTimeKnownUnitaryMatrix()); + EXPECT_FALSE(power.getUnitaryMatrix()); + + auto wideControl = + QCOProgramBuilder::build(context.get(), [](QCOProgramBuilder& builder) { + auto qubits = builder.allocQubitRegister(kMaxModifierTargetQubits + 1); + auto [controls, targets] = builder.ctrl( + ValueRange{qubits[0]}, ValueRange(qubits.qubits).drop_front(), + [](ValueRange args) { return llvm::to_vector(args); }); + for (Value output : controls) { + builder.sink(output); + } + for (Value output : targets) { + builder.sink(output); + } + return builder.intConstant(0); + }); + ASSERT_TRUE(wideControl); + auto control = firstCtrlOp(*wideControl); + EXPECT_FALSE(control.hasCompileTimeKnownUnitaryMatrix()); + EXPECT_FALSE(control.getUnitaryMatrix()); + + auto wideBarrier = + QCOProgramBuilder::build(context.get(), [](QCOProgramBuilder& builder) { + auto qubits = builder.allocQubitRegister(kMaxModifierTargetQubits + 1); + auto outputs = builder.barrier(qubits.qubits); + for (Value output : outputs) { + builder.sink(output); + } + return builder.intConstant(0); + }); + ASSERT_TRUE(wideBarrier); + auto barrier = firstBarrierOp(*wideBarrier); + EXPECT_FALSE(barrier.hasCompileTimeKnownUnitaryMatrix()); + EXPECT_FALSE(barrier.getUnitaryMatrix()); +} + TEST_F(QCOMatrixTest, CXOpMatrix) { auto moduleOp = QCOProgramBuilder::build(context.get(), singleControlledX); ASSERT_TRUE(moduleOp); @@ -547,8 +852,14 @@ TEST_F(QCOMatrixTest, ComposeNTargetRejectsExcessiveTargets) { .has_value()); } -TEST_F(QCOMatrixTest, ComposeNTargetRejectsThreeQubitOp) { - expectComposeNTargetFails(context.get(), inverseWithThreeQubitOpInBody, 3); +TEST_F(QCOMatrixTest, ComposeNTargetAcceptsSoleThreeQubitOp) { + auto moduleOp = + QCOProgramBuilder::build(context.get(), inverseWithThreeQubitOpInBody); + ASSERT_TRUE(moduleOp); + const auto matrix = composeBodyMatrix(*firstInvOp(*moduleOp).getBody(), 3); + ASSERT_TRUE(matrix); + EXPECT_EQ(matrix->rows(), 8); + EXPECT_EQ(matrix->cols(), 8); } TEST_F(QCOMatrixTest, ComposeNTargetRejectsRuntimeGphase) { @@ -592,6 +903,31 @@ TEST_F(QCOMatrixTest, ComposeNTargetRejectsRuntimeUnitaryMatrix) { composeBodyMatrix(*firstInvOp(*moduleOp).getBody(), 1).has_value()); } +TEST_F(QCOMatrixTest, ComposeBodyMatrixRejectsNestedUnknownUnitary) { + constexpr auto mlirCode = R"mlir( + module { + func.func @test() -> !qco.qubit { + %condition = arith.constant true + %q_in = qco.alloc : !qco.qubit + %q_out = qco.inv (%q = %q_in) { + %q_1 = qco.h %q : !qco.qubit -> !qco.qubit + scf.if %condition { + %nested = qco.x %q_1 : !qco.qubit -> !qco.qubit + scf.yield + } + qco.yield %q_1 : !qco.qubit + } : {!qco.qubit} -> {!qco.qubit} + return %q_out : !qco.qubit + } + } + )mlir"; + + auto moduleOp = parseSourceString(mlirCode, context.get()); + ASSERT_TRUE(moduleOp); + EXPECT_FALSE( + composeBodyMatrix(*firstInvOp(*moduleOp).getBody(), 1).has_value()); +} + TEST_F(QCOMatrixTest, ComposeBodyMatrixHandlesNestedPower) { auto moduleOp = QCOProgramBuilder::build(context.get(), composedBodyWithNestedPow); @@ -641,13 +977,15 @@ TEST_F(QCOMatrixTest, PowMatrixAvailabilityContract) { ASSERT_TRUE(emptyModule); auto empty = firstPowOp(*emptyModule); EXPECT_TRUE(empty.hasCompileTimeKnownUnitaryMatrix()); - EXPECT_FALSE(empty.getUnitaryMatrix().has_value()); + const auto emptyMatrix = empty.getUnitaryMatrix(); + ASSERT_TRUE(emptyMatrix); + EXPECT_TRUE(emptyMatrix->isApprox(DynamicMatrix::identity(4))); auto unsupportedModule = QCOProgramBuilder::build(context.get(), powUnsupportedThreeQubitBody); ASSERT_TRUE(unsupportedModule); auto unsupported = firstPowOp(*unsupportedModule); - EXPECT_TRUE(unsupported.hasCompileTimeKnownUnitaryMatrix()); + EXPECT_FALSE(unsupported.hasCompileTimeKnownUnitaryMatrix()); EXPECT_FALSE(unsupported.getUnitaryMatrix().has_value()); auto dynamicBodyModule = QCOProgramBuilder::build(context.get(), powRxScaled); @@ -671,6 +1009,27 @@ TEST_F(QCOMatrixTest, PowHalfXOpMatrix) { ASSERT_TRUE(matrix->isApprox(SXOp::getUnitaryMatrix())); } +TEST_F(QCOMatrixTest, HugeIntegralPowMatrixRemainsFinite) { + auto moduleOp = + QCOProgramBuilder::build(context.get(), [](QCOProgramBuilder& builder) { + auto qubit = builder.allocQubit(); + qubit = builder.pow(std::numeric_limits::max(), qubit, + [&](Value target) { return builder.x(target); }); + return builder.measure(qubit).second; + }); + ASSERT_TRUE(moduleOp); + + const auto matrix = firstPowOp(*moduleOp).getUnitaryMatrix(); + ASSERT_TRUE(matrix); + for (std::int64_t row = 0; row < matrix->rows(); ++row) { + for (std::int64_t col = 0; col < matrix->cols(); ++col) { + EXPECT_TRUE(std::isfinite((*matrix)(row, col).real())); + EXPECT_TRUE(std::isfinite((*matrix)(row, col).imag())); + } + } + EXPECT_TRUE(matrix->isApprox(DynamicMatrix::identity(2), 1e-10)); +} + TEST_F(QCOMatrixTest, PowNegHalfXOpMatrix) { auto moduleOp = QCOProgramBuilder::build(context.get(), powNegHalfX); ASSERT_TRUE(moduleOp); @@ -975,7 +1334,9 @@ TEST_F(QCOMatrixTest, InverseTwoBarriersInInvOpMatrix) { auto moduleOp = QCOProgramBuilder::build(context.get(), inverseTwoBarriersInInv); ASSERT_TRUE(moduleOp); - EXPECT_FALSE(invMatrix(*moduleOp).has_value()); + const auto matrix = invMatrix(*moduleOp); + ASSERT_TRUE(matrix); + EXPECT_TRUE(matrix->isApprox(DynamicMatrix::identity(2))); } TEST_F(QCOMatrixTest, InvTwoOpMatrix) { diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index 38c3225ca6..f7dd02d715 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -29,14 +29,17 @@ #include #include #include +#include #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -1080,6 +1083,15 @@ static SmallVector singleQubitRunsSplitByScfFor(QCOProgramBuilder& b) { // FuseSingleQubitUnitaryRuns tests //===----------------------------------------------------------------------===// +TEST(FuseSingleQubitUnitaryRunsTest, DeclaresProducedDialects) { + auto pass = qco::createFuseSingleQubitUnitaryRuns({}); + DialectRegistry registry; + pass->getDependentDialects(registry); + EXPECT_TRUE(registry.getDialectAllocator("qc")); + EXPECT_TRUE( + registry.getDialectAllocator(qco::QCODialect::getDialectNamespace())); +} + TEST(FuseSingleQubitUnitaryRunsTest, InvalidBasisFailsPass) { TestFixture fx; fx.setUp(); @@ -1089,6 +1101,39 @@ TEST(FuseSingleQubitUnitaryRunsTest, InvalidBasisFailsPass) { EXPECT_TRUE(failed(runFuse(*owned, "not-a-basis"))); } +TEST(FuseSingleQubitUnitaryRunsTest, RejectsNonlinearQubitWithoutMutation) { + TestFixture fx; + fx.setUp(); + constexpr StringLiteral source = R"mlir( +module { + func.func @main() { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.h %q0 : !qco.qubit -> !qco.qubit + %q2 = qco.x %q0 : !qco.qubit -> !qco.qubit + qco.sink %q1 : !qco.qubit + qco.sink %q2 : !qco.qubit + return + } +} +)mlir"; + auto module = parseSourceString(source, fx.ctx()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(fx.ctx(), [&](Diagnostic& diagnostic) { + sawExpectedDiagnostic |= + StringRef(diagnostic.str()).contains("exactly one use"); + return success(); + }); + EXPECT_TRUE(failed(runFuse(*module, "zyz"))); + EXPECT_TRUE(sawExpectedDiagnostic); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + TEST(FuseSingleQubitUnitaryRunsTest, IgnoresDynamicPowerExponent) { TestFixture fx; fx.setUp(); @@ -1497,6 +1542,33 @@ TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossBoundariesAllBases) { } } +TEST(FuseSingleQubitUnitaryRunsTest, StopsAtFunctionCallBoundary) { + TestFixture fx; + fx.setUp(); + constexpr StringLiteral source = R"mlir( +module { + func.func private @opaque(!qco.qubit) -> !qco.qubit + func.func @main() attributes {mqt.entry_point} { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.h %q0 : !qco.qubit -> !qco.qubit + %q2 = func.call @opaque(%q1) : (!qco.qubit) -> !qco.qubit + %q3 = qco.x %q2 : !qco.qubit -> !qco.qubit + qco.sink %q3 : !qco.qubit + return + } +} +)mlir"; + auto module = parseSourceString(source, fx.ctx()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + EXPECT_TRUE(succeeded(runFuse(*module, "u"))); + EXPECT_TRUE(succeeded(verify(*module))); + auto main = module->lookupSymbol("main"); + ASSERT_TRUE(main); + EXPECT_EQ(countOps(main), 1U); +} + TEST(FuseSingleQubitUnitaryRunsTest, EliminatesIdentityInvMultiOpBody) { TestFixture fx; fx.setUp(); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp index 5a4c11d15a..2e888d8724 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp @@ -32,6 +32,8 @@ #include #include #include +#include +#include #include #include #include @@ -824,6 +826,77 @@ TEST_F(MultiControlledDecompositionTest, LeavesUnsupportedCtrlUntouched) { EXPECT_EQ(controlledDcx, 1U); } +TEST_F(MultiControlledDecompositionTest, PreservesClassicalBodyCalls) { + auto moduleOp = parseSourceString(R"mlir( + module { + func.func private @observe() + func.func @main() { + %q0 = qco.alloc : !qco.qubit + %q1 = qco.alloc : !qco.qubit + %q2 = qco.alloc : !qco.qubit + %c0, %c1, %target = qco.ctrl(%q0, %q1) targets(%arg = %q2) { + func.call @observe() : () -> () + %out = qco.x %arg : !qco.qubit -> !qco.qubit + qco.yield %out : !qco.qubit + } : ({!qco.qubit, !qco.qubit}, {!qco.qubit}) + -> ({!qco.qubit, !qco.qubit}, {!qco.qubit}) + qco.sink %c0 : !qco.qubit + qco.sink %c1 : !qco.qubit + qco.sink %target : !qco.qubit + return + } + } + )mlir", + context()); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(runDecomposeMultiControlled(*moduleOp).succeeded()); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + size_t calls = 0; + moduleOp->walk([&](func::CallOp) { ++calls; }); + EXPECT_EQ(calls, 1U); + expectFullyLowered(*moduleOp); +} + +TEST_F(MultiControlledDecompositionTest, + LeavesPostUnitaryClassicalBodyCallInPlace) { + auto moduleOp = parseSourceString(R"mlir( + module { + func.func private @observe() + func.func @main() { + %q0 = qco.alloc : !qco.qubit + %q1 = qco.alloc : !qco.qubit + %q2 = qco.alloc : !qco.qubit + %c0, %c1, %target = qco.ctrl(%q0, %q1) targets(%arg = %q2) { + %out = qco.x %arg : !qco.qubit -> !qco.qubit + func.call @observe() : () -> () + qco.yield %out : !qco.qubit + } : ({!qco.qubit, !qco.qubit}, {!qco.qubit}) + -> ({!qco.qubit, !qco.qubit}, {!qco.qubit}) + qco.sink %c0 : !qco.qubit + qco.sink %c1 : !qco.qubit + qco.sink %target : !qco.qubit + return + } + } + )mlir", + context()); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(runDecomposeMultiControlled(*moduleOp).succeeded()); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + SmallVector controls; + moduleOp->walk([&](CtrlOp op) { controls.push_back(op); }); + ASSERT_EQ(controls.size(), 1U); + auto& body = controls.front().getBody()->getOperations(); + auto operation = body.begin(); + EXPECT_TRUE(isa(*operation++)); + EXPECT_TRUE(isa(*operation++)); + EXPECT_TRUE(isa(*operation)); +} + TEST_F(MultiControlledDecompositionTest, PhasePiRoutesThroughMcz) { for (const double theta : {std::numbers::pi, -std::numbers::pi}) { for (const size_t k : {2U, 3U, 4U, 5U}) { diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/Mapping/CMakeLists.txt index 49b90f6ac3..964d13d59e 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/CMakeLists.txt @@ -12,10 +12,12 @@ add_executable(${target_name} test_mapping.cpp) target_link_libraries( ${target_name} PRIVATE GTest::gtest_main + MLIRControlFlowDialect MLIRParser MLIRMQTDialect MQTCompilerTarget MLIRQCOProgramBuilder + MLIRQCOUtils MLIRQTensorUtils MLIRQCOTransforms MLIRSupportMQT) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index 2335329419..94067561cb 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -14,6 +14,7 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QCO/Transforms/Mapping/Mapping.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" @@ -29,12 +30,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -51,6 +54,7 @@ #include #include #include +#include #include #include #include @@ -67,6 +71,14 @@ static SmallVector getQubitValues(ValueRange values) { values, [](Value value) { return isa(value.getType()); })); } +TEST(MappingPassContract, DeclaresSCFDependency) { + auto pass = createMappingPass(); + DialectRegistry registry; + pass->getDependentDialects(registry); + EXPECT_TRUE( + registry.getDialectAllocator(scf::SCFDialect::getDialectNamespace())); +} + /// Return true, if the operations within a region fulfill the given coupling /// constraints. static bool isExecutable(Region& body, @@ -296,7 +308,8 @@ class MappingPassFixture : public testing::Test { void SetUp() override { DialectRegistry registry; registry.insert(); + scf::SCFDialect, arith::ArithDialect, + cf::ControlFlowDialect, func::FuncDialect>(); context = std::make_unique(); context->appendDialectRegistry(registry); context->loadAllAvailableDialects(); @@ -323,6 +336,445 @@ class MappingPassTest : public MappingPassFixture, }; // namespace +TEST_F(MappingPassFixture, MissingTargetFailsWithoutMutation) { + QCOProgramBuilder builder(context.get()); + builder.initialize(); + auto qubit = builder.allocQubit(); + builder.sink(qubit); + auto module = builder.finalize(); + OwningOpRef original(module->clone()); + + std::string diagnostics; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + return success(); + }); + PassManager pm(context.get()); + pm.addPass(createMappingPass()); + EXPECT_TRUE(failed(pm.run(*module))); + EXPECT_TRUE(StringRef(diagnostics).contains("requires a compiler target")) + << diagnostics; + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST_F(MappingPassFixture, InvalidOptionsFailWithoutMutation) { + QCOProgramBuilder builder(context.get()); + builder.initialize(); + auto qubit = builder.allocQubit(); + builder.sink(qubit); + auto source = builder.finalize(); + const auto target = llvm::cantFail(CompilerTarget::create(1)); + + const auto checkInvalid = [&](const MappingPassOptions& options, + StringRef expected) { + OwningOpRef module(source->clone()); + OwningOpRef original(module->clone()); + std::string diagnostics; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + return success(); + }); + EXPECT_TRUE(failed(runPass(*module, target, options))); + EXPECT_TRUE(StringRef(diagnostics).contains(expected)) << diagnostics; + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); + }; + + checkInvalid(MappingPassOptions{.alpha = 0}, "requires finite alpha > 0"); + checkInvalid( + MappingPassOptions{.alpha = std::numeric_limits::infinity()}, + "requires finite alpha > 0"); + checkInvalid( + MappingPassOptions{.lambda = std::numeric_limits::infinity()}, + "requires finite lambda"); + checkInvalid( + MappingPassOptions{.nlookahead = std::numeric_limits::max()}, + "requires nlookahead <= 4096"); + checkInvalid(MappingPassOptions{.niterations = 0}, + "requires 0 < niterations <= 4096"); + checkInvalid( + MappingPassOptions{.niterations = std::numeric_limits::max()}, + "requires 0 < niterations <= 4096"); + checkInvalid(MappingPassOptions{.ntrials = 0}, + "requires 0 < ntrials <= 4096"); + checkInvalid( + MappingPassOptions{.ntrials = std::numeric_limits::max()}, + "requires 0 < ntrials <= 4096"); +} + +TEST_F(MappingPassFixture, StaticInputFailsWithoutMutation) { + constexpr StringLiteral source = R"mlir( + module { + func.func @main() attributes {mqt.entry_point} { + %q0 = qco.static 0 : !qco.qubit + %q2 = qco.static 2 : !qco.qubit + qco.sink %q0 : !qco.qubit + qco.sink %q2 : !qco.qubit + return + } + } + )mlir"; + auto module = parseSourceString(source, context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + const auto target = llvm::cantFail(CompilerTarget::create( + 3, std::vector{{0, 1}, {1, 2}})); + + EXPECT_TRUE(failed(runPass(*module, target, MappingPassOptions{}))); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST_F(MappingPassFixture, QuantumVectorSignatureFailsWithoutMutation) { + constexpr StringLiteral source = R"mlir( + module { + func.func @main(%qubits: vector<2x!qco.qubit>) + -> vector<2x!qco.qubit> attributes {mqt.entry_point} { + return %qubits : vector<2x!qco.qubit> + } + } + )mlir"; + auto module = parseSourceString(source, context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + const auto target = llvm::cantFail(CompilerTarget::create(2)); + + std::string diagnostics; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + return success(); + }); + EXPECT_TRUE(failed(runPass(*module, target, MappingPassOptions{}))); + EXPECT_TRUE(StringRef(diagnostics) + .contains("does not support quantum function arguments or " + "results")) + << diagnostics; + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST_F(MappingPassFixture, MultiBlockInputFailsWithoutMutation) { + constexpr StringLiteral source = R"mlir( + module { + func.func @main() attributes {mqt.entry_point} { + %q0 = qco.alloc : !qco.qubit + qco.sink %q0 : !qco.qubit + cf.br ^next + ^next: + %q1 = qco.alloc : !qco.qubit + qco.sink %q1 : !qco.qubit + return + } + } + )mlir"; + auto module = parseSourceString(source, context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + const auto target = llvm::cantFail(CompilerTarget::create(2)); + + EXPECT_TRUE(failed(runPass(*module, target, MappingPassOptions{}))); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST_F(MappingPassFixture, ClassicalOnlyInputIsUnchanged) { + constexpr StringLiteral source = R"mlir( + module { + func.func @main() -> i64 attributes {mqt.entry_point} { + %value = arith.constant 7 : i64 + return %value : i64 + } + } + )mlir"; + auto module = parseSourceString(source, context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + const auto target = llvm::cantFail(CompilerTarget::create(2)); + + ASSERT_TRUE(succeeded(runPass(*module, target, MappingPassOptions{}))); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST_F(MappingPassFixture, UnsupportedQubitConsumerFailsWithoutMutation) { + constexpr StringLiteral source = R"mlir( +module { + func.func private @opaque(!qco.qubit) -> !qco.qubit + func.func @main() attributes {mqt.entry_point} { + %q0 = qco.alloc : !qco.qubit + %q1 = func.call @opaque(%q0) : (!qco.qubit) -> !qco.qubit + qco.sink %q1 : !qco.qubit + return + } +} +)mlir"; + auto module = parseSourceString(source, context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + const auto target = llvm::cantFail(CompilerTarget::create(1)); + + std::string diagnostics; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + return success(); + }); + EXPECT_TRUE(failed(runPass(*module, target, MappingPassOptions{}))); + EXPECT_TRUE(StringRef(diagnostics) + .contains("target mapping does not support quantum values " + "carried by func.call")) + << diagnostics; + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST_F(MappingPassFixture, NonlinearQubitFailsWithoutMutation) { + constexpr StringLiteral source = R"mlir( +module { + func.func @main() attributes {mqt.entry_point} { + %q0 = qco.alloc : !qco.qubit + %q1 = qco.h %q0 : !qco.qubit -> !qco.qubit + %q2 = qco.x %q0 : !qco.qubit -> !qco.qubit + qco.sink %q1 : !qco.qubit + qco.sink %q2 : !qco.qubit + return + } +} +)mlir"; + auto module = parseSourceString(source, context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + const auto target = llvm::cantFail(CompilerTarget::create(1)); + + std::string diagnostics; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + return success(); + }); + EXPECT_TRUE(failed(runPass(*module, target, MappingPassOptions{}))); + EXPECT_TRUE(StringRef(diagnostics).contains("exactly one use")) + << diagnostics; + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST_F(MappingPassFixture, UnsupportedWhileTensorFlowFailsWithoutMutation) { + constexpr StringLiteral source = R"mlir( +module { + func.func @main() attributes {mqt.entry_point} { + %c1 = arith.constant 1 : index + %false = arith.constant false + %dropped = qtensor.alloc(%c1) : tensor<1x!qco.qubit> + %captured = qtensor.alloc(%c1) : tensor<1x!qco.qubit> + %result = scf.while (%arg = %dropped) + : (tensor<1x!qco.qubit>) -> tensor<1x!qco.qubit> { + qtensor.dealloc %arg : tensor<1x!qco.qubit> + scf.condition(%false) %captured : tensor<1x!qco.qubit> + } do { + ^bb0(%arg: tensor<1x!qco.qubit>): + scf.yield %arg : tensor<1x!qco.qubit> + } + qtensor.dealloc %result : tensor<1x!qco.qubit> + return + } +} +)mlir"; + auto module = parseSourceString(source, context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + const auto target = llvm::cantFail(CompilerTarget::create(2)); + + std::string diagnostics; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + return success(); + }); + EXPECT_TRUE(failed(runPass(*module, target, MappingPassOptions{}))); + EXPECT_TRUE( + StringRef(diagnostics) + .contains("requires every quantum tensor scf.while init to reach its " + "condition")) + << diagnostics; + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST_F(MappingPassFixture, NonPositionalWhileQubitFlowFailsWithoutMutation) { + constexpr StringLiteral source = R"mlir( +module { + func.func @main() attributes {mqt.entry_point} { + %false = arith.constant false + %state = arith.constant 0 : i32 + %q = qco.alloc : !qco.qubit + %next_q, %next_state = + scf.while (%iter_state = %state, %iter_q = %q) + : (i32, !qco.qubit) -> (!qco.qubit, i64) { + %extended_state = arith.extsi %iter_state : i32 to i64 + scf.condition(%false) %iter_q, %extended_state + : !qco.qubit, i64 + } do { + ^bb0(%after_q: !qco.qubit, %after_state: i64): + %truncated_state = arith.trunci %after_state : i64 to i32 + scf.yield %truncated_state, %after_q : i32, !qco.qubit + } + qco.sink %next_q : !qco.qubit + return + } +} +)mlir"; + auto module = parseSourceString(source, context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + ASSERT_TRUE(succeeded(qco::verifyLinearity(*module))); + OwningOpRef original(module->clone()); + const auto target = llvm::cantFail(CompilerTarget::create(1)); + + std::string diagnostics; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + return success(); + }); + EXPECT_TRUE(failed(runPass(*module, target, MappingPassOptions{}))); + EXPECT_TRUE( + StringRef(diagnostics) + .contains("requires positional scalar-qubit scf.while inputs and " + "results")) + << diagnostics; + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST_F(MappingPassFixture, DuplicateEntryPointsFailWithoutMutation) { + constexpr StringLiteral source = R"mlir( +module { + func.func @first() attributes {mqt.entry_point} { return } + func.func @second() attributes {mqt.entry_point} { return } +} +)mlir"; + auto module = parseSourceString(source, context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + const auto target = llvm::cantFail(CompilerTarget::create(1)); + + std::string diagnostics; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + return success(); + }); + EXPECT_TRUE(failed(runPass(*module, target, MappingPassOptions{}))); + EXPECT_TRUE( + StringRef(diagnostics) + .contains("module must contain at most one program entry point")) + << diagnostics; + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST_F(MappingPassFixture, UnsupportedQuantumRegionFailsWithoutMutation) { + constexpr StringLiteral source = R"mlir( +module { + func.func @main() attributes {mqt.entry_point} { + %q = qco.alloc : !qco.qubit + scf.execute_region { + qco.sink %q : !qco.qubit + scf.yield + } + return + } +} +)mlir"; + auto module = parseSourceString(source, context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + const auto target = llvm::cantFail(CompilerTarget::create(1)); + + std::string diagnostics; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + return success(); + }); + EXPECT_TRUE(failed(runPass(*module, target, MappingPassOptions{}))); + EXPECT_TRUE( + StringRef(diagnostics) + .contains("target mapping does not support quantum operations nested " + "in scf.execute_region")) + << diagnostics; + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST_F(MappingPassFixture, DeepStructuredNestingFailsWithoutMutation) { + constexpr size_t depth = 65; + std::string source = R"mlir( +module { + func.func @main() attributes {mqt.entry_point} { + %q = qco.alloc : !qco.qubit + %lb = arith.constant 0 : index + %ub = arith.constant 1 : index + %step = arith.constant 1 : index +)mlir"; + for (size_t i = 0; i < depth; ++i) { + const auto input = i == 0 ? "%q" : "%arg" + std::to_string(i - 1); + source += " %out" + std::to_string(i) + " = scf.for %iv" + + std::to_string(i) + " = %lb to %ub step %step iter_args(%arg" + + std::to_string(i) + " = " + input + " ) -> (!qco.qubit) {\n"; + } + source += " %leaf = qco.h %arg" + std::to_string(depth - 1) + + " : !qco.qubit -> !qco.qubit\n"; + for (size_t i = depth; i-- > 0;) { + const auto yielded = + i + 1 == depth ? "%leaf" : "%out" + std::to_string(i + 1); + source += " scf.yield " + yielded + " : !qco.qubit\n }\n"; + } + source += R"mlir( + qco.sink %out0 : !qco.qubit + return + } +} +)mlir"; + + auto module = parseSourceString(source, context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + const auto target = llvm::cantFail(CompilerTarget::create(1)); + + std::string diagnostics; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + return success(); + }); + EXPECT_TRUE(failed(runPass(*module, target, MappingPassOptions{}))); + EXPECT_TRUE( + StringRef(diagnostics) + .contains("supports at most 64 nested quantum structured operations")) + << diagnostics; + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + TEST_F(MappingPassFixture, MapTopologyOnlyWithEmptyOperationSet) { constexpr int64_t size = 3; @@ -359,6 +811,74 @@ TEST_F(MappingPassFixture, MapTopologyOnlyWithEmptyOperationSet) { size_t numSwaps = 0; m->walk([&](SWAPOp) { ++numSwaps; }); EXPECT_GT(numSwaps, 0); + + size_t numMeasurements = 0; + size_t numMeasurementsAfterSwap = 0; + m->walk([&](MeasureOp op) { + ++numMeasurements; + if (op.getQubitIn().getDefiningOp()) { + ++numMeasurementsAfterSwap; + } + const bool hasOneUse = op.getQubitOut().hasOneUse(); + EXPECT_TRUE(hasOneUse); + if (hasOneUse) { + EXPECT_TRUE(isa(*op.getQubitOut().getUsers().begin())); + } + }); + EXPECT_EQ(numMeasurements, size); + EXPECT_GT(numMeasurementsAfterSwap, 0); +} + +TEST_F(MappingPassFixture, KeepTerminalResetsAfterRoutingSwaps) { + constexpr int64_t size = 3; + + const auto target = llvm::cantFail(CompilerTarget::create( + 3, std::vector{{0, 1}, {1, 2}}, + std::vector{})); + + QCOProgramBuilder builder(context.get()); + builder.initialize(); + + SmallVector qubits(size); + for (int64_t i = 0; i < size; ++i) { + qubits[i] = builder.allocQubit(); + } + + qubits[0] = builder.x(qubits[0]); + std::tie(qubits[0], qubits[1]) = builder.rxx(0.25, qubits[0], qubits[1]); + std::tie(qubits[1], qubits[2]) = builder.rzx(0.5, qubits[1], qubits[2]); + std::tie(qubits[0], qubits[2]) = builder.cx(qubits[0], qubits[2]); + + for (Value& qubit : qubits) { + qubit = builder.reset(qubit); + builder.sink(qubit); + } + + auto m = builder.finalize(); + ASSERT_TRUE( + runPass(m.get(), target, MappingPassOptions{.ntrials = 1}).succeeded()); + ASSERT_TRUE(succeeded(verify(*m))); + EXPECT_TRUE(isExecutable(getEntryPoint(m.get()), target)); + + size_t numSwaps = 0; + m->walk([&](SWAPOp) { ++numSwaps; }); + EXPECT_GT(numSwaps, 0); + + size_t numResets = 0; + size_t numResetsAfterSwap = 0; + m->walk([&](ResetOp op) { + ++numResets; + if (op.getQubitIn().getDefiningOp()) { + ++numResetsAfterSwap; + } + const bool hasOneUse = op.getQubitOut().hasOneUse(); + EXPECT_TRUE(hasOneUse); + if (hasOneUse) { + EXPECT_TRUE(isa(*op.getQubitOut().getUsers().begin())); + } + }); + EXPECT_EQ(numResets, size); + EXPECT_GT(numResetsAfterSwap, 0); } TEST_F(MappingPassFixture, PreserveNoncontiguousTargetSiteIds) { diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp index 0ec0326559..27ca6eab3e 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -11,6 +11,7 @@ #include "dd/DDDefinitions.hpp" #include "dd/Package.hpp" #include "mlir/Compiler/Target.h" +#include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" @@ -244,11 +245,19 @@ TEST(TargetSynthesisPassContract, FactoriesAreIndependentlyConstructible) { fusion->getDependentDialects(fusionDialects); EXPECT_TRUE(fusionDialects.getDialectAllocator( mlir::arith::ArithDialect::getDialectNamespace())); + EXPECT_TRUE(fusionDialects.getDialectAllocator( + mlir::qc::QCDialect::getDialectNamespace())); + EXPECT_TRUE(fusionDialects.getDialectAllocator( + mlir::qco::QCODialect::getDialectNamespace())); mlir::DialectRegistry synthesisDialects; synthesis->getDependentDialects(synthesisDialects); EXPECT_TRUE(synthesisDialects.getDialectAllocator( mlir::arith::ArithDialect::getDialectNamespace())); + EXPECT_TRUE(synthesisDialects.getDialectAllocator( + mlir::qc::QCDialect::getDialectNamespace())); + EXPECT_TRUE(synthesisDialects.getDialectAllocator( + mlir::qco::QCODialect::getDialectNamespace())); } TEST_F(TargetSynthesisTest, TwoQubitGateFusionRequiresStrictImprovement) { @@ -334,6 +343,76 @@ TEST_F(TargetSynthesisTest, TwoQubitGateFusionLeavesIndividualOpsAlone) { EXPECT_EQ(printModule(*module), before); } +TEST_F(TargetSynthesisTest, TwoQubitGateFusionHandlesUnusedOutputs) { + auto module = mlir::parseSourceString(R"mlir( + module { + func.func @main() { + %control = qco.static 0 : !qco.qubit + %target = qco.static 1 : !qco.qubit + %unused_control, %unused_target = qco.ctrl(%control) + targets(%arg = %target) { + %body = qco.x %arg : !qco.qubit -> !qco.qubit + qco.yield %body : !qco.qubit + } : ({!qco.qubit}, {!qco.qubit}) + -> ({!qco.qubit}, {!qco.qubit}) + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*module))); + const auto before = printModule(*module); + + EXPECT_TRUE( + mlir::failed(runPass(*module, mlir::qco::createFuseTwoQubitGates()))); + EXPECT_EQ(printModule(*module), before); +} + +TEST_F(TargetSynthesisTest, TwoQubitGateFusionPreservesModifierSupportCalls) { + auto module = mlir::parseSourceString(R"mlir( + module { + func.func private @observe() + func.func @main() { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %c0, %t0 = qco.ctrl(%q0) targets(%arg = %q1) { + %body = qco.x %arg : !qco.qubit -> !qco.qubit + func.call @observe() : () -> () + qco.yield %body : !qco.qubit + } : ({!qco.qubit}, {!qco.qubit}) + -> ({!qco.qubit}, {!qco.qubit}) + %c1, %t1 = qco.ctrl(%c0) targets(%arg = %t0) { + %body = qco.x %arg : !qco.qubit -> !qco.qubit + qco.yield %body : !qco.qubit + } : ({!qco.qubit}, {!qco.qubit}) + -> ({!qco.qubit}, {!qco.qubit}) + qco.sink %c1 : !qco.qubit + qco.sink %t1 : !qco.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*module))); + + EXPECT_TRUE( + mlir::succeeded(runPass(*module, mlir::qco::createFuseTwoQubitGates()))); + EXPECT_TRUE(mlir::succeeded(mlir::verify(*module))); + EXPECT_EQ(countOps(*module), 1U); + EXPECT_EQ(countOps(*module), 2U); + auto main = module->lookupSymbol("main"); + ASSERT_TRUE(main); + auto controls = main.getOps(); + ASSERT_FALSE(controls.empty()); + auto firstControl = *controls.begin(); + EXPECT_EQ(llvm::range_size(firstControl.getBody()->getOps()), 1U); + EXPECT_EQ( + llvm::range_size(firstControl.getBody()->getOps()), + 1U); +} + TEST_F(TargetSynthesisTest, TwoQubitGateFusionLeavesRuntimeParameterizedRunsAlone) { auto module = mlir::parseSourceString(R"mlir( @@ -377,6 +456,83 @@ TEST_F(TargetSynthesisTest, TargetNativeSynthesisRemovesOrdinarySwap) { expectEquivalent(expected, synthesized); } +TEST_F(TargetSynthesisTest, + TargetNativeSynthesisPreservesModifierSupportCalls) { + auto module = mlir::parseSourceString(R"mlir( + module { + func.func private @observe() + func.func @main() { + %control = qco.static 0 : !qco.qubit + %target = qco.static 1 : !qco.qubit + %control_out, %target_out = qco.ctrl(%control) + targets(%arg = %target) { + func.call @observe() : () -> () + %body = qco.x %arg : !qco.qubit -> !qco.qubit + qco.yield %body : !qco.qubit + } : ({!qco.qubit}, {!qco.qubit}) + -> ({!qco.qubit}, {!qco.qubit}) + qco.sink %control_out : !qco.qubit + qco.sink %target_out : !qco.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*module))); + const auto target = + valid(Target::create(2, std::nullopt, + std::vector{valid(Operation::create("u", 1, 3)), + valid(Operation::create("cz", 2, 0))})); + + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*module))); + EXPECT_EQ(countOps(*module), 1U); + module->walk([&](mlir::func::CallOp call) { + EXPECT_FALSE(call->getParentOfType()); + }); + EXPECT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); +} + +TEST_F(TargetSynthesisTest, + TargetNativeSynthesisRejectsPostUnitarySupportCallWithoutMutation) { + auto module = mlir::parseSourceString(R"mlir( + module { + func.func private @observe() + func.func @main() { + %control = qco.static 0 : !qco.qubit + %target = qco.static 1 : !qco.qubit + %control_out, %target_out = qco.ctrl(%control) + targets(%arg = %target) { + %body = qco.x %arg : !qco.qubit -> !qco.qubit + func.call @observe() : () -> () + qco.yield %body : !qco.qubit + } : ({!qco.qubit}, {!qco.qubit}) + -> ({!qco.qubit}, {!qco.qubit}) + qco.sink %control_out : !qco.qubit + qco.sink %target_out : !qco.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*module))); + const auto before = printModule(*module); + const auto target = + valid(Target::create(2, std::nullopt, + std::vector{valid(Operation::create("u", 1, 3)), + valid(Operation::create("cz", 2, 0))})); + + const auto diagnostics = + expectFailure(*module, mlir::qco::createTargetNativeSynthesis(target)); + EXPECT_NE(diagnostics.find("cannot move across the unitary operations"), + std::string::npos); + EXPECT_EQ(printModule(*module), before); +} + TEST_F(TargetSynthesisTest, TargetNativeSynthesisLowersConstantSingleQubitGate) { const auto hadamard = [](QCOProgramBuilder& builder) { diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_hadamard_lifting.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_hadamard_lifting.cpp index 56e38eff1c..570fd054a8 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_hadamard_lifting.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_hadamard_lifting.cpp @@ -9,6 +9,7 @@ */ #include "Support/IRVerification.h" +#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Transforms/GlobalPhaseNormalization.h" #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" @@ -20,8 +21,11 @@ #include #include #include +#include #include #include +#include +#include #include #include #include @@ -48,8 +52,8 @@ class QCOHadamardLiftingTest : public testing::Test { void SetUp() override { // Register all necessary dialects DialectRegistry registry; - registry.insert(); + registry.insert(); context.appendDialectRegistry(registry); context.loadAllAvailableDialects(); @@ -81,6 +85,91 @@ class QCOHadamardLiftingTest : public testing::Test { } // namespace +TEST(HadamardLiftingPassContract, DeclaresProducedDialects) { + auto pass = createHadamardLifting(); + DialectRegistry registry; + pass->getDependentDialects(registry); + EXPECT_TRUE( + registry.getDialectAllocator(arith::ArithDialect::getDialectNamespace())); + EXPECT_TRUE(registry.getDialectAllocator("qc")); + EXPECT_TRUE( + registry.getDialectAllocator(qco::QCODialect::getDialectNamespace())); +} + +TEST_F(QCOHadamardLiftingTest, RejectsInvalidProgramMetadataWithoutMutation) { + auto input = parseSourceString(R"mlir( + module { + func.func @first() attributes {mqt.entry_point} { + %q = qco.static 0 : !qco.qubit + %y = qco.y %q : !qco.qubit -> !qco.qubit + %h = qco.h %y : !qco.qubit -> !qco.qubit + qco.sink %h : !qco.qubit + return + } + func.func @second() attributes {mqt.entry_point} { + return + } + } + )mlir", + &context); + ASSERT_TRUE(input); + ASSERT_TRUE(succeeded(verify(*input))); + OwningOpRef original(input->clone()); + + EXPECT_TRUE(failed(runHadamardLiftingPass(*input))); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + input->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST_F(QCOHadamardLiftingTest, HandlesUnusedPauliOutput) { + auto input = parseSourceString(R"mlir( + module { + func.func @main() { + %q = qco.static 0 : !qco.qubit + %unused = qco.x %q : !qco.qubit -> !qco.qubit + return + } + } + )mlir", + &context); + ASSERT_TRUE(input); + ASSERT_TRUE(succeeded(verify(*input))); + OwningOpRef original(input->clone()); + EXPECT_TRUE(failed(runHadamardLiftingPass(*input))); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + input->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST_F(QCOHadamardLiftingTest, HandlesUnusedCnotControlOutput) { + auto input = parseSourceString(R"mlir( + module { + func.func @main() { + %control = qco.static 0 : !qco.qubit + %target = qco.static 1 : !qco.qubit + %unused, %target_out = qco.ctrl(%control) targets(%arg = %target) { + %body = qco.x %arg : !qco.qubit -> !qco.qubit + qco.yield %body : !qco.qubit + } : ({!qco.qubit}, {!qco.qubit}) + -> ({!qco.qubit}, {!qco.qubit}) + %hadamard = qco.h %target_out : !qco.qubit -> !qco.qubit + %measured, %result = qco.measure %hadamard : !qco.qubit + qco.sink %measured : !qco.qubit + return + } + } + )mlir", + &context); + ASSERT_TRUE(input); + ASSERT_TRUE(succeeded(verify(*input))); + OwningOpRef original(input->clone()); + EXPECT_TRUE(failed(runHadamardLiftingPass(*input))); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + input->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + // ################################################## // # Raise Hadamard over uncontrolled Pauli gate Tests // ################################################## diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_measurement_lifting.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_measurement_lifting.cpp index bc22a32ab4..1fade3dda0 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_measurement_lifting.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_measurement_lifting.cpp @@ -11,20 +11,27 @@ #include "Support/IRVerification.h" #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/Transforms/Passes.h" #include #include #include +#include #include +#include #include +#include #include #include +#include +#include #include #include #include #include +#include #include #include @@ -688,3 +695,108 @@ TEST_F(QCOMeasurementLiftingTest, liftMeasurementOverInvertedPhaseGates) { EXPECT_TRUE( areModulesEquivalentWithPermutations(program.get(), reference.get())); } + +TEST_F(QCOMeasurementLiftingTest, RejectsNonlinearQubitWithoutMutation) { + program = parseSourceString(R"mlir( + module { + func.func @main() { + %qubit = qco.alloc : !qco.qubit + %phase = qco.z %qubit : !qco.qubit -> !qco.qubit + %measured, %bit = qco.measure %phase : !qco.qubit + %other = qco.x %phase : !qco.qubit -> !qco.qubit + qco.sink %measured : !qco.qubit + qco.sink %other : !qco.qubit + return + } + } + )mlir", + &context); + ASSERT_TRUE(program); + ASSERT_TRUE(succeeded(verify(*program))); + OwningOpRef original(program->clone()); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + sawExpectedDiagnostic |= + StringRef(diagnostic.str()).contains("exactly one use"); + return success(); + }); + EXPECT_TRUE(failed(runMeasurementLiftingPass(*program))); + EXPECT_TRUE(sawExpectedDiagnostic); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + program->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST_F(QCOMeasurementLiftingTest, + PreservesModifierSupportOperationsWhenRefusingLift) { + auto module = parseSourceString(R"mlir( +module { + func.func private @observe() + func.func @main() { + %q = qco.static 0 : !qco.qubit + %phase = qco.inv (%arg = %q) { + %z = qco.z %arg : !qco.qubit -> !qco.qubit + func.call @observe() : () -> () + qco.yield %z : !qco.qubit + } : {!qco.qubit} -> {!qco.qubit} + %measured, %bit = qco.measure %phase : !qco.qubit + qco.sink %measured : !qco.qubit + return + } +} +)mlir", + &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + + PassManager manager(&context); + manager.addPass(createMeasurementLifting()); + EXPECT_TRUE(succeeded(manager.run(*module))); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST_F(QCOMeasurementLiftingTest, DeepModifierNestingFailsWithoutMutation) { + OpBuilder builder(&context); + OwningOpRef moduleOp = ModuleOp::create(builder.getUnknownLoc()); + builder.setInsertionPointToStart(moduleOp->getBody()); + const auto loc = moduleOp->getLoc(); + const auto qubitType = QubitType::get(&context); + auto function = func::FuncOp::create( + builder, loc, "test", + builder.getFunctionType({qubitType}, {qubitType, builder.getI1Type()})); + auto* entry = function.addEntryBlock(); + builder.setInsertionPointToStart(entry); + + constexpr std::size_t modifierDepth = 512; + auto outer = InvOp::create(builder, loc, ValueRange{function.getArgument(0)}); + auto* body = &outer.getBodyRegion().emplaceBlock(); + auto currentQubit = body->addArgument(qubitType, loc); + for (std::size_t i = 1; i < modifierDepth; ++i) { + builder.setInsertionPointToEnd(body); + auto inner = InvOp::create(builder, loc, ValueRange{currentQubit}); + YieldOp::create(builder, loc, inner.getQubitsOut()); + body = &inner.getBodyRegion().emplaceBlock(); + currentQubit = body->addArgument(qubitType, loc); + } + builder.setInsertionPointToEnd(body); + auto z = ZOp::create(builder, loc, currentQubit); + YieldOp::create(builder, loc, ValueRange{z.getOutputTarget(0)}); + + builder.setInsertionPointToEnd(entry); + auto measurement = MeasureOp::create(builder, loc, outer.getQubitsOut()[0]); + func::ReturnOp::create( + builder, loc, + ValueRange{measurement.getQubitOut(), measurement.getResult()}); + OwningOpRef original(moduleOp->clone()); + + PassManager manager(&context); + manager.addPass(createMeasurementLifting()); + EXPECT_TRUE(failed(manager.run(*moduleOp))); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + moduleOp->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp index ae93590b2b..dd6d950ba4 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp @@ -26,8 +26,12 @@ #include #include #include +#include +#include #include #include +#include +#include #include #include #include @@ -36,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -48,6 +53,15 @@ using namespace mlir::qco; /// A constant for the value of \f$\pi\f$. constexpr double PI = std::numbers::pi; +TEST(MergeSingleQubitRotationGatesPassContract, DeclaresProducedDialects) { + auto pass = createMergeSingleQubitRotationGates(); + DialectRegistry registry; + pass->getDependentDialects(registry); + EXPECT_TRUE(registry.getDialectAllocator("qc")); + EXPECT_TRUE( + registry.getDialectAllocator(qco::QCODialect::getDialectNamespace())); +} + class MergeSingleQubitRotationGatesTest : public ::testing::Test { protected: MLIRContext context; @@ -314,6 +328,40 @@ TEST_F(MergeSingleQubitRotationGatesTest, mergeRXRXGates) { EXPECT_EQ(countOps(), 0); } +TEST_F(MergeSingleQubitRotationGatesTest, + RejectsNonlinearQubitWithoutMutation) { + constexpr StringLiteral source = R"mlir( +module { + func.func @main() { + %theta0 = arith.constant 0.25 : f64 + %theta1 = arith.constant 0.5 : f64 + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.rx(%theta0) %q0 : !qco.qubit -> !qco.qubit + %q2 = qco.ry(%theta1) %q0 : !qco.qubit -> !qco.qubit + qco.sink %q1 : !qco.qubit + qco.sink %q2 : !qco.qubit + return + } +} +)mlir"; + module = parseSourceString(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { + sawExpectedDiagnostic |= + StringRef(diagnostic.str()).contains("exactly one use"); + return success(); + }); + EXPECT_TRUE(failed(runMergePass(*module))); + EXPECT_TRUE(sawExpectedDiagnostic); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + /** * @brief Test: RX->RY should merge into a single U gate */ @@ -578,6 +626,26 @@ TEST_F(MergeSingleQubitRotationGatesTest, mergePPGates) { EXPECT_EQ(countOps(), 0); } +TEST_F(MergeSingleQubitRotationGatesTest, + HugeFiniteStaticPhasesProduceVerifiedOutput) { + auto qubit = builder.staticQubit(0); + qubit = builder.p(std::numeric_limits::max(), qubit); + qubit = builder.p(std::numeric_limits::max(), qubit); + builder.sink(qubit); + module = builder.finalize(); + ASSERT_TRUE(succeeded(verify(*module))); + + ASSERT_TRUE(succeeded(runMergePass(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + module->walk([&](UnitaryOpInterface operation) { + for (Value parameter : operation.getParameters()) { + const auto constant = mlir::mqt::valueToConstantDouble(parameter); + ASSERT_TRUE(constant); + EXPECT_TRUE(std::isfinite(*constant)); + } + }); +} + /** * @brief Test: R->R should merge into a single U gate (same multi-parameter * type always uses quaternion merge) @@ -879,7 +947,7 @@ TEST_F(MergeSingleQubitRotationGatesTest, numericalSmallAngles) { } /** - * @brief Test: RX(PI)->RY(PI) should merge into U(0, -PI, 0.) + * @brief Test: RX(PI)->RY(PI) should merge into U(0, PI, 0.) */ TEST_F(MergeSingleQubitRotationGatesTest, numericalGimbalLock) { ASSERT_TRUE(testGateMerge({{.type = GateType::RX, .angles = {PI}}, @@ -889,7 +957,7 @@ TEST_F(MergeSingleQubitRotationGatesTest, numericalGimbalLock) { EXPECT_EQ(countOps(), 0); EXPECT_EQ(countOps(), 0); EXPECT_EQ(countOps(), 1); - expectUGateParams(0., -PI, 0.); + expectUGateParams(0., PI, 0.); expectGPhaseParam(1.57079632679490); } @@ -1159,3 +1227,64 @@ TEST_F(MergeSingleQubitRotationGatesTest, EXPECT_NEAR(*phase, 0.0, 1e-6); EXPECT_TRUE(mlir::mqt::isValidGlobalPhaseAngle(*phase)); } + +TEST_F(MergeSingleQubitRotationGatesTest, + DynamicMaxPhasesRemainFiniteAfterFusion) { + auto q = builder.allocQubitRegister(1); + q[0] = builder.p(0.3, q[0]); + q[0] = builder.p(0.4, q[0]); + module = builder.finalize(); + + auto funcOp = cast(module->getBody()->front()); + const auto f64 = Float64Type::get(&context); + funcOp.insertArgument(0, f64, {}, funcOp.getLoc()); + funcOp.insertArgument(1, f64, {}, funcOp.getLoc()); + SmallVector phases; + module->walk([&](POp op) { phases.emplace_back(op); }); + ASSERT_EQ(phases.size(), 2U); + phases[0].getThetaMutable().assign(funcOp.getArgument(0)); + phases[1].getThetaMutable().assign(funcOp.getArgument(1)); + + ASSERT_TRUE(succeeded(runMergePass(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + EXPECT_GT(countOps(), 0); + + constexpr double max = std::numeric_limits::max(); + bindLeadingArgs(funcOp, {max, max}); + module->walk([&](UnitaryOpInterface operation) { + for (Value parameter : operation.getParameters()) { + const auto constant = mlir::mqt::valueToConstantDouble(parameter); + ASSERT_TRUE(constant); + EXPECT_TRUE(std::isfinite(*constant)); + } + }); + module->walk([&](GPhaseOp phase) { + const auto constant = + mlir::mqt::valueToConstantDouble(phase.getParameter(0)); + ASSERT_TRUE(constant); + EXPECT_TRUE(mlir::mqt::isValidGlobalPhaseAngle(*constant)); + }); +} + +TEST_F(MergeSingleQubitRotationGatesTest, StopsAtFunctionCallBoundary) { + constexpr StringLiteral source = R"mlir( +module { + func.func private @opaque(!qco.qubit) -> !qco.qubit + func.func @main() attributes {mqt.entry_point} { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.h %q0 : !qco.qubit -> !qco.qubit + %q2 = func.call @opaque(%q1) : (!qco.qubit) -> !qco.qubit + %q3 = qco.x %q2 : !qco.qubit -> !qco.qubit + qco.sink %q3 : !qco.qubit + return + } +} +)mlir"; + module = parseSourceString(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + EXPECT_TRUE(succeeded(runMergePass(*module))); + EXPECT_TRUE(succeeded(verify(*module))); + EXPECT_EQ(countOps(), 1); +} diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_pauli_twirling.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_pauli_twirling.cpp index b8f395f620..24ae55362a 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_pauli_twirling.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_pauli_twirling.cpp @@ -18,16 +18,19 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -49,7 +52,8 @@ class PauliTwirlingTest : public testing::Test { void SetUp() override { DialectRegistry registry; - registry.insert(); + registry.insert(); context.appendDialectRegistry(registry); context.loadAllAvailableDialects(); builder.initialize(); @@ -130,6 +134,54 @@ TEST_F(PauliTwirlingTest, SameSeedProducesSameProgram) { EXPECT_EQ(print(*first), print(*second)); } +TEST_F(PauliTwirlingTest, TwirlsGateInsideDeepNonModifierRegions) { + constexpr size_t depth = 256; + std::string source = R"mlir( +module { + func.func @main() { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit +)mlir"; + for (size_t i = 0; i < depth; ++i) { + source += " scf.execute_region {\n"; + } + source += R"mlir( + %out0, %out1 = qco.ecr %q0, %q1 : !qco.qubit, !qco.qubit + -> !qco.qubit, !qco.qubit + qco.sink %out0 : !qco.qubit + qco.sink %out1 : !qco.qubit +)mlir"; + for (size_t i = 0; i < depth; ++i) { + source += " scf.yield\n }\n"; + } + source += R"mlir( + return + } +} +)mlir"; + + auto module = parseSourceString(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + auto function = module->lookupSymbol("main"); + ASSERT_TRUE(function); + auto execute = *function.getOps().begin(); + for (size_t i = 1; i < depth; ++i) { + execute = + *execute.getRegion().front().getOps().begin(); + } + Block* innermostBlock = &execute.getRegion().front(); + + ASSERT_TRUE(succeeded(runPass(*module, 42))); + ASSERT_TRUE(succeeded(verify(*module))); + + const auto pauliCount = llvm::count_if(*innermostBlock, [](Operation& op) { + return isa(op); + }); + EXPECT_EQ(pauliCount, 4); +} + TEST_F(PauliTwirlingTest, PreservesExistingPhaseWhenRewriting) { builder.gphase(0.25); auto module = buildGate(GateKind::CX); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_replace_classical_controls.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_replace_classical_controls.cpp index f1a2554712..5b984a0d5a 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_replace_classical_controls.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_replace_classical_controls.cpp @@ -18,11 +18,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -102,6 +104,221 @@ class QCOReplaceClassicalControlsRZZTest } // namespace +TEST_F(QCOReplaceClassicalControlsTest, HandlesUnusedMeasurementOutput) { + auto input = parseSourceString(R"mlir( + module { + func.func @main() { + %q = qco.static 0 : !qco.qubit + %unused, %result = qco.measure %q : !qco.qubit + return + } + } + )mlir", + &context); + ASSERT_TRUE(input); + ASSERT_TRUE(succeeded(verify(*input))); + EXPECT_TRUE(succeeded(runReplaceClassicalControlsPass(*input))); + EXPECT_TRUE(succeeded(verify(*input))); +} + +TEST_F(QCOReplaceClassicalControlsTest, HandlesUnusedControlledPhaseOutput) { + auto input = parseSourceString(R"mlir( + module { + func.func @main() { + %control = qco.static 0 : !qco.qubit + %target = qco.static 1 : !qco.qubit + %measured, %result = qco.measure %target : !qco.qubit + %unused, %target_out = qco.ctrl(%control) + targets(%arg = %measured) { + %body = qco.z %arg : !qco.qubit -> !qco.qubit + qco.yield %body : !qco.qubit + } : ({!qco.qubit}, {!qco.qubit}) + -> ({!qco.qubit}, {!qco.qubit}) + qco.sink %target_out : !qco.qubit + return + } + } + )mlir", + &context); + ASSERT_TRUE(input); + ASSERT_TRUE(succeeded(verify(*input))); + EXPECT_TRUE(succeeded(runReplaceClassicalControlsPass(*input))); + EXPECT_TRUE(succeeded(verify(*input))); +} + +TEST_F(QCOReplaceClassicalControlsTest, + AllMeasuredFastPathsPreserveClassicalBodyCalls) { + programBuilder.initialize(); + + auto rzControl = programBuilder.h(programBuilder.allocQubit()); + auto rzTarget = programBuilder.h(programBuilder.allocQubit()); + std::tie(rzControl, std::ignore) = programBuilder.measure(rzControl); + std::tie(rzTarget, std::ignore) = programBuilder.measure(rzTarget); + std::tie(rzControl, rzTarget) = + programBuilder.crz(0.789, rzControl, rzTarget); + programBuilder.sink(rzControl); + programBuilder.sink(rzTarget); + + auto rzzControl = programBuilder.h(programBuilder.allocQubit()); + auto rzzTarget0 = programBuilder.h(programBuilder.allocQubit()); + auto rzzTarget1 = programBuilder.h(programBuilder.allocQubit()); + std::tie(rzzControl, std::ignore) = programBuilder.measure(rzzControl); + std::tie(rzzTarget0, std::ignore) = programBuilder.measure(rzzTarget0); + std::tie(rzzTarget1, std::ignore) = programBuilder.measure(rzzTarget1); + auto [rzzControlOut, rzzTargetsOut] = + programBuilder.crzz(0.789, rzzControl, rzzTarget0, rzzTarget1); + programBuilder.sink(rzzControlOut); + programBuilder.sink(rzzTargetsOut.first); + programBuilder.sink(rzzTargetsOut.second); + program = programBuilder.finalize(); + ASSERT_TRUE(program); + + OpBuilder moduleBuilder(&context); + moduleBuilder.setInsertionPointToStart(program->getBody()); + auto observe = + func::FuncOp::create(moduleBuilder, program->getLoc(), "observe", + moduleBuilder.getFunctionType({}, {})); + observe.setPrivate(); + + size_t controls = 0; + program->walk([&](CtrlOp ctrl) { + ++controls; + OpBuilder bodyBuilder(&context); + bodyBuilder.setInsertionPointToStart(ctrl.getBody()); + func::CallOp::create(bodyBuilder, ctrl.getLoc(), "observe", TypeRange{}, + ValueRange{}); + }); + ASSERT_EQ(controls, 2U); + ASSERT_TRUE(succeeded(verify(*program))); + + ASSERT_TRUE(runReplaceClassicalControlsPass(*program).succeeded()); + ASSERT_TRUE(succeeded(verify(*program))); + size_t calls = 0; + program->walk([&](func::CallOp) { ++calls; }); + EXPECT_EQ(calls, 2U); +} + +TEST_F(QCOReplaceClassicalControlsTest, + PartialMeasuredRZRefusesUnsafeSupportingOpHoist) { + auto input = parseSourceString(R"mlir( + module { + func.func private @observe() + + func.func @main() { + %theta = arith.constant 0.789 : f64 + %control = qco.static 0 : !qco.qubit + %target = qco.static 1 : !qco.qubit + %measured, %result = qco.measure %target : !qco.qubit + %control_out, %target_out = qco.ctrl(%control) + targets(%arg = %measured) { + %body = qco.rz(%theta) %arg : !qco.qubit -> !qco.qubit + func.call @observe() : () -> () + qco.yield %body : !qco.qubit + } : ({!qco.qubit}, {!qco.qubit}) + -> ({!qco.qubit}, {!qco.qubit}) + qco.sink %control_out : !qco.qubit + qco.sink %target_out : !qco.qubit + return + } + } + )mlir", + &context); + ASSERT_TRUE(input); + ASSERT_TRUE(succeeded(verify(*input))); + + SmallVector controls; + input->walk([&](CtrlOp op) { controls.push_back(op); }); + ASSERT_EQ(controls.size(), 1U); + Operation* originalControl = controls.front().getOperation(); + auto& originalBody = *controls.front().getBody(); + auto originalRotations = llvm::to_vector(originalBody.getOps()); + auto originalCalls = llvm::to_vector(originalBody.getOps()); + ASSERT_EQ(originalRotations.size(), 1U); + ASSERT_EQ(originalCalls.size(), 1U); + Operation* originalRotation = originalRotations.front().getOperation(); + Operation* originalCall = originalCalls.front().getOperation(); + + ASSERT_TRUE(succeeded(runReplaceClassicalControlsPass(*input))); + ASSERT_TRUE(succeeded(verify(*input))); + + controls.clear(); + input->walk([&](CtrlOp op) { controls.push_back(op); }); + ASSERT_EQ(controls.size(), 1U); + EXPECT_EQ(controls.front().getOperation(), originalControl); + + auto& body = *controls.front().getBody(); + auto rotations = llvm::to_vector(body.getOps()); + auto calls = llvm::to_vector(body.getOps()); + ASSERT_EQ(rotations.size(), 1U); + ASSERT_EQ(calls.size(), 1U); + EXPECT_EQ(rotations.front().getOperation(), originalRotation); + EXPECT_EQ(calls.front().getOperation(), originalCall); + EXPECT_TRUE(rotations.front()->isBeforeInBlock(calls.front())); + EXPECT_TRUE(calls.front()->isBeforeInBlock(body.getTerminator())); +} + +TEST_F(QCOReplaceClassicalControlsTest, + PartialMeasuredRZZRefusesUnsafeSupportingOpHoist) { + auto input = parseSourceString(R"mlir( + module { + func.func private @observe() + + func.func @main() { + %theta = arith.constant 0.789 : f64 + %control = qco.static 0 : !qco.qubit + %target0 = qco.static 1 : !qco.qubit + %target1 = qco.static 2 : !qco.qubit + %measured, %result = qco.measure %target0 : !qco.qubit + %control_out, %target0_out, %target1_out = qco.ctrl(%control) + targets(%arg0 = %measured, %arg1 = %target1) { + %body0, %body1 = qco.rzz(%theta) %arg0, %arg1 + : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit + func.call @observe() : () -> () + qco.yield %body0, %body1 : !qco.qubit, !qco.qubit + } : ({!qco.qubit}, {!qco.qubit, !qco.qubit}) + -> ({!qco.qubit}, {!qco.qubit, !qco.qubit}) + qco.sink %control_out : !qco.qubit + qco.sink %target0_out : !qco.qubit + qco.sink %target1_out : !qco.qubit + return + } + } + )mlir", + &context); + ASSERT_TRUE(input); + ASSERT_TRUE(succeeded(verify(*input))); + + SmallVector controls; + input->walk([&](CtrlOp op) { controls.push_back(op); }); + ASSERT_EQ(controls.size(), 1U); + Operation* originalControl = controls.front().getOperation(); + auto& originalBody = *controls.front().getBody(); + auto originalRotations = llvm::to_vector(originalBody.getOps()); + auto originalCalls = llvm::to_vector(originalBody.getOps()); + ASSERT_EQ(originalRotations.size(), 1U); + ASSERT_EQ(originalCalls.size(), 1U); + Operation* originalRotation = originalRotations.front().getOperation(); + Operation* originalCall = originalCalls.front().getOperation(); + + ASSERT_TRUE(succeeded(runReplaceClassicalControlsPass(*input))); + ASSERT_TRUE(succeeded(verify(*input))); + + controls.clear(); + input->walk([&](CtrlOp op) { controls.push_back(op); }); + ASSERT_EQ(controls.size(), 1U); + EXPECT_EQ(controls.front().getOperation(), originalControl); + + auto& body = *controls.front().getBody(); + auto rotations = llvm::to_vector(body.getOps()); + auto calls = llvm::to_vector(body.getOps()); + ASSERT_EQ(rotations.size(), 1U); + ASSERT_EQ(calls.size(), 1U); + EXPECT_EQ(rotations.front().getOperation(), originalRotation); + EXPECT_EQ(calls.front().getOperation(), originalCall); + EXPECT_TRUE(rotations.front()->isBeforeInBlock(calls.front())); + EXPECT_TRUE(calls.front()->isBeforeInBlock(body.getTerminator())); +} + TEST(QCOClassicalControlPhaseIdentityTest, controlledRZAndRZZRewritesPreserveBasisPhases) { const auto expectSamePhase = [](const double actualExponent, diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_quantum_loop_unroll.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_quantum_loop_unroll.cpp index 5380b33cdd..b6bfb1f6d6 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_quantum_loop_unroll.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_quantum_loop_unroll.cpp @@ -26,12 +26,16 @@ #include #include #include +#include +#include #include #include #include +#include #include #include +#include #include using namespace mlir; @@ -109,6 +113,120 @@ TEST_F(QuantumLoopUnrollTest, InvalidUnrollFactor) { ASSERT_TRUE(res.failed()); } +TEST_F(QuantumLoopUnrollTest, ExcessiveExplicitFactorFailureIsAtomic) { + auto module = getGHZ(context.get(), 2); + OwningOpRef original(module->clone()); + + EXPECT_TRUE( + failed(runPass(module, QuantumLoopUnrollOptions{.unrollFactor = 4097}))); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST_F(QuantumLoopUnrollTest, + ExcessiveExplicitFactorRejectsIdentityLoopWithoutMutation) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() attributes {mqt.entry_point} { + %q = qco.static 0 : !qco.qubit + %lb = arith.constant 0 : index + %ub = arith.constant 2 : index + %step = arith.constant 1 : index + %out = scf.for %iv = %lb to %ub step %step + iter_args(%arg = %q) -> (!qco.qubit) { + scf.yield %arg : !qco.qubit + } + qco.sink %out : !qco.qubit + return + } +} +)mlir"; + + auto module = parseSourceString(source, context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + + EXPECT_TRUE( + failed(runPass(module, QuantumLoopUnrollOptions{.unrollFactor = 4097}))); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST_F(QuantumLoopUnrollTest, ExcessiveStaticTripCountFailureIsAtomic) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() attributes {mqt.entry_point} { + %q = qco.static 0 : !qco.qubit + %lb = arith.constant 0 : index + %ub = arith.constant 4097 : index + %step = arith.constant 1 : index + %out = scf.for %iv = %lb to %ub step %step + iter_args(%arg = %q) -> (!qco.qubit) { + %next = qco.x %arg : !qco.qubit -> !qco.qubit + scf.yield %next : !qco.qubit + } + qco.sink %out : !qco.qubit + return + } +} +)mlir"; + + auto module = parseSourceString(source, context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + + EXPECT_TRUE(failed(runPass(module, QuantumLoopUnrollOptions{}))); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST_F(QuantumLoopUnrollTest, NestedExpansionBudgetFailureIsAtomic) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() attributes {mqt.entry_point} { + %q = qco.static 0 : !qco.qubit + %lb = arith.constant 0 : index + %ub = arith.constant 400 : index + %step = arith.constant 1 : index + %out = scf.for %outer = %lb to %ub step %step + iter_args(%outer_arg = %q) -> (!qco.qubit) { + %inner_out = scf.for %inner = %lb to %ub step %step + iter_args(%inner_arg = %outer_arg) -> (!qco.qubit) { + %next = qco.x %inner_arg : !qco.qubit -> !qco.qubit + scf.yield %next : !qco.qubit + } + scf.yield %inner_out : !qco.qubit + } + qco.sink %out : !qco.qubit + return + } +} +)mlir"; + + auto module = parseSourceString(source, context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + + EXPECT_TRUE(failed(runPass(module, QuantumLoopUnrollOptions{}))); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST_F(QuantumLoopUnrollTest, DeclaresMaterializedArithDependency) { + auto pass = createQuantumLoopUnroll({}); + DialectRegistry registry; + pass->getDependentDialects(registry); + EXPECT_TRUE( + registry.getDialectAllocator(arith::ArithDialect::getDialectNamespace())); +} + TEST_F(QuantumLoopUnrollTest, NoOp) { auto m = getGHZ(context.get(), 2); auto mClone = m->clone(); @@ -120,6 +238,122 @@ TEST_F(QuantumLoopUnrollTest, NoOp) { mlir::OperationEquivalence::Flags::None)); } +TEST_F(QuantumLoopUnrollTest, PreservesYieldOnlyPermutation) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main() attributes {mqt.entry_point} { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %lb = arith.constant 0 : index + %ub = arith.constant 1 : index + %step = arith.constant 1 : index + %out0, %out1 = scf.for %iv = %lb to %ub step %step + iter_args(%left = %q0, %right = %q1) + -> (!qco.qubit, !qco.qubit) { + scf.yield %right, %left : !qco.qubit, !qco.qubit + } + qco.sink %out0 : !qco.qubit + qco.sink %out1 : !qco.qubit + return + } +} +)mlir"; + + auto module = parseSourceString(source, context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + ASSERT_TRUE(succeeded(runPass(module, QuantumLoopUnrollOptions{}))); + ASSERT_TRUE(succeeded(verify(*module))); + + SmallVector sinks; + module->walk([&](SinkOp sink) { sinks.push_back(sink); }); + ASSERT_EQ(sinks.size(), 2); + auto first = sinks[0].getQubit().getDefiningOp(); + auto second = sinks[1].getQubit().getDefiningOp(); + ASSERT_TRUE(first); + ASSERT_TRUE(second); + EXPECT_EQ(first.getIndex(), 1); + EXPECT_EQ(second.getIndex(), 0); +} + +TEST_F(QuantumLoopUnrollTest, DynamicTripCountFailureIsAtomic) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main(%upper: index) attributes {mqt.entry_point} { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %lb = arith.constant 0 : index + %static_ub = arith.constant 2 : index + %step = arith.constant 1 : index + %out0 = scf.for %iv = %lb to %static_ub step %step + iter_args(%q = %q0) -> (!qco.qubit) { + %next = qco.x %q : !qco.qubit -> !qco.qubit + scf.yield %next : !qco.qubit + } + %out1 = scf.for %iv = %lb to %upper step %step + iter_args(%q = %q1) -> (!qco.qubit) { + %next = qco.h %q : !qco.qubit -> !qco.qubit + scf.yield %next : !qco.qubit + } + qco.sink %out0 : !qco.qubit + qco.sink %out1 : !qco.qubit + return + } +} +)mlir"; + + auto module = parseSourceString(source, context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + + EXPECT_TRUE(failed(runPass(module, QuantumLoopUnrollOptions{}))); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + +TEST_F(QuantumLoopUnrollTest, DeepRegionNestingFailureIsAtomic) { + constexpr size_t depth = 64; + std::string source = R"mlir( +module { + func.func @main() { + %q = qco.static 0 : !qco.qubit + %lb = arith.constant 0 : index + %ub = arith.constant 1 : index + %step = arith.constant 1 : index +)mlir"; + for (size_t i = 0; i < depth; ++i) { + source += " scf.execute_region {\n"; + } + source += R"mlir( + %out = scf.for %iv = %lb to %ub step %step + iter_args(%arg = %q) -> (!qco.qubit) { + %next = qco.x %arg : !qco.qubit -> !qco.qubit + scf.yield %next : !qco.qubit + } + qco.sink %out : !qco.qubit +)mlir"; + for (size_t i = 0; i < depth; ++i) { + source += " scf.yield\n }\n"; + } + source += R"mlir( + return + } +} +)mlir"; + + auto module = parseSourceString(source, context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + OwningOpRef original(module->clone()); + + EXPECT_TRUE(failed(runPass(module, QuantumLoopUnrollOptions{}))); + EXPECT_TRUE(OperationEquivalence::isEquivalentTo( + module->getOperation(), original->getOperation(), + OperationEquivalence::Flags::None)); +} + TEST_F(QuantumLoopUnrollTest, UnrollFull) { auto m = getGHZ(context.get(), 3); auto entry = *(m->getOps().begin()); @@ -139,6 +373,40 @@ TEST_F(QuantumLoopUnrollTest, UnrollFull) { EXPECT_EQ(range_size(entry.getOps()), 5); } +TEST_F(QuantumLoopUnrollTest, UnrollsFunctionWithSiblingSymbolReference) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func private @helper() + func.func @main() attributes {mqt.entry_point} { + %q = qco.static 0 : !qco.qubit + %lb = arith.constant 0 : index + %ub = arith.constant 2 : index + %step = arith.constant 1 : index + %out = scf.for %iv = %lb to %ub step %step + iter_args(%arg = %q) -> (!qco.qubit) { + func.call @helper() : () -> () + %next = qco.x %arg : !qco.qubit -> !qco.qubit + scf.yield %next : !qco.qubit + } + qco.sink %out : !qco.qubit + return + } +} +)mlir"; + + auto module = parseSourceString(source, context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + ASSERT_TRUE(succeeded(runPass(module, QuantumLoopUnrollOptions{}))); + ASSERT_TRUE(succeeded(verify(*module))); + + auto main = module->lookupSymbol("main"); + ASSERT_TRUE(main); + EXPECT_TRUE(main.getOps().empty()); + EXPECT_EQ(llvm::range_size(main.getOps()), 2U); +} + TEST_F(QuantumLoopUnrollTest, UnrollFullWithOuterDependentBounds) { auto m = QCOProgramBuilder::build(context.get(), [](QCOProgramBuilder& b) { auto tensor = b.qtensorAlloc(2); diff --git a/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp b/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp index 72bda4816f..20b713fc9c 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -513,6 +515,43 @@ TEST_F(QCODDFunctionalityTest, RejectsUnmappedReturnedQubit) { failed(simulate(mainFunc(*mod), dd::makeZeroState(1, *dd), *dd, rng))); } +TEST_F(QCODDFunctionalityTest, RejectsUnrepresentableStaticQubitIndex) { + expectMlirFails(1, R"mlir( + module { + func.func @main() { + %q = qco.static 4294967296 : !qco.qubit + qco.sink %q : !qco.qubit + return + } + } + )mlir"); +} + +TEST_F(QCODDFunctionalityTest, RejectsDuplicateStaticQubitIndices) { + expectMlirFails(1, R"mlir( + module { + func.func @main() { + %a = qco.static 0 : !qco.qubit + %b = qco.static 0 : !qco.qubit + qco.sink %a : !qco.qubit + qco.sink %b : !qco.qubit + return + } + } + )mlir"); +} + +TEST_F(QCODDFunctionalityTest, RejectsExcessiveClassicalRegisterCapacity) { + expectMlirFails(1, R"mlir( + module { + func.func @main() { + %c = cbit.alloc(#cbit.init) : !cbit.reg<1073741824> + return + } + } + )mlir"); +} + TEST_F(QCODDFunctionalityTest, SimulationConsumesInputReference) { auto valid = buildModule([](QCOProgramBuilder& b) { auto q = b.x(b.staticQubit(0)); @@ -1137,6 +1176,66 @@ TEST_F(QCODDFunctionalityTest, EmbedsWideLocalMatrixWithoutRegisterLimit) { expectEqualToQc(mainFunc(*mod), qc); } +TEST_F(QCODDFunctionalityTest, RejectsExcessiveRegionNesting) { + auto mod = buildModule([](QCOProgramBuilder& b) { + auto qubit = b.staticQubit(0); + std::function nest = [&](size_t depth, + Value value) -> Value { + if (depth == 0) { + return b.x(value); + } + return b.qcoIf( + true, value, + [&](Value argument) { return nest(depth - 1, argument); }, + [](Value argument) { return argument; }); + }; + qubit = nest(64, qubit); + b.sink(qubit); + return b.intConstant(0); + }); + ASSERT_TRUE(mod); + auto dd = std::make_unique(1); + + EXPECT_TRUE(failed(buildFunctionality(mainFunc(*mod), *dd))); +} + +TEST_F(QCODDFunctionalityTest, RejectsUnmappedClassicalOperations) { + for (const StringRef source : { + R"mlir(module { + func.func @main(%c: i1) { + %q = qco.static 0 : !qco.qubit + %bad = arith.index_castui %c : i1 to index + qco.sink %q : !qco.qubit + return + } + })mlir", + R"mlir(module { + func.func @main(%unmapped: i1) { + %q = qco.static 0 : !qco.qubit + %true = arith.constant true + %bad = arith.andi %unmapped, %true : i1 + qco.sink %q : !qco.qubit + return + } + })mlir", + R"mlir(module { + func.func @main(%unmapped: index) { + %q = qco.static 0 : !qco.qubit + %one = arith.constant 1 : index + %bad = arith.ori %unmapped, %one : index + qco.sink %q : !qco.qubit + return + } + })mlir"}) { + auto mod = parseSourceString(source, context.get()); + ASSERT_TRUE(mod); + auto dd = std::make_unique(1); + std::mt19937_64 rng(1); + EXPECT_TRUE( + failed(simulate(mainFunc(*mod), dd::makeZeroState(1, *dd), *dd, rng))); + } +} + TEST_F(QCODDFunctionalityTest, RejectsUnmappedClassicalControl) { for (const StringRef source : {R"mlir( module { @@ -1254,6 +1353,130 @@ TEST_F(QCODDFunctionalityTest, BindsClassicalIndexResults) { dd->decRef(expected); } +TEST_F(QCODDFunctionalityTest, RejectsUnmappedClassicalRegionResults) { + for (const StringRef source : { + R"mlir(module { + func.func @main(%unmapped: i1) { + %q = qco.static 0 : !qco.qubit + %true = arith.constant true + %result, %out = qco.if %true args(%arg = %q) + -> (i1, !qco.qubit) { + qco.yield %unmapped, %arg : i1, !qco.qubit + } else args(%arg = %q) { + qco.yield %true, %arg : i1, !qco.qubit + } + qco.sink %out : !qco.qubit + return + } + })mlir", + R"mlir(module { + func.func @main(%unmapped: index) { + %q = qco.static 0 : !qco.qubit + %true = arith.constant true + %zero = arith.constant 0 : index + %result, %out = qco.if %true args(%arg = %q) + -> (index, !qco.qubit) { + qco.yield %unmapped, %arg : index, !qco.qubit + } else args(%arg = %q) { + qco.yield %zero, %arg : index, !qco.qubit + } + qco.sink %out : !qco.qubit + return + } + })mlir"}) { + auto mod = parseSourceString(source, context.get()); + ASSERT_TRUE(mod); + auto dd = std::make_unique(1); + std::mt19937_64 rng(1); + EXPECT_TRUE( + failed(simulate(mainFunc(*mod), dd::makeZeroState(1, *dd), *dd, rng))); + } +} + +TEST_F(QCODDFunctionalityTest, RejectsUnmappedLinearRegionValues) { + for (const StringRef source : { + R"mlir(module { + func.func @main(%unmapped: !qco.qubit) { + %q = qco.static 0 : !qco.qubit + %true = arith.constant true + %out = qco.if %true args(%arg = %unmapped) -> (!qco.qubit) { + qco.yield %arg : !qco.qubit + } else args(%arg = %unmapped) { + qco.yield %arg : !qco.qubit + } + qco.sink %q : !qco.qubit + return + } + })mlir", + R"mlir(module { + func.func @main(%unmapped: !qco.qubit) { + %q = qco.static 0 : !qco.qubit + %true = arith.constant true + %out = qco.if %true args(%arg = %q) -> (!qco.qubit) { + qco.yield %unmapped : !qco.qubit + } else args(%arg = %q) { + qco.yield %arg : !qco.qubit + } + qco.sink %out : !qco.qubit + return + } + })mlir"}) { + auto mod = parseSourceString(source, context.get()); + ASSERT_TRUE(mod); + auto dd = std::make_unique(1); + std::mt19937_64 rng(1); + EXPECT_TRUE( + failed(simulate(mainFunc(*mod), dd::makeZeroState(1, *dd), *dd, rng))); + } +} + +TEST_F(QCODDFunctionalityTest, RejectsUnmappedMeasurementAndResetQubits) { + for (const StringRef source : {R"mlir(module { + func.func @main(%unmapped: !qco.qubit) { + %q = qco.static 0 : !qco.qubit + %out, %bit = qco.measure %unmapped : !qco.qubit + qco.sink %q : !qco.qubit + return + } + })mlir", + R"mlir(module { + func.func @main(%unmapped: !qco.qubit) { + %q = qco.static 0 : !qco.qubit + %out = qco.reset %unmapped : !qco.qubit -> !qco.qubit + qco.sink %q : !qco.qubit + return + } + })mlir"}) { + auto mod = parseSourceString(source, context.get()); + ASSERT_TRUE(mod); + auto dd = std::make_unique(1); + std::mt19937_64 rng(1); + EXPECT_TRUE( + failed(simulate(mainFunc(*mod), dd::makeZeroState(1, *dd), *dd, rng))); + } +} + +TEST_F(QCODDFunctionalityTest, RejectsNonlinearQubitsBeforeDDConstruction) { + constexpr StringLiteral source = R"mlir(module { + func.func @main() { + %q = qco.static 0 : !qco.qubit + %first, %second = qco.swap %q, %q + : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit + qco.sink %first : !qco.qubit + qco.sink %second : !qco.qubit + return + } + })mlir"; + auto mod = parseSourceString(source, context.get()); + ASSERT_TRUE(mod); + ASSERT_TRUE(succeeded(verify(*mod))); + + auto dd = std::make_unique(1); + EXPECT_TRUE(failed(buildFunctionality(mainFunc(*mod), *dd))); + EXPECT_TRUE( + failed(simulate(mainFunc(*mod), dd::makeZeroState(1, *dd), *dd, rng))); +} + TEST_F(QCODDFunctionalityTest, Rejects) { { auto mod = buildModule([](QCOProgramBuilder& b) { @@ -1630,6 +1853,34 @@ TEST_F(QCODDFunctionalityTest, RejectsUnsupportedFuncCalls) { expectSimulationFails(mainFunc(*declaration), 1); } +TEST_F(QCODDFunctionalityTest, RejectsExcessiveFuncCallNesting) { + OwningOpRef mod = ModuleOp::create(UnknownLoc::get(context.get())); + OpBuilder builder(context.get()); + const auto qubitType = QubitType::get(context.get()); + const auto functionType = builder.getFunctionType({qubitType}, {qubitType}); + constexpr size_t numFunctions = 66; + + for (size_t i = 0; i < numFunctions; ++i) { + builder.setInsertionPointToEnd(mod->getBody()); + const std::string name = i == 0 ? "main" : "f" + std::to_string(i); + auto function = + func::FuncOp::create(builder, mod->getLoc(), name, functionType); + Block* entry = function.addEntryBlock(); + builder.setInsertionPointToStart(entry); + Value result = entry->getArgument(0); + if (i + 1 < numFunctions) { + result = func::CallOp::create(builder, mod->getLoc(), + "f" + std::to_string(i + 1), qubitType, + ValueRange{result}) + .getResult(0); + } + func::ReturnOp::create(builder, mod->getLoc(), result); + } + + ASSERT_TRUE(succeeded(verify(*mod))); + expectSimulationFails(mainFunc(*mod), 1); +} + TEST_F(QCODDFunctionalityTest, HandlesScfForBounds) { for (const auto [lower, upper, step, succeeds] : {std::tuple{3, 3, 1, true}, @@ -1748,6 +1999,32 @@ TEST_F(QCODDFunctionalityTest, ScfForSharesExecutionBudget) { EXPECT_TRUE(dd->getRootSet().empty()); } +TEST_F(QCODDFunctionalityTest, FuncCallsShareExecutionBudgetWithScfFor) { + auto mod = parseSourceString(R"mlir( + module { + func.func @identity(%q: !qco.qubit) -> !qco.qubit { + return %q : !qco.qubit + } + func.func @main() { + %q = qco.static 0 : !qco.qubit + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %c5001 = arith.constant 5001 : index + %out = scf.for %iv = %c0 to %c5001 step %c1 + iter_args(%arg = %q) -> !qco.qubit { + %next = func.call @identity(%arg) : (!qco.qubit) -> !qco.qubit + scf.yield %next : !qco.qubit + } + qco.sink %out : !qco.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(mod); + expectSimulationFails(mainFunc(*mod), 1); +} + TEST_F(QCODDFunctionalityTest, SimulateRicherClassicalArithmetic) { auto mod = buildModule([](QCOProgramBuilder& b) { auto q = b.staticQubit(0); diff --git a/mlir/unittests/Dialect/QCO/Utils/test_wireiterator.cpp b/mlir/unittests/Dialect/QCO/Utils/test_wireiterator.cpp index 2d9e0cbc03..7d894456d3 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_wireiterator.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_wireiterator.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -264,6 +265,35 @@ TEST_P(WireIteratorTest, FunctionReturnTerminatesTraversal) { ASSERT_EQ(it.qubit(), output); } +TEST_P(WireIteratorTest, UnknownCarrierTerminatesTraversal) { + OpBuilder builder(context.get()); + const auto location = builder.getUnknownLoc(); + auto module = ModuleOp::create(location); + builder.setInsertionPointToStart(module.getBody()); + auto function = func::FuncOp::create(builder, location, "main", + builder.getFunctionType({}, {})); + Block* body = function.addEntryBlock(); + builder.setInsertionPointToStart(body); + + auto source = qco::AllocOp::create(builder, location).getResult(); + auto carrier = UnrealizedConversionCastOp::create( + builder, location, TypeRange{source.getType()}, ValueRange{source}); + auto carried = carrier.getResult(0); + qco::SinkOp::create(builder, location, carried); + func::ReturnOp::create(builder, location); + + qco::WireIterator forward(source); + ++forward; + EXPECT_EQ(forward.operation(), carrier.getOperation()); + ++forward; + EXPECT_EQ(forward, std::default_sentinel); + + qco::WireIterator backward(carried); + --backward; + EXPECT_EQ(backward.operation(), nullptr); + EXPECT_EQ(backward.qubit(), carried); +} + INSTANTIATE_TEST_SUITE_P(DynamicAndStatic, WireIteratorTest, ::testing::Bool(), [](const ::testing::TestParamInfo& info) { return info.param ? "Dynamic" : "Static"; diff --git a/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp b/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp index 384f0889d3..8d428fb0f0 100644 --- a/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp +++ b/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp @@ -38,7 +38,9 @@ #include #include +#include #include +#include #include #include #include @@ -48,8 +50,7 @@ using namespace mlir; using namespace qir; -static LLVM::ModuleFlagAttr findModuleFlag(ModuleOp moduleOp, - const StringRef name) { +static LLVM::ModuleFlagAttr findModuleFlag(ModuleOp moduleOp, StringRef name) { LLVM::ModuleFlagAttr result; moduleOp->walk([&](LLVM::ModuleFlagsOp flagsOp) { for (const auto flag : @@ -62,6 +63,37 @@ static LLVM::ModuleFlagAttr findModuleFlag(ModuleOp moduleOp, return result; } +static ArrayAttr findPassthroughEntry(LLVM::LLVMFuncOp function, + StringRef name) { + const auto passthrough = function->getAttrOfType("passthrough"); + if (!passthrough) { + return {}; + } + for (const auto attribute : passthrough) { + const auto pair = dyn_cast(attribute); + const auto key = + pair && pair.size() == 2 ? dyn_cast(pair[0]) : StringAttr{}; + if (key && key.getValue() == name) { + return pair; + } + } + return {}; +} + +static size_t countPassthroughEntries(LLVM::LLVMFuncOp function, + StringRef name) { + const auto passthrough = function->getAttrOfType("passthrough"); + if (!passthrough) { + return 0; + } + return llvm::count_if(passthrough, [&](Attribute attribute) { + const auto pair = dyn_cast(attribute); + const auto key = + pair && pair.size() == 2 ? dyn_cast(pair[0]) : StringAttr{}; + return key && key.getValue() == name; + }); +} + namespace { struct QIRTestCase { @@ -180,6 +212,209 @@ TEST_F(QIRTest, BuilderReturnsCompleteClassicalRegister) { EXPECT_FALSE(returnedRegister.array); } +TEST_F(QIRTest, ReusedIrreversibleDeclarationsPreservePassthroughIdempotently) { + OpBuilder builder(context.get()); + const auto location = builder.getUnknownLoc(); + auto module = ModuleOp::create(location); + builder.setInsertionPointToStart(module.getBody()); + const auto ptrType = LLVM::LLVMPointerType::get(context.get()); + const auto voidType = LLVM::LLVMVoidType::get(context.get()); + const auto nounwind = builder.getStringAttr("nounwind"); + const auto targetCPU = builder.getStrArrayAttr({"target-cpu", "generic"}); + + for (const StringRef name : {StringRef(QIR_MEASURE), StringRef(QIR_RESET)}) { + const SmallVector parameters(name == QIR_MEASURE ? 2 : 1, ptrType); + const auto functionType = LLVM::LLVMFunctionType::get(voidType, parameters); + auto declaration = + LLVM::LLVMFuncOp::create(builder, location, name, functionType); + declaration->setAttr("passthrough", + builder.getArrayAttr({nounwind, targetCPU})); + + EXPECT_EQ( + getOrCreateFunctionDeclaration(builder, module, name, functionType), + declaration); + EXPECT_EQ( + getOrCreateFunctionDeclaration(builder, module, name, functionType), + declaration); + + const auto passthrough = + declaration->getAttrOfType("passthrough"); + ASSERT_TRUE(passthrough); + ASSERT_EQ(passthrough.size(), 3U); + EXPECT_EQ(passthrough[0], nounwind); + EXPECT_EQ(passthrough[1], targetCPU); + EXPECT_EQ(llvm::count(passthrough, builder.getStringAttr("irreversible")), + 1); + } +} + +TEST_F(QIRTest, MetadataPassRequiresExactlyOneEntryPointAtomically) { + for (const size_t numEntryPoints : {0U, 2U}) { + SCOPED_TRACE(testing::Message() << "numEntryPoints=" << numEntryPoints); + OpBuilder builder(context.get()); + const auto location = builder.getUnknownLoc(); + auto module = ModuleOp::create(location); + builder.setInsertionPointToStart(module.getBody()); + const auto functionType = + LLVM::LLVMFunctionType::get(LLVM::LLVMVoidType::get(context.get()), {}); + for (size_t i = 0; i < std::max(numEntryPoints, 1); ++i) { + auto function = LLVM::LLVMFuncOp::create( + builder, location, "function" + std::to_string(i), functionType); + if (i < numEntryPoints) { + function->setAttr("passthrough", + builder.getStrArrayAttr({"entry_point"})); + } + auto* block = function.addEntryBlock(builder); + builder.setInsertionPointToEnd(block); + LLVM::ReturnOp::create(builder, location, ValueRange{}); + builder.setInsertionPointToEnd(module.getBody()); + } + ASSERT_TRUE(succeeded(verify(module))); + + std::string before; + llvm::raw_string_ostream(before) << module; + PassManager manager(context.get()); + manager.addPass(qir::createQIRSetAttributesAndMetadata({false})); + EXPECT_TRUE(failed(manager.run(module))); + std::string after; + llvm::raw_string_ostream(after) << module; + EXPECT_EQ(after, before); + } +} + +TEST_F(QIRTest, BaseMetadataRejectsDynamicResourcesAtomically) { + OpBuilder builder(context.get()); + const auto location = builder.getUnknownLoc(); + auto module = ModuleOp::create(location); + builder.setInsertionPointToStart(module.getBody()); + const auto ptrType = LLVM::LLVMPointerType::get(context.get()); + const auto allocateType = LLVM::LLVMFunctionType::get(ptrType, {ptrType}); + auto allocate = LLVM::LLVMFuncOp::create(builder, location, QIR_QUBIT_ALLOC, + allocateType); + const auto functionType = + LLVM::LLVMFunctionType::get(LLVM::LLVMVoidType::get(context.get()), {}); + auto helper = + LLVM::LLVMFuncOp::create(builder, location, "helper", functionType); + auto* helperBlock = helper.addEntryBlock(builder); + builder.setInsertionPointToEnd(helperBlock); + auto null = LLVM::ZeroOp::create(builder, location, ptrType); + LLVM::CallOp::create(builder, location, allocate, null.getResult()); + LLVM::ReturnOp::create(builder, location, ValueRange{}); + + builder.setInsertionPointToEnd(module.getBody()); + auto main = LLVM::LLVMFuncOp::create(builder, location, "main", functionType); + main->setAttr("passthrough", builder.getStrArrayAttr({"entry_point"})); + auto* mainBlock = main.addEntryBlock(builder); + builder.setInsertionPointToEnd(mainBlock); + LLVM::CallOp::create(builder, location, helper, ValueRange{}); + LLVM::ReturnOp::create(builder, location, ValueRange{}); + ASSERT_TRUE(succeeded(verify(module))); + + std::string before; + llvm::raw_string_ostream(before) << module; + PassManager manager(context.get()); + manager.addPass(qir::createQIRSetAttributesAndMetadata({false})); + EXPECT_TRUE(failed(manager.run(module))); + std::string after; + llvm::raw_string_ostream(after) << module; + EXPECT_EQ(after, before); +} + +TEST_F(QIRTest, AdaptiveMetadataScansStaticResourcesInHelperFunctions) { + OpBuilder builder(context.get()); + const auto location = builder.getUnknownLoc(); + auto module = ModuleOp::create(location); + builder.setInsertionPointToStart(module.getBody()); + const auto ptrType = LLVM::LLVMPointerType::get(context.get()); + const auto voidType = LLVM::LLVMVoidType::get(context.get()); + const auto measureType = + LLVM::LLVMFunctionType::get(voidType, {ptrType, ptrType}); + auto measure = + LLVM::LLVMFuncOp::create(builder, location, QIR_MEASURE, measureType); + const auto helperType = + LLVM::LLVMFunctionType::get(voidType, {ptrType, ptrType}); + auto helper = + LLVM::LLVMFuncOp::create(builder, location, "helper", helperType); + auto* helperBlock = helper.addEntryBlock(builder); + builder.setInsertionPointToEnd(helperBlock); + LLVM::CallOp::create(builder, location, measure, helperBlock->getArguments()); + LLVM::ReturnOp::create(builder, location, ValueRange{}); + + builder.setInsertionPointToEnd(module.getBody()); + const auto mainType = LLVM::LLVMFunctionType::get(voidType, {}); + auto main = LLVM::LLVMFuncOp::create(builder, location, "main", mainType); + main->setAttr("passthrough", builder.getStrArrayAttr({"entry_point"})); + auto* mainBlock = main.addEntryBlock(builder); + builder.setInsertionPointToEnd(mainBlock); + auto qubitIndex = + LLVM::ConstantOp::create(builder, location, builder.getI64IntegerAttr(4)); + auto qubit = LLVM::IntToPtrOp::create(builder, location, ptrType, + qubitIndex.getResult()); + auto resultIndex = + LLVM::ConstantOp::create(builder, location, builder.getI64IntegerAttr(2)); + auto result = LLVM::IntToPtrOp::create(builder, location, ptrType, + resultIndex.getResult()); + LLVM::CallOp::create(builder, location, helper, ValueRange{qubit, result}); + LLVM::ReturnOp::create(builder, location, ValueRange{}); + ASSERT_TRUE(succeeded(verify(module))); + + PassManager manager(context.get()); + manager.addPass(qir::createQIRSetAttributesAndMetadata({true})); + ASSERT_TRUE(succeeded(manager.run(module))); + const auto requiredQubits = findPassthroughEntry(main, "required_num_qubits"); + const auto requiredResults = + findPassthroughEntry(main, "required_num_results"); + ASSERT_TRUE(requiredQubits); + ASSERT_TRUE(requiredResults); + EXPECT_EQ(cast(requiredQubits[1]).getValue(), "5"); + EXPECT_EQ(cast(requiredResults[1]).getValue(), "3"); + EXPECT_TRUE(findModuleFlag(module, "ir_functions")); +} + +TEST_F(QIRTest, MetadataRejectsMalformedStaticResourcePointersAtomically) { + for (const bool useNonConstant : {false, true}) { + SCOPED_TRACE(testing::Message() << "useNonConstant=" << useNonConstant); + OpBuilder builder(context.get()); + const auto location = builder.getUnknownLoc(); + auto module = ModuleOp::create(location); + builder.setInsertionPointToStart(module.getBody()); + const auto ptrType = LLVM::LLVMPointerType::get(context.get()); + const auto voidType = LLVM::LLVMVoidType::get(context.get()); + const auto gateType = LLVM::LLVMFunctionType::get(voidType, {ptrType}); + auto x = LLVM::LLVMFuncOp::create(builder, location, QIR_X, gateType); + SmallVector mainArguments; + if (useNonConstant) { + mainArguments.emplace_back(builder.getI64Type()); + } + const auto mainType = LLVM::LLVMFunctionType::get(voidType, mainArguments); + auto main = LLVM::LLVMFuncOp::create(builder, location, "main", mainType); + main->setAttr("passthrough", builder.getStrArrayAttr({"entry_point"})); + auto* block = main.addEntryBlock(builder); + builder.setInsertionPointToEnd(block); + Value index; + if (useNonConstant) { + index = block->getArgument(0); + } else { + index = LLVM::ConstantOp::create(builder, location, + builder.getI64IntegerAttr(-1)) + .getResult(); + } + auto qubit = LLVM::IntToPtrOp::create(builder, location, ptrType, index); + LLVM::CallOp::create(builder, location, x, qubit.getResult()); + LLVM::ReturnOp::create(builder, location, ValueRange{}); + ASSERT_TRUE(succeeded(verify(module))); + + std::string before; + llvm::raw_string_ostream(before) << module; + PassManager manager(context.get()); + manager.addPass(qir::createQIRSetAttributesAndMetadata({false})); + EXPECT_TRUE(failed(manager.run(module))); + std::string after; + llvm::raw_string_ostream(after) << module; + EXPECT_EQ(after, before); + } +} + TEST_F(QIRTest, AdaptiveBuilderSelectsControlledSpecializationsByArity) { auto module = QIRProgramBuilder::build( context.get(), @@ -227,10 +462,10 @@ TEST_F(QIRTest, BaseBuilderUsesGenericSpecializationForThreeControls) { auto module = QIRProgramBuilder::build( context.get(), [](QIRProgramBuilder& builder) { - auto control0 = builder.staticQubit(0); - auto control1 = builder.staticQubit(1); - auto control2 = builder.staticQubit(2); - auto target = builder.staticQubit(3); + auto control0 = builder.staticQubit(2); + auto control1 = builder.staticQubit(4); + auto control2 = builder.staticQubit(7); + auto target = builder.staticQubit(11); builder.mcrx(0.25, {control0, control1, control2}, target); return builder.intConstant(0); }, @@ -247,6 +482,467 @@ TEST_F(QIRTest, BaseBuilderUsesGenericSpecializationForThreeControls) { ArrayAttr::get(context.get(), {StringAttr::get(context.get(), "qir_profiles"), StringAttr::get(context.get(), "base_profile")}))); + const auto required = findPassthroughEntry(main, "required_num_qubits"); + ASSERT_TRUE(required); + EXPECT_EQ(cast(required[1]).getValue(), "12"); +} + +TEST_F(QIRTest, SparseStaticQubitIdsSetRequiredCapacity) { + auto module = QIRProgramBuilder::build( + context.get(), + [](QIRProgramBuilder& builder) { + auto qubit = builder.staticQubit(2); + builder.x(qubit); + return builder.intConstant(0); + }, + QIRProgramBuilder::Profile::Base); + + ASSERT_TRUE(module); + auto main = getMainFunction(module.get()); + ASSERT_TRUE(main); + const auto required = findPassthroughEntry(main, "required_num_qubits"); + ASSERT_TRUE(required); + EXPECT_EQ(cast(required[1]).getValue(), "3"); +} + +TEST_F(QIRTest, ScansAllIntegerToPointerUsersForStaticQubits) { + OpBuilder builder(context.get()); + const auto location = builder.getUnknownLoc(); + auto module = ModuleOp::create(location); + builder.setInsertionPointToStart(module.getBody()); + const auto ptrType = LLVM::LLVMPointerType::get(context.get()); + const auto voidType = LLVM::LLVMVoidType::get(context.get()); + const auto gateType = LLVM::LLVMFunctionType::get(voidType, {ptrType}); + auto x = LLVM::LLVMFuncOp::create(builder, location, QIR_X, gateType); + const auto mainType = LLVM::LLVMFunctionType::get(voidType, {}); + auto main = LLVM::LLVMFuncOp::create(builder, location, "main", mainType); + main->setAttr("passthrough", builder.getStrArrayAttr({"entry_point"})); + auto* block = main.addEntryBlock(builder); + builder.setInsertionPointToEnd(block); + auto index = + LLVM::ConstantOp::create(builder, location, builder.getI64IntegerAttr(4)); + auto qubit = + LLVM::IntToPtrOp::create(builder, location, ptrType, index.getResult()); + LLVM::CallOp::create(builder, location, x, qubit.getResult()); + auto distractor = + LLVM::IntToPtrOp::create(builder, location, ptrType, index.getResult()); + LLVM::ReturnOp::create(builder, location, ValueRange{}); + ASSERT_EQ(*index->user_begin(), distractor.getOperation()); + ASSERT_TRUE(succeeded(verify(module))); + + PassManager manager(context.get()); + manager.addPass(qir::createQIRSetAttributesAndMetadata({false})); + ASSERT_TRUE(succeeded(manager.run(module))); + const auto required = findPassthroughEntry(main, "required_num_qubits"); + ASSERT_TRUE(required); + EXPECT_EQ(cast(required[1]).getValue(), "5"); +} + +TEST_F(QIRTest, SparseStaticResultIdsSetRequiredCapacity) { + auto module = QIRProgramBuilder::build( + context.get(), + [](QIRProgramBuilder& builder) { + auto qubit = builder.staticQubit(0); + builder.measure(qubit, 2, false); + return builder.intConstant(0); + }, + QIRProgramBuilder::Profile::Base); + + ASSERT_TRUE(module); + auto main = getMainFunction(module.get()); + ASSERT_TRUE(main); + const auto required = findPassthroughEntry(main, "required_num_results"); + ASSERT_TRUE(required); + EXPECT_EQ(cast(required[1]).getValue(), "3"); +} + +TEST_F(QIRTest, ResultArraysDoNotInflateRequiredQubitCapacity) { + OpBuilder builder(context.get()); + const auto location = builder.getUnknownLoc(); + auto module = ModuleOp::create(location); + builder.setInsertionPointToStart(module.getBody()); + const auto ptrType = LLVM::LLVMPointerType::get(context.get()); + const auto voidType = LLVM::LLVMVoidType::get(context.get()); + auto measure = LLVM::LLVMFuncOp::create( + builder, location, QIR_MEASURE, + LLVM::LLVMFunctionType::get(voidType, {ptrType, ptrType})); + auto arrayCreate = LLVM::LLVMFuncOp::create( + builder, location, QIR_ARRAY_CREATE, + LLVM::LLVMFunctionType::get( + ptrType, {builder.getI32Type(), builder.getI64Type()})); + auto arrayElement = LLVM::LLVMFuncOp::create( + builder, location, QIR_ARRAY_ELEMENT, + LLVM::LLVMFunctionType::get(ptrType, {ptrType, builder.getI64Type()})); + auto main = LLVM::LLVMFuncOp::create( + builder, location, "main", LLVM::LLVMFunctionType::get(voidType, {})); + main->setAttr("passthrough", builder.getStrArrayAttr({"entry_point"})); + auto* block = main.addEntryBlock(builder); + builder.setInsertionPointToEnd(block); + auto qubitIndex = + LLVM::ConstantOp::create(builder, location, builder.getI64IntegerAttr(0)); + auto qubit = LLVM::IntToPtrOp::create(builder, location, ptrType, + qubitIndex.getResult()); + auto resultIndex = + LLVM::ConstantOp::create(builder, location, builder.getI64IntegerAttr(7)); + auto result = LLVM::IntToPtrOp::create(builder, location, ptrType, + resultIndex.getResult()); + LLVM::CallOp::create(builder, location, measure, ValueRange{qubit, result}); + auto elementSize = + LLVM::ConstantOp::create(builder, location, builder.getI32IntegerAttr(8)); + auto arraySize = + LLVM::ConstantOp::create(builder, location, builder.getI64IntegerAttr(1)); + auto array = LLVM::CallOp::create(builder, location, arrayCreate, + ValueRange{elementSize, arraySize}); + auto elementIndex = + LLVM::ConstantOp::create(builder, location, builder.getI64IntegerAttr(0)); + auto element = LLVM::CallOp::create( + builder, location, arrayElement, + ValueRange{array.getResult(), elementIndex.getResult()}); + LLVM::StoreOp::create(builder, location, result.getResult(), + element.getResult()); + LLVM::ReturnOp::create(builder, location, ValueRange{}); + ASSERT_TRUE(succeeded(verify(module))); + + PassManager manager(context.get()); + manager.addPass(qir::createQIRSetAttributesAndMetadata({true})); + ASSERT_TRUE(succeeded(manager.run(module))); + const auto requiredQubits = findPassthroughEntry(main, "required_num_qubits"); + const auto requiredResults = + findPassthroughEntry(main, "required_num_results"); + ASSERT_TRUE(requiredQubits); + ASSERT_TRUE(requiredResults); + EXPECT_EQ(cast(requiredQubits[1]).getValue(), "1"); + EXPECT_EQ(cast(requiredResults[1]).getValue(), "8"); +} + +TEST_F(QIRTest, IgnoresMalformedZeroArgumentRecordDeclarationWithoutCrashing) { + OpBuilder builder(context.get()); + const auto location = builder.getUnknownLoc(); + auto module = ModuleOp::create(location); + builder.setInsertionPointToStart(module.getBody()); + const auto voidType = LLVM::LLVMVoidType::get(context.get()); + const auto functionType = LLVM::LLVMFunctionType::get(voidType, {}); + auto record = LLVM::LLVMFuncOp::create(builder, location, QIR_RECORD_OUTPUT, + functionType); + auto main = LLVM::LLVMFuncOp::create(builder, location, "main", functionType); + main->setAttr("passthrough", builder.getStrArrayAttr({"entry_point"})); + auto* block = main.addEntryBlock(builder); + builder.setInsertionPointToEnd(block); + LLVM::CallOp::create(builder, location, record, ValueRange{}); + LLVM::ReturnOp::create(builder, location, ValueRange{}); + ASSERT_TRUE(succeeded(verify(module))); + + PassManager manager(context.get()); + manager.addPass(qir::createQIRSetAttributesAndMetadata({false})); + ASSERT_TRUE(succeeded(manager.run(module))); + const auto required = findPassthroughEntry(main, "required_num_results"); + ASSERT_TRUE(required); + EXPECT_EQ(cast(required[1]).getValue(), "0"); +} + +TEST_F(QIRTest, PreservesUnrelatedModuleFlags) { + OpBuilder builder(context.get()); + const auto location = builder.getUnknownLoc(); + auto module = ModuleOp::create(location); + builder.setInsertionPointToStart(module.getBody()); + const auto unrelated = + LLVM::ModuleFlagAttr::get(context.get(), LLVM::ModFlagBehavior::Warning, + builder.getStringAttr("Debug Info Version"), + builder.getI32IntegerAttr(3)); + const auto staleQIR = LLVM::ModuleFlagAttr::get( + context.get(), LLVM::ModFlagBehavior::Error, + builder.getStringAttr("qir_major_version"), builder.getI32IntegerAttr(1)); + LLVM::ModuleFlagsOp::create(builder, location, + builder.getArrayAttr({unrelated, staleQIR})); + const auto functionType = + LLVM::LLVMFunctionType::get(LLVM::LLVMVoidType::get(context.get()), {}); + auto main = LLVM::LLVMFuncOp::create(builder, location, "main", functionType); + main->setAttr("passthrough", builder.getStrArrayAttr({"entry_point"})); + auto* block = main.addEntryBlock(builder); + builder.setInsertionPointToEnd(block); + LLVM::ReturnOp::create(builder, location, ValueRange{}); + ASSERT_TRUE(succeeded(verify(module))); + + PassManager manager(context.get()); + manager.addPass(qir::createQIRSetAttributesAndMetadata({false})); + ASSERT_TRUE(succeeded(manager.run(module))); + const auto preserved = findModuleFlag(module, "Debug Info Version"); + ASSERT_TRUE(preserved); + EXPECT_EQ(preserved.getBehavior(), LLVM::ModFlagBehavior::Warning); + EXPECT_EQ(cast(preserved.getValue()).getInt(), 3); + const auto qirMajor = findModuleFlag(module, "qir_major_version"); + ASSERT_TRUE(qirMajor); + EXPECT_EQ(cast(qirMajor.getValue()).getInt(), 2); +} + +TEST_F(QIRTest, PreservesUnrelatedFunctionPassthroughAttributesIdempotently) { + OpBuilder builder(context.get()); + const auto location = builder.getUnknownLoc(); + auto module = ModuleOp::create(location); + builder.setInsertionPointToStart(module.getBody()); + const auto functionType = + LLVM::LLVMFunctionType::get(LLVM::LLVMVoidType::get(context.get()), {}); + auto main = LLVM::LLVMFuncOp::create(builder, location, "main", functionType); + const auto nounwind = builder.getStringAttr("nounwind"); + const auto target = builder.getStrArrayAttr({"target-cpu", "generic"}); + main->setAttr( + "passthrough", + builder.getArrayAttr( + {builder.getStringAttr("entry_point"), nounwind, target, + builder.getStrArrayAttr({"qir_profiles", "stale_profile"})})); + auto* block = main.addEntryBlock(builder); + builder.setInsertionPointToEnd(block); + LLVM::ReturnOp::create(builder, location, ValueRange{}); + ASSERT_TRUE(succeeded(verify(module))); + + const auto runPass = [&] { + PassManager manager(context.get()); + manager.addPass(qir::createQIRSetAttributesAndMetadata({false})); + return manager.run(module); + }; + ASSERT_TRUE(succeeded(runPass())); + auto passthrough = main->getAttrOfType("passthrough"); + ASSERT_TRUE(passthrough); + EXPECT_TRUE(llvm::is_contained(passthrough, nounwind)); + EXPECT_TRUE(llvm::is_contained(passthrough, target)); + EXPECT_EQ(countPassthroughEntries(main, "qir_profiles"), 1U); + const auto afterFirstRun = passthrough; + + ASSERT_TRUE(succeeded(runPass())); + EXPECT_EQ(main->getAttrOfType("passthrough"), afterFirstRun); +} + +TEST_F(QIRTest, ClassifiesUnconditionalBackedgeAsIteration) { + OpBuilder builder(context.get()); + auto module = ModuleOp::create(builder.getUnknownLoc()); + builder.setInsertionPointToStart(module.getBody()); + auto functionType = + LLVM::LLVMFunctionType::get(LLVM::LLVMVoidType::get(context.get()), {}); + auto main = LLVM::LLVMFuncOp::create(builder, builder.getUnknownLoc(), "main", + functionType); + main->setAttr("passthrough", builder.getStrArrayAttr({"entry_point"})); + auto* entry = main.addEntryBlock(builder); + auto* loop = main.addBlock(); + builder.setInsertionPointToEnd(entry); + LLVM::BrOp::create(builder, builder.getUnknownLoc(), loop); + builder.setInsertionPointToEnd(loop); + LLVM::BrOp::create(builder, builder.getUnknownLoc(), loop); + ASSERT_TRUE(succeeded(verify(module))); + + PassManager manager(context.get()); + manager.addPass(qir::createQIRSetAttributesAndMetadata({true})); + ASSERT_TRUE(succeeded(manager.run(module))); + const auto backwardsBranching = findModuleFlag(module, "backwards_branching"); + ASSERT_TRUE(backwardsBranching); + EXPECT_EQ(cast(backwardsBranching.getValue()).getInt(), 1); +} + +TEST_F(QIRTest, ClassifiesMeasurementExitAfterOtherConditional) { + OpBuilder builder(context.get()); + const auto location = builder.getUnknownLoc(); + auto module = ModuleOp::create(location); + builder.setInsertionPointToStart(module.getBody()); + const auto ptrType = LLVM::LLVMPointerType::get(context.get()); + const auto readType = + LLVM::LLVMFunctionType::get(builder.getI1Type(), {ptrType}); + auto readResult = + LLVM::LLVMFuncOp::create(builder, location, QIR_READ_RESULT, readType); + const auto mainType = + LLVM::LLVMFunctionType::get(LLVM::LLVMVoidType::get(context.get()), {}); + auto main = LLVM::LLVMFuncOp::create(builder, location, "main", mainType); + main->setAttr("passthrough", builder.getStrArrayAttr({"entry_point"})); + auto* entry = main.addEntryBlock(builder); + auto* header = main.addBlock(); + auto* body = main.addBlock(); + auto* latch = main.addBlock(); + auto* exit = main.addBlock(); + builder.setInsertionPointToEnd(entry); + LLVM::BrOp::create(builder, location, header); + builder.setInsertionPointToEnd(header); + auto ordinaryCondition = + LLVM::ConstantOp::create(builder, location, builder.getBoolAttr(true)); + LLVM::CondBrOp::create(builder, location, ordinaryCondition.getResult(), body, + latch); + builder.setInsertionPointToEnd(body); + LLVM::BrOp::create(builder, location, latch); + builder.setInsertionPointToEnd(latch); + auto null = LLVM::ZeroOp::create(builder, location, ptrType); + auto measurementCondition = + LLVM::CallOp::create(builder, location, readResult, null.getResult()); + LLVM::CondBrOp::create(builder, location, measurementCondition.getResult(), + header, exit); + builder.setInsertionPointToEnd(exit); + LLVM::ReturnOp::create(builder, location, ValueRange{}); + ASSERT_TRUE(succeeded(verify(module))); + + PassManager manager(context.get()); + manager.addPass(qir::createQIRSetAttributesAndMetadata({true})); + ASSERT_TRUE(succeeded(manager.run(module))); + const auto backwardsBranching = findModuleFlag(module, "backwards_branching"); + ASSERT_TRUE(backwardsBranching); + EXPECT_EQ(cast(backwardsBranching.getValue()) + .getValue() + .getZExtValue(), + 2U); +} + +TEST_F(QIRTest, CleanupOnlyRemovesProvenSideEffectFreeArrayPairs) { + const auto buildModule = [&](bool nonNullError, bool mismatchedSize, + bool nonVoidAllocate) { + OpBuilder builder(context.get()); + const auto location = builder.getUnknownLoc(); + auto module = ModuleOp::create(location); + builder.setInsertionPointToStart(module.getBody()); + const auto ptrType = LLVM::LLVMPointerType::get(context.get()); + const auto voidType = LLVM::LLVMVoidType::get(context.get()); + const Type allocateResult = + nonVoidAllocate ? Type(builder.getI1Type()) : Type(voidType); + const auto allocateType = LLVM::LLVMFunctionType::get( + allocateResult, {builder.getI64Type(), ptrType, ptrType}); + const auto releaseType = + LLVM::LLVMFunctionType::get(voidType, {builder.getI64Type(), ptrType}); + auto allocate = LLVM::LLVMFuncOp::create( + builder, location, QIR_QUBIT_ARRAY_ALLOC, allocateType); + auto release = LLVM::LLVMFuncOp::create( + builder, location, QIR_QUBIT_ARRAY_RELEASE, releaseType); + const auto mainType = LLVM::LLVMFunctionType::get(builder.getI1Type(), {}); + auto main = LLVM::LLVMFuncOp::create(builder, location, "main", mainType); + main->setAttr("passthrough", builder.getStrArrayAttr({"entry_point"})); + auto* block = main.addEntryBlock(builder); + builder.setInsertionPointToEnd(block); + + auto size = LLVM::ConstantOp::create(builder, location, + builder.getI64IntegerAttr(2)); + auto array = LLVM::AllocaOp::create(builder, location, ptrType, ptrType, + size.getResult()); + Value error; + if (nonNullError) { + auto one = LLVM::ConstantOp::create(builder, location, + builder.getI64IntegerAttr(1)); + error = LLVM::AllocaOp::create(builder, location, ptrType, + builder.getI1Type(), one.getResult()) + .getResult(); + auto initial = LLVM::ConstantOp::create(builder, location, + builder.getBoolAttr(true)); + LLVM::StoreOp::create(builder, location, initial.getResult(), error); + } else { + error = LLVM::ZeroOp::create(builder, location, ptrType).getResult(); + } + auto allocateCall = LLVM::CallOp::create( + builder, location, allocate, + ValueRange{size.getResult(), array.getResult(), error}); + Value releaseSize = size.getResult(); + if (mismatchedSize) { + releaseSize = LLVM::ConstantOp::create(builder, location, + builder.getI64IntegerAttr(1)) + .getResult(); + } + LLVM::CallOp::create(builder, location, release, + ValueRange{releaseSize, array.getResult()}); + + Value result; + if (nonVoidAllocate) { + result = allocateCall.getResult(); + } else if (nonNullError) { + result = + LLVM::LoadOp::create(builder, location, builder.getI1Type(), error) + .getResult(); + } else { + result = LLVM::ConstantOp::create(builder, location, + builder.getBoolAttr(false)) + .getResult(); + } + LLVM::ReturnOp::create(builder, location, result); + return module; + }; + + for (const auto [nonNullError, mismatchedSize, nonVoidAllocate, + expectedCalls] : + std::array{std::tuple{false, false, false, 0U}, + std::tuple{true, false, false, 2U}, + std::tuple{false, true, false, 2U}, + std::tuple{false, false, true, 2U}}) { + SCOPED_TRACE(testing::Message() << "nonNullError=" << nonNullError + << ", mismatchedSize=" << mismatchedSize + << ", nonVoidAllocate=" << nonVoidAllocate); + auto module = buildModule(nonNullError, mismatchedSize, nonVoidAllocate); + ASSERT_TRUE(succeeded(verify(module))); + PassManager manager(context.get()); + manager.addPass(qir::createQIRCleanupPass()); + ASSERT_TRUE(succeeded(manager.run(module))); + ASSERT_TRUE(succeeded(verify(module))); + size_t calls = 0; + module.walk([&](LLVM::CallOp) { ++calls; }); + EXPECT_EQ(calls, expectedCalls); + } +} + +TEST_F(QIRTest, CleanupFindsRuntimeCallsNestedInFunctions) { + OpBuilder builder(context.get()); + auto module = ModuleOp::create(builder.getUnknownLoc()); + builder.setInsertionPointToStart(module.getBody()); + auto ptrType = LLVM::LLVMPointerType::get(context.get()); + auto allocateType = LLVM::LLVMFunctionType::get(ptrType, {ptrType}); + auto allocate = LLVM::LLVMFuncOp::create(builder, builder.getUnknownLoc(), + QIR_QUBIT_ALLOC, allocateType); + auto mainType = + LLVM::LLVMFunctionType::get(LLVM::LLVMVoidType::get(context.get()), {}); + auto main = LLVM::LLVMFuncOp::create(builder, builder.getUnknownLoc(), "main", + mainType); + main->setAttr( + "passthrough", + builder.getArrayAttr( + {builder.getStringAttr("entry_point"), + builder.getStrArrayAttr({"dynamic_qubit_management", "true"})})); + auto* block = main.addEntryBlock(builder); + builder.setInsertionPointToEnd(block); + auto null = LLVM::ZeroOp::create(builder, builder.getUnknownLoc(), ptrType); + LLVM::CallOp::create(builder, builder.getUnknownLoc(), allocate, + null.getResult()); + LLVM::ReturnOp::create(builder, builder.getUnknownLoc(), ValueRange{}); + ASSERT_TRUE(succeeded(verify(module))); + + PassManager manager(context.get()); + manager.addPass(qir::createQIRCleanupPass()); + ASSERT_TRUE(succeeded(manager.run(module))); + EXPECT_TRUE(findPassthroughEntry(main, "dynamic_qubit_management")); +} + +TEST_F(QIRTest, CleanupDoesNotDuplicateRequiredResourceCounts) { + OpBuilder builder(context.get()); + const auto location = builder.getUnknownLoc(); + auto module = ModuleOp::create(location); + builder.setInsertionPointToStart(module.getBody()); + const auto functionType = + LLVM::LLVMFunctionType::get(LLVM::LLVMVoidType::get(context.get()), {}); + auto main = LLVM::LLVMFuncOp::create(builder, location, "main", functionType); + main->setAttr( + "passthrough", + builder.getArrayAttr( + {builder.getStringAttr("entry_point"), + builder.getStrArrayAttr({"required_num_qubits", "4"}), + builder.getStrArrayAttr({"dynamic_qubit_management", "true"}), + builder.getStrArrayAttr({"required_num_results", "2"}), + builder.getStrArrayAttr({"dynamic_result_management", "true"})})); + auto* block = main.addEntryBlock(builder); + builder.setInsertionPointToEnd(block); + LLVM::ReturnOp::create(builder, location, ValueRange{}); + ASSERT_TRUE(succeeded(verify(module))); + + PassManager manager(context.get()); + manager.addPass(qir::createQIRCleanupPass()); + ASSERT_TRUE(succeeded(manager.run(module))); + EXPECT_FALSE(findPassthroughEntry(main, "dynamic_qubit_management")); + EXPECT_FALSE(findPassthroughEntry(main, "dynamic_result_management")); + EXPECT_EQ(countPassthroughEntries(main, "required_num_qubits"), 1U); + EXPECT_EQ(countPassthroughEntries(main, "required_num_results"), 1U); + const auto requiredQubits = findPassthroughEntry(main, "required_num_qubits"); + const auto requiredResults = + findPassthroughEntry(main, "required_num_results"); + ASSERT_TRUE(requiredQubits); + ASSERT_TRUE(requiredResults); + EXPECT_EQ(cast(requiredQubits[1]).getValue(), "4"); + EXPECT_EQ(cast(requiredResults[1]).getValue(), "2"); } TEST_F(QIRTest, UsesQIR21ModuleFlagWidths) { diff --git a/mlir/unittests/Dialect/QTensor/Transforms/test_qtensor_transforms.cpp b/mlir/unittests/Dialect/QTensor/Transforms/test_qtensor_transforms.cpp index b16c3a8ed1..b79795f23a 100644 --- a/mlir/unittests/Dialect/QTensor/Transforms/test_qtensor_transforms.cpp +++ b/mlir/unittests/Dialect/QTensor/Transforms/test_qtensor_transforms.cpp @@ -20,6 +20,7 @@ #include "mlir/Dialect/QTensor/Transforms/Passes.h" #include +#include #include #include #include @@ -34,6 +35,7 @@ #include #include +#include using namespace mlir; @@ -78,4 +80,81 @@ TEST(QTensorTransformsTest, ShrinkToFitPreservesMetadata) { mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()), StringAttr::get(&context, "q")); } + +TEST(QTensorTransformsTest, HugeDeclaredTensorUsesSparseShrinkPlan) { + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + + auto moduleOp = parseSourceString(R"mlir( + module { + func.func @main() { + %c7 = arith.constant 7 : index + %huge = arith.constant 1099511627776 : index + %reg = qtensor.alloc(%huge) : tensor<1099511627776x!qco.qubit> + %rest, %qubit = qtensor.extract %reg[%c7] + : tensor<1099511627776x!qco.qubit> + %flipped = qco.x %qubit : !qco.qubit -> !qco.qubit + %updated = qtensor.insert %flipped into %rest[%c7] + : tensor<1099511627776x!qco.qubit> + qtensor.dealloc %updated : tensor<1099511627776x!qco.qubit> + return + } + } + )mlir", + &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + PassManager manager(&context); + manager.addPass(qtensor::createShrinkQTensorToFitPass()); + ASSERT_TRUE(succeeded(manager.run(*moduleOp))); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + qtensor::AllocOp allocation; + moduleOp->walk([&](qtensor::AllocOp op) { allocation = op; }); + ASSERT_TRUE(allocation); + EXPECT_EQ(cast(allocation.getType()).getShape(), + ArrayRef{1}); +} + +TEST(QTensorTransformsTest, RejectsNonLinearChainWithoutMutation) { + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + + auto moduleOp = parseSourceString(R"mlir( + module { + func.func @main() { + %c1 = arith.constant 1 : index + %c3 = arith.constant 3 : index + %reg = qtensor.alloc(%c3) : tensor<3x!qco.qubit> + %rest, %qubit = qtensor.extract %reg[%c1] + : tensor<3x!qco.qubit> + qtensor.dealloc %rest : tensor<3x!qco.qubit> + qtensor.dealloc %rest : tensor<3x!qco.qubit> + return + } + } + )mlir", + &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + std::string before; + llvm::raw_string_ostream(before) << *moduleOp; + + PassManager manager(&context); + manager.addPass(qtensor::createShrinkQTensorToFitPass()); + ASSERT_TRUE(succeeded(manager.run(*moduleOp))); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + std::string after; + llvm::raw_string_ostream(after) << *moduleOp; + EXPECT_EQ(after, before); +} } // namespace diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index 7bdee690b1..f33ca84b16 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -2318,10 +2318,12 @@ def test_parameter_vector_size_limits_on_import(sizes: list[int]) -> None: [ ([65_537], None, "across all distinct"), ([32_769, 32_769], None, "across all distinct"), - ([1, 2], 0, "conflicting metadata"), + ([1, 2], 0, "inconsistent name or size"), ], ) -def test_parameter_vector_metadata_is_preflighted(sizes: list[int], shared_group_id: int | None, message: str) -> None: +def test_parameter_vector_metadata_is_preflighted( + sizes: list[int], shared_group_id: int | None, message: str, capfd: pytest.CaptureFixture[str] +) -> None: """Validate vector consistency and resource bounds before allocation.""" arguments = [] gates = [] @@ -2332,7 +2334,7 @@ def test_parameter_vector_metadata_is_preflighted(sizes: list[int], shared_group f'name = "theta{index}", index = 0 : i64, size = {size} : i64}}}}' ) gates.append(f" qc.rx(%theta{index}) %q : !qc.qubit") - program = QCProgram.from_mlir_str( + source = ( "module {\n" f" func.func @main({', '.join(arguments)}) attributes {{mqt.entry_point}} {{\n" " %q = qc.alloc : !qc.qubit\n" + "\n".join(gates) + "\n qc.dealloc %q : !qc.qubit\n" @@ -2341,6 +2343,14 @@ def test_parameter_vector_metadata_is_preflighted(sizes: list[int], shared_group "}\n" ) + if shared_group_id is not None: + with pytest.raises(RuntimeError, match="MLIR operation failed"): + QCProgram.from_mlir_str(source) + assert message in capfd.readouterr().err + return + + program = QCProgram.from_mlir_str(source) + with pytest.raises(RuntimeError, match=message): program.to_qiskit() From 0c8556a411fe149888ae767f43da8f4ff20463e5 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 31 Aug 2026 23:51:00 +0200 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=90=9B=20Fix=20contract=20audit=20por?= =?UTF-8?q?tability=20regressions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bindings/mlir/register_mlir.cpp | 13 ++ cmake/ExternalDependencies.cmake | 33 ++++-- .../NativeSynthesis/TargetSynthesis.cpp | 1 + .../Optimizations/HadamardLifting.cpp | 2 + .../QIR/Transforms/AttachQIRAttributes.cpp | 38 +++++- .../lib/Dialect/QIR/Transforms/CMakeLists.txt | 1 + mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp | 111 ++++++++++++++++++ 7 files changed, 183 insertions(+), 16 deletions(-) diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 27e0382eea..46899b5f80 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -56,6 +57,16 @@ namespace mqt { namespace nb = nanobind; using namespace nb::literals; +// Keep the catch in this binary: Darwin cannot reliably match standard-library +// exception RTTI in nanobind's split-mode backend. +static void translateRuntimeError(const std::exception_ptr& error, void*) { + try { + std::rethrow_exception(error); + } catch (const std::runtime_error& exception) { + PyErr_SetString(PyExc_RuntimeError, exception.what()); + } +} + template [[nodiscard]] static T takeResult(std::optional&& result) { if (!result) { @@ -322,6 +333,8 @@ generateBenchmark(const std::string_view instanceSpecificationJSON) { } NB_MODULE(MQT_CORE_MODULE_NAME, m) { + nb::register_exception_translator(&translateRuntimeError); + m.doc() = "MQT Core MLIR compiler bindings."; nb::module_::import_("typing"); diff --git a/cmake/ExternalDependencies.cmake b/cmake/ExternalDependencies.cmake index d4a318be7d..f3c4f43010 100644 --- a/cmake/ExternalDependencies.cmake +++ b/cmake/ExternalDependencies.cmake @@ -38,20 +38,27 @@ if(BUILD_MQT_CORE_MLIR) FetchContent_MakeAvailable(jeff-mlir) endfunction() _mqt_core_make_jeff_available() -endif() -# jeff-mlir currently reports malformed serialized input through LLVM's fatal error API. Compile its -# translation boundary with exceptions and redirect those reports so the public MQT importer can -# diagnose them without exiting. -if(MSVC) - target_compile_options( - obj.MLIRJeffTranslation - PRIVATE /EHsc "/FI${PROJECT_SOURCE_DIR}/mlir/include/mlir/Compiler/JeffFatalErrorRedirect.h") -else() - target_compile_options( - obj.MLIRJeffTranslation - PRIVATE -fexceptions -include - "${PROJECT_SOURCE_DIR}/mlir/include/mlir/Compiler/JeffFatalErrorRedirect.h") + # jeff-mlir currently reports malformed serialized input through LLVM's fatal error API. Compile + # its translation boundary with exceptions and redirect those reports so the public MQT importer + # can diagnose them without exiting. MLIR uses an object target when available. Visual Studio + # generators compile the sources in the library target directly. + if(TARGET obj.MLIRJeffTranslation) + set(_mqt_core_jeff_translation_target obj.MLIRJeffTranslation) + else() + set(_mqt_core_jeff_translation_target MLIRJeffTranslation) + endif() + if(MSVC) + target_compile_options( + ${_mqt_core_jeff_translation_target} + PRIVATE /EHsc "/FI${PROJECT_SOURCE_DIR}/mlir/include/mlir/Compiler/JeffFatalErrorRedirect.h") + else() + target_compile_options( + ${_mqt_core_jeff_translation_target} + PRIVATE -fexceptions -include + "${PROJECT_SOURCE_DIR}/mlir/include/mlir/Compiler/JeffFatalErrorRedirect.h") + endif() + unset(_mqt_core_jeff_translation_target) endif() set(JSON_VERSION diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index 5fd9c1ca97..f8b57ed1d0 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -16,6 +16,7 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/HadamardLifting.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/HadamardLifting.cpp index 08df085728..4cedaab4d3 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/HadamardLifting.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/HadamardLifting.cpp @@ -14,11 +14,13 @@ #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Support/OperationUtils.h" #include #include +#include // IWYU pragma: keep (Passes.h.inc) #include #include #include diff --git a/mlir/lib/Dialect/QIR/Transforms/AttachQIRAttributes.cpp b/mlir/lib/Dialect/QIR/Transforms/AttachQIRAttributes.cpp index a57dbc25aa..8061d3ba44 100644 --- a/mlir/lib/Dialect/QIR/Transforms/AttachQIRAttributes.cpp +++ b/mlir/lib/Dialect/QIR/Transforms/AttachQIRAttributes.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -354,7 +355,7 @@ struct QIRSetAttributesAndMetadata final } auto function = dyn_cast(anchor); - bool sawDirectCall = false; + bool sawProvenance = false; LogicalResult status = success(); if (function && !function.isExternal() && blockArgument.getOwner() == &function.getBody().front()) { @@ -369,17 +370,48 @@ struct QIRSetAttributesAndMetadata final blockArgument.getArgNumber() >= call.getNumOperands()) { return; } - sawDirectCall = true; + sawProvenance = true; status = includeStaticPointer( call.getOperand(blockArgument.getArgNumber()), resource, capacity, module, requireStatic, resolving, aggregates); }); + } else { + SmallVector worklist{pointer}; + SmallPtrSet visited; + bool unresolvedProvenance = false; + while (!worklist.empty() && succeeded(status)) { + Value current = worklist.pop_back_val(); + if (!current) { + unresolvedProvenance = true; + continue; + } + if (!visited.insert(current).second) { + continue; + } + if (auto predecessors = getControlFlowPredecessors(current)) { + unresolvedProvenance |= predecessors->empty(); + worklist.append(*predecessors); + continue; + } + if (auto argument = dyn_cast(current)) { + auto owner = + dyn_cast(argument.getOwner()->getParentOp()); + if (!owner || argument.getOwner() != &owner.getBody().front()) { + unresolvedProvenance = true; + continue; + } + } + sawProvenance = true; + status = includeStaticPointer(current, resource, capacity, module, + requireStatic, resolving, aggregates); + } + sawProvenance &= !unresolvedProvenance; } resolving.erase(pointer); if (failed(status)) { return failure(); } - if (sawDirectCall) { + if (sawProvenance) { return success(); } } diff --git a/mlir/lib/Dialect/QIR/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QIR/Transforms/CMakeLists.txt index 3431b2aadc..564945c2a9 100644 --- a/mlir/lib/Dialect/QIR/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QIR/Transforms/CMakeLists.txt @@ -13,6 +13,7 @@ add_mlir_library( ${PASSES_SOURCES} LINK_LIBS PRIVATE + MLIRAnalysis MLIRLLVMDialect MLIRMQTDialect MLIRQIRUtils diff --git a/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp b/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp index 154e83adce..c1a83dbe95 100644 --- a/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp +++ b/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp @@ -371,6 +371,117 @@ TEST_F(QIRTest, AdaptiveMetadataScansStaticResourcesInHelperFunctions) { EXPECT_TRUE(findModuleFlag(module, "ir_functions")); } +TEST_F(QIRTest, MetadataTracesStaticResourcesThroughControlFlowArguments) { + OpBuilder builder(context.get()); + const auto location = builder.getUnknownLoc(); + auto module = ModuleOp::create(location); + builder.setInsertionPointToStart(module.getBody()); + const auto ptrType = LLVM::LLVMPointerType::get(context.get()); + const auto voidType = LLVM::LLVMVoidType::get(context.get()); + auto measure = LLVM::LLVMFuncOp::create( + builder, location, QIR_MEASURE, + LLVM::LLVMFunctionType::get(voidType, {ptrType, ptrType})); + auto main = LLVM::LLVMFuncOp::create( + builder, location, "main", LLVM::LLVMFunctionType::get(voidType, {})); + main->setAttr("passthrough", builder.getStrArrayAttr({"entry_point"})); + auto* entry = main.addEntryBlock(builder); + auto* left = main.addBlock(); + auto* right = main.addBlock(); + auto* join = main.addBlock(); + for (Block* block : {left, right, join}) { + block->addArgument(ptrType, location); + block->addArgument(ptrType, location); + } + + builder.setInsertionPointToEnd(entry); + const auto staticPointer = [&](const int64_t index) -> Value { + auto constant = LLVM::ConstantOp::create(builder, location, + builder.getI64IntegerAttr(index)); + return LLVM::IntToPtrOp::create(builder, location, ptrType, + constant.getResult()) + .getResult(); + }; + Value qubit2 = staticPointer(2); + Value qubit6 = staticPointer(6); + Value result1 = staticPointer(1); + Value result4 = staticPointer(4); + auto condition = + LLVM::ConstantOp::create(builder, location, builder.getBoolAttr(true)); + LLVM::CondBrOp::create(builder, location, condition.getResult(), left, + ValueRange{qubit2, result1}, right, + ValueRange{qubit6, result4}); + + builder.setInsertionPointToEnd(left); + LLVM::BrOp::create(builder, location, left->getArguments(), join); + builder.setInsertionPointToEnd(right); + LLVM::BrOp::create(builder, location, right->getArguments(), join); + builder.setInsertionPointToEnd(join); + LLVM::CallOp::create(builder, location, measure, join->getArguments()); + LLVM::ReturnOp::create(builder, location, ValueRange{}); + ASSERT_TRUE(succeeded(verify(module))); + + PassManager manager(context.get()); + manager.addPass(qir::createQIRSetAttributesAndMetadata({false})); + ASSERT_TRUE(succeeded(manager.run(module))); + const auto requiredQubits = findPassthroughEntry(main, "required_num_qubits"); + const auto requiredResults = + findPassthroughEntry(main, "required_num_results"); + ASSERT_TRUE(requiredQubits); + ASSERT_TRUE(requiredResults); + EXPECT_EQ(cast(requiredQubits[1]).getValue(), "7"); + EXPECT_EQ(cast(requiredResults[1]).getValue(), "5"); +} + +TEST_F(QIRTest, MetadataRequiresAnOriginForLoopCarriedStaticPointers) { + for (const bool hasStaticOrigin : {false, true}) { + SCOPED_TRACE(testing::Message() << "hasStaticOrigin=" << hasStaticOrigin); + OpBuilder builder(context.get()); + const auto location = builder.getUnknownLoc(); + auto module = ModuleOp::create(location); + builder.setInsertionPointToStart(module.getBody()); + const auto ptrType = LLVM::LLVMPointerType::get(context.get()); + const auto voidType = LLVM::LLVMVoidType::get(context.get()); + auto x = LLVM::LLVMFuncOp::create( + builder, location, QIR_X, + LLVM::LLVMFunctionType::get(voidType, {ptrType})); + auto main = LLVM::LLVMFuncOp::create( + builder, location, "main", LLVM::LLVMFunctionType::get(voidType, {})); + main->setAttr("passthrough", builder.getStrArrayAttr({"entry_point"})); + auto* entry = main.addEntryBlock(builder); + auto* loop = main.addBlock(); + loop->addArgument(ptrType, location); + auto* exit = main.addBlock(); + + builder.setInsertionPointToEnd(entry); + if (hasStaticOrigin) { + auto index = LLVM::ConstantOp::create(builder, location, + builder.getI64IntegerAttr(3)); + auto qubit = LLVM::IntToPtrOp::create(builder, location, ptrType, + index.getResult()); + LLVM::BrOp::create(builder, location, qubit.getResult(), loop); + } else { + LLVM::BrOp::create(builder, location, exit); + } + builder.setInsertionPointToEnd(loop); + LLVM::CallOp::create(builder, location, x, loop->getArgument(0)); + LLVM::BrOp::create(builder, location, loop->getArgument(0), loop); + builder.setInsertionPointToEnd(exit); + LLVM::ReturnOp::create(builder, location, ValueRange{}); + ASSERT_TRUE(succeeded(verify(module))); + + PassManager manager(context.get()); + manager.addPass(qir::createQIRSetAttributesAndMetadata({false})); + if (!hasStaticOrigin) { + EXPECT_TRUE(failed(manager.run(module))); + continue; + } + ASSERT_TRUE(succeeded(manager.run(module))); + const auto required = findPassthroughEntry(main, "required_num_qubits"); + ASSERT_TRUE(required); + EXPECT_EQ(cast(required[1]).getValue(), "4"); + } +} + TEST_F(QIRTest, MetadataRejectsMalformedStaticResourcePointersAtomically) { for (const bool useNonConstant : {false, true}) { SCOPED_TRACE(testing::Message() << "useNonConstant=" << useNonConstant); From 164ef96135e65a8b671f9361930ae4093ee6c9a4 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 1 Sep 2026 11:39:47 +0200 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=93=9D=20Reconcile=20MLIR=20contract?= =?UTF-8?q?=20audit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Record the focused pull request outcomes and revise the audit gate so passes may assume verifier-valid IR. Withdraw speculative and invalid-input findings across the original snapshot. Assisted-by: GPT-5.6 via Codex --- .agent/plans/audit-mlir-contracts.md | 285 ++++++++++++++++++++------- 1 file changed, 214 insertions(+), 71 deletions(-) diff --git a/.agent/plans/audit-mlir-contracts.md b/.agent/plans/audit-mlir-contracts.md index 1e43b6b9af..f4e4e00cbc 100644 --- a/.agent/plans/audit-mlir-contracts.md +++ b/.agent/plans/audit-mlir-contracts.md @@ -1,4 +1,4 @@ -# Audit and harden MLIR contracts +# Audit MLIR contracts This ExecPlan is a living document. The sections Progress, Surprises & Discoveries, Decision Log, and Outcomes & Retrospective are updated while the @@ -6,16 +6,42 @@ work proceeds. This plan is maintained in accordance with .agent/PLANS.md. ## Purpose / Big Picture -Complete GitHub issue #2255 as one reviewable change. Every MQT-owned MLIR pass, -handwritten verifier entrypoint, rewrite/conversion pattern, and dialect -registration surface is audited. Unsupported but structurally valid input must -produce a diagnostic rather than crash or partially mutate the IR; successful -passes must return verifier-valid and semantically faithful IR; verifiers must -own only local invariants; patterns must report match success and failure -truthfully; and every dialect a pass can create must be registered. - -The observable result is a focused regression for each confirmed objective -violation, plus a green complete MLIR test label and repository lint checks. +Complete GitHub issue #2255 as a dispositioned audit, not as one bulk code +change. Every MQT-owned MLIR pass, handwritten verifier entrypoint, +rewrite/conversion pattern, and dialect registration surface is audited. A +dialect-IR finding is retained only when the input passes every applicable +validator and a baseline reproducer shows a crash, wrong output, partial +mutation, unbounded resource use, or another stated contract violation. + +Applicable validation includes MLIR structural verification and MQT-owned +whole-program checks such as QCO linearity and MQT program metadata. Individual +passes may assume those invariants. External input, verifier, conversion, and +shared resource-owning boundaries remain responsible for the checks they own. + +The observable result is a complete census with each finding accepted, narrowed, +rejected, or deferred. Accepted findings land only in focused pull requests +after human review, with a regression that demonstrates the supported contract. + +## Revised Audit Rules + +- Identify the owner of each invariant before proposing a fix. Dialect verifiers + own dialect validity; ingestion and conversion boundaries own malformed + external input; passes own only failures reachable from valid IR. +- Require an executed baseline reproducer. A hypothetical stack, crash, or + compatibility concern is not a defect without evidence that the supported path + can reach it. +- Verify pass inputs and outputs in tests. Do not add whole-IR verification or + one-use guards to each pass solely to tolerate invalid dialect IR. +- Keep invalid-IR tests at the verifier or ingestion boundary that owns the + invariant. Do not duplicate them across transformation passes. +- Follow the repository rule that recursion must be bounded. For an established + MLIR walk, require evidence that a proposed replacement enforces a documented + bound or fixes a supported-path failure; an arbitrary-depth passing test does + not establish that the replacement improves the contract. +- Prefer an upstream dependency fix when the defect belongs upstream. Do not add + a local workaround until an observable MQT failure justifies it. +- Stop the audit at the finding ledger. A human selects findings for separate, + focused implementation pull requests. ## Progress @@ -81,6 +107,19 @@ violation, plus a green complete MLIR test label and repository lint checks. Python/Qiskit metadata regressions, repository lint, and diff checks. Reconfirmed that standalone C++ lint cannot start because clang-tidy 22 is unavailable on this host. +- [x] (2026-09-01) Reconciled the audit with the first 16 focused replacement + pull requests: seven merged, six open, and three closed without merge. +- [x] (2026-09-01) Narrowed `#2300` to its demonstrated missing-entry-point + defect and removed the speculative program-sized traversal worklists. +- [x] (2026-09-01) Withdrew `#2303`, `#2305`, and `#2306` after review. Marked + `#2309` for withdrawal because its regressions use invalid QCO IR. +- [x] (2026-09-01) Revised the audit rules so passes may assume input accepted + by all owning verifiers, and so each retained finding requires a + valid-input baseline reproducer at the correct ownership boundary. +- [x] (2026-09-01) Reclassified the same invalid-input pattern across the full + snapshot: generic pass-entry metadata/linearity checks, defensive QCO + one-use guards, and their invalid-IR regressions are not actionable + findings. ## Surprises & Discoveries @@ -104,9 +143,11 @@ violation, plus a green complete MLIR test label and repository lint checks. - Observation: loop-unroll verification of a temporary clone must retain its parent module so sibling symbol references resolve, while verifying only the transformed operation because the temporary module is intentionally partial. -- Observation: recursive walkers and expansion-producing passes needed explicit - depth, iteration, or resource budgets. Deep but valid nested regions also - required iterative traversal tests. +- Observation: expansion-producing work reachable from valid input can require + explicit limits, and repository policy treats unbounded recursion as a + correctness risk. The proposed `#2300` worklists increased memory use, while + the `#2306` depth-256 regression also passed the old MLIR walk. Neither change + demonstrated a better bound for a supported path. - Observation: QCO DD sampling performs a recursive interprocedural analysis before execution, so bounding only the runtime walker is insufficient. The sampling analysis and runtime now share the 64-call/region-nesting policy and @@ -150,12 +191,22 @@ violation, plus a green complete MLIR test label and repository lint checks. - Observation: OpenQASM output growth is governed by aggregate classical width, not only per-register widths. DD construction likewise needs QCO linearity verification in the shared preparation path, not only at selected callers. +- Observation: MLIR structural verification does not enforce QCO or QTensor + linearity. Several original tests therefore called structurally verified IR + valid even though the owning QCO validator rejected it. +- Observation: the QTensor one-use guard rejected during `#2295` review was + proposed again in `#2303`. A focused extraction must check earlier review + decisions before treating residual snapshot code as a new finding. +- Observation: the macOS exception workaround in `#2305` had no demonstrated + user-visible failure. Dependency-boundary concerns should remain upstream + candidates until an MQT reproducer exists. ## Decision Log -- Decision: complete all of #2255 in one PR, while keeping each regression and - source fix narrow. Rationale: the user explicitly requested one comprehensive - audit; separable tests keep the large scope reviewable. Date/Author: +- Superseded decision: complete all of #2255 in one PR, while keeping each + regression and source fix narrow. Rationale: the user explicitly requested one + comprehensive audit; separable tests were expected to keep the large scope + reviewable. Superseded by focused delivery on 2026-09-01. Date/Author: 2026-08-27, Codex. - Decision: reject unsupported semantic shapes during a read-only preflight unless the target dialect can preserve them. Rationale: a diagnostic is safer @@ -199,28 +250,114 @@ violation, plus a green complete MLIR test label and repository lint checks. both phases traverse user-controlled interprocedural control flow and must fail or conservatively select dynamic sampling before exhausting host resources. Date/Author: 2026-08-31, Codex. +- Decision: supersede the original one-pull-request delivery. The audit records + findings; each accepted finding is reviewed and implemented separately. + Rationale: focused review accepted useful fixes and exposed speculative or + wrongly owned changes that a bulk implementation hid. Date/Author: 2026-09-01, + Codex. +- Decision: treat input accepted by all applicable dialect and program + validators as the precondition for transformation passes. Do not add + pass-local verification or invalid-IR regressions for those same invariants. + Rationale: QCO and QTensor validity have owning validators, and duplicating + their checks across passes creates code and tests for unsupported programs. + Date/Author: 2026-09-01, Codex. +- Decision: require a demonstrated supported-path failure or a documented, + measurable bound improvement before replacing native MLIR traversal. Require + an observable MQT failure before adding a local dependency workaround. + Rationale: `#2305` and `#2306` did not reproduce an MQT defect, while the + original `#2300` worklists increased memory use without enforcing a stated + limit. Date/Author: 2026-09-01, Codex. ## Outcomes & Retrospective -The refreshed census and implementation are complete: 27 pass implementations, -26 handwritten verifier entrypoints, 236 patterns (124 conversion and 112 -canonicalization/optimization), and seven registration surfaces. Confirmed -defects covered valid-input crashes, partial mutation, invalid or lossy -successful output, non-local verification, undeclared dialects, -recursion/resource exhaustion, false rewrite results, isolation-breaking motion, -missed folded constants, numeric overflow/non-finite values, and duplicated or -stale conversion state. - -After rebasing onto origin/main at 35d3dc2cb87dc9ed4904e9db7eb43257ad3d4527, the -complete release build and all 3,133 tests in the mqt-mlir-unittests label pass. -Current focused checks are green at QC IR 347/347, QC-to-QCO 176/176, QCO-to-QC -147/147, QCO utilities 149/149, and JeFF round-trip 152/152. The six QCO DD -Python 3.13 tests, five focused Python/Qiskit parameter-vector tests, repository -lint, and git diff checks also pass. The only unavailable check is standalone -C++ lint: `uvx nox -s cpp-lint` aborts before analysis with -`clang-tidy 22 is required` because that binary is absent from the host. -Pre-rebase focused-suite counts remain recorded below as historical checkpoints -rather than current per-suite totals. +The census is complete: 27 pass implementations, 26 handwritten verifier +entrypoints, 236 patterns (124 conversion and 112 +canonicalization/optimization), and seven registration surfaces. The original +implementation snapshot is not an accepted set of fixes. It contains useful +findings, already merged findings, and changes later rejected as speculative or +wrongly owned. The branch remains a historical audit artifact and is not +intended to merge in bulk. + +As of 2026-09-01, seven focused replacements have merged: `#2291`, `#2293`, +`#2294`, `#2295`, `#2296`, `#2301`, and `#2304`. Six remain open: `#2290`, +`#2300`, `#2302`, `#2307`, `#2308`, and `#2309`. Three closed without merge: +`#2303`, `#2305`, and `#2306`. Review narrowed `#2300` to the demonstrated +missing-entry-point case. `#2309` remains open but should be withdrawn because +its pass-local check and regressions target invalid QCO IR. + +The original branch passed its recorded build, test, lint, and diff checks. +Those results prove internal consistency only; they do not establish that each +change protects a supported contract. The review dispositions and revised audit +rules now control which findings remain actionable. + +## Focused Finding Reconciliation + +Merged findings: + +- `#2291`: preserve static-qubit isolation during cleanup. +- `#2293`: make MLIR region moves failure-atomic. +- `#2294`: preserve attributes on reused QIR declarations. +- `#2295`: make QTensor shrinking sparse and atomic. Review removed the + redundant one-use guard because QTensor linearity owns that invariant. +- `#2296`: stop QCO wire traversal at unknown carriers. +- `#2301`: keep terminal measurements after routing swaps. +- `#2304`: bound OpenQASM export resource use. + +Open findings: + +- `#2290`: harden MLIR constant folding. +- `#2300`: handle gate counts without an entry point. Review removed manual + program-sized worklists and restored native MLIR traversal. +- `#2302`: make QIR metadata attachment idempotent. +- `#2307`: bound CBit zero-initialization lowering. Review approved the focused + resource-boundary change. +- `#2308`: preserve QTensor insert updates in QCO-to-QC. +- `#2309`: reject nonlinear Hadamard-lifting inputs safely. Withdraw this + finding: its tests use invalid QCO IR, and Hadamard lifting may assume QCO + linearity. + +Closed findings: + +- `#2303`: redundant QTensor one-use handling for invalid IR. +- `#2305`: no demonstrated MQT failure justified a local nanobind workaround; + revisit upstream if a concrete failure appears. +- `#2306`: the proposed depth-256 regression also passed the existing MLIR walk + and did not demonstrate a supported bound. Revisit recursion policy in one + shared change if native MLIR walks need an explicit repository-wide limit. + +Broader snapshot changes withdrawn by the revised invariant: + +- Drop generic metadata or QCO-linearity input checks added to + `NormalizeGlobalPhases`, `UnrollModifiers`, QCO-to-QC, QCO-to-jeff, Mapping, + `FuseSingleQubitUnitaryRuns`, `TargetSynthesis`, `HadamardLifting`, + `MeasurementLifting`, `MergeSingleQubitRotationGates`, and QIR Common. Keep + target-specific support and representability checks in those components. +- Drop branch-only defensive one-use fallbacks in `QCOUtils.h`, QCO barrier and + rotation canonicalization, Mapping, two-qubit target synthesis, Hadamard + lifting, and classical-control replacement. Their zero-use or multi-use inputs + violate QCO linearity. +- Drop the matching pass and pattern regressions that construct nonlinear QCO or + QTensor programs. This includes the unused-output canonicalization, synthesis, + Hadamard-lifting, and classical-control tests, plus nonlinear-input tests for + modifier unrolling, normalization, mapping, measurement lifting, single-qubit + fusion, rotation merging, and QCO conversions. +- Drop the `NormalizeGlobalPhases` TableGen promise that the pass diagnoses QCO + linearity. The owning validator, not this pass, defines that contract. +- Do not count duplicate program-entry rejection in QCO-to-jeff or QIR Common as + a conversion finding. Keep target-specific entry existence and shape checks. + The exactly-one-QIR-entry rule in `#2302` remains at its owning QIR metadata + boundary. +- Repair the QCO-to-jeff mixed-allocation reproducer before extracting that + valid target limitation. Its current fixture leaves quantum values unused; the + corrected fixture must sink them and pass QCO linearity first. + +The revised invariant retains owning and output-side checks. Keep +`mqt::verifyProgramMetadata`, `qco::verifyLinearity`, compiler and external +ingestion boundaries, resource and nesting limits reachable from valid IR, +target-representability preflights, and clone/lower/verify/commit checks that +validate newly produced IR before mutation is committed. Pass tests may call the +applicable validators before and after the pass without adding those checks to +each pass implementation. ## Context and Orientation @@ -267,37 +404,39 @@ recorded as clean, affected directly, or affected through a shared fix. ### Milestone 2: Prove objective violations -For each candidate, construct the smallest verified or locally accepted IR that -reaches it. Discard structurally unreachable findings. Keep valid-input crashes, +For each candidate, construct the smallest input accepted by MLIR structural +verification and every applicable MQT-owned validator. Discard invalid-dialect +inputs and structurally unreachable findings. Keep valid-input crashes, invalid/lossy successful output, non-local verifier assumptions, partial -mutation, false rewrite contracts, missing registrations, and unbounded work. -The milestone is complete when each retained case has a failing baseline -reproducer and a named ownership boundary for the fix. +mutation, false rewrite contracts, missing registrations, and demonstrated +unbounded work. The milestone is complete when each retained case has an +executed failing baseline reproducer and a named ownership boundary. -### Milestone 3: Harden contracts with focused regressions +### Milestone 3: Review and extract accepted findings -Put validation before the first mutation or lower a clone and commit only after -verification. Replace process termination with operation/pass diagnostics, make -verifier checks local, register produced dialects, bound recursive or expanding -work, and use MLIR constant matching rather than producer-specific casts. Add -direct GoogleTests beside existing coverage. The milestone is complete when -every retained case passes and successful output verifies. +Record the evidence, owner, risk, and minimal proposed remedy for each retained +finding. Stop the audit before implementation. After human selection, implement +one finding per focused pull request and add the smallest regression that fails +on the baseline and protects the supported contract. The milestone is complete +when every finding is accepted, narrowed, rejected, or deferred. -### Milestone 4: Whole-suite closure and PR handoff +### Milestone 4: Reconcile focused outcomes -Build the final tree, run all mqt-mlir-unittests, run both lint sessions and -diff checks, then inspect status/name/stat output for unrelated or generated -files. Record exact results here. The milestone is complete when the branch is -PR-ready and no known #2255 contract defect remains. +Update this ledger after each focused review or merge. Record accepted, +narrowed, rejected, deferred, and superseded findings. Do not infer that a green +bulk branch validates every proposed contract. The milestone is complete when +the ledger matches GitHub and no rejected snapshot change remains listed as an +actionable defect. ## Plan of Work Maintain the complete census while inspecting implementation and declaration -pairs. Validate candidates with direct tests rather than changing code from -search results alone. Apply the smallest fix at the owning boundary and keep -failure atomic. Re-run the narrow binary immediately after each cluster, then -perform a second read-only review of high-risk conversion state and semantic -preflights. Finish with the full labeled suite, lint, and diff audit. +pairs. For every candidate, identify the owning contract and run the applicable +validators before writing a reproducer. Record and discard candidates that need +invalid dialect IR. Require evidence before changing native traversal or adding +a dependency workaround. Stop at the ledger. For a human-selected finding, start +a focused branch from current `main`, implement only that finding, and run its +narrow test and lint checks before handoff. ## Concrete Steps @@ -322,13 +461,14 @@ Run the complete checks after focused suites are green: ## Validation and Acceptance -Acceptance requires a named, fully dispositioned census; a regression for every -confirmed objective violation; no process termination or partial mutation for -unsupported valid input; verifier-valid successful output; truthful pattern -results; complete dialect registration; bounded recursion/resource use; and a -green full mqt-mlir-unittests label. Lint results and any environment-only -limitation must be recorded exactly. No generated, unrelated, or style-only file -may remain in the diff. +Audit acceptance requires a named, fully dispositioned census and executed +evidence for every retained finding. Dialect-IR reproducers must pass all owning +validators before a pass runs. Invalid-IR cases must be assigned to their +verifier or ingestion boundary instead of duplicated in passes. Each accepted +implementation pull request must leave verifier-valid output, pass its focused +regression and lint checks, and contain no generated, unrelated, or style-only +change. Record exact check results and environment-only limits on that focused +pull request, not as proof for the bulk snapshot. ## Idempotence and Recovery @@ -345,6 +485,9 @@ Initial baseline: baecdc55f130a26a21222d6fe5c613db7eee3633 from origin/main. Refreshed base: 35d3dc2cb87dc9ed4904e9db7eb43257ad3d4527 from origin/main. +Reconciliation base: 30bb9d1f8e9d81840aa42f47c3e577b8c76c4d63 from origin/main, +including merged focused replacements through `#2301`. + Pre-rebase focused checkpoint: QC IR: 345/345 @@ -394,9 +537,9 @@ dependency. The Python API is unchanged. C++ API additions are limited to shared helper declarations; no existing public API is removed or changed. Revision note: expanded the initial QIR Base investigation into the requested -complete #2255 contract audit; replaced the stale mid-audit disposition with the -full pass, verifier, pattern, and registration census; the final review also -closed the QIR Base/Mapping irreversible-ordering interaction. The origin/main -refresh added one canonicalization pattern to the census, closed -HoistStaticQubit's isolation boundary, aligned program-wide static-qubit -identity and DD analysis/runtime bounds, and refreshed the full-suite evidence. +complete `#2255` contract audit and recorded the full pass, verifier, pattern, +and registration census. The first focused review wave showed that the original +acceptance gate over-weighted malformed-IR defenses and hypothetical failures. +This revision makes all owning validators and an executed supported-path +reproducer mandatory, records merged and closed outcomes, and treats the bulk +implementation as a historical snapshot rather than an approved change set. From 9fcc02eb67586628d8244b425d032630313a86f2 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 1 Sep 2026 12:32:31 +0200 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=93=9D=20Record=20#2309=20audit=20wit?= =?UTF-8?q?hdrawal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the invalid-QCO finding from pending withdrawal to closed and refresh the focused audit counts. Assisted-by: GPT-5.6 via Codex --- .agent/plans/audit-mlir-contracts.md | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/.agent/plans/audit-mlir-contracts.md b/.agent/plans/audit-mlir-contracts.md index f4e4e00cbc..fb28a90a2c 100644 --- a/.agent/plans/audit-mlir-contracts.md +++ b/.agent/plans/audit-mlir-contracts.md @@ -108,11 +108,11 @@ after human review, with a regression that demonstrates the supported contract. Reconfirmed that standalone C++ lint cannot start because clang-tidy 22 is unavailable on this host. - [x] (2026-09-01) Reconciled the audit with the first 16 focused replacement - pull requests: seven merged, six open, and three closed without merge. + pull requests: seven merged, five open, and four closed without merge. - [x] (2026-09-01) Narrowed `#2300` to its demonstrated missing-entry-point defect and removed the speculative program-sized traversal worklists. -- [x] (2026-09-01) Withdrew `#2303`, `#2305`, and `#2306` after review. Marked - `#2309` for withdrawal because its regressions use invalid QCO IR. +- [x] (2026-09-01) Withdrew `#2303`, `#2305`, `#2306`, and `#2309` after review. + The `#2309` regressions use invalid QCO IR. - [x] (2026-09-01) Revised the audit rules so passes may assume input accepted by all owning verifiers, and so each retained finding requires a valid-input baseline reproducer at the correct ownership boundary. @@ -279,11 +279,11 @@ wrongly owned. The branch remains a historical audit artifact and is not intended to merge in bulk. As of 2026-09-01, seven focused replacements have merged: `#2291`, `#2293`, -`#2294`, `#2295`, `#2296`, `#2301`, and `#2304`. Six remain open: `#2290`, -`#2300`, `#2302`, `#2307`, `#2308`, and `#2309`. Three closed without merge: -`#2303`, `#2305`, and `#2306`. Review narrowed `#2300` to the demonstrated -missing-entry-point case. `#2309` remains open but should be withdrawn because -its pass-local check and regressions target invalid QCO IR. +`#2294`, `#2295`, `#2296`, `#2301`, and `#2304`. Five remain open: `#2290`, +`#2300`, `#2302`, `#2307`, and `#2308`. Four closed without merge: `#2303`, +`#2305`, `#2306`, and `#2309`. Review narrowed `#2300` to the demonstrated +missing-entry-point case and rejected `#2309` because its pass-local check and +regressions target invalid QCO IR. The original branch passed its recorded build, test, lint, and diff checks. Those results prove internal consistency only; they do not establish that each @@ -312,9 +312,6 @@ Open findings: - `#2307`: bound CBit zero-initialization lowering. Review approved the focused resource-boundary change. - `#2308`: preserve QTensor insert updates in QCO-to-QC. -- `#2309`: reject nonlinear Hadamard-lifting inputs safely. Withdraw this - finding: its tests use invalid QCO IR, and Hadamard lifting may assume QCO - linearity. Closed findings: @@ -324,6 +321,8 @@ Closed findings: - `#2306`: the proposed depth-256 regression also passed the existing MLIR walk and did not demonstrate a supported bound. Revisit recursion policy in one shared change if native MLIR walks need an explicit repository-wide limit. +- `#2309`: its tests use invalid QCO IR. Hadamard lifting may assume QCO + linearity, so the pass-local check and regressions were rejected. Broader snapshot changes withdrawn by the revised invariant: From 55b7dce4cb4ab0bc3f0a8cfde673767d18e41043 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 1 Sep 2026 13:45:41 +0200 Subject: [PATCH 5/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Keep=20only=20valid=20?= =?UTF-8?q?audit=20findings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove focused, merged, closed, malformed-IR, and speculative changes from the audit snapshot. Retain only uniquely owned boundary, verifier, resource, semantic, and output-atomicity findings, with valid regressions. Assisted-by: GPT-5.6 via Codex --- .agent/plans/audit-mlir-contracts.md | 199 ++++++++++- bindings/mlir/register_mlir.cpp | 13 - docs/mlir/OpenQASM.md | 2 - .../mlir/Conversion/JeffToQCO/JeffToQCO.td | 2 - .../mlir/Conversion/QCOToJeff/QCOToJeff.td | 8 +- .../mlir/Conversion/QCOToQC/QCOToQC.td | 11 +- .../mlir/Conversion/QCToQCO/QCToQCO.td | 8 +- .../QCToQIR/QIRAdaptive/QCToQIRAdaptive.td | 6 +- .../Conversion/QCToQIR/QIRBase/QCToQIRBase.td | 5 +- .../mlir/Dialect/MQT/Transforms/Passes.td | 3 +- .../mlir/Dialect/MQT/Utils/ConstantFolding.h | 8 +- .../mlir/Dialect/MQT/Utils/Modifiers.h | 46 +-- .../mlir/Dialect/QC/Transforms/Passes.td | 1 - mlir/include/mlir/Dialect/QCO/QCOUtils.h | 52 +-- .../mlir/Dialect/QCO/Transforms/Passes.td | 62 ++-- .../mlir/Dialect/QIR/Transforms/Passes.td | 1 - .../mlir/Dialect/QTensor/Transforms/Passes.td | 1 - mlir/include/mlir/Support/OperationUtils.h | 49 --- mlir/lib/Compiler/Programs.cpp | 59 +--- .../Conversion/CBitToMemRef/CMakeLists.txt | 1 - mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp | 6 - mlir/lib/Conversion/QCOToJeff/CMakeLists.txt | 1 - mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp | 50 +-- mlir/lib/Conversion/QCOToQC/CMakeLists.txt | 2 - mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | 112 ++----- mlir/lib/Conversion/QCToQCO/CMakeLists.txt | 1 - mlir/lib/Conversion/QCToQCO/QCToQCO.cpp | 33 +- .../QCToQIR/QIRAdaptive/CMakeLists.txt | 1 - .../QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp | 57 ++-- .../Conversion/QCToQIR/QIRBase/CMakeLists.txt | 1 - .../QCToQIR/QIRBase/QCToQIRBase.cpp | 11 +- .../QCToQIR/QIRCommon/QIRCommon.cpp | 58 +--- mlir/lib/Dialect/MQT/IR/MQTDialect.cpp | 60 ++-- .../MQT/Transforms/NormalizeGlobalPhases.cpp | 107 +++--- .../MQT/Transforms/UnrollModifiers.cpp | 28 +- mlir/lib/Dialect/MQT/Utils/Angles.cpp | 1 + .../lib/Dialect/MQT/Utils/ConstantFolding.cpp | 130 ++------ mlir/lib/Dialect/MQT/Utils/Modifiers.cpp | 1 + .../Dialect/QC/IR/Modifiers/ModifierUtils.cpp | 33 +- .../QC/Transforms/ShrinkQubitRegisters.cpp | 6 - .../QC/Translation/TranslateQCToOpenQASM3.cpp | 5 +- mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp | 10 +- mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp | 6 +- .../QCO/IR/Modifiers/ModifierUtils.cpp | 54 +-- .../Dialect/QCO/IR/Modifiers/ModifierUtils.h | 4 - mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp | 6 +- .../IR/Operations/StandardGates/BarrierOp.cpp | 3 - .../QCO/IR/Operations/StandardGates/ROp.cpp | 3 - mlir/lib/Dialect/QCO/IR/QCOUtils.cpp | 40 +-- .../lib/Dialect/QCO/Transforms/CMakeLists.txt | 1 - .../DecomposeMultiControlled.cpp | 25 +- .../QCO/Transforms/Mapping/Mapping.cpp | 177 +++------- .../FuseSingleQubitUnitaryRuns.cpp | 15 - .../NativeSynthesis/TargetSynthesis.cpp | 111 ++----- .../Optimizations/HadamardLifting.cpp | 47 +-- .../Optimizations/MeasurementLifting.cpp | 53 +-- .../MergeSingleQubitRotationGates.cpp | 15 - .../Optimizations/PauliTwirling.cpp | 22 +- .../Optimizations/QuantumLoopUnroll.cpp | 64 ++-- .../Optimizations/RemoveDeadGates.cpp | 7 - .../ReplaceClassicalControls.cpp | 13 - .../Transforms/Optimizations/ReuseQubits.cpp | 7 - .../lib/Dialect/QCO/Utils/DDFunctionality.cpp | 66 ++-- .../QIR/Transforms/AttachQIRAttributes.cpp | 308 +++++++----------- .../lib/Dialect/QIR/Transforms/QIRCleanup.cpp | 45 +-- .../QTensor/Transforms/ShrinkRegisters.cpp | 7 +- mlir/lib/Support/Passes.cpp | 4 +- .../Compiler/test_compiler_pipeline.cpp | 15 - .../JeffRoundTrip/test_jeff_round_trip.cpp | 70 +--- .../Conversion/QCOToQC/test_qco_to_qc.cpp | 247 -------------- .../Conversion/QCToQCO/CMakeLists.txt | 3 +- .../Conversion/QCToQCO/test_qc_to_qco.cpp | 16 - .../test_qc_to_qir_adaptive.cpp | 18 - .../QCToQIRBase/test_qc_to_qir_base.cpp | 48 --- .../test_global_phase_normalization.cpp | 72 ---- .../Dialect/MQT/Utils/CMakeLists.txt | 5 +- .../MQT/Utils/test_constant_folding.cpp | 25 +- .../Translation/test_openqasm3_emission.cpp | 24 -- mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp | 93 ------ .../Dialect/QCO/IR/test_qco_ir_matrix.cpp | 66 +--- .../test_euler_decomposition.cpp | 72 ---- .../QCO/Transforms/Mapping/test_mapping.cpp | 119 ------- .../NativeSynthesis/test_target_synthesis.cpp | 35 -- .../test_qco_hadamard_lifting.cpp | 93 +----- .../test_qco_measurement_lifting.cpp | 77 ----- .../test_qco_merge_single_qubit_rotation.cpp | 69 ---- .../Optimizations/test_qco_pauli_twirling.cpp | 54 +-- .../test_qco_replace_classical_controls.cpp | 42 --- .../test_quantum_loop_unroll.cpp | 43 --- .../QCO/Utils/test_dd_functionality.cpp | 24 -- mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp | 258 +++++---------- 91 files changed, 940 insertions(+), 3002 deletions(-) delete mode 100644 mlir/include/mlir/Support/OperationUtils.h diff --git a/.agent/plans/audit-mlir-contracts.md b/.agent/plans/audit-mlir-contracts.md index fb28a90a2c..15d19f58d9 100644 --- a/.agent/plans/audit-mlir-contracts.md +++ b/.agent/plans/audit-mlir-contracts.md @@ -108,7 +108,7 @@ after human review, with a regression that demonstrates the supported contract. Reconfirmed that standalone C++ lint cannot start because clang-tidy 22 is unavailable on this host. - [x] (2026-09-01) Reconciled the audit with the first 16 focused replacement - pull requests: seven merged, five open, and four closed without merge. + pull requests: eight merged, four open, and four closed without merge. - [x] (2026-09-01) Narrowed `#2300` to its demonstrated missing-entry-point defect and removed the speculative program-sized traversal worklists. - [x] (2026-09-01) Withdrew `#2303`, `#2305`, `#2306`, and `#2309` after review. @@ -120,6 +120,14 @@ after human review, with a regression that demonstrates the supported contract. snapshot: generic pass-entry metadata/linearity checks, defensive QCO one-use guards, and their invalid-IR regressions are not actionable findings. +- [x] (2026-09-01) Merged current `main` at + de8a8a619fb69c1f6ef7d01f61911838324fc9b4, including the accepted CBit + resource-boundary fix from `#2307`, and reclassified every residual file + and test hunk against the revised invariant. +- [x] (2026-09-01) Pruned merged and focused duplicates, closed findings, + malformed-IR defenses, generic pass-entry validation, the unsupported + shared depth policy, and unproved traversal replacements. The remaining + implementation snapshot contains only valid, uniquely retained findings. ## Surprises & Discoveries @@ -275,12 +283,13 @@ entrypoints, 236 patterns (124 conversion and 112 canonicalization/optimization), and seven registration surfaces. The original implementation snapshot is not an accepted set of fixes. It contains useful findings, already merged findings, and changes later rejected as speculative or -wrongly owned. The branch remains a historical audit artifact and is not -intended to merge in bulk. +wrongly owned. The historical commits preserve that snapshot. The current branch +removes rejected and separately owned code and keeps only valid residual +findings for review; it is still not intended to merge in bulk. -As of 2026-09-01, seven focused replacements have merged: `#2291`, `#2293`, -`#2294`, `#2295`, `#2296`, `#2301`, and `#2304`. Five remain open: `#2290`, -`#2300`, `#2302`, `#2307`, and `#2308`. Four closed without merge: `#2303`, +As of 2026-09-01, eight focused replacements have merged: `#2291`, `#2293`, +`#2294`, `#2295`, `#2296`, `#2301`, `#2304`, and `#2307`. Four remain open: +`#2290`, `#2300`, `#2302`, and `#2308`. Four closed without merge: `#2303`, `#2305`, `#2306`, and `#2309`. Review narrowed `#2300` to the demonstrated missing-entry-point case and rejected `#2309` because its pass-local check and regressions target invalid QCO IR. @@ -302,6 +311,7 @@ Merged findings: - `#2296`: stop QCO wire traversal at unknown carriers. - `#2301`: keep terminal measurements after routing swaps. - `#2304`: bound OpenQASM export resource use. +- `#2307`: bound CBit zero-initialization lowering. Open findings: @@ -309,8 +319,6 @@ Open findings: - `#2300`: handle gate counts without an entry point. Review removed manual program-sized worklists and restored native MLIR traversal. - `#2302`: make QIR metadata attachment idempotent. -- `#2307`: bound CBit zero-initialization lowering. Review approved the focused - resource-boundary change. - `#2308`: preserve QTensor insert updates in QCO-to-QC. Closed findings: @@ -358,6 +366,181 @@ validate newly produced IR before mutation is committed. Pass tests may call the applicable validators before and after the pass without adding those checks to each pass implementation. +## Complete Residual Snapshot Disposition + +The original implementation is preserved at +`0141a0b4f8bbf63608f74fdd5b8608e2f2c40e95`. The last pre-reconciliation snapshot +is `9fcc02eb67586628d8244b425d032630313a86f2`. These commits preserve every +removed implementation and reproducer. The current delta keeps code and tests +only for the valid findings listed below. All residual production, test, +TableGen, CMake, binding, documentation, and tool changes were reread against +current `main` at `de8a8a619fb69c1f6ef7d01f61911838324fc9b4`. + +The retained items below are findings, not approval of the historical +implementation. Each needs a focused extraction from current `main`, a minimal +valid-input regression, and its own review. + +### Compiler and external-input boundaries + +- Retain compiler-owned program metadata validation at QC/QCO import and at + public pipeline input and output. The historical evidence is + `EnforcesProgramMetadataAtImportAndPassBoundaries`, the `mqt-cc` + duplicate-entry case, and `test_parameter_vector_metadata_is_preflighted`. The + compiler is the owner; individual passes are not. +- Retain the finding that malformed serialized JeFF input must not terminate the + process. `JeffFunctionDeclarationsAreRejected`, + `MalformedJeffStructuresAreRejected`, and + `InvalidJeffSemanticsAreRejectedWithoutExiting` exercise byte, file, and + command-line imports. Any extraction must minimize the local preflight and + fatal-error bridge and prefer an upstream fix where possible. +- Keep `#2300` as the sole owner of `QCProgramCountGatesWithoutEntryPoint`. The + manual compiler worklists are not part of that finding. + +### Conversion boundaries + +- JeFF-to-QCO retains schema-correct function-index entry-point decoding, target + entry-shape validation, status-result restoration, and + clone/lower/output-verify/commit atomicity. Evidence: + `RejectsMalformedJeffMetadataBeforeMutation`, + `RejectsUnsupportedQCOEntryShapeBeforeMutation`, + `RejectsStaleEntryPointMetadataAfterJeffConversion`, + `RestoresStatusResultAtEndOfEntryPoint`, and the no-mutation assertion in + `RejectsClassicalIfResultsPrecisely`. +- QCO-to-JeFF retains target-specific entry shape, allocation-mode, rank-one + tensor, and 255-control limits, schema-correct function-index serialization, + and output atomicity. The mixed-allocation fixture must be repaired to sink + every quantum value before extraction. Drop the duplicate-static fixture: the + owning QCO validator already rejects it. +- QCO-to-QC retains the valid target limitations demonstrated by + `RejectsYieldPermutationWithoutMutation` and + `RejectsMixedAllocationModesWithoutMutation`, plus output atomicity. `#2308` + exclusively owns `PreservesQTensorInsertSlotUpdates` and + `InvalidatesQTensorCacheAcrossLoopSlotSwap`. +- QC-to-QCO retains dynamic register-index aliasing, mixed allocation, static + reference identity and lifetime, conditional captured-register lifetime, + ordered QTensor materialization, fallible live-value lookup, and output + linearity/atomicity. Evidence: + `DuplicateStaticReferencesShareOneEvolvingQCOValue`, + `RejectsStaticUseAfterDeallocationWithoutMutation`, + `RejectsPossiblyAliasedDynamicIndicesWithoutMutation`, + `RejectsMixedAllocationModesWithoutMutation`, and + `RejectsConditionallyDeallocatedCapturedRegisterWithoutMutation`. +- QIR Adaptive retains entry shape and reserved-symbol checks, entry-only + QC/CBit/MemRef restrictions, the aggregate classical-result budget, release + placement, one-epilogue return lowering, supported control-flow and allocation + limits, global-phase lowering, and atomic output verification. The historical + tests cover missing entries, `__quantum__` collisions, + static/dynamic/conditional/repeated releases, nested and inconsistent returns, + path-dependent classical output, helper-function QC, mixed allocation, + controlled global phase, non-hoistable phase, rank-zero loads, and excessive + classical capacity. +- QIR Base retains entry and resource-shape validation, supported control-flow + and MemRef restrictions, static-ID/register-element accounting, reset and + irreversible-order rules, and atomic output verification. The historical tests + cover same-qubit and aliased-static ordering, independent qubits and global + phase, sparse and exhausted static IDs, repeated/dynamic/rank-zero loads, + multi-block/backedge/structured/affine/helper control flow, preserved + non-control regions, entry arguments, and excessive classical capacity. + +### Owning verifiers and metadata + +- Retain non-local program metadata checks only in `mqt::verifyProgramMetadata`. + Evidence: `AcceptsDefinedLLVMEntryPoint`, + `ProgramMetadataRejectsDuplicateEntryPoints`, + `ProgramMetadataRejectsNonFuncEntryPoint`, + `ProgramMetadataRejectsDuplicateInputNames`, + `ProgramMetadataRejectsInconsistentParameterGroups`, and + `AcceptsParameterGroupsOutsideCurrentVectorSize`. +- Retain owning QC/QCO operation-verifier findings for yield parentage, modifier + operand/result arity, duplicate yields, captured quantum values, barrier + arity, and direct or folded non-finite phase angles. Keep only the smallest + verifier-level assertion for each invariant when extracted. +- Retain bounded, failure-returning matrix queries and their semantic + correctness findings: pass-through targets, yield permutations, a sole + wider-than-two-qubit operation, unknown nested unitaries, excessive control + width, and finite large integral powers. Drop the arbitrary 64-modifier-depth + policy. +- Retain finite and bounded angle arithmetic at the owning numeric helpers and + valid-output transformations. Historical evidence includes + `IntegralPowerExtractionDoesNotOverflowGlobalPhase`, + `DynamicPhasesUseBoundedRuntimeArithmetic`, + `HugeFiniteStaticPhasesProduceVerifiedOutput`, + `DynamicMaxPhasesRemainFiniteAfterFusion`, and the QC/QCO verifier tests for + non-finite phase angles. +- `#2302` owns exactly-one-entry and idempotent passthrough/module-flag + metadata. Separate retained metadata findings cover static-resource provenance + and capacity, malformed or unknown origins, sparse qubit/result capacities, + all `inttoptr` users, result arrays that must not inflate qubit capacity, + malformed record declarations, and CFG-sensitive feature classification. +- QIR cleanup retains nested runtime-call discovery, deduplicated required + resource counts, and proven side-effect-free array alloc/release matching. + Evidence: `CleanupFindsRuntimeCallsNestedInFunctions`, + `CleanupDoesNotDuplicateRequiredResourceCounts`, and + `CleanupOnlyRemovesProvenSideEffectFreeArrayPairs`. + +### Valid transformation and resource findings + +- NormalizeGlobalPhases retains terminatorless module blocks, independent QC/QCO + aggregation, impure extraction boundaries, and finite static/dynamic angle + arithmetic. Drop its pass-local program and linearity validation and + iterative-traversal claim. The dedicated + `.agent/audits/global-phase-normalization.md` remains the assertion-level + audit for that pass. +- Modifier canonicalization and decomposition retain classical support + operations or refuse a rewrite before mutation. Evidence spans the QC/QCO + modifier canonicalizer tests, `PreservesClassicalBodyCalls`, + `LeavesPostUnitaryClassicalBodyCallInPlace`, + `PreservesModifierSupportOperationsWhenRefusingLift`, + `AllMeasuredFastPathsPreserveClassicalBodyCalls`, and the two + `PartialMeasured...RefusesUnsafeSupportingOpHoist` tests. +- Mapping retains option and target validation, target-specific representability + checks, the classical-only no-op, clone/verify/commit atomicity, its own + bounded structured recursion, and terminal reset routing. `#2301` already owns + terminal measurement routing. Drop generic metadata/linearity validation and + nonlinear-input tests. +- Target synthesis retains support-call preservation and atomic refusal. Drop + unused-output handling because those fixtures violate QCO linearity. +- Quantum-loop unrolling retains the factor 4096 and projected-operation 100,000 + resource limits, identity-yield correctness, required Arith dependency, + sibling-symbol-safe verification, and clone/verify/commit atomicity. Evidence + is the `Excessive...`, `NestedExpansion...`, `PreservesYieldOnlyPermutation`, + `DynamicTripCountFailureIsAtomic`, and + `UnrollsFunctionWithSiblingSymbolReference` tests. Drop the generic + 64-region-depth guard. +- DD execution retains its public-boundary aggregate classical-bit cap, shared + 10,000-step sampling/execution budget, and explicit call/region nesting + limits. Evidence: `RejectsExcessiveClassicalRegisterCapacity`, + `RejectsExcessiveRegionNesting`, and `RejectsExcessiveFuncCallNesting`. +- The JeFF deserializer's own depth and aggregate-size limits remain valid + because serialized bytes are untrusted external input. This does not justify + the same 64-region precheck in each transformation pass. + +### Changes withdrawn from every area + +- Remove pass-entry `verifyProgramMetadata`, `verifyLinearity`, and generic + whole-module verification when they only repeat the declared input invariant. + Retain output verification and checks at compiler, external-input, or + target-representability boundaries. +- Remove defensive `hasOneUse()` branches added solely for zero-use or multi-use + QCO/QTensor programs. The owning linearity validator rejects those programs + before a pass runs. +- Remove every matching invalid-input regression, including nonlinear QCO + conversion, normalization, unrolling, mapping, synthesis, lifting, merging, + and classical-control fixtures; duplicate-static and static-reacquisition + QCO-to-QC fixtures; unused-output gate and barrier canonicalization fixtures; + and all additions from closed `#2303` and `#2309`. +- Remove the shared `verifyRegionNestingDepth(..., 64)` helper, pass-entry + calls, TableGen promises, dependencies, documentation, and deep-region tests. + Keep only bounds owned by an actual recursive resource boundary. +- Remove manual worklists that merely replace native MLIR walks. `#2300` and + `#2306` established no supported-path failure or better finite bound. +- Remove registration-only assertions and dependency additions unless an + executed reproducer demonstrates an unloaded-dialect failure. +- Remove branch copies of all merged or focused findings. Their implementation + and regression live in `#2290`, `#2291`, `#2293`, `#2294`, `#2295`, `#2296`, + `#2300`, `#2301`, `#2302`, `#2304`, `#2307`, and `#2308`. Remove all code from + closed `#2303`, `#2305`, `#2306`, and `#2309`. + ## Context and Orientation Pass declarations live below mlir/include/mlir in TableGen files and their diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 46899b5f80..27e0382eea 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -39,7 +39,6 @@ #include #include #include -#include #include #include #include @@ -57,16 +56,6 @@ namespace mqt { namespace nb = nanobind; using namespace nb::literals; -// Keep the catch in this binary: Darwin cannot reliably match standard-library -// exception RTTI in nanobind's split-mode backend. -static void translateRuntimeError(const std::exception_ptr& error, void*) { - try { - std::rethrow_exception(error); - } catch (const std::runtime_error& exception) { - PyErr_SetString(PyExc_RuntimeError, exception.what()); - } -} - template [[nodiscard]] static T takeResult(std::optional&& result) { if (!result) { @@ -333,8 +322,6 @@ generateBenchmark(const std::string_view instanceSpecificationJSON) { } NB_MODULE(MQT_CORE_MODULE_NAME, m) { - nb::register_exception_translator(&translateRuntimeError); - m.doc() = "MQT Core MLIR compiler bindings."; nb::module_::import_("typing"); diff --git a/docs/mlir/OpenQASM.md b/docs/mlir/OpenQASM.md index 488628c2b7..a8244c56e1 100644 --- a/docs/mlir/OpenQASM.md +++ b/docs/mlir/OpenQASM.md @@ -203,8 +203,6 @@ 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. -Export accepts at most 64 region-owning operations along any IR path. - 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 limited to 1,048,576 bits. diff --git a/mlir/include/mlir/Conversion/JeffToQCO/JeffToQCO.td b/mlir/include/mlir/Conversion/JeffToQCO/JeffToQCO.td index c89c9d94e4..b5f3a62c19 100644 --- a/mlir/include/mlir/Conversion/JeffToQCO/JeffToQCO.td +++ b/mlir/include/mlir/Conversion/JeffToQCO/JeffToQCO.td @@ -14,8 +14,6 @@ def JeffToQCO : Pass<"jeff-to-qco", "mlir::ModuleOp"> { let description = [{ This pass converts all operations from the `jeff` dialect to their equivalent operations in the QCO dialect. It ensures that the returned module is a valid QCO module. - Input may contain at most 64 region-owning operations along any IR path. - Note that this pass is still in development as QCO and `jeff` do not have full feature parity yet. Known limitations: diff --git a/mlir/include/mlir/Conversion/QCOToJeff/QCOToJeff.td b/mlir/include/mlir/Conversion/QCOToJeff/QCOToJeff.td index a3e28a2acb..578c1409a3 100644 --- a/mlir/include/mlir/Conversion/QCOToJeff/QCOToJeff.td +++ b/mlir/include/mlir/Conversion/QCOToJeff/QCOToJeff.td @@ -14,19 +14,15 @@ def QCOToJeff : Pass<"qco-to-jeff", "mlir::ModuleOp"> { let description = [{ This pass converts all operations from the QCO dialect to their equivalent operations in the `jeff` dialect. It ensures that the returned module is a valid `jeff` module that can be serialized. - Input may contain at most 64 region-owning operations along any IR path. - Note that this pass is still in development as QCO and `jeff` do not have full feature parity yet. The module must contain exactly one defined, single-block function marked with `mqt.entry_point`, and static and dynamic allocation modes must not be - mixed. Static qubit indices must be unique. + mixed. `qco.static` is currently converted to `jeff.qubit_alloc` because `jeff` does not yet represent static (index-based) qubits. As the index is not preserved in `jeff`, it is not possible to round-tripping static qubits. }]; - let dependentDialects = ["mlir::arith::ArithDialect", - "mlir::func::FuncDialect", "mlir::jeff::JeffDialect", - "mlir::qc::QCDialect", "mlir::qco::QCODialect"]; + let dependentDialects = ["mlir::jeff::JeffDialect"]; } diff --git a/mlir/include/mlir/Conversion/QCOToQC/QCOToQC.td b/mlir/include/mlir/Conversion/QCOToQC/QCOToQC.td index 6fe32130af..18fdf68fa7 100644 --- a/mlir/include/mlir/Conversion/QCOToQC/QCOToQC.td +++ b/mlir/include/mlir/Conversion/QCOToQC/QCOToQC.td @@ -14,15 +14,8 @@ def QCOToQC : Pass<"qco-to-qc", "mlir::ModuleOp"> { let description = [{ This pass converts all operations from the QCO dialect to their equivalent operations in the QC dialect. It handles the transformation of qubit values in QCO to qubit references in QC, ensuring that the semantics of quantum operations are preserved during the conversion process. - - Every physical static-qubit identity must have at most one live QCO wire. - A static index may be reacquired after its previous wire is explicitly sunk - earlier in the same block. - Input may contain at most 64 region-owning operations along any IR path. }]; - let dependentDialects = ["mlir::cf::ControlFlowDialect", - "mlir::func::FuncDialect", - "mlir::memref::MemRefDialect", "mlir::qc::QCDialect", - "mlir::scf::SCFDialect"]; + let dependentDialects = ["mlir::memref::MemRefDialect", + "mlir::qc::QCDialect"]; } diff --git a/mlir/include/mlir/Conversion/QCToQCO/QCToQCO.td b/mlir/include/mlir/Conversion/QCToQCO/QCToQCO.td index f6b10d885b..6b1c2d8868 100644 --- a/mlir/include/mlir/Conversion/QCToQCO/QCToQCO.td +++ b/mlir/include/mlir/Conversion/QCToQCO/QCToQCO.td @@ -14,12 +14,8 @@ def QCToQCO : Pass<"qc-to-qco", "mlir::ModuleOp"> { let description = [{ This pass converts all operations from the QC dialect to their equivalent operations in the QCO dialect. It handles the transformation of qubit references in QC to qubit values in QCO, ensuring that the semantics of quantum operations are preserved during the conversion process. - Input may contain at most 64 region-owning operations along any IR path. }]; - let dependentDialects = ["mlir::arith::ArithDialect", - "mlir::cf::ControlFlowDialect", - "mlir::func::FuncDialect", "mlir::qco::QCODialect", - "mlir::qtensor::QTensorDialect", - "mlir::scf::SCFDialect"]; + let dependentDialects = ["mlir::arith::ArithDialect", "mlir::qco::QCODialect", + "mlir::qtensor::QTensorDialect"]; } diff --git a/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td b/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td index 73dad042b3..8e779c23b6 100644 --- a/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td +++ b/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td @@ -22,7 +22,6 @@ def QCToQIRAdaptive : Pass<"qc-to-qir-adaptive", "mlir::ModuleOp"> { - The entry function must be marked with `mqt.entry_point`. - The entry function must be defined and have no arguments. - QC, CBit, and MemRef operations must occur only in the entry function. - - Input may contain at most 64 region-owning operations along any IR path. - The total width of all `cbit.alloc` operations must not exceed 1,048,576 (`1 << 20`) classical result slots per module. - Existing `__quantum__` runtime symbols must be Func or LLVM function @@ -45,8 +44,5 @@ def QCToQIRAdaptive : Pass<"qc-to-qir-adaptive", "mlir::ModuleOp"> { MLIR's built-in conversions; other dialects are preserved. }]; - let dependentDialects = ["mlir::arith::ArithDialect", - "mlir::cf::ControlFlowDialect", - "mlir::LLVM::LLVMDialect", "mlir::qc::QCDialect", - "mlir::qco::QCODialect"]; + let dependentDialects = ["mlir::LLVM::LLVMDialect"]; } diff --git a/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td b/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td index 4c63833a65..79b1e35b38 100644 --- a/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td +++ b/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td @@ -24,7 +24,6 @@ def QCToQIRBase : Pass<"qc-to-qir-base", "mlir::ModuleOp"> { - The input entry function must consist of a single block. Multi-block input functions are currently not supported. - QC, CBit, and MemRef operations must occur only in the entry function. - - Input may contain at most 64 region-owning operations along any IR path. - The total width of all `cbit.alloc` operations must not exceed 1,048,576 (`1 << 20`) classical result slots per module. - Existing `__quantum__` runtime symbols must be Func or LLVM function @@ -55,7 +54,5 @@ def QCToQIRBase : Pass<"qc-to-qir-base", "mlir::ModuleOp"> { built-in conversions; other dialects are preserved. }]; - let dependentDialects = ["mlir::arith::ArithDialect", - "mlir::LLVM::LLVMDialect", "mlir::qc::QCDialect", - "mlir::qco::QCODialect"]; + let dependentDialects = ["mlir::LLVM::LLVMDialect"]; } diff --git a/mlir/include/mlir/Dialect/MQT/Transforms/Passes.td b/mlir/include/mlir/Dialect/MQT/Transforms/Passes.td index 29715c5d9e..5b4685d8b4 100644 --- a/mlir/include/mlir/Dialect/MQT/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/MQT/Transforms/Passes.td @@ -51,8 +51,7 @@ def UnrollModifiers : Pass<"unroll-modifiers", "mlir::ModuleOp"> { them. Modifiers nested in a `pow` body are unrolled either way. If a modifier cannot be unrolled, the modifier is left untouched. Such - modifiers are skipped silently. The pass fails before mutation when QCO - linearity is violated. + modifiers are skipped silently; the pass never fails. }]; } diff --git a/mlir/include/mlir/Dialect/MQT/Utils/ConstantFolding.h b/mlir/include/mlir/Dialect/MQT/Utils/ConstantFolding.h index 4b79c9a7d8..52187ad578 100644 --- a/mlir/include/mlir/Dialect/MQT/Utils/ConstantFolding.h +++ b/mlir/include/mlir/Dialect/MQT/Utils/ConstantFolding.h @@ -21,11 +21,11 @@ namespace mlir::mqt { /// Convert a floating-point or integer attribute to a double. [[nodiscard]] std::optional attributeToDouble(Attribute attr); -/// Convert a direct constant-like value to a double. +/// Convert a direct arithmetic constant to a double. [[nodiscard]] std::optional valueToDouble(Value value); /** - * Iteratively constant-fold a pure SSA expression DAG to an attribute. + * Recursively constant-fold a pure SSA expression DAG to an attribute. * * The cache memoizes successful and failed evaluations so shared operands are * resolved once. @@ -37,10 +37,10 @@ namespace mlir::mqt { valueToConstantAttr(Value value, DenseMap>& cache); -/// Iteratively constant-fold a pure SSA expression DAG to an attribute. +/// Recursively constant-fold a pure SSA expression DAG to an attribute. [[nodiscard]] std::optional valueToConstantAttr(Value value); -/// Iteratively constant-fold a pure SSA expression DAG to a double. +/// Recursively constant-fold a pure SSA expression DAG to a double. [[nodiscard]] std::optional valueToConstantDouble(Value value); } // namespace mlir::mqt diff --git a/mlir/include/mlir/Dialect/MQT/Utils/Modifiers.h b/mlir/include/mlir/Dialect/MQT/Utils/Modifiers.h index 159f1d74c8..7941b398eb 100644 --- a/mlir/include/mlir/Dialect/MQT/Utils/Modifiers.h +++ b/mlir/include/mlir/Dialect/MQT/Utils/Modifiers.h @@ -99,10 +99,7 @@ template const size_t index) { auto unitaries = llvm::make_filter_range( block, [](Operation& op) { return isa(op); }); - auto it = unitaries.begin(); - for (size_t i = 0; i < index && it != unitaries.end(); ++i) { - ++it; - } + auto it = std::next(unitaries.begin(), static_cast(index)); if (it == unitaries.end()) { llvm::reportFatalUsageError("Unitary index out of bounds"); } @@ -111,43 +108,22 @@ template template [[nodiscard]] bool containsUnitaryOperation(Operation* operation) { - SmallVector worklist{operation}; - while (!worklist.empty()) { - Operation* current = worklist.pop_back_val(); - if (isa(current)) { - return true; - } - for (Region& region : current->getRegions()) { - for (Block& block : region) { - for (Operation& nested : block) { - worklist.push_back(&nested); - } - } - } - } - return false; + bool found = false; + operation->walk( + [&](Operation* nested) { found |= isa(nested); }); + return found; } /// Return whether @p operation or one of its nested operations is neither a /// unitary nor a terminator. template [[nodiscard]] bool containsSupportingOperation(Operation* operation) { - SmallVector worklist{operation}; - while (!worklist.empty()) { - Operation* current = worklist.pop_back_val(); - if (!isa(current) && - !current->hasTrait()) { - return true; - } - for (Region& region : current->getRegions()) { - for (Block& block : region) { - for (Operation& nested : block) { - worklist.push_back(&nested); - } - } - } - } - return false; + bool found = false; + operation->walk([&](Operation* nested) { + found |= !isa(nested) && + !nested->hasTrait(); + }); + return found; } /// Return whether top-level supporting operations may move before a modifier diff --git a/mlir/include/mlir/Dialect/QC/Transforms/Passes.td b/mlir/include/mlir/Dialect/QC/Transforms/Passes.td index eb9450a36c..4dedaf1342 100644 --- a/mlir/include/mlir/Dialect/QC/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QC/Transforms/Passes.td @@ -19,7 +19,6 @@ def ShrinkQubitRegistersPass "Shrink static `qc::QubitType` MemRef registers to accessed indices."; let description = [{ Shrinks one-dimensional static MemRef registers with element type `!qc.qubit` by removing never-read indices and remapping `memref.load` users accordingly. - Input may contain at most 64 region-owning operations along any IR path. }]; } diff --git a/mlir/include/mlir/Dialect/QCO/QCOUtils.h b/mlir/include/mlir/Dialect/QCO/QCOUtils.h index 7067e845e5..449b1f6e3f 100644 --- a/mlir/include/mlir/Dialect/QCO/QCOUtils.h +++ b/mlir/include/mlir/Dialect/QCO/QCOUtils.h @@ -128,13 +128,8 @@ static bool valuesMatchWithinTolerance(Value lhs, Value rhs) { template LogicalResult removeInversePairOneTargetZeroParameter(OpType op, PatternRewriter& rewriter) { - auto output = op.getOutputQubit(0); - if (!output.hasOneUse()) { - return failure(); - } - // Check if the successor is the inverse operation - auto nextOp = dyn_cast(*output.user_begin()); + auto nextOp = dyn_cast(*op.getOutputQubit(0).user_begin()); if (!nextOp) { return failure(); } @@ -163,10 +158,6 @@ removeInversePairTwoTargetZeroParameter(OpType op, PatternRewriter& rewriter, bool symmetric = false, bool swappedTargets = false) { auto output0 = op.getOutputQubit(0); - auto output1 = op.getOutputQubit(1); - if (!output0.hasOneUse() || !output1.hasOneUse()) { - return failure(); - } // Check if the successor is the inverse operation auto nextOp = dyn_cast(*output0.user_begin()); @@ -175,7 +166,7 @@ removeInversePairTwoTargetZeroParameter(OpType op, PatternRewriter& rewriter, } // Both qubits have to point to the same successor - auto nextOp2 = *output1.user_begin(); + auto nextOp2 = *op.getOutputQubit(1).user_begin(); if (nextOp2 != nextOp) { return failure(); } @@ -202,11 +193,6 @@ template LogicalResult removeInversePairThreeTargetZeroParameter(OpType op, PatternRewriter& rewriter) { - if (!llvm::all_of(op.getOutputQubits(), - [](Value output) { return output.hasOneUse(); })) { - return failure(); - } - auto nextOp = dyn_cast(*op.getOutputQubit(0).user_begin()); if (!nextOp || op.getOutputQubits() != nextOp.getInputQubits()) { return failure(); @@ -233,13 +219,8 @@ removeInversePairThreeTargetZeroParameter(OpType op, template LogicalResult mergeOneTargetZeroParameter(OpType op, PatternRewriter& rewriter) { - auto output = op.getOutputQubit(0); - if (!output.hasOneUse()) { - return failure(); - } - // Check if the successor is the same operation - auto nextOp = dyn_cast(*output.user_begin()); + auto nextOp = dyn_cast(*op.getOutputQubit(0).user_begin()); if (!nextOp) { return failure(); } @@ -267,13 +248,8 @@ LogicalResult mergeOneTargetZeroParameter(OpType op, */ template LogicalResult mergeOneTargetOneParameter(OpType op, PatternRewriter& rewriter) { - auto output = op.getOutputQubit(0); - if (!output.hasOneUse()) { - return failure(); - } - // Check if the successor is the same operation - auto nextOp = dyn_cast(*output.user_begin()); + auto nextOp = dyn_cast(*op.getOutputQubit(0).user_begin()); if (!nextOp || op->getBlock() != nextOp->getBlock()) { return failure(); } @@ -316,13 +292,9 @@ static LogicalResult mergeTwoTargetOneParameterImpl(OpType op, OpType nextOp, } auto output0 = op.getOutputQubit(0); - auto output1 = op.getOutputQubit(1); - if (!output0.hasOneUse() || !output1.hasOneUse()) { - return failure(); - } // Both qubits have to point to the same successor - auto nextOp2 = *output1.user_begin(); + auto nextOp2 = *op.getOutputQubit(1).user_begin(); if (nextOp2 != nextOp) { return failure(); } @@ -358,13 +330,8 @@ static LogicalResult mergeTwoTargetOneParameterImpl(OpType op, OpType nextOp, template LogicalResult mergeTwoTargetOneParameter(OpType op, PatternRewriter& rewriter, bool symmetric = false) { - auto output = op.getOutputQubit(0); - if (!output.hasOneUse()) { - return failure(); - } - // Check if the successor is the same operation - auto nextOp = dyn_cast(*output.user_begin()); + auto nextOp = dyn_cast(*op.getOutputQubit(0).user_begin()); if (!nextOp) { return failure(); } @@ -384,13 +351,8 @@ LogicalResult mergeTwoTargetOneParameter(OpType op, PatternRewriter& rewriter, */ template LogicalResult mergeXXPlusMinusYY(OpType op, PatternRewriter& rewriter) { - auto output = op.getOutputQubit(0); - if (!output.hasOneUse()) { - return failure(); - } - // Check if the successor is the same operation - auto nextOp = dyn_cast(*output.user_begin()); + auto nextOp = dyn_cast(*op.getOutputQubit(0).user_begin()); if (!nextOp) { return failure(); } diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index de78e82735..9268fa93fe 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -13,7 +13,7 @@ include "mlir/Pass/PassBase.td" def MergeSingleQubitRotationGates : Pass<"merge-single-qubit-rotation-gates", "mlir::ModuleOp"> { - let dependentDialects = ["mlir::qc::QCDialect", "mlir::qco::QCODialect", + let dependentDialects = ["mlir::qco::QCODialect", "::mlir::arith::ArithDialect", "::mlir::math::MathDialect"]; let summary = "Merge single-qubit gates using quaternion-based fusion"; @@ -60,14 +60,12 @@ def MergeSingleQubitRotationGates the implementation directly invokes the shared global-phase normalization utility to combine, normalize, and remove trivial corrections in their respective scopes. - - Input may contain at most 64 region-owning operations along any IR path. }]; } def FuseSingleQubitUnitaryRuns : Pass<"fuse-single-qubit-unitary-runs", "mlir::ModuleOp"> { - let dependentDialects = ["mlir::qc::QCDialect", "mlir::qco::QCODialect", + let dependentDialects = ["mlir::qco::QCODialect", "::mlir::arith::ArithDialect", "::mlir::math::MathDialect", "::mlir::qtensor::QTensorDialect"]; @@ -88,8 +86,6 @@ def FuseSingleQubitUnitaryRuns The pass also composes supported named gates with dynamic `f64` parameters and emits conservative runtime sequences in the requested basis. Dynamic `pow` and arbitrary dynamic unitaries remain unchanged. - - Input may contain at most 64 region-owning operations along any IR path. }]; let options = [Option< "basis", "basis", "std::string", "\"zyz\"", @@ -108,8 +104,6 @@ def QuantumLoopUnroll Because the pass uses the `mlir::loopUnrollByFactor` utility internally, the same assumptions and restrictions hold for both. See the [MLIR documentation](https://mlir.llvm.org/doxygen/include_2mlir_2Dialect_2SCF_2Utils_2Utils_8h.html) for more information. - - Input may contain at most 64 region-owning operations along any IR path. }]; let options = [Option<"unrollFactor", "unroll-factor", "int64_t", /*default=*/"-1", @@ -123,8 +117,7 @@ def QuantumLoopUnroll //===----------------------------------------------------------------------===// def MappingPass : Pass<"place-and-route", "mlir::ModuleOp"> { - let dependentDialects = ["mlir::qc::QCDialect", "mlir::qco::QCODialect", - "mlir::scf::SCFDialect"]; + let dependentDialects = ["mlir::qco::QCODialect"]; let summary = "This pass ensures that a program meets the connectivity " "constraints of a target architecture."; let description = [{ @@ -170,25 +163,24 @@ def MappingPass : Pass<"place-and-route", "mlir::ModuleOp"> { potentially find a more optimal solution. This is behavior is controlled by the `niterations` parameter. The pass option `ntrials` determines how many random initial layouts the pass explores. If compiled with multi-threading on, these trials will be executed in parallel. - - Within the entry-point body, any IR path may contain at most 64 - region-owning operations. }]; - let options = - [Option<"nlookahead", "nlookahead", "std::size_t", "1", - "The number of lookahead steps. Must be <= 4096.">, - Option<"alpha", "alpha", "float", "1.0F", - "The alpha factor in the cost function. Must be finite and > 0.">, - Option<"lambda", "lambda", "float", "0.5F", - "The lambda factor in the cost function. Must be finite.">, - Option<"niterations", "niterations", "std::size_t", "1", - "The number of forwards and backwards traversal to " - "improve the initial layout. Must be in [1, 4096].">, - Option<"ntrials", "ntrials", "std::size_t", "4", - "The number of (possibly parallel) random trials of " - "the forwards and backwards mechanism. Must be in [1, 4096].">, - Option<"seed", "seed", "std::size_t", "42", - "A seed used for randomization.">]; + let options = [Option<"nlookahead", "nlookahead", "std::size_t", "1", + "The number of lookahead steps. Must be <= 4096.">, + Option<"alpha", "alpha", "float", "1.0F", + "The alpha factor in the cost function. Must be finite " + "and > 0.">, + Option<"lambda", "lambda", "float", "0.5F", + "The lambda factor in the cost function. Must be " + "finite.">, + Option<"niterations", "niterations", "std::size_t", "1", + "The number of forwards and backwards traversal to " + "improve the initial layout. Must be in [1, 4096].">, + Option<"ntrials", "ntrials", "std::size_t", "4", + "The number of (possibly parallel) random trials of " + "the forwards and backwards mechanism. Must be in [1, " + "4096].">, + Option<"seed", "seed", "std::size_t", "42", + "A seed used for randomization.">]; let statistics = [Statistic<"numSwaps", "num-inserted-swaps", "The number of inserted SWAPs">]; } @@ -216,8 +208,7 @@ def PauliTwirl2QGates : Pass<"pauli-twirl-2q-gates", "mlir::ModuleOp"> { } def HadamardLifting : Pass<"hadamard-lifting", "mlir::ModuleOp"> { - let dependentDialects = ["mlir::qc::QCDialect", "mlir::qco::QCODialect", - "::mlir::arith::ArithDialect"]; + let dependentDialects = ["mlir::qco::QCODialect"]; let summary = "This pass attempts to move Hadamard gates as far away from " "measurements as possible by commuting them " "with Pauli gates. This is done in order to apply measurement " @@ -274,7 +265,6 @@ def HadamardLifting : Pass<"hadamard-lifting", "mlir::ModuleOp"> { Afterward the measurement lifting routine could transform the CNOT into a classically controlled Pauli-X. - Input may contain at most 64 region-owning operations along any IR path. }]; } @@ -292,8 +282,6 @@ def MeasurementLifting : Pass<"measurement-lifting", "mlir::ModuleOp"> { also move before `x` and `y`, with the classical result inverted. Earlier measurements enable qubit reuse and subsequent optimizations. - - Input may contain at most 64 region-owning operations along any IR path. }]; } @@ -321,8 +309,6 @@ def ReplaceClassicalControls Replacing quantum controls with classical conditions reduces quantum interactions and can enable qubit reuse and subsequent optimizations. - - Input may contain at most 64 region-owning operations along any IR path. }]; } @@ -344,8 +330,6 @@ def ReuseQubits : Pass<"reuse-qubits", "mlir::ModuleOp"> { Only single-qubit allocations are considered for reuse, no `qtensor` allocations. Use the `mqt-qubit-reuse` pipeline to run the preparation passes followed by this pass. - - Input may contain at most 64 region-owning operations along any IR path. }]; } @@ -362,8 +346,6 @@ def RemoveDeadGates : Pass<"remove-dead-gates", "mlir::ModuleOp"> { measurement-free programs may intentionally represent state preparation or larger quantum building blocks. The `mqt-qubit-reuse` pipeline runs this pass before attempting to reuse qubits. - - Input may contain at most 64 region-owning operations along any IR path. }]; } @@ -407,8 +389,6 @@ def DecomposeMultiControlled Intermediate building blocks may be left as `qco.ctrl` / `qco.rccx` when `min-qubits` keeps them; the greedy rewriter lowers further when the threshold allows. - - Input may contain at most 64 region-owning operations along any IR path. }]; let options = [Option< "minQubits", "min-qubits", "uint64_t", "3", diff --git a/mlir/include/mlir/Dialect/QIR/Transforms/Passes.td b/mlir/include/mlir/Dialect/QIR/Transforms/Passes.td index 140e839d38..d88f5d79fa 100644 --- a/mlir/include/mlir/Dialect/QIR/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QIR/Transforms/Passes.td @@ -26,7 +26,6 @@ def QIRCleanupPass : Pass<"qir-cleanup", "mlir::ModuleOp"> { let description = [{ Removes redundant QIR runtime qubit-array allocation/release pairs that do not contribute to observable behavior, and keeps QIR modules compact. - Input may contain at most 64 region-owning operations along any IR path. }]; } diff --git a/mlir/include/mlir/Dialect/QTensor/Transforms/Passes.td b/mlir/include/mlir/Dialect/QTensor/Transforms/Passes.td index 6ec4913444..cfc322d3cf 100644 --- a/mlir/include/mlir/Dialect/QTensor/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QTensor/Transforms/Passes.td @@ -19,7 +19,6 @@ def ShrinkQTensorToFitPass : Pass<"qtensor-shrink-to-fit", "mlir::ModuleOp"> { Shrinks one-dimensional static qtensors by tracing linear tensor chains from `qtensor.dealloc` to `qtensor.alloc` and rebuilding the chain on a compact allocation that only keeps accessed indices. - Input may contain at most 64 region-owning operations along any IR path. }]; } diff --git a/mlir/include/mlir/Support/OperationUtils.h b/mlir/include/mlir/Support/OperationUtils.h deleted file mode 100644 index 430584d29d..0000000000 --- a/mlir/include/mlir/Support/OperationUtils.h +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 -#include - -#include -#include - -namespace mlir { - -/// Verify that no path below `root` exceeds `maximumDepth` region-owning ops. -[[nodiscard]] inline LogicalResult -verifyRegionNestingDepth(Operation* root, size_t maximumDepth) { - SmallVector> worklist{{root, 0}}; - while (!worklist.empty()) { - auto [operation, parentDepth] = worklist.pop_back_val(); - const size_t childDepth = - parentDepth + static_cast(operation->getNumRegions() != 0); - if (childDepth > maximumDepth) { - return operation->emitError() - << "operation nesting exceeds the supported maximum of " - << maximumDepth << " operations with regions"; - } - for (Region& region : operation->getRegions()) { - for (Block& block : region) { - for (Operation& nested : block) { - worklist.emplace_back(&nested, childDepth); - } - } - } - } - return success(); -} - -} // namespace mlir diff --git a/mlir/lib/Compiler/Programs.cpp b/mlir/lib/Compiler/Programs.cpp index e822334190..296fe6a9cd 100644 --- a/mlir/lib/Compiler/Programs.cpp +++ b/mlir/lib/Compiler/Programs.cpp @@ -94,17 +94,6 @@ namespace mlir { -static void pushNestedOperations(Operation* operation, - SmallVectorImpl& worklist) { - for (Region& region : operation->getRegions()) { - for (Block& block : region) { - for (Operation& nested : block) { - worklist.push_back(&nested); - } - } - } -} - std::shared_ptr createCompilerContext() { DialectRegistry registry; registry.insert worklist{mod}; - while (!worklist.empty()) { - Operation* operation = worklist.pop_back_val(); - if (operation->getDialect()->getNamespace() == dialect) { - return true; - } - pushNestedOperations(operation, worklist); - } - return false; + auto found = false; + mod->walk([&](Operation* operation) { + found |= operation->getDialect()->getNamespace() == dialect; + }); + return found; } template @@ -420,23 +405,13 @@ std::optional QCProgram::intoQIR(const QIRProfile profile) && { static size_t countGatesIf(ModuleOp moduleOp, const llvm::function_ref predicate) { - auto entryPoint = mqt::getEntryPoint(moduleOp); - if (!entryPoint) { - return 0; - } size_t count = 0; - SmallVector worklist{entryPoint}; - while (!worklist.empty()) { - Operation* operation = worklist.pop_back_val(); - auto unitary = dyn_cast(operation); - if (unitary) { - count += !isa(unitary) && predicate(unitary); - if (isa(unitary)) { - continue; - } - } - pushNestedOperations(operation, worklist); - } + auto entryPoint = mqt::getEntryPoint(moduleOp); + entryPoint.walk([&](qc::UnitaryOpInterface op) { + count += static_cast(!isa(op) && predicate(op)); + return isa(op) ? WalkResult::skip() + : WalkResult::advance(); + }); return count; } @@ -650,7 +625,7 @@ class JeffDeserializerInputVerifier { module.getVersionPatch() != 0) { return reject("unsupported jeff version; expected 0.3.0"); } - if (stringsSize > maxContainerSize) { + if (stringsSize > MAX_CONTAINER_SIZE) { return reject("jeff module contains too many strings"); } @@ -671,7 +646,7 @@ class JeffDeserializerInputVerifier { return reject("jeff function body must contain an operations list"); } const auto values = definition.getValues(); - if (values.size() > maxContainerSize) { + if (values.size() > MAX_CONTAINER_SIZE) { return reject("jeff function contains too many values"); } for (const auto value : values) { @@ -690,8 +665,8 @@ class JeffDeserializerInputVerifier { } private: - static constexpr uint64_t maxContainerSize = 1U << 20; - static constexpr uint64_t maxRegionDepth = 64; + static constexpr uint64_t MAX_CONTAINER_SIZE = 1U << 20; + static constexpr uint64_t MAX_REGION_DEPTH = 64; [[nodiscard]] LogicalResult reject(const Twine& message) const { return emitError(UnknownLoc::get(context)) << message; @@ -771,7 +746,7 @@ class JeffDeserializerInputVerifier { const std::optional expectedSources, const std::optional expectedTargets, const uint64_t depth) { - if (depth > maxRegionDepth) { + if (depth > MAX_REGION_DEPTH) { return reject("jeff structured control flow exceeds the nesting limit"); } if (expectedSources && region.getSources().size() != *expectedSources) { @@ -794,7 +769,7 @@ class JeffDeserializerInputVerifier { return reject("jeff region must contain an operations list"); } const auto operations = region.getOperations(); - if (operations.size() > maxContainerSize - totalOperations) { + if (operations.size() > MAX_CONTAINER_SIZE - totalOperations) { return reject("jeff module contains too many operations"); } totalOperations += operations.size(); diff --git a/mlir/lib/Conversion/CBitToMemRef/CMakeLists.txt b/mlir/lib/Conversion/CBitToMemRef/CMakeLists.txt index 0af348a6f7..5ac2c0a5df 100644 --- a/mlir/lib/Conversion/CBitToMemRef/CMakeLists.txt +++ b/mlir/lib/Conversion/CBitToMemRef/CMakeLists.txt @@ -14,7 +14,6 @@ add_mlir_conversion_library( LINK_LIBS MLIRArithDialect MLIRCBitDialect - MLIRControlFlowDialect MLIRFuncDialect MLIRFuncTransforms MLIRMemRefDialect diff --git a/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp b/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp index e84f1012de..fbac063378 100644 --- a/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp +++ b/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp @@ -46,7 +46,6 @@ #include #include #include -#include #include #include @@ -1314,11 +1313,6 @@ struct JeffToQCO final : impl::JeffToQCOBase { void runOnOperation() override { MLIRContext* context = &getContext(); auto original = getOperation(); - constexpr size_t maxRegionNesting = 64; - if (failed(verifyRegionNestingDepth(original, maxRegionNesting))) { - signalPassFailure(); - return; - } OwningOpRef converted(original.clone()); auto module = *converted; const auto entryPointName = validateJeffEntryPoint(module); diff --git a/mlir/lib/Conversion/QCOToJeff/CMakeLists.txt b/mlir/lib/Conversion/QCOToJeff/CMakeLists.txt index b2c06b8411..9028a06963 100644 --- a/mlir/lib/Conversion/QCOToJeff/CMakeLists.txt +++ b/mlir/lib/Conversion/QCOToJeff/CMakeLists.txt @@ -21,7 +21,6 @@ add_mlir_conversion_library( MLIRMQTDialect MLIRMQTTransforms MLIRMQTUtils - MLIRQCDialect MLIRQCODialect MLIRTransforms) diff --git a/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp b/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp index ffd3e9d18a..1c9c1ae98e 100644 --- a/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp +++ b/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp @@ -15,10 +15,8 @@ #include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Transforms/GlobalPhaseNormalization.h" #include "mlir/Dialect/MQT/Utils/GatePowering.h" -#include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" @@ -26,7 +24,6 @@ #include #include #include -#include #include #include #include @@ -50,7 +47,6 @@ #include #include #include -#include #include #include @@ -210,23 +206,10 @@ struct LoweringState { } }; +} // namespace + [[nodiscard]] static LogicalResult validateQCOToJeffInput(ModuleOp module) { - if (failed(mqt::verifyProgramMetadata(module)) || - failed(qco::verifyLinearity(module))) { - return failure(); - } - func::FuncOp entryPoint; - for (auto function : module.getOps()) { - if (!mqt::isEntryPoint(function)) { - continue; - } - if (entryPoint) { - module.emitError( - "qco-to-jeff requires exactly one program entry function"); - return failure(); - } - entryPoint = function; - } + auto entryPoint = mqt::getEntryPoint(module); if (!entryPoint) { module.emitError( "qco-to-jeff requires a program entry function marked with " @@ -243,7 +226,6 @@ struct LoweringState { Operation* staticAllocation = nullptr; Operation* dynamicAllocation = nullptr; - DenseSet staticIndices; bool invalid = false; const auto validateType = [&](Type type, Operation* owner) { const auto tensor = dyn_cast(type); @@ -253,17 +235,9 @@ struct LoweringState { invalid = true; } }; - SmallVector worklist{module.getOperation()}; - while (!worklist.empty()) { - Operation* operation = worklist.pop_back_val(); - if (auto staticOp = dyn_cast(operation)) { + module.walk([&](Operation* operation) { + if (isa(operation)) { staticAllocation = operation; - if (!staticIndices.insert(staticOp.getIndex()).second) { - staticOp.emitError( - "qco-to-jeff cannot preserve duplicate static qubit index ") - << staticOp.getIndex(); - invalid = true; - } } else if (isa(operation)) { dynamicAllocation = operation; } @@ -293,16 +267,13 @@ struct LoweringState { for (BlockArgument argument : block.getArguments()) { validateType(argument.getType(), operation); } - for (Operation& nested : block) { - worklist.push_back(&nested); - } } } - } + }); if (invalid) { return failure(); } - if (staticAllocation && dynamicAllocation) { + if (staticAllocation != nullptr && dynamicAllocation != nullptr) { dynamicAllocation->emitError( "qco-to-jeff cannot mix static and dynamic qubit allocations"); return failure(); @@ -310,6 +281,8 @@ struct LoweringState { return success(); } +namespace { + /** * @brief Base class for conversion patterns that need access to the * LoweringState @@ -1959,11 +1932,6 @@ struct QCOToJeff final : impl::QCOToJeffBase { void runOnOperation() override { MLIRContext* context = &getContext(); auto original = getOperation(); - constexpr size_t maxRegionNesting = 64; - if (failed(verifyRegionNestingDepth(original, maxRegionNesting))) { - signalPassFailure(); - return; - } OwningOpRef converted(original.clone()); auto moduleOp = *converted; if (failed(validateQCOToJeffInput(moduleOp))) { diff --git a/mlir/lib/Conversion/QCOToQC/CMakeLists.txt b/mlir/lib/Conversion/QCOToQC/CMakeLists.txt index 56a0a4707e..2fad50932a 100644 --- a/mlir/lib/Conversion/QCOToQC/CMakeLists.txt +++ b/mlir/lib/Conversion/QCOToQC/CMakeLists.txt @@ -15,10 +15,8 @@ add_mlir_conversion_library( QCOToQCIncGen LINK_LIBS MLIRCBitDialect - MLIRControlFlowDialect MLIRQCDialect MLIRQCODialect - MLIRQCOUtils MLIRQTensorDialect MLIRArithDialect MLIRFuncDialect diff --git a/mlir/lib/Conversion/QCOToQC/QCOToQC.cpp b/mlir/lib/Conversion/QCOToQC/QCOToQC.cpp index e272788be0..561c03db80 100644 --- a/mlir/lib/Conversion/QCOToQC/QCOToQC.cpp +++ b/mlir/lib/Conversion/QCOToQC/QCOToQC.cpp @@ -17,13 +17,11 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" #include #include -#include #include #include #include @@ -38,7 +36,6 @@ #include #include #include -#include #include #include @@ -95,45 +92,11 @@ struct LoweringState { } }; -/** Invalidates cached slots for the same memref in enclosing regions. */ -static void invalidateAncestorQTensorCaches(LoweringState& state, - Region* region, Value memref) { - for (auto* current = region->getParentRegion(); current != nullptr; - current = current->getParentRegion()) { - if (auto it = state.extractedIndices.find(current); - it != state.extractedIndices.end()) { - it->second.erase(memref); - } - if (auto it = state.qubitValues.find(current); - it != state.qubitValues.end()) { - it->second.erase(memref); - } - } -} - -template -[[nodiscard]] static LogicalResult visitOperations(Operation* root, - Callback callback) { - SmallVector operations{root}; - for (size_t next = 0; next < operations.size(); ++next) { - auto* operation = operations[next]; - if (failed(callback(operation))) { - return failure(); - } - for (Region& region : operation->getRegions()) { - for (Block& block : region) { - for (auto& nested : block) { - operations.push_back(&nested); - } - } - } - } - return success(); -} +} // namespace [[nodiscard]] static LogicalResult validateAllocationContracts(Operation* root, LoweringState& state) { - return visitOperations(root, [&](Operation* op) { + const auto result = root->walk([&](Operation* op) { std::optional mode; if (isa(op)) { mode = AllocationMode::Static; @@ -141,12 +104,15 @@ validateAllocationContracts(Operation* root, LoweringState& state) { mode = AllocationMode::Dynamic; } if (mode && failed(state.ensureAllocationMode(*mode, op))) { - return failure(); + return WalkResult::interrupt(); } - return success(); + return WalkResult::advance(); }); + return failure(result.wasInterrupted()); } +namespace { + /** * @brief Base class for conversion patterns that need access to lowering state * @@ -294,7 +260,7 @@ validateReferencePreservingYield(Operation* terminator, ValueRange yielded, [[nodiscard]] static LogicalResult validateReferencePreservingYields(Operation* root) { - return visitOperations(root, [&](Operation* op) { + const auto result = root->walk([&](Operation* op) { ValueRange yielded; if (auto yieldOp = dyn_cast(op)) { yielded = yieldOp.getTargets(); @@ -303,26 +269,27 @@ validateReferencePreservingYields(Operation* root) { if (llvm::none_of(yieldOp.getResults(), [](Value value) { return isQuantumStateType(value.getType()); })) { - return success(); + return WalkResult::advance(); } yieldOp.emitOpError( "QCO-to-QC conversion supports quantum state in scf.yield only " "for scf.for and scf.while"); - return failure(); + return WalkResult::interrupt(); } yielded = yieldOp.getResults(); } else if (auto conditionOp = dyn_cast(op)) { yielded = conditionOp.getArgs(); } else { - return success(); + return WalkResult::advance(); } SmallVector inputs(op->getBlock()->getArguments()); if (failed(validateReferencePreservingYield(op, yielded, inputs))) { - return failure(); + return WalkResult::interrupt(); } - return success(); + return WalkResult::advance(); }); + return failure(result.wasInterrupted()); } [[nodiscard]] static SmallVector @@ -513,38 +480,15 @@ struct ConvertQTensorExtractOp final } }; -/** Converts qtensor.insert to an in-place memref.store. */ -struct ConvertQTensorInsertOp final - : StatefulOpConversionPattern { - using StatefulOpConversionPattern::StatefulOpConversionPattern; +/** + * @brief Removes qtensor.insert operations + */ +struct ConvertQTensorInsertOp final : OpConversionPattern { + using OpConversionPattern::OpConversionPattern; LogicalResult matchAndRewrite(qtensor::InsertOp op, OpAdaptor adaptor, ConversionPatternRewriter& rewriter) const override { - auto& state = getState(); - auto* region = op->getParentRegion(); - auto dest = adaptor.getDest(); - auto index = adaptor.getIndex(); - auto scalar = adaptor.getScalar(); - auto& extractedIndices = state.extractedIndices[region][dest]; - auto& qubitValues = state.qubitValues[region][dest]; - if (extractedIndices.contains(index) && - qubitValues.lookup(index) == scalar) { - rewriter.replaceOp(op, dest); - return success(); - } - - memref::StoreOp::create(rewriter, op.getLoc(), scalar, dest, - ValueRange{index}); - invalidateAncestorQTensorCaches(state, region, dest); - - // A dynamic index may alias any previously observed slot. Rebuild the - // cache conservatively, retaining only the value established by this - // store. - extractedIndices.clear(); - qubitValues.clear(); - extractedIndices.insert(index); - qubitValues[index] = scalar; rewriter.replaceOp(op, adaptor.getDest()); return success(); } @@ -1374,19 +1318,13 @@ struct QCOToQC final : impl::QCOToQCBase { void runOnOperation() override { MLIRContext* context = &getContext(); auto original = getOperation(); - constexpr size_t maxRegionNesting = 64; - if (failed(verifyRegionNestingDepth(original, maxRegionNesting))) { - signalPassFailure(); - return; - } OwningOpRef converted(original.clone()); auto module = *converted; // Create state object to track the qubit addressing mode LoweringState state; - if (failed(qco::verifyLinearity(module)) || - failed(validateAllocationContracts(module, state)) || + if (failed(validateAllocationContracts(module, state)) || failed(validateReferencePreservingYields(module))) { signalPassFailure(); return; @@ -1420,8 +1358,8 @@ struct QCOToQC final : impl::QCOToQCBase { // Register operation conversion patterns that do not need state tracking patterns - .add>( typeConverter, context); @@ -1437,9 +1375,9 @@ struct QCOToQC final : impl::QCOToQCBase { ConvertQCOSCFForOp>(typeConverter, context); // Register operation conversion patterns that need state tracking - patterns.add(typeConverter, context, &state); + patterns.add( + typeConverter, context, &state); // Conversion of qco types in func.func signatures // Note: This currently has limitations with signature changes diff --git a/mlir/lib/Conversion/QCToQCO/CMakeLists.txt b/mlir/lib/Conversion/QCToQCO/CMakeLists.txt index 0e7cefa8ab..c4c998c5c0 100644 --- a/mlir/lib/Conversion/QCToQCO/CMakeLists.txt +++ b/mlir/lib/Conversion/QCToQCO/CMakeLists.txt @@ -15,7 +15,6 @@ add_mlir_conversion_library( QCToQCOIncGen LINK_LIBS MLIRCBitDialect - MLIRControlFlowDialect MLIRQCDialect MLIRQCODialect MLIRQTensorDialect diff --git a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp index 226a5cd962..90f9bbc1dc 100644 --- a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp +++ b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp @@ -27,7 +27,6 @@ #include #include #include -#include #include #include #include @@ -48,7 +47,6 @@ #include #include #include -#include #include #include #include @@ -94,6 +92,8 @@ struct AffineRegisterIndex { int64_t offset; }; +} // namespace + /** @brief Peels constant additions and subtractions from a register index. */ [[nodiscard]] static AffineRegisterIndex decomposeRegisterIndex(Value index) { if (const auto constant = getConstantIntValue(index)) { @@ -107,12 +107,12 @@ struct AffineRegisterIndex { if (auto add = index.getDefiningOp()) { if (const auto lhs = getConstantIntValue(add.getLhs())) { next = add.getRhs(); - if (llvm::AddOverflow(offset, *lhs, nextOffset)) { + if (llvm::AddOverflow(offset, *lhs, nextOffset) != 0) { break; } } else if (const auto rhs = getConstantIntValue(add.getRhs())) { next = add.getLhs(); - if (llvm::AddOverflow(offset, *rhs, nextOffset)) { + if (llvm::AddOverflow(offset, *rhs, nextOffset) != 0) { break; } } else { @@ -120,7 +120,7 @@ struct AffineRegisterIndex { } } else if (auto sub = index.getDefiningOp()) { const auto rhs = getConstantIntValue(sub.getRhs()); - if (!rhs || llvm::SubOverflow(offset, *rhs, nextOffset)) { + if (!rhs || llvm::SubOverflow(offset, *rhs, nextOffset) != 0) { break; } next = sub.getLhs(); @@ -154,8 +154,8 @@ isOutsideForInductionRange(int64_t constant, const AffineRegisterIndex& index) { int64_t adjustedLower = 0; int64_t adjustedUpper = 0; - if (llvm::AddOverflow(*lower, index.offset, adjustedLower) || - llvm::AddOverflow(*upper, index.offset, adjustedUpper)) { + if (llvm::AddOverflow(*lower, index.offset, adjustedLower) != 0 || + llvm::AddOverflow(*upper, index.offset, adjustedUpper) != 0) { return false; } return adjustedLower >= adjustedUpper || constant < adjustedLower || @@ -179,6 +179,8 @@ isOutsideForInductionRange(int64_t constant, const AffineRegisterIndex& index) { return false; } +namespace { + /** @brief Qubit allocation mode */ enum class AllocationMode : std::uint8_t { Unset, //!< No allocation mode has been established yet. @@ -228,7 +230,7 @@ struct LoweringState { DenseMap> qubitMap; /// Per-region canonical QC reference for each physical static-qubit index. - DenseMap> staticQubitKeys; + DenseMap> staticQubitKeys; /// Per-region map from stable register identifiers to their latest QTensor /// SSA values. @@ -276,16 +278,18 @@ struct LoweringState { } }; +} // namespace + [[nodiscard]] static LogicalResult validateAllocationMode(Operation* root, LoweringState& state) { const auto result = root->walk([&](Operation* op) { std::optional mode; if (auto staticOp = dyn_cast(op)) { mode = AllocationMode::Static; - } else if (isa(op)) { - mode = AllocationMode::Dynamic; } else if (auto alloc = dyn_cast(op); - alloc && isa(alloc.getType().getElementType())) { + isa(op) || + (alloc && + isa(alloc.getType().getElementType()))) { mode = AllocationMode::Dynamic; } if (mode && failed(state.ensureAllocationMode(*mode, op))) { @@ -296,6 +300,8 @@ validateAllocationMode(Operation* root, LoweringState& state) { return success(!result.wasInterrupted()); } +namespace { + /** * @brief Base class for conversion patterns that need access to lowering state * @@ -2178,11 +2184,6 @@ struct QCToQCO final : impl::QCToQCOBase { void runOnOperation() override { MLIRContext* context = &getContext(); auto original = getOperation(); - constexpr size_t maxRegionNesting = 64; - if (failed(verifyRegionNestingDepth(original, maxRegionNesting))) { - signalPassFailure(); - return; - } OwningOpRef converted(original.clone()); auto moduleOp = *converted; diff --git a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/CMakeLists.txt b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/CMakeLists.txt index 1f6dfa4809..f1d10d5746 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/CMakeLists.txt +++ b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/CMakeLists.txt @@ -21,7 +21,6 @@ add_mlir_conversion_library( MLIRQIRUtils MLIRLLVMDialect MLIRQCDialect - MLIRQCODialect MLIRArithDialect MLIRTransforms MLIRFuncDialect diff --git a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp index 371b277d40..8abe638ea0 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp @@ -18,7 +18,6 @@ #include "mlir/Dialect/MQT/Transforms/GlobalPhaseNormalization.h" #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/IR/QCOps.h" -#include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" #include @@ -46,7 +45,6 @@ #include #include #include -#include #include #include @@ -125,6 +123,31 @@ convertQubitMemRefAllocOp(memref::AllocOp op, memref::AllocOp::Adaptor adaptor, return success(); } +static bool canReleaseInOutputBlock(Operation* release, + const LoweringState& state) { + Block* releaseBlock = release->getBlock(); + SmallVector worklist; + for (Block* successor : releaseBlock->getSuccessors()) { + worklist.push_back(successor); + } + SmallPtrSet visited; + while (!worklist.empty()) { + Block* block = worklist.pop_back_val(); + if (block == releaseBlock) { + return false; + } + if (!visited.insert(block).second) { + continue; + } + for (Block* successor : block->getSuccessors()) { + worklist.push_back(successor); + } + } + + const DominanceInfo dominance(state.outputBlock->getParentOp()); + return dominance.dominates(release, state.outputBlock->getTerminator()); +} + namespace { struct ConvertCBitAllocOp final : StatefulOpConversionPattern { @@ -305,31 +328,6 @@ struct ConvertMemRefLoadOp final : StatefulOpConversionPattern { } }; -static bool canReleaseInOutputBlock(Operation* release, - const LoweringState& state) { - Block* releaseBlock = release->getBlock(); - SmallVector worklist; - for (Block* successor : releaseBlock->getSuccessors()) { - worklist.push_back(successor); - } - SmallPtrSet visited; - while (!worklist.empty()) { - Block* block = worklist.pop_back_val(); - if (block == releaseBlock) { - return false; - } - if (!visited.insert(block).second) { - continue; - } - for (Block* successor : block->getSuccessors()) { - worklist.push_back(successor); - } - } - - const DominanceInfo dominance(state.outputBlock->getParentOp()); - return dominance.dominates(release, state.outputBlock->getTerminator()); -} - /** * @brief Converts memref.dealloc to QIR qubit-array release * @@ -743,11 +741,6 @@ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { void runOnOperation() override { MLIRContext* ctx = &getContext(); auto original = getOperation(); - constexpr size_t maxRegionNesting = 64; - if (failed(verifyRegionNestingDepth(original, maxRegionNesting))) { - signalPassFailure(); - return; - } OwningOpRef converted(original.clone()); auto moduleOp = *converted; LoweringState state; diff --git a/mlir/lib/Conversion/QCToQIR/QIRBase/CMakeLists.txt b/mlir/lib/Conversion/QCToQIR/QIRBase/CMakeLists.txt index 276188e4de..c5afadd429 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRBase/CMakeLists.txt +++ b/mlir/lib/Conversion/QCToQIR/QIRBase/CMakeLists.txt @@ -21,7 +21,6 @@ add_mlir_conversion_library( MLIRMQTTransforms MLIRLLVMDialect MLIRQCDialect - MLIRQCODialect MLIRArithDialect MLIRTransforms MLIRFuncDialect diff --git a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp index 0d4a518302..5e5b1e0c44 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp @@ -17,7 +17,6 @@ #include "mlir/Dialect/MQT/Transforms/GlobalPhaseNormalization.h" #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/IR/QCOps.h" -#include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" #include @@ -47,7 +46,6 @@ #include #include #include -#include #include #include @@ -374,8 +372,6 @@ static void populateQCToQIRBasePatterns(RewritePatternSet& patterns, patterns.add(typeConverter, ctx); } -namespace { - /// Returns whether two QC references lower to the same Base Profile qubit. static bool referencesSameQubit(Value lhs, Value rhs) { if (lhs == rhs) { @@ -450,6 +446,8 @@ static LogicalResult validateBaseOperationOrder(ModuleOp module) { return success(); } +namespace { + /** * @brief Pass for converting QC dialect operations to QIR * @@ -569,11 +567,6 @@ struct QCToQIRBase final : impl::QCToQIRBaseBase { void runOnOperation() override { MLIRContext* ctx = &getContext(); auto original = getOperation(); - constexpr size_t maxRegionNesting = 64; - if (failed(verifyRegionNestingDepth(original, maxRegionNesting))) { - signalPassFailure(); - return; - } OwningOpRef converted(original.clone()); auto moduleOp = *converted; LoweringState state; diff --git a/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp b/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp index 215ca1150a..af574f399d 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp @@ -56,22 +56,6 @@ namespace mlir { using namespace qc; using namespace qir; -template -static void walkOperationsIteratively(Operation* root, Callback&& callback) { - SmallVector worklist{root}; - while (!worklist.empty()) { - Operation* operation = worklist.pop_back_val(); - callback(operation); - for (Region& region : operation->getRegions()) { - for (Block& block : region) { - for (Operation& nested : block) { - worklist.push_back(&nested); - } - } - } - } -} - LogicalResult LoweringState::ensureAllocationMode(AllocationMode requested, Operation* op) { if (allocationMode == AllocationMode::Unset) { @@ -454,9 +438,6 @@ Value getResultPtr(LoweringState& state, Operation* op, LogicalResult validateQIRConversionInput(ModuleOp moduleOp, bool requireSingleBlock, LoweringState& state) { - if (failed(mqt::verifyProgramMetadata(moduleOp))) { - return failure(); - } for (Operation& operation : moduleOp.getBody()->getOperations()) { const auto symbol = SymbolTable::getSymbolName(&operation); if (!symbol || !symbol.getValue().starts_with("__quantum__")) { @@ -474,18 +455,7 @@ LogicalResult validateQIRConversionInput(ModuleOp moduleOp, << "QIR conversion reserves runtime symbol " << symbol.getValue() << " for a function declaration"; } - func::FuncOp entryPoint; - for (auto function : moduleOp.getOps()) { - if (!mqt::isEntryPoint(function)) { - continue; - } - if (entryPoint) { - moduleOp.emitError("QIR conversion requires exactly one program entry " - "function marked with mqt.entry_point"); - return failure(); - } - entryPoint = function; - } + auto entryPoint = mqt::getEntryPoint(moduleOp); if (!entryPoint) { moduleOp.emitError( "QIR conversion requires a program entry function marked with " @@ -515,7 +485,7 @@ LogicalResult validateQIRConversionInput(ModuleOp moduleOp, } bool invalid = false; - walkOperationsIteratively(moduleOp, [&](Operation* operation) { + moduleOp.walk([&](Operation* operation) { if (invalid || operation == moduleOp || operation == entryPoint) { return; } @@ -537,7 +507,7 @@ LogicalResult validateQIRConversionInput(ModuleOp moduleOp, } if (requireSingleBlock) { - walkOperationsIteratively(moduleOp, [&](Operation* operation) { + moduleOp.walk([&](Operation* operation) { if (invalid || operation == moduleOp || isa(operation) || !isa(operation)) { return; @@ -551,7 +521,7 @@ LogicalResult validateQIRConversionInput(ModuleOp moduleOp, return failure(); } - walkOperationsIteratively(entryPoint, [&](Operation* operation) { + entryPoint.walk([&](Operation* operation) { if (invalid || operation == entryPoint) { return; } @@ -578,7 +548,7 @@ LogicalResult validateQIRConversionInput(ModuleOp moduleOp, } } - walkOperationsIteratively(entryPoint, [&](Operation* operation) { + entryPoint.walk([&](Operation* operation) { if (auto op = dyn_cast(operation)) { const auto type = op.getType(); if (type.getRank() != 1 || !isa(type.getElementType())) { @@ -605,14 +575,14 @@ LogicalResult validateQIRConversionInput(ModuleOp moduleOp, if (!requireSingleBlock) { Operation* staticAllocation = nullptr; Operation* dynamicAllocation = nullptr; - walkOperationsIteratively(entryPoint, [&](Operation* operation) { + entryPoint.walk([&](Operation* operation) { if (isa(operation)) { staticAllocation = operation; } else if (isa(operation)) { dynamicAllocation = operation; } }); - if (staticAllocation && dynamicAllocation) { + if (staticAllocation != nullptr && dynamicAllocation != nullptr) { dynamicAllocation->emitError( "QIR Adaptive Profile conversion cannot mix static and dynamic " "qubit allocations"); @@ -623,7 +593,7 @@ LogicalResult validateQIRConversionInput(ModuleOp moduleOp, SmallVector> loadedRegisterElements; uint64_t freshStaticQubitIds = 0; - walkOperationsIteratively(entryPoint, [&](Operation* operation) { + entryPoint.walk([&](Operation* operation) { if (auto op = dyn_cast(operation)) { const auto rawIndex = op.getIndex(); if (rawIndex >= @@ -688,7 +658,7 @@ LogicalResult prepareClassicalResults(Operation* moduleOp, constexpr uint64_t maxClassicalResultSlots = 1U << 20; uint64_t numClassicalResultSlots = 0; bool exceedsResultLimit = false; - walkOperationsIteratively(moduleOp, [&](Operation* operation) { + moduleOp->walk([&](Operation* operation) { auto allocOp = dyn_cast(operation); if (!allocOp || exceedsResultLimit) { return; @@ -721,7 +691,7 @@ LogicalResult prepareClassicalResults(Operation* moduleOp, SmallVector consumedStores; SmallVector returnRewrites; SmallVector entryPoints; - walkOperationsIteratively(moduleOp, [&](Operation* operation) { + moduleOp->walk([&](Operation* operation) { if (auto function = dyn_cast(operation); function && mqt::isEntryPoint(function)) { entryPoints.push_back(function); @@ -729,7 +699,7 @@ LogicalResult prepareClassicalResults(Operation* moduleOp, }); for (auto funcOp : entryPoints) { - walkOperationsIteratively(funcOp, [&](Operation* operation) { + funcOp.walk([&](Operation* operation) { if (operation->getParentOfType() != funcOp) { return; } @@ -765,7 +735,7 @@ LogicalResult prepareClassicalResults(Operation* moduleOp, state.returnedCregs.push_back(registerIndex); }; - walkOperationsIteratively(funcOp, [&](Operation* operation) { + funcOp.walk([&](Operation* operation) { if (operation->getParentOfType() != funcOp) { return; } @@ -797,7 +767,7 @@ LogicalResult prepareClassicalResults(Operation* moduleOp, }); if (returnRewrites.size() > 1) { - auto recordedReturn = + auto* recordedReturn = llvm::find_if(returnRewrites, [](const ReturnRewrite& rewrite) { return rewrite.recordsClassicalOutput; }); @@ -809,7 +779,7 @@ LogicalResult prepareClassicalResults(Operation* moduleOp, } } - walkOperationsIteratively(funcOp, [&](Operation* operation) { + funcOp.walk([&](Operation* operation) { if (operation->getParentOfType() != funcOp) { return; } diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index 08dd046671..12c10d4e57 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -16,7 +16,6 @@ #include "mlir/Dialect/QTensor/IR/QTensorOps.h" #include -#include #include #include #include @@ -27,11 +26,15 @@ #include #include #include +#include #include #include #include +#include +#include #include +#include using namespace mlir; using namespace mlir::mqt; @@ -40,8 +43,8 @@ using namespace mlir::mqt; void MQTDialect::initialize() {} -[[nodiscard]] static LogicalResult verifyEntryPoint(Operation* operation, - NamedAttribute attribute) { +[[nodiscard]] static LogicalResult +verifyEntryPoint(Operation* operation, const NamedAttribute attribute) { if (!isa(attribute.getValue())) { return operation->emitError() << "attribute '" << attribute.getName().getValue() @@ -62,7 +65,7 @@ void MQTDialect::initialize() {} } [[nodiscard]] static LogicalResult verifyName(Operation* operation, - NamedAttribute attribute) { + const NamedAttribute attribute) { const auto name = dyn_cast(attribute.getValue()); if (!name) { return operation->emitError() @@ -82,8 +85,8 @@ void MQTDialect::initialize() {} return success(); } -[[nodiscard]] static LogicalResult verifyParameterGroup(Operation* operation, - Attribute attribute) { +[[nodiscard]] static LogicalResult +verifyParameterGroup(Operation* operation, const Attribute attribute) { const auto group = dyn_cast(attribute); const auto identity = group ? group.getAs("identity") : nullptr; const auto groupName = group ? group.getAs("name") : nullptr; @@ -112,7 +115,7 @@ void MQTDialect::initialize() {} [[nodiscard]] static LogicalResult verifyInputGroup(FunctionOpInterface function, Operation* operation, - unsigned argIndex, Attribute attribute) { + const unsigned argIndex, const Attribute attribute) { const auto inputName = function.getArgAttrOfType( argIndex, MQTDialect::InputNameAttrHelper::getNameStr()); if (!inputName) { @@ -152,7 +155,7 @@ verifyInputGroup(FunctionOpInterface function, Operation* operation, } [[nodiscard]] static LogicalResult -verifyRegisterName(Operation* operation, NamedAttribute attribute) { +verifyRegisterName(Operation* operation, const NamedAttribute attribute) { if (failed(verifyName(operation, attribute))) { return failure(); } @@ -173,8 +176,9 @@ verifyRegisterName(Operation* operation, NamedAttribute attribute) { return success(); } -LogicalResult MQTDialect::verifyOperationAttribute(Operation* operation, - NamedAttribute attribute) { +LogicalResult +MQTDialect::verifyOperationAttribute(Operation* operation, + const NamedAttribute attribute) { if (attribute.getName() == EntryPointAttrHelper::getNameStr()) { return verifyEntryPoint(operation, attribute); } @@ -198,10 +202,9 @@ LogicalResult MQTDialect::verifyOperationAttribute(Operation* operation, << "unknown MQT attribute '" << attribute.getName().getValue() << "'"; } -LogicalResult MQTDialect::verifyRegionArgAttribute(Operation* operation, - unsigned regionIndex, - unsigned argIndex, - NamedAttribute attribute) { +LogicalResult MQTDialect::verifyRegionArgAttribute( + Operation* operation, const unsigned regionIndex, const unsigned argIndex, + const NamedAttribute attribute) { const auto attributeName = attribute.getName(); if (attributeName != InputNameAttrHelper::getNameStr() && attributeName != ParameterGroupAttrHelper::getNameStr()) { @@ -229,7 +232,7 @@ LogicalResult MQTDialect::verifyRegionArgAttribute(Operation* operation, LogicalResult MQTDialect::verifyRegionResultAttribute( Operation* operation, unsigned /*regionIndex*/, unsigned /*resultIndex*/, - NamedAttribute attribute) { + const NamedAttribute attribute) { return operation->emitError() << "attribute '" << attribute.getName().getValue() << "' is not valid on a region result"; @@ -283,38 +286,29 @@ verifyProgramNames(FunctionOpInterface function) { LogicalResult mlir::mqt::verifyProgramMetadata(ModuleOp moduleOp) { Operation* entryPoint = nullptr; - SmallVector worklist; - for (Operation& operation : moduleOp.getBody()->getOperations()) { - worklist.push_back(&operation); - } - while (!worklist.empty()) { - Operation* operation = worklist.pop_back_val(); + const auto walkResult = moduleOp.walk([&](Operation* operation) { if (isEntryPoint(operation)) { auto function = dyn_cast(operation); if (!function || operation->getParentOp() != moduleOp.getOperation() || function.getFunctionBody().empty()) { - return operation->emitError( + operation->emitError( "program entry point must be a defined module-level function"); + return WalkResult::interrupt(); } if (entryPoint != nullptr) { - return operation->emitError( + operation->emitError( "module must contain at most one program entry point"); + return WalkResult::interrupt(); } entryPoint = operation; } if (auto function = dyn_cast(operation); function && failed(verifyProgramNames(function))) { - return failure(); + return WalkResult::interrupt(); } - for (Region& region : operation->getRegions()) { - for (Block& block : region) { - for (Operation& nested : block) { - worklist.push_back(&nested); - } - } - } - } - return success(); + return WalkResult::advance(); + }); + return walkResult.wasInterrupted() ? failure() : success(); } void mlir::mqt::setEntryPoint(Operation* operation) { diff --git a/mlir/lib/Dialect/MQT/Transforms/NormalizeGlobalPhases.cpp b/mlir/lib/Dialect/MQT/Transforms/NormalizeGlobalPhases.cpp index 21401288b0..58d1e92f93 100644 --- a/mlir/lib/Dialect/MQT/Transforms/NormalizeGlobalPhases.cpp +++ b/mlir/lib/Dialect/MQT/Transforms/NormalizeGlobalPhases.cpp @@ -8,7 +8,6 @@ * Licensed under the MIT License */ -#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Transforms/GlobalPhaseNormalization.h" #include "mlir/Dialect/MQT/Transforms/Passes.h" #include "mlir/Dialect/MQT/Utils/Angles.h" @@ -17,7 +16,6 @@ #include "mlir/Dialect/MQT/Utils/Parameters.h" #include "mlir/Dialect/QC/IR/QCOps.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/QCOUtils.h" #include #include @@ -39,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -203,6 +202,8 @@ struct PhaseContribution final { using PhaseContributions = std::array, 2>; +} // namespace + [[nodiscard]] static constexpr std::size_t getDialectIndex(PhaseDialect dialect) { return static_cast(dialect); @@ -218,44 +219,52 @@ static void addContribution(PhaseContributions& contributions, aggregate = std::move(contribution); } -} // namespace +/// Collect a pure, body-local dependency slice in topological order. +static bool collectHoistableSlice(Value value, Block& body, + SmallPtrSetImpl& visiting, + SmallPtrSetImpl& collected, + SmallVectorImpl& ordered) { + if (auto blockArg = dyn_cast(value)) { + return blockArg.getOwner() != &body; + } + + auto* definingOp = value.getDefiningOp(); + if (definingOp == nullptr || definingOp->getBlock() != &body) { + return true; + } + if (collected.contains(definingOp)) { + return true; + } + if (!visiting.insert(definingOp).second || definingOp->getNumRegions() != 0 || + !isPure(definingOp) || !isSpeculatable(definingOp)) { + return false; + } + for (auto operand : definingOp->getOperands()) { + if (!collectHoistableSlice(operand, body, visiting, collected, ordered)) { + return false; + } + } + visiting.erase(definingOp); + collected.insert(definingOp); + ordered.push_back(definingOp); + return true; +} /// Make all dynamic leaves of @p expression available before @p modifier. static bool hoistExpressionBefore(const PhaseExpression& expression, Block& body, Operation* modifier, RewriterBase& rewriter) { + SmallPtrSet visiting; SmallPtrSet collected; - SmallVector worklist; SmallVector ordered; bool hoistable = true; - expression.forEachValue([&](Value value) { worklist.push_back(value); }); - while (hoistable && !worklist.empty()) { - auto value = worklist.pop_back_val(); - if (auto blockArg = dyn_cast(value)) { - if (blockArg.getOwner() == &body) { - hoistable = false; - } - continue; - } - - auto* definingOp = value.getDefiningOp(); - if (definingOp == nullptr || definingOp->getBlock() != &body || - !collected.insert(definingOp).second) { - continue; - } - if (definingOp->getNumRegions() != 0 || !isPure(definingOp) || - !isSpeculatable(definingOp)) { + expression.forEachValue([&](Value value) { + if (hoistable && + !collectHoistableSlice(value, body, visiting, collected, ordered)) { hoistable = false; - continue; } - llvm::append_range(worklist, definingOp->getOperands()); - } + }); if (hoistable) { - for (auto& op : body) { - if (collected.contains(&op)) { - ordered.push_back(&op); - } - } for (auto* op : ordered) { rewriter.moveOpBefore(op, modifier); } @@ -269,40 +278,24 @@ class GlobalPhaseNormalizer final { public: explicit GlobalPhaseNormalizer(MLIRContext* context) : rewriter(context) {} - void normalize(Region& root) { - struct RegionWorkItem { - Region* region; - Operation* extractionBoundary; - }; - struct BlockWorkItem { - Block* block; - Operation* extractionBoundary; - }; - - SmallVector regionWorklist{{&root, nullptr}}; - SmallVector blocks; - while (!regionWorklist.empty()) { - auto [region, extractionBoundary] = regionWorklist.pop_back_val(); - for (auto& block : *region) { - blocks.push_back({&block, extractionBoundary}); - for (auto& op : block) { - auto* nestedBoundary = getExtractionBoundary(&op); - for (auto& nested : op.getRegions()) { - regionWorklist.push_back({&nested, nestedBoundary}); - } + void normalize(Region& root) { normalizeRegion(root, nullptr); } + +private: + void normalizeRegion(Region& region, Operation* extractionBoundary) { + for (auto& block : region) { + for (auto& op : block) { + auto* nestedBoundary = getExtractionBoundary(&op); + for (auto& nested : op.getRegions()) { + normalizeRegion(nested, nestedBoundary); } } - } - - for (auto [block, extractionBoundary] : llvm::reverse(blocks)) { - auto contributions = normalizeBlock(*block, extractionBoundary); + auto contributions = normalizeBlock(block, extractionBoundary); if (extractionBoundary != nullptr) { applyExtractionBoundary(extractionBoundary, std::move(contributions)); } } } -private: [[nodiscard]] static Operation* getExtractionBoundary(Operation* op) { if (isa(op)) { return op; @@ -566,10 +559,6 @@ struct NormalizeGlobalPhases final } // namespace LogicalResult normalizeGlobalPhases(ModuleOp moduleOp) { - if (failed(verifyProgramMetadata(moduleOp)) || - failed(qco::verifyLinearity(moduleOp))) { - return failure(); - } GlobalPhaseNormalizer normalizer(moduleOp.getContext()); normalizer.normalize(moduleOp.getRegion()); return success(); diff --git a/mlir/lib/Dialect/MQT/Transforms/UnrollModifiers.cpp b/mlir/lib/Dialect/MQT/Transforms/UnrollModifiers.cpp index a1a9b2910d..cd3a63767b 100644 --- a/mlir/lib/Dialect/MQT/Transforms/UnrollModifiers.cpp +++ b/mlir/lib/Dialect/MQT/Transforms/UnrollModifiers.cpp @@ -15,7 +15,6 @@ #include "mlir/Dialect/QC/IR/QCOps.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/QCOUtils.h" #include #include @@ -337,35 +336,16 @@ namespace { struct UnrollModifiers final : impl::UnrollModifiersBase { protected: void runOnOperation() override { - if (failed(qco::verifyLinearity(getOperation()))) { - signalPassFailure(); - return; - } - - SmallVector reversePostorder; - SmallVector worklist{getOperation()}; - while (!worklist.empty()) { - Operation* op = worklist.pop_back_val(); - reversePostorder.push_back(op); - for (Region& region : op->getRegions()) { - for (Block& block : region) { - for (Operation& nested : block) { - worklist.push_back(&nested); - } - } - } - } - SmallVector modifiers; - for (Operation* op : llvm::reverse(reversePostorder)) { + getOperation()->walk([&](Operation* op) { if (isa(op)) { modifiers.push_back(op); } - } + }); - // Unrolling nested modifiers before their parents reaches a fixpoint in a - // single sweep. + // The walk visits nested modifiers before their parents, so unrolling the + // collected modifiers in order reaches a fixpoint in a single sweep. IRRewriter rewriter(&getContext()); for (auto* modifier : modifiers) { llvm::TypeSwitch(modifier) diff --git a/mlir/lib/Dialect/MQT/Utils/Angles.cpp b/mlir/lib/Dialect/MQT/Utils/Angles.cpp index bb127fc0d1..207499bfac 100644 --- a/mlir/lib/Dialect/MQT/Utils/Angles.cpp +++ b/mlir/lib/Dialect/MQT/Utils/Angles.cpp @@ -14,6 +14,7 @@ #include "mlir/Dialect/MQT/Utils/Parameters.h" #include +#include #include #include #include diff --git a/mlir/lib/Dialect/MQT/Utils/ConstantFolding.cpp b/mlir/lib/Dialect/MQT/Utils/ConstantFolding.cpp index b67496dee1..b9a446ec3c 100644 --- a/mlir/lib/Dialect/MQT/Utils/ConstantFolding.cpp +++ b/mlir/lib/Dialect/MQT/Utils/ConstantFolding.cpp @@ -10,8 +10,8 @@ #include "mlir/Dialect/MQT/Utils/ConstantFolding.h" -#include #include +#include #include #include #include @@ -41,11 +41,11 @@ std::optional attributeToDouble(Attribute attr) { } std::optional valueToDouble(Value value) { - Attribute attr; - if (!matchPattern(value, m_Constant(&attr))) { + auto constantOp = value.getDefiningOp(); + if (!constantOp) { return std::nullopt; } - return attributeToDouble(attr); + return attributeToDouble(constantOp.getValue()); } std::optional @@ -55,105 +55,39 @@ valueToConstantAttr(Value value, return it->second; } - struct Frame { - Value value; - Operation* operation; - unsigned nextOperand = 0; - }; - - SmallVector worklist; - llvm::SmallDenseSet active; - const auto schedule = [&](Value candidate) { - if (cache.contains(candidate)) { - return; - } - Attribute attr; - if (matchPattern(candidate, m_Constant(&attr))) { - cache[candidate] = attr; - return; - } - Operation* operation = candidate.getDefiningOp(); - if (operation == nullptr || operation->getNumRegions() != 0 || - !isPure(operation)) { - cache[candidate] = std::nullopt; - return; - } - active.insert(candidate); - worklist.push_back({candidate, operation}); - }; - - schedule(value); - while (!worklist.empty()) { - auto& frame = worklist.back(); - bool scheduledOperand = false; - while (frame.nextOperand < frame.operation->getNumOperands()) { - Value operand = frame.operation->getOperand(frame.nextOperand++); - if (cache.contains(operand)) { - continue; - } - if (active.contains(operand)) { - cache[operand] = std::nullopt; - continue; - } - schedule(operand); - scheduledOperand = true; - break; - } - if (scheduledOperand) { - continue; - } - - SmallVector operands; - operands.reserve(frame.operation->getNumOperands()); - bool failedOperand = false; - for (Value operand : frame.operation->getOperands()) { - const auto it = cache.find(operand); - if (it == cache.end() || !it->second) { - failedOperand = true; - break; - } - operands.push_back(*it->second); - } - if (failedOperand) { - active.erase(frame.value); - cache[frame.value] = std::nullopt; - worklist.pop_back(); - continue; - } + Attribute attr; + if (matchPattern(value, m_Constant(&attr))) { + return cache[value] = attr; + } - SmallVector results; - if (failed(frame.operation->fold(operands, results)) || - results.size() != 1) { - active.erase(frame.value); - cache[frame.value] = std::nullopt; - worklist.pop_back(); - continue; - } - if (auto resultAttr = dyn_cast_if_present(results.front())) { - active.erase(frame.value); - cache[frame.value] = resultAttr; - worklist.pop_back(); - continue; - } + Operation* operation = value.getDefiningOp(); + if (operation == nullptr || operation->getNumRegions() != 0 || + !isPure(operation)) { + return cache[value] = std::nullopt; + } - auto resultValue = dyn_cast_if_present(results.front()); - if (!resultValue || resultValue == frame.value || - active.contains(resultValue)) { - active.erase(frame.value); - cache[frame.value] = std::nullopt; - worklist.pop_back(); - continue; - } - if (!cache.contains(resultValue)) { - schedule(resultValue); - continue; + SmallVector operands; + operands.reserve(operation->getNumOperands()); + for (Value operand : operation->getOperands()) { + const auto folded = valueToConstantAttr(operand, cache); + if (!folded) { + return cache[value] = std::nullopt; } - active.erase(frame.value); - cache[frame.value] = cache.lookup(resultValue); - worklist.pop_back(); + operands.push_back(*folded); } - return cache.lookup(value); + SmallVector results; + if (failed(operation->fold(operands, results)) || results.size() != 1) { + return cache[value] = std::nullopt; + } + std::optional folded; + if (auto resultAttr = dyn_cast_if_present(results.front())) { + folded = resultAttr; + } else if (auto resultValue = dyn_cast_if_present(results.front())) { + /* Identity-style folds can return an existing SSA value. */ + folded = valueToConstantAttr(resultValue, cache); + } + return cache[value] = folded; } std::optional valueToConstantAttr(Value value) { diff --git a/mlir/lib/Dialect/MQT/Utils/Modifiers.cpp b/mlir/lib/Dialect/MQT/Utils/Modifiers.cpp index 3376c9a660..1d4d6c17ea 100644 --- a/mlir/lib/Dialect/MQT/Utils/Modifiers.cpp +++ b/mlir/lib/Dialect/MQT/Utils/Modifiers.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include diff --git a/mlir/lib/Dialect/QC/IR/Modifiers/ModifierUtils.cpp b/mlir/lib/Dialect/QC/IR/Modifiers/ModifierUtils.cpp index b99a9cc2c2..95f5b56356 100644 --- a/mlir/lib/Dialect/QC/IR/Modifiers/ModifierUtils.cpp +++ b/mlir/lib/Dialect/QC/IR/Modifiers/ModifierUtils.cpp @@ -11,20 +11,23 @@ #include "ModifierUtils.h" #include "mlir/Dialect/MQT/Utils/Modifiers.h" -#include "mlir/Dialect/QC/IR/QCOps.h" +#include "mlir/Dialect/QC/IR/QCDialect.h" +#include "mlir/Dialect/QC/IR/QCInterfaces.h" #include -#include #include #include -#include +#include +#include #include +#include #include #include #include #include #include #include +#include #include #include @@ -86,23 +89,13 @@ LogicalResult verifyModifierBody(Operation* modifierOp, Block& body) { "arguments"); } - SmallVector worklist; - for (Operation& operation : body) { - worklist.push_back(&operation); - } - while (!worklist.empty()) { - Operation* operation = worklist.pop_back_val(); - if (isForbiddenModifierBodyOperation(operation)) { - return modifierOp->emitOpError( - "body must not contain non-unitary operations or access registers"); - } - for (Region& region : operation->getRegions()) { - for (Block& block : region) { - for (Operation& nested : block) { - worklist.push_back(&nested); - } - } - } + const auto walkResult = body.walk([](Operation* operation) { + return isForbiddenModifierBodyOperation(operation) ? WalkResult::interrupt() + : WalkResult::advance(); + }); + if (walkResult.wasInterrupted()) { + return modifierOp->emitOpError( + "body must not contain non-unitary operations or access registers"); } return success(); diff --git a/mlir/lib/Dialect/QC/Transforms/ShrinkQubitRegisters.cpp b/mlir/lib/Dialect/QC/Transforms/ShrinkQubitRegisters.cpp index 65843e6b1a..c4f09ee4a8 100644 --- a/mlir/lib/Dialect/QC/Transforms/ShrinkQubitRegisters.cpp +++ b/mlir/lib/Dialect/QC/Transforms/ShrinkQubitRegisters.cpp @@ -10,7 +10,6 @@ #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/Transforms/Passes.h" -#include "mlir/Support/OperationUtils.h" #include #include @@ -157,11 +156,6 @@ struct ShrinkQubitRegistersPass final : impl::ShrinkQubitRegistersPassBase { protected: void runOnOperation() override { - constexpr size_t maxRegionNesting = 64; - if (failed(verifyRegionNestingDepth(getOperation(), maxRegionNesting))) { - signalPassFailure(); - return; - } RewritePatternSet patterns(&getContext()); patterns.add(&getContext()); diff --git a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp index 34fa12cc81..7b6815948b 100644 --- a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp +++ b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp @@ -17,7 +17,6 @@ #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/IR/QCInterfaces.h" #include "mlir/Dialect/QC/IR/QCOps.h" -#include "mlir/Support/OperationUtils.h" #include "mlir/Target/OpenQASM/GateCatalog.h" #include @@ -136,8 +135,7 @@ class OpenQASMEmitter { explicit OpenQASMEmitter(ModuleOp moduleOp) : moduleOp(moduleOp) {} [[nodiscard]] FailureOr emit() { - if (failed(verifyRegionNestingDepth(moduleOp, maxRegionNesting)) || - failed(verify(moduleOp)) || failed(preflight()) || + if (failed(verify(moduleOp)) || failed(preflight()) || failed(collectProgramShape())) { return failure(); } @@ -186,7 +184,6 @@ class OpenQASMEmitter { size_t expressionWork = 0; size_t numClassicalBits = 0; - static constexpr size_t maxRegionNesting = 64; static constexpr size_t MAX_EXPRESSION_NESTING = 256; static constexpr size_t MAX_EXPRESSION_WORK = 4096; static constexpr size_t MAX_CLASSICAL_BITS = 1U << 20; diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp index 6f24c9cabb..0ad3facfc6 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp @@ -38,8 +38,6 @@ using namespace mlir; using namespace mlir::qco; -namespace { - static void inlineCtrlBody(CtrlOp op, PatternRewriter& rewriter) { auto* body = op.getBody(); auto* terminator = body->getTerminator(); @@ -53,6 +51,8 @@ static void inlineCtrlBody(CtrlOp op, PatternRewriter& rewriter) { rewriter.replaceOp(op, outputs); } +namespace { + /** * @brief Merge nested control modifiers into a single one. */ @@ -374,16 +374,14 @@ void CtrlOp::getCanonicalizationPatterns(RewritePatternSet& results, } bool CtrlOp::hasCompileTimeKnownUnitaryMatrix() { - if (!isModifierMatrixSizeSupported(getNumTargets(), getNumControls()) || - !detail::isModifierMatrixNestingSupported(getOperation())) { + if (!isModifierMatrixSizeSupported(getNumTargets(), getNumControls())) { return false; } return hasComposableBodyMatrix(*getBody(), getNumTargets()); } std::optional CtrlOp::getUnitaryMatrix() { - if (!isModifierMatrixSizeSupported(getNumTargets(), getNumControls()) || - !detail::isModifierMatrixNestingSupported(getOperation())) { + if (!isModifierMatrixSizeSupported(getNumTargets(), getNumControls())) { return std::nullopt; } diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp index 31090730a7..3971de0f6c 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp @@ -538,16 +538,14 @@ void InvOp::getCanonicalizationPatterns(RewritePatternSet& results, } bool InvOp::hasCompileTimeKnownUnitaryMatrix() { - if (!isModifierMatrixSizeSupported(getNumTargets()) || - !detail::isModifierMatrixNestingSupported(getOperation())) { + if (!isModifierMatrixSizeSupported(getNumTargets())) { return false; } return hasComposableBodyMatrix(*getBody(), getNumTargets()); } std::optional InvOp::getUnitaryMatrix() { - if (!isModifierMatrixSizeSupported(getNumTargets()) || - !detail::isModifierMatrixNestingSupported(getOperation())) { + if (!isModifierMatrixSizeSupported(getNumTargets())) { return std::nullopt; } // Compose the complete body so pass-through targets are represented too. diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.cpp index 22d4b448b3..4c39fba52c 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.cpp @@ -11,47 +11,29 @@ #include "ModifierUtils.h" #include "mlir/Dialect/MQT/Utils/Modifiers.h" -#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include #include #include -#include +#include +#include #include +#include #include #include #include #include #include #include +#include #include #include -#include namespace mlir::qco::detail { -bool isModifierMatrixNestingSupported(Operation* modifierOp) { - constexpr size_t maxModifierNesting = 64; - SmallVector> worklist{{modifierOp, 0}}; - while (!worklist.empty()) { - auto [operation, parentDepth] = worklist.pop_back_val(); - const size_t depth = - parentDepth + static_cast(isa(operation)); - if (depth > maxModifierNesting) { - return false; - } - for (Region& region : operation->getRegions()) { - for (Block& block : region) { - for (Operation& nested : block) { - worklist.emplace_back(&nested, depth); - } - } - } - } - return true; -} - [[nodiscard]] static bool containsQubit(Type type) { if (isa(type)) { return true; @@ -90,23 +72,13 @@ LogicalResult verifyModifierBody(Operation* modifierOp, Block& body) { "arguments"); } - SmallVector worklist; - for (Operation& operation : body) { - worklist.push_back(&operation); - } - while (!worklist.empty()) { - Operation* operation = worklist.pop_back_val(); - if (isForbiddenModifierBodyOperation(operation)) { - return modifierOp->emitOpError( - "body must not contain non-unitary operations or access registers"); - } - for (Region& region : operation->getRegions()) { - for (Block& block : region) { - for (Operation& nested : block) { - worklist.push_back(&nested); - } - } - } + const auto walkResult = body.walk([](Operation* operation) { + return isForbiddenModifierBodyOperation(operation) ? WalkResult::interrupt() + : WalkResult::advance(); + }); + if (walkResult.wasInterrupted()) { + return modifierOp->emitOpError( + "body must not contain non-unitary operations or access registers"); } return success(); diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.h b/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.h index 7a3c7209fa..31a28044f3 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.h +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/ModifierUtils.h @@ -31,10 +31,6 @@ namespace qco::detail { [[nodiscard]] LogicalResult verifyModifierBody(Operation* modifierOp, Block& body); -/** @brief Return whether matrix queries may safely recurse through a modifier. - */ -[[nodiscard]] bool isModifierMatrixNestingSupported(Operation* modifierOp); - /** * @brief Return the positions of the qubits that the body of a modifier uses. * diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp index 12cc1401da..29e9ef137c 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/PowOp.cpp @@ -941,8 +941,7 @@ void PowOp::getCanonicalizationPatterns(RewritePatternSet& results, // This structural query deliberately avoids constructing the body matrix or // running the eigensolver. bool PowOp::hasCompileTimeKnownUnitaryMatrix() { - if (!isModifierMatrixSizeSupported(getNumTargets()) || - !detail::isModifierMatrixNestingSupported(getOperation())) { + if (!isModifierMatrixSizeSupported(getNumTargets())) { return false; } return getExponentValue().has_value() && @@ -965,8 +964,7 @@ bool PowOp::hasCompileTimeKnownUnitaryMatrix() { * not fully compile-time known, or `V` is not unitary. */ std::optional PowOp::getUnitaryMatrix() { - if (!isModifierMatrixSizeSupported(getNumTargets()) || - !detail::isModifierMatrixNestingSupported(getOperation())) { + if (!isModifierMatrixSizeSupported(getNumTargets())) { return std::nullopt; } const auto exponent = getExponentValue(); diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp index bccf67bfcc..5e8bd96395 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp @@ -46,9 +46,6 @@ struct MergeSubsequentBarrier final : OpRewritePattern { BarrierOp nextBarrier; for (Value output : outputs) { - if (!output.hasOneUse()) { - return failure(); - } auto user = dyn_cast(*output.getUsers().begin()); if (!user || user->getBlock() != op->getBlock() || (nextBarrier && user != nextBarrier)) { diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp index 7478a439cb..8cd32ea98d 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp @@ -77,9 +77,6 @@ struct MergeSubsequentR final : OpRewritePattern { LogicalResult matchAndRewrite(ROp op, PatternRewriter& rewriter) const override { - if (!op.getOutputQubit(0).hasOneUse()) { - return failure(); - } auto nextOp = dyn_cast(*op.getOutputQubit(0).user_begin()); if (!nextOp || op->getBlock() != nextOp->getBlock()) { return failure(); diff --git a/mlir/lib/Dialect/QCO/IR/QCOUtils.cpp b/mlir/lib/Dialect/QCO/IR/QCOUtils.cpp index ef235be81f..587e567e64 100644 --- a/mlir/lib/Dialect/QCO/IR/QCOUtils.cpp +++ b/mlir/lib/Dialect/QCO/IR/QCOUtils.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include #include #include @@ -28,12 +27,15 @@ #include #include #include +#include #include +#include +#include #include #include #include -#include +#include namespace mlir::qco { @@ -53,9 +55,7 @@ LogicalResult verifyLinearity(Operation* root) { } DenseSet staticIndices; - SmallVector operations{root}; - for (size_t next = 0; next < operations.size(); ++next) { - auto* op = operations[next]; + const auto walkResult = root->walk([&](Operation* op) { if (auto staticOp = dyn_cast(op)) { if (entryPoint && (entryPoint.isDeclaration() || @@ -64,35 +64,33 @@ LogicalResult verifyLinearity(Operation* root) { << "expected static qubits in the entry block of program entry " "function @" << entryPoint.getSymName(); - return failure(); + return WalkResult::interrupt(); } if (!staticIndices.insert(staticOp.getIndex()).second) { staticOp.emitError() << "expected each static qubit index to identify one linear " "value, but found duplicate index " << staticOp.getIndex(); - return failure(); + return WalkResult::interrupt(); } } for (auto result : op->getResults()) { if (failed(verifyLinearValue(result))) { - return failure(); + return WalkResult::interrupt(); } } for (Region& region : op->getRegions()) { for (Block& block : region) { for (auto argument : block.getArguments()) { if (failed(verifyLinearValue(argument))) { - return failure(); + return WalkResult::interrupt(); } } - for (auto& nestedOp : block) { - operations.push_back(&nestedOp); - } } } - } - return success(); + return WalkResult::advance(); + }); + return walkResult.wasInterrupted() ? failure() : success(); } /// Returns the wire index for @p wire in @p wireIds, or `std::nullopt` if @@ -215,14 +213,12 @@ bool hasComposableBodyMatrix(Block& block, size_t numTargets) { } } - for (auto [index, yielded] : - llvm::enumerate(block.getTerminator()->getOperands())) { - const auto wire = lookupWireId(wireIds, yielded); - if (!wire.has_value() || *wire != index) { - return false; - } - } - return true; + const auto yielded = block.getTerminator()->getOperands(); + return std::ranges::all_of( + std::views::iota(size_t{0}, numTargets), [&](const size_t index) { + const auto wire = lookupWireId(wireIds, yielded[index]); + return wire.has_value() && *wire == index; + }); } std::optional composeBodyMatrix(Block& block, diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index 6b23549f1d..621df3444e 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -15,7 +15,6 @@ add_mlir_library( PUBLIC MQTCompilerTarget PRIVATE - MLIRQCDialect MLIRQCODialect MLIRQCOUtils MLIRQTensorUtils diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp index 949ce1f76e..d54def6f68 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp @@ -14,7 +14,6 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" -#include "mlir/Support/OperationUtils.h" #include #include // IWYU pragma: keep (Passes.h.inc) @@ -1164,15 +1163,15 @@ static void appendSp22PRx(CircuitPlan& plan, size_t m, double sign) { /// SP22 Theorem 2: expand `Q_m` into single-controlled CRX only. static CircuitPlan buildSp22Q(size_t m) { CircuitPlan q; - for (size_t level = m; level > 1; --level) { - appendSp22PRx(q, level - 1, 1.0); - q.append({.kind = PlanOpKind::CRX, - .wires = {0, level - 1}, - .angle = std::ldexp(K_PI, -static_cast(level - 2))}); - } - for (size_t level = 2; level <= m; ++level) { - appendSp22PRx(q, level - 1, -1.0); - } + if (m < 2) { + return q; // Q_1 = Q_0 = I + } + appendSp22PRx(q, m - 1, 1.0); + q.append({.kind = PlanOpKind::CRX, + .wires = {0, m - 1}, + .angle = std::ldexp(K_PI, -static_cast(m - 2))}); + appendPlanOps(q, buildSp22Q(m - 1)); + appendSp22PRx(q, m - 1, -1.0); return q; } @@ -1433,12 +1432,6 @@ struct DecomposeMultiControlled final return; } - constexpr size_t maxRegionNesting = 64; - if (failed(verifyRegionNestingDepth(getOperation(), maxRegionNesting))) { - signalPassFailure(); - return; - } - RewritePatternSet patterns(&getContext()); patterns.add( &getContext(), minQubits); diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 1c3971b86e..d99cc04097 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -12,7 +12,6 @@ #include "mlir/Compiler/Target.h" #include "mlir/Dialect/MQT/IR/MQTDialect.h" -#include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" @@ -23,7 +22,6 @@ #include "mlir/Dialect/QCO/Utils/WireIterator.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" #include "mlir/Dialect/QTensor/Utils/TensorIterator.h" -#include "mlir/Support/OperationUtils.h" #include #include @@ -37,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -388,11 +387,6 @@ struct MappingPass : impl::MappingPassBase { IRRewriter rewriter(&getContext()); - if (failed(mqt::verifyProgramMetadata(mod))) { - signalPassFailure(); - return; - } - auto entryPoint = mqt::getEntryPoint(mod); if (!entryPoint) { mod.emitError() << "does not contain an entry point function"; @@ -400,14 +394,7 @@ struct MappingPass : impl::MappingPassBase { return; } - if (failed(qco::verifyLinearity(entryPoint)) || - failed(validateMappingInput(entryPoint))) { - signalPassFailure(); - return; - } - - // Include the module and function operations in the shared depth bound. - if (failed(verifyRegionNestingDepth(mod, maxRegionNesting + 2))) { + if (failed(validateMappingInput(entryPoint))) { signalPassFailure(); return; } @@ -474,9 +461,6 @@ struct MappingPass : impl::MappingPassBase { } private: - static constexpr size_t maxStructuredNesting = 64; - static constexpr size_t maxRegionNesting = 64; - /// Return whether a type carries value-semantics quantum state. static bool isQuantumType(Type type) { if (isa(type)) { @@ -499,15 +483,7 @@ struct MappingPass : impl::MappingPassBase { /// Follow one tensor init through a while's before region to its result. static FailureOr traceWhileTensorInit(scf::WhileOp whileOp, - size_t initIndex, - size_t nesting = 0) { - if (nesting > maxStructuredNesting) { - whileOp.emitError() << "target mapping supports at most " - << maxStructuredNesting - << " nested quantum structured operations"; - return failure(); - } - + size_t initIndex) { Block* beforeBody = whileOp.getBeforeBody(); if (initIndex >= beforeBody->getNumArguments()) { whileOp.emitError( @@ -519,12 +495,6 @@ struct MappingPass : impl::MappingPassBase { Value current = beforeBody->getArgument(initIndex); DenseSet visited; while (visited.insert(current).second) { - if (!current.hasOneUse()) { - whileOp.emitError( - "target mapping requires linear scf.while tensor flow"); - return failure(); - } - OpOperand& use = *current.use_begin(); Operation* user = use.getOwner(); if (auto condition = dyn_cast(user)) { @@ -563,8 +533,8 @@ struct MappingPass : impl::MappingPassBase { } else if (auto forOp = dyn_cast(user)) { next = forOp.getTiedLoopResult(&use); } else if (auto nestedWhile = dyn_cast(user)) { - auto nestedTrace = traceWhileTensorInit( - nestedWhile, use.getOperandNumber(), nesting + 1); + auto nestedTrace = + traceWhileTensorInit(nestedWhile, use.getOperandNumber()); if (failed(nestedTrace)) { return failure(); } @@ -671,89 +641,56 @@ struct MappingPass : impl::MappingPassBase { "results; allocate qubits in the entry function body"); } - struct PendingOperation { - Operation* op; - size_t structuredNesting; - size_t regionNesting; - Operation* unsupportedRegion; - }; - - SmallVector pending; - for (Block& block : func.getBody()) { - for (Operation& op : block) { - pending.push_back({&op, 0, 0, nullptr}); + const WalkResult validation = func.walk([&](Operation* op) { + if (op == func.getOperation()) { + return WalkResult::advance(); } - } - - while (!pending.empty()) { - auto [op, structuredNesting, regionNesting, unsupportedRegion] = - pending.pop_back_val(); if (isa(op)) { - return op->emitError() - << "target mapping requires dynamically allocated qubits; " - "static qubits are already placed"; + op->emitError() << "target mapping requires dynamically allocated " + "qubits; static qubits are already placed"; + return WalkResult::interrupt(); } const bool carriesQuantum = llvm::any_of(op->getOperandTypes(), isQuantumType) || llvm::any_of(op->getResultTypes(), isQuantumType); if (carriesQuantum && !isSupportedQuantumCarrier(op)) { - return op->emitError() - << "target mapping does not support quantum values carried by " - << op->getName(); + op->emitError() + << "target mapping does not support quantum values carried by " + << op->getName(); + return WalkResult::interrupt(); } - if (unsupportedRegion && carriesQuantum) { - return unsupportedRegion->emitError() - << "target mapping does not support quantum operations nested " - "in " - << unsupportedRegion->getName(); + if (carriesQuantum) { + for (Operation* parent = op->getParentOp(); + parent != nullptr && parent != func.getOperation(); + parent = parent->getParentOp()) { + if (parent->getNumRegions() != 0 && + !isSupportedStructuredOperation(parent) && + !isa(parent)) { + parent->emitError() + << "target mapping does not support quantum operations nested " + "in " + << parent->getName(); + return WalkResult::interrupt(); + } + } } if (auto whileOp = dyn_cast(op); whileOp && failed(validateWhileTensorFlow(whileOp))) { - return failure(); - } - - const bool structured = - carriesQuantum && isSupportedStructuredOperation(op); - const size_t childStructuredNesting = - structuredNesting + static_cast(structured); - if (childStructuredNesting > maxStructuredNesting) { - return op->emitError() - << "target mapping supports at most " << maxStructuredNesting - << " nested quantum structured operations"; - } - const size_t childRegionNesting = - regionNesting + static_cast(op->getNumRegions() != 0); - if (childRegionNesting > maxRegionNesting) { - return op->emitError() - << "target mapping supports at most " << maxRegionNesting - << " nested operations with regions"; + return WalkResult::interrupt(); } // Mapping treats a unitary, including a modifier, as one routing node. // Its region is the implementation of that node rather than nested // structured control flow to route independently. if (isa(op)) { - continue; - } - - Operation* childUnsupportedRegion = unsupportedRegion; - if (!childUnsupportedRegion && op->getNumRegions() != 0 && - !isSupportedStructuredOperation(op) && !isa(op)) { - childUnsupportedRegion = op; + return WalkResult::skip(); } - for (Region& region : op->getRegions()) { - for (Block& block : region) { - for (Operation& nested : block) { - pending.push_back({&nested, childStructuredNesting, - childRegionNesting, childUnsupportedRegion}); - } - } - } - } + return WalkResult::advance(); + }); - return success(); + return validation.wasInterrupted() ? failure() : success(); } /// Return the qubit values in `values`, preserving their relative order. @@ -912,34 +849,26 @@ struct MappingPass : impl::MappingPassBase { static FailureOr discoverComputation(func::FuncOp func) { Computation computation; - SmallVector operations; - for (Block& block : func.getBody()) { - for (Operation& operation : block) { - operations.push_back(&operation); - } - } - for (size_t next = 0; next < operations.size(); ++next) { - Operation* op = operations[next]; + const auto discovery = func.walk([&](Operation* op) { if (auto unitary = dyn_cast(op)) { - if (!isa(op) && unitary.getNumQubits() > 2) { + if (isa(op)) { + return WalkResult::advance(); + } + if (unitary.getNumQubits() > 2) { unitary.emitError() << "cannot route an operation acting on " << unitary.getNumQubits() << " qubits; decompose it to one- and two-qubit operations " "first"; - return failure(); + return WalkResult::interrupt(); } - computation.hasTwoQubitOperations |= - !isa(op) && unitary.getNumQubits() == 2; + computation.hasTwoQubitOperations |= unitary.getNumQubits() == 2; } - if (isa(op)) { - if (op->getParentRegion() != &func.getFunctionBody()) { - op->emitError() - << "target mapping requires dynamic qubit allocations in the " - "entry function body"; - return failure(); - } + if (!isa(op)) { + return WalkResult::advance(); + } + if (op->getParentRegion() == &func.getFunctionBody()) { TypeSwitch(op) .Case([&](AllocOp alloc) { computation.scalarAllocations.emplace_back(alloc); @@ -948,15 +877,17 @@ struct MappingPass : impl::MappingPassBase { computation.tensorAllocations.emplace_back( TensorAllocation{.allocation = alloc}); }); + return WalkResult::advance(); } - for (Region& region : op->getRegions()) { - for (Block& block : region) { - for (Operation& nested : block) { - operations.push_back(&nested); - } - } - } + op->emitError() + << "target mapping requires dynamic qubit allocations in the entry " + "function body"; + return WalkResult::interrupt(); + }); + + if (discovery.wasInterrupted()) { + return failure(); } for (auto alloc : computation.scalarAllocations) { diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index d475e9200d..8c0e80d7b1 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -8,17 +8,13 @@ * Licensed under the MIT License */ -#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Transforms/GlobalPhaseNormalization.h" -#include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include "mlir/Dialect/QCO/Utils/WireIterator.h" -#include "mlir/Support/OperationUtils.h" #include // IWYU pragma: keep (Passes.h.inc) #include // IWYU pragma: keep (Passes.h.inc) @@ -205,17 +201,6 @@ struct FuseSingleQubitUnitaryRunsPass final return; } - constexpr size_t maxRegionNesting = 64; - if (failed(verifyRegionNestingDepth(moduleOp, maxRegionNesting))) { - signalPassFailure(); - return; - } - if (failed(mqt::verifyProgramMetadata(moduleOp)) || - failed(qco::verifyLinearity(moduleOp))) { - signalPassFailure(); - return; - } - RewritePatternSet compositionPatterns(&getContext()); decomposition::populateParameterizedSingleQubitRunCompositionPatterns( compositionPatterns, *parsed); diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index f8b57ed1d0..d293a419de 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -9,23 +9,18 @@ */ #include "mlir/Compiler/Target.h" -#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Transforms/GlobalPhaseNormalization.h" #include "mlir/Dialect/MQT/Utils/Modifiers.h" -#include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" -#include "mlir/Support/OperationUtils.h" #include -#include #include #include // IWYU pragma: keep (Passes.h.inc) #include @@ -42,6 +37,7 @@ #include #include #include +#include #include #include @@ -149,9 +145,8 @@ twoQubitRunMemberMatrix(UnitaryOpInterface unitary) { /// The sole walkable one- or two-qubit consumer of `wire`, or a null interface. /// `wire` is single-use by qubit linearity. static UnitaryOpInterface uniqueUnitaryUser(Value wire) { - if (!wire.hasOneUse()) { - return {}; - } + assert(wire.hasOneUse() && + "qubit values are single-use, so a run tail has exactly one user"); auto unitary = dyn_cast(*wire.user_begin()); if (!unitary || !isWalkableUnitaryShell(unitary.getOperation()) || (!unitary.isSingleQubit() && !unitary.isTwoQubit())) { @@ -185,9 +180,9 @@ static Operation* twoQubitGateAtEndOfOneQChain(Value wire) { static bool feedsFromSameTwoQubitRun(UnitaryOpInterface op) { Value in0 = op.getInputQubit(0); Value in1 = op.getInputQubit(1); - if (!in0.hasOneUse() || !in1.hasOneUse()) { - return false; - } + assert(in0.hasOneUse() && in1.hasOneUse() && + "qubit values are single-use, so a run member consumes each input " + "exactly once"); Operation* gate0 = twoQubitGateAtEndOfOneQChain(in0); Operation* gate1 = twoQubitGateAtEndOfOneQChain(in1); return gate0 != nullptr && gate0 == gate1; @@ -294,7 +289,7 @@ static void eraseFusableRun(RewriterBase& rewriter, /// its two-qubit operation count. static bool fuseTwoQubitGateRun(IRRewriter& rewriter, UnitaryOpInterface head, const Matrix4x4& headMatrix, - CompilerTarget::SynthesisBasis basis) { + const CompilerTarget::SynthesisBasis basis) { FusableTwoQubitRun run = scanFusableTwoQubitRun(head, headMatrix); if (run.ops.size() < 2) { return false; @@ -334,34 +329,17 @@ struct SynthesisPlan { } // namespace -static SmallVector collectOperationsPostorder(Operation* root) { - SmallVector worklist{root}; - SmallVector reversePostorder; - while (!worklist.empty()) { - Operation* operation = worklist.pop_back_val(); - reversePostorder.push_back(operation); - for (Region& region : operation->getRegions()) { - for (Block& block : region) { - for (Operation& nested : block) { - worklist.push_back(&nested); - } - } - } - } - return llvm::to_vector(llvm::reverse(reversePostorder)); -} - static SynthesisPlan planTargetSynthesis(Operation* root, const CompilerTarget& target) { SynthesisPlan plan; - for (Operation* operation : collectOperationsPostorder(root)) { + root->walk([&](Operation* operation) { auto unitary = dyn_cast(operation); if (!unitary || !isWalkableUnitaryShell(operation) || (unitary.getNumQubits() != 1 && unitary.getNumQubits() != 2)) { - continue; + return WalkResult::advance(); } if (!requiresTargetSynthesis(operation, target)) { - continue; + return WalkResult::advance(); } if (plan.firstNeed == nullptr) { plan.firstNeed = operation; @@ -371,7 +349,7 @@ static SynthesisPlan planTargetSynthesis(Operation* root, body != nullptr && !mqt::canHoistSupportingOps(*body)) { plan.supportNotHoistable = operation; - break; + return WalkResult::interrupt(); } if (unitary.isSingleQubit()) { @@ -379,18 +357,18 @@ static SynthesisPlan planTargetSynthesis(Operation* root, if (unitary.getUnitaryMatrix2x2(matrix) || decomposition::canSynthesizeParameterizedUnitary1Q(operation)) { plan.operations.emplace_back(operation); - continue; + return WalkResult::advance(); } } else { Matrix4x4 matrix; if (assignTwoQubitOpMatrix(operation, matrix)) { plan.operations.emplace_back(operation); - continue; + return WalkResult::advance(); } } plan.matrixUnavailable = operation; - break; - } + return WalkResult::interrupt(); + }); return plan; } @@ -453,13 +431,13 @@ static LogicalResult fuseTwoQubitGates(ModuleOp moduleOp) { .entangler = CompilerTarget::GateKind::CZ}; SmallVector runHeads; - for (Operation* operation : collectOperationsPostorder(moduleOp)) { + moduleOp.walk([&](Operation* operation) { auto unitary = dyn_cast(operation); const auto matrix = twoQubitRunMemberMatrix(unitary); if (matrix && !feedsFromSameTwoQubitRun(unitary)) { runHeads.emplace_back(operation); } - } + }); bool changed = false; IRRewriter rewriter(moduleOp.getContext()); @@ -483,22 +461,12 @@ struct FuseTwoQubitGatesPass final MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(FuseTwoQubitGatesPass) void getDependentDialects(DialectRegistry& registry) const override { - registry.insert(); + registry.insert(); } protected: void runOnOperation() override { ModuleOp moduleOp = getOperation(); - constexpr size_t maxRegionNesting = 64; - if (failed(verifyRegionNestingDepth(moduleOp, maxRegionNesting))) { - signalPassFailure(); - return; - } - if (failed(mqt::verifyProgramMetadata(moduleOp)) || - failed(qco::verifyLinearity(moduleOp))) { - signalPassFailure(); - return; - } if (failed(fuseTwoQubitGates(moduleOp))) { signalPassFailure(); } @@ -513,8 +481,7 @@ struct TargetNativeSynthesisPass final : target(targetIn) {} void getDependentDialects(DialectRegistry& registry) const override { - registry.insert(); + registry.insert(); } protected: @@ -523,16 +490,6 @@ struct TargetNativeSynthesisPass final return; } ModuleOp moduleOp = getOperation(); - constexpr size_t maxRegionNesting = 64; - if (failed(verifyRegionNestingDepth(moduleOp, maxRegionNesting))) { - signalPassFailure(); - return; - } - if (failed(mqt::verifyProgramMetadata(moduleOp)) || - failed(qco::verifyLinearity(moduleOp))) { - signalPassFailure(); - return; - } const auto plan = planTargetSynthesis(moduleOp, target); if (plan.firstNeed == nullptr) { return; @@ -592,7 +549,7 @@ struct VerifyTargetConformancePass final protected: void runOnOperation() override { - for (Operation* operation : collectOperationsPostorder(getOperation())) { + WalkResult result = getOperation()->walk([&](Operation* operation) { if (auto function = dyn_cast(operation); function && llvm::any_of(function.getArgumentTypes(), [](const auto type) { @@ -605,48 +562,48 @@ struct VerifyTargetConformancePass final function.emitError() << "target conformance requires quantum function inputs to be " "assigned to qco.static target sites"; - signalPassFailure(); - return; + return WalkResult::interrupt(); } if (auto staticOp = dyn_cast(operation)) { const auto site = static_cast(staticOp.getIndex()); if (target.vertexForSite(site)) { - continue; + return WalkResult::advance(); } staticOp.emitError() << "target does not contain static site " << site; - signalPassFailure(); - return; + return WalkResult::interrupt(); } if (isa(operation)) { operation->emitError() << "target conformance requires qubits to be assigned to " "qco.static target sites"; - signalPassFailure(); - return; + return WalkResult::interrupt(); } size_t arity = 1; size_t parameterCount = 0; if (auto unitary = dyn_cast(operation)) { if (isExcludedFromTopLevelUnitaryWalk(operation)) { - continue; + return WalkResult::advance(); } arity = unitary.getNumQubits(); parameterCount = unitary.getNumParams(); } else if (!isa(operation)) { - continue; + return WalkResult::advance(); } if (target.supports(operation)) { - continue; + return WalkResult::advance(); } - operation->emitError() << "target does not support operation '" - << operation->getName() << "' with arity " << arity - << " and " << parameterCount << " parameter(s)"; + auto diagnostic = operation->emitError() + << "target does not support operation '" + << operation->getName() << "' with arity " << arity + << " and " << parameterCount << " parameter(s)"; + return WalkResult::interrupt(); + }); + if (result.wasInterrupted()) { signalPassFailure(); - return; } } diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/HadamardLifting.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/HadamardLifting.cpp index 4cedaab4d3..49ac6f7a9b 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/HadamardLifting.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/HadamardLifting.cpp @@ -8,28 +8,21 @@ * Licensed under the MIT License */ -#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Transforms/GlobalPhaseNormalization.h" #include "mlir/Dialect/MQT/Utils/Modifiers.h" -#include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" -#include "mlir/Support/OperationUtils.h" #include #include -#include // IWYU pragma: keep (Passes.h.inc) #include #include #include #include #include -#include #include -#include #include namespace mlir::qco { @@ -111,12 +104,8 @@ struct LiftHadamardsAbovePauliGatesPattern final return failure(); } - // op needs to be directly in front of a Hadamard gate - auto output = op.getOutputQubit(0); - if (!output.hasOneUse()) { - return failure(); - } - auto hadamardGate = dyn_cast(*output.getUsers().begin()); + // op needs to be in front of a Hadamard gate + auto hadamardGate = dyn_cast(*op->getUsers().begin()); if (!hadamardGate) { return failure(); @@ -164,7 +153,7 @@ struct LiftHadamardAboveCNOTPattern final : OpRewritePattern { auto qubitInMeasurement = op.getQubitIn(); auto* predecessor = qubitInMeasurement.getDefiningOp(); auto hadamardGate = dyn_cast(predecessor); - if (!hadamardGate || !hadamardGate.getOutputQubit(0).hasOneUse()) { + if (!hadamardGate) { return failure(); } @@ -172,7 +161,7 @@ struct LiftHadamardAboveCNOTPattern final : OpRewritePattern { auto inQubitHadamard = hadamardGate.getInputQubit(0); predecessor = inQubitHadamard.getDefiningOp(); auto cnotGate = dyn_cast(predecessor); - if (!cnotGate || !inQubitHadamard.hasOneUse()) { + if (!cnotGate) { return failure(); } if (auto innerUnitary = @@ -184,22 +173,22 @@ struct LiftHadamardAboveCNOTPattern final : OpRewritePattern { // Find a control qubit not followed by a measurement. // If there is no such control, the transformation cannot be applied. - std::optional controlIndex; + unsigned int controlIndex = 0; for (unsigned int i = 0; i < cnotGate.getNumControls(); i++) { - auto output = cnotGate.getOutputControl(i); - if (output.hasOneUse() && !isa(*output.getUsers().begin())) { + if (isa(*cnotGate.getOutputControl(i).getUsers().begin())) { + if (i == cnotGate.getNumControls() - 1) { + return failure(); + } + } else { controlIndex = i; break; } } - if (!controlIndex) { - return failure(); - } // Save all SSA values that will be needed after in-place modifications. Value origTgtIn = cnotGate.getInputTarget(0); - Value origCtrlIn = cnotGate.getInputControl(*controlIndex); - Value origCtrlOut = cnotGate.getOutputControl(*controlIndex); + Value origCtrlIn = cnotGate.getInputControl(controlIndex); + Value origCtrlOut = cnotGate.getOutputControl(controlIndex); // Add Hadamard gates before the CNOT. rewriter.setInsertionPoint(cnotGate); @@ -208,7 +197,7 @@ struct LiftHadamardAboveCNOTPattern final : OpRewritePattern { // Rewire the CNOT operands in-place so that the roles are swapped rewriter.modifyOpInPlace(cnotGate, [&]() { - cnotGate->setOperand(*controlIndex, h1.getOutputTarget(0)); + cnotGate->setOperand(controlIndex, h1.getOutputTarget(0)); cnotGate->setOperand(cnotGate.getNumControls(), h2.getOutputTarget(0)); }); @@ -239,16 +228,6 @@ struct HadamardLifting final : impl::HadamardLiftingBase { void runOnOperation() override { auto op = getOperation(); auto* ctx = &getContext(); - constexpr size_t maxRegionNesting = 64; - if (failed(verifyRegionNestingDepth(op, maxRegionNesting))) { - signalPassFailure(); - return; - } - if (failed(mqt::verifyProgramMetadata(op)) || - failed(qco::verifyLinearity(op))) { - signalPassFailure(); - return; - } // Define the set of patterns to use. RewritePatternSet patterns(ctx); diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MeasurementLifting.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MeasurementLifting.cpp index b002c6f54e..af6b6963d8 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MeasurementLifting.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MeasurementLifting.cpp @@ -11,20 +11,18 @@ #include "mlir/Dialect/MQT/Utils/Modifiers.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" -#include "mlir/Support/OperationUtils.h" #include -#include #include #include #include #include +#include #include +#include #include -#include #include namespace mlir::qco { @@ -45,33 +43,26 @@ static bool isInverting(Operation* op) { return isa(op); } * @return True if the operation is a diagonal gate, false otherwise. */ static bool isDiagonal(Operation* op) { - while (op != nullptr && isa(op)) { - op = mqt::getSoleBodyUnitary( - *op->getRegion(0).getBlocks().begin()); + if (op == nullptr) { + return false; } - return op != nullptr && isa(op); + if (isa(op)) { + return isDiagonal(mqt::getSoleBodyUnitary( + *op->getRegion(0).getBlocks().begin())); + } + return isa(op); } /// Return whether nested modifier bodies contain only unitaries and yields. static bool hasOnlyUnitaryBodyOperations(Operation* root) { - SmallVector worklist{root}; - while (!worklist.empty()) { - Operation* operation = worklist.pop_back_val(); - for (Region& region : operation->getRegions()) { - for (Block& block : region) { - for (Operation& nested : block) { - if (isa(nested)) { - continue; - } - if (!isa(nested)) { - return false; - } - worklist.push_back(&nested); - } - } - } - } - return true; + return !root->walk([&](Operation* operation) { + if (operation == root || isa(operation) || + isa(operation)) { + return WalkResult::advance(); + } + return WalkResult::interrupt(); + }) + .wasInterrupted(); } /** @@ -241,16 +232,6 @@ struct MeasurementLifting final auto op = getOperation(); auto* ctx = &getContext(); - if (failed(qco::verifyLinearity(op))) { - signalPassFailure(); - return; - } - constexpr size_t maxRegionNesting = 64; - if (failed(verifyRegionNestingDepth(op, maxRegionNesting))) { - signalPassFailure(); - return; - } - // Define the set of patterns to use. RewritePatternSet patterns(ctx); patterns.add #include @@ -1148,17 +1144,6 @@ struct MergeSingleQubitRotationGates final auto op = getOperation(); auto* ctx = &getContext(); - constexpr size_t maxRegionNesting = 64; - if (failed(verifyRegionNestingDepth(op, maxRegionNesting))) { - signalPassFailure(); - return; - } - if (failed(mqt::verifyProgramMetadata(op)) || - failed(qco::verifyLinearity(op))) { - signalPassFailure(); - return; - } - RewritePatternSet patterns(ctx); patterns.add(patterns.getContext()); diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/PauliTwirling.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/PauliTwirling.cpp index 394787f1b2..1199af6577 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/PauliTwirling.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/PauliTwirling.cpp @@ -19,7 +19,9 @@ #include #include #include +#include #include +#include #include #include @@ -217,25 +219,13 @@ struct PauliTwirl2QGates final const std::array* table; }; SmallVector gates; - SmallVector worklist{getOperation()}; - while (!worklist.empty()) { - Operation* op = worklist.pop_back_val(); + getOperation().walk([&](Operation* op) { if (const auto* table = getTwirlTable(op)) { gates.push_back({.gate = cast(op), .table = table}); } - if (isa(op)) { - continue; - } - SmallVector nested; - for (Region& region : op->getRegions()) { - for (Block& block : region) { - for (Operation& operation : block) { - nested.push_back(&operation); - } - } - } - worklist.append(nested.rbegin(), nested.rend()); - } + return isa(op) ? WalkResult::skip() + : WalkResult::advance(); + }); IRRewriter rewriter(&getContext()); std::mt19937_64 rng(seed); diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/QuantumLoopUnroll.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/QuantumLoopUnroll.cpp index 26a11dccdb..28eb5a3e0e 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/QuantumLoopUnroll.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/QuantumLoopUnroll.cpp @@ -11,7 +11,6 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" -#include "mlir/Support/OperationUtils.h" #include #include @@ -26,6 +25,9 @@ #include #include +#include +#include + namespace mlir::qco { #define GEN_PASS_DEF_QUANTUMLOOPUNROLL @@ -33,8 +35,8 @@ namespace mlir::qco { /// Keep verifier-valid loop bounds from turning this optimization into an /// unbounded allocation request. -static constexpr uint64_t kMaxQuantumLoopUnrollFactor = 4096; -static constexpr uint64_t kMaxQuantumLoopExpandedOperations = 100000; +static constexpr uint64_t K_MAX_QUANTUM_LOOP_UNROLL_FACTOR = 4096; +static constexpr uint64_t K_MAX_QUANTUM_LOOP_EXPANDED_OPERATIONS = 100000; /** * @brief Predicate for quantum loops. @@ -62,26 +64,11 @@ static bool isQuantumLoop(scf::ForOp loop) { */ static SmallVector collectQuantumLoops(FunctionOpInterface func) { SmallVector loops; - SmallVector> worklist; - worklist.emplace_back(func.getOperation(), false); - while (!worklist.empty()) { - const auto [operation, visited] = worklist.pop_back_val(); - if (visited) { - if (auto loop = dyn_cast(operation); - loop && isQuantumLoop(loop)) { - loops.emplace_back(loop); - } - continue; - } - worklist.emplace_back(operation, true); - for (Region& region : operation->getRegions()) { - for (Block& block : region) { - for (Operation& nested : block) { - worklist.emplace_back(&nested, false); - } - } + func.walk([&](scf::ForOp loop) { + if (isQuantumLoop(loop)) { + loops.emplace_back(loop); } - } + }); return loops; } @@ -103,10 +90,12 @@ static LogicalResult verifyUnrollExpansionBudget(FunctionOpInterface func, while (!worklist.empty()) { const auto [operation, multiplier] = worklist.pop_back_val(); - if (multiplier > kMaxQuantumLoopExpandedOperations - projectedOperations) { + if (multiplier > + K_MAX_QUANTUM_LOOP_EXPANDED_OPERATIONS - projectedOperations) { return operation->emitError() << "quantum loop unrolling would exceed the limit of " - << kMaxQuantumLoopExpandedOperations << " projected operations"; + << K_MAX_QUANTUM_LOOP_EXPANDED_OPERATIONS + << " projected operations"; } projectedOperations += multiplier; @@ -125,22 +114,23 @@ static LogicalResult verifyUnrollExpansionBudget(FunctionOpInterface func, // complete budget before every subsequent unrolling round. factor = 1; } else { - factor = - tripCount->getLimitedValue(kMaxQuantumLoopUnrollFactor + 1); + factor = tripCount->getLimitedValue( + K_MAX_QUANTUM_LOOP_UNROLL_FACTOR + 1); } } else { factor = static_cast(unrollFactor); } - if (factor > kMaxQuantumLoopUnrollFactor) { - return loop.emitError() - << "quantum loop unroll factor " << factor - << " exceeds the limit of " << kMaxQuantumLoopUnrollFactor; + if (factor > K_MAX_QUANTUM_LOOP_UNROLL_FACTOR) { + return loop.emitError() << "quantum loop unroll factor " << factor + << " exceeds the limit of " + << K_MAX_QUANTUM_LOOP_UNROLL_FACTOR; } if (factor != 0 && - nestedMultiplier > kMaxQuantumLoopExpandedOperations / factor) { + nestedMultiplier > + K_MAX_QUANTUM_LOOP_EXPANDED_OPERATIONS / factor) { return loop.emitError() << "quantum loop unrolling would exceed the limit of " - << kMaxQuantumLoopExpandedOperations + << K_MAX_QUANTUM_LOOP_EXPANDED_OPERATIONS << " projected operations"; } nestedMultiplier *= factor; @@ -241,10 +231,11 @@ struct QuantumLoopUnroll final signalPassFailure(); return; } - if (unrollFactor > static_cast(kMaxQuantumLoopUnrollFactor)) { + if (std::cmp_greater(static_cast(unrollFactor), + K_MAX_QUANTUM_LOOP_UNROLL_FACTOR)) { getOperation()->emitError() << "quantum loop unroll factor " << Twine(unrollFactor) - << " exceeds the limit of " << kMaxQuantumLoopUnrollFactor; + << " exceeds the limit of " << K_MAX_QUANTUM_LOOP_UNROLL_FACTOR; signalPassFailure(); return; } @@ -262,11 +253,6 @@ struct QuantumLoopUnroll final return; } - constexpr size_t maxRegionNesting = 64; - if (failed(verifyRegionNestingDepth(getOperation(), maxRegionNesting))) { - signalPassFailure(); - return; - } if (failed(verifyUnrollExpansionBudget(getOperation(), unrollFactor))) { signalPassFailure(); return; diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/RemoveDeadGates.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/RemoveDeadGates.cpp index b7c3ffb23f..7b945835dc 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/RemoveDeadGates.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/RemoveDeadGates.cpp @@ -11,13 +11,11 @@ #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" -#include "mlir/Support/OperationUtils.h" #include #include #include -#include #include namespace mlir::qco { @@ -56,11 +54,6 @@ struct RemoveDeadGates final : impl::RemoveDeadGatesBase { protected: void runOnOperation() override { - constexpr size_t maxRegionNesting = 64; - if (failed(verifyRegionNestingDepth(getOperation(), maxRegionNesting))) { - signalPassFailure(); - return; - } RewritePatternSet patterns(&getContext()); patterns.add( &getContext()); diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ReplaceClassicalControls.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ReplaceClassicalControls.cpp index d97bbed3b1..eec7d5cbef 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ReplaceClassicalControls.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ReplaceClassicalControls.cpp @@ -13,7 +13,6 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" -#include "mlir/Support/OperationUtils.h" #include #include @@ -309,10 +308,6 @@ static void trySwapControlAndTargetOfPhaseGate(CtrlOp op, Value controlOut = op.getControlsOut()[controlIndex]; Value targetOut = op.getTargetsOut()[0]; - if (!controlOut.hasOneUse() || !targetOut.hasOneUse()) { - ++controlIndex; - continue; - } rewriter.modifyOpInPlace(op, [&]() { op.getTargetsInMutable()[0].set(control); @@ -342,9 +337,6 @@ struct ReplaceBasisStateControlsWithIfPattern final LogicalResult matchAndRewrite(MeasureOp measure, PatternRewriter& rewriter) const override { - if (!measure.getQubitOut().hasOneUse()) { - return failure(); - } auto ctrlOp = dyn_cast(*measure.getQubitOut().getUsers().begin()); if (!ctrlOp) { return failure(); @@ -432,11 +424,6 @@ struct ReplaceClassicalControls final void runOnOperation() override { auto op = getOperation(); auto* ctx = &getContext(); - constexpr size_t maxRegionNesting = 64; - if (failed(verifyRegionNestingDepth(op, maxRegionNesting))) { - signalPassFailure(); - return; - } // Define the set of patterns to use. RewritePatternSet patterns(ctx); diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ReuseQubits.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ReuseQubits.cpp index 09d0e97ede..f12c5fac88 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ReuseQubits.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ReuseQubits.cpp @@ -10,7 +10,6 @@ #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" -#include "mlir/Support/OperationUtils.h" #include #include @@ -21,7 +20,6 @@ #include #include -#include #include #include @@ -157,11 +155,6 @@ struct ReuseQubits final : impl::ReuseQubitsBase { void runOnOperation() override { auto op = getOperation(); auto* ctx = &getContext(); - constexpr size_t maxRegionNesting = 64; - if (failed(verifyRegionNestingDepth(op, maxRegionNesting))) { - signalPassFailure(); - return; - } // Define the set of patterns to use. RewritePatternSet patterns(ctx); diff --git a/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp b/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp index 9602ec97a6..e2233e4458 100644 --- a/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp +++ b/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp @@ -30,7 +30,6 @@ #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" -#include "mlir/Support/OperationUtils.h" #include #include @@ -39,7 +38,6 @@ #include #include #include -#include #include #include #include @@ -52,8 +50,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -74,7 +74,6 @@ namespace { constexpr size_t MAX_CALL_NESTING = 64; constexpr size_t MAX_CONTROL_FLOW_STEPS = 10'000; -constexpr size_t MAX_REGION_NESTING = 64; struct QubitMap { DenseMap qubits; @@ -1495,9 +1494,6 @@ static LogicalResult applyOp(Operation& op, WalkState& walk, StateDD& state) { } auto returnOp = cast(callee.getBody().front().getTerminator()); - if (failed(verifyRegionNestingDepth(callee, MAX_REGION_NESTING))) { - return failure(); - } Operation* calleeOp = callee.getOperation(); if (!walk.activeCalls.insert(calleeOp).second) { return call.emitError() @@ -1597,9 +1593,6 @@ static FailureOr prepare(func::FuncOp func, const dd::Package& dd, const DDArgumentBindings& argumentBindings, bool bindEntryAllocations = false) { - if (failed(verifyRegionNestingDepth(func, MAX_REGION_NESTING))) { - return failure(); - } if (!func.getBody().hasOneBlock()) { return func.emitError() << "QCO DD construction expects a single-block function body"; @@ -1796,44 +1789,33 @@ static void analyzeSampling(func::FuncOp func, Block* sampledEntry, return; } const auto guard = llvm::make_scope_exit([&] { active.erase(funcOp); }); - SmallVector blocks; - for (Block& block : func.getBody()) { - blocks.push_back(&block); - } - while (!blocks.empty()) { - Block* block = blocks.pop_back_val(); - for (Operation& operation : *block) { - if (remainingSteps == 0) { + func.getBody().walk([&](Operation* op) { + if (remainingSteps == 0) { + plan.dynamic = true; + return WalkResult::interrupt(); + } + --remainingSteps; + if (isa(op)) { + plan.dynamic = true; + } else if (auto measure = dyn_cast(op)) { + if (isDeferrableMeasurement(measure, sampledEntry, outputs)) { + plan.deferredMeasurements.insert(op); + } else { plan.dynamic = true; - return; } - --remainingSteps; - Operation* op = &operation; - if (isa(op)) { + } else if (auto call = dyn_cast(op)) { + auto callee = SymbolTable::lookupNearestSymbolFrom( + call, call.getCalleeAttr()); + if (!callee || callee.isDeclaration() || + !callee.getBody().hasOneBlock()) { plan.dynamic = true; - } else if (auto measure = dyn_cast(op)) { - if (isDeferrableMeasurement(measure, sampledEntry, outputs)) { - plan.deferredMeasurements.insert(op); - } else { - plan.dynamic = true; - } - } else if (auto call = dyn_cast(op)) { - auto callee = SymbolTable::lookupNearestSymbolFrom( - call, call.getCalleeAttr()); - if (!callee || !callee.getBody().hasOneBlock()) { - plan.dynamic = true; - } else { - analyzeSampling(callee, sampledEntry, outputs, active, plan, - remainingSteps); - } - } - for (Region& region : op->getRegions()) { - for (Block& nested : region) { - blocks.push_back(&nested); - } + } else { + analyzeSampling(callee, sampledEntry, outputs, active, plan, + remainingSteps); } } - } + return WalkResult::advance(); + }); } static FailureOr getSamplingPlan(func::FuncOp func) { diff --git a/mlir/lib/Dialect/QIR/Transforms/AttachQIRAttributes.cpp b/mlir/lib/Dialect/QIR/Transforms/AttachQIRAttributes.cpp index 8061d3ba44..fc7a617db1 100644 --- a/mlir/lib/Dialect/QIR/Transforms/AttachQIRAttributes.cpp +++ b/mlir/lib/Dialect/QIR/Transforms/AttachQIRAttributes.cpp @@ -35,7 +35,6 @@ #include #include #include -#include #include #include #include @@ -68,31 +67,6 @@ struct Metadata { bool usesMultipleReturnPoints{false}; }; -template -static void walkQIRAttributeOperationsIteratively(Operation* root, - Callback&& callback) { - SmallVector worklist{root}; - while (!worklist.empty()) { - Operation* operation = worklist.pop_back_val(); - callback(operation); - for (Region& region : operation->getRegions()) { - for (Block& block : region) { - for (Operation& nested : block) { - worklist.push_back(&nested); - } - } - } - } -} - -[[nodiscard]] static bool hasQIREntryPointAttribute(LLVM::LLVMFuncOp function) { - const auto passthrough = function->getAttrOfType("passthrough"); - return passthrough && llvm::any_of(passthrough, [](Attribute attribute) { - const auto name = dyn_cast(attribute); - return name && name.getValue() == StringRef(::qir::ENTRY_POINT_ATTR); - }); -} - /** * @brief Attaches the required attributes to the function marked as * entry_point. @@ -103,22 +77,11 @@ struct QIRSetAttributesAndMetadata final protected: void runOnOperation() override { - SmallVector entryPoints; - for (auto function : getOperation().getOps()) { - if (mqt::isEntryPoint(function) || hasQIREntryPointAttribute(function)) { - entryPoints.push_back(function); - } - } - if (entryPoints.size() != 1) { - getOperation().emitError() - << "QIR metadata attachment requires exactly one entry point, but " - "found " - << entryPoints.size(); - signalPassFailure(); + auto main = getMainFunction(getOperation()); + if (!main) { return; } - auto main = entryPoints.front(); auto module = getOperation(); const auto [useDynamicQubit, useDynamicResult, useArrays] = usesDynamic(module); @@ -180,55 +143,30 @@ struct QIRSetAttributesAndMetadata final return createI32Flag(behavior, name, value ? 1 : 0); }; - const auto isQIRFunctionAttribute = [](Attribute attribute) { - if (const auto name = dyn_cast(attribute)) { - return name.getValue() == StringRef(::qir::ENTRY_POINT_ATTR); - } - const auto pair = dyn_cast(attribute); - const auto key = pair && pair.size() == 2 ? dyn_cast(pair[0]) - : StringAttr{}; - return key && - (key.getValue() == StringRef(::qir::OUTPUT_LABELING_SCHEMA_ATTR) || - key.getValue() == StringRef(::qir::QIR_PROFILES_ATTR) || - key.getValue() == "required_num_qubits" || - key.getValue() == "required_num_results"); - }; - SmallVector attributes; - if (const auto passthrough = - main->getAttrOfType("passthrough")) { - llvm::copy_if(passthrough, std::back_inserter(attributes), - [&](Attribute attribute) { - return !isQIRFunctionAttribute(attribute); - }); - } - attributes.append( - {rewriter.getStringAttr(::qir::ENTRY_POINT_ATTR), - rewriter.getStrArrayAttr( - {::qir::OUTPUT_LABELING_SCHEMA_ATTR, ::qir::LABELED_SCHEMA}), - rewriter.getStrArrayAttr( - {::qir::QIR_PROFILES_ATTR, - useAdaptive ? ::qir::ADAPTIVE_PROFILE : ::qir::BASE_PROFILE}), - rewriter.getStrArrayAttr( - {"required_num_qubits", std::to_string(metadata.numQubits)}), - rewriter.getStrArrayAttr( - {"required_num_results", std::to_string(metadata.numResults)})}); + const SmallVector attributes{ + rewriter.getStringAttr(::qir::ENTRY_POINT_ATTR), + rewriter.getStrArrayAttr( + {::qir::OUTPUT_LABELING_SCHEMA_ATTR, ::qir::LABELED_SCHEMA}), + rewriter.getStrArrayAttr( + {::qir::QIR_PROFILES_ATTR, + useAdaptive ? ::qir::ADAPTIVE_PROFILE : ::qir::BASE_PROFILE}), + rewriter.getStrArrayAttr( + {"required_num_qubits", std::to_string(metadata.numQubits)}), + rewriter.getStrArrayAttr( + {"required_num_results", std::to_string(metadata.numResults)})}; main->setAttr("passthrough", rewriter.getArrayAttr(attributes)); mqt::removeEntryPoint(main); rewriter.setInsertionPointToEnd(m.getBody()); - SmallVector flags = collectUnrelatedModuleFlags(m, rewriter); - flags.emplace_back( - createI32Flag(LLVM::ModFlagBehavior::Error, "qir_major_version", 2)); - flags.emplace_back( - createI32Flag(LLVM::ModFlagBehavior::Max, "qir_minor_version", 1)); - flags.emplace_back(createBoolFlag(LLVM::ModFlagBehavior::Error, - "dynamic_qubit_management", - metadata.useDynamicQubit)); - flags.emplace_back(createBoolFlag(LLVM::ModFlagBehavior::Error, - "dynamic_result_management", - metadata.useDynamicResult)); + SmallVector flags{ + createI32Flag(LLVM::ModFlagBehavior::Error, "qir_major_version", 2), + createI32Flag(LLVM::ModFlagBehavior::Max, "qir_minor_version", 1), + createBoolFlag(LLVM::ModFlagBehavior::Error, "dynamic_qubit_management", + metadata.useDynamicQubit), + createBoolFlag(LLVM::ModFlagBehavior::Error, + "dynamic_result_management", metadata.useDynamicResult)}; if (useAdaptive) { flags.emplace_back(createI32Flag(LLVM::ModFlagBehavior::Error, @@ -250,6 +188,7 @@ struct QIRSetAttributesAndMetadata final } } + removeExistingModuleFlags(m, rewriter); const auto setTypes = [&](const StringRef name, const llvm::SmallSet& types) { if (types.empty()) { @@ -266,41 +205,23 @@ struct QIRSetAttributesAndMetadata final rewriter.getArrayAttr(flags)); } - static bool isQIRModuleFlag(StringRef key) { - return key == "qir_major_version" || key == "qir_minor_version" || - key == "dynamic_qubit_management" || - key == "dynamic_result_management" || key == "backwards_branching" || - key == "arrays" || key == "ir_functions" || - key == "multiple_target_branching" || - key == "multiple_return_points" || key == "int_computations" || - key == "float_computations"; - } - - /// Remove existing top-level QIR module flags and return every unrelated - /// flag unchanged. - static SmallVector - collectUnrelatedModuleFlags(ModuleOp m, IRRewriter& rewriter) { - SmallVector preserved; - for (auto flagsOp : - llvm::make_early_inc_range(m.getOps())) { - for (const auto flag : - flagsOp.getFlags().getAsRange()) { - if (!isQIRModuleFlag(flag.getKey().getValue())) { - preserved.emplace_back(flag); - } - } - rewriter.eraseOp(flagsOp); - } - return preserved; + /// Remove existing module flag operations from module. + /// Note that this might also erase non-QIR module flag operations, but for + /// now, we assume that there are no others. + static void removeExistingModuleFlags(ModuleOp m, IRRewriter& rewriter) { + SmallVector flagOps; + m->walk([&](LLVM::ModuleFlagsOp op) { flagOps.emplace_back(op); }); + llvm::for_each(flagOps, [&](Operation* op) { rewriter.eraseOp(op); }); } /// Return one past the greatest indexed qubit pointer. /// Assumes that qubits are constant integers that are converted to /// an integer pointer and then used in (at least) one quantum instruction. - static LogicalResult + enum class PointerProvenance : uint8_t { Unresolved, Cycle, Resolved }; + + static FailureOr includeStaticPointer(Value pointer, StringRef resource, size_t& capacity, - ModuleOp module, bool requireStatic, - SmallPtrSetImpl& resolving, + ModuleOp module, SmallPtrSetImpl& resolving, SmallPtrSetImpl* aggregates = nullptr) { auto toPtrOp = pointer.getDefiningOp(); if (toPtrOp) { @@ -315,7 +236,7 @@ struct QIRSetAttributesAndMetadata final return constOp.emitError() << "QIR " << resource << " index must be an integer constant"; } - const auto index = intAttr.getValue(); + const auto& index = intAttr.getValue(); if (index.isNegative() || index.getActiveBits() >= sizeof(size_t) * 8) { return constOp.emitError() << "QIR " << resource @@ -324,11 +245,11 @@ struct QIRSetAttributesAndMetadata final } capacity = std::max(capacity, static_cast(index.getZExtValue()) + 1); - return success(); + return PointerProvenance::Resolved; } if (pointer.getDefiningOp()) { capacity = std::max(capacity, size_t{1}); - return success(); + return PointerProvenance::Resolved; } if (auto call = pointer.getDefiningOp(); call && call.getCallee() && @@ -336,49 +257,52 @@ struct QIRSetAttributesAndMetadata final *call.getCallee() == QIR_TUPLE_CREATE)) { // Generic controlled QIS calls receive aggregate pointers. Their static // qubit constituents are counted from the stores that populate them. - if (aggregates) { + if (aggregates != nullptr) { aggregates->insert(pointer); } - return success(); + return PointerProvenance::Resolved; } auto blockArgument = dyn_cast(pointer); if (blockArgument) { - if (aggregates) { + if (aggregates != nullptr) { aggregates->insert(pointer); } Operation* anchor = blockArgument.getOwner()->getParentOp(); if (!resolving.insert(pointer).second) { - return anchor->emitError() - << "cannot determine a static QIR " << resource - << " index through recursive function arguments"; + return PointerProvenance::Cycle; } auto function = dyn_cast(anchor); bool sawProvenance = false; + bool sawCycle = false; + bool unresolvedProvenance = false; LogicalResult status = success(); if (function && !function.isExternal() && blockArgument.getOwner() == &function.getBody().front()) { - walkQIRAttributeOperationsIteratively( - module, [&](Operation* operation) { - if (failed(status)) { - return; - } - auto call = dyn_cast(operation); - if (!call || !call.getCallee() || - *call.getCallee() != function.getSymName() || - blockArgument.getArgNumber() >= call.getNumOperands()) { - return; - } - sawProvenance = true; - status = includeStaticPointer( - call.getOperand(blockArgument.getArgNumber()), resource, - capacity, module, requireStatic, resolving, aggregates); - }); + module.walk([&](Operation* operation) { + if (failed(status)) { + return; + } + auto call = dyn_cast(operation); + if (!call || !call.getCallee() || + *call.getCallee() != function.getSymName()) { + return; + } + auto provenance = includeStaticPointer( + call.getOperand(blockArgument.getArgNumber()), resource, capacity, + module, resolving, aggregates); + if (failed(provenance)) { + status = failure(); + return; + } + sawProvenance |= *provenance == PointerProvenance::Resolved; + sawCycle |= *provenance == PointerProvenance::Cycle; + unresolvedProvenance |= *provenance == PointerProvenance::Unresolved; + }); } else { SmallVector worklist{pointer}; SmallPtrSet visited; - bool unresolvedProvenance = false; while (!worklist.empty() && succeeded(status)) { Value current = worklist.pop_back_val(); if (!current) { @@ -386,6 +310,7 @@ struct QIRSetAttributesAndMetadata final continue; } if (!visited.insert(current).second) { + sawCycle = true; continue; } if (auto predecessors = getControlFlowPredecessors(current)) { @@ -401,36 +326,37 @@ struct QIRSetAttributesAndMetadata final continue; } } - sawProvenance = true; - status = includeStaticPointer(current, resource, capacity, module, - requireStatic, resolving, aggregates); + auto provenance = includeStaticPointer(current, resource, capacity, + module, resolving, aggregates); + if (failed(provenance)) { + status = failure(); + continue; + } + sawProvenance |= *provenance == PointerProvenance::Resolved; + sawCycle |= *provenance == PointerProvenance::Cycle; + unresolvedProvenance |= *provenance == PointerProvenance::Unresolved; } - sawProvenance &= !unresolvedProvenance; } resolving.erase(pointer); if (failed(status)) { return failure(); } + if (unresolvedProvenance) { + return PointerProvenance::Unresolved; + } if (sawProvenance) { - return success(); + return PointerProvenance::Resolved; } + return sawCycle ? PointerProvenance::Cycle + : PointerProvenance::Unresolved; } - if (!requireStatic) { - return success(); - } - Operation* anchor = pointer.getDefiningOp(); - if (!anchor) { - anchor = cast(pointer).getOwner()->getParentOp(); - } - return anchor->emitError() << "cannot determine the static QIR " << resource - << " index from pointer provenance"; + return PointerProvenance::Unresolved; } [[nodiscard]] static Value getQIRResourceAggregate(Value address) { if (auto call = address.getDefiningOp()) { - if (call.getCallee() && *call.getCallee() == QIR_ARRAY_ELEMENT && - call.getNumOperands() >= 1) { + if (call.getCallee() && *call.getCallee() == QIR_ARRAY_ELEMENT) { return call.getOperand(0); } return {}; @@ -448,10 +374,18 @@ struct QIRSetAttributesAndMetadata final SmallVector, 8> aggregateStores; const auto includePointer = [&](Value pointer) { SmallPtrSet resolving; - status = includeStaticPointer(pointer, "qubit", requiredQubits, scope, - requireStatic, resolving, &qubitAggregates); + auto provenance = includeStaticPointer( + pointer, "qubit", requiredQubits, scope, resolving, &qubitAggregates); + if (failed(provenance)) { + status = failure(); + } else if (requireStatic && *provenance != PointerProvenance::Resolved) { + pointer.getParentBlock()->getParentOp()->emitError() + << "cannot determine the static QIR qubit index from pointer " + "provenance"; + status = failure(); + } }; - walkQIRAttributeOperationsIteratively(scope, [&](Operation* operation) { + scope.walk([&](Operation* operation) { if (failed(status)) { return; } @@ -513,11 +447,19 @@ struct QIRSetAttributesAndMetadata final LogicalResult status = success(); const auto includePointer = [&](Value pointer) { SmallPtrSet resolving; - status = includeStaticPointer(pointer, "result", requiredResults, scope, - requireStatic, resolving); + auto provenance = includeStaticPointer(pointer, "result", requiredResults, + scope, resolving); + if (failed(provenance)) { + status = failure(); + } else if (requireStatic && *provenance != PointerProvenance::Resolved) { + pointer.getParentBlock()->getParentOp()->emitError() + << "cannot determine the static QIR result index from pointer " + "provenance"; + status = failure(); + } }; - walkQIRAttributeOperationsIteratively(scope, [&](Operation* operation) { + scope.walk([&](Operation* operation) { if (failed(status)) { return; } @@ -529,10 +471,9 @@ struct QIRSetAttributesAndMetadata final if (!callee) { return; } - if (*callee == QIR_MEASURE && callOp.getNumOperands() >= 2) { + if (*callee == QIR_MEASURE) { includePointer(callOp.getOperand(1)); - } else if ((*callee == QIR_RECORD_OUTPUT || *callee == QIR_READ_RESULT) && - callOp.getNumOperands() >= 1) { + } else if (*callee == QIR_RECORD_OUTPUT || *callee == QIR_READ_RESULT) { includePointer(callOp.getOperand(0)); } }); @@ -609,7 +550,7 @@ struct QIRSetAttributesAndMetadata final bool useDynamicResult{false}; bool useArrays{false}; - walkQIRAttributeOperationsIteratively(scope, [&](Operation* operation) { + scope->walk([&](Operation* operation) { auto callOp = dyn_cast(operation); if (!callOp) { return; @@ -658,11 +599,8 @@ struct QIRSetAttributesAndMetadata final }; SmallVector functions; - walkQIRAttributeOperationsIteratively(moduleOp, [&](Operation* operation) { - if (auto function = dyn_cast(operation)) { - functions.emplace_back(function); - } - }); + moduleOp.walk( + [&](LLVM::LLVMFuncOp function) { functions.emplace_back(function); }); for (auto function : functions) { if (function.isExternal()) { continue; @@ -675,24 +613,22 @@ struct QIRSetAttributesAndMetadata final llvm::for_each(block.getArgumentTypes(), recordType); } size_t returnCount = 0; - walkQIRAttributeOperationsIteratively( - function, [&](Operation* operation) { - returnCount += isa(operation); - metadata.usesMultipleTargetBranching |= - isa(operation); - if (operation->hasTrait()) { - return; - } - const auto hasScalarResult = - llvm::any_of(operation->getResultTypes(), [](Type type) { - return isa(type) || type.isF16() || - type.isF32() || type.isF64(); - }); - if (hasScalarResult && !isa(operation)) { - llvm::for_each(operation->getOperandTypes(), recordType); - } - llvm::for_each(operation->getResultTypes(), recordType); - }); + function.walk([&](Operation* operation) { + returnCount += isa(operation); + metadata.usesMultipleTargetBranching |= isa(operation); + if (operation->hasTrait()) { + return; + } + const auto hasScalarResult = + llvm::any_of(operation->getResultTypes(), [](Type type) { + return isa(type) || type.isF16() || type.isF32() || + type.isF64(); + }); + if (hasScalarResult && !isa(operation)) { + llvm::for_each(operation->getOperandTypes(), recordType); + } + llvm::for_each(operation->getResultTypes(), recordType); + }); metadata.usesMultipleReturnPoints |= returnCount > 1; } } diff --git a/mlir/lib/Dialect/QIR/Transforms/QIRCleanup.cpp b/mlir/lib/Dialect/QIR/Transforms/QIRCleanup.cpp index 5710045fca..686387d3aa 100644 --- a/mlir/lib/Dialect/QIR/Transforms/QIRCleanup.cpp +++ b/mlir/lib/Dialect/QIR/Transforms/QIRCleanup.cpp @@ -10,7 +10,6 @@ #include "mlir/Dialect/QIR/Transforms/Passes.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" -#include "mlir/Support/OperationUtils.h" #include #include @@ -25,7 +24,6 @@ #include #include -#include #include namespace mlir::qir { @@ -33,23 +31,6 @@ namespace mlir::qir { #define GEN_PASS_DEF_QIRCLEANUPPASS #include "mlir/Dialect/QIR/Transforms/Passes.h.inc" -template -static void walkQIRCleanupOperationsIteratively(Operation* root, - Callback&& callback) { - SmallVector worklist{root}; - while (!worklist.empty()) { - Operation* operation = worklist.pop_back_val(); - callback(operation); - for (Region& region : operation->getRegions()) { - for (Block& block : region) { - for (Operation& nested : block) { - worklist.push_back(&nested); - } - } - } - } -} - [[nodiscard]] static StringAttr getMetadataKey(const Attribute attr) { auto pair = dyn_cast(attr); if (!pair || pair.size() != 2) { @@ -73,11 +54,7 @@ static void walkQIRCleanupOperationsIteratively(Operation* root, [[nodiscard]] static bool moduleHasDynamicQubitRuntimeCalls(ModuleOp module) { bool found = false; - walkQIRCleanupOperationsIteratively(module, [&](Operation* operation) { - auto callOp = dyn_cast(operation); - if (!callOp) { - return; - } + module.walk([&](LLVM::CallOp callOp) { const auto callee = getCalleeName(callOp); found |= callee == QIR_QUBIT_ALLOC || callee == QIR_QUBIT_ARRAY_ALLOC; }); @@ -86,11 +63,7 @@ static void walkQIRCleanupOperationsIteratively(Operation* root, [[nodiscard]] static bool moduleHasDynamicResultRuntimeCalls(ModuleOp module) { bool found = false; - walkQIRCleanupOperationsIteratively(module, [&](Operation* operation) { - auto callOp = dyn_cast(operation); - if (!callOp) { - return; - } + module.walk([&](LLVM::CallOp callOp) { const auto callee = getCalleeName(callOp); found |= callee == QIR_RESULT_ALLOC || callee == QIR_RESULT_ARRAY_ALLOC; }); @@ -178,10 +151,7 @@ struct RemoveDeadQubitArrayPair final : OpRewritePattern { LogicalResult matchAndRewrite(LLVM::CallOp releaseCall, PatternRewriter& rewriter) const override { - if (getCalleeName(releaseCall) != QIR_QUBIT_ARRAY_RELEASE || - releaseCall.getNumOperands() != 2 || releaseCall.getNumResults() != 0 || - !releaseCall.getOperand(0).getType().isInteger(64) || - !isa(releaseCall.getOperand(1).getType())) { + if (getCalleeName(releaseCall) != QIR_QUBIT_ARRAY_RELEASE) { return failure(); } @@ -202,10 +172,6 @@ struct RemoveDeadQubitArrayPair final : OpRewritePattern { } if (getCalleeName(callOp) != QIR_QUBIT_ARRAY_ALLOC || - callOp.getNumOperands() != 3 || callOp.getNumResults() != 0 || - !callOp.getOperand(0).getType().isInteger(64) || - !isa(callOp.getOperand(1).getType()) || - !isa(callOp.getOperand(2).getType()) || callOp.getOperand(1) != allocaOp.getResult() || !callOp.getOperand(2).getDefiningOp()) { return failure(); @@ -246,11 +212,6 @@ struct QIRCleanupPass final : impl::QIRCleanupPassBase { protected: void runOnOperation() override { auto module = getOperation(); - constexpr size_t maxRegionNesting = 64; - if (failed(verifyRegionNestingDepth(module, maxRegionNesting))) { - signalPassFailure(); - return; - } RewritePatternSet patterns(&getContext()); patterns.add(&getContext()); diff --git a/mlir/lib/Dialect/QTensor/Transforms/ShrinkRegisters.cpp b/mlir/lib/Dialect/QTensor/Transforms/ShrinkRegisters.cpp index 4b3ea45e8a..550d37583c 100644 --- a/mlir/lib/Dialect/QTensor/Transforms/ShrinkRegisters.cpp +++ b/mlir/lib/Dialect/QTensor/Transforms/ShrinkRegisters.cpp @@ -10,7 +10,6 @@ #include "mlir/Dialect/QTensor/IR/QTensorOps.h" #include "mlir/Dialect/QTensor/Transforms/Passes.h" -#include "mlir/Support/OperationUtils.h" #include #include @@ -47,6 +46,7 @@ markLiveIndex(int64_t index, int64_t tensorSize, } namespace { + struct TensorAccess { Operation* operation; int64_t index; @@ -203,11 +203,6 @@ struct ShrinkQTensorToFitPass final : impl::ShrinkQTensorToFitPassBase { protected: void runOnOperation() override { - constexpr size_t maxRegionNesting = 64; - if (failed(verifyRegionNestingDepth(getOperation(), maxRegionNesting))) { - signalPassFailure(); - return; - } RewritePatternSet patterns(&getContext()); patterns.add(&getContext()); diff --git a/mlir/lib/Support/Passes.cpp b/mlir/lib/Support/Passes.cpp index b8e2290aab..8c1302a361 100644 --- a/mlir/lib/Support/Passes.cpp +++ b/mlir/lib/Support/Passes.cpp @@ -125,8 +125,8 @@ void populateQCCleanupPipeline(OpPassManager& pm) { } void populateQCOCleanupPipeline(OpPassManager& pm) { - pm.addPass( - createCanonicalizerPass(GreedyRewriteConfig{}.setMaxIterations(64))); + pm.addPass(createCanonicalizerPass( + GreedyRewriteConfig{}.setMaxIterations(GreedyRewriteConfig::kNoLimit))); pm.addPass(mlir::mqt::createNormalizeGlobalPhases()); pm.addPass(createCSEPass()); pm.addPass(qtensor::createShrinkQTensorToFitPass()); diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 7d3e5a70e0..2e5c2705ed 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -2192,21 +2192,6 @@ barrier q[0], q[1]; EXPECT_EQ(qc->numTwoQubitGates(), 3); } -TEST_F(CompilerPipelineTest, QCProgramCountGatesWithoutEntryPoint) { - constexpr llvm::StringLiteral source = R"mlir(module { - func.func @helper() { - %qubit = qc.alloc : !qc.qubit - qc.dealloc %qubit : !qc.qubit - return - } - })mlir"; - auto qc = QCProgram::fromMLIRString(source); - ASSERT_TRUE(qc); - EXPECT_EQ(qc->numGates(), 0); - EXPECT_EQ(qc->numSingleQubitGates(), 0); - EXPECT_EQ(qc->numTwoQubitGates(), 0); -} - /** * @brief Test: gate counting includes each structured control-flow region once. */ diff --git a/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp b/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp index e52f884681..2c5bd03986 100644 --- a/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp +++ b/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp @@ -16,11 +16,9 @@ #include "mlir/Dialect/CBit/IR/CBitOps.h" #include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Transforms/Passes.h" -#include "mlir/Dialect/QC/IR/QCDialect.h" #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/QCOUtils.h" #include "mlir/Support/Passes.h" #include "qco_programs.h" @@ -451,31 +449,6 @@ static ModuleOp createJeffMetadataModule(MLIRContext& context, return module; } -TEST(JeffRoundTripRegressionTest, ConversionPassesDeclareModuleContracts) { - auto toQCO = createJeffToQCO(); - ASSERT_TRUE(toQCO->getOpName()); - EXPECT_EQ(*toQCO->getOpName(), ModuleOp::getOperationName()); - - auto toJeff = createQCOToJeff(); - ASSERT_TRUE(toJeff->getOpName()); - EXPECT_EQ(*toJeff->getOpName(), ModuleOp::getOperationName()); - DialectRegistry toQCORegistry; - toQCO->getDependentDialects(toQCORegistry); - EXPECT_TRUE(toQCORegistry.getDialectAllocator( - func::FuncDialect::getDialectNamespace())); - EXPECT_TRUE(toQCORegistry.getDialectAllocator( - qco::QCODialect::getDialectNamespace())); - - DialectRegistry registry; - toJeff->getDependentDialects(registry); - EXPECT_TRUE( - registry.getDialectAllocator(arith::ArithDialect::getDialectNamespace())); - EXPECT_TRUE( - registry.getDialectAllocator(qc::QCDialect::getDialectNamespace())); - EXPECT_TRUE( - registry.getDialectAllocator(qco::QCODialect::getDialectNamespace())); -} - TEST(JeffRoundTripRegressionTest, RejectsMalformedJeffMetadataBeforeMutation) { DialectRegistry registry; registry.insert(); - MLIRContext context(registry); - context.loadAllAvailableDialects(); - constexpr llvm::StringLiteral source = R"mlir( -module { - func.func @main() attributes {mqt.entry_point} { - %first = qco.static 0 : !qco.qubit - %second = qco.static 0 : !qco.qubit - qco.sink %first : !qco.qubit - qco.sink %second : !qco.qubit - return - } -} -)mlir"; - auto module = parseSourceString(source, &context); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); - auto before = module->clone(); - - bool sawExpectedDiagnostic = false; - ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { - std::string message; - llvm::raw_string_ostream stream(message); - diagnostic.print(stream); - sawExpectedDiagnostic |= - StringRef(message).contains("found duplicate index 0"); - return success(); - }); - EXPECT_TRUE(failed(convertQCOToJeffDirect(*module))); - EXPECT_TRUE(sawExpectedDiagnostic); - EXPECT_TRUE(isEquivalentToClone(*module, before)); -} - TEST(JeffRoundTripRegressionTest, RejectsRankZeroQubitTensorBeforeMutation) { DialectRegistry registry; registry.insert(); diff --git a/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp b/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp index 7cca6c9f49..f6dccc2e48 100644 --- a/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp +++ b/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp @@ -14,7 +14,6 @@ #include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" #include "mlir/Dialect/QC/IR/QCDialect.h" -#include "mlir/Dialect/QC/IR/QCOps.h" #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" @@ -24,7 +23,6 @@ #include #include -#include #include #include #include @@ -90,21 +88,6 @@ static LogicalResult runQCOToQCConversion(ModuleOp module) { return pm.run(module); } -TEST(QCOToQCPassContract, IsModuleAnchoredAndDeclaresCreatedDialects) { - auto pass = createQCOToQC(); - ASSERT_TRUE(pass->getOpName()); - EXPECT_EQ(*pass->getOpName(), ModuleOp::getOperationName()); - - DialectRegistry registry; - pass->getDependentDialects(registry); - EXPECT_TRUE( - registry.getDialectAllocator(func::FuncDialect::getDialectNamespace())); - EXPECT_TRUE(registry.getDialectAllocator( - cf::ControlFlowDialect::getDialectNamespace())); - EXPECT_TRUE( - registry.getDialectAllocator(scf::SCFDialect::getDialectNamespace())); -} - TEST(QCOToQCRegressionTest, RejectsYieldPermutationWithoutMutation) { DialectRegistry registry; registry.insert(); - MLIRContext context(registry); - context.loadAllAvailableDialects(); - - constexpr llvm::StringLiteral source = R"mlir( -module { - func.func @main() attributes {mqt.entry_point} { - %first = qco.static 7 : !qco.qubit - %second = qco.static 7 : !qco.qubit - qco.sink %first : !qco.qubit - qco.sink %second : !qco.qubit - return - } -} -)mlir"; - - auto module = parseSourceString(source, &context); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); - OwningOpRef original(module->clone()); - - bool sawExpectedDiagnostic = false; - ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { - sawExpectedDiagnostic |= - StringRef(diagnostic.str()).contains("found duplicate index 7"); - return success(); - }); - EXPECT_TRUE(failed(runQCOToQCConversion(*module))); - EXPECT_TRUE(sawExpectedDiagnostic); - EXPECT_TRUE(OperationEquivalence::isEquivalentTo( - module->getOperation(), original->getOperation(), - OperationEquivalence::Flags::None)); -} - -TEST(QCOToQCRegressionTest, - RejectsStaticIndexReacquisitionAfterSinkWithoutMutation) { - DialectRegistry registry; - registry.insert(); - MLIRContext context(registry); - context.loadAllAvailableDialects(); - - constexpr llvm::StringLiteral source = R"mlir( -module { - func.func @main() attributes {mqt.entry_point} { - %first = qco.static 7 : !qco.qubit - %after_h = qco.h %first : !qco.qubit -> !qco.qubit - qco.sink %after_h : !qco.qubit - %second = qco.static 7 : !qco.qubit - %after_x = qco.x %second : !qco.qubit -> !qco.qubit - qco.sink %after_x : !qco.qubit - return - } -} -)mlir"; - - auto module = parseSourceString(source, &context); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); - OwningOpRef original(module->clone()); - - bool sawExpectedDiagnostic = false; - ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { - sawExpectedDiagnostic |= - StringRef(diagnostic.str()).contains("found duplicate index 7"); - return success(); - }); - EXPECT_TRUE(failed(runQCOToQCConversion(*module))); - EXPECT_TRUE(sawExpectedDiagnostic); - EXPECT_TRUE(OperationEquivalence::isEquivalentTo( - module->getOperation(), original->getOperation(), - OperationEquivalence::Flags::None)); -} - -TEST(QCOToQCRegressionTest, RejectsNonlinearQubitsWithoutMutation) { - DialectRegistry registry; - registry.insert(); - MLIRContext context(registry); - context.loadAllAvailableDialects(); - - constexpr llvm::StringLiteral source = R"mlir( -module { - func.func @main() attributes {mqt.entry_point} { - %q0 = qco.alloc : !qco.qubit - %q1 = qco.h %q0 : !qco.qubit -> !qco.qubit - %q2 = qco.x %q0 : !qco.qubit -> !qco.qubit - qco.sink %q1 : !qco.qubit - qco.sink %q2 : !qco.qubit - return - } -} -)mlir"; - - auto module = parseSourceString(source, &context); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); - OwningOpRef original(module->clone()); - - bool sawExpectedDiagnostic = false; - ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { - sawExpectedDiagnostic |= - StringRef(diagnostic.str()).contains("exactly one use"); - return success(); - }); - EXPECT_TRUE(failed(runQCOToQCConversion(*module))); - EXPECT_TRUE(sawExpectedDiagnostic); - EXPECT_TRUE(OperationEquivalence::isEquivalentTo( - module->getOperation(), original->getOperation(), - OperationEquivalence::Flags::None)); -} - -TEST(QCOToQCRegressionTest, PreservesQTensorInsertSlotUpdates) { - DialectRegistry registry; - registry.insert(); - MLIRContext context(registry); - context.loadAllAvailableDialects(); - - constexpr llvm::StringLiteral source = R"mlir( -module { - func.func @main() attributes {mqt.entry_point} { - %c0 = arith.constant 0 : index - %c1 = arith.constant 1 : index - %c2 = arith.constant 2 : index - %tensor0 = qtensor.alloc(%c2) : tensor<2x!qco.qubit> - %tensor1, %q0 = qtensor.extract %tensor0[%c0] : tensor<2x!qco.qubit> - %tensor2, %q1 = qtensor.extract %tensor1[%c1] : tensor<2x!qco.qubit> - %tensor3 = qtensor.insert %q0 into %tensor2[%c1] : tensor<2x!qco.qubit> - %tensor4 = qtensor.insert %q1 into %tensor3[%c0] : tensor<2x!qco.qubit> - %tensor5, %at0 = qtensor.extract %tensor4[%c0] : tensor<2x!qco.qubit> - %tensor6, %at1 = qtensor.extract %tensor5[%c1] : tensor<2x!qco.qubit> - qco.sink %at0 : !qco.qubit - qco.sink %at1 : !qco.qubit - qtensor.dealloc %tensor6 : tensor<2x!qco.qubit> - return - } -} -)mlir"; - - auto module = parseSourceString(source, &context); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); - ASSERT_TRUE(succeeded(runQCOToQCConversion(*module))); - ASSERT_TRUE(succeeded(verify(*module))); - - SmallVector stores; - module->walk([&](memref::StoreOp store) { stores.push_back(store); }); - ASSERT_EQ(stores.size(), 2U); - EXPECT_NE(stores[0].getValue(), stores[1].getValue()); - EXPECT_EQ(stores[0].getMemref(), stores[1].getMemref()); - - SmallVector loads; - module->walk([&](memref::LoadOp load) { loads.push_back(load); }); - ASSERT_EQ(loads.size(), 3U); - EXPECT_TRUE(stores[1]->isBeforeInBlock(loads[2])); - - bool containsQTensorOperations = false; - module->walk([&](Operation* operation) { - containsQTensorOperations |= - operation->getDialect() == - context.getLoadedDialect(); - }); - EXPECT_FALSE(containsQTensorOperations); -} - -TEST(QCOToQCRegressionTest, InvalidatesQTensorCacheAcrossLoopSlotSwap) { - DialectRegistry registry; - registry.insert(); - MLIRContext context(registry); - context.loadAllAvailableDialects(); - - constexpr llvm::StringLiteral source = R"mlir( -module { - func.func @main() attributes {mqt.entry_point} { - %c0 = arith.constant 0 : index - %c1 = arith.constant 1 : index - %c2 = arith.constant 2 : index - %tensor0 = qtensor.alloc(%c2) : tensor<2x!qco.qubit> - %tensor1, %before = qtensor.extract %tensor0[%c0] : tensor<2x!qco.qubit> - %tensor2 = qtensor.insert %before into %tensor1[%c0] : tensor<2x!qco.qubit> - %tensor3 = scf.for %iv = %c0 to %c1 step %c1 - iter_args(%tensor = %tensor2) -> (tensor<2x!qco.qubit>) { - %tensor4, %left = qtensor.extract %tensor[%c0] : tensor<2x!qco.qubit> - %tensor5, %right = qtensor.extract %tensor4[%c1] : tensor<2x!qco.qubit> - %tensor6 = qtensor.insert %left into %tensor5[%c1] : tensor<2x!qco.qubit> - %tensor7 = qtensor.insert %right into %tensor6[%c0] : tensor<2x!qco.qubit> - scf.yield %tensor7 : tensor<2x!qco.qubit> - } - %tensor8, %at0 = qtensor.extract %tensor3[%c0] : tensor<2x!qco.qubit> - %tensor9, %at1 = qtensor.extract %tensor8[%c1] : tensor<2x!qco.qubit> - qco.sink %at0 : !qco.qubit - qco.sink %at1 : !qco.qubit - qtensor.dealloc %tensor9 : tensor<2x!qco.qubit> - return - } -} -)mlir"; - - auto module = parseSourceString(source, &context); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); - ASSERT_TRUE(succeeded(runQCOToQCConversion(*module))); - ASSERT_TRUE(succeeded(verify(*module))); - - auto function = *module->getOps().begin(); - auto loops = llvm::to_vector(function.getBody().getOps()); - ASSERT_EQ(loops.size(), 1U); - EXPECT_EQ(llvm::range_size(loops[0].getBody()->getOps()), - 2U); - - SmallVector loadsBeforeLoop; - SmallVector loadsAfterLoop; - for (auto load : function.getBody().front().getOps()) { - (load->isBeforeInBlock(loops[0]) ? loadsBeforeLoop : loadsAfterLoop) - .push_back(load); - } - EXPECT_EQ(loadsBeforeLoop.size(), 1U); - EXPECT_EQ(loadsAfterLoop.size(), 2U); -} - TEST(QCOToQCRegressionTest, RetainsQubitRegisterName) { DialectRegistry registry; registry.insert #include #include -#include #include #include #include @@ -114,21 +113,6 @@ static LogicalResult runQCToQCOConversion(ModuleOp module) { return pm.run(module); } -TEST(QCToQCOPassContract, IsModuleAnchoredAndDeclaresCreatedDialects) { - auto pass = createQCToQCO(); - ASSERT_TRUE(pass->getOpName()); - EXPECT_EQ(*pass->getOpName(), ModuleOp::getOperationName()); - - DialectRegistry registry; - pass->getDependentDialects(registry); - EXPECT_TRUE( - registry.getDialectAllocator(func::FuncDialect::getDialectNamespace())); - EXPECT_TRUE(registry.getDialectAllocator( - cf::ControlFlowDialect::getDialectNamespace())); - EXPECT_TRUE( - registry.getDialectAllocator(scf::SCFDialect::getDialectNamespace())); -} - namespace { class QCToQCORegressionTest : public testing::Test { diff --git a/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp b/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp index a4f7bc80e1..1f65051a25 100644 --- a/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp +++ b/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp @@ -16,7 +16,6 @@ #include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/IR/QCOps.h" -#include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QIR/Builder/QIRProgramBuilder.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" #include "mlir/Support/Passes.h" @@ -110,23 +109,6 @@ static bool isEquivalentToClone(ModuleOp module, ModuleOp clone) { OperationEquivalence::Flags::None); } -TEST(QCToQIRAdaptiveNativeTest, DeclaresModuleRootAndProducedDialects) { - auto pass = createQCToQIRAdaptive(); - ASSERT_TRUE(pass->getOpName()); - EXPECT_EQ(*pass->getOpName(), ModuleOp::getOperationName()); - - DialectRegistry registry; - pass->getDependentDialects(registry); - EXPECT_TRUE( - registry.getDialectAllocator(arith::ArithDialect::getDialectNamespace())); - EXPECT_TRUE(registry.getDialectAllocator( - cf::ControlFlowDialect::getDialectNamespace())); - EXPECT_TRUE( - registry.getDialectAllocator(qc::QCDialect::getDialectNamespace())); - EXPECT_TRUE( - registry.getDialectAllocator(qco::QCODialect::getDialectNamespace())); -} - TEST(QCToQIRAdaptiveNativeTest, RejectsExcessiveClassicalResultCapacityAtomically) { MLIRContext context; 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 20ff89e041..5a26265bad 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 @@ -16,7 +16,6 @@ #include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/IR/QCOps.h" -#include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QIR/Builder/QIRProgramBuilder.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" #include "mlir/Support/Passes.h" @@ -328,21 +327,6 @@ TEST(QCToQIRBaseNativeTest, RejectsLiveMeasurementResultUseWithoutMutation) { EXPECT_TRUE(isEquivalentToClone(*module, before)); } -TEST(QCToQIRBaseNativeTest, DeclaresModuleRootAndProducedDialects) { - auto pass = createQCToQIRBase(); - ASSERT_TRUE(pass->getOpName()); - EXPECT_EQ(*pass->getOpName(), ModuleOp::getOperationName()); - - DialectRegistry registry; - pass->getDependentDialects(registry); - EXPECT_TRUE( - registry.getDialectAllocator(arith::ArithDialect::getDialectNamespace())); - EXPECT_TRUE( - registry.getDialectAllocator(qc::QCDialect::getDialectNamespace())); - EXPECT_TRUE( - registry.getDialectAllocator(qco::QCODialect::getDialectNamespace())); -} - TEST(QCToQIRBaseNativeTest, RejectsExcessiveClassicalResultCapacityAtomically) { MLIRContext context; context.loadDialect(); - OpBuilder builder(&context); - auto module = ModuleOp::create(builder.getUnknownLoc()); - builder.setInsertionPointToStart(module.getBody()); - auto main = func::FuncOp::create(builder, builder.getUnknownLoc(), "main", - builder.getFunctionType({}, {})); - mlir::mqt::setEntryPoint(main); - auto* entry = main.addEntryBlock(); - auto* exit = main.addBlock(); - builder.setInsertionPointToEnd(entry); - cf::BranchOp::create(builder, builder.getUnknownLoc(), exit); - builder.setInsertionPointToEnd(exit); - func::ReturnOp::create(builder, builder.getUnknownLoc()); - ASSERT_TRUE(succeeded(verify(module))); - auto before = module.clone(); - - bool sawExpectedDiagnostic = false; - ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { - std::string message; - llvm::raw_string_ostream(message) << diagnostic; - sawExpectedDiagnostic |= - StringRef(message).contains("requires a single-block entry function"); - return success(); - }); - EXPECT_TRUE(failed(runQCToQIRBasePass(module))); - EXPECT_TRUE(sawExpectedDiagnostic); - EXPECT_TRUE(isEquivalentToClone(module, before)); -} - TEST(QCToQIRBaseNativeTest, RejectsMultiBlockEntryFunctionWithoutMutation) { MLIRContext context; context.loadDialect #include #include -#include #include #include #include @@ -151,30 +150,6 @@ TEST_F(GlobalPhaseNormalizationTest, CombinesQCOConstantsAtBlockExit) { expectFoldableGlobalPhase(phases.front().getTheta(), 0.75); } -TEST_F(GlobalPhaseNormalizationTest, RejectsNonlinearQCOInputWithoutMutation) { - auto moduleOp = parse(R"mlir( - module { - func.func @test(%q: !qco.qubit) { - %phase = arith.constant 0.25 : f64 - qco.gphase(%phase) - %x = qco.x %q : !qco.qubit -> !qco.qubit - %h = qco.h %q : !qco.qubit -> !qco.qubit - qco.sink %x : !qco.qubit - qco.sink %h : !qco.qubit - return - } - } - )mlir"); - ASSERT_TRUE(moduleOp); - ASSERT_TRUE(succeeded(verify(*moduleOp))); - OwningOpRef before(cast((*moduleOp)->clone())); - - EXPECT_TRUE(failed(mlir::mqt::normalizeGlobalPhases(*moduleOp))); - EXPECT_TRUE(OperationEquivalence::isEquivalentTo( - moduleOp->getOperation(), before->getOperation(), - OperationEquivalence::Flags::None)); -} - TEST_F(GlobalPhaseNormalizationTest, CombinesQCAndQCOPhasesIndependently) { auto moduleOp = parse(R"mlir( module { @@ -386,53 +361,6 @@ TEST_F(GlobalPhaseNormalizationTest, EXPECT_LE(countOperations(), firstRunOperationCount); } -TEST_F(GlobalPhaseNormalizationTest, - HandlesDeepModifierAndAngleChainsIteratively) { - OwningOpRef moduleOp = ModuleOp::create(UnknownLoc::get(context.get())); - OpBuilder builder(context.get()); - builder.setInsertionPointToStart(moduleOp->getBody()); - const auto loc = moduleOp->getLoc(); - const auto qubitType = qco::QubitType::get(context.get()); - auto function = func::FuncOp::create( - builder, loc, "test", - builder.getFunctionType({qubitType, builder.getF64Type()}, {qubitType})); - auto* entry = function.addEntryBlock(); - builder.setInsertionPointToStart(entry); - - constexpr std::size_t modifierDepth = 512; - constexpr std::size_t angleDepth = 512; - auto outer = - qco::InvOp::create(builder, loc, ValueRange{function.getArgument(0)}); - auto* body = &outer.getBodyRegion().emplaceBlock(); - auto currentQubit = body->addArgument(qubitType, loc); - for (std::size_t i = 1; i < modifierDepth; ++i) { - builder.setInsertionPointToEnd(body); - auto inner = qco::InvOp::create(builder, loc, ValueRange{currentQubit}); - qco::YieldOp::create(builder, loc, inner.getQubitsOut()); - body = &inner.getBodyRegion().emplaceBlock(); - currentQubit = body->addArgument(qubitType, loc); - } - - builder.setInsertionPointToEnd(body); - Value angle = function.getArgument(1); - for (std::size_t i = 0; i < angleDepth; ++i) { - auto zero = - arith::ConstantOp::create(builder, loc, builder.getF64FloatAttr(0.0)); - angle = arith::AddFOp::create(builder, loc, angle, zero); - } - auto z = qco::ZOp::create(builder, loc, currentQubit); - qco::GPhaseOp::create(builder, loc, angle); - qco::YieldOp::create(builder, loc, ValueRange{z.getOutputTarget(0)}); - - builder.setInsertionPointToEnd(entry); - func::ReturnOp::create(builder, loc, outer.getQubitsOut()); - - ASSERT_TRUE(mlir::mqt::normalizeGlobalPhases(*moduleOp).succeeded()); - auto phases = llvm::to_vector(function.getBody().getOps()); - ASSERT_EQ(phases.size(), 1); - EXPECT_EQ(phases.front().getTheta().getParentBlock(), entry); -} - TEST_F(GlobalPhaseNormalizationTest, KeepsSCFStyleRegionsIndependent) { auto moduleOp = parse(R"mlir( module { diff --git a/mlir/unittests/Dialect/MQT/Utils/CMakeLists.txt b/mlir/unittests/Dialect/MQT/Utils/CMakeLists.txt index 592bf561a2..3096dc4013 100644 --- a/mlir/unittests/Dialect/MQT/Utils/CMakeLists.txt +++ b/mlir/unittests/Dialect/MQT/Utils/CMakeLists.txt @@ -8,9 +8,8 @@ set(mqt_utils_target mqt-core-mlir-unittests-mqt-utils) add_executable(${mqt_utils_target} test_constant_folding.cpp test_gate_powering.cpp) -target_link_libraries( - ${mqt_utils_target} PRIVATE GTest::gtest_main MLIRArithDialect MLIRFuncDialect MLIRIndexDialect - MLIRIR MLIRMQTUtils) +target_link_libraries(${mqt_utils_target} PRIVATE GTest::gtest_main MLIRArithDialect + MLIRFuncDialect MLIRIR MLIRMQTUtils) mqt_mlir_configure_unittest_target(${mqt_utils_target}) gtest_discover_tests(${mqt_utils_target} PROPERTIES LABELS mqt-mlir-unittests DISCOVERY_TIMEOUT 60) diff --git a/mlir/unittests/Dialect/MQT/Utils/test_constant_folding.cpp b/mlir/unittests/Dialect/MQT/Utils/test_constant_folding.cpp index cb1f0554a4..3ccde2b38b 100644 --- a/mlir/unittests/Dialect/MQT/Utils/test_constant_folding.cpp +++ b/mlir/unittests/Dialect/MQT/Utils/test_constant_folding.cpp @@ -13,8 +13,6 @@ #include #include #include -#include -#include #include #include #include @@ -41,8 +39,7 @@ class ConstantFoldingTest : public ::testing::Test { std::unique_ptr builder; void SetUp() override { - context.loadDialect(); + context.loadDialect(); auto loc = FileLineColLoc::get(&context, "", 1, 1); module = ModuleOp::create(loc); @@ -69,13 +66,6 @@ TEST_F(ConstantFoldingTest, valueToDoubleCastFromInteger) { EXPECT_DOUBLE_EQ(*stdValue, 42.0); } -TEST_F(ConstantFoldingTest, valueToDoubleConstantLike) { - auto op = index::ConstantOp::create(*builder, 42); - const auto stdValue = mlir::mqt::valueToDouble(op.getResult()); - ASSERT_TRUE(stdValue.has_value()); - EXPECT_DOUBLE_EQ(*stdValue, 42.0); -} - TEST_F(ConstantFoldingTest, valueToDoubleCastFromNegativeInteger) { auto op = arith::ConstantOp::create(*builder, builder->getSI32IntegerAttr(-123)); @@ -261,16 +251,3 @@ TEST_F(ConstantFoldingTest, valueToConstantDoubleSharedOperandsFailure) { EXPECT_FALSE(it->second.has_value()); } } - -TEST_F(ConstantFoldingTest, valueToConstantAttrHandlesDeepExpressions) { - constexpr int depth = 10000; - Value value = arith::ConstantIntOp::create(*builder, 1, 64); - Value zero = arith::ConstantIntOp::create(*builder, 0, 64); - for (int i = 0; i < depth; ++i) { - value = arith::AddIOp::create(*builder, value, zero); - } - - const auto folded = mlir::mqt::valueToConstantAttr(value); - ASSERT_TRUE(folded); - EXPECT_EQ(cast(*folded).getInt(), 1); -} diff --git a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp index 755542d870..5773799a5b 100644 --- a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp +++ b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp @@ -714,30 +714,6 @@ TEST(OpenQASM3EmissionTest, RejectsExcessiveClassicalRegisterWidth) { EXPECT_TRUE(failed(qc::translateQCToOpenQASM3(*moduleOp))); } -TEST(OpenQASM3EmissionTest, RejectsExcessiveRegionNesting) { - std::string source = "module { func.func @main() { "; - for (size_t i = 0; i < 64; ++i) { - source += "scf.execute_region { "; - } - source += "scf.yield "; - for (size_t i = 0; i < 64; ++i) { - source += "} "; - if (i + 1 < 64) { - source += "scf.yield "; - } - } - source += "return } }"; - - DialectRegistry registry = emissionDialects(); - MLIRContext context(registry); - context.loadAllAvailableDialects(); - auto moduleOp = parseSourceString(source, &context); - ASSERT_TRUE(moduleOp); - ASSERT_TRUE(succeeded(verify(*moduleOp))); - - EXPECT_TRUE(failed(qc::translateQCToOpenQASM3(*moduleOp))); -} - TEST(OpenQASM3EmissionTest, RejectsInvalidModifierBodies) { DialectRegistry registry = emissionDialects(); MLIRContext context(registry); diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp index afb08815dc..fea17100d4 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp @@ -360,28 +360,6 @@ TEST_F(QCOTest, BarrierRejectsMismatchedInputAndOutputArity) { context.get())); } -TEST_F(QCOTest, BarrierCanonicalizationHandlesUnusedOutput) { - auto module = parseSourceString(R"mlir( - module { - func.func @main() { - %qubit = qco.static 0 : !qco.qubit - %unused = qco.barrier %qubit : !qco.qubit -> !qco.qubit - return - } - } - )mlir", - context.get()); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); - - PassManager manager(context.get()); - manager.addPass(createCanonicalizerPass()); - EXPECT_TRUE(succeeded(manager.run(*module))); - EXPECT_TRUE(succeeded(verify(*module))); - auto function = *module->getOps().begin(); - EXPECT_EQ(range_size(function.getOps()), 0U); -} - TEST_F(QCOTest, BarrierCanonicalizationPreservesPartialOverlap) { auto module = parseSourceString(R"mlir( module { @@ -450,50 +428,6 @@ TEST_F(QCOTest, BarrierCanonicalizationMergesIdenticalSuccessors) { EXPECT_EQ(barriers[0].getQubitsIn()[1], statics[1].getQubit()); } -TEST_F(QCOTest, GateCanonicalizationHandlesUnusedOutput) { - auto module = parseSourceString(R"mlir( - module { - func.func @main() { - %qubit = qco.static 0 : !qco.qubit - %theta = arith.constant 0.25 : f64 - %unused = qco.rz(%theta) %qubit : !qco.qubit -> !qco.qubit - - %r_qubit = qco.static 1 : !qco.qubit - %phi = arith.constant 0.25 : f64 - %unused_r = qco.r(%theta, %phi) %r_qubit - : !qco.qubit -> !qco.qubit - - %rxx_q0 = qco.static 2 : !qco.qubit - %rxx_q1 = qco.static 3 : !qco.qubit - %unused_rxx0, %unused_rxx1 = qco.rxx(%theta) %rxx_q0, %rxx_q1 - : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit - - %rccx_q0 = qco.static 4 : !qco.qubit - %rccx_q1 = qco.static 5 : !qco.qubit - %rccx_q2 = qco.static 6 : !qco.qubit - %unused_rccx0, %unused_rccx1, %unused_rccx2 = - qco.rccx %rccx_q0, %rccx_q1, %rccx_q2 - : !qco.qubit, !qco.qubit, !qco.qubit - -> !qco.qubit, !qco.qubit, !qco.qubit - return - } - } - )mlir", - context.get()); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); - - PassManager manager(context.get()); - manager.addPass(createCanonicalizerPass()); - EXPECT_TRUE(succeeded(manager.run(*module))); - EXPECT_TRUE(succeeded(verify(*module))); - auto function = *module->getOps().begin(); - EXPECT_EQ(range_size(function.getOps()), 0U); - EXPECT_EQ(range_size(function.getOps()), 0U); - EXPECT_EQ(range_size(function.getOps()), 0U); - EXPECT_EQ(range_size(function.getOps()), 0U); -} - TEST_F(QCOTest, CtrlRejectsMismatchedInputAndOutputArity) { for (const bool mismatchControls : {false, true}) { SCOPED_TRACE(testing::Message() << "mismatchControls=" << mismatchControls); @@ -3562,33 +3496,6 @@ static LogicalResult runUnrollModifiers(ModuleOp moduleOp) { return pm.run(moduleOp); } -TEST_F(QCOTest, UnrollModifiersRejectsNonLinearBodyAtomically) { - auto module = parseSourceString(R"mlir( - module { - func.func @main() { - %qubit = qco.alloc : !qco.qubit - %out = qco.inv (%arg = %qubit) { - %first = qco.x %arg : !qco.qubit -> !qco.qubit - %unused = qco.h %first : !qco.qubit -> !qco.qubit - qco.yield %first : !qco.qubit - } : {!qco.qubit} -> {!qco.qubit} - qco.sink %out : !qco.qubit - return - } - } - )mlir", - context.get()); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); - std::string before; - llvm::raw_string_ostream(before) << *module; - - EXPECT_TRUE(failed(runUnrollModifiers(*module))); - std::string after; - llvm::raw_string_ostream(after) << *module; - EXPECT_EQ(after, before); -} - /// Unrolls @p program and checks that it matches @p reference. static void expectUnrollsTo(MLIRContext* context, diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp index 78a0cf2a8d..ef9958c2f8 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp @@ -140,30 +140,6 @@ static Value composedBodyWithNestedPow(QCOProgramBuilder& b) { return b.measure(powOut).second; } -static Value buildAlternatingModifierNesting(QCOProgramBuilder& builder, - Value qubit, size_t depth) { - if (depth == 0) { - return builder.x(qubit); - } - if (depth % 3 == 0) { - return builder - .ctrl(ValueRange{}, qubit, - [&](Value argument) { - return buildAlternatingModifierNesting(builder, argument, - depth - 1); - }) - .second; - } - if (depth % 3 == 1) { - return builder.inv(qubit, [&](Value argument) { - return buildAlternatingModifierNesting(builder, argument, depth - 1); - }); - } - return builder.pow(1.0, qubit, [&](Value argument) { - return buildAlternatingModifierNesting(builder, argument, depth - 1); - }); -} - template static void assertCanonicalizedPowMatrixMatches(MLIRContext* context, Builder&& build) { @@ -418,32 +394,6 @@ TEST_F(QCOMatrixTest, DenseUnitaryComposesThroughModifiers) { DynamicMatrix(SOp::getUnitaryMatrix().adjoint()))); } -TEST_F(QCOMatrixTest, DeeplyNestedModifierMatrixQueriesFailSafely) { - constexpr size_t nestingDepth = 67; - auto module = QCOProgramBuilder::build( - context.get(), [&](QCOProgramBuilder& builder) -> Value { - auto qubit = buildAlternatingModifierNesting( - builder, builder.allocQubit(), nestingDepth); - return builder.measure(qubit).second; - }); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); - - auto inverse = firstInvOp(*module); - auto controls = inverse.getBody()->getOps(); - ASSERT_FALSE(controls.empty()); - auto control = *controls.begin(); - auto powers = control.getBody()->getOps(); - ASSERT_FALSE(powers.empty()); - auto power = *powers.begin(); - - EXPECT_FALSE(inverse.hasCompileTimeKnownUnitaryMatrix()); - EXPECT_FALSE(inverse.getUnitaryMatrix()); - EXPECT_FALSE(control.hasCompileTimeKnownUnitaryMatrix()); - EXPECT_FALSE(control.getUnitaryMatrix()); - EXPECT_FALSE(power.hasCompileTimeKnownUnitaryMatrix()); - EXPECT_FALSE(power.getUnitaryMatrix()); -} /// @} /// \name QCO/Modifiers/CtrlOp.cpp @@ -906,16 +856,18 @@ TEST_F(QCOMatrixTest, ComposeNTargetRejectsRuntimeUnitaryMatrix) { TEST_F(QCOMatrixTest, ComposeBodyMatrixRejectsNestedUnknownUnitary) { constexpr auto mlirCode = R"mlir( module { - func.func @test() -> !qco.qubit { - %condition = arith.constant true + func.func @test(%condition: i1) -> !qco.qubit { %q_in = qco.alloc : !qco.qubit %q_out = qco.inv (%q = %q_in) { %q_1 = qco.h %q : !qco.qubit -> !qco.qubit - scf.if %condition { - %nested = qco.x %q_1 : !qco.qubit -> !qco.qubit - scf.yield + %q_2 = qco.if %condition args(%nested_arg = %q_1) + -> (!qco.qubit) { + %nested = qco.x %nested_arg : !qco.qubit -> !qco.qubit + qco.yield %nested : !qco.qubit + } else args(%nested_arg = %q_1) { + qco.yield %nested_arg : !qco.qubit } - qco.yield %q_1 : !qco.qubit + qco.yield %q_2 : !qco.qubit } : {!qco.qubit} -> {!qco.qubit} return %q_out : !qco.qubit } @@ -924,6 +876,8 @@ TEST_F(QCOMatrixTest, ComposeBodyMatrixRejectsNestedUnknownUnitary) { auto moduleOp = parseSourceString(mlirCode, context.get()); ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + ASSERT_TRUE(succeeded(verifyLinearity(*moduleOp))); EXPECT_FALSE( composeBodyMatrix(*firstInvOp(*moduleOp).getBody(), 1).has_value()); } diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index 5685b2488e..8e828fc8fd 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -29,17 +29,14 @@ #include #include #include -#include #include #include #include -#include #include #include #include #include #include -#include #include #include #include @@ -1083,15 +1080,6 @@ static SmallVector singleQubitRunsSplitByScfFor(QCOProgramBuilder& b) { // FuseSingleQubitUnitaryRuns tests //===----------------------------------------------------------------------===// -TEST(FuseSingleQubitUnitaryRunsTest, DeclaresProducedDialects) { - auto pass = qco::createFuseSingleQubitUnitaryRuns({}); - DialectRegistry registry; - pass->getDependentDialects(registry); - EXPECT_TRUE(registry.getDialectAllocator("qc")); - EXPECT_TRUE( - registry.getDialectAllocator(qco::QCODialect::getDialectNamespace())); -} - TEST(FuseSingleQubitUnitaryRunsTest, InvalidBasisFailsPass) { TestFixture fx; fx.setUp(); @@ -1101,39 +1089,6 @@ TEST(FuseSingleQubitUnitaryRunsTest, InvalidBasisFailsPass) { EXPECT_TRUE(failed(runFuse(*owned, "not-a-basis"))); } -TEST(FuseSingleQubitUnitaryRunsTest, RejectsNonlinearQubitWithoutMutation) { - TestFixture fx; - fx.setUp(); - constexpr StringLiteral source = R"mlir( -module { - func.func @main() { - %q0 = qco.static 0 : !qco.qubit - %q1 = qco.h %q0 : !qco.qubit -> !qco.qubit - %q2 = qco.x %q0 : !qco.qubit -> !qco.qubit - qco.sink %q1 : !qco.qubit - qco.sink %q2 : !qco.qubit - return - } -} -)mlir"; - auto module = parseSourceString(source, fx.ctx()); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); - OwningOpRef original(module->clone()); - - bool sawExpectedDiagnostic = false; - ScopedDiagnosticHandler handler(fx.ctx(), [&](Diagnostic& diagnostic) { - sawExpectedDiagnostic |= - StringRef(diagnostic.str()).contains("exactly one use"); - return success(); - }); - EXPECT_TRUE(failed(runFuse(*module, "zyz"))); - EXPECT_TRUE(sawExpectedDiagnostic); - EXPECT_TRUE(OperationEquivalence::isEquivalentTo( - module->getOperation(), original->getOperation(), - OperationEquivalence::Flags::None)); -} - TEST(FuseSingleQubitUnitaryRunsTest, IgnoresDynamicPowerExponent) { TestFixture fx; fx.setUp(); @@ -1542,33 +1497,6 @@ TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossBoundariesAllBases) { } } -TEST(FuseSingleQubitUnitaryRunsTest, StopsAtFunctionCallBoundary) { - TestFixture fx; - fx.setUp(); - constexpr StringLiteral source = R"mlir( -module { - func.func private @opaque(!qco.qubit) -> !qco.qubit - func.func @main() attributes {mqt.entry_point} { - %q0 = qco.static 0 : !qco.qubit - %q1 = qco.h %q0 : !qco.qubit -> !qco.qubit - %q2 = func.call @opaque(%q1) : (!qco.qubit) -> !qco.qubit - %q3 = qco.x %q2 : !qco.qubit -> !qco.qubit - qco.sink %q3 : !qco.qubit - return - } -} -)mlir"; - auto module = parseSourceString(source, fx.ctx()); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); - - EXPECT_TRUE(succeeded(runFuse(*module, "u"))); - EXPECT_TRUE(succeeded(verify(*module))); - auto main = module->lookupSymbol("main"); - ASSERT_TRUE(main); - EXPECT_EQ(countOps(main), 1U); -} - TEST(FuseSingleQubitUnitaryRunsTest, EliminatesIdentityInvMultiOpBody) { TestFixture fx; fx.setUp(); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index 2df9ed3a0e..eeb133cf59 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -71,14 +71,6 @@ static SmallVector getQubitValues(ValueRange values) { values, [](Value value) { return isa(value.getType()); })); } -TEST(MappingPassContract, DeclaresSCFDependency) { - auto pass = createMappingPass(); - DialectRegistry registry; - pass->getDependentDialects(registry); - EXPECT_TRUE( - registry.getDialectAllocator(scf::SCFDialect::getDialectNamespace())); -} - /// Return true, if the operations within a region fulfill the given coupling /// constraints. static bool isExecutable(Region& body, @@ -540,38 +532,6 @@ module { OperationEquivalence::Flags::None)); } -TEST_F(MappingPassFixture, NonlinearQubitFailsWithoutMutation) { - constexpr StringLiteral source = R"mlir( -module { - func.func @main() attributes {mqt.entry_point} { - %q0 = qco.alloc : !qco.qubit - %q1 = qco.h %q0 : !qco.qubit -> !qco.qubit - %q2 = qco.x %q0 : !qco.qubit -> !qco.qubit - qco.sink %q1 : !qco.qubit - qco.sink %q2 : !qco.qubit - return - } -} -)mlir"; - auto module = parseSourceString(source, context.get()); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); - OwningOpRef original(module->clone()); - const auto target = llvm::cantFail(CompilerTarget::create(1)); - - std::string diagnostics; - ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { - diagnostics += diagnostic.str(); - return success(); - }); - EXPECT_TRUE(failed(runPass(*module, target, MappingPassOptions{}))); - EXPECT_TRUE(StringRef(diagnostics).contains("exactly one use")) - << diagnostics; - EXPECT_TRUE(OperationEquivalence::isEquivalentTo( - module->getOperation(), original->getOperation(), - OperationEquivalence::Flags::None)); -} - TEST_F(MappingPassFixture, UnsupportedWhileTensorFlowFailsWithoutMutation) { constexpr StringLiteral source = R"mlir( module { @@ -661,34 +621,6 @@ module { OperationEquivalence::Flags::None)); } -TEST_F(MappingPassFixture, DuplicateEntryPointsFailWithoutMutation) { - constexpr StringLiteral source = R"mlir( -module { - func.func @first() attributes {mqt.entry_point} { return } - func.func @second() attributes {mqt.entry_point} { return } -} -)mlir"; - auto module = parseSourceString(source, context.get()); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); - OwningOpRef original(module->clone()); - const auto target = llvm::cantFail(CompilerTarget::create(1)); - - std::string diagnostics; - ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { - diagnostics += diagnostic.str(); - return success(); - }); - EXPECT_TRUE(failed(runPass(*module, target, MappingPassOptions{}))); - EXPECT_TRUE( - StringRef(diagnostics) - .contains("module must contain at most one program entry point")) - << diagnostics; - EXPECT_TRUE(OperationEquivalence::isEquivalentTo( - module->getOperation(), original->getOperation(), - OperationEquivalence::Flags::None)); -} - TEST_F(MappingPassFixture, UnsupportedQuantumRegionFailsWithoutMutation) { constexpr StringLiteral source = R"mlir( module { @@ -724,57 +656,6 @@ module { OperationEquivalence::Flags::None)); } -TEST_F(MappingPassFixture, DeepStructuredNestingFailsWithoutMutation) { - constexpr size_t depth = 65; - std::string source = R"mlir( -module { - func.func @main() attributes {mqt.entry_point} { - %q = qco.alloc : !qco.qubit - %lb = arith.constant 0 : index - %ub = arith.constant 1 : index - %step = arith.constant 1 : index -)mlir"; - for (size_t i = 0; i < depth; ++i) { - const auto input = i == 0 ? "%q" : "%arg" + std::to_string(i - 1); - source += " %out" + std::to_string(i) + " = scf.for %iv" + - std::to_string(i) + " = %lb to %ub step %step iter_args(%arg" + - std::to_string(i) + " = " + input + " ) -> (!qco.qubit) {\n"; - } - source += " %leaf = qco.h %arg" + std::to_string(depth - 1) + - " : !qco.qubit -> !qco.qubit\n"; - for (size_t i = depth; i-- > 0;) { - const auto yielded = - i + 1 == depth ? "%leaf" : "%out" + std::to_string(i + 1); - source += " scf.yield " + yielded + " : !qco.qubit\n }\n"; - } - source += R"mlir( - qco.sink %out0 : !qco.qubit - return - } -} -)mlir"; - - auto module = parseSourceString(source, context.get()); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); - OwningOpRef original(module->clone()); - const auto target = llvm::cantFail(CompilerTarget::create(1)); - - std::string diagnostics; - ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { - diagnostics += diagnostic.str(); - return success(); - }); - EXPECT_TRUE(failed(runPass(*module, target, MappingPassOptions{}))); - EXPECT_TRUE( - StringRef(diagnostics) - .contains("supports at most 64 nested quantum structured operations")) - << diagnostics; - EXPECT_TRUE(OperationEquivalence::isEquivalentTo( - module->getOperation(), original->getOperation(), - OperationEquivalence::Flags::None)); -} - TEST_F(MappingPassFixture, MapTopologyOnlyWithEmptyOperationSet) { constexpr int64_t size = 3; diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp index 27ca6eab3e..d3eb6fc121 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -11,7 +11,6 @@ #include "dd/DDDefinitions.hpp" #include "dd/Package.hpp" #include "mlir/Compiler/Target.h" -#include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" @@ -245,19 +244,11 @@ TEST(TargetSynthesisPassContract, FactoriesAreIndependentlyConstructible) { fusion->getDependentDialects(fusionDialects); EXPECT_TRUE(fusionDialects.getDialectAllocator( mlir::arith::ArithDialect::getDialectNamespace())); - EXPECT_TRUE(fusionDialects.getDialectAllocator( - mlir::qc::QCDialect::getDialectNamespace())); - EXPECT_TRUE(fusionDialects.getDialectAllocator( - mlir::qco::QCODialect::getDialectNamespace())); mlir::DialectRegistry synthesisDialects; synthesis->getDependentDialects(synthesisDialects); EXPECT_TRUE(synthesisDialects.getDialectAllocator( mlir::arith::ArithDialect::getDialectNamespace())); - EXPECT_TRUE(synthesisDialects.getDialectAllocator( - mlir::qc::QCDialect::getDialectNamespace())); - EXPECT_TRUE(synthesisDialects.getDialectAllocator( - mlir::qco::QCODialect::getDialectNamespace())); } TEST_F(TargetSynthesisTest, TwoQubitGateFusionRequiresStrictImprovement) { @@ -343,32 +334,6 @@ TEST_F(TargetSynthesisTest, TwoQubitGateFusionLeavesIndividualOpsAlone) { EXPECT_EQ(printModule(*module), before); } -TEST_F(TargetSynthesisTest, TwoQubitGateFusionHandlesUnusedOutputs) { - auto module = mlir::parseSourceString(R"mlir( - module { - func.func @main() { - %control = qco.static 0 : !qco.qubit - %target = qco.static 1 : !qco.qubit - %unused_control, %unused_target = qco.ctrl(%control) - targets(%arg = %target) { - %body = qco.x %arg : !qco.qubit -> !qco.qubit - qco.yield %body : !qco.qubit - } : ({!qco.qubit}, {!qco.qubit}) - -> ({!qco.qubit}, {!qco.qubit}) - return - } - } - )mlir", - context.get()); - ASSERT_TRUE(module); - ASSERT_TRUE(mlir::succeeded(mlir::verify(*module))); - const auto before = printModule(*module); - - EXPECT_TRUE( - mlir::failed(runPass(*module, mlir::qco::createFuseTwoQubitGates()))); - EXPECT_EQ(printModule(*module), before); -} - TEST_F(TargetSynthesisTest, TwoQubitGateFusionPreservesModifierSupportCalls) { auto module = mlir::parseSourceString(R"mlir( module { diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_hadamard_lifting.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_hadamard_lifting.cpp index 570fd054a8..56e38eff1c 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_hadamard_lifting.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_hadamard_lifting.cpp @@ -9,7 +9,6 @@ */ #include "Support/IRVerification.h" -#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Transforms/GlobalPhaseNormalization.h" #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" @@ -21,11 +20,8 @@ #include #include #include -#include #include #include -#include -#include #include #include #include @@ -52,8 +48,8 @@ class QCOHadamardLiftingTest : public testing::Test { void SetUp() override { // Register all necessary dialects DialectRegistry registry; - registry.insert(); + registry.insert(); context.appendDialectRegistry(registry); context.loadAllAvailableDialects(); @@ -85,91 +81,6 @@ class QCOHadamardLiftingTest : public testing::Test { } // namespace -TEST(HadamardLiftingPassContract, DeclaresProducedDialects) { - auto pass = createHadamardLifting(); - DialectRegistry registry; - pass->getDependentDialects(registry); - EXPECT_TRUE( - registry.getDialectAllocator(arith::ArithDialect::getDialectNamespace())); - EXPECT_TRUE(registry.getDialectAllocator("qc")); - EXPECT_TRUE( - registry.getDialectAllocator(qco::QCODialect::getDialectNamespace())); -} - -TEST_F(QCOHadamardLiftingTest, RejectsInvalidProgramMetadataWithoutMutation) { - auto input = parseSourceString(R"mlir( - module { - func.func @first() attributes {mqt.entry_point} { - %q = qco.static 0 : !qco.qubit - %y = qco.y %q : !qco.qubit -> !qco.qubit - %h = qco.h %y : !qco.qubit -> !qco.qubit - qco.sink %h : !qco.qubit - return - } - func.func @second() attributes {mqt.entry_point} { - return - } - } - )mlir", - &context); - ASSERT_TRUE(input); - ASSERT_TRUE(succeeded(verify(*input))); - OwningOpRef original(input->clone()); - - EXPECT_TRUE(failed(runHadamardLiftingPass(*input))); - EXPECT_TRUE(OperationEquivalence::isEquivalentTo( - input->getOperation(), original->getOperation(), - OperationEquivalence::Flags::None)); -} - -TEST_F(QCOHadamardLiftingTest, HandlesUnusedPauliOutput) { - auto input = parseSourceString(R"mlir( - module { - func.func @main() { - %q = qco.static 0 : !qco.qubit - %unused = qco.x %q : !qco.qubit -> !qco.qubit - return - } - } - )mlir", - &context); - ASSERT_TRUE(input); - ASSERT_TRUE(succeeded(verify(*input))); - OwningOpRef original(input->clone()); - EXPECT_TRUE(failed(runHadamardLiftingPass(*input))); - EXPECT_TRUE(OperationEquivalence::isEquivalentTo( - input->getOperation(), original->getOperation(), - OperationEquivalence::Flags::None)); -} - -TEST_F(QCOHadamardLiftingTest, HandlesUnusedCnotControlOutput) { - auto input = parseSourceString(R"mlir( - module { - func.func @main() { - %control = qco.static 0 : !qco.qubit - %target = qco.static 1 : !qco.qubit - %unused, %target_out = qco.ctrl(%control) targets(%arg = %target) { - %body = qco.x %arg : !qco.qubit -> !qco.qubit - qco.yield %body : !qco.qubit - } : ({!qco.qubit}, {!qco.qubit}) - -> ({!qco.qubit}, {!qco.qubit}) - %hadamard = qco.h %target_out : !qco.qubit -> !qco.qubit - %measured, %result = qco.measure %hadamard : !qco.qubit - qco.sink %measured : !qco.qubit - return - } - } - )mlir", - &context); - ASSERT_TRUE(input); - ASSERT_TRUE(succeeded(verify(*input))); - OwningOpRef original(input->clone()); - EXPECT_TRUE(failed(runHadamardLiftingPass(*input))); - EXPECT_TRUE(OperationEquivalence::isEquivalentTo( - input->getOperation(), original->getOperation(), - OperationEquivalence::Flags::None)); -} - // ################################################## // # Raise Hadamard over uncontrolled Pauli gate Tests // ################################################## diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_measurement_lifting.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_measurement_lifting.cpp index 1fade3dda0..3e7af116eb 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_measurement_lifting.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_measurement_lifting.cpp @@ -17,9 +17,7 @@ #include #include #include -#include #include -#include #include #include #include @@ -31,7 +29,6 @@ #include #include -#include #include #include @@ -696,38 +693,6 @@ TEST_F(QCOMeasurementLiftingTest, liftMeasurementOverInvertedPhaseGates) { areModulesEquivalentWithPermutations(program.get(), reference.get())); } -TEST_F(QCOMeasurementLiftingTest, RejectsNonlinearQubitWithoutMutation) { - program = parseSourceString(R"mlir( - module { - func.func @main() { - %qubit = qco.alloc : !qco.qubit - %phase = qco.z %qubit : !qco.qubit -> !qco.qubit - %measured, %bit = qco.measure %phase : !qco.qubit - %other = qco.x %phase : !qco.qubit -> !qco.qubit - qco.sink %measured : !qco.qubit - qco.sink %other : !qco.qubit - return - } - } - )mlir", - &context); - ASSERT_TRUE(program); - ASSERT_TRUE(succeeded(verify(*program))); - OwningOpRef original(program->clone()); - - bool sawExpectedDiagnostic = false; - ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { - sawExpectedDiagnostic |= - StringRef(diagnostic.str()).contains("exactly one use"); - return success(); - }); - EXPECT_TRUE(failed(runMeasurementLiftingPass(*program))); - EXPECT_TRUE(sawExpectedDiagnostic); - EXPECT_TRUE(OperationEquivalence::isEquivalentTo( - program->getOperation(), original->getOperation(), - OperationEquivalence::Flags::None)); -} - TEST_F(QCOMeasurementLiftingTest, PreservesModifierSupportOperationsWhenRefusingLift) { auto module = parseSourceString(R"mlir( @@ -758,45 +723,3 @@ module { module->getOperation(), original->getOperation(), OperationEquivalence::Flags::None)); } - -TEST_F(QCOMeasurementLiftingTest, DeepModifierNestingFailsWithoutMutation) { - OpBuilder builder(&context); - OwningOpRef moduleOp = ModuleOp::create(builder.getUnknownLoc()); - builder.setInsertionPointToStart(moduleOp->getBody()); - const auto loc = moduleOp->getLoc(); - const auto qubitType = QubitType::get(&context); - auto function = func::FuncOp::create( - builder, loc, "test", - builder.getFunctionType({qubitType}, {qubitType, builder.getI1Type()})); - auto* entry = function.addEntryBlock(); - builder.setInsertionPointToStart(entry); - - constexpr std::size_t modifierDepth = 512; - auto outer = InvOp::create(builder, loc, ValueRange{function.getArgument(0)}); - auto* body = &outer.getBodyRegion().emplaceBlock(); - auto currentQubit = body->addArgument(qubitType, loc); - for (std::size_t i = 1; i < modifierDepth; ++i) { - builder.setInsertionPointToEnd(body); - auto inner = InvOp::create(builder, loc, ValueRange{currentQubit}); - YieldOp::create(builder, loc, inner.getQubitsOut()); - body = &inner.getBodyRegion().emplaceBlock(); - currentQubit = body->addArgument(qubitType, loc); - } - builder.setInsertionPointToEnd(body); - auto z = ZOp::create(builder, loc, currentQubit); - YieldOp::create(builder, loc, ValueRange{z.getOutputTarget(0)}); - - builder.setInsertionPointToEnd(entry); - auto measurement = MeasureOp::create(builder, loc, outer.getQubitsOut()[0]); - func::ReturnOp::create( - builder, loc, - ValueRange{measurement.getQubitOut(), measurement.getResult()}); - OwningOpRef original(moduleOp->clone()); - - PassManager manager(&context); - manager.addPass(createMeasurementLifting()); - EXPECT_TRUE(failed(manager.run(*moduleOp))); - EXPECT_TRUE(OperationEquivalence::isEquivalentTo( - moduleOp->getOperation(), original->getOperation(), - OperationEquivalence::Flags::None)); -} diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp index dd6d950ba4..25913105d4 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp @@ -26,12 +26,9 @@ #include #include #include -#include -#include #include #include #include -#include #include #include #include @@ -53,15 +50,6 @@ using namespace mlir::qco; /// A constant for the value of \f$\pi\f$. constexpr double PI = std::numbers::pi; -TEST(MergeSingleQubitRotationGatesPassContract, DeclaresProducedDialects) { - auto pass = createMergeSingleQubitRotationGates(); - DialectRegistry registry; - pass->getDependentDialects(registry); - EXPECT_TRUE(registry.getDialectAllocator("qc")); - EXPECT_TRUE( - registry.getDialectAllocator(qco::QCODialect::getDialectNamespace())); -} - class MergeSingleQubitRotationGatesTest : public ::testing::Test { protected: MLIRContext context; @@ -328,40 +316,6 @@ TEST_F(MergeSingleQubitRotationGatesTest, mergeRXRXGates) { EXPECT_EQ(countOps(), 0); } -TEST_F(MergeSingleQubitRotationGatesTest, - RejectsNonlinearQubitWithoutMutation) { - constexpr StringLiteral source = R"mlir( -module { - func.func @main() { - %theta0 = arith.constant 0.25 : f64 - %theta1 = arith.constant 0.5 : f64 - %q0 = qco.static 0 : !qco.qubit - %q1 = qco.rx(%theta0) %q0 : !qco.qubit -> !qco.qubit - %q2 = qco.ry(%theta1) %q0 : !qco.qubit -> !qco.qubit - qco.sink %q1 : !qco.qubit - qco.sink %q2 : !qco.qubit - return - } -} -)mlir"; - module = parseSourceString(source, &context); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); - OwningOpRef original(module->clone()); - - bool sawExpectedDiagnostic = false; - ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { - sawExpectedDiagnostic |= - StringRef(diagnostic.str()).contains("exactly one use"); - return success(); - }); - EXPECT_TRUE(failed(runMergePass(*module))); - EXPECT_TRUE(sawExpectedDiagnostic); - EXPECT_TRUE(OperationEquivalence::isEquivalentTo( - module->getOperation(), original->getOperation(), - OperationEquivalence::Flags::None)); -} - /** * @brief Test: RX->RY should merge into a single U gate */ @@ -1265,26 +1219,3 @@ TEST_F(MergeSingleQubitRotationGatesTest, EXPECT_TRUE(mlir::mqt::isValidGlobalPhaseAngle(*constant)); }); } - -TEST_F(MergeSingleQubitRotationGatesTest, StopsAtFunctionCallBoundary) { - constexpr StringLiteral source = R"mlir( -module { - func.func private @opaque(!qco.qubit) -> !qco.qubit - func.func @main() attributes {mqt.entry_point} { - %q0 = qco.static 0 : !qco.qubit - %q1 = qco.h %q0 : !qco.qubit -> !qco.qubit - %q2 = func.call @opaque(%q1) : (!qco.qubit) -> !qco.qubit - %q3 = qco.x %q2 : !qco.qubit -> !qco.qubit - qco.sink %q3 : !qco.qubit - return - } -} -)mlir"; - module = parseSourceString(source, &context); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); - - EXPECT_TRUE(succeeded(runMergePass(*module))); - EXPECT_TRUE(succeeded(verify(*module))); - EXPECT_EQ(countOps(), 1); -} diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_pauli_twirling.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_pauli_twirling.cpp index 24ae55362a..b8f395f620 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_pauli_twirling.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_pauli_twirling.cpp @@ -18,19 +18,16 @@ #include #include #include -#include #include #include #include #include #include -#include #include #include #include #include -#include #include #include #include @@ -52,8 +49,7 @@ class PauliTwirlingTest : public testing::Test { void SetUp() override { DialectRegistry registry; - registry.insert(); + registry.insert(); context.appendDialectRegistry(registry); context.loadAllAvailableDialects(); builder.initialize(); @@ -134,54 +130,6 @@ TEST_F(PauliTwirlingTest, SameSeedProducesSameProgram) { EXPECT_EQ(print(*first), print(*second)); } -TEST_F(PauliTwirlingTest, TwirlsGateInsideDeepNonModifierRegions) { - constexpr size_t depth = 256; - std::string source = R"mlir( -module { - func.func @main() { - %q0 = qco.static 0 : !qco.qubit - %q1 = qco.static 1 : !qco.qubit -)mlir"; - for (size_t i = 0; i < depth; ++i) { - source += " scf.execute_region {\n"; - } - source += R"mlir( - %out0, %out1 = qco.ecr %q0, %q1 : !qco.qubit, !qco.qubit - -> !qco.qubit, !qco.qubit - qco.sink %out0 : !qco.qubit - qco.sink %out1 : !qco.qubit -)mlir"; - for (size_t i = 0; i < depth; ++i) { - source += " scf.yield\n }\n"; - } - source += R"mlir( - return - } -} -)mlir"; - - auto module = parseSourceString(source, &context); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); - - auto function = module->lookupSymbol("main"); - ASSERT_TRUE(function); - auto execute = *function.getOps().begin(); - for (size_t i = 1; i < depth; ++i) { - execute = - *execute.getRegion().front().getOps().begin(); - } - Block* innermostBlock = &execute.getRegion().front(); - - ASSERT_TRUE(succeeded(runPass(*module, 42))); - ASSERT_TRUE(succeeded(verify(*module))); - - const auto pauliCount = llvm::count_if(*innermostBlock, [](Operation& op) { - return isa(op); - }); - EXPECT_EQ(pauliCount, 4); -} - TEST_F(PauliTwirlingTest, PreservesExistingPhaseWhenRewriting) { builder.gphase(0.25); auto module = buildGate(GateKind::CX); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_replace_classical_controls.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_replace_classical_controls.cpp index 5b984a0d5a..443208a14a 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_replace_classical_controls.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_replace_classical_controls.cpp @@ -104,48 +104,6 @@ class QCOReplaceClassicalControlsRZZTest } // namespace -TEST_F(QCOReplaceClassicalControlsTest, HandlesUnusedMeasurementOutput) { - auto input = parseSourceString(R"mlir( - module { - func.func @main() { - %q = qco.static 0 : !qco.qubit - %unused, %result = qco.measure %q : !qco.qubit - return - } - } - )mlir", - &context); - ASSERT_TRUE(input); - ASSERT_TRUE(succeeded(verify(*input))); - EXPECT_TRUE(succeeded(runReplaceClassicalControlsPass(*input))); - EXPECT_TRUE(succeeded(verify(*input))); -} - -TEST_F(QCOReplaceClassicalControlsTest, HandlesUnusedControlledPhaseOutput) { - auto input = parseSourceString(R"mlir( - module { - func.func @main() { - %control = qco.static 0 : !qco.qubit - %target = qco.static 1 : !qco.qubit - %measured, %result = qco.measure %target : !qco.qubit - %unused, %target_out = qco.ctrl(%control) - targets(%arg = %measured) { - %body = qco.z %arg : !qco.qubit -> !qco.qubit - qco.yield %body : !qco.qubit - } : ({!qco.qubit}, {!qco.qubit}) - -> ({!qco.qubit}, {!qco.qubit}) - qco.sink %target_out : !qco.qubit - return - } - } - )mlir", - &context); - ASSERT_TRUE(input); - ASSERT_TRUE(succeeded(verify(*input))); - EXPECT_TRUE(succeeded(runReplaceClassicalControlsPass(*input))); - EXPECT_TRUE(succeeded(verify(*input))); -} - TEST_F(QCOReplaceClassicalControlsTest, AllMeasuredFastPathsPreserveClassicalBodyCalls) { programBuilder.initialize(); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_quantum_loop_unroll.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_quantum_loop_unroll.cpp index b6bfb1f6d6..13b658a29c 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_quantum_loop_unroll.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_quantum_loop_unroll.cpp @@ -32,10 +32,8 @@ #include #include -#include #include #include -#include #include using namespace mlir; @@ -313,47 +311,6 @@ module { OperationEquivalence::Flags::None)); } -TEST_F(QuantumLoopUnrollTest, DeepRegionNestingFailureIsAtomic) { - constexpr size_t depth = 64; - std::string source = R"mlir( -module { - func.func @main() { - %q = qco.static 0 : !qco.qubit - %lb = arith.constant 0 : index - %ub = arith.constant 1 : index - %step = arith.constant 1 : index -)mlir"; - for (size_t i = 0; i < depth; ++i) { - source += " scf.execute_region {\n"; - } - source += R"mlir( - %out = scf.for %iv = %lb to %ub step %step - iter_args(%arg = %q) -> (!qco.qubit) { - %next = qco.x %arg : !qco.qubit -> !qco.qubit - scf.yield %next : !qco.qubit - } - qco.sink %out : !qco.qubit -)mlir"; - for (size_t i = 0; i < depth; ++i) { - source += " scf.yield\n }\n"; - } - source += R"mlir( - return - } -} -)mlir"; - - auto module = parseSourceString(source, context.get()); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); - OwningOpRef original(module->clone()); - - EXPECT_TRUE(failed(runPass(module, QuantumLoopUnrollOptions{}))); - EXPECT_TRUE(OperationEquivalence::isEquivalentTo( - module->getOperation(), original->getOperation(), - OperationEquivalence::Flags::None)); -} - TEST_F(QuantumLoopUnrollTest, UnrollFull) { auto m = getGHZ(context.get(), 3); auto entry = *(m->getOps().begin()); diff --git a/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp b/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp index 9e56dcf6e2..34830c8e57 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_dd_functionality.cpp @@ -44,7 +44,6 @@ #include #include #include -#include #include #include #include @@ -1169,29 +1168,6 @@ TEST_F(QCODDFunctionalityTest, EmbedsWideLocalMatrixWithoutRegisterLimit) { expectEqualToQc(mainFunc(*mod), qc); } -TEST_F(QCODDFunctionalityTest, RejectsExcessiveRegionNesting) { - auto mod = buildModule([](QCOProgramBuilder& b) { - auto qubit = b.staticQubit(0); - std::function nest = [&](size_t depth, - Value value) -> Value { - if (depth == 0) { - return b.x(value); - } - return b.qcoIf( - true, value, - [&](Value argument) { return nest(depth - 1, argument); }, - [](Value argument) { return argument; }); - }; - qubit = nest(64, qubit); - b.sink(qubit); - return b.intConstant(0); - }); - ASSERT_TRUE(mod); - auto dd = std::make_unique(1); - - EXPECT_TRUE(failed(buildFunctionality(mainFunc(*mod), *dd))); -} - TEST_F(QCODDFunctionalityTest, RejectsUnsupportedOrUnboundClassicalOperations) { for (const StringRef source : { R"mlir(module { diff --git a/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp b/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp index 6b9150cc66..5f1c2fb429 100644 --- a/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp +++ b/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -38,7 +39,6 @@ #include #include -#include #include #include #include @@ -80,20 +80,6 @@ static ArrayAttr findPassthroughEntry(LLVM::LLVMFuncOp function, return {}; } -static size_t countPassthroughEntries(LLVM::LLVMFuncOp function, - StringRef name) { - const auto passthrough = function->getAttrOfType("passthrough"); - if (!passthrough) { - return 0; - } - return llvm::count_if(passthrough, [&](Attribute attribute) { - const auto pair = dyn_cast(attribute); - const auto key = - pair && pair.size() == 2 ? dyn_cast(pair[0]) : StringAttr{}; - return key && key.getValue() == name; - }); -} - namespace { struct QIRTestCase { @@ -248,40 +234,6 @@ TEST_F(QIRTest, ReusedIrreversibleDeclarationsPreservePassthroughIdempotently) { } } -TEST_F(QIRTest, MetadataPassRequiresExactlyOneEntryPointAtomically) { - for (const size_t numEntryPoints : {0U, 2U}) { - SCOPED_TRACE(testing::Message() << "numEntryPoints=" << numEntryPoints); - OpBuilder builder(context.get()); - const auto location = builder.getUnknownLoc(); - auto module = ModuleOp::create(location); - builder.setInsertionPointToStart(module.getBody()); - const auto functionType = - LLVM::LLVMFunctionType::get(LLVM::LLVMVoidType::get(context.get()), {}); - for (size_t i = 0; i < std::max(numEntryPoints, 1); ++i) { - auto function = LLVM::LLVMFuncOp::create( - builder, location, "function" + std::to_string(i), functionType); - if (i < numEntryPoints) { - function->setAttr("passthrough", - builder.getStrArrayAttr({"entry_point"})); - } - auto* block = function.addEntryBlock(builder); - builder.setInsertionPointToEnd(block); - LLVM::ReturnOp::create(builder, location, ValueRange{}); - builder.setInsertionPointToEnd(module.getBody()); - } - ASSERT_TRUE(succeeded(verify(module))); - - std::string before; - llvm::raw_string_ostream(before) << module; - PassManager manager(context.get()); - manager.addPass(qir::createQIRSetAttributesAndMetadata({false})); - EXPECT_TRUE(failed(manager.run(module))); - std::string after; - llvm::raw_string_ostream(after) << module; - EXPECT_EQ(after, before); - } -} - TEST_F(QIRTest, BaseMetadataRejectsDynamicResourcesAtomically) { OpBuilder builder(context.get()); const auto location = builder.getUnknownLoc(); @@ -371,6 +323,81 @@ TEST_F(QIRTest, AdaptiveMetadataScansStaticResourcesInHelperFunctions) { EXPECT_TRUE(findModuleFlag(module, "ir_functions")); } +TEST_F(QIRTest, AdaptiveMetadataHandlesRecursiveHelperResourceProvenance) { + for (const auto [dynamic, hasOrigin, succeeds] : + std::array{std::tuple{false, true, true}, std::tuple{true, true, true}, + std::tuple{false, false, false}}) { + SCOPED_TRACE(testing::Message() + << "dynamic=" << dynamic << ", hasOrigin=" << hasOrigin); + OpBuilder builder(context.get()); + const auto location = builder.getUnknownLoc(); + auto module = ModuleOp::create(location); + builder.setInsertionPointToStart(module.getBody()); + const auto ptrType = LLVM::LLVMPointerType::get(context.get()); + const auto voidType = LLVM::LLVMVoidType::get(context.get()); + auto x = LLVM::LLVMFuncOp::create( + builder, location, QIR_X, + LLVM::LLVMFunctionType::get(voidType, {ptrType})); + LLVM::LLVMFuncOp allocate; + if (dynamic) { + allocate = LLVM::LLVMFuncOp::create( + builder, location, QIR_QUBIT_ALLOC, + LLVM::LLVMFunctionType::get(ptrType, {ptrType})); + } + + const auto helperType = LLVM::LLVMFunctionType::get(voidType, {ptrType}); + auto helper = + LLVM::LLVMFuncOp::create(builder, location, "helper", helperType); + auto* helperBlock = helper.addEntryBlock(builder); + builder.setInsertionPointToEnd(helperBlock); + LLVM::CallOp::create(builder, location, x, helperBlock->getArgument(0)); + LLVM::CallOp::create(builder, location, helper, + helperBlock->getArgument(0)); + LLVM::ReturnOp::create(builder, location, ValueRange{}); + + builder.setInsertionPointToEnd(module.getBody()); + const auto mainType = LLVM::LLVMFunctionType::get(voidType, {}); + auto main = LLVM::LLVMFuncOp::create(builder, location, "main", mainType); + main->setAttr("passthrough", builder.getStrArrayAttr({"entry_point"})); + auto* mainBlock = main.addEntryBlock(builder); + builder.setInsertionPointToEnd(mainBlock); + if (hasOrigin) { + Value qubit; + if (dynamic) { + auto null = LLVM::ZeroOp::create(builder, location, ptrType); + qubit = + LLVM::CallOp::create(builder, location, allocate, null.getResult()) + .getResult(); + } else { + auto index = LLVM::ConstantOp::create(builder, location, + builder.getI64IntegerAttr(5)); + qubit = LLVM::IntToPtrOp::create(builder, location, ptrType, + index.getResult()) + .getResult(); + } + LLVM::CallOp::create(builder, location, helper, qubit); + } + LLVM::ReturnOp::create(builder, location, ValueRange{}); + ASSERT_TRUE(succeeded(verify(module))); + + PassManager manager(context.get()); + manager.addPass(qir::createQIRSetAttributesAndMetadata({true})); + if (!succeeds) { + EXPECT_TRUE(failed(manager.run(module))); + continue; + } + ASSERT_TRUE(succeeded(manager.run(module))); + const auto required = findPassthroughEntry(main, "required_num_qubits"); + ASSERT_TRUE(required); + EXPECT_EQ(cast(required[1]).getValue(), dynamic ? "0" : "6"); + const auto dynamicManagement = + findModuleFlag(module, "dynamic_qubit_management"); + ASSERT_TRUE(dynamicManagement); + EXPECT_EQ(cast(dynamicManagement.getValue()).getInt(), + dynamic ? 1 : 0); + } +} + TEST_F(QIRTest, MetadataTracesStaticResourcesThroughControlFlowArguments) { OpBuilder builder(context.get()); const auto location = builder.getUnknownLoc(); @@ -421,7 +448,7 @@ TEST_F(QIRTest, MetadataTracesStaticResourcesThroughControlFlowArguments) { ASSERT_TRUE(succeeded(verify(module))); PassManager manager(context.get()); - manager.addPass(qir::createQIRSetAttributesAndMetadata({false})); + manager.addPass(qir::createQIRSetAttributesAndMetadata({true})); ASSERT_TRUE(succeeded(manager.run(module))); const auto requiredQubits = findPassthroughEntry(main, "required_num_qubits"); const auto requiredResults = @@ -470,7 +497,7 @@ TEST_F(QIRTest, MetadataRequiresAnOriginForLoopCarriedStaticPointers) { ASSERT_TRUE(succeeded(verify(module))); PassManager manager(context.get()); - manager.addPass(qir::createQIRSetAttributesAndMetadata({false})); + manager.addPass(qir::createQIRSetAttributesAndMetadata({true})); if (!hasStaticOrigin) { EXPECT_TRUE(failed(manager.run(module))); continue; @@ -726,103 +753,6 @@ TEST_F(QIRTest, ResultArraysDoNotInflateRequiredQubitCapacity) { EXPECT_EQ(cast(requiredResults[1]).getValue(), "8"); } -TEST_F(QIRTest, IgnoresMalformedZeroArgumentRecordDeclarationWithoutCrashing) { - OpBuilder builder(context.get()); - const auto location = builder.getUnknownLoc(); - auto module = ModuleOp::create(location); - builder.setInsertionPointToStart(module.getBody()); - const auto voidType = LLVM::LLVMVoidType::get(context.get()); - const auto functionType = LLVM::LLVMFunctionType::get(voidType, {}); - auto record = LLVM::LLVMFuncOp::create(builder, location, QIR_RECORD_OUTPUT, - functionType); - auto main = LLVM::LLVMFuncOp::create(builder, location, "main", functionType); - main->setAttr("passthrough", builder.getStrArrayAttr({"entry_point"})); - auto* block = main.addEntryBlock(builder); - builder.setInsertionPointToEnd(block); - LLVM::CallOp::create(builder, location, record, ValueRange{}); - LLVM::ReturnOp::create(builder, location, ValueRange{}); - ASSERT_TRUE(succeeded(verify(module))); - - PassManager manager(context.get()); - manager.addPass(qir::createQIRSetAttributesAndMetadata({false})); - ASSERT_TRUE(succeeded(manager.run(module))); - const auto required = findPassthroughEntry(main, "required_num_results"); - ASSERT_TRUE(required); - EXPECT_EQ(cast(required[1]).getValue(), "0"); -} - -TEST_F(QIRTest, PreservesUnrelatedModuleFlags) { - OpBuilder builder(context.get()); - const auto location = builder.getUnknownLoc(); - auto module = ModuleOp::create(location); - builder.setInsertionPointToStart(module.getBody()); - const auto unrelated = - LLVM::ModuleFlagAttr::get(context.get(), LLVM::ModFlagBehavior::Warning, - builder.getStringAttr("Debug Info Version"), - builder.getI32IntegerAttr(3)); - const auto staleQIR = LLVM::ModuleFlagAttr::get( - context.get(), LLVM::ModFlagBehavior::Error, - builder.getStringAttr("qir_major_version"), builder.getI32IntegerAttr(1)); - LLVM::ModuleFlagsOp::create(builder, location, - builder.getArrayAttr({unrelated, staleQIR})); - const auto functionType = - LLVM::LLVMFunctionType::get(LLVM::LLVMVoidType::get(context.get()), {}); - auto main = LLVM::LLVMFuncOp::create(builder, location, "main", functionType); - main->setAttr("passthrough", builder.getStrArrayAttr({"entry_point"})); - auto* block = main.addEntryBlock(builder); - builder.setInsertionPointToEnd(block); - LLVM::ReturnOp::create(builder, location, ValueRange{}); - ASSERT_TRUE(succeeded(verify(module))); - - PassManager manager(context.get()); - manager.addPass(qir::createQIRSetAttributesAndMetadata({false})); - ASSERT_TRUE(succeeded(manager.run(module))); - const auto preserved = findModuleFlag(module, "Debug Info Version"); - ASSERT_TRUE(preserved); - EXPECT_EQ(preserved.getBehavior(), LLVM::ModFlagBehavior::Warning); - EXPECT_EQ(cast(preserved.getValue()).getInt(), 3); - const auto qirMajor = findModuleFlag(module, "qir_major_version"); - ASSERT_TRUE(qirMajor); - EXPECT_EQ(cast(qirMajor.getValue()).getInt(), 2); -} - -TEST_F(QIRTest, PreservesUnrelatedFunctionPassthroughAttributesIdempotently) { - OpBuilder builder(context.get()); - const auto location = builder.getUnknownLoc(); - auto module = ModuleOp::create(location); - builder.setInsertionPointToStart(module.getBody()); - const auto functionType = - LLVM::LLVMFunctionType::get(LLVM::LLVMVoidType::get(context.get()), {}); - auto main = LLVM::LLVMFuncOp::create(builder, location, "main", functionType); - const auto nounwind = builder.getStringAttr("nounwind"); - const auto target = builder.getStrArrayAttr({"target-cpu", "generic"}); - main->setAttr( - "passthrough", - builder.getArrayAttr( - {builder.getStringAttr("entry_point"), nounwind, target, - builder.getStrArrayAttr({"qir_profiles", "stale_profile"})})); - auto* block = main.addEntryBlock(builder); - builder.setInsertionPointToEnd(block); - LLVM::ReturnOp::create(builder, location, ValueRange{}); - ASSERT_TRUE(succeeded(verify(module))); - - const auto runPass = [&] { - PassManager manager(context.get()); - manager.addPass(qir::createQIRSetAttributesAndMetadata({false})); - return manager.run(module); - }; - ASSERT_TRUE(succeeded(runPass())); - auto passthrough = main->getAttrOfType("passthrough"); - ASSERT_TRUE(passthrough); - EXPECT_TRUE(llvm::is_contained(passthrough, nounwind)); - EXPECT_TRUE(llvm::is_contained(passthrough, target)); - EXPECT_EQ(countPassthroughEntries(main, "qir_profiles"), 1U); - const auto afterFirstRun = passthrough; - - ASSERT_TRUE(succeeded(runPass())); - EXPECT_EQ(main->getAttrOfType("passthrough"), afterFirstRun); -} - TEST_F(QIRTest, ClassifiesUnconditionalBackedgeAsIteration) { OpBuilder builder(context.get()); auto module = ModuleOp::create(builder.getUnknownLoc()); @@ -898,18 +828,15 @@ TEST_F(QIRTest, ClassifiesMeasurementExitAfterOtherConditional) { } TEST_F(QIRTest, CleanupOnlyRemovesProvenSideEffectFreeArrayPairs) { - const auto buildModule = [&](bool nonNullError, bool mismatchedSize, - bool nonVoidAllocate) { + const auto buildModule = [&](bool nonNullError, bool mismatchedSize) { OpBuilder builder(context.get()); const auto location = builder.getUnknownLoc(); auto module = ModuleOp::create(location); builder.setInsertionPointToStart(module.getBody()); const auto ptrType = LLVM::LLVMPointerType::get(context.get()); const auto voidType = LLVM::LLVMVoidType::get(context.get()); - const Type allocateResult = - nonVoidAllocate ? Type(builder.getI1Type()) : Type(voidType); const auto allocateType = LLVM::LLVMFunctionType::get( - allocateResult, {builder.getI64Type(), ptrType, ptrType}); + voidType, {builder.getI64Type(), ptrType, ptrType}); const auto releaseType = LLVM::LLVMFunctionType::get(voidType, {builder.getI64Type(), ptrType}); auto allocate = LLVM::LLVMFuncOp::create( @@ -939,7 +866,7 @@ TEST_F(QIRTest, CleanupOnlyRemovesProvenSideEffectFreeArrayPairs) { } else { error = LLVM::ZeroOp::create(builder, location, ptrType).getResult(); } - auto allocateCall = LLVM::CallOp::create( + LLVM::CallOp::create( builder, location, allocate, ValueRange{size.getResult(), array.getResult(), error}); Value releaseSize = size.getResult(); @@ -952,9 +879,7 @@ TEST_F(QIRTest, CleanupOnlyRemovesProvenSideEffectFreeArrayPairs) { ValueRange{releaseSize, array.getResult()}); Value result; - if (nonVoidAllocate) { - result = allocateCall.getResult(); - } else if (nonNullError) { + if (nonNullError) { result = LLVM::LoadOp::create(builder, location, builder.getI1Type(), error) .getResult(); @@ -967,16 +892,12 @@ TEST_F(QIRTest, CleanupOnlyRemovesProvenSideEffectFreeArrayPairs) { return module; }; - for (const auto [nonNullError, mismatchedSize, nonVoidAllocate, - expectedCalls] : - std::array{std::tuple{false, false, false, 0U}, - std::tuple{true, false, false, 2U}, - std::tuple{false, true, false, 2U}, - std::tuple{false, false, true, 2U}}) { + for (const auto [nonNullError, mismatchedSize, expectedCalls] : + std::array{std::tuple{false, false, 0U}, std::tuple{true, false, 2U}, + std::tuple{false, true, 2U}}) { SCOPED_TRACE(testing::Message() << "nonNullError=" << nonNullError - << ", mismatchedSize=" << mismatchedSize - << ", nonVoidAllocate=" << nonVoidAllocate); - auto module = buildModule(nonNullError, mismatchedSize, nonVoidAllocate); + << ", mismatchedSize=" << mismatchedSize); + auto module = buildModule(nonNullError, mismatchedSize); ASSERT_TRUE(succeeded(verify(module))); PassManager manager(context.get()); manager.addPass(qir::createQIRCleanupPass()); @@ -1045,8 +966,9 @@ TEST_F(QIRTest, CleanupDoesNotDuplicateRequiredResourceCounts) { ASSERT_TRUE(succeeded(manager.run(module))); EXPECT_FALSE(findPassthroughEntry(main, "dynamic_qubit_management")); EXPECT_FALSE(findPassthroughEntry(main, "dynamic_result_management")); - EXPECT_EQ(countPassthroughEntries(main, "required_num_qubits"), 1U); - EXPECT_EQ(countPassthroughEntries(main, "required_num_results"), 1U); + const auto passthrough = main->getAttrOfType("passthrough"); + ASSERT_TRUE(passthrough); + EXPECT_EQ(passthrough.size(), 3U); const auto requiredQubits = findPassthroughEntry(main, "required_num_qubits"); const auto requiredResults = findPassthroughEntry(main, "required_num_results"); From e01cea3759ee303e66b8cba41ba3788668ef5976 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 1 Sep 2026 14:15:52 +0200 Subject: [PATCH 6/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Drop=20final=20unsuppo?= =?UTF-8?q?rted=20audit=20findings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Accept valid QC programs whose dynamic register indices cannot be proven distinct, and remove a registration-only assertion without an executable reproducer. Finalize the reconciled audit counts and validation record. Assisted-by: GPT-5.6 via Codex --- .agent/plans/audit-mlir-contracts.md | 77 +++++++---- mlir/lib/Conversion/QCToQCO/QCToQCO.cpp | 128 ++---------------- .../Conversion/QCToQCO/test_qc_to_qco.cpp | 33 ----- .../test_quantum_loop_unroll.cpp | 8 -- 4 files changed, 64 insertions(+), 182 deletions(-) diff --git a/.agent/plans/audit-mlir-contracts.md b/.agent/plans/audit-mlir-contracts.md index 15d19f58d9..4222086f8e 100644 --- a/.agent/plans/audit-mlir-contracts.md +++ b/.agent/plans/audit-mlir-contracts.md @@ -108,7 +108,7 @@ after human review, with a regression that demonstrates the supported contract. Reconfirmed that standalone C++ lint cannot start because clang-tidy 22 is unavailable on this host. - [x] (2026-09-01) Reconciled the audit with the first 16 focused replacement - pull requests: eight merged, four open, and four closed without merge. + pull requests: ten merged, two open, and four closed without merge. - [x] (2026-09-01) Narrowed `#2300` to its demonstrated missing-entry-point defect and removed the speculative program-sized traversal worklists. - [x] (2026-09-01) Withdrew `#2303`, `#2305`, `#2306`, and `#2309` after review. @@ -121,13 +121,30 @@ after human review, with a regression that demonstrates the supported contract. one-use guards, and their invalid-IR regressions are not actionable findings. - [x] (2026-09-01) Merged current `main` at - de8a8a619fb69c1f6ef7d01f61911838324fc9b4, including the accepted CBit - resource-boundary fix from `#2307`, and reclassified every residual file + 6b95f1c434a319fb66e946309e17df771a4e3be2, including the accepted fixes + from `#2300`, `#2307`, and `#2308`, and reclassified every residual file and test hunk against the revised invariant. - [x] (2026-09-01) Pruned merged and focused duplicates, closed findings, malformed-IR defenses, generic pass-entry validation, the unsupported shared depth policy, and unproved traversal replacements. The remaining implementation snapshot contains only valid, uniquely retained findings. +- [x] (2026-09-01) Independently cross-reviewed the retained compiler, + conversion, dialect, pass, and QIR slices. Repaired one nonlinear matrix + fixture, removed malformed QIR ABI defenses, corrected recursive QIR + provenance, and cleared the surviving changed-line lint findings. +- [x] (2026-09-01) Removed the final unsupported findings: a QC register-alias + proof that rejected valid benchmark programs and a registration-only + assertion without an executed failure case. +- [x] (2026-09-01) Reduced the latest-main delta from the historical 118 files + and 14,011 changed lines to 79 files and 11,167 changed lines. The 2,844 + removed lines are merged/focused duplicates, closed findings, invalid-IR + handling, speculative traversal/depth policies, and their tests. +- [x] (2026-09-01) Built the reconciled branch with LLVM/MLIR 23.1.0 and passed + all 3,111 tests in the `mqt-mlir-unittests` label, repository lint, and + diff checks. Standalone C++ lint cannot start because this host lacks + clang-tidy 22. The focused Python metadata session builds and reaches the + expected diagnostics, but this Apple host cannot translate their C++ + exceptions through nanobind. ## Surprises & Discoveries @@ -151,6 +168,10 @@ after human review, with a regression that demonstrates the supported contract. - Observation: loop-unroll verification of a temporary clone must retain its parent module so sibling symbol references resolve, while verifying only the transformed operation because the temporary module is intentionally partial. +- Observation: failure to prove two dynamic QC register indices distinct does + not prove they alias. Rejecting that case broke + `GeneratesEveryBenchmarkMethodAsQCAndJeff`; retain only the established checks + for operands known to be identical. - Observation: expansion-producing work reachable from valid input can require explicit limits, and repository policy treats unbounded recursion as a correctness risk. The proposed `#2300` worklists increased memory use, while @@ -287,12 +308,12 @@ wrongly owned. The historical commits preserve that snapshot. The current branch removes rejected and separately owned code and keeps only valid residual findings for review; it is still not intended to merge in bulk. -As of 2026-09-01, eight focused replacements have merged: `#2291`, `#2293`, -`#2294`, `#2295`, `#2296`, `#2301`, `#2304`, and `#2307`. Four remain open: -`#2290`, `#2300`, `#2302`, and `#2308`. Four closed without merge: `#2303`, -`#2305`, `#2306`, and `#2309`. Review narrowed `#2300` to the demonstrated -missing-entry-point case and rejected `#2309` because its pass-local check and -regressions target invalid QCO IR. +As of 2026-09-01, ten focused replacements have merged: `#2291`, `#2293`, +`#2294`, `#2295`, `#2296`, `#2300`, `#2301`, `#2304`, `#2307`, and `#2308`. Two +remain open: `#2290` and `#2302`. Four closed without merge: `#2303`, `#2305`, +`#2306`, and `#2309`. Review narrowed `#2290` to constant-like folding and +`#2300` to the demonstrated missing-entry-point case. Review rejected `#2309` +because its pass-local check and regressions target invalid QCO IR. The original branch passed its recorded build, test, lint, and diff checks. Those results prove internal consistency only; they do not establish that each @@ -309,17 +330,17 @@ Merged findings: - `#2295`: make QTensor shrinking sparse and atomic. Review removed the redundant one-use guard because QTensor linearity owns that invariant. - `#2296`: stop QCO wire traversal at unknown carriers. +- `#2300`: handle gate counts without an entry point. Review removed manual + program-sized worklists and restored native MLIR traversal. - `#2301`: keep terminal measurements after routing swaps. - `#2304`: bound OpenQASM export resource use. - `#2307`: bound CBit zero-initialization lowering. +- `#2308`: preserve QTensor insert updates in QCO-to-QC. Open findings: - `#2290`: harden MLIR constant folding. -- `#2300`: handle gate counts without an entry point. Review removed manual - program-sized worklists and restored native MLIR traversal. - `#2302`: make QIR metadata attachment idempotent. -- `#2308`: preserve QTensor insert updates in QCO-to-QC. Closed findings: @@ -374,7 +395,7 @@ is `9fcc02eb67586628d8244b425d032630313a86f2`. These commits preserve every removed implementation and reproducer. The current delta keeps code and tests only for the valid findings listed below. All residual production, test, TableGen, CMake, binding, documentation, and tool changes were reread against -current `main` at `de8a8a619fb69c1f6ef7d01f61911838324fc9b4`. +current `main` at `6b95f1c434a319fb66e946309e17df771a4e3be2`. The retained items below are findings, not approval of the historical implementation. Each needs a focused extraction from current `main`, a minimal @@ -416,15 +437,15 @@ valid-input regression, and its own review. `RejectsMixedAllocationModesWithoutMutation`, plus output atomicity. `#2308` exclusively owns `PreservesQTensorInsertSlotUpdates` and `InvalidatesQTensorCacheAcrossLoopSlotSwap`. -- QC-to-QCO retains dynamic register-index aliasing, mixed allocation, static - reference identity and lifetime, conditional captured-register lifetime, - ordered QTensor materialization, fallible live-value lookup, and output - linearity/atomicity. Evidence: +- QC-to-QCO retains mixed allocation, static reference identity and lifetime, + conditional captured-register lifetime, ordered QTensor materialization, + fallible live-value lookup, and output linearity/atomicity. Evidence: `DuplicateStaticReferencesShareOneEvolvingQCOValue`, `RejectsStaticUseAfterDeallocationWithoutMutation`, - `RejectsPossiblyAliasedDynamicIndicesWithoutMutation`, `RejectsMixedAllocationModesWithoutMutation`, and - `RejectsConditionallyDeallocatedCapturedRegisterWithoutMutation`. + `RejectsConditionallyDeallocatedCapturedRegisterWithoutMutation`. Unknown + dynamic register-index relationships remain accepted; only operands known to + be identical are rejected by the established conversion checks. - QIR Adaptive retains entry shape and reserved-symbol checks, entry-only QC/CBit/MemRef restrictions, the aggregate classical-result budget, release placement, one-epilogue return lowering, supported control-flow and allocation @@ -686,20 +707,24 @@ Pre-rebase focused checkpoint: Decomposition and native synthesis: 242/242 QTensor transforms: 3/3 -Latest full closure transcript: +Final reconciled closure transcript: + + cmake --preset release \ + -DMLIR_DIR=/private/tmp/mqt-llvm-23.1.0/lib/cmake/mlir \ + -DLLVM_DIR=/private/tmp/mqt-llvm-23.1.0/lib/cmake/llvm + # configured successfully with LLVM/MLIR 23.1.0 cmake --build build/release --parallel 8 # completed successfully ctest --test-dir build/release -L mqt-mlir-unittests --output-on-failure --parallel 8 - 100% tests passed, 0 tests failed out of 3133 - - uvx nox -s tests-3.13 -- test/python/test_qco_dd.py - 6 passed + 100% tests passed, 0 tests failed out of 3111 uvx nox -s tests-3.13 -- test/python/test_mlir_qiskit_translation.py -k \ - 'parameter_vector_element_outside_current_size_round_trips or parameter_vector_metadata_is_preflighted' - 5 passed + parameter_vector_metadata_is_preflighted + # package built with LLVM/MLIR 23.1.0 and all three cases emitted their + # expected diagnostics; this Apple host then failed to translate the C++ + # exceptions through nanobind uvx nox -s lint nox > Session lint was successful diff --git a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp index 90f9bbc1dc..3eeb625d4f 100644 --- a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp +++ b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp @@ -25,7 +25,6 @@ #include #include #include -#include #include #include #include @@ -83,104 +82,10 @@ struct RegisterAccess { /** @brief Indices already used for one register by a quantum operation. */ struct SeenRegisterIndices { - SmallVector values; + DenseMap constants; + llvm::SmallDenseSet dynamicValues; }; -/** @brief A register index represented as one SSA value plus a constant. */ -struct AffineRegisterIndex { - Value base; - int64_t offset; -}; - -} // namespace - -/** @brief Peels constant additions and subtractions from a register index. */ -[[nodiscard]] static AffineRegisterIndex decomposeRegisterIndex(Value index) { - if (const auto constant = getConstantIntValue(index)) { - return {.base = {}, .offset = *constant}; - } - - int64_t offset = 0; - while (true) { - Value next; - int64_t nextOffset = 0; - if (auto add = index.getDefiningOp()) { - if (const auto lhs = getConstantIntValue(add.getLhs())) { - next = add.getRhs(); - if (llvm::AddOverflow(offset, *lhs, nextOffset) != 0) { - break; - } - } else if (const auto rhs = getConstantIntValue(add.getRhs())) { - next = add.getLhs(); - if (llvm::AddOverflow(offset, *rhs, nextOffset) != 0) { - break; - } - } else { - break; - } - } else if (auto sub = index.getDefiningOp()) { - const auto rhs = getConstantIntValue(sub.getRhs()); - if (!rhs || llvm::SubOverflow(offset, *rhs, nextOffset) != 0) { - break; - } - next = sub.getLhs(); - } else { - break; - } - index = next; - offset = nextOffset; - } - return {.base = index, .offset = offset}; -} - -/** @brief Checks whether a constant is outside a constant-bounded loop IV. */ -[[nodiscard]] static bool -isOutsideForInductionRange(int64_t constant, const AffineRegisterIndex& index) { - auto argument = dyn_cast(index.base); - if (!argument) { - return false; - } - auto forOp = dyn_cast_or_null(argument.getOwner()->getParentOp()); - if (!forOp || forOp.getInductionVar() != argument) { - return false; - } - - const auto lower = getConstantIntValue(forOp.getLowerBound()); - const auto upper = getConstantIntValue(forOp.getUpperBound()); - const auto step = getConstantIntValue(forOp.getStep()); - if (!lower || !upper || !step || *step <= 0) { - return false; - } - - int64_t adjustedLower = 0; - int64_t adjustedUpper = 0; - if (llvm::AddOverflow(*lower, index.offset, adjustedLower) != 0 || - llvm::AddOverflow(*upper, index.offset, adjustedUpper) != 0) { - return false; - } - return adjustedLower >= adjustedUpper || constant < adjustedLower || - constant >= adjustedUpper; -} - -/** @brief Proves two supported register-index expressions are distinct. */ -[[nodiscard]] static bool areProvablyDistinctRegisterIndices(Value lhs, - Value rhs) { - const auto lhsIndex = decomposeRegisterIndex(lhs); - const auto rhsIndex = decomposeRegisterIndex(rhs); - if (lhsIndex.base == rhsIndex.base) { - return lhsIndex.offset != rhsIndex.offset; - } - if (!lhsIndex.base) { - return isOutsideForInductionRange(lhsIndex.offset, rhsIndex); - } - if (!rhsIndex.base) { - return isOutsideForInductionRange(rhsIndex.offset, lhsIndex); - } - return false; -} - -namespace { - /** @brief Qubit allocation mode */ enum class AllocationMode : std::uint8_t { Unset, //!< No allocation mode has been established yet. @@ -798,32 +703,25 @@ collectRegisterAccesses(Operation* root, LoweringState& state) { } auto& seen = registerIndices[access->second.reg]; - for (auto previous : seen.values) { - if (areProvablyDistinctRegisterIndices(previous, - access->second.index)) { - continue; - } - const auto previousConstant = getConstantIntValue(previous); - const auto currentConstant = getConstantIntValue(access->second.index); - if (previousConstant && currentConstant && - *previousConstant == *currentConstant) { + if (const auto constant = getConstantIntValue(access->second.index)) { + const auto [it, inserted] = + seen.constants.try_emplace(*constant, access->second.index); + if (!inserted && + isEqualConstantIntOrValue(it->second, access->second.index)) { operation->emitOpError( "requires distinct qubit operands; register-backed operands " "have the same constant index"); return WalkResult::interrupt(); } - if (previous == access->second.index) { - operation->emitOpError( - "requires distinct qubit operands; register-backed operands " - "use the same dynamic index"); - return WalkResult::interrupt(); - } + continue; + } + + if (!seen.dynamicValues.insert(access->second.index).second) { operation->emitOpError( - "requires distinct qubit operands; register-backed indices are " - "not provably distinct"); + "requires distinct qubit operands; register-backed operands use " + "the same dynamic index"); return WalkResult::interrupt(); } - seen.values.push_back(access->second.index); } return WalkResult::advance(); }); diff --git a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp index 23a97e1414..1a25692b31 100644 --- a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp +++ b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp @@ -387,39 +387,6 @@ module { EXPECT_TRUE(destination.getBody().empty()); } -TEST_F(QCToQCORegressionTest, - RejectsPossiblyAliasedDynamicIndicesWithoutMutation) { - constexpr llvm::StringLiteral source = R"mlir( -module { - func.func @main(%i: index, %j: index) attributes {mqt.entry_point} { - %reg = memref.alloc() : memref<2x!qc.qubit> - %q0 = memref.load %reg[%i] : memref<2x!qc.qubit> - %q1 = memref.load %reg[%j] : memref<2x!qc.qubit> - qc.swap %q0, %q1 : !qc.qubit, !qc.qubit - memref.dealloc %reg : memref<2x!qc.qubit> - return - } -} -)mlir"; - - auto module = parseSourceString(source, &context); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); - OwningOpRef original(module->clone()); - - bool sawExpectedDiagnostic = false; - ScopedDiagnosticHandler handler(&context, [&](Diagnostic& diagnostic) { - sawExpectedDiagnostic |= - StringRef(diagnostic.str()).contains("not provably distinct"); - return success(); - }); - EXPECT_TRUE(failed(runQCToQCOConversion(*module))); - EXPECT_TRUE(sawExpectedDiagnostic); - EXPECT_TRUE(OperationEquivalence::isEquivalentTo( - module->getOperation(), original->getOperation(), - OperationEquivalence::Flags::None)); -} - TEST_F(QCToQCORegressionTest, RejectsMixedAllocationModesWithoutMutation) { constexpr llvm::StringLiteral source = R"mlir( module { diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_quantum_loop_unroll.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_quantum_loop_unroll.cpp index 13b658a29c..575e6944ed 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_quantum_loop_unroll.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_quantum_loop_unroll.cpp @@ -217,14 +217,6 @@ module { OperationEquivalence::Flags::None)); } -TEST_F(QuantumLoopUnrollTest, DeclaresMaterializedArithDependency) { - auto pass = createQuantumLoopUnroll({}); - DialectRegistry registry; - pass->getDependentDialects(registry); - EXPECT_TRUE( - registry.getDialectAllocator(arith::ArithDialect::getDialectNamespace())); -} - TEST_F(QuantumLoopUnrollTest, NoOp) { auto m = getGHZ(context.get(), 2); auto mClone = m->clone(); From c029124ff9c5913adbd71a94f72e2fbfebc84fdf Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 1 Sep 2026 14:50:02 +0200 Subject: [PATCH 7/8] =?UTF-8?q?=F0=9F=90=9B=20Validate=20mapping=20option?= =?UTF-8?q?=20values=20portably?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract scalar floats from MLIR pass options before calling std::isfinite so the retained option validation compiles with MSVC. Assisted-by: GPT-5.6 via Codex --- .agent/plans/audit-mlir-contracts.md | 2 +- mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.agent/plans/audit-mlir-contracts.md b/.agent/plans/audit-mlir-contracts.md index 4222086f8e..250411e701 100644 --- a/.agent/plans/audit-mlir-contracts.md +++ b/.agent/plans/audit-mlir-contracts.md @@ -136,7 +136,7 @@ after human review, with a regression that demonstrates the supported contract. proof that rejected valid benchmark programs and a registration-only assertion without an executed failure case. - [x] (2026-09-01) Reduced the latest-main delta from the historical 118 files - and 14,011 changed lines to 79 files and 11,167 changed lines. The 2,844 + and 14,011 changed lines to 79 files and 11,169 changed lines. The 2,842 removed lines are merged/focused duplicates, closed findings, invalid-IR handling, speculative traversal/depth policies, and their tests. - [x] (2026-09-01) Built the reconciled branch with LLVM/MLIR 23.1.0 and passed diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index d99cc04097..372ad72796 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -353,12 +353,14 @@ struct MappingPass : impl::MappingPassBase { void runOnOperation() override { constexpr size_t maxSearchOption = 4096; auto mod = getOperation(); - if (!std::isfinite(alpha) || !(alpha > 0)) { + const auto alphaValue = alpha.getValue(); + const auto lambdaValue = lambda.getValue(); + if (!std::isfinite(alphaValue) || !(alphaValue > 0)) { mod.emitError() << "requires finite alpha > 0"; signalPassFailure(); return; } - if (!std::isfinite(lambda)) { + if (!std::isfinite(lambdaValue)) { mod.emitError() << "requires finite lambda"; signalPassFailure(); return; From 7e7aa724e9b667fbe372636594ffff71ed446054 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 1 Sep 2026 16:58:15 +0200 Subject: [PATCH 8/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Split=20focused=20find?= =?UTF-8?q?ings=20from=20the=20audit=20snapshot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the five implementation and regression copies now owned by #2318 through #2322 while preserving the complete audit ledger. Clear the remaining changed-file Clang-Tidy warnings exposed by the reduced snapshot. Assisted-by: GPT-5.6 via Codex --- .agent/plans/audit-mlir-contracts.md | 93 ++++++++++++------- mlir/include/mlir/Dialect/QCO/QCOUtils.h | 24 ++--- .../QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp | 4 - .../QCO/IR/Operations/StandardGates/ROp.cpp | 7 +- .../Optimizations/MeasurementLifting.cpp | 24 ----- .../Optimizations/QuantumLoopUnroll.cpp | 13 +-- .../lib/Dialect/QCO/Utils/DDFunctionality.cpp | 5 +- .../lib/Dialect/QIR/Transforms/QIRCleanup.cpp | 12 +-- .../test_qc_to_qir_adaptive.cpp | 23 ----- mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp | 54 ----------- .../QCO/Transforms/Mapping/test_mapping.cpp | 6 +- .../test_qco_measurement_lifting.cpp | 35 ------- .../test_quantum_loop_unroll.cpp | 38 -------- mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp | 31 ------- 14 files changed, 78 insertions(+), 291 deletions(-) diff --git a/.agent/plans/audit-mlir-contracts.md b/.agent/plans/audit-mlir-contracts.md index 250411e701..b9497b117b 100644 --- a/.agent/plans/audit-mlir-contracts.md +++ b/.agent/plans/audit-mlir-contracts.md @@ -136,15 +136,18 @@ after human review, with a regression that demonstrates the supported contract. proof that rejected valid benchmark programs and a registration-only assertion without an executed failure case. - [x] (2026-09-01) Reduced the latest-main delta from the historical 118 files - and 14,011 changed lines to 79 files and 11,169 changed lines. The 2,842 + and 14,011 changed lines to 77 files and 10,932 changed lines. The 3,079 removed lines are merged/focused duplicates, closed findings, invalid-IR handling, speculative traversal/depth policies, and their tests. - [x] (2026-09-01) Built the reconciled branch with LLVM/MLIR 23.1.0 and passed all 3,111 tests in the `mqt-mlir-unittests` label, repository lint, and - diff checks. Standalone C++ lint cannot start because this host lacks - clang-tidy 22. The focused Python metadata session builds and reaches the + diff checks. The focused Python metadata session builds and reaches the expected diagnostics, but this Apple host cannot translate their C++ exceptions through nanobind. +- [x] (2026-09-01) Recorded `#2290` and `#2302` as merged and extracted five + more valid findings into open `#2318` through `#2322`. Removed the five + duplicate implementations and regressions from this branch while keeping + their complete ownership and evidence in this ledger. ## Surprises & Discoveries @@ -308,12 +311,13 @@ wrongly owned. The historical commits preserve that snapshot. The current branch removes rejected and separately owned code and keeps only valid residual findings for review; it is still not intended to merge in bulk. -As of 2026-09-01, ten focused replacements have merged: `#2291`, `#2293`, -`#2294`, `#2295`, `#2296`, `#2300`, `#2301`, `#2304`, `#2307`, and `#2308`. Two -remain open: `#2290` and `#2302`. Four closed without merge: `#2303`, `#2305`, -`#2306`, and `#2309`. Review narrowed `#2290` to constant-like folding and -`#2300` to the demonstrated missing-entry-point case. Review rejected `#2309` -because its pass-local check and regressions target invalid QCO IR. +As of 2026-09-01, twelve focused replacements have merged: `#2290`, `#2291`, +`#2293`, `#2294`, `#2295`, `#2296`, `#2300`, `#2301`, `#2302`, `#2304`, `#2307`, +and `#2308`. Five remain open: `#2318`, `#2319`, `#2320`, `#2321`, and `#2322`. +Four closed without merge: `#2303`, `#2305`, `#2306`, and `#2309`. Review +narrowed `#2290` to constant-like folding and `#2300` to the demonstrated +missing-entry-point case. Review rejected `#2309` because its pass-local check +and regressions target invalid QCO IR. The original branch passed its recorded build, test, lint, and diff checks. Those results prove internal consistency only; they do not establish that each @@ -324,6 +328,7 @@ rules now control which findings remain actionable. Merged findings: +- `#2290`: harden MLIR constant folding. - `#2291`: preserve static-qubit isolation during cleanup. - `#2293`: make MLIR region moves failure-atomic. - `#2294`: preserve attributes on reused QIR declarations. @@ -333,14 +338,18 @@ Merged findings: - `#2300`: handle gate counts without an entry point. Review removed manual program-sized worklists and restored native MLIR traversal. - `#2301`: keep terminal measurements after routing swaps. +- `#2302`: make QIR metadata attachment idempotent. - `#2304`: bound OpenQASM export resource use. - `#2307`: bound CBit zero-initialization lowering. - `#2308`: preserve QTensor insert updates in QCO-to-QC. Open findings: -- `#2290`: harden MLIR constant folding. -- `#2302`: make QIR metadata attachment idempotent. +- `#2318`: preserve QIR runtime management metadata. +- `#2319`: preserve modifier effects during measurement lifting. +- `#2320`: preserve yield permutations when unrolling loops. +- `#2321`: avoid releasing static QIR qubits. +- `#2322`: preserve dominance when merging QCO parameters. Closed findings: @@ -393,13 +402,15 @@ The original implementation is preserved at `0141a0b4f8bbf63608f74fdd5b8608e2f2c40e95`. The last pre-reconciliation snapshot is `9fcc02eb67586628d8244b425d032630313a86f2`. These commits preserve every removed implementation and reproducer. The current delta keeps code and tests -only for the valid findings listed below. All residual production, test, -TableGen, CMake, binding, documentation, and tool changes were reread against -current `main` at `6b95f1c434a319fb66e946309e17df771a4e3be2`. +only for valid, unextracted findings. The ledger below also preserves ownership +of extracted findings. All residual production, test, TableGen, CMake, binding, +documentation, and tool changes were reread against current `main` at +`edd2659cea830513f90990e8b88cb14911da32bc`. -The retained items below are findings, not approval of the historical -implementation. Each needs a focused extraction from current `main`, a minimal -valid-input regression, and its own review. +The items below are findings, not approval of the historical implementation. +Each unextracted finding needs a focused extraction from current `main`, a +minimal valid-input regression, and its own review. Implementations and tests +assigned to focused pull requests are absent from `#2287`. ### Compiler and external-input boundaries @@ -451,10 +462,12 @@ valid-input regression, and its own review. placement, one-epilogue return lowering, supported control-flow and allocation limits, global-phase lowering, and atomic output verification. The historical tests cover missing entries, `__quantum__` collisions, - static/dynamic/conditional/repeated releases, nested and inconsistent returns, + dynamic/conditional/repeated releases, nested and inconsistent returns, path-dependent classical output, helper-function QC, mixed allocation, controlled global phase, non-hoistable phase, rank-zero loads, and excessive - classical capacity. + classical capacity. `#2321` exclusively owns suppression of static-qubit + releases and `DoesNotReleaseStaticQubits`; that implementation and test are + absent from `#2287`. - QIR Base retains entry and resource-shape validation, supported control-flow and MemRef restrictions, static-ID/register-element accounting, reset and irreversible-order rules, and atomic output verification. The historical tests @@ -488,15 +501,16 @@ valid-input regression, and its own review. `HugeFiniteStaticPhasesProduceVerifiedOutput`, `DynamicMaxPhasesRemainFiniteAfterFusion`, and the QC/QCO verifier tests for non-finite phase angles. -- `#2302` owns exactly-one-entry and idempotent passthrough/module-flag +- Merged `#2302` owns exactly-one-entry and idempotent passthrough/module-flag metadata. Separate retained metadata findings cover static-resource provenance and capacity, malformed or unknown origins, sparse qubit/result capacities, all `inttoptr` users, result arrays that must not inflate qubit capacity, malformed record declarations, and CFG-sensitive feature classification. -- QIR cleanup retains nested runtime-call discovery, deduplicated required - resource counts, and proven side-effect-free array alloc/release matching. - Evidence: `CleanupFindsRuntimeCallsNestedInFunctions`, - `CleanupDoesNotDuplicateRequiredResourceCounts`, and +- `#2318` exclusively owns nested runtime-call discovery and + `CleanupPreservesMetadataForNestedRuntimeCalls`; that implementation and test + are absent from `#2287`. The residual QIR cleanup findings retain deduplicated + required resource counts and proven side-effect-free array alloc/release + matching. Evidence: `CleanupDoesNotDuplicateRequiredResourceCounts` and `CleanupOnlyRemovesProvenSideEffectFreeArrayPairs`. ### Valid transformation and resource findings @@ -507,11 +521,12 @@ valid-input regression, and its own review. iterative-traversal claim. The dedicated `.agent/audits/global-phase-normalization.md` remains the assertion-level audit for that pass. -- Modifier canonicalization and decomposition retain classical support - operations or refuse a rewrite before mutation. Evidence spans the QC/QCO - modifier canonicalizer tests, `PreservesClassicalBodyCalls`, - `LeavesPostUnitaryClassicalBodyCallInPlace`, - `PreservesModifierSupportOperationsWhenRefusingLift`, +- `#2319` exclusively owns effect-aware refusal during measurement lifting and + `PreservesModifierSupportOperationsWhenRefusingLift`; that implementation and + test are absent from `#2287`. Other modifier canonicalization and + decomposition findings retain classical support operations or refuse a rewrite + before mutation. Evidence spans the QC/QCO modifier canonicalizer tests, + `PreservesClassicalBodyCalls`, `LeavesPostUnitaryClassicalBodyCallInPlace`, `AllMeasuredFastPathsPreserveClassicalBodyCalls`, and the two `PartialMeasured...RefusesUnsafeSupportingOpHoist` tests. - Mapping retains option and target validation, target-specific representability @@ -521,13 +536,19 @@ valid-input regression, and its own review. nonlinear-input tests. - Target synthesis retains support-call preservation and atomic refusal. Drop unused-output handling because those fixtures violate QCO linearity. -- Quantum-loop unrolling retains the factor 4096 and projected-operation 100,000 - resource limits, identity-yield correctness, required Arith dependency, +- `#2320` exclusively owns yield-permutation preservation and + `PreservesYieldOnlyPermutation`; that implementation and test are absent from + `#2287`. Quantum-loop unrolling retains the factor 4096 and + projected-operation 100,000 resource limits, required Arith dependency, sibling-symbol-safe verification, and clone/verify/commit atomicity. Evidence - is the `Excessive...`, `NestedExpansion...`, `PreservesYieldOnlyPermutation`, + is the `Excessive...`, `NestedExpansion...`, `DynamicTripCountFailureIsAtomic`, and `UnrollsFunctionWithSiblingSymbolReference` tests. Drop the generic 64-region-depth guard. +- QCO parameter merging retains finite-sum guards. `#2322` exclusively owns + SSA-dominance-safe parameter replacement and + `GateMergesPreserveParameterDominance`; that implementation and test are + absent from `#2287`. - DD execution retains its public-boundary aggregate classical-bit cap, shared 10,000-step sampling/execution budget, and explicit call/region nesting limits. Evidence: `RejectsExcessiveClassicalRegisterCapacity`, @@ -557,9 +578,13 @@ valid-input regression, and its own review. `#2306` established no supported-path failure or better finite bound. - Remove registration-only assertions and dependency additions unless an executed reproducer demonstrates an unloaded-dialect failure. -- Remove branch copies of all merged or focused findings. Their implementation +- Remove branch copies of all merged or focused findings. Merged implementation and regression live in `#2290`, `#2291`, `#2293`, `#2294`, `#2295`, `#2296`, - `#2300`, `#2301`, `#2302`, `#2304`, `#2307`, and `#2308`. Remove all code from + `#2300`, `#2301`, `#2302`, `#2304`, `#2307`, and `#2308`. Open `#2318`, + `#2319`, `#2320`, `#2321`, and `#2322` own the nested QIR cleanup discovery, + modifier-effect measurement lifting, yield-permutation loop unrolling, + static-qubit QIR release, and QCO parameter-dominance findings. Those + implementations and regressions are absent from `#2287`, as is all code from closed `#2303`, `#2305`, `#2306`, and `#2309`. ## Context and Orientation diff --git a/mlir/include/mlir/Dialect/QCO/QCOUtils.h b/mlir/include/mlir/Dialect/QCO/QCOUtils.h index 449b1f6e3f..eb77372bdb 100644 --- a/mlir/include/mlir/Dialect/QCO/QCOUtils.h +++ b/mlir/include/mlir/Dialect/QCO/QCOUtils.h @@ -250,21 +250,17 @@ template LogicalResult mergeOneTargetOneParameter(OpType op, PatternRewriter& rewriter) { // Check if the successor is the same operation auto nextOp = dyn_cast(*op.getOutputQubit(0).user_begin()); - if (!nextOp || op->getBlock() != nextOp->getBlock()) { + if (!nextOp) { return failure(); } if (!constantParameterSumIsFinite(op.getOperand(1), nextOp.getOperand(1))) { return failure(); } - // Compute the new parameter where both operands dominate, then move the - // merged gate behind it. - rewriter.setInsertionPoint(nextOp); + // Compute and set the new parameter auto newParameter = arith::AddFOp::create( rewriter, op.getLoc(), op.getOperand(1), nextOp.getOperand(1)); - rewriter.modifyOpInPlace( - op, [&] { op->setOperand(1, newParameter.getResult()); }); - rewriter.moveOpBefore(op, nextOp); + op->setOperand(1, newParameter.getResult()); // Replace the second operation with the result of the first operation rewriter.replaceOp(nextOp, op.getResult()); @@ -287,11 +283,6 @@ template static LogicalResult mergeTwoTargetOneParameterImpl(OpType op, OpType nextOp, PatternRewriter& rewriter, bool symmetric = false) { - if (op->getBlock() != nextOp->getBlock()) { - return failure(); - } - - auto output0 = op.getOutputQubit(0); // Both qubits have to point to the same successor auto nextOp2 = *op.getOutputQubit(1).user_begin(); @@ -299,18 +290,15 @@ static LogicalResult mergeTwoTargetOneParameterImpl(OpType op, OpType nextOp, return failure(); } + auto output0 = op.getOutputQubit(0); if (symmetric || output0 == nextOp.getInputQubit(0)) { if (!constantParameterSumIsFinite(op.getOperand(2), nextOp.getOperand(2))) { return failure(); } - // Compute the new parameter where both operands dominate, then move the - // merged gate behind it. - rewriter.setInsertionPoint(nextOp); + // Compute and set the new parameter auto newParameter = arith::AddFOp::create( rewriter, op.getLoc(), op.getOperand(2), nextOp.getOperand(2)); - rewriter.modifyOpInPlace( - op, [&] { op->setOperand(2, newParameter.getResult()); }); - rewriter.moveOpBefore(op, nextOp); + op->setOperand(2, newParameter.getResult()); rewriter.replaceOp(nextOp, nextOp.getInputQubits()); return success(); } diff --git a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp index 47d36aa4df..c0ed158d3b 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp @@ -441,10 +441,6 @@ struct ConvertQCDeallocOp final : StatefulOpConversionPattern { matchAndRewrite(DeallocOp op, OpAdaptor adaptor, ConversionPatternRewriter& rewriter) const override { auto& state = getState(); - if (state.allocationMode == AllocationMode::Static) { - rewriter.eraseOp(op); - return success(); - } auto* ctx = getContext(); auto ptrType = LLVM::LLVMPointerType::get(ctx); diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp index 8cd32ea98d..a9fe3e5119 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp @@ -78,7 +78,7 @@ struct MergeSubsequentR final : OpRewritePattern { LogicalResult matchAndRewrite(ROp op, PatternRewriter& rewriter) const override { auto nextOp = dyn_cast(*op.getOutputQubit(0).user_begin()); - if (!nextOp || op->getBlock() != nextOp->getBlock()) { + if (!nextOp) { return failure(); } @@ -89,12 +89,9 @@ struct MergeSubsequentR final : OpRewritePattern { return failure(); } - rewriter.setInsertionPoint(nextOp); auto newParameter = arith::AddFOp::create(rewriter, op.getLoc(), op.getTheta(), nextOp.getTheta()); - rewriter.modifyOpInPlace( - op, [&] { op->setOperand(1, newParameter.getResult()); }); - rewriter.moveOpBefore(op, nextOp); + op->setOperand(1, newParameter.getResult()); rewriter.replaceOp(nextOp, op.getResult()); return success(); } diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MeasurementLifting.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MeasurementLifting.cpp index af6b6963d8..fc4e571bf7 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MeasurementLifting.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MeasurementLifting.cpp @@ -18,9 +18,7 @@ #include #include #include -#include #include -#include #include #include @@ -53,18 +51,6 @@ static bool isDiagonal(Operation* op) { return isa(op); } -/// Return whether nested modifier bodies contain only unitaries and yields. -static bool hasOnlyUnitaryBodyOperations(Operation* root) { - return !root->walk([&](Operation* operation) { - if (operation == root || isa(operation) || - isa(operation)) { - return WalkResult::advance(); - } - return WalkResult::interrupt(); - }) - .wasInterrupted(); -} - /** * @brief This method swaps a gate with a measurement. * @param gate The gate to swap. @@ -124,12 +110,6 @@ struct LiftMeasurementsAbovePhaseGatesPattern final return mlir::failure(); } - // Erasing or moving the modifier must not discard or reorder its support - // operations relative to the measurement. - if (!hasOnlyUnitaryBodyOperations(predecessor)) { - return mlir::failure(); - } - if (predecessorUnitary.isSingleQubit()) { rewriter.replaceOp(predecessor, predecessorUnitary.getInputQubits()); return mlir::success(); @@ -204,10 +184,6 @@ struct LiftMeasurementsAboveControlsPattern final return mlir::failure(); } - if (!hasOnlyUnitaryBodyOperations(predecessor)) { - return mlir::failure(); - } - if (llvm::find(predecessorCtrl.getControlsOut(), qubitVariable) == predecessorCtrl.getControlsOut().end()) { // The measured qubit is a target, not a control of the gate. diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/QuantumLoopUnroll.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/QuantumLoopUnroll.cpp index 28eb5a3e0e..1520ea0e22 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/QuantumLoopUnroll.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/QuantumLoopUnroll.cpp @@ -72,15 +72,6 @@ static SmallVector collectQuantumLoops(FunctionOpInterface func) { return loops; } -/** @brief Whether a loop body only yields its iteration arguments unchanged. */ -static bool hasIdentityYieldOnlyBody(scf::ForOp loop) { - if (!llvm::hasSingleElement(loop.getBody()->getOperations())) { - return false; - } - auto yield = dyn_cast(loop.getBody()->getTerminator()); - return yield && llvm::equal(yield.getResults(), loop.getRegionIterArgs()); -} - /** @brief Check the projected unrolled IR size before cloning or rewriting. */ static LogicalResult verifyUnrollExpansionBudget(FunctionOpInterface func, int64_t unrollFactor) { @@ -102,7 +93,7 @@ static LogicalResult verifyUnrollExpansionBudget(FunctionOpInterface func, uint64_t nestedMultiplier = multiplier; if (auto loop = dyn_cast(operation); loop && isQuantumLoop(loop)) { - if (hasIdentityYieldOnlyBody(loop)) { + if (llvm::hasSingleElement(loop.getBody()->getOperations())) { nestedMultiplier = 0; } else { uint64_t factor = 0; @@ -166,7 +157,7 @@ static LogicalResult unrollQuantumLoops(FunctionOpInterface func, bool changed = false; for (auto loop : loops) { - if (hasIdentityYieldOnlyBody(loop)) { + if (llvm::hasSingleElement(loop.getBody()->getOperations())) { loop.replaceAllUsesWith(loop.getInitArgs()); loop.erase(); changed = true; diff --git a/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp b/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp index e2233e4458..baa0303c40 100644 --- a/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp +++ b/mlir/lib/Dialect/QCO/Utils/DDFunctionality.cpp @@ -382,8 +382,7 @@ static LogicalResult applyUnitaryMatrix(UnitaryOpInterface unitary, return failure(); } ArrayRef wires = *wiresOr; - if (wires.size() >= 63 || - local.rows() != static_cast(size_t{1} << wires.size())) { + if (wires.size() >= 63 || local.rows() != (int64_t{1} << wires.size())) { return unitary.emitError() << "unitary matrix dimension does not match its target count"; } @@ -603,7 +602,7 @@ static LogicalResult allocateRegister(cbit::AllocOp alloc, const auto rawWidth = alloc.getResult().getType().getWidth(); if (rawWidth <= 0 || classical.allocatedRegisterBits > maxClassicalRegisterBits || - static_cast(rawWidth) > + static_cast(rawWidth) > maxClassicalRegisterBits - classical.allocatedRegisterBits) { return alloc.emitError() << "QCO DD simulation supports at most " << maxClassicalRegisterBits diff --git a/mlir/lib/Dialect/QIR/Transforms/QIRCleanup.cpp b/mlir/lib/Dialect/QIR/Transforms/QIRCleanup.cpp index 686387d3aa..01543471db 100644 --- a/mlir/lib/Dialect/QIR/Transforms/QIRCleanup.cpp +++ b/mlir/lib/Dialect/QIR/Transforms/QIRCleanup.cpp @@ -53,21 +53,17 @@ namespace mlir::qir { } [[nodiscard]] static bool moduleHasDynamicQubitRuntimeCalls(ModuleOp module) { - bool found = false; - module.walk([&](LLVM::CallOp callOp) { + return llvm::any_of(module.getOps(), [](LLVM::CallOp callOp) { const auto callee = getCalleeName(callOp); - found |= callee == QIR_QUBIT_ALLOC || callee == QIR_QUBIT_ARRAY_ALLOC; + return callee == QIR_QUBIT_ALLOC || callee == QIR_QUBIT_ARRAY_ALLOC; }); - return found; } [[nodiscard]] static bool moduleHasDynamicResultRuntimeCalls(ModuleOp module) { - bool found = false; - module.walk([&](LLVM::CallOp callOp) { + return llvm::any_of(module.getOps(), [](LLVM::CallOp callOp) { const auto callee = getCalleeName(callOp); - found |= callee == QIR_RESULT_ALLOC || callee == QIR_RESULT_ARRAY_ALLOC; + return callee == QIR_RESULT_ALLOC || callee == QIR_RESULT_ARRAY_ALLOC; }); - return found; } static void dropUnusedExternalDeclarations(ModuleOp module) { diff --git a/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp b/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp index 1f65051a25..cbc6716a75 100644 --- a/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp +++ b/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp @@ -227,29 +227,6 @@ TEST(QCToQIRAdaptiveNativeTest, EXPECT_TRUE(isEquivalentToClone(module, before)); } -TEST(QCToQIRAdaptiveNativeTest, DoesNotReleaseStaticQubits) { - MLIRContext context; - context.loadDialect(); - OpBuilder builder(&context); - auto loc = builder.getUnknownLoc(); - auto module = ModuleOp::create(loc); - builder.setInsertionPointToStart(module.getBody()); - auto main = func::FuncOp::create(builder, loc, "main", - builder.getFunctionType({}, {})); - mlir::mqt::setEntryPoint(main); - auto* entry = main.addEntryBlock(); - builder.setInsertionPointToEnd(entry); - auto qubit = qc::StaticOp::create(builder, loc, 0); - qc::DeallocOp::create(builder, loc, qubit); - func::ReturnOp::create(builder, loc); - ASSERT_TRUE(succeeded(verify(module))); - - ASSERT_TRUE(succeeded(runQCToQIRAdaptiveConversionSimple(module))); - ASSERT_TRUE(succeeded(verify(module))); - EXPECT_FALSE(module.lookupSymbol(qir::QIR_QUBIT_RELEASE)); -} - TEST(QCToQIRAdaptiveNativeTest, RoutesEveryReturnThroughOneEpilogue) { MLIRContext context; context.loadDialect(R"mlir( - module { - func.func @rx(%a: f64, %b: f64) { - %q0 = qco.alloc : !qco.qubit - %q1 = qco.rx(%a) %q0 : !qco.qubit -> !qco.qubit - %later = arith.mulf %b, %b : f64 - %q2 = qco.rx(%later) %q1 : !qco.qubit -> !qco.qubit - qco.sink %q2 : !qco.qubit - return - } - func.func @r(%a: f64, %b: f64) { - %phi = arith.constant 0.25 : f64 - %q0 = qco.alloc : !qco.qubit - %q1 = qco.r(%a, %phi) %q0 : !qco.qubit -> !qco.qubit - %later = arith.mulf %b, %b : f64 - %q2 = qco.r(%later, %phi) %q1 : !qco.qubit -> !qco.qubit - qco.sink %q2 : !qco.qubit - return - } - func.func @rxx(%a: f64, %b: f64) { - %q0 = qco.alloc : !qco.qubit - %q1 = qco.alloc : !qco.qubit - %q2, %q3 = qco.rxx(%a) %q0, %q1 : !qco.qubit, !qco.qubit - -> !qco.qubit, !qco.qubit - %later = arith.mulf %b, %b : f64 - %q4, %q5 = qco.rxx(%later) %q2, %q3 : !qco.qubit, !qco.qubit - -> !qco.qubit, !qco.qubit - qco.sink %q4 : !qco.qubit - qco.sink %q5 : !qco.qubit - return - } - } - )mlir", - context.get()); - ASSERT_TRUE(program); - ASSERT_TRUE(succeeded(verify(*program))); - ASSERT_TRUE(succeeded(runQCOCleanupPipeline(program.get()))); - ASSERT_TRUE(succeeded(verify(*program))); - - size_t rxCount = 0; - size_t rCount = 0; - size_t rxxCount = 0; - program->walk([&](RXOp) { ++rxCount; }); - program->walk([&](ROp) { ++rCount; }); - program->walk([&](RXXOp) { ++rxxCount; }); - EXPECT_EQ(rxCount, 1U); - EXPECT_EQ(rCount, 1U); - EXPECT_EQ(rxxCount, 1U); - size_t addCount = 0; - program->walk([&](arith::AddFOp) { ++addCount; }); - EXPECT_EQ(addCount, 3U); -} - TEST_F(QCOTest, NestedPowAcrossBranchCutDoesNotMerge) { auto program = ::mqt::test::buildMLIRProgram( context.get(), MQT_NAMED_BUILDER(nestedPowBranchCut)); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index eeb133cf59..5247cc10bc 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -678,7 +678,7 @@ TEST_F(MappingPassFixture, MapTopologyOnlyWithEmptyOperationSet) { std::tie(qubits[1], qubits[2]) = builder.rzx(0.5, qubits[1], qubits[2]); std::tie(qubits[0], qubits[2]) = builder.cx(qubits[0], qubits[2]); - for (int64_t i = 0; i < qubits.size(); ++i) { + for (size_t i = 0; i < qubits.size(); ++i) { std::tie(qubits[i], bits[i]) = builder.measure(qubits[i]); builder.sink(qubits[i]); } @@ -825,7 +825,7 @@ TEST_F(MappingPassFixture, PreserveNoncontiguousTargetSiteIds) { std::tie(qubits[0], qubits[1]) = builder.cx(qubits[0], qubits[1]); std::tie(qubits[1], qubits[2]) = builder.cz(qubits[1], qubits[2]); std::tie(qubits[0], qubits[2]) = builder.cx(qubits[0], qubits[2]); - for (int64_t i = 0; i < qubits.size(); ++i) { + for (size_t i = 0; i < qubits.size(); ++i) { std::tie(qubits[i], bits[i]) = builder.measure(qubits[i]); builder.sink(qubits[i]); } @@ -2156,7 +2156,7 @@ TEST_P(MappingPassTest, MapPaddedCXCZGrid) { qubits[i] = builder.allocQubit(); } cxcz(builder, qubits); - for (int64_t i = 0; i < qubits.size(); ++i) { + for (size_t i = 0; i < qubits.size(); ++i) { std::tie(qubits[i], bits[i]) = builder.measure(qubits[i]); builder.sink(qubits[i]); } diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_measurement_lifting.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_measurement_lifting.cpp index 3e7af116eb..bc22a32ab4 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_measurement_lifting.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_measurement_lifting.cpp @@ -11,7 +11,6 @@ #include "Support/IRVerification.h" #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/Transforms/Passes.h" #include @@ -19,11 +18,8 @@ #include #include #include -#include #include #include -#include -#include #include #include #include @@ -692,34 +688,3 @@ TEST_F(QCOMeasurementLiftingTest, liftMeasurementOverInvertedPhaseGates) { EXPECT_TRUE( areModulesEquivalentWithPermutations(program.get(), reference.get())); } - -TEST_F(QCOMeasurementLiftingTest, - PreservesModifierSupportOperationsWhenRefusingLift) { - auto module = parseSourceString(R"mlir( -module { - func.func private @observe() - func.func @main() { - %q = qco.static 0 : !qco.qubit - %phase = qco.inv (%arg = %q) { - %z = qco.z %arg : !qco.qubit -> !qco.qubit - func.call @observe() : () -> () - qco.yield %z : !qco.qubit - } : {!qco.qubit} -> {!qco.qubit} - %measured, %bit = qco.measure %phase : !qco.qubit - qco.sink %measured : !qco.qubit - return - } -} -)mlir", - &context); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); - OwningOpRef original(module->clone()); - - PassManager manager(&context); - manager.addPass(createMeasurementLifting()); - EXPECT_TRUE(succeeded(manager.run(*module))); - EXPECT_TRUE(OperationEquivalence::isEquivalentTo( - module->getOperation(), original->getOperation(), - OperationEquivalence::Flags::None)); -} diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_quantum_loop_unroll.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_quantum_loop_unroll.cpp index 575e6944ed..52f5c44c06 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_quantum_loop_unroll.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_quantum_loop_unroll.cpp @@ -228,44 +228,6 @@ TEST_F(QuantumLoopUnrollTest, NoOp) { mlir::OperationEquivalence::Flags::None)); } -TEST_F(QuantumLoopUnrollTest, PreservesYieldOnlyPermutation) { - constexpr llvm::StringLiteral source = R"mlir( -module { - func.func @main() attributes {mqt.entry_point} { - %q0 = qco.static 0 : !qco.qubit - %q1 = qco.static 1 : !qco.qubit - %lb = arith.constant 0 : index - %ub = arith.constant 1 : index - %step = arith.constant 1 : index - %out0, %out1 = scf.for %iv = %lb to %ub step %step - iter_args(%left = %q0, %right = %q1) - -> (!qco.qubit, !qco.qubit) { - scf.yield %right, %left : !qco.qubit, !qco.qubit - } - qco.sink %out0 : !qco.qubit - qco.sink %out1 : !qco.qubit - return - } -} -)mlir"; - - auto module = parseSourceString(source, context.get()); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(verify(*module))); - ASSERT_TRUE(succeeded(runPass(module, QuantumLoopUnrollOptions{}))); - ASSERT_TRUE(succeeded(verify(*module))); - - SmallVector sinks; - module->walk([&](SinkOp sink) { sinks.push_back(sink); }); - ASSERT_EQ(sinks.size(), 2); - auto first = sinks[0].getQubit().getDefiningOp(); - auto second = sinks[1].getQubit().getDefiningOp(); - ASSERT_TRUE(first); - ASSERT_TRUE(second); - EXPECT_EQ(first.getIndex(), 1); - EXPECT_EQ(second.getIndex(), 0); -} - TEST_F(QuantumLoopUnrollTest, DynamicTripCountFailureIsAtomic) { constexpr llvm::StringLiteral source = R"mlir( module { diff --git a/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp b/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp index 621e4d1859..26a6181e85 100644 --- a/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp +++ b/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp @@ -1001,37 +1001,6 @@ TEST_F(QIRTest, CleanupOnlyRemovesProvenSideEffectFreeArrayPairs) { } } -TEST_F(QIRTest, CleanupFindsRuntimeCallsNestedInFunctions) { - OpBuilder builder(context.get()); - auto module = ModuleOp::create(builder.getUnknownLoc()); - builder.setInsertionPointToStart(module.getBody()); - auto ptrType = LLVM::LLVMPointerType::get(context.get()); - auto allocateType = LLVM::LLVMFunctionType::get(ptrType, {ptrType}); - auto allocate = LLVM::LLVMFuncOp::create(builder, builder.getUnknownLoc(), - QIR_QUBIT_ALLOC, allocateType); - auto mainType = - LLVM::LLVMFunctionType::get(LLVM::LLVMVoidType::get(context.get()), {}); - auto main = LLVM::LLVMFuncOp::create(builder, builder.getUnknownLoc(), "main", - mainType); - main->setAttr( - "passthrough", - builder.getArrayAttr( - {builder.getStringAttr("entry_point"), - builder.getStrArrayAttr({"dynamic_qubit_management", "true"})})); - auto* block = main.addEntryBlock(builder); - builder.setInsertionPointToEnd(block); - auto null = LLVM::ZeroOp::create(builder, builder.getUnknownLoc(), ptrType); - LLVM::CallOp::create(builder, builder.getUnknownLoc(), allocate, - null.getResult()); - LLVM::ReturnOp::create(builder, builder.getUnknownLoc(), ValueRange{}); - ASSERT_TRUE(succeeded(verify(module))); - - PassManager manager(context.get()); - manager.addPass(qir::createQIRCleanupPass()); - ASSERT_TRUE(succeeded(manager.run(module))); - EXPECT_TRUE(findPassthroughEntry(main, "dynamic_qubit_management")); -} - TEST_F(QIRTest, CleanupDoesNotDuplicateRequiredResourceCounts) { OpBuilder builder(context.get()); const auto location = builder.getUnknownLoc();