diff --git a/.agent/plans/qiskit-circuit-translation.md b/.agent/plans/qiskit-circuit-translation.md index fd1d94fdf8..9822824301 100644 --- a/.agent/plans/qiskit-circuit-translation.md +++ b/.agent/plans/qiskit-circuit-translation.md @@ -12,13 +12,13 @@ translation does not create a `QuantumComputation`. The existing `mqt.core.load`, `qiskit_to_mqt`, and `mqt_to_qiskit` APIs remain independent and retain their wider Qiskit compatibility. -The direct translation supports Qiskit `>=2.5.0,<2.6.0`. Import covers numeric -standard gates and modifiers, global phase, canonical registers, measurement, -reset, barrier, recursive custom definitions, and structured control flow with -classical-bit and register conditions and supported constant expressions. -Standalone classical variables are rejected. Export covers the flat -constructible subset. Validation completes before the destination program is -created. +The direct translation supports Qiskit `>=2.5.0,<2.6.0`. Import covers standard +gates and modifiers with supported numeric or symbolic parameters, global phase, +canonical registers, measurement, reset, barrier, recursive custom definitions, +and structured control flow with classical-bit and register conditions and +supported expressions. Standalone classical variables are rejected. Export +covers the flat constructible subset. Validation completes before the +destination program is created. ## Progress @@ -43,10 +43,10 @@ created. - Qiskit's header function tables and `qk_import()` state are local to one translation unit. All `Qk*` types, functions, and table access therefore stay in `Qiskit2_5.cpp`. -- A custom instruction's definition can still contain its original parameter - object after the call site receives a numeric value. The version-specific - reader binds call parameters to definition parameters before the generic - importer reads the definition. +- A custom instruction's definition exposes the symbols and expressions bound at + its call site. The symbolic-parameter translation validates those values + against the current global and lexical identities; it needs no separate + formal-parameter substitution scheme. - Qiskit 2.5 provides native inspection for structured control flow and classical expressions but does not provide the corresponding constructors. Import can represent these structures in SCF and Arith. Export must reject @@ -69,10 +69,12 @@ created. extension. Rationale: nanobind owns Python object lifetimes and stable-ABI configuration, while source properties keep Qiskit's private headers and extension macro local. Date/Author: 2026-08-12 / Codex. -- Decision: Reject free compile-time parameters and arbitrary unitaries. - Rationale: neither has a complete compiler representation and round-trip - contract in this change. Lexically bound loop parameters remain local values. - Date/Author: 2026-08-12 / Codex. +- Decision: Reject free compile-time parameters and arbitrary unitaries in the + original change. Rationale: neither had a complete compiler representation and + round-trip contract at that time. The free-parameter decision is superseded by + `.agent/plans/qiskit-symbolic-parameters.md`; the arbitrary-unitary decision + is unchanged here. Date/Author: 2026-08-12, partially superseded 2026-08-18 / + Codex. - Decision: Expand unknown instructions through their definitions. Rationale: recursive expansion supports composite gates without adding custom-operation semantics to QC. Expansion is bounded by a depth of 64 and 10 million @@ -160,11 +162,11 @@ the final diff must all pass. The importer opens the source circuit through the selected version reader. It then validates the full reachable instruction graph. Validation checks numeric -parameters, gate and modifier arity, canonical registers, classical expression -types, control-flow mappings, custom-definition cycles, definition arity, -definition depth, and the operation budget. Arbitrary unitaries and unsupported -operations fail during this pass. Only then does the importer allocate an MLIR -context and module. +and supported symbolic parameters, gate and modifier arity, canonical registers, +classical expression types, control-flow mappings, custom-definition cycles, +definition arity, definition depth, and the operation budget. Arbitrary +unitaries, unsupported expressions, and other unsupported operations fail during +this pass. Only then does the importer allocate an MLIR context and module. The importer creates leading anonymous allocations for loose resources and one named allocation for each canonical register. Classical bit references map each @@ -175,7 +177,8 @@ classical-expression trees contain constants but no variables. Loop induction parameters remain lexically bound values. The exporter borrows the program module, checks the single entry function, -rejects function inputs and structured or runtime classical execution, and +accepts named `f64` inputs and supported Arith and Math expression graphs, +rejects other input types and structured or runtime classical execution, and collects flat operations and allocation attributes. It validates the recovered register layout before it selects a Qiskit writer or allocates a circuit. @@ -226,19 +229,20 @@ Acceptance requires: - C++ tests for borrowed module access, checked ownership transfer, shared gate descriptors, and existing OpenQASM translation. -- Table-driven Qiskit gate import and export with constructible numeric - modifiers, global phase, canonical registers, measurement, reset, and barrier; - import also accepts other finite numeric modifiers. -- Recursive numeric custom definitions plus missing, cyclic, mismatched, and - overly deep definitions. +- Table-driven Qiskit gate import and export with finite numeric modifiers, + global phase, canonical registers, measurement, reset, and barrier. +- Recursive parameterized custom definitions plus missing, cyclic, mismatched, + and overly deep definitions. - Nested structured control flow, loop-bound parameters, and representative Boolean, unsigned integer, and floating-point expressions. -- Early rejection of free symbols, arbitrary unitaries, aliases, and interleaved - registers and standalone classical variables without source mutation. +- Supported free symbols and parameter expressions, plus early rejection of + unsupported expressions, arbitrary unitaries, aliases, interleaved registers, + and standalone classical variables without source mutation. - Successful import of a circuit with `circ.layout`, with layout metadata absent from the compiler program. -- Flat export rejection for structured programs and runtime inputs, unsupported - version dispatch, lazy Qiskit import, and unchanged existing converter tests. +- Flat export rejection for structured programs and unsupported runtime input + types or expression graphs, unsupported version dispatch, lazy Qiskit import, + and unchanged existing converter tests. - Python 3.10 regular-ABI and Python 3.12 or newer stable-ABI builds where those interpreters are available, generated stubs, documentation, repository lint, and `git diff --check`. @@ -265,7 +269,8 @@ The vendored Qiskit 2.5.0 snapshot includes its Apache-2.0 license, `PROVENANCE.json`, `API_SURFACE.json`, and per-header SHA-256 hashes. It is a private build input and is not installed as an MQT C++ interface. -The focused validation produced these final summaries: +The original #2031 validation produced these final summaries. The symbolic +parameter ExecPlan records the later symbolic validation: [ PASSED ] 234 tests. # compiler program and pipeline tests [ PASSED ] 291 tests. # QC and OpenQASM translation tests diff --git a/.agent/plans/qiskit-symbolic-parameters.md b/.agent/plans/qiskit-symbolic-parameters.md new file mode 100644 index 0000000000..b43566dea8 --- /dev/null +++ b/.agent/plans/qiskit-symbolic-parameters.md @@ -0,0 +1,409 @@ +# Support symbolic Qiskit parameter expressions + +This ExecPlan is a living document. The sections `Progress`, +`Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must +stay current as the implementation changes. + +This ExecPlan must be maintained in accordance with `.agent/PLANS.md` from the +repository root. + +## Purpose / Big Picture + +Users can import Qiskit circuits whose gates and global phase use free +parameters or real-valued parameter expressions. The compiler represents each +free parameter as a named `f64` function input and represents arithmetic with +frontend-neutral Arith and Math dialect operations. Users can export the +program, bind the reconstructed Qiskit parameters, and obtain the same numeric +circuit. Parameter names are unique across free and lexically bound Qiskit +parameters. Import rejects circuits that violate this source contract. + +This work completes issue #2067. It extends the Qiskit circuit translation +introduced by #2031 and builds on the CBit representation from #2158. It must +not weaken the existing preflight checks, mutate input circuits, or expose a +partially constructed output circuit after a failure. Exact +`ParameterVectorElement` provenance is a separate follow-up. + +## Progress + +- [x] (2026-08-18 11:26Z) Stack the direct-symbol implementation on the current + #2136 branch and preserve both changes through the overlapping Qiskit + files. +- [x] (2026-08-18 11:26Z) Confirm that Qiskit 2.5 records parameter expressions + as a postfix `_qpy_replay` sequence and provides C constructors for the + supported arithmetic operations. +- [x] (2026-08-18 14:45Z) Normalize Qiskit numbers, symbols, and supported + expressions into one bounded, frontend-neutral C++ tree in the + version-specific translation. +- [x] (2026-08-18 14:45Z) Materialize normalized expressions as `f64` Arith and + Math SSA values on import, with symbol lookup by name. +- [x] (2026-08-18 14:45Z) Reconstruct normalized expressions from supported + compiler SSA on export and materialize shared Qiskit parameter objects + through the Qiskit C API. +- [x] (2026-08-18 14:45Z) Permit parameterized custom definitions when all + symbols resolve, and preserve lexical bindings through nested control + flow. +- [x] (2026-08-19 13:40Z) Split exact `ParameterVectorElement` provenance into a + follow-up and reject vector elements explicitly in this scalar-symbol + layer. +- [x] (2026-08-18 14:45Z) Add contract tests, update the support table, and + update this plan. +- [x] (2026-08-18 15:22Z) Fold pull request #2150 into the existing Qiskit + changelog entry without changing its wording. +- [x] (2026-08-18 13:51Z) Rebase on the current #2136 head and run the Release, + C++, Python 3.13, Qiskit 2.5.0 stable-ABI, documentation, stub, and lint + validation. +- [x] (2026-08-18 14:59Z) Rebase the symbolic commit onto the `main` merge of + #2136. Retain the merged controlled-unitary helper and the symbolic + parameter reconstruction path in the only conflicting file. +- [x] (2026-08-19 14:15Z) Port the scalar-symbol layer onto the exact current + #2158 head and rerun the focused Python and complete compiler suites. +- [x] (2026-08-19 19:50Z) Rebase the validated scalar-symbol commit onto `main` + after #2158 merged, rebuild the Python bindings, rerun all 157 Qiskit + translation tests, and pass the focused formatting and diff checks. +- [x] (2026-08-19 20:01Z) Reject named `f64` inputs that do not occur in any + exported parameter tree, add the source-unchanged regression, rebuild, and + pass all 158 Qiskit translation tests. +- [x] (2026-08-20 11:12Z) Register the `mqt` metadata dialect, declare its + discardable attributes, and enforce their placement, value, and uniqueness + contracts. All seven focused dialect tests pass. +- [x] (2026-08-20 11:31Z) Replace raw `mqt.*` string constants with generated + dialect helpers and preserve compatible metadata through QC/QCO + conversions and allocation rewrites. +- [x] (2026-08-20 11:36Z) Replace Qiskit UUID-based parameter identity with the + supported unique-name contract and reject all free/bound name collisions + before module creation. +- [x] (2026-08-20 11:44Z) Replace the nullable parameter-expression node with a + closed variant so malformed node states cannot be constructed. +- [x] (2026-08-20 11:55Z) Reject non-finite statically known values through the + shared QC and QCO unitary-interface verifier, including values nested in + parameter-expression DAGs. +- [x] (2026-08-20 12:10Z) Run focused dialect, conversion, compiler, Qiskit, + documentation, stub, and lint validation; inspect every signed commit and + the final diff. +- [x] (2026-08-20 12:50Z) Replace quantum and classical source-name fields with + one `mqt.register_name` contract, preserve it through CBit lowering, and + reject collisions with named program inputs. +- [x] (2026-08-20 13:09Z) Replace the high-level LLVM passthrough convention + with a verified `mqt.entry_point` marker, preserve the marker through + QC/QCO conversion, and lower it to QIR passthrough metadata only at the + LLVM/QIR boundary. +- [x] (2026-08-20 13:19Z) Build the complete Release tree and pass all 4,301 + configured CTests, 206 focused Python MLIR tests, MLIR documentation, stub + generation, and the repository lint suite. The Sphinx build could not + fetch the external QDMI tag file because DNS resolution failed on both + attempts. +- [x] (2026-08-20 14:36Z) Centralize entry-point metadata access in the MQT + dialect, restrict pass dependencies to dialects the passes produce, remove + redundant namespace qualifiers, and add #2150 to the compiler collection + launch entry. +- [x] (2026-08-20 14:43Z) Add the MQT dialect reference to the published + compiler collection documentation. + +## Surprises & Discoveries + +- Observation: Qiskit 2.5 has no public expression-tree reader that works + without an optional SymPy installation. Its own parameter-expression code + records a stable postfix replay sequence in `_qpy_replay`. Evidence: nested + expressions expose `OPReplay` records with `op`, `lhs`, and `rhs`; reverse + subtraction, division, and power use distinct opcodes. +- Observation: Qiskit rejects two free parameters with the same name in one + circuit, but it permits a lexically bound loop parameter and a distinct free + parameter to share a name. Evidence: the existing name-keyed local map + incorrectly captured the free parameter in such a loop body. +- Observation: Qiskit can construct parameter objects that share a UUID but + disagree on their name. Such objects are outside Qiskit's intended contract. + The importer does not inspect UUIDs; it validates the supported unique-name + contract instead. +- Observation: a custom gate's definition already contains the actual symbols or + expressions supplied at its call site. The importer does not need a separate + formal-parameter substitution scheme. It must validate the definition against + the current global and lexical bindings. +- Observation: an expression can convert to a number while still tracking free + parameters. The version-specific reader must inspect `parameters` before it + treats a value as a numeric constant. +- Observation: treating `ParameterVectorElement` as an ordinary standalone + symbol changes positional binding order. This layer therefore rejects vector + elements instead of inferring semantics from names such as `theta[10]`. +- Observation: Merely collecting a named function argument does not preserve it + in Qiskit. The writer only creates parameters reached from emitted gate or + global-phase expression trees, so an unused input would otherwise disappear. +- Observation: `mqt.input_name` and `mqt.qubit_register_name` are raw strings in + `mlir/include/mlir/Dialect/Utils/Utils.h`; no dialect owns or verifies the + namespace. MLIR dialect ODS can declare typed discardable attributes and + generate helpers for them. +- Observation: QC/QCO conversion copies only `mqt.qubit_register_name`, and the + QC and QTensor register-shrinking rewrites drop it when they replace an + allocation. Compatible discardable metadata must be transferred as a group. +- Observation: The project namespace `::mqt` and the existing MLIR utility + namespace `::mlir::mqt` require explicit qualification in translation units + that import both namespaces. The metadata dialect belongs in the existing + `::mlir::mqt` namespace; changing its C++ namespace would split related MQT + MLIR APIs to avoid a local lookup issue. +- Observation: Both dialects expose gate parameters, including power exponents, + through `UnitaryOpInterface`. An interface verifier is the one shared MLIR + hook that covers standard gates and modifiers without adding a verifier to + each operation. +- Observation: MLIR's dialect documentation generator requires `OpBase.td` even + for an operation-free dialect. `DialectBase.td` declares the dialect but does + not make the `Op` base class visible to the documentation backend. +- Observation: `cbit.alloc` modeled its non-semantic `source_name` as an + inherent operation field. The name has the same cross-frontend contract as + quantum register names and belongs in the shared discardable metadata. +- Observation: QC, QCO, and jeff used LLVM dialect `passthrough` metadata to + identify the program entry point before LLVM lowering. The string array had no + high-level owner or verifier and mixed the program model with a target + encoding. + +## Decision Log + +- Decision: Use one immutable, copyable scalar expression tree at the generic + reader/writer boundary. Rationale: Qiskit-specific replay objects remain in + `Qiskit2_5.cpp`, while import and export share one frontend-neutral contract. + Date/Author: 2026-08-18 / Codex. +- Decision: Support finite numbers, symbols, add, subtract, multiply, divide, + power, negate, sine, cosine, tangent, inverse sine, inverse cosine, inverse + tangent, exponential, logarithm, absolute value, and real conjugation. + Rationale: Arith, Math, and Qiskit's 2.5 C API represent this real-valued + subset directly. Operations without matching compiler semantics fail with a + precise diagnostic. Date/Author: 2026-08-18 / Codex. +- Decision: Require unique names across all free and lexically bound Qiskit + parameters, then key import state by name. Continue to key compiler export by + SSA value and use `mqt.input_name` for the public name. Rationale: Qiskit + programs that reuse a parameter name for a distinct parameter are ambiguous + and outside the supported source contract. Date/Author: 2026-08-20 / Codex. +- Decision: Use the source name as parameter identity and do not inspect or + preserve Qiskit's UUID across a round trip. Rationale: the writer creates + exactly one Qiskit symbol for each named compiler input and reuses it + throughout gates and global phase. Date/Author: 2026-08-20 / Codex. +- Decision: Bound normalized expression depth and node count before compiler or + circuit construction. Rationale: the existing definition and control-flow + readers are bounded, and parameter replay must have the same fail-closed + behavior for adversarial input. Date/Author: 2026-08-18 / Codex. +- Decision: Reject `ParameterVectorElement` input in this PR and implement exact + vector provenance as a stacked follow-up. Rationale: scalar symbols complete + issue #2067, while vector identity, allocation bounds, sparse indices, and + vector-level binding form an independently reviewable contract. Date/Author: + 2026-08-19 / Codex. +- Decision: Require every named `f64` input to occur in the normalized parameter + trees that will be emitted. Rationale: Qiskit circuits cannot declare an + otherwise unused parameter, so failing before writer allocation avoids + silently changing the public parameter set. Date/Author: 2026-08-19 / Codex. +- Decision: Define `mqt.input_name` and `mqt.register_name` as typed discardable + attributes in an operation-free `mqt` dialect. Verify them with the dialect's + operation and region-argument hooks. Rationale: MLIR assigns the semantics of + a dialect-prefixed discardable attribute to that dialect; this provides one + frontend-neutral owner and generated type-safe helpers. Date/Author: + 2026-08-20 / Codex. +- Decision: Use one function-wide namespace for named inputs and quantum or + classical registers. Rationale: duplicate public names are ambiguous even when + a source library happens to accept some cross-kind collisions. Rejecting them + in MQT metadata keeps import and export contracts deterministic. Date/Author: + 2026-08-20 / Codex. +- Decision: Mark the single defined module-level program function with the unit + attribute `mqt.entry_point`. Preserve it as discardable metadata through + high-level conversions. Materialize LLVM `passthrough = ["entry_point", ...]` + only when QIR metadata is attached, then remove the MQT marker. Rationale: the + MQT dialect owns the frontend-neutral program contract, while LLVM passthrough + attributes remain a QIR target detail. Date/Author: 2026-08-20 / Codex. +- Decision: Query, set, and remove `mqt.entry_point` through MQT dialect helper + functions. Declare the MQT dialect as a pass dependency only when the pass + creates MQT metadata. Rationale: this keeps the attribute key private to its + owner and follows the MLIR pass contract for dependent dialects. Date/Author: + 2026-08-20 / Codex. +- Decision: Keep `mqt.input_name` independent of the argument type. Rationale: + the name is shared program metadata, while Qiskit and future OpenQASM + exporters decide which input types they can represent. Date/Author: 2026-08-20 + / Codex. +- Decision: Copy compatible discardable attributes when a conversion or rewrite + replaces their owner. Rationale: this preserves current and future shared + metadata without source-format-specific key handling. Date/Author: 2026-08-20 + / Codex. +- Decision: Represent normalized parameters as a private variant of number, + symbol, unary, and binary nodes. Use separate unary and binary operation enums + and factory methods that always allocate the required operands. Rationale: + consumers no longer validate redundant kind and pointer combinations because + malformed tree shapes are not representable. Date/Author: 2026-08-20 / Codex. +- Decision: Make finite statically known parameter values a `UnitaryOpInterface` + invariant in QC and QCO. Traverse pure expression DAGs and memoize folding so + a non-finite literal or folded subexpression cannot be hidden below a dynamic + root. Keep runtime finiteness as a precondition for dynamic values. Rationale: + dialect verification owns valid quantum IR, while import readers still reject + invalid source values before construction and exporters can assume verified + IR. Date/Author: 2026-08-20 / Codex. + +## Outcomes & Retrospective + +The scalar implementation is complete. The shared MQT metadata dialect owns +input names, register names, and the program entry point. QC/QCO conversions +preserve all compatible discardable metadata without duplicate dialect-specific +attributes. The Qiskit boundary uses unique names instead of UUID edge cases and +a closed normalized expression variant. QC and QCO reject non-finite statically +known unitary parameters through their shared interface contract. + +The final Release build and all 4,301 configured tests pass; one QDMI test is +skipped by its environment guard. All 206 focused Python MLIR tests pass after a +fresh extension build. MLIR documentation, stub generation, and the repository +lint suite also pass. The Sphinx build remains unverified because both attempts +failed to resolve the host for its external QDMI tag file. + +## Context and Orientation + +`bindings/mlir/qiskit/QiskitTranslation.h` defines the normalized objects shared +by the generic translation and one Qiskit-version adapter. +`bindings/mlir/qiskit/Qiskit2_5.cpp` is the only file that reads Python +parameter objects, `_qpy_replay`, or calls Qiskit's `qk_param_*` C functions. + +`bindings/mlir/qiskit/QiskitImport.cpp` validates a complete source circuit, +creates a QC program, inserts one named `f64` entry argument per free symbol, +and lowers normalized expressions to SSA values. +`bindings/mlir/qiskit/QiskitExport.cpp` performs the reverse preflight: it +recognizes a supported `f64` SSA expression graph, builds normalized +expressions, and only then asks a version-specific writer to allocate a Qiskit +circuit. + +The importer uses `mqt.input_name` for the stable public name of each compiler +input. `mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td` declares this metadata, +`mqt.register_name`, and `mqt.entry_point`; the operation-free `mqt` dialect +owns their contracts. The compiler representation uses `arith.addf`, +`arith.subf`, `arith.mulf`, `arith.divf`, and `arith.negf`, plus matching +real-valued Math dialect operations. A local `for` induction parameter is a +temporary SSA value keyed by its unique source name. It is not a function input. + +## Plan of Work + +First, replace the number-or-symbol `Parameter` value in `QiskitTranslation.h` +with an immutable expression node. Keep the node copyable because instructions, +modifiers, and global phase own values. In `Qiskit2_5.cpp`, normalize a number +or direct symbol immediately. For a parameter expression, replay `_qpy_replay` +into a bounded stack. Normalize reverse binary opcodes by swapping their +operands. Reject malformed stacks, non-finite constants, unsupported functions, +excessive depth, and excessive node count before returning to generic import. +Read a `for` parameter through the public control-flow operation and cross-check +its name against the native control-flow metadata. + +Next, change `QiskitImport.cpp` to validate every tree leaf by name and to emit +each supported node as an `f64` Arith or Math value. Register the Math dialect +in the import context. Reject duplicate free names and all collisions between +free and lexically bound names before constructing a module. Key both local and +global parameter maps by name. Remove the numeric-only custom-definition check +in the version adapter; the existing recursive definition preflight then +validates its actual symbols and expressions against the same maps. + +Then change `QiskitExport.cpp` to recognize compiler inputs, constants, and the +supported Arith and Math operations recursively. Cache each SSA result so a +shared compiler subexpression remains shared in the normalized tree. Represent +inverse angles through expression negation and combine all global phase +contributions through expression addition. Complete this preflight before the +writer allocates a destination circuit. In `Qiskit2_5.cpp`, recursively +construct `QkParam` values and reuse one cached Qiskit symbol for each compiler +input name. + +Finally, add focused Python regressions for direct and shared symbols, nested +binary and unary expressions, reverse operators, partial binding, global phase, +parameterized custom definitions, lexical name collisions, supported manual MLIR +expression export, explicit vector-element rejection, and fail-closed +unsupported input. Update only the support table and concise surrounding text. +Mark the prior numeric-only decision in +`.agent/plans/qiskit-circuit-translation.md` as superseded by this plan. Keep +changelog prose unchanged and add pull request #2150 to the existing Qiskit +translation entry. + +## Concrete Steps + +Run all commands from the repository root. Build the changed binding after each +production batch: + + cmake --build build/release --target mqt-core-mlir-bindings --parallel 2 + +Run the focused translation tests in a synchronized environment that builds and +installs the current worktree for parent and child processes: + + uvx nox -s tests-3.13 -- -q -o addopts= test/python/test_mlir_qiskit_translation.py + +Build the MLIR reference documentation and the complete Sphinx documentation: + + cmake --build --preset release --target mlir-doc + uvx nox --non-interactive -s docs + +Finish with generated-stub verification, repository lint, and whitespace +validation: + + uvx nox -s stubs + uvx nox -s lint + git diff --check + +## Validation and Acceptance + +Import a Qiskit circuit with two shared free symbols in nested arithmetic, gate +arguments, and global phase. The QC entry function must have one named `f64` +argument per symbol and must contain the matching Arith and Math operations. +Export it, bind the parameters, and compare its numeric operator and global +phase with the source circuit. + +Import partially bound expressions and a parameterized custom gate. Both must +resolve the remaining symbols without source mutation. Reject a `for` loop whose +binder has the same displayed name as a distinct free symbol before module +construction. + +Export hand-written QC with supported `f64` Arith and Math expressions. The +result must contain shared Qiskit parameters and bind to the same numeric +values. Duplicate or unused named inputs, unsupported SSA operations, +unsupported Qiskit functions, non-finite constants, malformed trees, and +excessive expressions must fail during preflight. + +Reject a `ParameterVectorElement` before module construction and leave the +source circuit unchanged. Continue to accept standalone scalar parameters whose +names contain brackets without inferring vector semantics. + +## Idempotence and Recovery + +All build and test commands are repeatable. Build artifacts remain under +`build/` and are not committed. + +If `main` advances before publication, rebase this scalar commit first. Preserve +the CBit resource model and the symbolic-expression fields and paths in +overlapping Qiskit translation files, then restack each dependent Qiskit commit. + +Do not push, open a pull request, edit issue text, or post review replies +without fresh human authorization. Preserve unrelated worktree changes. + +## Artifacts and Notes + +The Qiskit 2.5 replay opcodes required by this implementation are addition, +subtraction, multiplication, division, power, their reverse forms, sine, cosine, +tangent, inverse sine, inverse cosine, inverse tangent, exponential, logarithm, +absolute value, and conjugation. Reverse subtraction, division, and power swap +the replay operands before creating the generic tree. Real conjugation is an +identity operation. Other replay opcodes fail with their operation name in the +diagnostic. + +The Release compiler suite passed all 133 tests before the final rebase. A fresh +nanobind 2.15.0 and Qiskit 2.5.2 build passed all 158 focused scalar-symbol +Qiskit translation tests after #2158 merged. Rebasing onto `cb5cf0103` after +pull request 2173 only relocated the changelog entry. The production source tree +did not change. The focused Clang format, Ruff, Rumdl, and committed-diff checks +also pass. Stub generation, the warnings-as-errors documentation build, +repository lint, and focused clang-tidy checks remain part of publication +validation. + +## Interfaces and Dependencies + +`Parameter` in `QiskitTranslation.h` is a copyable immutable tree. Its private +variant contains a number, a symbol, a unary node, or a binary node. Factory +methods create all nodes and allocate every required child. `Loop::parameter` is +`std::optional` and must contain a symbol when present. +`CircuitReader` returns normalized trees for instruction parameters and global +phase. `CircuitWriter` accepts the same tree and reconstructs Qiskit parameters +with the version-specific C and public Python APIs. + +No SymPy dependency is added. No Qiskit object or expression string is stored in +MLIR. The supported compiler operations remain frontend-neutral Arith and Math +dialect operations on `f64` values. + +Revision note (2026-08-19): Split exact vector provenance into a separate +follow-up and aligned this plan with the scalar-symbol contract on #2158. + +Revision note (2026-08-20): Replaced UUID edge-case support with a unique-name +source contract. Added the shared MQT metadata dialect, closed expression-tree, +metadata-preservation, and finite gate-parameter milestones after architecture +review. diff --git a/CHANGELOG.md b/CHANGELOG.md index 765aed4872..90514b8e4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ releases may include breaking changes. [#1807], [#1808], [#1815], [#1824], [#1869], [#1872], [#1914], [#1925], [#1927], [#1935], [#1936], [#1938], [#1975], [#1976], [#2006], [#2014], [#2015], [#2017], [#2026], [#2028], [#2054], [#2058], [#2125], [#2136], - [#2158]) ([**@burgholzer**], [**@denialhaag**], [**@taminob**], + [#2150], [#2158]) ([**@burgholzer**], [**@denialhaag**], [**@taminob**], [**@DRovara**], [**@li-mingbao**], [**@Ectras**], [**@MatthiasReumann**], [**@simon1hofmann**], [**@J4MMlE**]) - ✨ Add decision diagram-based construction, simulation, and sampling for QCO @@ -37,7 +37,7 @@ releases may include breaking changes. #### Import and export - ✨ Add Qiskit circuit import and target-aware export to the compiler - collection ([#2031], [#2133], [#2140]) ([**@burgholzer**], + collection ([#2031], [#2133], [#2140], [#2150]) ([**@burgholzer**], [**@simon1hofmann**]) - ✨ Add conversions between `jeff` and QCO ([#1479], [#1548], [#1565], [#1637], [#1676], [#1706], [#1776], [#1836], [#1934], [#2000], [#2018], [#2105]) @@ -791,6 +791,7 @@ for previous changelogs._ [#2157]: https://github.com/munich-quantum-toolkit/core/pull/2157 [#2156]: https://github.com/munich-quantum-toolkit/core/pull/2156 [#2154]: https://github.com/munich-quantum-toolkit/core/pull/2154 +[#2150]: https://github.com/munich-quantum-toolkit/core/pull/2150 [#2148]: https://github.com/munich-quantum-toolkit/core/pull/2148 [#2147]: https://github.com/munich-quantum-toolkit/core/pull/2147 [#2141]: https://github.com/munich-quantum-toolkit/core/pull/2141 diff --git a/bindings/mlir/CMakeLists.txt b/bindings/mlir/CMakeLists.txt index b484b91080..0f1bbb6106 100644 --- a/bindings/mlir/CMakeLists.txt +++ b/bindings/mlir/CMakeLists.txt @@ -82,6 +82,7 @@ if(NOT TARGET ${TARGET_NAME}) LINK_LIBS MQTCompilerQDMIAdapter MQTCompilerPipeline + MLIRMQTDialect MLIRQCTranslationSupport) if(MQT_QISKIT_CAPI_CANDIDATE_VERSION) diff --git a/bindings/mlir/qiskit/Qiskit2_5.cpp b/bindings/mlir/qiskit/Qiskit2_5.cpp index 314b301776..c5c32c434a 100644 --- a/bindings/mlir/qiskit/Qiskit2_5.cpp +++ b/bindings/mlir/qiskit/Qiskit2_5.cpp @@ -14,6 +14,7 @@ // Qiskit requires its umbrella header before the extension function table. #include #include +#include // NOLINT(misc-include-cleaner): enables the std::complex caster. #include // NOLINT(misc-include-cleaner): enables the std::string caster. #include #include @@ -33,6 +34,7 @@ #include #include #include +#include #include #include @@ -96,6 +98,17 @@ constexpr size_t MAX_ANNOTATED_OPERATION_DEPTH = 64U; return pythonText(pythonAttribute(object, name, error), error); } +[[nodiscard]] uint64_t pythonUnsignedAttribute(const nb::handle object, + const char* name, + const std::string_view error) { + const auto attribute = pythonAttribute(object, name, error); + uint64_t result = 0; + if (!nb::try_cast(attribute, result)) { + throw std::runtime_error(std::string(error)); + } + return result; +} + [[noreturn]] void throwPythonError(const std::string_view message) { const nb::python_error error; throw std::runtime_error(std::string(message) + ": " + error.what()); @@ -163,47 +176,288 @@ QkExitCode addParameterizedGate(QkCircuit* circuit, const QkGate gate, const auto isNumber = qk_param_equal(parameter, numeric); qk_param_free(numeric); if (isNumber) { - return {.number = number}; + return Parameter::number(number); } } - // qk_str_free requires the mutable allocation returned by Qiskit. - // NOLINTNEXTLINE(misc-const-correctness) - char* const text = qk_param_str(parameter); - if (text == nullptr) { - throwPythonError("Qiskit failed to format an instruction parameter"); - } - Parameter result{.number = std::nullopt, .text = text}; - qk_str_free(text); - return result; + throw std::runtime_error( + "Qiskit's native API does not expose symbolic parameter-expression " + "structure"); } -[[nodiscard]] Parameter normalizePythonParameter(const nb::handle parameter) { +[[nodiscard]] Parameter +normalizePythonParameterLeaf(const nb::handle parameter) { double number = 0.0; if (nb::try_cast(parameter, number)) { - return {.number = number}; + if (!std::isfinite(number)) { + throw std::runtime_error("Qiskit returned a non-finite parameter"); + } + return Parameter::number(number); } - if (nb::hasattr(parameter, "name")) { - return {.number = std::nullopt, - .text = pythonStringAttribute( - parameter, "name", - "Qiskit modifier exponent has an invalid symbol name")}; - } - auto text = - pythonText(parameter, "Qiskit modifier exponent has a non-text value"); - return {.number = std::nullopt, .text = std::move(text)}; -} -[[nodiscard]] uint64_t pythonUnsignedAttribute(const nb::handle object, - const char* name, - const std::string_view error) { - const auto attribute = pythonAttribute(object, name, error); - uint64_t result = 0; - if (!nb::try_cast(attribute, result)) { - throw std::runtime_error(std::string(error)); + std::complex complexNumber; + if (nb::try_cast(parameter, complexNumber)) { + if (!std::isfinite(complexNumber.real()) || + !std::isfinite(complexNumber.imag())) { + throw std::runtime_error("Qiskit returned a non-finite parameter"); + } + if (complexNumber.imag() != 0.0) { + throw std::runtime_error( + "Qiskit parameter expressions with complex values are not " + "supported"); + } + return Parameter::number(complexNumber.real()); + } + + if (!nb::hasattr(parameter, "name")) { + throw std::runtime_error( + "Qiskit parameter expression contains an unsupported operand"); + } + auto name = pythonStringAttribute( + parameter, "name", "Qiskit parameter has an invalid symbol name"); + if (name.empty()) { + throw std::runtime_error("Qiskit parameter has an empty symbol name"); + } + if (name.find('\0') != std::string::npos) { + throw std::runtime_error( + "Qiskit parameter names cannot contain null characters"); + } + auto result = Parameter::symbol(std::move(name)); + const auto vectorElement = + nb::module_::import_("qiskit.circuit").attr("ParameterVectorElement"); + if (nb::isinstance(parameter, vectorElement)) { + throw std::runtime_error( + "Qiskit parameter-vector elements are not supported"); } return result; } +struct ParsedParameter { + Parameter value; + size_t depth = 1U; +}; + +[[noreturn]] void throwParameterExpressionSizeError() { + throw std::runtime_error( + "Qiskit parameter expression exceeds the supported " + + std::to_string(MAX_PARAMETER_EXPRESSION_NODES) + "-node size"); +} + +[[noreturn]] void throwParameterExpressionDepthError() { + throw std::runtime_error( + "Qiskit parameter expression exceeds the supported " + + std::to_string(MAX_PARAMETER_EXPRESSION_DEPTH) + "-level nesting depth"); +} + +void countParameterExpressionNode(size_t& nodeCount) { + if (nodeCount >= MAX_PARAMETER_EXPRESSION_NODES) { + throwParameterExpressionSizeError(); + } + ++nodeCount; +} + +[[nodiscard]] ParsedParameter +takeParameterExpressionOperand(const nb::handle operand, + std::vector& stack, + size_t& nodeCount) { + if (operand.is_none()) { + if (stack.empty()) { + throw std::runtime_error( + "Qiskit parameter expression replay has too few operands"); + } + auto result = std::move(stack.back()); + stack.pop_back(); + return result; + } + countParameterExpressionNode(nodeCount); + return {.value = normalizePythonParameterLeaf(operand)}; +} + +[[nodiscard]] Parameter makeUnaryParameter(const UnaryParameterKind kind, + Parameter operand) { + return Parameter::unary(kind, std::move(operand)); +} + +[[nodiscard]] Parameter makeBinaryParameter(const BinaryParameterKind kind, + Parameter lhs, Parameter rhs) { + return Parameter::binary(kind, std::move(lhs), std::move(rhs)); +} + +[[nodiscard]] std::string parameterOpcode(const nb::handle replayEntry) { + auto opcode = pythonText( + pythonAttribute(replayEntry, "op", + "Qiskit parameter replay entry has no operation"), + "Qiskit parameter replay entry has an invalid operation"); + constexpr std::string_view prefix = "OpCode."; + if (opcode.starts_with(prefix)) { + opcode.erase(0U, prefix.size()); + } + return opcode; +} + +[[nodiscard]] bool isUnaryParameterOpcode(const std::string_view opcode) { + return opcode == "NEG" || opcode == "SIN" || opcode == "COS" || + opcode == "TAN" || opcode == "ASIN" || opcode == "ACOS" || + opcode == "ATAN" || opcode == "EXP" || opcode == "LOG" || + opcode == "ABS" || opcode == "CONJ" || opcode == "CONJUGATE"; +} + +[[nodiscard]] UnaryParameterKind +unaryParameterKind(const std::string_view opcode) { + if (opcode == "NEG") { + return UnaryParameterKind::Negate; + } + if (opcode == "SIN") { + return UnaryParameterKind::Sin; + } + if (opcode == "COS") { + return UnaryParameterKind::Cos; + } + if (opcode == "TAN") { + return UnaryParameterKind::Tan; + } + if (opcode == "ASIN") { + return UnaryParameterKind::ArcSin; + } + if (opcode == "ACOS") { + return UnaryParameterKind::ArcCos; + } + if (opcode == "ATAN") { + return UnaryParameterKind::ArcTan; + } + if (opcode == "EXP") { + return UnaryParameterKind::Exp; + } + if (opcode == "LOG") { + return UnaryParameterKind::Log; + } + if (opcode == "ABS") { + return UnaryParameterKind::Abs; + } + return UnaryParameterKind::Conjugate; +} + +[[nodiscard]] bool isBinaryParameterOpcode(const std::string_view opcode) { + return opcode == "ADD" || opcode == "SUB" || opcode == "MUL" || + opcode == "DIV" || opcode == "POW" || opcode == "RSUB" || + opcode == "RDIV" || opcode == "RPOW"; +} + +[[nodiscard]] BinaryParameterKind +binaryParameterKind(const std::string_view opcode) { + if (opcode == "ADD") { + return BinaryParameterKind::Add; + } + if (opcode == "SUB" || opcode == "RSUB") { + return BinaryParameterKind::Subtract; + } + if (opcode == "MUL") { + return BinaryParameterKind::Multiply; + } + if (opcode == "DIV" || opcode == "RDIV") { + return BinaryParameterKind::Divide; + } + return BinaryParameterKind::Power; +} + +[[nodiscard]] Parameter normalizePythonParameter(const nb::handle parameter) { + if (nb::hasattr(parameter, "name")) { + return normalizePythonParameterLeaf(parameter); + } + + bool hasTrackedSymbols = false; + if (nb::hasattr(parameter, "parameters")) { + const auto parameters = pythonAttribute( + parameter, "parameters", + "Qiskit parameter expression has no tracked-symbol set"); + try { + hasTrackedSymbols = nb::len(parameters) != 0U; + } catch (const nb::python_error& error) { + throwPythonError( + "Qiskit parameter expression tracked-symbol set is not sized", error); + } + } + if (!hasTrackedSymbols) { + return normalizePythonParameterLeaf(parameter); + } + + const auto replay = pythonAttribute( + parameter, "_qpy_replay", + "Qiskit parameter expression does not expose its operation replay"); + size_t replaySize = 0U; + try { + replaySize = nb::len(replay); + } catch (const nb::python_error& error) { + throwPythonError("Qiskit parameter expression replay is not sized", error); + } + if (replaySize == 0U) { + throw std::runtime_error("Qiskit parameter expression replay is empty"); + } + if (replaySize > MAX_PARAMETER_EXPRESSION_NODES) { + throwParameterExpressionSizeError(); + } + + size_t nodeCount = 0U; + std::vector stack; + stack.reserve(replaySize); + try { + for (const nb::handle replayEntry : nb::iter(replay)) { + const auto opcode = parameterOpcode(replayEntry); + if (opcode == "SIGN" || opcode == "GRAD" || opcode == "SUBSTITUTE") { + throw std::runtime_error("Qiskit parameter expression operation '" + + opcode + "' is not supported"); + } + const auto lhs = + pythonAttribute(replayEntry, "lhs", + "Qiskit parameter replay entry has no left operand"); + const auto rhs = + pythonAttribute(replayEntry, "rhs", + "Qiskit parameter replay entry has no right operand"); + if (isUnaryParameterOpcode(opcode)) { + if (!rhs.is_none()) { + throw std::runtime_error( + "Qiskit unary parameter replay entry has a right operand"); + } + auto operand = takeParameterExpressionOperand(lhs, stack, nodeCount); + countParameterExpressionNode(nodeCount); + ++operand.depth; + if (operand.depth > MAX_PARAMETER_EXPRESSION_DEPTH) { + throwParameterExpressionDepthError(); + } + operand.value = makeUnaryParameter(unaryParameterKind(opcode), + std::move(operand.value)); + stack.push_back(std::move(operand)); + continue; + } + if (!isBinaryParameterOpcode(opcode)) { + throw std::runtime_error("Qiskit parameter expression operation '" + + opcode + "' is not supported"); + } + auto right = takeParameterExpressionOperand(rhs, stack, nodeCount); + auto left = takeParameterExpressionOperand(lhs, stack, nodeCount); + if (opcode == "RSUB" || opcode == "RDIV" || opcode == "RPOW") { + std::swap(left, right); + } + countParameterExpressionNode(nodeCount); + const auto depth = std::max(left.depth, right.depth) + 1U; + if (depth > MAX_PARAMETER_EXPRESSION_DEPTH) { + throwParameterExpressionDepthError(); + } + stack.push_back({.value = makeBinaryParameter(binaryParameterKind(opcode), + std::move(left.value), + std::move(right.value)), + .depth = depth}); + } + } catch (const nb::python_error& error) { + throwPythonError("Qiskit parameter expression replay is not iterable", + error); + } + if (stack.size() != 1U) { + throw std::runtime_error( + "Qiskit parameter expression replay leaves multiple results"); + } + return std::move(stack.back().value); +} + void appendControlModifier(const nb::handle object, std::vector& modifiers) { const auto controls = pythonUnsignedAttribute( @@ -505,6 +759,12 @@ normalizeExpression(const QkExprNode* expression, const size_t depth = 0U) { class OwnedParameter final { public: + OwnedParameter() : value_(qk_param_zero()) { + if (value_ == nullptr) { + throwPythonError("Qiskit failed to allocate a parameter expression"); + } + } + explicit OwnedParameter(const double value) { if (!std::isfinite(value)) { throw std::runtime_error( @@ -516,6 +776,17 @@ class OwnedParameter final { } } + explicit OwnedParameter(const std::string_view name) { + if (name.empty()) { + throw std::runtime_error( + "cannot construct a Qiskit parameter with an empty name"); + } + value_ = qk_param_new_symbol(std::string(name).c_str()); + if (value_ == nullptr) { + throwPythonError("Qiskit failed to construct a symbolic parameter"); + } + } + OwnedParameter(const OwnedParameter&) = delete; OwnedParameter& operator=(const OwnedParameter&) = delete; OwnedParameter(OwnedParameter&&) = delete; @@ -523,6 +794,7 @@ class OwnedParameter final { ~OwnedParameter() { qk_param_free(value_); } [[nodiscard]] const QkParam* get() const { return value_; } + [[nodiscard]] QkParam* getMutable() { return value_; } private: QkParam* value_ = nullptr; @@ -700,18 +972,28 @@ class NativeCircuitReader final : public CircuitReader { return result; } - [[nodiscard]] Parameter globalPhase() const override { - // qk_param_free requires the mutable allocation returned by Qiskit. - // NOLINTNEXTLINE(misc-const-correctness) - QkParam* const phase = qk_circuit_global_phase(circuit_); - if (phase == nullptr) { - throwPythonError("Qiskit failed to read the circuit global phase"); + [[nodiscard]] std::vector parameters() const override { + std::vector result; + const auto parameters = + pythonAttribute(pythonCircuit_, "parameters", + "Qiskit circuit does not expose its free parameters"); + try { + result.reserve(nb::len(parameters)); + for (const nb::handle parameter : nb::iter(parameters)) { + result.push_back(normalizePythonParameter(parameter)); + } + } catch (const nb::python_error& error) { + throwPythonError("Qiskit circuit parameters are not iterable", error); } - const auto result = normalizeParameter(phase); - qk_param_free(phase); return result; } + [[nodiscard]] Parameter globalPhase() const override { + return normalizePythonParameter( + pythonAttribute(pythonCircuit_, "global_phase", + "Qiskit circuit does not expose its global phase")); + } + [[nodiscard]] Instruction instruction(const size_t index) const override { const auto kind = normalizeKind(qk_circuit_instruction_kind(circuit_, index)); @@ -753,9 +1035,27 @@ class NativeCircuitReader final : public CircuitReader { std::copy_n(native.clbits, native.num_clbits, result.clbits.begin()); } result.parameters.reserve(native.num_params); - for (const auto* parameter : - std::span(native.params, static_cast(native.num_params))) { - result.parameters.emplace_back(normalizeParameter(parameter)); + if (result.kind == OperationKind::Gate || + result.kind == OperationKind::Unknown) { + const auto parameters = + pythonAttribute(pythonOperation(index), "params", + "Qiskit operation does not expose its parameters"); + try { + for (const nb::handle parameter : nb::iter(parameters)) { + result.parameters.push_back(normalizePythonParameter(parameter)); + } + } catch (const nb::python_error& error) { + throwPythonError("Qiskit operation parameters are not iterable", error); + } + if (result.parameters.size() != native.num_params) { + throw std::runtime_error( + "Qiskit Python and native parameter counts do not match"); + } + } else { + for (const auto* parameter : + std::span(native.params, static_cast(native.num_params))) { + result.parameters.emplace_back(normalizeParameter(parameter)); + } } if (result.kind == OperationKind::Unknown) { result.name = std::move(normalizedUnknown->name); @@ -857,18 +1157,7 @@ class NativeCircuitReader final : public CircuitReader { instruction(index).name + "' has no circuit definition"); } - - const auto definitionParameters = - nb::cast(nb::module_::import_("builtins") - .attr("list")(pythonAttribute( - definition, "parameters", - "Qiskit definition has no parameter list"))); - if (definitionParameters.empty()) { - return std::make_unique(definition); - } - throw std::runtime_error( - "Qiskit custom instruction definitions must be numerically bound " - "before import"); + return std::make_unique(definition); } [[nodiscard]] uintptr_t @@ -1072,12 +1361,39 @@ class NativeControlFlowReader final : public ControlFlowReader { case QkLoopParamKind_Parameter: { auto symbol = qk_control_flow_loop_symbol_info(controlFlow_); if (symbol.ty != QkSymbolType_Standalone) { - qk_str_free(symbol.name); + if (symbol.name != nullptr) { + qk_str_free(symbol.name); + } throw std::runtime_error( - "Qiskit indexed parameter-vector loop variables are not supported"); + "Qiskit indexed parameter-vector loop variables are not " + "supported"); + } + if (symbol.name == nullptr) { + throwPythonError("Qiskit failed to read a loop-parameter name"); } - result.parameter = symbol.name; + const std::string nativeName = symbol.name; qk_str_free(symbol.name); + const auto parameters = pythonAttribute( + operation_, "params", + "Qiskit for-loop operation does not expose its parameters"); + try { + if (nb::len(parameters) < 2U) { + throw std::runtime_error( + "Qiskit for-loop operation has no loop parameter"); + } + auto parameter = normalizePythonParameter(parameters[1]); + const auto* parameterSymbol = parameter.getSymbol(); + if (parameterSymbol == nullptr) { + throw std::runtime_error("Qiskit for-loop parameter is not a symbol"); + } + if (parameterSymbol->name != nativeName) { + throw std::runtime_error( + "Qiskit Python and native loop-parameter names do not match"); + } + result.parameter = std::move(parameter); + } catch (const nb::python_error& error) { + throwPythonError("Qiskit failed to inspect a loop parameter", error); + } break; } case QkLoopParamKind_Variable: @@ -1184,15 +1500,16 @@ class NativeCircuitWriter final : public CircuitWriter { qk_classical_register_free(reg); } - void setGlobalPhase(const double phase) override { - const OwnedParameter parameter(phase); - checkExitCode(qk_circuit_set_global_phase(circuit_, parameter.get()), + void setGlobalPhase(const Parameter& phase) override { + std::vector> ownedParameters; + const auto* parameter = nativeParameter(phase, ownedParameters); + checkExitCode(qk_circuit_set_global_phase(circuit_, parameter), "setting global phase"); } void addGate(const StandardGateMapping mapping, const std::vector& qubits, - const std::vector& parameters) override { + const std::vector& parameters) override { const auto* gate = versionGate(mapping); if (gate == nullptr) { const auto& descriptor = @@ -1217,9 +1534,9 @@ class NativeCircuitWriter final : public CircuitWriter { std::vector nativeParameters; ownedParameters.reserve(parameters.size()); nativeParameters.reserve(parameters.size()); - for (const auto parameter : parameters) { - ownedParameters.emplace_back(std::make_unique(parameter)); - nativeParameters.emplace_back(ownedParameters.back()->get()); + for (const auto& parameter : parameters) { + nativeParameters.emplace_back( + nativeParameter(parameter, ownedParameters)); } checkExitCode(addParameterizedGate(circuit_, gate->native, qubits.data(), nativeParameters.data()), @@ -1330,8 +1647,115 @@ class NativeCircuitWriter final : public CircuitWriter { } } + [[nodiscard]] const QkParam* nativeParameter( + const Parameter& parameter, + std::vector>& ownedParameters) { + size_t nodeCount = 0U; + return nativeParameter(parameter, ownedParameters, nodeCount, 1U); + } + + [[nodiscard]] const QkParam* + nativeParameter(const Parameter& parameter, + std::vector>& ownedParameters, + size_t& nodeCount, const size_t depth) { + countParameterExpressionNode(nodeCount); + if (depth > MAX_PARAMETER_EXPRESSION_DEPTH) { + throwParameterExpressionDepthError(); + } + if (const auto* number = parameter.getNumber()) { + ownedParameters.emplace_back( + std::make_unique(number->value)); + return ownedParameters.back()->get(); + } + if (const auto* symbol = parameter.getSymbol()) { + if (symbol->name.empty()) { + throw std::runtime_error( + "cannot export a symbolic parameter without a name"); + } + const auto found = symbols_.find(symbol->name); + if (found != symbols_.end()) { + return found->second->get(); + } + auto [inserted, success] = symbols_.emplace( + symbol->name, std::make_unique(symbol->name)); + static_cast(success); + return inserted->second->get(); + } + + auto output = std::make_unique(); + QkExitCode result = QkExitCode_Success; + if (const auto* unary = parameter.getUnary()) { + const auto* operand = nativeParameter(*unary->operand, ownedParameters, + nodeCount, depth + 1U); + switch (unary->operation) { + case UnaryParameterKind::Negate: + result = qk_param_neg(output->getMutable(), operand); + break; + case UnaryParameterKind::Sin: + result = qk_param_sin(output->getMutable(), operand); + break; + case UnaryParameterKind::Cos: + result = qk_param_cos(output->getMutable(), operand); + break; + case UnaryParameterKind::Tan: + result = qk_param_tan(output->getMutable(), operand); + break; + case UnaryParameterKind::ArcSin: + result = qk_param_asin(output->getMutable(), operand); + break; + case UnaryParameterKind::ArcCos: + result = qk_param_acos(output->getMutable(), operand); + break; + case UnaryParameterKind::ArcTan: + result = qk_param_atan(output->getMutable(), operand); + break; + case UnaryParameterKind::Exp: + result = qk_param_exp(output->getMutable(), operand); + break; + case UnaryParameterKind::Log: + result = qk_param_log(output->getMutable(), operand); + break; + case UnaryParameterKind::Abs: + result = qk_param_abs(output->getMutable(), operand); + break; + case UnaryParameterKind::Conjugate: + result = qk_param_conjugate(output->getMutable(), operand); + break; + } + } else if (const auto* binary = parameter.getBinary()) { + const auto* left = nativeParameter(*binary->left, ownedParameters, + nodeCount, depth + 1U); + const auto* right = nativeParameter(*binary->right, ownedParameters, + nodeCount, depth + 1U); + switch (binary->operation) { + case BinaryParameterKind::Add: + result = qk_param_add(output->getMutable(), left, right); + break; + case BinaryParameterKind::Subtract: + result = qk_param_sub(output->getMutable(), left, right); + break; + case BinaryParameterKind::Multiply: + result = qk_param_mul(output->getMutable(), left, right); + break; + case BinaryParameterKind::Divide: + result = qk_param_div(output->getMutable(), left, right); + break; + case BinaryParameterKind::Power: + result = qk_param_pow(output->getMutable(), left, right); + break; + } + } else { + throw std::runtime_error("unknown normalized parameter expression"); + } + checkExitCode(result, "constructing a parameter expression"); + const auto* value = output->get(); + ownedParameters.push_back(std::move(output)); + return value; + } + QkCircuit* circuit_ = nullptr; std::vector pendingControlledUnitaries_; + std::unordered_map> symbols_; }; class NativeTranslation final : public VersionedTranslation { diff --git a/bindings/mlir/qiskit/QiskitExport.cpp b/bindings/mlir/qiskit/QiskitExport.cpp index 35a22781f6..f4c02e0c11 100644 --- a/bindings/mlir/qiskit/QiskitExport.cpp +++ b/bindings/mlir/qiskit/QiskitExport.cpp @@ -17,6 +17,7 @@ #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/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/IR/QCInterfaces.h" #include "mlir/Dialect/QC/IR/QCOps.h" @@ -26,9 +27,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -40,12 +43,12 @@ #include #include -#include #include #include #include #include #include +#include #include #include #include @@ -69,20 +72,180 @@ struct ExportedInstruction { StandardGateMapping gate; std::vector qubits; std::vector clbits; - std::vector parameters; + std::vector parameters; std::vector> matrix; uint32_t unitaryControls = 0; }; -[[nodiscard]] double exportParameter(const mlir::Value value) { +using ExportedParameters = llvm::DenseMap; + +[[noreturn]] void throwExportedParameterExpressionSizeError() { + throw std::runtime_error("QC parameter expression exceeds the supported " + + std::to_string(MAX_PARAMETER_EXPRESSION_NODES) + + "-node size"); +} + +[[noreturn]] void throwExportedParameterExpressionDepthError() { + throw std::runtime_error("QC parameter expression exceeds the supported " + + std::to_string(MAX_PARAMETER_EXPRESSION_DEPTH) + + "-level nesting depth"); +} + +[[nodiscard]] Parameter numberParameter(const double value) { + return Parameter::number(value); +} + +[[nodiscard]] Parameter unaryParameter(const UnaryParameterKind kind, + Parameter operand) { + return Parameter::unary(kind, std::move(operand)); +} + +[[nodiscard]] Parameter binaryParameter(const BinaryParameterKind kind, + Parameter left, Parameter right) { + return Parameter::binary(kind, std::move(left), std::move(right)); +} + +[[nodiscard]] Parameter exportParameterImpl(mlir::Value value, + ExportedParameters& parameters, + const size_t depth, size_t& nodes) { + if (depth > MAX_PARAMETER_EXPRESSION_DEPTH) { + throwExportedParameterExpressionDepthError(); + } + if (const auto cached = parameters.find(value); cached != parameters.end()) { + return cached->second; + } + if (++nodes > MAX_PARAMETER_EXPRESSION_NODES) { + throwExportedParameterExpressionSizeError(); + } if (const auto number = mlir::utils::valueToDouble(value)) { - if (!std::isfinite(*number)) { - throw std::runtime_error("cannot export a non-finite QC parameter"); + auto result = numberParameter(*number); + parameters.try_emplace(value, result); + return result; + } + if (!value.getType().isF64()) { + throw std::runtime_error( + "Qiskit circuit export requires f64 scalar parameters"); + } + auto* const operation = value.getDefiningOp(); + if (operation == nullptr || operation->getNumResults() != 1U || + operation->getResult(0) != value) { + throw std::runtime_error( + "Qiskit circuit export cannot resolve an unnamed scalar parameter"); + } + const auto unary = [&](const UnaryParameterKind kind) { + if (operation->getNumOperands() != 1U) { + throw std::runtime_error("QC parameter operation '" + + operation->getName().getStringRef().str() + + "' has invalid arity"); } - return *number; + return unaryParameter(kind, + exportParameterImpl(operation->getOperand(0), + parameters, depth + 1U, nodes)); + }; + const auto binary = [&](const BinaryParameterKind kind) { + if (operation->getNumOperands() != 2U) { + throw std::runtime_error("QC parameter operation '" + + operation->getName().getStringRef().str() + + "' has invalid arity"); + } + auto left = exportParameterImpl(operation->getOperand(0), parameters, + depth + 1U, nodes); + auto right = exportParameterImpl(operation->getOperand(1), parameters, + depth + 1U, nodes); + return binaryParameter(kind, std::move(left), std::move(right)); + }; + + Parameter result; + if (llvm::isa(*operation)) { + result = binary(BinaryParameterKind::Add); + } else if (llvm::isa(*operation)) { + result = binary(BinaryParameterKind::Subtract); + } else if (llvm::isa(*operation)) { + result = binary(BinaryParameterKind::Multiply); + } else if (llvm::isa(*operation)) { + result = binary(BinaryParameterKind::Divide); + } else if (llvm::isa(*operation)) { + result = binary(BinaryParameterKind::Power); + } else if (llvm::isa(*operation)) { + result = unary(UnaryParameterKind::Negate); + } else if (llvm::isa(*operation)) { + result = unary(UnaryParameterKind::Sin); + } else if (llvm::isa(*operation)) { + result = unary(UnaryParameterKind::Cos); + } else if (llvm::isa(*operation)) { + result = unary(UnaryParameterKind::Tan); + } else if (llvm::isa(*operation)) { + result = unary(UnaryParameterKind::ArcSin); + } else if (llvm::isa(*operation)) { + result = unary(UnaryParameterKind::ArcCos); + } else if (llvm::isa(*operation)) { + result = unary(UnaryParameterKind::ArcTan); + } else if (llvm::isa(*operation)) { + result = unary(UnaryParameterKind::Exp); + } else if (llvm::isa(*operation)) { + result = unary(UnaryParameterKind::Log); + } else if (llvm::isa(*operation)) { + result = unary(UnaryParameterKind::Abs); + } else { + throw std::runtime_error( + "Qiskit circuit export does not support scalar parameter operation '" + + operation->getName().getStringRef().str() + "'"); } - throw std::runtime_error( - "Qiskit circuit export supports only numeric parameters"); + parameters.try_emplace(value, result); + return result; +} + +[[nodiscard]] Parameter exportParameter(const mlir::Value value, + ExportedParameters& parameters) { + size_t nodes = 0U; + return exportParameterImpl(value, parameters, 1U, nodes); +} + +void validateExportParameterImpl(const Parameter& parameter, const size_t depth, + size_t& nodes) { + if (depth > MAX_PARAMETER_EXPRESSION_DEPTH) { + throwExportedParameterExpressionDepthError(); + } + if (++nodes > MAX_PARAMETER_EXPRESSION_NODES) { + throwExportedParameterExpressionSizeError(); + } + if (parameter.getNumber() != nullptr) { + return; + } + if (const auto* symbol = parameter.getSymbol()) { + if (symbol->name.empty()) { + throw std::runtime_error("QC parameter symbol has an invalid name"); + } + if (symbol->name.find('\0') != std::string::npos) { + throw std::runtime_error( + "QC parameter symbol name contains a null character"); + } + return; + } + if (const auto* unary = parameter.getUnary()) { + validateExportParameterImpl(*unary->operand, depth + 1U, nodes); + return; + } + if (const auto* binary = parameter.getBinary()) { + validateExportParameterImpl(*binary->left, depth + 1U, nodes); + validateExportParameterImpl(*binary->right, depth + 1U, nodes); + return; + } + throw std::runtime_error("unknown QC parameter expression"); +} + +void validateExportParameter(const Parameter& parameter) { + size_t nodes = 0U; + validateExportParameterImpl(parameter, 1U, nodes); +} + +[[nodiscard]] bool isParameterExpressionOperation(mlir::Operation& operation) { + return llvm::isa(operation); } [[nodiscard]] uint32_t checkedIndex(const int64_t index, @@ -138,11 +301,94 @@ struct ExportState { std::vector instructions; std::vector quantumRegisters; std::vector classicalRegisters; - double globalPhase = 0.0; + ExportedParameters parameters; + std::vector inputParameters; + Parameter globalPhase; uint32_t numQubits = 0; uint32_t numClbits = 0; }; +void collectParameterNames(const Parameter& parameter, + llvm::StringSet<>& names) { + if (const auto* symbol = parameter.getSymbol()) { + names.insert(symbol->name); + return; + } + if (const auto* unary = parameter.getUnary()) { + collectParameterNames(*unary->operand, names); + return; + } + if (const auto* binary = parameter.getBinary()) { + collectParameterNames(*binary->left, names); + collectParameterNames(*binary->right, names); + } +} + +void validateExportParameters(const ExportState& state) { + llvm::StringSet<> usedNames; + const auto validate = [&](const Parameter& parameter) { + validateExportParameter(parameter); + collectParameterNames(parameter, usedNames); + }; + validate(state.globalPhase); + for (const auto& instruction : state.instructions) { + for (const auto& parameter : instruction.parameters) { + validate(parameter); + } + } + for (const auto& input : state.inputParameters) { + const auto* symbol = input.getSymbol(); + if (symbol == nullptr) { + throw std::runtime_error("QC program input is not a parameter symbol"); + } + if (!usedNames.contains(symbol->name)) { + throw std::runtime_error( + "Qiskit circuit export cannot preserve unused named f64 program " + "input '" + + symbol->name + "'"); + } + } +} + +void collectParameters(mlir::func::FuncOp function, ExportState& state) { + for (const auto [index, argument] : + llvm::enumerate(function.getArguments())) { + const auto name = function.getArgAttrOfType( + index, mlir::mqt::MQTDialect::InputNameAttrHelper::getNameStr()); + if (!argument.getType().isF64() || !name) { + throw std::runtime_error( + "Qiskit circuit export requires named f64 program inputs"); + } + auto parameter = Parameter::symbol(name.str()); + state.parameters[argument] = parameter; + state.inputParameters.push_back(std::move(parameter)); + } +} + +void addGlobalPhase(ExportState& state, const Parameter& phase) { + if (const auto* number = phase.getNumber()) { + if (const auto* globalNumber = state.globalPhase.getNumber()) { + const auto sum = globalNumber->value + number->value; + if (!std::isfinite(sum)) { + throw std::runtime_error( + "QC global phase cannot be represented by Qiskit"); + } + state.globalPhase = Parameter::number(sum); + return; + } + if (std::abs(number->value) <= mlir::utils::TOLERANCE) { + return; + } + } else if (const auto* globalNumber = state.globalPhase.getNumber(); + globalNumber != nullptr && + std::abs(globalNumber->value) <= mlir::utils::TOLERANCE) { + state.globalPhase = phase; + return; + } + state.globalPhase = binaryParameter(BinaryParameterKind::Add, + std::move(state.globalPhase), phase); +} + [[nodiscard]] std::vector mapQubits(const mlir::ValueRange values, const llvm::DenseMap& qubits) { @@ -161,7 +407,8 @@ mapQubits(const mlir::ValueRange values, [[nodiscard]] ExportedInstruction collectUnitaryInstruction(mlir::Operation& operation, - const llvm::DenseMap& qubits); + const llvm::DenseMap& qubits, + ExportedParameters& parameters); [[nodiscard]] std::vector modifierBodyOperations(mlir::Region& region) { @@ -171,7 +418,8 @@ modifierBodyOperations(mlir::Region& region) { } std::vector operations; for (auto& operation : region.front()) { - if (!llvm::isa(operation)) { + if (!llvm::isa(operation) && + !isParameterExpressionOperation(operation)) { operations.push_back(&operation); } } @@ -262,15 +510,17 @@ void invertGate(ExportedInstruction& instruction) { if (instruction.parameters.empty()) { throw std::runtime_error("QC inverse modifier has invalid arity"); } - instruction.parameters.front() = -instruction.parameters.front(); + instruction.parameters.front() = unaryParameter( + UnaryParameterKind::Negate, std::move(instruction.parameters.front())); return; } if (instruction.gate.gate == Gate::U3 && instruction.parameters.size() == 3U) { - const std::array values{instruction.parameters[0], - instruction.parameters[1], - instruction.parameters[2]}; - instruction.parameters = {-values[0], -values[2], -values[1]}; + auto parameters = std::move(instruction.parameters); + instruction.parameters = { + unaryParameter(UnaryParameterKind::Negate, std::move(parameters[0])), + unaryParameter(UnaryParameterKind::Negate, std::move(parameters[2])), + unaryParameter(UnaryParameterKind::Negate, std::move(parameters[1]))}; return; } throw std::runtime_error( @@ -279,7 +529,8 @@ void invertGate(ExportedInstruction& instruction) { [[nodiscard]] ExportedInstruction collectUnitaryInstruction(mlir::Operation& operation, - const llvm::DenseMap& qubits) { + const llvm::DenseMap& qubits, + ExportedParameters& parameters) { if (auto control = llvm::dyn_cast(operation)) { auto bodyOperations = modifierBodyOperations(control.getRegion()); const auto controls = mapQubits(control.getControls(), qubits); @@ -298,16 +549,18 @@ collectUnitaryInstruction(mlir::Operation& operation, .gate = {mlir::qc::StandardGate::CU, 0}, .qubits = {controls.front(), targets.front()}}; for (const auto parameter : unitary.getParameters()) { - result.parameters.push_back(exportParameter(parameter)); + result.parameters.push_back(exportParameter(parameter, parameters)); } - result.parameters.push_back(exportParameter(phase.getTheta())); + result.parameters.push_back( + exportParameter(phase.getTheta(), parameters)); return result; } if (bodyOperations.size() != 1U) { throw std::runtime_error( "QC control export requires one standard gate in the modifier body"); } - auto result = collectUnitaryInstruction(*bodyOperations.front(), nestedMap); + auto result = collectUnitaryInstruction(*bodyOperations.front(), nestedMap, + parameters); auto& numControls = result.kind == ExportedInstruction::Kind::Unitary ? result.unitaryControls : result.gate.controls; @@ -328,7 +581,8 @@ collectUnitaryInstruction(mlir::Operation& operation, } auto nestedMap = modifierQubitMap(qubits, inverse.getRegion().front(), inverse.getQubits()); - auto result = collectUnitaryInstruction(*bodyOperations.front(), nestedMap); + auto result = collectUnitaryInstruction(*bodyOperations.front(), nestedMap, + parameters); invertGate(result); return result; } @@ -338,15 +592,17 @@ collectUnitaryInstruction(mlir::Operation& operation, throw std::runtime_error( "QC power export requires one standard gate in the modifier body"); } - const auto exponent = exportParameter(power.getExponent()); - if (exponent != 1.0 && exponent != -1.0) { + const auto exponent = exportParameter(power.getExponent(), parameters); + const auto* number = exponent.getNumber(); + if (number == nullptr || (number->value != 1.0 && number->value != -1.0)) { throw std::runtime_error( "QC power export supports only constant exponents 1 and -1"); } auto nestedMap = modifierQubitMap(qubits, power.getRegion().front(), power.getQubits()); - auto result = collectUnitaryInstruction(*bodyOperations.front(), nestedMap); - if (exponent == -1.0) { + auto result = collectUnitaryInstruction(*bodyOperations.front(), nestedMap, + parameters); + if (number->value == -1.0) { invertGate(result); } return result; @@ -381,7 +637,7 @@ collectUnitaryInstruction(mlir::Operation& operation, } result.gate.gate = descriptor->gate; for (const auto parameter : gate.getParameters()) { - result.parameters.push_back(exportParameter(parameter)); + result.parameters.push_back(exportParameter(parameter, parameters)); } return result; } @@ -443,7 +699,7 @@ void collectResources(mlir::func::FuncOp function, ExportState& state, state.quantumBases[alloc.getResult()] = state.numQubits; state.quantumSizes[alloc.getResult()] = size; if (const auto name = operation.getAttrOfType( - mlir::utils::QUBIT_REGISTER_NAME_ATTR)) { + mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr())) { Register reg{.name = name.str()}; reg.bits.resize(size); std::iota(reg.bits.begin(), reg.bits.end(), state.numQubits); @@ -507,7 +763,8 @@ void collectResources(mlir::func::FuncOp function, ExportState& state, .size = size, .initialization = alloc.getInitialization()}; - if (const auto name = alloc.getSourceNameAttr()) { + if (const auto name = alloc->getAttrOfType( + mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr())) { Register reg{.name = name.str()}; reg.bits.resize(size); std::iota(reg.bits.begin(), reg.bits.end(), state.numClbits); @@ -567,7 +824,8 @@ void collectFlatInstructions(mlir::func::FuncOp function, ExportState& state) { throw std::runtime_error( "QC to Qiskit export encountered an unsupported memory allocation"); } - if (llvm::isa(operation)) { + if (llvm::isa(operation) || + isParameterExpressionOperation(operation)) { continue; } if (auto load = llvm::dyn_cast(operation)) { @@ -598,11 +856,8 @@ void collectFlatInstructions(mlir::func::FuncOp function, ExportState& state) { continue; } if (auto phase = llvm::dyn_cast(operation)) { - state.globalPhase += exportParameter(phase.getTheta()); - if (!std::isfinite(state.globalPhase)) { - throw std::runtime_error( - "QC global phase cannot be represented by Qiskit"); - } + addGlobalPhase(state, + exportParameter(phase.getTheta(), state.parameters)); continue; } if (auto measure = llvm::dyn_cast(operation)) { @@ -645,7 +900,7 @@ void collectFlatInstructions(mlir::func::FuncOp function, ExportState& state) { } if (llvm::isa(operation)) { state.instructions.push_back( - collectUnitaryInstruction(operation, state.qubits)); + collectUnitaryInstruction(operation, state.qubits, state.parameters)); continue; } if (llvm::isa(operation)) { state.instructions.push_back( - collectUnitaryInstruction(operation, state.qubits)); + collectUnitaryInstruction(operation, state.qubits, state.parameters)); continue; } + if (operation.getNumResults() == 1U && + operation.getResult(0).getType().isF64()) { + throw std::runtime_error("Qiskit circuit export does not support scalar " + "parameter operation '" + + operation.getName().getStringRef().str() + "'"); + } throw std::runtime_error("unsupported QC operation in Qiskit export: " + operation.getName().getStringRef().str()); } @@ -690,18 +951,15 @@ nb::object exportCircuit(const mlir::QCProgram& program, throw std::runtime_error( "QC to Qiskit export requires a single-block entry function"); } - if (!function.getArguments().empty()) { - throw std::runtime_error( - "Qiskit circuit export does not support symbolic or runtime inputs"); - } - ExportState state; + collectParameters(function, state); if (target != nullptr) { state.numQubits = checkedIndex(static_cast(target->numQubits()), "target qubit count"); } collectResources(function, state, target); collectFlatInstructions(function, state); + validateExportParameters(state); if (target != nullptr) { Register reg{.name = "q"}; reg.bits.resize(state.numQubits); diff --git a/bindings/mlir/qiskit/QiskitImport.cpp b/bindings/mlir/qiskit/QiskitImport.cpp index ac69754644..98e406d692 100644 --- a/bindings/mlir/qiskit/QiskitImport.cpp +++ b/bindings/mlir/qiskit/QiskitImport.cpp @@ -15,6 +15,7 @@ #include "jeff/IR/JeffDialect.h" #include "mlir/Compiler/Programs.h" #include "mlir/Dialect/CBit/IR/CBitDialect.h" +#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/Translation/StandardGate.h" @@ -37,8 +38,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -75,17 +78,35 @@ namespace { using ParameterValue = std::variant; using LocalParameters = llvm::StringMap; +using GlobalParameters = llvm::StringMap; +using ValidationParameters = llvm::StringMap; constexpr size_t MAX_DEFINITION_DEPTH = 64U; constexpr size_t MAX_CONTROL_FLOW_DEPTH = 64U; constexpr size_t MAX_EXPANDED_OPERATIONS = 10'000'000U; +[[nodiscard]] mlir::Value floatConstant(mlir::ImplicitLocOpBuilder& builder, + double value); + +[[noreturn]] void throwImportedParameterExpressionSizeError() { + throw std::runtime_error( + "Qiskit parameter expression exceeds the supported " + + std::to_string(MAX_PARAMETER_EXPRESSION_NODES) + "-node size"); +} + +[[noreturn]] void throwImportedParameterExpressionDepthError() { + throw std::runtime_error( + "Qiskit parameter expression exceeds the supported " + + std::to_string(MAX_PARAMETER_EXPRESSION_DEPTH) + "-level nesting depth"); +} + [[nodiscard]] std::shared_ptr createContext() { mlir::DialectRegistry registry; - registry.insert(); mlir::registerBuiltinDialectTranslation(registry); @@ -95,44 +116,159 @@ constexpr size_t MAX_EXPANDED_OPERATIONS = 10'000'000U; return context; } -void validateParameter(const Parameter& parameter, - const llvm::StringSet<>& localParameters) { - if (parameter.number) { - if (!std::isfinite(*parameter.number)) { +void validateParameterImpl(const Parameter& parameter, + const ValidationParameters& localParameters, + const ValidationParameters& freeParameters, + const size_t depth, size_t& nodes) { + if (depth > MAX_PARAMETER_EXPRESSION_DEPTH) { + throwImportedParameterExpressionDepthError(); + } + if (++nodes > MAX_PARAMETER_EXPRESSION_NODES) { + throwImportedParameterExpressionSizeError(); + } + if (const auto* number = parameter.getNumber()) { + if (!std::isfinite(number->value)) { throw std::runtime_error("Qiskit returned a non-finite parameter"); } return; } - if (localParameters.contains(parameter.text)) { + if (const auto* symbol = parameter.getSymbol()) { + if (symbol->name.empty()) { + throw std::runtime_error( + "Qiskit returned a parameter with invalid symbol metadata"); + } + if (localParameters.contains(symbol->name)) { + return; + } + if (freeParameters.contains(symbol->name)) { + return; + } + throw std::runtime_error("Qiskit parameter symbol '" + symbol->name + + "' is not defined in this circuit scope"); + } + if (const auto* unary = parameter.getUnary()) { + validateParameterImpl(*unary->operand, localParameters, freeParameters, + depth + 1U, nodes); return; } - throw std::runtime_error( - "Qiskit circuit import does not support free symbolic parameter '" + - parameter.text + "'"); + if (const auto* binary = parameter.getBinary()) { + validateParameterImpl(*binary->left, localParameters, freeParameters, + depth + 1U, nodes); + validateParameterImpl(*binary->right, localParameters, freeParameters, + depth + 1U, nodes); + return; + } + throw std::runtime_error("unknown normalized Qiskit parameter expression"); } -[[nodiscard]] ParameterValue -parameterValue(const std::string_view text, - const LocalParameters& localParameters) { - if (const auto local = localParameters.find(text); - local != localParameters.end()) { - return local->second; - } - throw std::runtime_error( - "Qiskit circuit import does not support free symbolic parameter '" + - std::string(text) + "'"); +void validateParameter(const Parameter& parameter, + const ValidationParameters& localParameters, + const ValidationParameters& freeParameters) { + size_t nodes = 0U; + validateParameterImpl(parameter, localParameters, freeParameters, 1U, nodes); +} + +[[nodiscard]] mlir::Value +materializeParameterValue(mlir::qc::QCProgramBuilder& builder, + const ParameterValue& parameter) { + return std::holds_alternative(parameter) + ? floatConstant(builder, std::get(parameter)) + : std::get(parameter); } [[nodiscard]] ParameterValue -parameterValue(const Parameter& parameter, - const LocalParameters& localParameters) { - if (parameter.number) { - if (!std::isfinite(*parameter.number)) { - throw std::runtime_error("Qiskit returned a non-finite parameter"); +parameterValueImpl(mlir::qc::QCProgramBuilder& builder, + const Parameter& parameter, + const LocalParameters& localParameters, + const GlobalParameters& globalParameters, const size_t depth, + size_t& nodes) { + if (depth > MAX_PARAMETER_EXPRESSION_DEPTH) { + throwImportedParameterExpressionDepthError(); + } + if (++nodes > MAX_PARAMETER_EXPRESSION_NODES) { + throwImportedParameterExpressionSizeError(); + } + if (const auto* number = parameter.getNumber()) { + return number->value; + } + if (const auto* symbol = parameter.getSymbol()) { + if (const auto local = localParameters.find(symbol->name); + local != localParameters.end()) { + return local->second; + } + if (const auto global = globalParameters.find(symbol->name); + global != globalParameters.end()) { + return global->second; + } + throw std::runtime_error("Qiskit parameter symbol '" + symbol->name + + "' is not defined in this circuit scope"); + } + + if (const auto* unary = parameter.getUnary()) { + if (unary->operation == UnaryParameterKind::Conjugate) { + /// QC scalar parameters are real-valued, so conjugation is the identity. + return parameterValueImpl(builder, *unary->operand, localParameters, + globalParameters, depth + 1U, nodes); + } + const auto operand = materializeParameterValue( + builder, parameterValueImpl(builder, *unary->operand, localParameters, + globalParameters, depth + 1U, nodes)); + switch (unary->operation) { + case UnaryParameterKind::Negate: + return mlir::arith::NegFOp::create(builder, operand).getResult(); + case UnaryParameterKind::Sin: + return mlir::math::SinOp::create(builder, operand).getResult(); + case UnaryParameterKind::Cos: + return mlir::math::CosOp::create(builder, operand).getResult(); + case UnaryParameterKind::Tan: + return mlir::math::TanOp::create(builder, operand).getResult(); + case UnaryParameterKind::ArcSin: + return mlir::math::AsinOp::create(builder, operand).getResult(); + case UnaryParameterKind::ArcCos: + return mlir::math::AcosOp::create(builder, operand).getResult(); + case UnaryParameterKind::ArcTan: + return mlir::math::AtanOp::create(builder, operand).getResult(); + case UnaryParameterKind::Exp: + return mlir::math::ExpOp::create(builder, operand).getResult(); + case UnaryParameterKind::Log: + return mlir::math::LogOp::create(builder, operand).getResult(); + case UnaryParameterKind::Abs: + return mlir::math::AbsFOp::create(builder, operand).getResult(); + case UnaryParameterKind::Conjugate: + break; } - return *parameter.number; } - return parameterValue(parameter.text, localParameters); + + if (const auto* binary = parameter.getBinary()) { + const auto left = materializeParameterValue( + builder, parameterValueImpl(builder, *binary->left, localParameters, + globalParameters, depth + 1U, nodes)); + const auto right = materializeParameterValue( + builder, parameterValueImpl(builder, *binary->right, localParameters, + globalParameters, depth + 1U, nodes)); + switch (binary->operation) { + case BinaryParameterKind::Add: + return mlir::arith::AddFOp::create(builder, left, right).getResult(); + case BinaryParameterKind::Subtract: + return mlir::arith::SubFOp::create(builder, left, right).getResult(); + case BinaryParameterKind::Multiply: + return mlir::arith::MulFOp::create(builder, left, right).getResult(); + case BinaryParameterKind::Divide: + return mlir::arith::DivFOp::create(builder, left, right).getResult(); + case BinaryParameterKind::Power: + return mlir::math::PowFOp::create(builder, left, right).getResult(); + } + } + throw std::runtime_error("unknown normalized Qiskit parameter expression"); +} + +[[nodiscard]] ParameterValue +parameterValue(mlir::qc::QCProgramBuilder& builder, const Parameter& parameter, + const LocalParameters& localParameters, + const GlobalParameters& globalParameters) { + size_t nodes = 0U; + return parameterValueImpl(builder, parameter, localParameters, + globalParameters, 1U, nodes); } void requireArity(const Instruction& instruction, const size_t qubits, @@ -257,6 +393,7 @@ void emitModifiedOperation( mlir::qc::QCProgramBuilder& builder, const Instruction& instruction, const mlir::ValueRange qubits, const ModifiedQubitArity arity, const LocalParameters& localParameters, + const GlobalParameters& globalParameters, llvm::function_ref emitBase) { const auto targets = qubits.drop_front(arity.controls); const auto emitModifiers = @@ -279,7 +416,8 @@ void emitModifiedOperation( }); return; case GateModifierKind::Power: { - const auto exponent = parameterValue(modifier.exponent, localParameters); + const auto exponent = parameterValue(builder, modifier.exponent, + localParameters, globalParameters); builder.pow(exponent, targetArguments, [&](const mlir::ValueRange innerArguments) { self(self, count - 1U, innerArguments); @@ -305,7 +443,8 @@ void emitModifiedGate(mlir::qc::QCProgramBuilder& builder, const Instruction& instruction, const mlir::ValueRange qubits, const llvm::ArrayRef parameters, - const LocalParameters& localParameters) { + const LocalParameters& localParameters, + const GlobalParameters& globalParameters) { const auto arity = gateArity(instruction); if (!arity) { throw std::runtime_error("unsupported modified Qiskit standard gate '" + @@ -318,7 +457,7 @@ void emitModifiedGate(mlir::qc::QCProgramBuilder& builder, emitModifiedOperation( builder, instruction, qubits, modifiedQubitArity(instruction, arity->first), localParameters, - [&](const mlir::ValueRange targetArguments) { + globalParameters, [&](const mlir::ValueRange targetArguments) { emitStandardGate(builder, instruction, targetArguments, parameters); }); } @@ -359,7 +498,8 @@ void emitGate(mlir::qc::QCProgramBuilder& builder, const Instruction& instruction, const llvm::ArrayRef allQubits, const llvm::ArrayRef qubitMap, - const LocalParameters& localParameters) { + const LocalParameters& localParameters, + const GlobalParameters& globalParameters) { llvm::SmallVector qubits; qubits.reserve(instruction.qubits.size()); for (const auto index : instruction.qubits) { @@ -372,13 +512,14 @@ void emitGate(mlir::qc::QCProgramBuilder& builder, llvm::SmallVector parameters; parameters.reserve(instruction.parameters.size()); for (const auto& parameter : instruction.parameters) { - parameters.push_back(parameterValue(parameter, localParameters)); + parameters.push_back( + parameterValue(builder, parameter, localParameters, globalParameters)); } const llvm::ArrayRef qubitRange(qubits); if (!instruction.modifiers.empty()) { emitModifiedGate(builder, instruction, qubitRange, parameters, - localParameters); + localParameters, globalParameters); return; } emitStandardGate(builder, instruction, qubitRange, parameters); @@ -765,6 +906,7 @@ void translateCircuit(mlir::qc::QCProgramBuilder& builder, llvm::ArrayRef allQubits, llvm::ArrayRef classicalBits, const LocalParameters& localParameters, + const GlobalParameters& globalParameters, size_t definitionDepth, size_t controlFlowDepth); [[nodiscard]] int64_t rangeLength(const Loop& loop) { @@ -821,6 +963,7 @@ void translateControlFlow(mlir::qc::QCProgramBuilder& builder, llvm::ArrayRef rootQubitMap, llvm::ArrayRef rootClbitMap, const LocalParameters& localParameters, + const GlobalParameters& globalParameters, const size_t definitionDepth, const size_t controlFlowDepth) { if (controlFlowDepth >= MAX_CONTROL_FLOW_DEPTH) { @@ -849,7 +992,7 @@ void translateControlFlow(mlir::qc::QCProgramBuilder& builder, const LocalParameters& parameters) { translateCircuit(builder, block, qubitMap, clbitMap, rootQubitMap, rootClbitMap, allQubits, classicalBits, parameters, - definitionDepth, controlFlowDepth + 1U); + globalParameters, definitionDepth, controlFlowDepth + 1U); }; switch (controlFlow.kind()) { @@ -904,7 +1047,12 @@ void translateControlFlow(mlir::qc::QCProgramBuilder& builder, auto parameters = localParameters; if (loop.parameter) { requireExactLoopParameter(value); - parameters[*loop.parameter] = + const auto* symbol = loop.parameter->getSymbol(); + if (symbol == nullptr) { + throw std::runtime_error( + "Qiskit for-loop parameter is not a symbol"); + } + parameters[symbol->name] = floatConstant(builder, static_cast(value)); } translateBlock(*body, parameters); @@ -919,8 +1067,11 @@ void translateControlFlow(mlir::qc::QCProgramBuilder& builder, builder.scfFor(0, count, 1, [&](const mlir::Value iteration) { auto parameters = localParameters; if (loop.parameter) { - parameters[*loop.parameter] = - loopParameterValue(builder, iteration, loop); + const auto* symbol = loop.parameter->getSymbol(); + if (symbol == nullptr) { + throw std::runtime_error("Qiskit for-loop parameter is not a symbol"); + } + parameters[symbol->name] = loopParameterValue(builder, iteration, loop); } translateBlock(*body, parameters); }); @@ -988,9 +1139,11 @@ void translateCircuit(mlir::qc::QCProgramBuilder& builder, const llvm::ArrayRef allQubits, const llvm::ArrayRef classicalBits, const LocalParameters& localParameters, + const GlobalParameters& globalParameters, const size_t definitionDepth, const size_t controlFlowDepth) { - builder.gphase(parameterValue(circuit.globalPhase(), localParameters)); + builder.gphase(parameterValue(builder, circuit.globalPhase(), localParameters, + globalParameters)); const auto getQubit = [&](const uint32_t local) { if (local >= qubitMap.size() || qubitMap[local] >= allQubits.size()) { throw std::runtime_error( @@ -1028,8 +1181,8 @@ void translateCircuit(mlir::qc::QCProgramBuilder& builder, } translateCircuit(builder, *definition, definitionQubits, definitionClbits, definitionQubits, definitionClbits, allQubits, - classicalBits, localParameters, definitionDepth + 1U, - controlFlowDepth); + classicalBits, localParameters, globalParameters, + definitionDepth + 1U, controlFlowDepth); }; for (size_t index = 0; index < circuit.numInstructions(); ++index) { @@ -1037,7 +1190,8 @@ void translateCircuit(mlir::qc::QCProgramBuilder& builder, switch (instruction.kind) { case OperationKind::Gate: if (instruction.standardGate) { - emitGate(builder, instruction, allQubits, qubitMap, localParameters); + emitGate(builder, instruction, allQubits, qubitMap, localParameters, + globalParameters); } else { translateDefinition(index, instruction); } @@ -1081,7 +1235,7 @@ void translateCircuit(mlir::qc::QCProgramBuilder& builder, const auto matrix = mlir::DenseElementsAttr::get( type, llvm::ArrayRef>(values)); emitModifiedOperation(builder, instruction, operands, arity, - localParameters, + localParameters, globalParameters, [&](const mlir::ValueRange targetArguments) { builder.unitary(targetArguments, matrix); }); @@ -1090,7 +1244,7 @@ void translateCircuit(mlir::qc::QCProgramBuilder& builder, const auto controlFlow = circuit.controlFlow(index); translateControlFlow(builder, *controlFlow, allQubits, classicalBits, rootQubitMap, rootClbitMap, localParameters, - definitionDepth, controlFlowDepth); + globalParameters, definitionDepth, controlFlowDepth); break; } case OperationKind::Delay: @@ -1230,9 +1384,11 @@ expansionSummary(const CircuitReader& circuit, ExpansionCountState& state, } void validateCircuit(const CircuitReader& circuit, - const llvm::StringSet<>& localParameters, - uint32_t rootQubits, uint32_t rootClbits, - size_t definitionDepth, size_t controlFlowDepth); + const ValidationParameters& localParameters, + const ValidationParameters& freeParameters, + llvm::StringSet<>& parameterNames, uint32_t rootQubits, + uint32_t rootClbits, size_t definitionDepth, + size_t controlFlowDepth); void validateExpression(const Expression& expression) { if (expression.type == ClassicalType::Uint && @@ -1293,7 +1449,9 @@ void validateTarget(const ClassicalTarget& target, const uint32_t rootClbits) { } void validateControlFlow(const ControlFlowReader& controlFlow, - llvm::StringSet<> localParameters, + ValidationParameters localParameters, + const ValidationParameters& freeParameters, + llvm::StringSet<>& parameterNames, const uint32_t rootQubits, const uint32_t rootClbits, const size_t definitionDepth, const size_t controlFlowDepth) { @@ -1353,7 +1511,16 @@ void validateControlFlow(const ControlFlowReader& controlFlow, } } if (loop.parameter) { - localParameters.insert(*loop.parameter); + const auto* symbol = loop.parameter->getSymbol(); + if (symbol == nullptr || symbol->name.empty()) { + throw std::runtime_error( + "Qiskit for-loop parameter has invalid symbol metadata"); + } + if (!parameterNames.insert(symbol->name).second) { + throw std::runtime_error( + "Qiskit circuit contains distinct parameters with the same name"); + } + localParameters[symbol->name] = *loop.parameter; } break; } @@ -1408,13 +1575,16 @@ void validateControlFlow(const ControlFlowReader& controlFlow, throw std::runtime_error( "Qiskit control-flow block operands do not match its bit mapping"); } - validateCircuit(*block, localParameters, rootQubits, rootClbits, - definitionDepth, controlFlowDepth + 1U); + validateCircuit(*block, localParameters, freeParameters, parameterNames, + rootQubits, rootClbits, definitionDepth, + controlFlowDepth + 1U); } } void validateDefinition(const CircuitReader& circuit, const size_t index, - const llvm::StringSet<>& localParameters, + const ValidationParameters& localParameters, + const ValidationParameters& freeParameters, + llvm::StringSet<>& parameterNames, const size_t definitionDepth, const size_t controlFlowDepth) { if (definitionDepth >= MAX_DEFINITION_DEPTH) { @@ -1422,13 +1592,15 @@ void validateDefinition(const CircuitReader& circuit, const size_t index, "Qiskit instruction definitions exceed the nesting limit of 64"); } const auto definition = circuit.definition(index); - validateCircuit(*definition, localParameters, definition->numQubits(), - definition->numClbits(), definitionDepth + 1U, - controlFlowDepth); + validateCircuit(*definition, localParameters, freeParameters, parameterNames, + definition->numQubits(), definition->numClbits(), + definitionDepth + 1U, controlFlowDepth); } void validateCircuit(const CircuitReader& circuit, - const llvm::StringSet<>& localParameters, + const ValidationParameters& localParameters, + const ValidationParameters& freeParameters, + llvm::StringSet<>& parameterNames, const uint32_t rootQubits, const uint32_t rootClbits, const size_t definitionDepth, const size_t controlFlowDepth) { @@ -1441,7 +1613,7 @@ void validateCircuit(const CircuitReader& circuit, circuit.numQubits(), "quantum")); static_cast(validateRegisterLayout(circuitRegisters(circuit, false), circuit.numClbits(), "classical")); - validateParameter(circuit.globalPhase(), localParameters); + validateParameter(circuit.globalPhase(), localParameters, freeParameters); for (size_t index = 0; index < circuit.numInstructions(); ++index) { const auto instruction = circuit.instruction(index); @@ -1458,11 +1630,11 @@ void validateCircuit(const CircuitReader& circuit, } } for (const auto& parameter : instruction.parameters) { - validateParameter(parameter, localParameters); + validateParameter(parameter, localParameters, freeParameters); } for (const auto& modifier : instruction.modifiers) { if (modifier.kind == GateModifierKind::Power) { - validateParameter(modifier.exponent, localParameters); + validateParameter(modifier.exponent, localParameters, freeParameters); } } @@ -1491,8 +1663,8 @@ void validateCircuit(const CircuitReader& circuit, "Qiskit circuit import does not support modifiers on custom " "instructions"); } - validateDefinition(circuit, index, localParameters, definitionDepth, - controlFlowDepth); + validateDefinition(circuit, index, localParameters, freeParameters, + parameterNames, definitionDepth, controlFlowDepth); break; case OperationKind::Unknown: if (!instruction.modifiers.empty()) { @@ -1500,8 +1672,8 @@ void validateCircuit(const CircuitReader& circuit, "Qiskit circuit import does not support modifiers on custom " "instructions"); } - validateDefinition(circuit, index, localParameters, definitionDepth, - controlFlowDepth); + validateDefinition(circuit, index, localParameters, freeParameters, + parameterNames, definitionDepth, controlFlowDepth); break; case OperationKind::Barrier: if (!instruction.parameters.empty() || !instruction.clbits.empty()) { @@ -1526,7 +1698,8 @@ void validateCircuit(const CircuitReader& circuit, break; case OperationKind::ControlFlow: { const auto controlFlow = circuit.controlFlow(index); - validateControlFlow(*controlFlow, localParameters, rootQubits, rootClbits, + validateControlFlow(*controlFlow, localParameters, freeParameters, + parameterNames, rootQubits, rootClbits, definitionDepth, controlFlowDepth); break; } @@ -1541,12 +1714,35 @@ void validateCircuit(const CircuitReader& circuit, mlir::QCProgram importCircuit(const nb::handle circuit) { auto translation = selectTranslation(); auto view = translation->openCircuit(circuit); + const auto freeParameters = view->parameters(); + ValidationParameters freeParameterSymbols; + llvm::StringSet<> parameterNames; + for (const auto& parameter : freeParameters) { + const auto* symbol = parameter.getSymbol(); + if (symbol == nullptr || symbol->name.empty()) { + throw std::runtime_error( + "Qiskit circuit returned an invalid free parameter"); + } + if (!parameterNames.insert(symbol->name).second) { + throw std::runtime_error( + "Qiskit circuit contains distinct parameters with the same name"); + } + freeParameterSymbols.try_emplace(symbol->name, parameter); + } ExpansionCountState expansion; static_cast(expansionSummary(*view, expansion)); - validateCircuit(*view, {}, view->numQubits(), view->numClbits(), 0U, 0U); + validateCircuit(*view, {}, freeParameterSymbols, parameterNames, + view->numQubits(), view->numClbits(), 0U, 0U); const auto quantumRegisters = circuitRegisters(*view, true); const auto classicalRegisters = circuitRegisters(*view, false); + for (const auto& reg : + llvm::concat(quantumRegisters, classicalRegisters)) { + if (!parameterNames.insert(reg.name).second) { + throw std::runtime_error( + "Qiskit circuit requires unique parameter and register names"); + } + } const auto looseQubits = validateRegisterLayout(quantumRegisters, view->numQubits(), "quantum"); const auto looseClbits = validateRegisterLayout( @@ -1568,6 +1764,32 @@ mlir::QCProgram importCircuit(const nb::handle circuit) { } } builder.initialize(resultTypes); + auto function = llvm::cast( + builder.getInsertionBlock()->getParentOp()); + GlobalParameters globalParameters; + for (const auto& parameter : freeParameters) { + const auto* symbol = parameter.getSymbol(); + if (symbol == nullptr) { + throw std::runtime_error( + "Qiskit circuit returned an invalid free parameter"); + } + const llvm::SmallVector argumentAttributes{ + builder.getNamedAttr( + mlir::mqt::MQTDialect::InputNameAttrHelper::getNameStr(), + builder.getStringAttr(symbol->name))}; + const auto index = function.getNumArguments(); + // MLIR types are handles. Converting FloatType to Type keeps the same + // storage and does not slice object state. + // NOLINTNEXTLINE(cppcoreguidelines-slicing) + const mlir::Type parameterType = builder.getF64Type(); + if (failed(function.insertArgument( + index, parameterType, builder.getDictionaryAttr(argumentAttributes), + builder.getLoc()))) { + throw std::runtime_error( + "failed to create a compiler input for a Qiskit parameter"); + } + globalParameters[symbol->name] = function.getArgument(index); + } llvm::SmallVector qubits; qubits.reserve(view->numQubits()); @@ -1607,7 +1829,7 @@ mlir::QCProgram importCircuit(const nb::handle circuit) { std::iota(qubitMap.begin(), qubitMap.end(), 0U); std::iota(clbitMap.begin(), clbitMap.end(), 0U); translateCircuit(builder, *view, qubitMap, clbitMap, qubitMap, clbitMap, - qubits, classicalBits, {}, 0U, 0U); + qubits, classicalBits, {}, globalParameters, 0U, 0U); auto moduleOp = classicalStorage.empty() ? builder.finalize() : builder.finalize(classicalStorage); diff --git a/bindings/mlir/qiskit/QiskitTranslation.h b/bindings/mlir/qiskit/QiskitTranslation.h index a200a7e7d7..67c02b66e9 100644 --- a/bindings/mlir/qiskit/QiskitTranslation.h +++ b/bindings/mlir/qiskit/QiskitTranslation.h @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include namespace mqt::bindings::qiskit { @@ -48,9 +50,100 @@ struct Register { validateRegisterLayout(const std::vector& registers, uint32_t total, std::string_view kind); -struct Parameter { - std::optional number = 0.0; - std::string text; +inline constexpr size_t MAX_PARAMETER_EXPRESSION_DEPTH = 64U; +inline constexpr size_t MAX_PARAMETER_EXPRESSION_NODES = 4096U; + +enum class UnaryParameterKind : uint8_t { + Negate, + Sin, + Cos, + Tan, + ArcSin, + ArcCos, + ArcTan, + Exp, + Log, + Abs, + Conjugate, +}; + +enum class BinaryParameterKind : uint8_t { + Add, + Subtract, + Multiply, + Divide, + Power, +}; + +/** One normalized scalar parameter-expression tree. */ +class Parameter { +public: + struct Number { + double value; + }; + + struct Symbol { + std::string name; + }; + + struct Unary { + UnaryParameterKind operation; + std::shared_ptr operand; + }; + + struct Binary { + BinaryParameterKind operation; + std::shared_ptr left; + std::shared_ptr right; + }; + + Parameter() = default; + + [[nodiscard]] static Parameter number(const double value) { + return Parameter(Number{value}); + } + + [[nodiscard]] static Parameter symbol(std::string name) { + return Parameter(Symbol{std::move(name)}); + } + + [[nodiscard]] static Parameter unary(const UnaryParameterKind operation, + Parameter operand) { + return Parameter(Unary{ + .operation = operation, + .operand = std::make_shared(std::move(operand))}); + } + + [[nodiscard]] static Parameter binary(const BinaryParameterKind operation, + Parameter left, Parameter right) { + return Parameter( + Binary{.operation = operation, + .left = std::make_shared(std::move(left)), + .right = std::make_shared(std::move(right))}); + } + + [[nodiscard]] const Number* getNumber() const { + return std::get_if(&storage); + } + + [[nodiscard]] const Symbol* getSymbol() const { + return std::get_if(&storage); + } + + [[nodiscard]] const Unary* getUnary() const { + return std::get_if(&storage); + } + + [[nodiscard]] const Binary* getBinary() const { + return std::get_if(&storage); + } + +private: + using Value = std::variant; + + explicit Parameter(Value value) : storage(std::move(value)) {} + + Value storage = Number{0.0}; }; enum class GateModifierKind : uint8_t { @@ -62,7 +155,7 @@ enum class GateModifierKind : uint8_t { struct GateModifier { GateModifierKind kind = GateModifierKind::Inverse; uint32_t numControls = 0; - Parameter exponent{}; + Parameter exponent; }; struct StandardGateMapping { @@ -169,7 +262,7 @@ struct Loop { int64_t stop = 0; int64_t step = 1; std::vector values; - std::optional parameter; + std::optional parameter; }; struct SwitchCase { @@ -196,6 +289,8 @@ class CircuitReader { [[nodiscard]] virtual bool hasClassicalVariables() const = 0; [[nodiscard]] virtual Register quantumRegister(size_t index) const = 0; [[nodiscard]] virtual Register classicalRegister(size_t index) const = 0; + /** Return the circuit's free scalar parameters in a stable order. */ + [[nodiscard]] virtual std::vector parameters() const = 0; [[nodiscard]] virtual Parameter globalPhase() const = 0; [[nodiscard]] virtual Instruction instruction(size_t index) const = 0; [[nodiscard]] virtual std::vector> @@ -239,10 +334,10 @@ class CircuitWriter { virtual void addQuantumRegister(std::string_view name, uint32_t size) = 0; virtual void addClassicalRegister(std::string_view name, uint32_t size) = 0; - virtual void setGlobalPhase(double phase) = 0; + virtual void setGlobalPhase(const Parameter& phase) = 0; virtual void addGate(StandardGateMapping gate, const std::vector& qubits, - const std::vector& parameters) = 0; + const std::vector& parameters) = 0; virtual void addMeasure(uint32_t qubit, uint32_t clbit) = 0; virtual void addReset(uint32_t qubit) = 0; virtual void addBarrier(const std::vector& qubits) = 0; diff --git a/docs/mlir/MQT.md b/docs/mlir/MQT.md new file mode 100644 index 0000000000..4f941ea488 --- /dev/null +++ b/docs/mlir/MQT.md @@ -0,0 +1,7 @@ +--- +tocdepth: 3 +--- + +```{include} Dialects/MQTDialect.md + +``` diff --git a/docs/mlir/index.md b/docs/mlir/index.md index 0cca4fecb2..8d6de6b343 100644 --- a/docs/mlir/index.md +++ b/docs/mlir/index.md @@ -12,6 +12,8 @@ technical reference for the underlying MLIR infrastructure. We define multiple dialects, each with its dedicated purpose: +- The {doc}`MQT dialect ` stores frontend-neutral program metadata that + remains meaningful across dialect conversions. - The {doc}`QC dialect ` uses reference semantics and is designed as a compatibility dialect that simplifies translations from and to existing languages such as Qiskit, OpenQASM, or QIR. @@ -37,6 +39,7 @@ directly to QC and emits structured OpenQASM from QC. python_compiler_collection target_compilation +MQT QC QCO QTensor diff --git a/docs/mlir/python_compiler_collection.md b/docs/mlir/python_compiler_collection.md index 695960c9d6..d7f2fbc329 100644 --- a/docs/mlir/python_compiler_collection.md +++ b/docs/mlir/python_compiler_collection.md @@ -173,16 +173,25 @@ program structures than its C API can construct. | Classical-bit and register conditions | Supported | Rejected | | Constant Boolean, `Uint` up to 64 bits, and `Float` expressions | Supported | Rejected | | Standalone classical variables or variable expressions | Rejected | Rejected | -| Free symbolic parameters | Rejected | Rejected | +| Free symbols and supported real parameter expressions | Supported | Supported | +| Parameter-vector elements | Rejected | Not emitted | | Dense numeric unitaries up to eight qubits | Supported | Supported | | Register aliases or interleaved membership | Rejected | Rejected | | Transpiler layout metadata | Accepted and ignored | Not emitted | -Lexically bound {code}`for`-loop induction parameters are supported. Numeric -parameters passed to a custom instruction are bound before its definition is -expanded. Definition expansion rejects missing definitions, cycles, operand -arity mismatches, nesting beyond 64 levels, and more than 10 million expanded -operations. +Free standalone symbols become named {code}`f64` program inputs. +Parameter-vector elements are rejected because converting them to standalone +parameters would change positional binding order. Standalone parameter names +that contain brackets remain ordinary scalar names. Parameter-expression trees +support at most 64 levels and 4,096 nodes. Import and export support real +addition, subtraction, multiplication, division, power, negation, trigonometric +and inverse trigonometric functions, exponential, logarithm, absolute value, and +real conjugation. Other parameter-expression functions are rejected. Lexically +bound {code}`for`-loop induction parameters are supported and remain distinct +from free symbols. Parameterized custom-instruction definitions are expanded +after their symbols and expressions are resolved. Definition expansion rejects +missing definitions, cycles, operand arity mismatches, nesting beyond 64 levels, +and more than 10 million expanded operations. Dense numeric unitaries remain explicit matrix operations during import and export. Target compilation synthesizes supported one- and two-qubit matrices to diff --git a/mlir/include/mlir/Conversion/JeffToQCO/JeffToQCO.td b/mlir/include/mlir/Conversion/JeffToQCO/JeffToQCO.td index 97e4c3c633..f2c5df2fcd 100644 --- a/mlir/include/mlir/Conversion/JeffToQCO/JeffToQCO.td +++ b/mlir/include/mlir/Conversion/JeffToQCO/JeffToQCO.td @@ -26,7 +26,7 @@ def JeffToQCO : Pass<"jeff-to-qco"> { let dependentDialects = ["mlir::arith::ArithDialect", "mlir::cbit::CBitDialect", "mlir::math::MathDialect", - "mlir::scf::SCFDialect", + "mlir::mqt::MQTDialect", "mlir::scf::SCFDialect", "mlir::tensor::TensorDialect", "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 7abd899a79..a56d0b263e 100644 --- a/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td +++ b/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td @@ -19,7 +19,7 @@ def QCToQIRAdaptive : Pass<"qc-to-qir-adaptive"> { Requirements: - Input is a valid module in the QC dialect. - - The entry function must be marked with the `entry_point` attribute. + - The entry function must be marked with `mqt.entry_point`. Behavior: diff --git a/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td b/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td index ad37812fb2..6d15fbb842 100644 --- a/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td +++ b/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td @@ -19,7 +19,7 @@ def QCToQIRBase : Pass<"qc-to-qir-base"> { Requirements: - Input is a valid module in the QC dialect. - - The entry function must be marked with the `entry_point` attribute. + - The entry function must be marked with `mqt.entry_point`. - The input entry function must consist of a single block. Multi-block input functions are currently not supported. - The program must have straight-line control flow (i.e., Base Profile QIR). diff --git a/mlir/include/mlir/Dialect/CBit/IR/CBitOps.td b/mlir/include/mlir/Dialect/CBit/IR/CBitOps.td index 14cf23816d..3d0450f78a 100644 --- a/mlir/include/mlir/Dialect/CBit/IR/CBitOps.td +++ b/mlir/include/mlir/Dialect/CBit/IR/CBitOps.td @@ -57,22 +57,20 @@ def AllocOp : CBitOp<"alloc", [MemoryEffects<[MemAlloc]>]> { let description = [{ Allocates a static classical-bit register. `zero` initialization defines every element as false. Reading an element of an `undefined` register - before a store is undefined behavior. The optional source name records the - spelling used by an input format and does not make the register public. + before a store is undefined behavior. The optional `mqt.register_name` + discardable attribute records the source-level name. Example: ```mlir - %c = cbit.alloc(#cbit.init) source_name = "c" : !cbit.reg<2> + %c = cbit.alloc(#cbit.init) {mqt.register_name = "c"} + : !cbit.reg<2> ``` }]; - let arguments = (ins CBit_InitializationAttr:$initialization, - OptionalAttr:$source_name); + let arguments = (ins CBit_InitializationAttr:$initialization); let results = (outs CBit_RegisterType:$result); let assemblyFormat = [{ - `(` qualified($initialization) `)` - (`source_name` `=` $source_name^)? attr-dict - `:` qualified(type($result)) + `(` qualified($initialization) `)` attr-dict `:` qualified(type($result)) }]; } diff --git a/mlir/include/mlir/Dialect/CMakeLists.txt b/mlir/include/mlir/Dialect/CMakeLists.txt index 387567ff29..04bd1922f4 100644 --- a/mlir/include/mlir/Dialect/CMakeLists.txt +++ b/mlir/include/mlir/Dialect/CMakeLists.txt @@ -7,6 +7,7 @@ # Licensed under the MIT License add_subdirectory(CBit) +add_subdirectory(MQT) add_subdirectory(QC) add_subdirectory(QCO) add_subdirectory(QIR) diff --git a/mlir/include/mlir/Dialect/MQT/CMakeLists.txt b/mlir/include/mlir/Dialect/MQT/CMakeLists.txt new file mode 100644 index 0000000000..b181a84fed --- /dev/null +++ b/mlir/include/mlir/Dialect/MQT/CMakeLists.txt @@ -0,0 +1,9 @@ +# 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 + +add_subdirectory(IR) diff --git a/mlir/include/mlir/Dialect/MQT/IR/CMakeLists.txt b/mlir/include/mlir/Dialect/MQT/IR/CMakeLists.txt new file mode 100644 index 0000000000..d0e04e4d20 --- /dev/null +++ b/mlir/include/mlir/Dialect/MQT/IR/CMakeLists.txt @@ -0,0 +1,14 @@ +# 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 + +set(LLVM_TARGET_DEFINITIONS MQTDialect.td) +mlir_tablegen(MQTDialect.h.inc -gen-dialect-decls -dialect=mqt) +mlir_tablegen(MQTDialect.cpp.inc -gen-dialect-defs -dialect=mqt) +add_mlir_dialect_tablegen_target(MLIRMQTDialectIncGen) + +add_mlir_doc(MQTDialect MQTDialect Dialects/ -gen-dialect-doc -dialect=mqt) diff --git a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h new file mode 100644 index 0000000000..7cf2b9604a --- /dev/null +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h @@ -0,0 +1,37 @@ +/* + * 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 + +//===----------------------------------------------------------------------===// +// Dialect +//===----------------------------------------------------------------------===// + +#include "mlir/Dialect/MQT/IR/MQTDialect.h.inc" // IWYU pragma: export + +namespace mlir::mqt { +/// Return whether an operation is the program entry point. +[[nodiscard]] bool isEntryPoint(Operation* operation); + +/// Mark an operation as the program entry point. +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. +[[nodiscard]] func::FuncOp getEntryPoint(ModuleOp moduleOp); +} // namespace mlir::mqt diff --git a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td new file mode 100644 index 0000000000..5471ec3976 --- /dev/null +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td @@ -0,0 +1,39 @@ +// 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 + +#ifndef MLIR_DIALECT_MQT_IR_MQTDIALECT_TD +#define MLIR_DIALECT_MQT_IR_MQTDIALECT_TD + +include "mlir/IR/OpBase.td" + +def MQTDialect : Dialect { + let name = "mqt"; + let cppNamespace = "::mlir::mqt"; + + let summary = "Shared metadata for MQT quantum programs."; + let description = [{ + The MQT dialect owns frontend-neutral metadata that must remain meaningful + across quantum dialect conversions. It defines no operations or types. + + `mqt.input_name` records the source-level name of a function input. + `mqt.register_name` records the source-level name of a quantum or classical + register allocation. Input and register names share one function-wide + namespace. + `mqt.entry_point` marks the single defined program entry function in a + module. + }]; + + let discardableAttrs = (ins "::mlir::StringAttr":$input_name, + "::mlir::StringAttr":$register_name, "::mlir::UnitAttr":$entry_point); + + let hasOperationAttrVerify = 1; + let hasRegionArgAttrVerify = 1; + let hasRegionResultAttrVerify = 1; +} + +#endif // MLIR_DIALECT_MQT_IR_MQTDIALECT_TD diff --git a/mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h b/mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h index 0cc189a502..a15de4e5cd 100644 --- a/mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h +++ b/mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h @@ -12,7 +12,6 @@ #include "mlir/Dialect/CBit/IR/CBitAttributes.h" -#include #include #include #include @@ -78,7 +77,7 @@ class QCProgramBuilder final : public ImplicitLocOpBuilder { * a default return type of i64. * * @details - * Creates a main function with an entry_point attribute. Must be called + * Creates a main function with an `mqt.entry_point` attribute. Must be called * before adding operations. */ void initialize(); @@ -89,7 +88,7 @@ class QCProgramBuilder final : public ImplicitLocOpBuilder { * @param returnTypes The return types for the main function * * @details - * Creates a main function with an entry_point attribute. Must be called + * Creates a main function with an `mqt.entry_point` attribute. Must be called * before adding operations. */ void initialize(TypeRange returnTypes); @@ -256,7 +255,8 @@ class QCProgramBuilder final : public ImplicitLocOpBuilder { * auto c = builder.allocClassicalBitRegister(3, "c"); * ``` * ```mlir - * %c = cbit.alloc(#cbit.init) source_name = "c" : !cbit.reg<3> + * %c = cbit.alloc(#cbit.init) {mqt.register_name = "c"} + * : !cbit.reg<3> * ``` */ Value allocClassicalBitRegister( @@ -1405,9 +1405,6 @@ class QCProgramBuilder final : public ImplicitLocOpBuilder { /// Track allocated memrefs for automatic deallocation DenseSet allocatedQregs; - /// Track non-empty source-level qubit register names. - llvm::StringSet<> qubitRegisterNames; - /// Check if the builder has been finalized void checkFinalized() const; diff --git a/mlir/include/mlir/Dialect/QC/IR/QCInterfaces.h b/mlir/include/mlir/Dialect/QC/IR/QCInterfaces.h index 8629c7ea31..698f192743 100644 --- a/mlir/include/mlir/Dialect/QC/IR/QCInterfaces.h +++ b/mlir/include/mlir/Dialect/QC/IR/QCInterfaces.h @@ -15,6 +15,12 @@ #include +namespace mlir::qc { + +LogicalResult verifyUnitaryOpInterface(Operation* op); + +} + // clang-format:off #include "mlir/Dialect/QC/IR/QCInterfaces.h.inc" // IWYU pragma: export // clang-format:on diff --git a/mlir/include/mlir/Dialect/QC/IR/QCInterfaces.td b/mlir/include/mlir/Dialect/QC/IR/QCInterfaces.td index e82752a127..5b0e6eddbb 100644 --- a/mlir/include/mlir/Dialect/QC/IR/QCInterfaces.td +++ b/mlir/include/mlir/Dialect/QC/IR/QCInterfaces.td @@ -23,6 +23,10 @@ def UnitaryOpInterface : OpInterface<"UnitaryOpInterface"> { The interface enables uniform introspection and composition capabilities across all unitary operations in the QC dialect. + + Every statically known floating-point value in a parameter expression + must be finite. Dynamic parameter values have the same runtime + precondition. }]; let cppNamespace = "::mlir::qc"; @@ -76,6 +80,10 @@ def UnitaryOpInterface : OpInterface<"UnitaryOpInterface"> { InterfaceMethod<"Returns the base symbol/mnemonic of the operation.", "StringRef", "getBaseSymbol", (ins)>, ]; + + let verify = [{ + return ::mlir::qc::verifyUnitaryOpInterface($_op); + }]; } #endif // MLIR_DIALECT_QC_IR_QCINTERFACES_TD diff --git a/mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h b/mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h index 6f087b86ba..1a68e7e5aa 100644 --- a/mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h +++ b/mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h @@ -14,7 +14,6 @@ #include #include -#include #include #include #include @@ -88,7 +87,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * a default return type of i64. * * @details - * Creates a main function with an entry_point attribute. Must be called + * Creates a main function with an `mqt.entry_point` attribute. Must be called * before adding operations. */ void initialize(); @@ -99,7 +98,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * @param returnTypes The return types for the main function * * @details - * Creates a main function with an entry_point attribute. Must be called + * Creates a main function with an `mqt.entry_point` attribute. Must be called * before adding operations. */ void initialize(TypeRange returnTypes); @@ -351,7 +350,8 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * auto c = builder.allocClassicalBitRegister(3, "c"); * ``` * ```mlir - * %c = cbit.alloc(#cbit.init) source_name = "c" : !cbit.reg<3> + * %c = cbit.alloc(#cbit.init) {mqt.register_name = "c"} + * : !cbit.reg<3> * ``` */ Value allocClassicalBitRegister( @@ -1950,9 +1950,6 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { MLIRContext* ctx{}; Operation* module; - /// Track non-empty source-level qubit register names. - llvm::StringSet<> qubitRegisterNames; - /// Check if the builder has been finalized void checkFinalized() const; diff --git a/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.h b/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.h index 0940cea2af..abb3b7cb7f 100644 --- a/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.h +++ b/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.h @@ -22,6 +22,12 @@ #include #include +namespace mlir::qco { + +LogicalResult verifyUnitaryOpInterface(Operation* op); + +} + // clang-format:off #include "mlir/Dialect/QCO/IR/QCOInterfaces.h.inc" // IWYU pragma: export // clang-format:on diff --git a/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td b/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td index acb2ff00e8..117d5ba279 100644 --- a/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td +++ b/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td @@ -24,6 +24,10 @@ def UnitaryOpInterface : OpInterface<"UnitaryOpInterface"> { The interface enables uniform introspection and composition capabilities across all unitary operations with value semantics. + + Every statically known floating-point value in a parameter expression + must be finite. Dynamic parameter values have the same runtime + precondition. }]; let cppNamespace = "::mlir::qco"; @@ -247,6 +251,10 @@ def UnitaryOpInterface : OpInterface<"UnitaryOpInterface"> { return std::nullopt; } }]; + + let verify = [{ + return ::mlir::qco::verifyUnitaryOpInterface($_op); + }]; } #endif // MLIR_DIALECT_QCO_IR_QCOINTERFACES_TD diff --git a/mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h b/mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h index 962be998e4..aae7fd7bbd 100644 --- a/mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h +++ b/mlir/include/mlir/Dialect/QIR/Utils/QIRUtils.h @@ -139,11 +139,11 @@ void emitQISCall(OpBuilder& builder, Operation* anchor, Location loc, StringRef fnName); /** - * @brief Find the main LLVM function with entry_point attribute + * @brief Find the main LLVM function * * @details - * Searches for the LLVM function marked with the "entry_point" attribute in - * the passthrough attributes. + * Searches first for the MQT program entry-point marker. It also accepts the + * lowered QIR `entry_point` passthrough attribute. * * @param op The module operation to search in * @return The main LLVM function, or nullptr if not found diff --git a/mlir/include/mlir/Dialect/Utils/Utils.h b/mlir/include/mlir/Dialect/Utils/Utils.h index a4b9b6dc24..ed7afbd38d 100644 --- a/mlir/include/mlir/Dialect/Utils/Utils.h +++ b/mlir/include/mlir/Dialect/Utils/Utils.h @@ -11,13 +11,13 @@ #pragma once #include +#include #include #include #include #include #include #include -#include #include #include #include @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -42,10 +43,6 @@ namespace mlir::utils { -/// Attribute used to retain a source-level qubit-register name. -inline constexpr llvm::StringLiteral QUBIT_REGISTER_NAME_ATTR = - "mqt.qubit_register_name"; - /// Check if a floating-point value is an integer. [[nodiscard]] inline bool isIntegerExponent(double r) { return r == std::floor(r) && std::isfinite(r); @@ -169,7 +166,7 @@ variantToValue(OpBuilder& builder, Location loc, /// operands are resolved once (linear in the expression DAG). [[nodiscard]] inline std::optional valueToConstantAttr(Value value, - llvm::DenseMap>& cache) { + DenseMap>& cache) { if (const auto it = cache.find(value); it != cache.end()) { return it->second; } @@ -199,11 +196,10 @@ valueToConstantAttr(Value value, return cache[value] = std::nullopt; } std::optional folded; - if (const auto resultAttr = - llvm::dyn_cast_if_present(results.front())) { + if (const auto resultAttr = dyn_cast_if_present(results.front())) { folded = resultAttr; } else if (const auto resultValue = - llvm::dyn_cast_if_present(results.front())) { + dyn_cast_if_present(results.front())) { // Identity-style folds may return an existing SSA value (e.g. `addf x, // -0`). folded = valueToConstantAttr(resultValue, cache); @@ -213,7 +209,7 @@ valueToConstantAttr(Value value, /// Recursively constant-fold a pure SSA expression DAG to an attribute. [[nodiscard]] inline std::optional valueToConstantAttr(Value value) { - llvm::DenseMap> cache; + DenseMap> cache; return valueToConstantAttr(value, cache); } @@ -229,6 +225,37 @@ valueToConstantAttr(Value value, return std::nullopt; } +/// Verify that each statically known floating-point value in a parameter +/// expression is finite. +[[nodiscard]] inline LogicalResult +verifyFiniteConstantParameters(Operation* op, const ValueRange parameters) { + DenseMap> constantCache; + DenseSet visited; + for (const auto [index, parameter] : llvm::enumerate(parameters)) { + SmallVector worklist{parameter}; + while (!worklist.empty()) { + const Value value = worklist.pop_back_val(); + if (!visited.insert(value).second) { + continue; + } + if (const auto constant = valueToConstantAttr(value, constantCache)) { + if (const auto floating = dyn_cast(*constant); + floating && !floating.getValue().isFinite()) { + return op->emitOpError() << "constant parameter expression at index " + << index << " must be finite"; + } + } + Operation* definingOp = value.getDefiningOp(); + if (definingOp == nullptr || definingOp->getNumRegions() != 0 || + !isPure(definingOp)) { + continue; + } + llvm::append_range(worklist, definingOp->getOperands()); + } + } + return success(); +} + /** * @brief Parse a list of aliased qubits. * @@ -465,35 +492,4 @@ inlineBodyReturningYields(Block& source, ValueRange blockArgReplacements, return yielded; } -/** - * @brief Find the entry point function with the entry_point attribute - * - * @details - * Searches for the function marked with the "entry_point" attribute in - * the passthrough attributes. If multiple functions are marked, returns the - * first one encountered. - * - * @param op The module operation to search in. - * @returns the entry point function, or nullptr if not found. - */ -inline func::FuncOp getEntryPoint(ModuleOp op) { - static constexpr StringRef PASSTHROUGH_LABEL = "passthrough"; - static constexpr StringRef ENTRY_POINT_LABEL = "entry_point"; - - const auto isEntry = [](Attribute attr) { - const auto strAttr = dyn_cast(attr); - return strAttr && strAttr.getValue() == ENTRY_POINT_LABEL; - }; - - for (auto func : op.getOps()) { - if (const auto passthrough = - func->getAttrOfType(PASSTHROUGH_LABEL); - passthrough && llvm::any_of(passthrough, isEntry)) { - return func; - } - } - - return nullptr; -} - } // namespace mlir::utils diff --git a/mlir/lib/Compiler/CMakeLists.txt b/mlir/lib/Compiler/CMakeLists.txt index 566c7b8697..08fa49cf9e 100644 --- a/mlir/lib/Compiler/CMakeLists.txt +++ b/mlir/lib/Compiler/CMakeLists.txt @@ -75,6 +75,8 @@ add_mlir_library( MLIRTargetLLVMIRExport MLIRBuiltinToLLVMIRTranslation MLIRLLVMToLLVMIRTranslation + MLIRMathDialect + MLIRMQTDialect MQTCompilerTarget MQT::MLIRSupport) diff --git a/mlir/lib/Compiler/Programs.cpp b/mlir/lib/Compiler/Programs.cpp index b73d58ff3f..4828575d56 100644 --- a/mlir/lib/Compiler/Programs.cpp +++ b/mlir/lib/Compiler/Programs.cpp @@ -18,6 +18,7 @@ #include "mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.h" #include "mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.h" #include "mlir/Dialect/CBit/IR/CBitDialect.h" +#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/Translation/TranslateQASM3ToQC.h" #include "mlir/Dialect/QC/Translation/TranslateQCToOpenQASM3.h" @@ -46,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -83,10 +85,10 @@ namespace mlir { [[nodiscard]] static std::shared_ptr createCompilerContext() { DialectRegistry registry; - registry.insert(); registerBuiltinDialectTranslation(registry); registerLLVMDialectTranslation(registry); @@ -326,7 +328,7 @@ bool QCProgram::cleanup() { } bool QCProgram::normalizeGlobalPhases() { - return succeeded(mlir::mqt::normalizeGlobalPhases(mod())); + return succeeded(mqt::normalizeGlobalPhases(mod())); } std::optional QCProgram::toOpenQASM3() const { @@ -397,7 +399,7 @@ bool QCOProgram::cleanup() { } bool QCOProgram::normalizeGlobalPhases() { - return succeeded(mlir::mqt::normalizeGlobalPhases(mod())); + return succeeded(mqt::normalizeGlobalPhases(mod())); } bool QCOProgram::runPassPipeline(const std::string_view pipeline, diff --git a/mlir/lib/Conversion/CBitToMemRef/CBitToMemRef.cpp b/mlir/lib/Conversion/CBitToMemRef/CBitToMemRef.cpp index 7abf34c6bf..a9921cc0d5 100644 --- a/mlir/lib/Conversion/CBitToMemRef/CBitToMemRef.cpp +++ b/mlir/lib/Conversion/CBitToMemRef/CBitToMemRef.cpp @@ -53,6 +53,7 @@ struct ConvertAllocOp final : OpConversionPattern { const auto type = cast( getTypeConverter()->convertType(op.getResult().getType())); auto allocation = memref::AllocOp::create(rewriter, op.getLoc(), type); + allocation->setDiscardableAttrs(op->getDiscardableAttrDictionary()); if (op.getInitialization() == cbit::Initialization::Zero) { auto zero = arith::ConstantOp::create(rewriter, op.getLoc(), diff --git a/mlir/lib/Conversion/JeffToQCO/CMakeLists.txt b/mlir/lib/Conversion/JeffToQCO/CMakeLists.txt index eb3cf18ab0..f1a712a5cd 100644 --- a/mlir/lib/Conversion/JeffToQCO/CMakeLists.txt +++ b/mlir/lib/Conversion/JeffToQCO/CMakeLists.txt @@ -17,5 +17,6 @@ add_mlir_conversion_library( MLIRJeff MLIRJeffToNative MLIRCBitDialect + MLIRMQTDialect MLIRQCODialect MLIRTransforms) diff --git a/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp b/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp index a24b62e9eb..b274b6d7d8 100644 --- a/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp +++ b/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp @@ -13,6 +13,7 @@ #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/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" @@ -336,8 +337,8 @@ struct ConvertJeffIntArrayZeroOpToCBit final return rewriter.notifyMatchFailure( op, "CBit array length must match its static result width"); } - rewriter.replaceOpWithNewOp( - op, registerType, cbit::Initialization::Zero, StringAttr{}); + rewriter.replaceOpWithNewOp(op, registerType, + cbit::Initialization::Zero); return success(); } }; @@ -1188,7 +1189,7 @@ struct ConvertJeffYieldOpToQCO final : OpConversionPattern { * ``` * is converted to * ```mlir - * func.func @main() -> i64 attributes {passthrough = ["entry_point"]} { + * func.func @main() -> i64 attributes {mqt.entry_point} { * %0 = arith.constant 0 : i64 * return %0 * } @@ -1229,8 +1230,7 @@ struct ConvertJeffMainToQCO final : OpConversionPattern { resultTypes.push_back(rewriter.getI64Type()); } rewriter.modifyOpInPlace(op, [&] { - op->setAttr("passthrough", rewriter.getArrayAttr( - {rewriter.getStringAttr("entry_point")})); + mqt::setEntryPoint(op); op.setType(rewriter.getFunctionType(inputTypes, resultTypes)); for (const auto& [argument, type] : llvm::zip_equal(block->getArguments(), inputTypes)) { @@ -1303,7 +1303,7 @@ struct JeffToQCO final : impl::JeffToQCOBase { target.addDynamicallyLegalOp([&](func::FuncOp op) { return (op.getSymName() != getEntryPointName(module) || - op->hasAttr("passthrough")) && + mqt::isEntryPoint(op)) && typeConverter.isSignatureLegal(op.getFunctionType()) && typeConverter.isLegal(&op.getBody()); }); diff --git a/mlir/lib/Conversion/QCOToJeff/CMakeLists.txt b/mlir/lib/Conversion/QCOToJeff/CMakeLists.txt index e9f463a316..d04a1f0ea5 100644 --- a/mlir/lib/Conversion/QCOToJeff/CMakeLists.txt +++ b/mlir/lib/Conversion/QCOToJeff/CMakeLists.txt @@ -18,6 +18,7 @@ add_mlir_conversion_library( MLIRCBitDialect MLIRJeff MLIRNativeToJeff + MLIRMQTDialect MLIRMQTTransforms MLIRQCODialect MLIRTransforms) diff --git a/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp b/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp index acee45580a..ca29ebec3e 100644 --- a/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp +++ b/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp @@ -12,6 +12,7 @@ #include "mlir/Dialect/CBit/IR/CBitDialect.h" #include "mlir/Dialect/CBit/IR/CBitOps.h" +#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" @@ -25,7 +26,6 @@ #include #include #include -#include #include #include #include @@ -1644,7 +1644,7 @@ struct ConvertSCFWhileOpToJeff final * * @par Example: * ```mlir - * func.func @main() -> i64 attributes {passthrough = ["entry_point"]} { ... } + * func.func @main() -> i64 attributes {mqt.entry_point} { ... } * ``` * is converted to * ```mlir @@ -1657,15 +1657,7 @@ struct ConvertQCOMainToJeff final : StatefulOpConversionPattern { LogicalResult matchAndRewrite(func::FuncOp op, OpAdaptor /*adaptor*/, ConversionPatternRewriter& rewriter) const override { - auto passthrough = op->getAttrOfType("passthrough"); - if (!passthrough) { - return failure(); - } - - if (!llvm::any_of(passthrough, [](Attribute attr) { - const auto strAttr = dyn_cast(attr); - return strAttr && strAttr.getValue() == "entry_point"; - })) { + if (!mqt::isEntryPoint(op)) { return failure(); } @@ -1699,7 +1691,7 @@ struct ConvertQCOMainToJeff final : StatefulOpConversionPattern { llvm::zip_equal(block->getArguments(), newInputs)) { argument.setType(type); } - op->removeAttr("passthrough"); + mqt::removeEntryPoint(op); rewriter.finalizeOpModification(op); return success(); @@ -1863,7 +1855,7 @@ struct QCOToJeff final : impl::QCOToJeffBase { void runOnOperation() override { MLIRContext* context = &getContext(); auto* moduleOp = getOperation(); - if (failed(mlir::mqt::normalizeGlobalPhases(cast(moduleOp)))) { + if (failed(mqt::normalizeGlobalPhases(cast(moduleOp)))) { signalPassFailure(); return; } @@ -1883,7 +1875,7 @@ struct QCOToJeff final : impl::QCOToJeffBase { target.addLegalDialect(); target.addDynamicallyLegalOp( - [](func::FuncOp op) { return !op->hasAttr("passthrough"); }); + [](func::FuncOp op) { return !mqt::isEntryPoint(op); }); target.addDynamicallyLegalOp([](func::ReturnOp op) { return llvm::none_of(op.getOperandTypes(), [](Type type) { return isa(type); diff --git a/mlir/lib/Conversion/QCOToQC/CMakeLists.txt b/mlir/lib/Conversion/QCOToQC/CMakeLists.txt index c5ead0313c..2fad50932a 100644 --- a/mlir/lib/Conversion/QCOToQC/CMakeLists.txt +++ b/mlir/lib/Conversion/QCOToQC/CMakeLists.txt @@ -21,6 +21,7 @@ add_mlir_conversion_library( MLIRArithDialect MLIRFuncDialect MLIRMemRefDialect + MLIRMQTDialect MLIRTransforms MLIRFuncTransforms) diff --git a/mlir/lib/Conversion/QCOToQC/QCOToQC.cpp b/mlir/lib/Conversion/QCOToQC/QCOToQC.cpp index bf86eb7c77..bbc6ce542b 100644 --- a/mlir/lib/Conversion/QCOToQC/QCOToQC.cpp +++ b/mlir/lib/Conversion/QCOToQC/QCOToQC.cpp @@ -18,7 +18,6 @@ #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" -#include "mlir/Dialect/Utils/Utils.h" #include #include @@ -280,7 +279,6 @@ struct ConvertQTensorAllocOp final auto tensorType = cast(op.getResult().getType()); auto memrefType = MemRefType::get(tensorType.getShape(), qubitType); - const auto registerName = op->getAttr(utils::QUBIT_REGISTER_NAME_ATTR); memref::AllocOp alloc; if (tensorType.hasStaticShape()) { // Static size: no dynamic size operand needed @@ -290,9 +288,7 @@ struct ConvertQTensorAllocOp final alloc = memref::AllocOp::create(rewriter, op.getLoc(), memrefType, op.getSize()); } - if (registerName) { - alloc->setAttr(utils::QUBIT_REGISTER_NAME_ATTR, registerName); - } + alloc->setDiscardableAttrs(op->getDiscardableAttrDictionary()); rewriter.replaceOp(op, alloc.getResult()); return success(); } diff --git a/mlir/lib/Conversion/QCToQCO/CMakeLists.txt b/mlir/lib/Conversion/QCToQCO/CMakeLists.txt index e49fa72677..c4c998c5c0 100644 --- a/mlir/lib/Conversion/QCToQCO/CMakeLists.txt +++ b/mlir/lib/Conversion/QCToQCO/CMakeLists.txt @@ -22,6 +22,7 @@ add_mlir_conversion_library( MLIRFuncDialect MLIRSCFDialect MLIRMemRefDialect + MLIRMQTDialect MLIRTransforms MLIRFuncTransforms) diff --git a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp index 96118dda49..14a384c24f 100644 --- a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp +++ b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp @@ -20,7 +20,6 @@ #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" -#include "mlir/Dialect/Utils/Utils.h" #include #include @@ -840,7 +839,6 @@ struct ConvertMemRefAllocOp final return failure(); } - const auto registerName = op->getAttr(utils::QUBIT_REGISTER_NAME_ATTR); qtensor::AllocOp alloc; if (shape[0] == ShapedType::kDynamic) { alloc = qtensor::AllocOp::create(rewriter, op.getLoc(), @@ -850,9 +848,7 @@ struct ConvertMemRefAllocOp final arith::ConstantIndexOp::create(rewriter, op.getLoc(), shape[0]); alloc = qtensor::AllocOp::create(rewriter, op.getLoc(), size.getResult()); } - if (registerName) { - alloc->setAttr(utils::QUBIT_REGISTER_NAME_ATTR, registerName); - } + alloc->setDiscardableAttrs(op->getDiscardableAttrDictionary()); auto& state = getState(); auto memref = op.getResult(); diff --git a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/CMakeLists.txt b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/CMakeLists.txt index 0f3aa5c00e..f1d10d5746 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/CMakeLists.txt +++ b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/CMakeLists.txt @@ -16,6 +16,7 @@ add_mlir_conversion_library( LINK_LIBS MLIRCBitDialect MLIRQCToQIRCommon + MLIRMQTDialect MLIRMQTTransforms MLIRQIRUtils MLIRLLVMDialect diff --git a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp index 59fbac91d3..9294657c50 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp @@ -685,7 +685,7 @@ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { void runOnOperation() override { MLIRContext* ctx = &getContext(); auto* moduleOp = getOperation(); - if (failed(mlir::mqt::normalizeGlobalPhases(cast(moduleOp)))) { + if (failed(mqt::normalizeGlobalPhases(cast(moduleOp)))) { signalPassFailure(); return; } @@ -730,7 +730,7 @@ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { auto main = getMainFunction(moduleOp); if (!main) { - moduleOp->emitError("No main function with entry_point attribute found"); + moduleOp->emitError("no main function with mqt.entry_point found"); signalPassFailure(); return; } diff --git a/mlir/lib/Conversion/QCToQIR/QIRBase/CMakeLists.txt b/mlir/lib/Conversion/QCToQIR/QIRBase/CMakeLists.txt index daf79f02d3..c5afadd429 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRBase/CMakeLists.txt +++ b/mlir/lib/Conversion/QCToQIR/QIRBase/CMakeLists.txt @@ -17,6 +17,7 @@ add_mlir_conversion_library( MLIRCBitDialect MLIRQIRUtils MLIRQCToQIRCommon + MLIRMQTDialect MLIRMQTTransforms MLIRLLVMDialect MLIRQCDialect diff --git a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp index af368ad30c..87d02a9d21 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp @@ -462,7 +462,7 @@ struct QCToQIRBase final : impl::QCToQIRBaseBase { void runOnOperation() override { MLIRContext* ctx = &getContext(); auto* moduleOp = getOperation(); - if (failed(mlir::mqt::normalizeGlobalPhases(cast(moduleOp)))) { + if (failed(mqt::normalizeGlobalPhases(cast(moduleOp)))) { signalPassFailure(); return; } @@ -494,7 +494,7 @@ struct QCToQIRBase final : impl::QCToQIRBaseBase { auto main = getMainFunction(moduleOp); if (!main) { - moduleOp->emitError("No main function with entry_point attribute found"); + moduleOp->emitError("no main function with mqt.entry_point found"); signalPassFailure(); return; } diff --git a/mlir/lib/Conversion/QCToQIR/QIRCommon/CMakeLists.txt b/mlir/lib/Conversion/QCToQIR/QIRCommon/CMakeLists.txt index 34b65564db..8ae2f7383f 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRCommon/CMakeLists.txt +++ b/mlir/lib/Conversion/QCToQIR/QIRCommon/CMakeLists.txt @@ -24,6 +24,7 @@ add_mlir_library( MLIRControlFlowToLLVM MLIRArithToLLVM MLIRMemRefDialect + MLIRMQTDialect MLIRFuncDialect MLIRControlFlowDialect MLIRArithDialect diff --git a/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp b/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp index 447ce2ac45..c9e619b779 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp @@ -12,11 +12,11 @@ #include "mlir/Dialect/CBit/IR/CBitDialect.h" #include "mlir/Dialect/CBit/IR/CBitOps.h" +#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/IR/QCOps.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" -#include #include #include #include @@ -435,16 +435,7 @@ LogicalResult prepareClassicalResults(Operation* moduleOp, bool hasInvalidMemory = false; SmallVector consumedStores; moduleOp->walk([&](func::FuncOp funcOp) { - // Check whether the given function is the main entrypoint - auto passthrough = funcOp->getAttrOfType("passthrough"); - bool isEntryPoint = false; - if (passthrough) { - isEntryPoint = llvm::any_of(passthrough, [](Attribute attr) { - auto strAttr = dyn_cast(attr); - return strAttr && strAttr.getValue() == "entry_point"; - }); - } - if (!isEntryPoint) { + if (!mqt::isEntryPoint(funcOp)) { return; } @@ -467,7 +458,8 @@ LogicalResult prepareClassicalResults(Operation* moduleOp, } auto& reg = state.cregs[it->second]; reg.record = false; - if (const auto name = allocOp.getSourceNameAttr()) { + if (const auto name = allocOp->getAttrOfType( + mqt::MQTDialect::RegisterNameAttrHelper::getNameStr())) { reg.label = name.str(); } const auto size = allocOp.getResult().getType().getWidth(); diff --git a/mlir/lib/Dialect/CMakeLists.txt b/mlir/lib/Dialect/CMakeLists.txt index 693bcc2fe3..89f90c85c5 100644 --- a/mlir/lib/Dialect/CMakeLists.txt +++ b/mlir/lib/Dialect/CMakeLists.txt @@ -7,6 +7,7 @@ # Licensed under the MIT License add_subdirectory(CBit) +add_subdirectory(MQT) add_subdirectory(QCO) add_subdirectory(QIR) add_subdirectory(QC) diff --git a/mlir/lib/Dialect/MQT/CMakeLists.txt b/mlir/lib/Dialect/MQT/CMakeLists.txt new file mode 100644 index 0000000000..b181a84fed --- /dev/null +++ b/mlir/lib/Dialect/MQT/CMakeLists.txt @@ -0,0 +1,9 @@ +# 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 + +add_subdirectory(IR) diff --git a/mlir/lib/Dialect/MQT/IR/CMakeLists.txt b/mlir/lib/Dialect/MQT/IR/CMakeLists.txt new file mode 100644 index 0000000000..871f7e2283 --- /dev/null +++ b/mlir/lib/Dialect/MQT/IR/CMakeLists.txt @@ -0,0 +1,44 @@ +# 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 + +add_mlir_dialect_library( + MLIRMQTDialect + MQTDialect.cpp + ADDITIONAL_HEADER_DIRS + ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Dialect/MQT + DEPENDS + MLIRMQTDialectIncGen + LINK_LIBS + PRIVATE + MLIRCBitDialect + MLIRFuncDialect + MLIRIR + MLIRMemRefDialect + MLIRQCDialect + MLIRQCODialect + MLIRQTensorDialect) + +mqt_mlir_target_use_project_options(MLIRMQTDialect) + +file(GLOB_RECURSE IR_HEADERS_SOURCE "${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Dialect/MQT/IR/*.h") +file(GLOB_RECURSE IR_HEADERS_BUILD "${MQT_MLIR_BUILD_INCLUDE_DIR}/mlir/Dialect/MQT/IR/*.inc") + +target_sources( + MLIRMQTDialect + PUBLIC FILE_SET + HEADERS + BASE_DIRS + ${MQT_MLIR_SOURCE_INCLUDE_DIR} + FILES + ${IR_HEADERS_SOURCE} + FILE_SET + HEADERS + BASE_DIRS + ${MQT_MLIR_BUILD_INCLUDE_DIR} + FILES + ${IR_HEADERS_BUILD}) diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp new file mode 100644 index 0000000000..f1da715d93 --- /dev/null +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/MQT/IR/MQTDialect.h" + +#include "mlir/Dialect/CBit/IR/CBitOps.h" +#include "mlir/Dialect/QC/IR/QCDialect.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QTensor/IR/QTensorOps.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace mlir; +using namespace mlir::mqt; + +#include "mlir/Dialect/MQT/IR/MQTDialect.cpp.inc" + +void MQTDialect::initialize() {} + +[[nodiscard]] static LogicalResult +verifyEntryPoint(Operation* operation, const NamedAttribute attribute) { + if (!isa(attribute.getValue())) { + return operation->emitError() + << "attribute '" << attribute.getName().getValue() + << "' must be a unit attribute"; + } + + auto function = dyn_cast(operation); + auto moduleOp = operation->getParentOfType(); + if (!function || !moduleOp || + operation->getParentOp() != moduleOp.getOperation() || + function.getFunctionBody().empty()) { + return operation->emitError() + << "attribute '" << attribute.getName().getValue() + << "' 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) { + const auto name = dyn_cast(attribute.getValue()); + if (!name) { + return operation->emitError() + << "attribute '" << attribute.getName().getValue() + << "' must be a string"; + } + if (name.getValue().empty()) { + return operation->emitError() + << "attribute '" << attribute.getName().getValue() + << "' must not be empty"; + } + if (name.getValue().contains('\0')) { + return operation->emitError() + << "attribute '" << attribute.getName().getValue() + << "' must not contain a null character"; + } + return success(); +} + +[[nodiscard]] static bool isRegisterAllocation(Operation* operation) { + if (isa(operation)) { + return true; + } + if (auto alloc = dyn_cast(operation)) { + const auto type = alloc.getType(); + return type.getRank() == 1 && (isa(type.getElementType()) || + type.getElementType().isInteger(1)); + } + if (auto alloc = dyn_cast(operation)) { + const auto type = cast(alloc.getType()); + return type.getRank() == 1 && isa(type.getElementType()); + } + return false; +} + +[[nodiscard]] static LogicalResult +verifyRegisterName(Operation* operation, const NamedAttribute attribute) { + if (failed(verifyName(operation, attribute))) { + return failure(); + } + if (!isRegisterAllocation(operation)) { + return operation->emitError() + << "attribute '" << attribute.getName().getValue() + << "' requires a rank-one quantum or classical register allocation"; + } + + auto function = operation->getParentOfType(); + if (!function || function.getFunctionBody().empty() || + operation->getBlock() != &function.getFunctionBody().front()) { + return operation->emitError() + << "attribute '" << attribute.getName().getValue() + << "' 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) { + if (attribute.getName() == EntryPointAttrHelper::getNameStr()) { + return verifyEntryPoint(operation, attribute); + } + if (attribute.getName() == RegisterNameAttrHelper::getNameStr()) { + return verifyRegisterName(operation, attribute); + } + if (attribute.getName() == InputNameAttrHelper::getNameStr()) { + return operation->emitError() + << "attribute '" << attribute.getName().getValue() + << "' is only valid on a function argument"; + } + return operation->emitError() + << "unknown MQT attribute '" << attribute.getName().getValue() << "'"; +} + +LogicalResult MQTDialect::verifyRegionArgAttribute( + Operation* operation, const unsigned regionIndex, const unsigned argIndex, + const NamedAttribute attribute) { + if (attribute.getName() != InputNameAttrHelper::getNameStr()) { + return operation->emitError() + << "attribute '" << attribute.getName().getValue() + << "' is not valid on a region argument"; + } + if (failed(verifyName(operation, attribute))) { + return failure(); + } + + auto function = dyn_cast(operation); + if (!function || regionIndex != 0) { + return operation->emitError() + << "attribute '" << attribute.getName().getValue() + << "' requires a function entry-block argument"; + } + + const auto name = cast(attribute.getValue()); + for (unsigned index = 0; index < function.getNumArguments(); ++index) { + if (index == argIndex) { + continue; + } + if (function.getArgAttrOfType(index, attribute.getName()) == + name) { + return operation->emitError() + << "duplicate program name '" << name.getValue() << "'"; + } + } + if (!function.getFunctionBody().empty()) { + for (Operation& candidate : function.getFunctionBody().front()) { + if (candidate.getAttrOfType( + RegisterNameAttrHelper::getNameStr()) == name) { + 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"; +} + +bool mlir::mqt::isEntryPoint(Operation* operation) { + return operation != nullptr && + operation->hasAttr(MQTDialect::EntryPointAttrHelper::getNameStr()); +} + +void mlir::mqt::setEntryPoint(Operation* operation) { + operation->setAttr(MQTDialect::EntryPointAttrHelper::getNameStr(), + UnitAttr::get(operation->getContext())); +} + +void mlir::mqt::removeEntryPoint(Operation* operation) { + operation->removeAttr(MQTDialect::EntryPointAttrHelper::getNameStr()); +} + +func::FuncOp mlir::mqt::getEntryPoint(ModuleOp moduleOp) { + for (auto function : moduleOp.getOps()) { + if (isEntryPoint(function)) { + return function; + } + } + return nullptr; +} diff --git a/mlir/lib/Dialect/QC/Builder/CMakeLists.txt b/mlir/lib/Dialect/QC/Builder/CMakeLists.txt index 7098f2f949..e88124e02e 100644 --- a/mlir/lib/Dialect/QC/Builder/CMakeLists.txt +++ b/mlir/lib/Dialect/QC/Builder/CMakeLists.txt @@ -15,6 +15,7 @@ add_mlir_library( MLIRCBitDialect MLIRFuncDialect MLIRMemRefDialect + MLIRMQTDialect MLIRSCFDialect MLIRQCDialect) diff --git a/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp b/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp index 48f98c47e1..5034956482 100644 --- a/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp +++ b/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp @@ -13,6 +13,7 @@ #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/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/IR/QCOps.h" #include "mlir/Dialect/Utils/Utils.h" @@ -47,7 +48,7 @@ QCProgramBuilder::QCProgramBuilder(MLIRContext* context) : ImplicitLocOpBuilder( FileLineColLoc::get(context, "", 1, 1), context), ctx(context), module(ModuleOp::create(*this)) { - ctx->loadDialect(); + ctx->loadDialect(); } void QCProgramBuilder::initialize() { initialize({getI64Type()}); } @@ -60,9 +61,7 @@ void QCProgramBuilder::initialize(TypeRange returnTypes) { auto funcType = getFunctionType({}, returnTypes); auto mainFunc = func::FuncOp::create(*this, "main", funcType); - // Add entry_point attribute to identify the main function - auto entryPointAttr = getStringAttr("entry_point"); - mainFunc->setAttr("passthrough", getArrayAttr({entryPointAttr})); + mqt::setEntryPoint(mainFunc); // Create entry block and set insertion point auto& entryBlock = mainFunc.getBody().emplaceBlock(); @@ -70,7 +69,7 @@ void QCProgramBuilder::initialize(TypeRange returnTypes) { } void QCProgramBuilder::retype(TypeRange returnTypes) { - auto mainFunc = getEntryPoint(mlir::cast(module)); + auto mainFunc = mqt::getEntryPoint(cast(module)); if (!mainFunc) { llvm::reportFatalUsageError("Main function not found for retyping"); } @@ -140,14 +139,12 @@ Value QCProgramBuilder::allocQubitRegisterStorage(const int64_t size, if (size <= 0) { llvm::reportFatalUsageError("Size must be positive"); } - if (!name.empty() && !qubitRegisterNames.insert(name).second) { - llvm::reportFatalUsageError("Qubit register names must be unique"); - } - auto memrefType = MemRefType::get({size}, QubitType::get(ctx)); auto alloc = memref::AllocOp::create(*this, memrefType); if (!name.empty()) { - alloc->setAttr(QUBIT_REGISTER_NAME_ATTR, getStringAttr(name)); + ctx->getLoadedDialect() + ->getRegisterNameAttrHelper() + .setAttr(alloc, getStringAttr(name)); } auto memref = alloc.getResult(); allocatedQregs.insert(memref); @@ -169,9 +166,13 @@ Value QCProgramBuilder::allocClassicalBitRegister( } const auto type = cbit::RegisterType::get(ctx, size); - const auto nameAttr = name.empty() ? StringAttr{} : getStringAttr(name); - return cbit::AllocOp::create(*this, type, initialization, nameAttr) - .getResult(); + auto alloc = cbit::AllocOp::create(*this, type, initialization); + if (!name.empty()) { + ctx->getLoadedDialect() + ->getRegisterNameAttrHelper() + .setAttr(alloc, getStringAttr(name)); + } + return alloc.getResult(); } Value QCProgramBuilder::loadClassicalBit( diff --git a/mlir/lib/Dialect/QC/IR/Operations/StandardGates/GPhaseOp.cpp b/mlir/lib/Dialect/QC/IR/Operations/StandardGates/GPhaseOp.cpp index 931b002a47..5a010790e2 100644 --- a/mlir/lib/Dialect/QC/IR/Operations/StandardGates/GPhaseOp.cpp +++ b/mlir/lib/Dialect/QC/IR/Operations/StandardGates/GPhaseOp.cpp @@ -15,6 +15,7 @@ #include #include +#include #include using namespace mlir; @@ -28,11 +29,10 @@ void GPhaseOp::build(OpBuilder& odsBuilder, OperationState& odsState, } LogicalResult GPhaseOp::verify() { - const auto theta = valueToDouble(getTheta()); - if (theta && !isValidGlobalPhaseAngle(*theta)) { - return emitOpError() - << "constant angle must be finite and have magnitude at most " - << MAX_GLOBAL_PHASE_ANGLE << " radians"; + const auto theta = valueToConstantDouble(getTheta()); + if (theta && std::abs(*theta) > MAX_GLOBAL_PHASE_ANGLE) { + return emitOpError() << "constant angle must have magnitude at most " + << MAX_GLOBAL_PHASE_ANGLE << " radians"; } return success(); } diff --git a/mlir/lib/Dialect/QC/IR/QCOps.cpp b/mlir/lib/Dialect/QC/IR/QCOps.cpp index 6a72833861..19378d60b1 100644 --- a/mlir/lib/Dialect/QC/IR/QCOps.cpp +++ b/mlir/lib/Dialect/QC/IR/QCOps.cpp @@ -72,6 +72,11 @@ void QCDialect::initialize() { // Interfaces //===----------------------------------------------------------------------===// +LogicalResult mlir::qc::verifyUnitaryOpInterface(Operation* op) { + return utils::verifyFiniteConstantParameters( + op, cast(op).getParameters()); +} + #include "mlir/Dialect/QC/IR/QCInterfaces.cpp.inc" //===----------------------------------------------------------------------===// diff --git a/mlir/lib/Dialect/QC/Transforms/ShrinkQubitRegisters.cpp b/mlir/lib/Dialect/QC/Transforms/ShrinkQubitRegisters.cpp index 583f5ff135..c4f09ee4a8 100644 --- a/mlir/lib/Dialect/QC/Transforms/ShrinkQubitRegisters.cpp +++ b/mlir/lib/Dialect/QC/Transforms/ShrinkQubitRegisters.cpp @@ -124,6 +124,7 @@ struct ShrinkQubitRegister final : OpRewritePattern { memRefType.getElementType()); auto newAlloc = memref::AllocOp::create(rewriter, allocOp.getLoc(), newMemRefType); + newAlloc->setDiscardableAttrs(allocOp->getDiscardableAttrDictionary()); for (auto loadOp : loadOps) { if (loadOp.getResult().use_empty()) { diff --git a/mlir/lib/Dialect/QC/Translation/CMakeLists.txt b/mlir/lib/Dialect/QC/Translation/CMakeLists.txt index 49aa67837c..f50d117870 100644 --- a/mlir/lib/Dialect/QC/Translation/CMakeLists.txt +++ b/mlir/lib/Dialect/QC/Translation/CMakeLists.txt @@ -31,6 +31,7 @@ add_mlir_library( MLIRControlFlowDialect MLIRMathDialect MLIRMemRefDialect + MLIRMQTDialect MLIRSCFDialect MLIRUBDialect MLIRQCDialect diff --git a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp index c85875311e..4ed1f88b1b 100644 --- a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp +++ b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp @@ -13,10 +13,10 @@ #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/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/IR/QCInterfaces.h" #include "mlir/Dialect/QC/IR/QCOps.h" -#include "mlir/Dialect/Utils/Utils.h" #include "mlir/Target/OpenQASM/GateCatalog.h" #include @@ -296,9 +296,9 @@ class OpenQASMEmitter { if (auto alloc = dyn_cast(&operation)) { const auto type = alloc.getResult().getType(); const bool isOutput = returnedRegisters.contains(alloc.getResult()); - const auto requested = alloc.getSourceNameAttr() - ? alloc.getSourceNameAttr().getValue() - : StringRef{}; + const auto name = alloc->getAttrOfType( + mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()); + const auto requested = name ? name.getValue() : StringRef{}; Resource resource{.kind = ResourceKind::Bit, .name = isOutput ? outputName(requested) : uniqueName("c", nextBit), @@ -324,7 +324,7 @@ class OpenQASMEmitter { } StringRef requested; if (const auto attr = alloc->getAttrOfType( - utils::QUBIT_REGISTER_NAME_ATTR)) { + mqt::MQTDialect::RegisterNameAttrHelper::getNameStr())) { requested = attr.getValue(); } Resource resource{.kind = ResourceKind::Qubit, diff --git a/mlir/lib/Dialect/QCO/Builder/CMakeLists.txt b/mlir/lib/Dialect/QCO/Builder/CMakeLists.txt index 0a32858f2f..344c9abe83 100644 --- a/mlir/lib/Dialect/QCO/Builder/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Builder/CMakeLists.txt @@ -14,6 +14,7 @@ add_mlir_library( MLIRArithDialect MLIRCBitDialect MLIRFuncDialect + MLIRMQTDialect MLIRSCFDialect MLIRQCODialect MLIRQTensorDialect) diff --git a/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp b/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp index 176648d344..e0056cce32 100644 --- a/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp +++ b/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp @@ -13,6 +13,7 @@ #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/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/QCOUtils.h" @@ -55,22 +56,21 @@ QCOProgramBuilder::QCOProgramBuilder(MLIRContext* context) : ImplicitLocOpBuilder( FileLineColLoc::get(context, "", 1, 1), context), ctx(context), module(ModuleOp::create(*this)) { - ctx->loadDialect(); + ctx->loadDialect(); } void QCOProgramBuilder::initialize() { initialize({getI64Type()}); } void QCOProgramBuilder::initialize(TypeRange returnTypes) { // Set insertion point to the module body - setInsertionPointToStart(mlir::cast(module).getBody()); + setInsertionPointToStart(cast(module).getBody()); // Create main function as entry point auto funcType = getFunctionType({}, returnTypes); auto mainFunc = func::FuncOp::create(*this, "main", funcType); - // Add entry_point attribute to identify the main function - auto entryPointAttr = getStringAttr("entry_point"); - mainFunc->setAttr("passthrough", getArrayAttr({entryPointAttr})); + mqt::setEntryPoint(mainFunc); // Create entry block and set insertion point auto& entryBlock = mainFunc.getBody().emplaceBlock(); @@ -78,7 +78,7 @@ void QCOProgramBuilder::initialize(TypeRange returnTypes) { } void QCOProgramBuilder::retype(TypeRange returnTypes) { - auto mainFunc = getEntryPoint(mlir::cast(module)); + auto mainFunc = mqt::getEntryPoint(cast(module)); if (!mainFunc) { llvm::reportFatalUsageError("Main function not found for retyping"); } @@ -143,14 +143,11 @@ QCOProgramBuilder::allocQubitRegister(const int64_t size, if (size <= 0) { llvm::reportFatalUsageError("Size must be positive"); } - if (!name.empty() && !qubitRegisterNames.insert(name).second) { - llvm::reportFatalUsageError("Qubit register names must be unique"); - } - auto qtensor = qtensorAlloc(size); if (!name.empty()) { - qtensor.getDefiningOp()->setAttr(QUBIT_REGISTER_NAME_ATTR, - getStringAttr(name)); + ctx->getLoadedDialect() + ->getRegisterNameAttrHelper() + .setAttr(qtensor.getDefiningOp(), getStringAttr(name)); } SmallVector qubits; @@ -174,9 +171,13 @@ Value QCOProgramBuilder::allocClassicalBitRegister( } const auto type = cbit::RegisterType::get(ctx, size); - const auto nameAttr = name.empty() ? StringAttr{} : getStringAttr(name); - return cbit::AllocOp::create(*this, type, initialization, nameAttr) - .getResult(); + auto alloc = cbit::AllocOp::create(*this, type, initialization); + if (!name.empty()) { + ctx->getLoadedDialect() + ->getRegisterNameAttrHelper() + .setAttr(alloc, getStringAttr(name)); + } + return alloc.getResult(); } Value QCOProgramBuilder::loadClassicalBit( diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/GPhaseOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/GPhaseOp.cpp index 50b6a08453..b12a6dc03e 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/GPhaseOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/GPhaseOp.cpp @@ -57,11 +57,10 @@ void GPhaseOp::build(OpBuilder& odsBuilder, OperationState& odsState, } LogicalResult GPhaseOp::verify() { - const auto theta = valueToDouble(getTheta()); - if (theta && !isValidGlobalPhaseAngle(*theta)) { - return emitOpError() - << "constant angle must be finite and have magnitude at most " - << MAX_GLOBAL_PHASE_ANGLE << " radians"; + const auto theta = valueToConstantDouble(getTheta()); + if (theta && std::abs(*theta) > MAX_GLOBAL_PHASE_ANGLE) { + return emitOpError() << "constant angle must have magnitude at most " + << MAX_GLOBAL_PHASE_ANGLE << " radians"; } return success(); } diff --git a/mlir/lib/Dialect/QCO/IR/QCOOps.cpp b/mlir/lib/Dialect/QCO/IR/QCOOps.cpp index dae57f52c9..d6e30eda5b 100644 --- a/mlir/lib/Dialect/QCO/IR/QCOOps.cpp +++ b/mlir/lib/Dialect/QCO/IR/QCOOps.cpp @@ -506,6 +506,11 @@ void QCODialect::initialize() { // Interfaces //===----------------------------------------------------------------------===// +LogicalResult mlir::qco::verifyUnitaryOpInterface(Operation* op) { + return utils::verifyFiniteConstantParameters( + op, cast(op).getParameters()); +} + #include "mlir/Dialect/QCO/IR/QCOInterfaces.cpp.inc" //===----------------------------------------------------------------------===// diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index a7adcd0cb4..84d620e383 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -20,6 +20,7 @@ add_mlir_library( MLIRQTensorUtils MLIRArithDialect MLIRMathDialect + MLIRMQTDialect MLIRMQTTransforms MLIRSCFUtils DEPENDS diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index afc13bdfbf..ef372562c2 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -11,6 +11,7 @@ #include "mlir/Dialect/QCO/Transforms/Mapping/Mapping.h" #include "mlir/Compiler/Target.h" +#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" @@ -20,7 +21,6 @@ #include "mlir/Dialect/QCO/Utils/WireIterator.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" #include "mlir/Dialect/QTensor/Utils/TensorIterator.h" -#include "mlir/Dialect/Utils/Utils.h" #include #include @@ -64,7 +64,6 @@ namespace mlir::qco { using namespace mlir::qtensor; -using namespace mlir::utils; #define GEN_PASS_DEF_MAPPINGPASS #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" @@ -358,7 +357,7 @@ struct MappingPass : impl::MappingPassBase { IRRewriter rewriter(&getContext()); auto mod = getOperation(); - auto func = getEntryPoint(mod); + auto func = mqt::getEntryPoint(mod); if (!func) { mod.emitError() << "does not contain an entry point function"; signalPassFailure(); @@ -410,7 +409,7 @@ struct MappingPass : impl::MappingPassBase { numSwaps += stats.nswaps; // Fix SSA Dominance issues. - for_each(body.getBlocks(), [](Block& b) { sortTopologically(&b); }); + llvm::for_each(body.getBlocks(), [](Block& b) { sortTopologically(&b); }); } private: diff --git a/mlir/lib/Dialect/QIR/Transforms/AttachQIRAttributes.cpp b/mlir/lib/Dialect/QIR/Transforms/AttachQIRAttributes.cpp index 1a496b5cfb..c46d81b188 100644 --- a/mlir/lib/Dialect/QIR/Transforms/AttachQIRAttributes.cpp +++ b/mlir/lib/Dialect/QIR/Transforms/AttachQIRAttributes.cpp @@ -8,6 +8,7 @@ * Licensed under the MIT License */ +#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/QIR/Transforms/Passes.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" @@ -119,6 +120,7 @@ struct QIRSetAttributesAndMetadata final {"required_num_results", std::to_string(metadata.numResults)})}; main->setAttr("passthrough", rewriter.getArrayAttr(attributes)); + mqt::removeEntryPoint(main); rewriter.setInsertionPointToEnd(m.getBody()); diff --git a/mlir/lib/Dialect/QIR/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QIR/Transforms/CMakeLists.txt index 95afa87e3f..3431b2aadc 100644 --- a/mlir/lib/Dialect/QIR/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QIR/Transforms/CMakeLists.txt @@ -14,6 +14,7 @@ add_mlir_library( LINK_LIBS PRIVATE MLIRLLVMDialect + MLIRMQTDialect MLIRQIRUtils DEPENDS MLIRQIRTransformsIncGen) diff --git a/mlir/lib/Dialect/QIR/Utils/CMakeLists.txt b/mlir/lib/Dialect/QIR/Utils/CMakeLists.txt index 48257df8c9..f97d7088b9 100644 --- a/mlir/lib/Dialect/QIR/Utils/CMakeLists.txt +++ b/mlir/lib/Dialect/QIR/Utils/CMakeLists.txt @@ -15,6 +15,7 @@ add_mlir_library( PUBLIC LLVMCore MLIRLLVMDialect + MLIRMQTDialect MLIRIR) mqt_mlir_target_use_project_options(MLIRQIRUtils) diff --git a/mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp b/mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp index 6f7ffc5f15..3367544635 100644 --- a/mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp +++ b/mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp @@ -10,6 +10,8 @@ #include "mlir/Dialect/QIR/Utils/QIRUtils.h" +#include "mlir/Dialect/MQT/IR/MQTDialect.h" + #include #include #include @@ -332,8 +334,10 @@ LLVM::LLVMFuncOp getMainFunction(Operation* op) { return nullptr; } - // Search for function with entry_point attribute for (const auto funcOp : moduleOp.getOps()) { + if (mqt::isEntryPoint(funcOp)) { + return funcOp; + } auto passthrough = funcOp->getAttrOfType("passthrough"); if (!passthrough) { continue; diff --git a/mlir/lib/Dialect/QTensor/Transforms/ShrinkRegisters.cpp b/mlir/lib/Dialect/QTensor/Transforms/ShrinkRegisters.cpp index 33146caed4..959009f007 100644 --- a/mlir/lib/Dialect/QTensor/Transforms/ShrinkRegisters.cpp +++ b/mlir/lib/Dialect/QTensor/Transforms/ShrinkRegisters.cpp @@ -171,6 +171,7 @@ struct ShrinkStaticQTensor final : OpRewritePattern { arith::ConstantIndexOp::create(rewriter, allocOp.getLoc(), newSize); auto newAlloc = AllocOp::create(rewriter, allocOp.getLoc(), size.getResult()); + newAlloc->setDiscardableAttrs(allocOp->getDiscardableAttrDictionary()); auto oldTensor = allocOp.getResult(); auto currentTensor = newAlloc.getResult(); diff --git a/mlir/tools/mqt-cc/CMakeLists.txt b/mlir/tools/mqt-cc/CMakeLists.txt index 196823bde4..deacb9e471 100644 --- a/mlir/tools/mqt-cc/CMakeLists.txt +++ b/mlir/tools/mqt-cc/CMakeLists.txt @@ -25,6 +25,7 @@ target_link_libraries( MLIRJeffToQCO MLIRBytecodeWriter MLIRMathDialect + MLIRMQTDialect MLIRQIRUtils MLIRTargetLLVMIRExport MLIRBuiltinToLLVMIRTranslation diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index 2402b61a0f..2962b12bce 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -17,6 +17,7 @@ #include "mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.h" #include "mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.h" #include "mlir/Dialect/CBit/IR/CBitDialect.h" +#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/Translation/TranslateQASM3ToQC.h" #include "mlir/Dialect/QC/Translation/TranslateQCToOpenQASM3.h" @@ -451,11 +452,12 @@ static int runCompiler(int argc, char** argv) { // Set up MLIR context with all required dialects DialectRegistry registry; - registry.insert(); + registry + .insert(); registerBuiltinDialectTranslation(registry); registerLLVMDialectTranslation(registry); diff --git a/mlir/unittests/Conversion/CBitToMemRef/CMakeLists.txt b/mlir/unittests/Conversion/CBitToMemRef/CMakeLists.txt index 18d00e753e..7b32b1bebd 100644 --- a/mlir/unittests/Conversion/CBitToMemRef/CMakeLists.txt +++ b/mlir/unittests/Conversion/CBitToMemRef/CMakeLists.txt @@ -16,6 +16,7 @@ target_link_libraries( MLIRCBitToMemRef MLIRFuncDialect MLIRMemRefDialect + MLIRMQTDialect MLIRParser MLIRSCFDialect MLIRSupport) diff --git a/mlir/unittests/Conversion/CBitToMemRef/test_cbit_to_memref.cpp b/mlir/unittests/Conversion/CBitToMemRef/test_cbit_to_memref.cpp index 36a51976e1..39723e68f7 100644 --- a/mlir/unittests/Conversion/CBitToMemRef/test_cbit_to_memref.cpp +++ b/mlir/unittests/Conversion/CBitToMemRef/test_cbit_to_memref.cpp @@ -16,6 +16,7 @@ #include "mlir/Conversion/CBitToMemRef/CBitToMemRef.h" #include "mlir/Dialect/CBit/IR/CBitDialect.h" #include "mlir/Dialect/CBit/IR/CBitOps.h" +#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include #include @@ -48,7 +49,7 @@ class CBitToMemRefTest : public ::testing::Test { void SetUp() override { DialectRegistry registry; registry.insert(); + memref::MemRefDialect, mqt::MQTDialect, scf::SCFDialect>(); context = std::make_unique(registry); context->loadAllAvailableDialects(); } @@ -73,7 +74,8 @@ TEST_F(CBitToMemRefTest, LowersInitializationLoadsAndStores) { func.func @main() -> (!cbit.reg<2>, !cbit.reg<1>) { %c0 = arith.constant 0 : index %true = arith.constant true - %zero = cbit.alloc(#cbit.init) : !cbit.reg<2> + %zero = cbit.alloc(#cbit.init) {mqt.register_name = "result"} + : !cbit.reg<2> %undefined = cbit.alloc(#cbit.init) : !cbit.reg<1> cbit.store %true, %undefined[%c0] : !cbit.reg<1> %bit = cbit.load %undefined[%c0] : !cbit.reg<1> @@ -88,14 +90,23 @@ TEST_F(CBitToMemRefTest, LowersInitializationLoadsAndStores) { moduleOp->walk([&](cbit::AllocOp) { containsCBit = true; }); EXPECT_FALSE(containsCBit); size_t allocations = 0; + StringAttr registerName; size_t stores = 0; size_t loads = 0; - moduleOp->walk([&](memref::AllocOp) { ++allocations; }); + moduleOp->walk([&](memref::AllocOp alloc) { + ++allocations; + if (const auto name = alloc->getAttrOfType( + mqt::MQTDialect::RegisterNameAttrHelper::getNameStr())) { + registerName = name; + } + }); moduleOp->walk([&](memref::StoreOp) { ++stores; }); moduleOp->walk([&](memref::LoadOp) { ++loads; }); EXPECT_EQ(allocations, 2); EXPECT_EQ(stores, 3); EXPECT_EQ(loads, 1); + ASSERT_TRUE(registerName); + EXPECT_EQ(registerName.getValue(), "result"); } TEST_F(CBitToMemRefTest, ConvertsFunctionSignaturesCallsAndReturns) { diff --git a/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp b/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp index d380dac45d..268f60c9c6 100644 --- a/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp +++ b/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp @@ -13,6 +13,7 @@ #include "mlir/Conversion/QCOToJeff/QCOToJeff.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/QCO/Builder/QCOProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" @@ -83,9 +84,9 @@ class JeffRoundTripTest : public testing::TestWithParam { void SetUp() override { // Register all necessary dialects DialectRegistry registry; - registry.insert(); + registry.insert(); context = std::make_unique(); context->appendDialectRegistry(registry); context->loadAllAvailableDialects(); @@ -368,8 +369,9 @@ static LogicalResult convertJeffToQCO(ModuleOp module) { TEST(JeffRoundTripRegressionTest, RestoresStatusResultAtEndOfEntryPoint) { DialectRegistry registry; - registry.insert(); + registry.insert(); MLIRContext context(registry); context.loadAllAvailableDialects(); OpBuilder builder(&context); @@ -400,9 +402,9 @@ TEST(JeffRoundTripRegressionTest, RestoresStatusResultAtEndOfEntryPoint) { TEST(JeffRoundTripRegressionTest, RestoresEntryPointWithObservableResults) { DialectRegistry registry; - registry.insert(); + registry.insert(); MLIRContext context(registry); context.loadAllAvailableDialects(); auto program = ::mqt::test::buildMLIRProgram( @@ -420,9 +422,7 @@ TEST(JeffRoundTripRegressionTest, RestoresEntryPointWithObservableResults) { ASSERT_TRUE(succeeded(convertJeffToQCO(*program))); auto main = program->lookupSymbol("main"); ASSERT_TRUE(main); - EXPECT_EQ( - main->getAttrOfType("passthrough"), - ArrayAttr::get(&context, {StringAttr::get(&context, "entry_point")})); + EXPECT_TRUE(mlir::mqt::isEntryPoint(main)); auto cregType = cbit::RegisterType::get(&context, 1); ASSERT_EQ(main.getFunctionType().getNumResults(), 1); EXPECT_EQ(main.getFunctionType().getResult(0), cregType); @@ -433,8 +433,9 @@ TEST(JeffRoundTripRegressionTest, RestoresEntryPointWithObservableResults) { TEST(JeffRoundTripRegressionTest, ConvertsJeffBitArraysDirectlyToCBit) { DialectRegistry registry; - registry.insert(); + registry.insert(); MLIRContext context(registry); context.loadAllAvailableDialects(); auto program = @@ -470,15 +471,16 @@ TEST(JeffRoundTripRegressionTest, ConvertsJeffBitArraysDirectlyToCBit) { TEST(JeffRoundTripRegressionTest, RejectsClassicalIfResultsPrecisely) { DialectRegistry registry; - registry.insert(); + registry.insert(); MLIRContext context(registry); context.loadAllAvailableDialects(); constexpr llvm::StringLiteral source = R"mlir( module { func.func @main(%condition: i1) -> i64 - attributes {passthrough = ["entry_point"]} { + attributes {mqt.entry_point} { %q0 = qco.alloc : !qco.qubit %then = arith.constant 1 : i64 %else = arith.constant 2 : i64 @@ -514,16 +516,16 @@ module { TEST(JeffRoundTripRegressionTest, RejectsLegacyClassicalMemref) { DialectRegistry registry; - registry.insert(); + registry.insert(); MLIRContext context(registry); context.loadAllAvailableDialects(); constexpr llvm::StringLiteral source = R"mlir( module { func.func @main(%size: index) -> memref - attributes {passthrough = ["entry_point"]} { + attributes {mqt.entry_point} { %c = memref.alloc(%size) : memref return %c : memref } diff --git a/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp b/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp index 8159d40c98..9e48ae88aa 100644 --- a/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp +++ b/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp @@ -10,12 +10,12 @@ #include "TestCaseUtils.h" #include "mlir/Conversion/QCOToQC/QCOToQC.h" +#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/QC/Builder/QCProgramBuilder.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/QTensor/IR/QTensorDialect.h" -#include "mlir/Dialect/Utils/Utils.h" #include "mlir/Support/IRVerification.h" #include "mlir/Support/Passes.h" #include "qc_programs.h" @@ -47,8 +47,8 @@ namespace { struct QCOToQCTestCase { std::string name; - mqt::test::NamedMLIRBuilder programBuilder; - mqt::test::NamedMLIRBuilder referenceBuilder; + ::mqt::test::NamedMLIRBuilder programBuilder; + ::mqt::test::NamedMLIRBuilder referenceBuilder; friend std::ostream& operator<<(std::ostream& os, const QCOToQCTestCase& info); @@ -56,10 +56,10 @@ struct QCOToQCTestCase { // NOLINTNEXTLINE(llvm-prefer-static-over-anonymous-namespace) std::ostream& operator<<(std::ostream& os, const QCOToQCTestCase& info) { - return os << "QCOToQC{" << info.name - << ", original=" << mqt::test::displayName(info.programBuilder.name) + return os << "QCOToQC{" << info.name << ", original=" + << ::mqt::test::displayName(info.programBuilder.name) << ", reference=" - << mqt::test::displayName(info.referenceBuilder.name) << "}"; + << ::mqt::test::displayName(info.referenceBuilder.name) << "}"; } class QCOToQCTest : public testing::TestWithParam { @@ -88,9 +88,9 @@ static LogicalResult runQCOToQCConversion(ModuleOp module) { TEST(QCOToQCRegressionTest, RetainsQubitRegisterName) { DialectRegistry registry; - registry.insert(); + registry.insert(); MLIRContext context(registry); context.loadAllAvailableDialects(); qco::QCOProgramBuilder builder(&context); @@ -107,24 +107,24 @@ TEST(QCOToQCRegressionTest, RetainsQubitRegisterName) { } }); ASSERT_TRUE(allocation); - const auto name = - allocation->getAttrOfType(utils::QUBIT_REGISTER_NAME_ATTR); + const auto name = allocation->getAttrOfType( + mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()); ASSERT_TRUE(name); EXPECT_EQ(name.getValue(), "named_qubits"); } TEST(QCOToQCRegressionTest, RetainsDynamicQubitRegisterName) { DialectRegistry registry; - registry.insert(); + registry.insert(); MLIRContext context(registry); context.loadAllAvailableDialects(); constexpr llvm::StringLiteral source = R"mlir( module { - func.func @main(%size: index) attributes {passthrough = ["entry_point"]} { - %reg = qtensor.alloc(%size) {mqt.qubit_register_name = "named_qubits"} : tensor + func.func @main(%size: index) attributes {mqt.entry_point} { + %reg = qtensor.alloc(%size) {mqt.register_name = "named_qubits"} : tensor qtensor.dealloc %reg : tensor return } @@ -142,8 +142,8 @@ module { ASSERT_EQ(allocation.getDynamicSizes().size(), 1); EXPECT_EQ(allocation.getDynamicSizes().front(), allocation->getBlock()->getArgument(0)); - const auto name = - allocation->getAttrOfType(utils::QUBIT_REGISTER_NAME_ATTR); + const auto name = allocation->getAttrOfType( + mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()); ASSERT_TRUE(name); EXPECT_EQ(name.getValue(), "named_qubits"); } @@ -175,7 +175,7 @@ TEST(QCOToQCRegressionTest, PreservesClassicalIfResult) { constexpr llvm::StringLiteral source = R"mlir( module { func.func @main(%condition: i1) -> i64 - attributes {passthrough = ["entry_point"]} { + attributes {mqt.entry_point} { %q0 = qco.alloc : !qco.qubit %then = arith.constant 1 : i64 %else = arith.constant 2 : i64 @@ -229,7 +229,7 @@ TEST(QCOToQCRegressionTest, PreservesClassicalIndexSwitchResult) { constexpr llvm::StringLiteral source = R"mlir( module { func.func @main(%index: index) -> i64 - attributes {passthrough = ["entry_point"]} { + attributes {mqt.entry_point} { %q0 = qco.alloc : !qco.qubit %result, %q1 = qco.index_switch %index -> (i64, !qco.qubit) case 0 args(%arg0 = %q0) { @@ -284,7 +284,7 @@ TEST(QCOToQCRegressionTest, PreservesClassicalForLoopState) { constexpr llvm::StringLiteral source = R"mlir( module { - func.func @main() -> i64 attributes {passthrough = ["entry_point"]} { + func.func @main() -> i64 attributes {mqt.entry_point} { %q0 = qco.alloc : !qco.qubit %lb = arith.constant 0 : index %ub = arith.constant 2 : index @@ -331,7 +331,7 @@ TEST(QCOToQCRegressionTest, PreservesTypeChangingClassicalWhileState) { constexpr llvm::StringLiteral source = R"mlir( module { - func.func @main() -> i64 attributes {passthrough = ["entry_point"]} { + func.func @main() -> i64 attributes {mqt.entry_point} { %q0 = qco.alloc : !qco.qubit %initial = arith.constant 1.0 : f32 %result, %q1 = scf.while (%input = %initial, %q = %q0) @@ -377,9 +377,9 @@ module { TEST_P(QCOToQCTest, ProgramEquivalence) { const auto& [nameStr, programBuilder, referenceBuilder] = GetParam(); const auto name = " (" + nameStr + ")"; - mqt::test::DeferredPrinter printer; + ::mqt::test::DeferredPrinter printer; - auto program = mqt::test::buildMLIRProgram(context.get(), programBuilder); + auto program = ::mqt::test::buildMLIRProgram(context.get(), programBuilder); ASSERT_TRUE(program); printer.record(program.get(), "Original QCO IR" + name); EXPECT_TRUE(verify(*program).succeeded()); @@ -396,7 +396,8 @@ TEST_P(QCOToQCTest, ProgramEquivalence) { printer.record(program.get(), "Canonicalized Converted QC IR" + name); EXPECT_TRUE(verify(*program).succeeded()); - auto reference = mqt::test::buildMLIRProgram(context.get(), referenceBuilder); + auto reference = + ::mqt::test::buildMLIRProgram(context.get(), referenceBuilder); ASSERT_TRUE(reference); printer.record(reference.get(), "Reference QC IR" + name); EXPECT_TRUE(verify(*reference).succeeded()); diff --git a/mlir/unittests/Conversion/QCQCORoundTrip/CMakeLists.txt b/mlir/unittests/Conversion/QCQCORoundTrip/CMakeLists.txt index f4ceb874d6..62a3f61d0c 100644 --- a/mlir/unittests/Conversion/QCQCORoundTrip/CMakeLists.txt +++ b/mlir/unittests/Conversion/QCQCORoundTrip/CMakeLists.txt @@ -16,6 +16,7 @@ target_link_libraries( MLIRCBitDialect MLIRFuncDialect MLIRMemRefDialect + MLIRMQTDialect MLIRParser MLIRPass MLIRQCDialect diff --git a/mlir/unittests/Conversion/QCQCORoundTrip/test_qc_qco_round_trip.cpp b/mlir/unittests/Conversion/QCQCORoundTrip/test_qc_qco_round_trip.cpp index 300faf85f3..cda3572613 100644 --- a/mlir/unittests/Conversion/QCQCORoundTrip/test_qc_qco_round_trip.cpp +++ b/mlir/unittests/Conversion/QCQCORoundTrip/test_qc_qco_round_trip.cpp @@ -13,9 +13,11 @@ #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/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/IR/QCOps.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include #include @@ -24,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -45,9 +48,10 @@ class QCQCORoundTripTest : public testing::Test { QCQCORoundTripTest() { DialectRegistry registry; - registry.insert(); + registry + .insert(); context.appendDialectRegistry(registry); context.loadAllAvailableDialects(); } @@ -71,14 +75,49 @@ class QCQCORoundTripTest : public testing::Test { } // namespace +TEST_F(QCQCORoundTripTest, PreservesSharedMQTMetadata) { + constexpr StringLiteral source = R"mlir( +module { + func.func @main(%theta: f64 {mqt.input_name = "theta"}) + attributes {mqt.entry_point} { + %reg = memref.alloc() {mqt.register_name = "q"} + : memref<2x!qc.qubit> + memref.dealloc %reg : memref<2x!qc.qubit> + return + } +} +)mlir"; + + auto moduleOp = parseSourceString(source, &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(succeeded(runRoundTrip(*moduleOp))); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + auto function = moduleOp->lookupSymbol("main"); + ASSERT_TRUE(function); + EXPECT_TRUE(mqt::isEntryPoint(function)); + const auto inputName = function.getArgAttrOfType( + 0, mqt::MQTDialect::InputNameAttrHelper::getNameStr()); + ASSERT_TRUE(inputName); + EXPECT_EQ(inputName.getValue(), "theta"); + + memref::AllocOp allocation; + moduleOp->walk([&](memref::AllocOp op) { allocation = op; }); + ASSERT_TRUE(allocation); + const auto registerName = allocation->getAttrOfType( + mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()); + ASSERT_TRUE(registerName); + EXPECT_EQ(registerName.getValue(), "q"); +} + TEST_F(QCQCORoundTripTest, PreservesClassicalRegistersWithoutConversion) { constexpr llvm::StringLiteral source = R"mlir( module { func.func @main() -> (!cbit.reg<2>, !cbit.reg<1>) - attributes {passthrough = ["entry_point"]} { + attributes {mqt.entry_point} { %c0 = arith.constant 0 : index - %zero = cbit.alloc(#cbit.init) source_name = "zero" : !cbit.reg<2> - %undefined = cbit.alloc(#cbit.init) source_name = "undefined" : !cbit.reg<1> + %zero = cbit.alloc(#cbit.init) {mqt.register_name = "zero"} : !cbit.reg<2> + %undefined = cbit.alloc(#cbit.init) {mqt.register_name = "undefined"} : !cbit.reg<1> %q = qc.alloc : !qc.qubit %measurement = qc.measure %q : !qc.qubit -> i1 cbit.store %measurement, %zero[%c0] : !cbit.reg<2> @@ -105,10 +144,18 @@ module { ASSERT_EQ(loads.size(), 1); ASSERT_EQ(stores.size(), 2); EXPECT_EQ(allocations[0].getInitialization(), cbit::Initialization::Zero); - EXPECT_EQ(allocations[0].getSourceName(), "zero"); + EXPECT_EQ(allocations[0] + ->getAttrOfType( + mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()) + .getValue(), + "zero"); EXPECT_EQ(allocations[1].getInitialization(), cbit::Initialization::Undefined); - EXPECT_EQ(allocations[1].getSourceName(), "undefined"); + EXPECT_EQ(allocations[1] + ->getAttrOfType( + mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()) + .getValue(), + "undefined"); EXPECT_EQ(loads.front().getReg(), allocations.front().getResult()); EXPECT_EQ(stores.front().getReg(), allocations.front().getResult()); EXPECT_EQ(stores.back().getReg(), allocations.back().getResult()); @@ -123,7 +170,7 @@ TEST_F(QCQCORoundTripTest, PreservesClassicalIfResultWithoutScratch) { constexpr llvm::StringLiteral source = R"mlir( module { func.func @main(%condition: i1) -> i64 - attributes {passthrough = ["entry_point"]} { + attributes {mqt.entry_point} { %q = qc.alloc : !qc.qubit %result = scf.if %condition -> i64 { qc.h %q : !qc.qubit @@ -162,7 +209,7 @@ TEST_F(QCQCORoundTripTest, PreservesClassicalIndexSwitchResultWithoutScratch) { constexpr llvm::StringLiteral source = R"mlir( module { func.func @main(%index: index) -> i64 - attributes {passthrough = ["entry_point"]} { + attributes {mqt.entry_point} { %q = qc.alloc : !qc.qubit %result = scf.index_switch %index -> i64 case 0 { @@ -202,7 +249,7 @@ module { TEST_F(QCQCORoundTripTest, PreservesDenseUnitaryMatrixAndQubitArity) { constexpr llvm::StringLiteral source = R"mlir( module { - func.func @main() attributes {passthrough = ["entry_point"]} { + func.func @main() attributes {mqt.entry_point} { %q0 = qc.alloc : !qc.qubit %q1 = qc.alloc : !qc.qubit qc.unitary dense<[ diff --git a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp index 2a490c2b7d..8e3b233ef8 100644 --- a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp +++ b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp @@ -13,6 +13,7 @@ #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/QC/Builder/QCProgramBuilder.h" #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" @@ -21,7 +22,6 @@ #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" -#include "mlir/Dialect/Utils/Utils.h" #include "mlir/Support/IRVerification.h" #include "mlir/Support/Passes.h" #include "qc_programs.h" @@ -66,8 +66,8 @@ namespace { struct QCToQCOTestCase { std::string name; - mqt::test::NamedMLIRBuilder programBuilder; - mqt::test::NamedMLIRBuilder referenceBuilder; + ::mqt::test::NamedMLIRBuilder programBuilder; + ::mqt::test::NamedMLIRBuilder referenceBuilder; bool expectsCompleteTensorState = false; bool skipReferenceComparison = false; @@ -77,10 +77,10 @@ struct QCToQCOTestCase { // NOLINTNEXTLINE(llvm-prefer-static-over-anonymous-namespace) std::ostream& operator<<(std::ostream& os, const QCToQCOTestCase& info) { - return os << "QCToQCO{" << info.name - << ", original=" << mqt::test::displayName(info.programBuilder.name) + return os << "QCToQCO{" << info.name << ", original=" + << ::mqt::test::displayName(info.programBuilder.name) << ", reference=" - << mqt::test::displayName(info.referenceBuilder.name) << "}"; + << ::mqt::test::displayName(info.referenceBuilder.name) << "}"; } class QCToQCOTest : public testing::TestWithParam { @@ -90,9 +90,10 @@ class QCToQCOTest : public testing::TestWithParam { void SetUp() override { // Register all necessary dialects DialectRegistry registry; - registry.insert(); + registry + .insert(); context = std::make_unique(); context->appendDialectRegistry(registry); context->loadAllAvailableDialects(); @@ -115,9 +116,10 @@ class QCToQCORegressionTest : public testing::Test { QCToQCORegressionTest() { DialectRegistry registry; - registry.insert(); + registry + .insert(); context.appendDialectRegistry(registry); context.loadAllAvailableDialects(); } @@ -222,7 +224,7 @@ class QCToQCORegressionTest : public testing::Test { TEST_F(QCToQCORegressionTest, PreservesForResultsWithQuantumState) { constexpr llvm::StringLiteral source = R"mlir( module { - func.func @main() -> i1 attributes {passthrough = ["entry_point"]} { + func.func @main() -> i1 attributes {mqt.entry_point} { %qc = qc.alloc : !qc.qubit %c0 = arith.constant 0 : index %c1 = arith.constant 1 : index @@ -262,7 +264,7 @@ module { TEST_F(QCToQCORegressionTest, PreservesWhileConditionArgumentsAndOrdering) { constexpr llvm::StringLiteral source = R"mlir( module { - func.func @main() -> i1 attributes {passthrough = ["entry_point"]} { + func.func @main() -> i1 attributes {mqt.entry_point} { %qc = qc.alloc : !qc.qubit %true = arith.constant true %zero = arith.constant 0 : i64 @@ -317,7 +319,7 @@ TEST_F(QCToQCORegressionTest, IgnoresClassicalRegisterLoadsInWhileState) { constexpr llvm::StringLiteral source = R"mlir( module { func.func @main() -> memref<1xi1> - attributes {passthrough = ["entry_point"]} { + attributes {mqt.entry_point} { %qc = qc.alloc : !qc.qubit %c = memref.alloc() : memref<1xi1> %c0 = arith.constant 0 : index @@ -357,7 +359,7 @@ module { TEST_F(QCToQCORegressionTest, ConvertsTypeChangingWhileWithQuantumState) { constexpr llvm::StringLiteral source = R"mlir( module { - func.func @main() -> i64 attributes {passthrough = ["entry_point"]} { + func.func @main() -> i64 attributes {mqt.entry_point} { %qc = qc.alloc : !qc.qubit %initial = arith.constant 1.0 : f32 %result = scf.while (%input = %initial) : (f32) -> i64 { @@ -405,7 +407,7 @@ module { TEST_F(QCToQCORegressionTest, LeavesUnrelatedSCFTerminatorsUntouched) { constexpr llvm::StringLiteral source = R"mlir( module { - func.func @main() -> i1 attributes {passthrough = ["entry_point"]} { + func.func @main() -> i1 attributes {mqt.entry_point} { %qc = qc.alloc : !qc.qubit qc.h %qc : !qc.qubit %result = scf.execute_region -> i1 { @@ -434,7 +436,7 @@ TEST_F(QCToQCORegressionTest, PreservesIfClassicalResultsWithoutScratch) { constexpr llvm::StringLiteral source = R"mlir( module { func.func @main(%condition: i1) -> i64 - attributes {passthrough = ["entry_point"]} { + attributes {mqt.entry_point} { %qc = qc.alloc : !qc.qubit %result = scf.if %condition -> i64 { qc.h %qc : !qc.qubit @@ -489,7 +491,7 @@ TEST_F(QCToQCORegressionTest, constexpr llvm::StringLiteral source = R"mlir( module { func.func @main(%index: index) -> i64 - attributes {passthrough = ["entry_point"]} { + attributes {mqt.entry_point} { %qc = qc.alloc : !qc.qubit %result = scf.index_switch %index -> i64 case 0 { @@ -547,7 +549,7 @@ TEST_F(QCToQCORegressionTest, MaterializesSequentialPotentialAliasesAtEachOperation) { constexpr llvm::StringLiteral source = R"mlir( module { - func.func @main(%i: index) attributes {passthrough = ["entry_point"]} { + func.func @main(%i: index) attributes {mqt.entry_point} { %reg = memref.alloc() : memref<2x!qc.qubit> %c0 = arith.constant 0 : index %q0 = memref.load %reg[%c0] : memref<2x!qc.qubit> @@ -601,8 +603,8 @@ TEST_F(QCToQCORegressionTest, RetainsQubitRegisterName) { qtensor::AllocOp allocation; moduleOp->walk([&](qtensor::AllocOp op) { allocation = op; }); ASSERT_TRUE(allocation); - const auto name = - allocation->getAttrOfType(utils::QUBIT_REGISTER_NAME_ATTR); + const auto name = allocation->getAttrOfType( + mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()); ASSERT_TRUE(name); EXPECT_EQ(name.getValue(), "named_qubits"); } @@ -610,8 +612,8 @@ TEST_F(QCToQCORegressionTest, RetainsQubitRegisterName) { TEST_F(QCToQCORegressionTest, RetainsDynamicQubitRegisterName) { constexpr llvm::StringLiteral source = R"mlir( module { - func.func @main(%size: index) attributes {passthrough = ["entry_point"]} { - %reg = memref.alloc(%size) {mqt.qubit_register_name = "named_qubits"} : memref + func.func @main(%size: index) attributes {mqt.entry_point} { + %reg = memref.alloc(%size) {mqt.register_name = "named_qubits"} : memref memref.dealloc %reg : memref return } @@ -627,8 +629,8 @@ module { ASSERT_TRUE(allocation); EXPECT_TRUE(allocation.getResult().getType().isDynamicDim(0)); EXPECT_EQ(allocation.getSize(), allocation->getBlock()->getArgument(0)); - const auto name = - allocation->getAttrOfType(utils::QUBIT_REGISTER_NAME_ATTR); + const auto name = allocation->getAttrOfType( + mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()); ASSERT_TRUE(name); EXPECT_EQ(name.getValue(), "named_qubits"); } @@ -637,7 +639,7 @@ TEST_F(QCToQCORegressionTest, RejectsRegisterBackedReferenceEscapes) { constexpr llvm::StringLiteral source = R"mlir( module { func.func private @escape(!qc.qubit) - func.func @main() attributes {passthrough = ["entry_point"]} { + func.func @main() attributes {mqt.entry_point} { %reg = memref.alloc() : memref<1x!qc.qubit> %c0 = arith.constant 0 : index %q = memref.load %reg[%c0] : memref<1x!qc.qubit> @@ -666,7 +668,7 @@ module { TEST_F(QCToQCORegressionTest, PreflightRejectsNonOneDimensionalQubitRegisters) { constexpr llvm::StringLiteral source = R"mlir( module { - func.func @main() attributes {passthrough = ["entry_point"]} { + func.func @main() attributes {mqt.entry_point} { %reg = memref.alloc() : memref %q = memref.load %reg[] : memref qc.x %q : !qc.qubit @@ -698,7 +700,7 @@ module { TEST_F(QCToQCORegressionTest, PreflightRejectsDerivedQubitRegisterValues) { constexpr llvm::StringLiteral source = R"mlir( module { - func.func @main() attributes {passthrough = ["entry_point"]} { + func.func @main() attributes {mqt.entry_point} { %reg = memref.alloc() : memref<1x!qc.qubit> %cast = memref.cast %reg : memref<1x!qc.qubit> to memref memref.dealloc %cast : memref @@ -732,7 +734,7 @@ TEST_F(QCToQCORegressionTest, R"mlir( module { func.func @main(%q: !qc.qubit) - attributes {passthrough = ["entry_point"]} { + attributes {mqt.entry_point} { qc.x %q : !qc.qubit return } @@ -741,7 +743,7 @@ module { R"mlir( module { func.func @main(%reg: memref<1x!qc.qubit>) - attributes {passthrough = ["entry_point"]} { + attributes {mqt.entry_point} { return } } @@ -749,7 +751,7 @@ module { R"mlir( module { func.func @main(%reg: memref<*x!qc.qubit>) - attributes {passthrough = ["entry_point"]} { + attributes {mqt.entry_point} { return } } @@ -785,7 +787,7 @@ TEST_F(QCToQCORegressionTest, constexpr auto sources = std::to_array({ R"mlir( module { - func.func @main() attributes {passthrough = ["entry_point"]} { + func.func @main() attributes {mqt.entry_point} { %q = qc.alloc : !qc.qubit scf.execute_region { qc.x %q : !qc.qubit @@ -798,7 +800,7 @@ module { )mlir", R"mlir( module { - func.func @main() attributes {passthrough = ["entry_point"]} { + func.func @main() attributes {mqt.entry_point} { %reg = memref.alloc() : memref<1x!qc.qubit> %c0 = arith.constant 0 : index %q = memref.load %reg[%c0] : memref<1x!qc.qubit> @@ -839,7 +841,7 @@ module { TEST_F(QCToQCORegressionTest, CapturesQubitsUsedByPowInsideFor) { constexpr llvm::StringLiteral source = R"mlir( module { - func.func @main() attributes {passthrough = ["entry_point"]} { + func.func @main() attributes {mqt.entry_point} { %q = qc.alloc : !qc.qubit %c0 = arith.constant 0 : index %c1 = arith.constant 1 : index @@ -1106,7 +1108,7 @@ buildInvalidCBitModifierProgram(MLIRContext* context, case CBitModifierBodyOp::Alloc: cbit::AllocOp::create(builder, cbit::RegisterType::get(builder.getContext(), 1), - cbit::Initialization::Zero, StringAttr{}); + cbit::Initialization::Zero); break; case CBitModifierBodyOp::Load: cbit::LoadOp::create(builder, builder.getI1Type(), reg, @@ -1351,7 +1353,7 @@ TEST_F(QCToQCORegressionTest, DoesNotCaptureQubitsAllocatedInsideIf) { constexpr llvm::StringLiteral source = R"mlir( module { func.func @main(%condition: i1) - attributes {passthrough = ["entry_point"]} { + attributes {mqt.entry_point} { scf.if %condition { %q = qc.alloc : !qc.qubit qc.h %q : !qc.qubit @@ -1382,7 +1384,7 @@ TEST_F(QCToQCORegressionTest, RejectsSameDynamicRegisterIndexWithinOneOperation) { constexpr llvm::StringLiteral source = R"mlir( module { - func.func @main(%i: index) attributes {passthrough = ["entry_point"]} { + func.func @main(%i: 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[%i] : memref<2x!qc.qubit> @@ -1411,7 +1413,7 @@ TEST_F(QCToQCORegressionTest, RejectsEqualConstantRegisterIndicesWithinOneOperation) { constexpr llvm::StringLiteral source = R"mlir( module { - func.func @main() attributes {passthrough = ["entry_point"]} { + func.func @main() attributes {mqt.entry_point} { %reg = memref.alloc() : memref<2x!qc.qubit> %lhs = arith.constant 0 : index %rhs = arith.constant 0 : index @@ -1442,9 +1444,9 @@ TEST_P(QCToQCOTest, ProgramConversion) { const auto& [_, programBuilder, referenceBuilder, expectsCompleteTensorState, skipReferenceComparison] = GetParam(); const auto name = " (" + GetParam().name + ")"; - mqt::test::DeferredPrinter printer; + ::mqt::test::DeferredPrinter printer; - auto program = mqt::test::buildMLIRProgram(context.get(), programBuilder); + auto program = ::mqt::test::buildMLIRProgram(context.get(), programBuilder); ASSERT_TRUE(program); printer.record(program.get(), "Original QC IR" + name); EXPECT_TRUE(verify(*program).succeeded()); @@ -1468,7 +1470,7 @@ TEST_P(QCToQCOTest, ProgramConversion) { if (!skipReferenceComparison) { auto reference = - mqt::test::buildMLIRProgram(context.get(), referenceBuilder); + ::mqt::test::buildMLIRProgram(context.get(), referenceBuilder); ASSERT_TRUE(reference); printer.record(reference.get(), "Reference QCO IR" + name); EXPECT_TRUE(verify(*reference).succeeded()); diff --git a/mlir/unittests/Dialect/CBit/IR/CMakeLists.txt b/mlir/unittests/Dialect/CBit/IR/CMakeLists.txt index af2a42e745..d6066563fe 100644 --- a/mlir/unittests/Dialect/CBit/IR/CMakeLists.txt +++ b/mlir/unittests/Dialect/CBit/IR/CMakeLists.txt @@ -14,6 +14,7 @@ target_link_libraries( MLIRArithDialect MLIRCBitDialect MLIRFuncDialect + MLIRMQTDialect MLIRParser MLIRSupport MLIRTransforms) diff --git a/mlir/unittests/Dialect/CBit/IR/test_cbit_ir.cpp b/mlir/unittests/Dialect/CBit/IR/test_cbit_ir.cpp index ee517ea8eb..6bdb47a4f5 100644 --- a/mlir/unittests/Dialect/CBit/IR/test_cbit_ir.cpp +++ b/mlir/unittests/Dialect/CBit/IR/test_cbit_ir.cpp @@ -15,6 +15,7 @@ #include "mlir/Dialect/CBit/IR/CBitDialect.h" #include "mlir/Dialect/CBit/IR/CBitOps.h" +#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include #include @@ -45,8 +46,8 @@ class CBitIRTest : public ::testing::Test { void SetUp() override { DialectRegistry registry; - registry - .insert(); + registry.insert(); context = std::make_unique(registry); context->loadAllAvailableDialects(); } @@ -62,7 +63,7 @@ TEST_F(CBitIRTest, ParsesAndPrintsRegisterOperations) { func.func @main() -> !cbit.reg<2> { %c0 = arith.constant 0 : index %false = arith.constant false - %reg = cbit.alloc(#cbit.init) source_name = "c" : !cbit.reg<2> + %reg = cbit.alloc(#cbit.init) {mqt.register_name = "c"} : !cbit.reg<2> cbit.store %false, %reg[%c0] : !cbit.reg<2> %bit = cbit.load %reg[%c0] : !cbit.reg<2> return %reg : !cbit.reg<2> @@ -76,8 +77,9 @@ TEST_F(CBitIRTest, ParsesAndPrintsRegisterOperations) { std::string printed; llvm::raw_string_ostream stream(printed); moduleOp->print(stream); - EXPECT_NE(printed.find("cbit.alloc(#cbit.init) source_name = \"c\""), - std::string::npos) + EXPECT_NE( + printed.find("cbit.alloc(#cbit.init) {mqt.register_name = \"c\"}"), + std::string::npos) << printed; EXPECT_NE(printed.find("!cbit.reg<2>"), std::string::npos); EXPECT_NE(printed.find("cbit.store"), std::string::npos); diff --git a/mlir/unittests/Dialect/CMakeLists.txt b/mlir/unittests/Dialect/CMakeLists.txt index 387567ff29..04bd1922f4 100644 --- a/mlir/unittests/Dialect/CMakeLists.txt +++ b/mlir/unittests/Dialect/CMakeLists.txt @@ -7,6 +7,7 @@ # Licensed under the MIT License add_subdirectory(CBit) +add_subdirectory(MQT) add_subdirectory(QC) add_subdirectory(QCO) add_subdirectory(QIR) diff --git a/mlir/unittests/Dialect/MQT/CMakeLists.txt b/mlir/unittests/Dialect/MQT/CMakeLists.txt new file mode 100644 index 0000000000..b181a84fed --- /dev/null +++ b/mlir/unittests/Dialect/MQT/CMakeLists.txt @@ -0,0 +1,9 @@ +# 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 + +add_subdirectory(IR) diff --git a/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt b/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt new file mode 100644 index 0000000000..cf0a00d560 --- /dev/null +++ b/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt @@ -0,0 +1,25 @@ +# 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 + +set(mqt_ir_target mqt-core-mlir-unittest-mqt-ir) +add_executable(${mqt_ir_target} test_mqt_ir.cpp) +target_link_libraries( + ${mqt_ir_target} + PRIVATE GTest::gtest_main + MLIRArithDialect + MLIRCBitDialect + MLIRFuncDialect + MLIRMemRefDialect + MLIRMQTDialect + MLIRParser + MLIRQCDialect + MLIRQCODialect + MLIRQTensorDialect) +mqt_mlir_configure_unittest_target(${mqt_ir_target}) + +gtest_discover_tests(${mqt_ir_target} PROPERTIES LABELS mqt-mlir-unittests DISCOVERY_TIMEOUT 60) diff --git a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp new file mode 100644 index 0000000000..6099633f98 --- /dev/null +++ b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp @@ -0,0 +1,234 @@ +/* + * 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 + */ + +/** + * @file test_mqt_ir.cpp + * @brief Unit tests for the MQT metadata dialect. + */ + +#include "mlir/Dialect/CBit/IR/CBitDialect.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/QTensor/IR/QTensorDialect.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace mlir; + +namespace { +class MQTIRTest : public ::testing::Test { +protected: + std::unique_ptr context; + + void SetUp() override { + DialectRegistry registry; + registry.insert(); + context = std::make_unique(registry); + context->loadAllAvailableDialects(); + } + + [[nodiscard]] OwningOpRef parse(const StringRef source) const { + return parseSourceString(source, context.get()); + } +}; + +TEST_F(MQTIRTest, AcceptsProgramInputAndRegisterNames) { + EXPECT_TRUE(parse(R"mlir( + module { + func.func @qc(%theta: f64 {mqt.input_name = "theta"}) { + %reg = memref.alloc() {mqt.register_name = "q"} + : memref<2x!qc.qubit> + return + } + func.func @qco(%enabled: i1 {mqt.input_name = "enabled"}) { + %c2 = arith.constant 2 : index + %reg = qtensor.alloc(%c2) {mqt.register_name = "r"} + : tensor<2x!qco.qubit> + return + } + func.func @cbit() { + %reg = cbit.alloc(#cbit.init) {mqt.register_name = "c"} + : !cbit.reg<2> + return + } + func.func @lowered_cbit() { + %reg = memref.alloc() {mqt.register_name = "lowered"} : memref<2xi1> + return + } + } + )mlir")); +} + +TEST_F(MQTIRTest, ManagesAndFindsEntryPoint) { + auto moduleOp = parse(R"mlir( + module { + func.func @helper() { return } + func.func @main() attributes {mqt.entry_point} { return } + } + )mlir"); + ASSERT_TRUE(moduleOp); + auto main = mqt::getEntryPoint(*moduleOp); + ASSERT_TRUE(main); + EXPECT_EQ(main.getSymName(), "main"); + EXPECT_TRUE(mqt::isEntryPoint(main)); + + mqt::removeEntryPoint(main); + EXPECT_FALSE(mqt::isEntryPoint(main)); + EXPECT_FALSE(mqt::getEntryPoint(*moduleOp)); + + auto helper = moduleOp->lookupSymbol("helper"); + ASSERT_TRUE(helper); + mqt::setEntryPoint(helper); + EXPECT_TRUE(mqt::isEntryPoint(helper)); + EXPECT_EQ(mqt::getEntryPoint(*moduleOp), helper); +} + +TEST_F(MQTIRTest, RejectsInvalidEntryPoints) { + EXPECT_FALSE(parse(R"mlir( + module { + func.func @main() attributes {mqt.entry_point = "yes"} { return } + } + )mlir")); + EXPECT_FALSE(parse(R"mlir( + module { + 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() { + %c0 = "arith.constant"() {mqt.entry_point, value = 0 : i64} + : () -> i64 + return + } + } + )mlir")); +} + +TEST_F(MQTIRTest, RejectsInvalidInputNames) { + EXPECT_FALSE(parse(R"mlir( + module { + func.func @empty(%arg: f64 {mqt.input_name = ""}) { return } + } + )mlir")); + EXPECT_FALSE(parse(R"mlir( + module { + func.func @null(%arg: f64 {mqt.input_name = "a\00b"}) { return } + } + )mlir")); + EXPECT_FALSE(parse(R"mlir( + module { + func.func @wrong_type(%arg: f64 {mqt.input_name = 1 : i64}) { return } + } + )mlir")); +} + +TEST_F(MQTIRTest, RejectsDuplicateInputNames) { + EXPECT_FALSE(parse(R"mlir( + module { + func.func @main(%lhs: f64 {mqt.input_name = "theta"}, + %rhs: i1 {mqt.input_name = "theta"}) { + return + } + } + )mlir")); +} + +TEST_F(MQTIRTest, RejectsInputNameOnOperation) { + EXPECT_FALSE(parse(R"mlir( + module { + func.func @main() { + %c0 = "arith.constant"() {mqt.input_name = "theta", value = 0.0 : f64} + : () -> f64 + return + } + } + )mlir")); +} + +TEST_F(MQTIRTest, RejectsInvalidRegisterNamesAndOwners) { + EXPECT_FALSE(parse(R"mlir( + module { + func.func @empty() { + %reg = cbit.alloc(#cbit.init) {mqt.register_name = ""} + : !cbit.reg<2> + return + } + } + )mlir")); + EXPECT_FALSE(parse(R"mlir( + module { + func.func @main() { + %reg = memref.alloc() {mqt.register_name = "values"} + : memref<2xf64> + return + } + } + )mlir")); + EXPECT_FALSE(parse(R"mlir( + module { + func.func @main(%arg: f64 {mqt.register_name = "q"}) { + return + } + } + )mlir")); +} + +TEST_F(MQTIRTest, RejectsDuplicateProgramNames) { + EXPECT_FALSE(parse(R"mlir( + module { + func.func @main() { + %lhs = memref.alloc() {mqt.register_name = "state"} + : memref<1x!qc.qubit> + %rhs = cbit.alloc(#cbit.init) {mqt.register_name = "state"} + : !cbit.reg<2> + return + } + } + )mlir")); + EXPECT_FALSE(parse(R"mlir( + module { + func.func @main(%arg: f64 {mqt.input_name = "state"}) { + %reg = cbit.alloc(#cbit.init) {mqt.register_name = "state"} + : !cbit.reg<2> + return + } + } + )mlir")); +} + +TEST_F(MQTIRTest, RejectsUnknownMQTAttributes) { + EXPECT_FALSE(parse(R"mlir( + module { + func.func @main() attributes {mqt.unknown} { return } + } + )mlir")); +} +} // namespace diff --git a/mlir/unittests/Dialect/QC/CMakeLists.txt b/mlir/unittests/Dialect/QC/CMakeLists.txt index c98100e703..7840251896 100644 --- a/mlir/unittests/Dialect/QC/CMakeLists.txt +++ b/mlir/unittests/Dialect/QC/CMakeLists.txt @@ -7,4 +7,5 @@ # Licensed under the MIT License add_subdirectory(IR) +add_subdirectory(Transforms) add_subdirectory(Translation) diff --git a/mlir/unittests/Dialect/QC/IR/CMakeLists.txt b/mlir/unittests/Dialect/QC/IR/CMakeLists.txt index d0693ba2f9..f7cf3b55c1 100644 --- a/mlir/unittests/Dialect/QC/IR/CMakeLists.txt +++ b/mlir/unittests/Dialect/QC/IR/CMakeLists.txt @@ -8,7 +8,14 @@ set(target_name mqt-core-mlir-unittest-qc-ir) add_executable(${target_name} test_qc_ir.cpp) -target_link_libraries(${target_name} PRIVATE MLIRParser MLIRSupportMQT GTest::gtest_main - MLIRQCProgramBuilder MLIRQCPrograms MLIRMQTTransforms) +target_link_libraries( + ${target_name} + PRIVATE MLIRParser + MLIRSupportMQT + GTest::gtest_main + MLIRMQTDialect + MLIRQCProgramBuilder + MLIRQCPrograms + MLIRMQTTransforms) mqt_mlir_configure_unittest_target(${target_name}) gtest_discover_tests(${target_name} PROPERTIES LABELS mqt-mlir-unittests DISCOVERY_TIMEOUT 60) diff --git a/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp b/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp index 72f1874f5e..751b68bbb2 100644 --- a/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp +++ b/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp @@ -12,6 +12,7 @@ #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/QC/Builder/QCProgramBuilder.h" #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/IR/QCInterfaces.h" @@ -41,6 +42,7 @@ #include #include #include +#include #include #include @@ -53,7 +55,6 @@ #include #include #include -#include using namespace mlir; using namespace mlir::qc; @@ -149,17 +150,6 @@ TEST_F(QCTest, BuilderRejectsMixedStaticAndDynamicQubitAllocationModes) { "Cannot mix dynamic and static qubit allocation modes"); } -TEST_F(QCTest, BuilderRejectsDuplicateNonEmptyQubitRegisterNames) { - EXPECT_DEATH( - { - QCProgramBuilder builder(context.get()); - builder.initialize(); - std::ignore = builder.allocQubitRegisterStorage(1, "q"); - std::ignore = builder.allocQubitRegisterStorage(1, "q"); - }, - "Qubit register names must be unique"); -} - TEST_F(QCTest, BuilderRejectsOutOfBoundsClassicalRegisterIndices) { EXPECT_DEATH( { @@ -215,10 +205,16 @@ TEST_F(QCTest, BuilderSupportsIndependentClassicalRegisterInitialization) { moduleOp->walk([&](cbit::AllocOp op) { allocations.push_back(op); }); ASSERT_EQ(allocations.size(), 2); EXPECT_EQ(allocations[0].getInitialization(), cbit::Initialization::Zero); - EXPECT_FALSE(allocations[0].getSourceNameAttr()); + EXPECT_FALSE(allocations[0]->getAttr( + ::mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr())); EXPECT_EQ(allocations[1].getInitialization(), cbit::Initialization::Undefined); - EXPECT_EQ(allocations[1].getSourceName(), "undefined"); + EXPECT_EQ( + allocations[1] + ->getAttrOfType( + ::mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()) + .getValue(), + "undefined"); } TEST_F(QCTest, BuilderAllowsRepeatedQubitLoadsAcrossNestedRegions) { @@ -292,6 +288,48 @@ TEST_F(QCTest, DirectSingleQubitPowBuilder) { EXPECT_TRUE(pow.verify().succeeded()); } +TEST_F(QCTest, UnitaryVerifierRejectsNonFiniteConstantParameters) { + constexpr std::array invalidPrograms{ + R"mlir( + module { + func.func @main(%input: f64) { + %q = qc.alloc : !qc.qubit + %infinity = arith.constant 0x7FF0000000000000 : f64 + %theta = arith.addf %input, %infinity : f64 + qc.rx(%theta) %q : !qc.qubit + qc.dealloc %q : !qc.qubit + return + } + } + )mlir", + R"mlir( + module { + func.func @main() { + %q = qc.alloc : !qc.qubit + %nan = arith.constant 0x7FF8000000000000 : f64 + qc.pow(%nan) (%arg = %q) { + qc.x %arg : !qc.qubit + qc.yield + } : !qc.qubit + qc.dealloc %q : !qc.qubit + return + } + } + )mlir"}; + + for (const auto source : invalidPrograms) { + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + 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); + } +} + TEST_F(QCTest, DenseUnitaryBuilderVerifiesAndCanonicalizesIdentity) { const auto matrixType = RankedTensorType::get( {2, 2}, ComplexType::get(Float64Type::get(context.get()))); @@ -553,7 +591,7 @@ static void emitForbiddenModifierBodyOperation( case ForbiddenModifierBodyOp::CBitAlloc: cbit::AllocOp::create(builder, cbit::RegisterType::get(builder.getContext(), 1), - cbit::Initialization::Zero, StringAttr{}); + cbit::Initialization::Zero); return; case ForbiddenModifierBodyOp::CBitLoad: cbit::LoadOp::create(builder, builder.getI1Type(), cbitReg, index); diff --git a/mlir/unittests/Dialect/QC/Transforms/CMakeLists.txt b/mlir/unittests/Dialect/QC/Transforms/CMakeLists.txt new file mode 100644 index 0000000000..d783fce088 --- /dev/null +++ b/mlir/unittests/Dialect/QC/Transforms/CMakeLists.txt @@ -0,0 +1,15 @@ +# 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 + +set(target_name mqt-core-mlir-unittest-qc-transforms) +add_executable(${target_name} test_qc_transforms.cpp) +target_link_libraries(${target_name} PRIVATE GTest::gtest_main MLIRMQTDialect MLIRParser MLIRPass + MLIRQCTransforms MLIRSupport) +mqt_mlir_configure_unittest_target(${target_name}) + +gtest_discover_tests(${target_name} PROPERTIES LABELS mqt-mlir-unittests DISCOVERY_TIMEOUT 60) diff --git a/mlir/unittests/Dialect/QC/Transforms/test_qc_transforms.cpp b/mlir/unittests/Dialect/QC/Transforms/test_qc_transforms.cpp new file mode 100644 index 0000000000..7a9ae0d66c --- /dev/null +++ b/mlir/unittests/Dialect/QC/Transforms/test_qc_transforms.cpp @@ -0,0 +1,76 @@ +/* + * 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 + */ + +/** + * @file test_qc_transforms.cpp + * @brief Unit tests for QC dialect transformations. + */ + +#include "mlir/Dialect/MQT/IR/MQTDialect.h" +#include "mlir/Dialect/QC/IR/QCDialect.h" +#include "mlir/Dialect/QC/Transforms/Passes.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace mlir; + +namespace { +TEST(QCTransformsTest, ShrinkQubitRegistersPreservesMetadata) { + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + + auto moduleOp = parseSourceString(R"mlir( + module { + func.func @main() { + %c1 = arith.constant 1 : index + %reg = memref.alloc() {mqt.register_name = "q"} + : memref<3x!qc.qubit> + %qubit = memref.load %reg[%c1] : memref<3x!qc.qubit> + qc.x %qubit : !qc.qubit + memref.dealloc %reg : memref<3x!qc.qubit> + return + } + } + )mlir", + &context); + ASSERT_TRUE(moduleOp); + + PassManager manager(&context); + manager.addPass(qc::createShrinkQubitRegistersPass()); + ASSERT_TRUE(succeeded(manager.run(*moduleOp))); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + + memref::AllocOp allocation; + moduleOp->walk([&](memref::AllocOp op) { allocation = op; }); + ASSERT_TRUE(allocation); + EXPECT_EQ(allocation.getType().getShape(), ArrayRef{1}); + EXPECT_EQ(allocation->getAttrOfType( + mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()), + StringAttr::get(&context, "q")); +} +} // namespace diff --git a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp index c40fc36f36..c4c7b2e57a 100644 --- a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp +++ b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp @@ -85,10 +85,10 @@ TEST(OpenQASM3EmissionTest, EmitsStrictPortableBellProgram) { TEST(OpenQASM3EmissionTest, PreservesMeasurementOrderBeforeDelayedStore) { constexpr llvm::StringLiteral source = R"mlir(module { func.func @main() -> !cbit.reg<1> - attributes {passthrough = ["entry_point"]} { + attributes {mqt.entry_point} { %zero = arith.constant 0 : index %qubit = qc.alloc : !qc.qubit - %bits = cbit.alloc(#cbit.init) source_name = "c" + %bits = cbit.alloc(#cbit.init) {mqt.register_name = "c"} : !cbit.reg<1> %measured = qc.measure %qubit : !qc.qubit -> i1 qc.x %qubit : !qc.qubit @@ -179,7 +179,7 @@ r = measure q; TEST(OpenQASM3EmissionTest, RenamesOutputsThatCollideWithStandardGates) { constexpr llvm::StringLiteral source = R"mlir(module { func.func @main() -> !cbit.reg<1> { - %bits = cbit.alloc(#cbit.init) source_name = "x" + %bits = cbit.alloc(#cbit.init) {mqt.register_name = "x"} : !cbit.reg<1> return %bits : !cbit.reg<1> } @@ -243,7 +243,7 @@ switch (selector) { TEST(OpenQASM3EmissionTest, EmitsNativeIndexSwitch) { constexpr llvm::StringLiteral source = R"mlir( module { - func.func @main() attributes {passthrough = ["entry_point"]} { + func.func @main() attributes {mqt.entry_point} { %qubit = qc.alloc : !qc.qubit %index = arith.constant 1 : index scf.index_switch %index @@ -372,7 +372,7 @@ TEST(OpenQASM3EmissionTest, EmitsSignedBooleanAndFloatingExpressions) { constexpr llvm::StringLiteral source = R"mlir( module { func.func @main() -> (i64, i1, f64, f64, i1) - attributes {passthrough = ["entry_point"]} { + attributes {mqt.entry_point} { %one = arith.constant 1 : i64 %two = arith.constant 2 : i64 %sum = arith.addi %one, %two : i64 @@ -538,9 +538,9 @@ TEST(OpenQASM3EmissionTest, ReusesClassicalRegisterNamesForOutputs) { constexpr llvm::StringLiteral source = R"mlir( module { func.func @main() -> (!cbit.reg<1>, !cbit.reg<2>, i1) { - %single = cbit.alloc(#cbit.init) source_name = "single" + %single = cbit.alloc(#cbit.init) {mqt.register_name = "single"} : !cbit.reg<1> - %bits = cbit.alloc(#cbit.init) source_name = "bits" + %bits = cbit.alloc(#cbit.init) {mqt.register_name = "bits"} : !cbit.reg<2> %qubit = qc.alloc : !qc.qubit %measured = qc.measure %qubit : !qc.qubit -> i1 diff --git a/mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp b/mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp index e33d9ab555..2a51a5ff3e 100644 --- a/mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp +++ b/mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp @@ -13,6 +13,7 @@ #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/QC/Builder/QCProgramBuilder.h" #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/IR/QCInterfaces.h" @@ -20,7 +21,6 @@ #include "mlir/Dialect/QC/Translation/TranslateQASM3ToQC.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" -#include "mlir/Dialect/Utils/Utils.h" #include "mlir/Support/IRVerification.h" #include "mlir/Support/Passes.h" #include "qasm_programs.h" @@ -62,7 +62,7 @@ namespace { struct QASM3TranslationTestCase { std::string name; std::string source; - mqt::test::NamedMLIRBuilder referenceBuilder; + ::mqt::test::NamedMLIRBuilder referenceBuilder; friend std::ostream& operator<<(std::ostream& os, const QASM3TranslationTestCase& test); @@ -72,7 +72,7 @@ struct QASM3TranslationTestCase { std::ostream& operator<<(std::ostream& os, const QASM3TranslationTestCase& test) { return os << "QASM3Translation{" << test.name << ", reference=" - << mqt::test::displayName(test.referenceBuilder.name) << "}"; + << ::mqt::test::displayName(test.referenceBuilder.name) << "}"; } class QASM3TranslationTest @@ -82,10 +82,10 @@ class QASM3TranslationTest void SetUp() override { DialectRegistry registry; - registry - .insert(); + registry.insert(); context = std::make_unique(); context->appendDialectRegistry(registry); context->loadAllAvailableDialects(); @@ -908,7 +908,7 @@ TEST_P(QASM3TranslationTest, ProgramEquivalence) { const auto name = " (" + GetParam().name + ")"; const auto& source = GetParam().source; const auto referenceBuilder = GetParam().referenceBuilder; - mqt::test::DeferredPrinter printer; + ::mqt::test::DeferredPrinter printer; auto translated = qc::translateQASM3ToQC(source, context.get()); ASSERT_TRUE(translated); @@ -922,7 +922,8 @@ TEST_P(QASM3TranslationTest, ProgramEquivalence) { const auto initialization = StringRef(source).contains("OPENQASM 2") ? cbit::Initialization::Zero : cbit::Initialization::Undefined; - auto reference = mqt::test::buildMLIRProgram(context.get(), referenceBuilder); + auto reference = + ::mqt::test::buildMLIRProgram(context.get(), referenceBuilder); ASSERT_TRUE(reference); reference->walk( [&](cbit::AllocOp op) { op.setInitialization(initialization); }); @@ -1005,7 +1006,8 @@ named_result = measure q; cbit::AllocOp classicalRegister; translated->walk([&](cbit::AllocOp op) { classicalRegister = op; }); ASSERT_TRUE(classicalRegister); - const auto name = classicalRegister.getSourceNameAttr(); + const auto name = classicalRegister->getAttrOfType( + ::mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()); ASSERT_TRUE(name); EXPECT_EQ(name.getValue(), "named_result"); } @@ -1056,8 +1058,8 @@ qubit[2] named_qubits; } }); ASSERT_TRUE(qubitRegister); - const auto name = - qubitRegister->getAttrOfType(utils::QUBIT_REGISTER_NAME_ATTR); + const auto name = qubitRegister->getAttrOfType( + mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()); ASSERT_TRUE(name); EXPECT_EQ(name.getValue(), "named_qubits"); } diff --git a/mlir/unittests/Dialect/QCO/IR/CMakeLists.txt b/mlir/unittests/Dialect/QCO/IR/CMakeLists.txt index deca42b010..897b407f08 100644 --- a/mlir/unittests/Dialect/QCO/IR/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/IR/CMakeLists.txt @@ -12,6 +12,7 @@ target_link_libraries( ${qco_ir_target} PRIVATE MLIRParser MLIRCBitDialect + MLIRMQTDialect MLIRSupportMQT GTest::gtest_main MLIRQCOProgramBuilder diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp index 5d4751f8da..7ddfa81cdc 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp @@ -12,6 +12,7 @@ #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/QCO/Builder/QCOProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" @@ -56,7 +57,6 @@ #include #include #include -#include #include #include @@ -237,17 +237,6 @@ TEST_F(QCOTest, BuilderRejectsUntrackedTensorInitArg) { "Invalid tensor value used"); } -TEST_F(QCOTest, BuilderRejectsDuplicateNonEmptyQubitRegisterNames) { - EXPECT_DEATH( - { - QCOProgramBuilder builder(context.get()); - builder.initialize(); - std::ignore = builder.allocQubitRegister(1, "q"); - std::ignore = builder.allocQubitRegister(1, "q"); - }, - "Qubit register names must be unique"); -} - TEST_F(QCOTest, BuilderRejectsOutOfBoundsClassicalRegisterIndices) { EXPECT_DEATH( { @@ -304,10 +293,16 @@ TEST_F(QCOTest, BuilderSupportsIndependentClassicalRegisterInitialization) { moduleOp->walk([&](cbit::AllocOp op) { allocations.push_back(op); }); ASSERT_EQ(allocations.size(), 2); EXPECT_EQ(allocations[0].getInitialization(), cbit::Initialization::Zero); - EXPECT_FALSE(allocations[0].getSourceNameAttr()); + EXPECT_FALSE(allocations[0]->getAttr( + ::mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr())); EXPECT_EQ(allocations[1].getInitialization(), cbit::Initialization::Undefined); - EXPECT_EQ(allocations[1].getSourceName(), "undefined"); + EXPECT_EQ( + allocations[1] + ->getAttrOfType( + ::mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()) + .getValue(), + "undefined"); } TEST_F(QCOTest, DirectSingleQubitPowBuilder) { @@ -333,6 +328,48 @@ TEST_F(QCOTest, DirectSingleQubitPowBuilder) { EXPECT_TRUE(pow.verify().succeeded()); } +TEST_F(QCOTest, UnitaryVerifierRejectsNonFiniteConstantParameters) { + constexpr std::array invalidPrograms{ + R"mlir( + module { + func.func @main(%input: f64) { + %q = qco.alloc : !qco.qubit + %infinity = arith.constant 0x7FF0000000000000 : f64 + %theta = arith.addf %input, %infinity : f64 + %out = qco.rx(%theta) %q : !qco.qubit -> !qco.qubit + qco.sink %out : !qco.qubit + return + } + } + )mlir", + R"mlir( + module { + func.func @main() { + %q = qco.alloc : !qco.qubit + %nan = arith.constant 0x7FF8000000000000 : f64 + %out = qco.pow(%nan) (%arg = %q) { + %body = qco.x %arg : !qco.qubit -> !qco.qubit + qco.yield %body : !qco.qubit + } : {!qco.qubit} -> {!qco.qubit} + qco.sink %out : !qco.qubit + return + } + } + )mlir"}; + + for (const auto source : invalidPrograms) { + bool sawExpectedDiagnostic = false; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + 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); + } +} + namespace { enum class VerifierModifierKind : uint8_t { Inv, Ctrl, Pow }; @@ -422,7 +459,7 @@ static Operation* buildInvalidNestedModifierBody( case ForbiddenModifierBodyOp::CBitAlloc: cbit::AllocOp::create( builder, cbit::RegisterType::get(builder.getContext(), 1), - cbit::Initialization::Zero, StringAttr{}); + cbit::Initialization::Zero); break; case ForbiddenModifierBodyOp::CBitLoad: cbit::LoadOp::create(builder, builder.getI1Type(), cbitReg, @@ -666,7 +703,7 @@ TEST_F(QCOTest, IfOpParser) { // Test IfOp parser const char* mlirCode = R"( module { - func.func @main() -> i1 attributes {passthrough = ["entry_point"]} { + func.func @main() -> i1 attributes {mqt.entry_point} { %c0 = arith.constant 0 : index %c1 = arith.constant 1 : index %q0_0 = qco.alloc : !qco.qubit @@ -929,7 +966,7 @@ TEST_F(QCOTest, IndexSwitchParser) { // Test IndexSwitch parser const char* mlirCode = R"( module { - func.func @main() -> !cbit.reg<3> attributes {passthrough = ["entry_point"]} { + func.func @main() -> !cbit.reg<3> attributes {mqt.entry_point} { %c2 = arith.constant 2 : index %c1 = arith.constant 1 : index %c0 = arith.constant 0 : index diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/Mapping/CMakeLists.txt index 994965634f..49b90f6ac3 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/CMakeLists.txt @@ -13,6 +13,7 @@ target_link_libraries( ${target_name} PRIVATE GTest::gtest_main MLIRParser + MLIRMQTDialect MQTCompilerTarget MLIRQCOProgramBuilder MLIRQTensorUtils diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index 6b9f7e92b5..012a02a52c 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -9,6 +9,7 @@ */ #include "mlir/Compiler/Target.h" +#include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" @@ -17,7 +18,6 @@ #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" -#include "mlir/Dialect/Utils/Utils.h" #include "mlir/Support/Passes.h" #include @@ -60,7 +60,7 @@ using namespace mlir; using namespace mlir::qco; -using namespace mlir::utils; +using mlir::mqt::getEntryPoint; static SmallVector getQubitValues(ValueRange values) { return to_vector(llvm::make_filter_range( @@ -296,8 +296,8 @@ class MappingPassFixture : public testing::Test { protected: void SetUp() override { DialectRegistry registry; - registry.insert(); + registry.insert(); context = std::make_unique(); context->appendDialectRegistry(registry); context->loadAllAvailableDialects(); @@ -597,7 +597,7 @@ TEST_P(MappingPassTest, FailNestedScalarAllocation) { const auto& target = GetParam(); constexpr StringLiteral source = R"mlir( module { - func.func @main() attributes {passthrough = ["entry_point"]} { + func.func @main() attributes {mqt.entry_point} { %condition = arith.constant true %q0 = qco.alloc : !qco.qubit %q1 = qco.if %condition args(%arg0 = %q0) -> (!qco.qubit) { @@ -635,7 +635,7 @@ TEST_P(MappingPassTest, FailNestedTensorAllocation) { const auto& target = GetParam(); constexpr StringLiteral source = R"mlir( module { - func.func @main() attributes {passthrough = ["entry_point"]} { + func.func @main() attributes {mqt.entry_point} { %condition = arith.constant true %c1 = arith.constant 1 : index %q0 = qco.alloc : !qco.qubit @@ -983,7 +983,7 @@ TEST_P(MappingPassTest, MapParallelLoopsWithClassicalDependencies) { const auto& target = GetParam(); constexpr StringLiteral source = R"mlir( module { - func.func @main() attributes {passthrough = ["entry_point"]} { + func.func @main() attributes {mqt.entry_point} { %c0 = arith.constant 0 : index %c1 = arith.constant 1 : index %q0 = qco.alloc : !qco.qubit @@ -1048,7 +1048,7 @@ TEST_P(MappingPassTest, MapForWithClassicalIterArg) { const auto& target = GetParam(); constexpr StringLiteral source = R"mlir( module { - func.func @main() -> i64 attributes {passthrough = ["entry_point"]} { + func.func @main() -> i64 attributes {mqt.entry_point} { %c0 = arith.constant 0 : index %c1 = arith.constant 1 : index %c2 = arith.constant 2 : index @@ -1092,7 +1092,7 @@ TEST_P(MappingPassTest, MapTypeChangingWhileWithClassicalState) { const auto& target = GetParam(); constexpr StringLiteral source = R"mlir( module { - func.func @main() -> i64 attributes {passthrough = ["entry_point"]} { + func.func @main() -> i64 attributes {mqt.entry_point} { %c0 = arith.constant 0 : index %c1 = arith.constant 1 : index %c2 = arith.constant 2 : index @@ -1143,7 +1143,7 @@ TEST_P(MappingPassTest, MapIfWithClassicalResult) { const auto& target = GetParam(); constexpr StringLiteral source = R"mlir( module { - func.func @main() -> i64 attributes {passthrough = ["entry_point"]} { + func.func @main() -> i64 attributes {mqt.entry_point} { %c0 = arith.constant 0 : index %c1 = arith.constant 1 : index %c2 = arith.constant 2 : index @@ -1202,7 +1202,7 @@ TEST_P(MappingPassTest, MapIndexSwitchWithClassicalResult) { constexpr StringLiteral source = R"mlir( module { func.func @main(%selector: index) -> i64 - attributes {passthrough = ["entry_point"]} { + attributes {mqt.entry_point} { %c0 = arith.constant 0 : index %c1 = arith.constant 1 : index %c2 = arith.constant 2 : index @@ -1268,7 +1268,7 @@ TEST_P(MappingPassTest, MapIndexSwitchRegions) { constexpr StringLiteral source = R"mlir( module { func.func @main(%selector: index) - attributes {passthrough = ["entry_point"]} { + attributes {mqt.entry_point} { %c0 = arith.constant 0 : index %c1 = arith.constant 1 : index %c2 = arith.constant 2 : index @@ -1330,7 +1330,7 @@ TEST_P(MappingPassTest, MapNestedOperationOnceWhileIndependentWiresAdvance) { constexpr StringLiteral source = R"mlir( module { func.func @main(%selector: index) - attributes {passthrough = ["entry_point"]} { + attributes {mqt.entry_point} { %c0 = arith.constant 0 : index %c1 = arith.constant 1 : index %c2 = arith.constant 2 : index diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_reuse_qubits.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_reuse_qubits.cpp index 53d0efd635..1abf58abe2 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_reuse_qubits.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_reuse_qubits.cpp @@ -196,7 +196,7 @@ TEST_F(QCOQubitReuseTest, preserveEffectfulUserOrder) { module { func.func private @record0(i1) func.func private @record1(i1) - func.func @main() attributes {passthrough = ["entry_point"]} { + func.func @main() attributes {mqt.entry_point} { %q0 = qco.alloc : !qco.qubit %q1 = qco.alloc : !qco.qubit %q1_h = qco.h %q1 : !qco.qubit -> !qco.qubit @@ -234,7 +234,7 @@ TEST_F(QCOQubitReuseTest, preserveEffectfulUserOrder) { TEST_F(QCOQubitReuseTest, skipReuseAcrossBlocks) { module = parseSourceString(R"mlir( module { - func.func @main() -> (i1, i1) attributes {passthrough = ["entry_point"]} { + func.func @main() -> (i1, i1) attributes {mqt.entry_point} { %q0 = qco.alloc : !qco.qubit %q1 = qco.alloc : !qco.qubit %q0_m, %c0 = qco.measure %q0 : !qco.qubit diff --git a/mlir/unittests/Dialect/QTensor/CMakeLists.txt b/mlir/unittests/Dialect/QTensor/CMakeLists.txt index e4045824ac..7b705e9b19 100644 --- a/mlir/unittests/Dialect/QTensor/CMakeLists.txt +++ b/mlir/unittests/Dialect/QTensor/CMakeLists.txt @@ -7,4 +7,5 @@ # Licensed under the MIT License add_subdirectory(IR) +add_subdirectory(Transforms) add_subdirectory(Utils) diff --git a/mlir/unittests/Dialect/QTensor/Transforms/CMakeLists.txt b/mlir/unittests/Dialect/QTensor/Transforms/CMakeLists.txt new file mode 100644 index 0000000000..8b566d4913 --- /dev/null +++ b/mlir/unittests/Dialect/QTensor/Transforms/CMakeLists.txt @@ -0,0 +1,22 @@ +# 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 + +set(target_name mqt-core-mlir-unittest-qtensor-transforms) +add_executable(${target_name} test_qtensor_transforms.cpp) +target_link_libraries( + ${target_name} + PRIVATE GTest::gtest_main + MLIRMQTDialect + MLIRParser + MLIRPass + MLIRQCODialect + MLIRQTensorTransforms + MLIRSupport) +mqt_mlir_configure_unittest_target(${target_name}) + +gtest_discover_tests(${target_name} PROPERTIES LABELS mqt-mlir-unittests DISCOVERY_TIMEOUT 60) diff --git a/mlir/unittests/Dialect/QTensor/Transforms/test_qtensor_transforms.cpp b/mlir/unittests/Dialect/QTensor/Transforms/test_qtensor_transforms.cpp new file mode 100644 index 0000000000..b16c3a8ed1 --- /dev/null +++ b/mlir/unittests/Dialect/QTensor/Transforms/test_qtensor_transforms.cpp @@ -0,0 +1,81 @@ +/* + * 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 + */ + +/** + * @file test_qtensor_transforms.cpp + * @brief Unit tests for QTensor dialect transformations. + */ + +#include "mlir/Dialect/MQT/IR/MQTDialect.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QTensor/IR/QTensorDialect.h" +#include "mlir/Dialect/QTensor/IR/QTensorOps.h" +#include "mlir/Dialect/QTensor/Transforms/Passes.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace mlir; + +namespace { +TEST(QTensorTransformsTest, ShrinkToFitPreservesMetadata) { + 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) {mqt.register_name = "q"} + : tensor<3x!qco.qubit> + %rest, %qubit = qtensor.extract %reg[%c1] : tensor<3x!qco.qubit> + %rotated = qco.x %qubit : !qco.qubit -> !qco.qubit + %updated = qtensor.insert %rotated into %rest[%c1] + : tensor<3x!qco.qubit> + qtensor.dealloc %updated : tensor<3x!qco.qubit> + return + } + } + )mlir", + &context); + ASSERT_TRUE(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}); + EXPECT_EQ(allocation->getAttrOfType( + mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()), + StringAttr::get(&context, "q")); +} +} // namespace diff --git a/mlir/unittests/Dialect/Utils/test_global_phase_normalization.cpp b/mlir/unittests/Dialect/Utils/test_global_phase_normalization.cpp index 8f5ef5c8ca..7bc07ee427 100644 --- a/mlir/unittests/Dialect/Utils/test_global_phase_normalization.cpp +++ b/mlir/unittests/Dialect/Utils/test_global_phase_normalization.cpp @@ -409,35 +409,6 @@ TEST_F(GlobalPhaseNormalizationTest, DynamicPowerRemainsBoundary) { EXPECT_TRUE(func.getBody().getOps().empty()); } -TEST_F(GlobalPhaseNormalizationTest, NonFinitePowerExponentsRemainBoundaries) { - for (const double exponent : {std::numeric_limits::quiet_NaN(), - std::numeric_limits::infinity()}) { - 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}, {qubitType})); - auto* entry = function.addEntryBlock(); - builder.setInsertionPointToStart(entry); - auto pow = qco::PowOp::create( - builder, loc, entry->getArgument(0), exponent, [&](Value target) { - const auto out = qco::XOp::create(builder, loc, target).getQubitOut(); - qco::GPhaseOp::create(builder, loc, - utils::constantFromScalar(builder, loc, 0.371)); - return out; - }); - func::ReturnOp::create(builder, loc, pow.getOutputTarget(0)); - - ASSERT_TRUE(mlir::mqt::normalizeGlobalPhases(*moduleOp).succeeded()); - ASSERT_TRUE(verify(*moduleOp).succeeded()); - EXPECT_EQ(llvm::range_size(pow.getBody()->getOps()), 1); - EXPECT_TRUE(function.getBody().getOps().empty()); - } -} - TEST_F(GlobalPhaseNormalizationTest, FactorsControlledPhaseOntoControl) { auto moduleOp = parse(R"mlir( module { diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index ed92a69415..611bb73e1a 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -45,7 +45,7 @@ ) MLIR_STRING = r"""module { - func.func @main() -> memref<2xi1> attributes {passthrough = ["entry_point"]} { + func.func @main() -> memref<2xi1> attributes {mqt.entry_point} { %c1 = arith.constant 1 : index %c0 = arith.constant 0 : index %alloc = memref.alloc() : memref<2x!qc.qubit> @@ -583,7 +583,7 @@ def test_qco_program_reuses_qubits() -> None: """Expose the raw and composite qubit-reuse flows.""" independent_qubits = """ module { - func.func @main() attributes {passthrough = ["entry_point"]} { + func.func @main() attributes {mqt.entry_point} { %q0 = qco.alloc : !qco.qubit %q1 = qco.alloc : !qco.qubit %q0_h = qco.h %q0 : !qco.qubit -> !qco.qubit diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index 40c918150c..e89e19f9db 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -14,6 +14,7 @@ import re import subprocess import sys +from typing import TYPE_CHECKING import numpy as np import pytest @@ -27,6 +28,8 @@ Gate, InverseModifier, Parameter, + ParameterExpression, + ParameterVector, PowerModifier, Qubit, library, @@ -37,6 +40,9 @@ from mqt.core.mlir import CompilerTarget, QCProgram, compile_program from mqt.core.plugins.qiskit import qiskit_to_mqt +if TYPE_CHECKING: + from collections.abc import Callable + installed_qiskit = Version(qiskit.__version__) candidate_version = os.environ.get("MQT_QISKIT_TEST_CANDIDATE_VERSION") if not (Version("2.5.0") <= installed_qiskit < Version("2.6.0") or qiskit.__version__ == candidate_version): @@ -224,7 +230,7 @@ def test_controlled_dense_unitary_export_preserves_operation_order() -> None: """Export a controlled dense matrix with a Qiskit control annotation.""" program = QCProgram.from_mlir_str( """module { - func.func @main() attributes {passthrough = ["entry_point"]} { + func.func @main() attributes {mqt.entry_point} { %control = qc.alloc : !qc.qubit %target = qc.alloc : !qc.qubit qc.x %control : !qc.qubit @@ -439,8 +445,8 @@ def test_flat_circuit_round_trip_preserves_supported_metadata() -> None: program = QCProgram.from_qiskit(circuit) restored = program.to_qiskit() - assert 'mqt.qubit_register_name = "input"' in program.ir - assert 'cbit.alloc(#cbit.init) source_name = "output"' in program.ir + assert 'mqt.register_name = "input"' in program.ir + assert 'cbit.alloc(#cbit.init) {mqt.register_name = "output"}' in program.ir assert restored.global_phase == pytest.approx(0.125) assert [(reg.name, len(reg)) for reg in restored.qregs] == [("input", 2)] assert [(reg.name, len(reg)) for reg in restored.cregs] == [("output", 2)] @@ -483,7 +489,7 @@ def test_flat_export_rejects_classical_store_after_quantum_work(late_value: str) """Reject constant CBit stores regardless of their position.""" program = QCProgram.from_mlir_str( f"""module {{ - func.func @main() -> !cbit.reg<2> attributes {{passthrough = ["entry_point"]}} {{ + func.func @main() -> !cbit.reg<2> attributes {{mqt.entry_point}} {{ %c0 = arith.constant 0 : index %c1 = arith.constant 1 : index %initial = arith.constant false @@ -544,7 +550,7 @@ def test_openqasm3_measurement_export_uses_undefined_cbit_register() -> None: restored = program.to_qiskit() assert "ub.poison" not in program.ir - assert 'cbit.alloc(#cbit.init) source_name = "c"' in program.ir + assert 'cbit.alloc(#cbit.init) {mqt.register_name = "c"}' in program.ir assert [(register.name, len(register)) for register in restored.qregs] == [("q", 2)] assert [(register.name, len(register)) for register in restored.cregs] == [("c", 1)] assert [item.operation.name for item in restored.data] == ["h", "measure"] @@ -557,7 +563,7 @@ def test_flat_export_rejects_undefined_returned_bits() -> None: """Reject a returned undefined register unless every bit is written.""" program = QCProgram.from_mlir_str( """module { - func.func @main() -> !cbit.reg<1> attributes {passthrough = ["entry_point"]} { + func.func @main() -> !cbit.reg<1> attributes {mqt.entry_point} { %q = qc.alloc : !qc.qubit %c = cbit.alloc(#cbit.init) : !cbit.reg<1> qc.dealloc %q : !qc.qubit @@ -590,10 +596,10 @@ def test_qiskit_export_excludes_internal_cbit_registers() -> None: """Export only CBit registers returned by the entry function.""" program = QCProgram.from_mlir_str( """module { - func.func @main() -> !cbit.reg<1> attributes {passthrough = ["entry_point"]} { + func.func @main() -> !cbit.reg<1> attributes {mqt.entry_point} { %q = qc.alloc : !qc.qubit - %output = cbit.alloc(#cbit.init) source_name = "output" : !cbit.reg<1> - %internal = cbit.alloc(#cbit.init) source_name = "internal" : !cbit.reg<2> + %output = cbit.alloc(#cbit.init) {mqt.register_name = "output"} : !cbit.reg<1> + %internal = cbit.alloc(#cbit.init) {mqt.register_name = "internal"} : !cbit.reg<2> qc.dealloc %q : !qc.qubit return %output : !cbit.reg<1> } @@ -621,7 +627,7 @@ def test_qiskit_export_rejects_measurement_with_multiple_destinations() -> None: """Reject one measurement result stored in more than one public bit.""" program = QCProgram.from_mlir_str( """module { - func.func @main() -> !cbit.reg<2> attributes {passthrough = ["entry_point"]} { + func.func @main() -> !cbit.reg<2> attributes {mqt.entry_point} { %c0 = arith.constant 0 : index %c1 = arith.constant 1 : index %q = qc.alloc : !qc.qubit @@ -644,7 +650,7 @@ def test_qiskit_export_rejects_dynamic_measurement_destination() -> None: """Require each Qiskit measurement destination to be static.""" program = QCProgram.from_mlir_str( """module { - func.func @main() -> !cbit.reg<1> attributes {passthrough = ["entry_point"]} { + func.func @main() -> !cbit.reg<1> attributes {mqt.entry_point} { %c0 = arith.constant 0 : index %index = arith.addi %c0, %c0 : index %q = qc.alloc : !qc.qubit @@ -704,7 +710,7 @@ def test_nested_numeric_custom_definitions_are_inlined() -> None: def test_ambiguous_custom_parameter_binding_is_rejected() -> None: - """Do not infer formal parameter order from Qiskit's sorted parameter set.""" + """Reject custom-definition symbols absent from the enclosing circuit.""" z = Parameter("z") a = Parameter("a") definition = QuantumCircuit(1) @@ -715,7 +721,7 @@ def test_ambiguous_custom_parameter_binding_is_rejected() -> None: circuit = QuantumCircuit(1) circuit.append(gate, [0]) - with pytest.raises(RuntimeError, match="must be numerically bound before import"): + with pytest.raises(RuntimeError, match="parameter symbol 'z' is not defined"): QCProgram.from_qiskit(circuit) @@ -835,9 +841,12 @@ def test_rejections_do_not_modify_source_circuits() -> None: """Reject unsupported parameters and inputs without mutation.""" theta = Parameter("theta") symbolic = QuantumCircuit(1) - symbolic.rx(theta, 0) + symbolic.rx(theta.sign(), 0) symbolic_data = list(symbolic.data) - with pytest.raises(RuntimeError, match="free symbolic parameter 'theta'"): + with pytest.raises( + RuntimeError, + match=r"(?i)Qiskit parameter expression operation 'sign' is not supported", + ): QCProgram.from_qiskit(symbolic) assert list(symbolic.data) == symbolic_data assert symbolic.parameters == {theta} @@ -852,6 +861,79 @@ def test_rejections_do_not_modify_source_circuits() -> None: assert list(runtime_input.data) == input_data +@pytest.mark.parametrize("value", [np.inf, np.nan], ids=["infinity", "nan"]) +def test_nonfinite_parameters_fail_closed_without_mutation(value: float) -> None: + """Reject non-finite scalar parameters before changing the source circuit.""" + circuit = QuantumCircuit(1) + circuit.rx(value, 0) + source_data = list(circuit.data) + + with pytest.raises(RuntimeError, match="Qiskit returned a non-finite parameter"): + QCProgram.from_qiskit(circuit) + + assert len(circuit.data) == len(source_data) + current = circuit.data[0] + original = source_data[0] + assert current.operation.name == original.operation.name == "rx" + assert current.qubits == original.qubits + assert current.clbits == original.clbits + assert len(current.operation.params) == 1 + assert np.isnan(current.operation.params[0]) == np.isnan(value) + assert np.isinf(current.operation.params[0]) == np.isinf(value) + + +def test_complex_parameter_expression_fails_closed_without_mutation() -> None: + """Reject a complex-valued expression before changing the source circuit.""" + theta = Parameter("theta") + circuit = QuantumCircuit(1) + circuit.rx(theta + 1j, 0) + source_data = list(circuit.data) + + with pytest.raises(RuntimeError, match="parameter expressions with complex values are not supported"): + QCProgram.from_qiskit(circuit) + + assert list(circuit.data) == source_data + assert circuit.parameters == {theta} + + +def test_excessively_nested_parameter_expression_fails_closed_without_mutation() -> None: + """Bound parameter-expression traversal before changing the source circuit.""" + theta = Parameter("theta") + angle: ParameterExpression = theta + for _ in range(65): + angle = angle.sin() + circuit = QuantumCircuit(1) + circuit.rz(angle, 0) + source_data = list(circuit.data) + + with pytest.raises(RuntimeError, match="exceeds the supported 64-level nesting depth"): + QCProgram.from_qiskit(circuit) + + assert list(circuit.data) == source_data + assert circuit.parameters == {theta} + + +def test_oversized_parameter_expression_fails_closed_without_mutation() -> None: + """Bound a wide parameter expression before changing the source circuit.""" + theta = Parameter("theta") + level: list[ParameterExpression] = [theta] + level.extend(theta + float(index) for index in range(1, 2049)) + while len(level) > 1: + level = [ + level[index] + level[index + 1] if index + 1 < len(level) else level[index] + for index in range(0, len(level), 2) + ] + circuit = QuantumCircuit(1) + circuit.rz(level[0], 0) + source_data = list(circuit.data) + + with pytest.raises(RuntimeError, match="exceeds the supported 4096-node size"): + QCProgram.from_qiskit(circuit) + + assert list(circuit.data) == source_data + assert circuit.parameters == {theta} + + @pytest.mark.parametrize("resource", ["quantum", "classical"]) @pytest.mark.parametrize("layout", ["alias", "interleaved"]) def test_noncanonical_register_membership_is_rejected(resource: str, layout: str) -> None: @@ -968,11 +1050,469 @@ def test_excessively_nested_control_flow_is_rejected() -> None: QCProgram.from_qiskit(body) -def test_flat_export_rejects_symbolic_inputs() -> None: - """Reject program inputs before allocating an output circuit.""" +def test_direct_symbolic_parameters_round_trip_with_shared_identity() -> None: + """Represent a shared Qiskit parameter as one named f64 input.""" + theta = Parameter("theta") + circuit = QuantumCircuit(1, global_phase=theta) + circuit.ry(theta, 0) + circuit.rz(theta, 0) + + program = QCProgram.from_qiskit(circuit) + restored = program.to_qiskit() + + assert 'mqt.input_name = "theta"' in program.ir + assert len(restored.parameters) == 1 + restored_theta = next(iter(restored.parameters)) + assert restored.global_phase == restored_theta + assert restored.data[0].operation.params[0] == restored_theta + assert restored.data[1].operation.params[0] == restored_theta + value = 0.375 + assert np.allclose( + Operator(restored.assign_parameters({restored_theta: value})).data, + Operator(circuit.assign_parameters({theta: value})).data, + ) + + +def test_parameter_vector_elements_fail_import_without_mutation() -> None: + """Reject vector elements until the provenance follow-up is applied.""" + vector = ParameterVector("theta", 2) + circuit = QuantumCircuit(1) + circuit.rx(vector[0], 0) + source_data = list(circuit.data) + + with pytest.raises(RuntimeError, match="parameter-vector elements are not supported"): + QCProgram.from_qiskit(circuit) + + assert list(circuit.data) == source_data + assert circuit.parameters == {vector[0]} + + +def test_standalone_bracket_parameter_names_remain_standalone() -> None: + """Do not infer an input group from a standalone parameter's name.""" + theta_ten = Parameter("theta[10]") + theta_two = Parameter("theta[2]") + circuit = QuantumCircuit(1) + circuit.rx(theta_ten, 0) + circuit.ry(theta_two, 0) + + program = QCProgram.from_qiskit(circuit) + restored = program.to_qiskit() + + assert "mqt.input_group" not in program.ir + assert {parameter.name for parameter in restored.parameters} == {"theta[2]", "theta[10]"} + values = [0.1, 0.2] + assert Operator(restored.assign_parameters(values)).equiv(Operator(circuit.assign_parameters(values))) + + +def _assign_parameter_values(circuit: QuantumCircuit, values: dict[str, float]) -> QuantumCircuit: + """Bind a circuit using parameter names after an import/export round trip. + + Returns: + A copy of the circuit with all parameters bound. + """ + return circuit.assign_parameters({parameter: values[parameter.name] for parameter in circuit.parameters}) + + +def test_nested_symbolic_arithmetic_round_trip_with_shared_global_phase() -> None: + """Preserve nested arithmetic and shared symbols in gates and global phase.""" + theta = Parameter("theta") + phi = Parameter("phi") + angle = -((2 - theta) * (phi.sin() + 0.25) / (theta**2 + 1)) + circuit = QuantumCircuit(1, global_phase=theta + phi) + circuit.ry(angle, 0) + circuit.rz(theta + phi, 0) + + program = QCProgram.from_qiskit(circuit) + restored = program.to_qiskit() + + assert {parameter.name for parameter in restored.parameters} == {"phi", "theta"} + assert len(restored.parameters) == 2 + values = {"phi": 0.4, "theta": -0.3} + assert np.allclose( + Operator(_assign_parameter_values(restored, values)).data, + Operator(_assign_parameter_values(circuit, values)).data, + ) + + +@pytest.mark.parametrize( + ("operation", "value"), + [ + (lambda parameter: 2 + parameter, 0.25), + (lambda parameter: 2 - parameter, 0.25), + (lambda parameter: 2 * parameter, 0.25), + (lambda parameter: 2 / parameter, 0.75), + (lambda parameter: 2**parameter, -0.5), + ], + ids=["reverse-add", "reverse-subtract", "reverse-multiply", "reverse-divide", "reverse-power"], +) +def test_reverse_symbolic_arithmetic_round_trip( + operation: Callable[[Parameter], ParameterExpression], value: float +) -> None: + """Preserve Qiskit's reflected arithmetic operators.""" + theta = Parameter("theta") + circuit = QuantumCircuit(1) + circuit.rz(operation(theta), 0) + + restored = QCProgram.from_qiskit(circuit).to_qiskit() + + values = {"theta": value} + assert np.allclose( + Operator(_assign_parameter_values(restored, values)).data, + Operator(_assign_parameter_values(circuit, values)).data, + ) + + +@pytest.mark.parametrize( + ("operation", "value"), + [ + (lambda parameter: parameter.sin(), 0.2), + (lambda parameter: parameter.cos(), 0.2), + (lambda parameter: parameter.tan(), 0.2), + (lambda parameter: parameter.arcsin(), 0.2), + (lambda parameter: parameter.arccos(), 0.2), + (lambda parameter: parameter.arctan(), 0.2), + (lambda parameter: parameter.exp(), 0.2), + (lambda parameter: parameter.log(), 1.2), + (abs, -0.2), + (lambda parameter: parameter.conjugate(), 0.2), + ], + ids=["sin", "cos", "tan", "arcsin", "arccos", "arctan", "exp", "log", "abs", "conjugate"], +) +def test_symbolic_unary_function_round_trip( + operation: Callable[[Parameter], ParameterExpression], value: float +) -> None: + """Preserve supported unary Qiskit parameter functions.""" + theta = Parameter("theta") + circuit = QuantumCircuit(1) + circuit.rx(operation(theta), 0) + + restored = QCProgram.from_qiskit(circuit).to_qiskit() + + values = {"theta": value} + assert np.allclose( + Operator(_assign_parameter_values(restored, values)).data, + Operator(_assign_parameter_values(circuit, values)).data, + ) + + +def test_partially_bound_symbolic_expression_round_trip() -> None: + """Keep the unbound parameter after partially binding an expression.""" + theta = Parameter("theta") + phi = Parameter("phi") + angle = (theta * phi + phi.sin()).bind({theta: 0.5}) + circuit = QuantumCircuit(1) + circuit.ry(angle, 0) + + restored = QCProgram.from_qiskit(circuit).to_qiskit() + + assert {parameter.name for parameter in restored.parameters} == {"phi"} + values = {"phi": 0.4} + assert np.allclose( + Operator(_assign_parameter_values(restored, values)).data, + Operator(_assign_parameter_values(circuit, values)).data, + ) + + +def test_float_castable_symbolic_expression_keeps_parameter_identity() -> None: + """Do not collapse a float-castable expression that still tracks a symbol.""" + theta = Parameter("theta") + angle = (theta - theta) + 2 + assert angle.parameters == {theta} + assert float(angle) == pytest.approx(2) + circuit = QuantumCircuit(1) + circuit.rz(angle, 0) + + program = QCProgram.from_qiskit(circuit) + restored = program.to_qiskit() + + assert 'mqt.input_name = "theta"' in program.ir + assert {parameter.name for parameter in restored.parameters} == {"theta"} + values = {"theta": 0.3} + assert np.allclose( + Operator(_assign_parameter_values(restored, values)).data, + Operator(_assign_parameter_values(circuit, values)).data, + ) + + +def test_parameterized_custom_definition_round_trip() -> None: + """Substitute symbolic call parameters while recursively inlining a definition.""" + formal = Parameter("formal") + definition = QuantumCircuit(1) + definition.rx(formal + 1, 0) + custom = definition.to_gate(label="symbolic") + circuit = QuantumCircuit(1) + circuit.append(custom, [0]) + theta = Parameter("theta") + circuit.assign_parameters({formal: theta + 0.25}, inplace=True) + + restored = QCProgram.from_qiskit(circuit).to_qiskit() + + assert {parameter.name for parameter in restored.parameters} == {"theta"} + values = {"theta": -0.2} + assert np.allclose( + Operator(_assign_parameter_values(restored, values)).data, + Operator(_assign_parameter_values(circuit, values)).data, + ) + + +def test_manual_arith_and_math_parameter_expression_exports_to_qiskit() -> None: + """Reconstruct an expression from generic Arith and Math operations.""" + program = QCProgram.from_mlir_str( + """module { + func.func @main(%theta: f64 {mqt.input_name = "theta"}) attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + %offset = arith.constant 5.000000e-01 : f64 + %sum = arith.addf %theta, %offset : f64 + %angle = math.sin %sum : f64 + qc.rz(%angle) %q : !qc.qubit + qc.dealloc %q : !qc.qubit + return + } +} +""" + ) + + restored = program.to_qiskit() + + theta = next(iter(restored.parameters)) + bound = restored.assign_parameters({theta: 0.25}) + assert bound.data[0].operation.params[0] == pytest.approx(np.sin(0.75)) + + +def _wide_parameter_expression_program(term_count: int) -> QCProgram: + """Build a program whose gate angle sums ``term_count`` distinct math.sin terms. + + Returns: + A QC program with a wide scalar parameter-expression graph. + """ + lines = [ + "module {", + ' func.func @main(%theta: f64 {mqt.input_name = "theta"}) attributes {mqt.entry_point} {', + " %q = qc.alloc : !qc.qubit", + ] + values = [] + for index in range(term_count): + value = f"%term{index}" + lines.append(f" {value} = math.sin %theta : f64") + values.append(value) + sum_index = 0 + while len(values) > 1: + next_values = [] + for index in range(0, len(values), 2): + if index + 1 == len(values): + next_values.append(values[index]) + continue + value = f"%sum{sum_index}" + sum_index += 1 + lines.append(f" {value} = arith.addf {values[index]}, {values[index + 1]} : f64") + next_values.append(value) + values = next_values + lines.extend([ + f" qc.rz({values[0]}) %q : !qc.qubit", + " qc.dealloc %q : !qc.qubit", + " return", + " }", + "}", + ]) + return QCProgram.from_mlir_str("\n".join(lines)) + + +@pytest.mark.parametrize( + "term_count", + [1366, 2049], + ids=["expanded-tree", "unique-ssa-graph"], +) +def test_oversized_export_parameter_expression_fails_without_mutation(term_count: int) -> None: + """Bound normalized trees and compiler SSA traversal before Qiskit construction.""" + program = _wide_parameter_expression_program(term_count) + source_ir = program.ir + + with pytest.raises(RuntimeError, match="exceeds the supported 4096-node size"): + program.to_qiskit() + + assert program.ir == source_ir + + +def test_unsupported_scalar_operation_fails_export_without_mutation() -> None: + """Reject an unsupported f64 producer before changing the source program.""" + program = QCProgram.from_mlir_str( + """module { + func.func @main(%theta: f64 {mqt.input_name = "theta"}) attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + %angle = math.sqrt %theta : f64 + qc.rz(%angle) %q : !qc.qubit + qc.dealloc %q : !qc.qubit + return + } +} +""" + ) + source_ir = program.ir + + with pytest.raises( + RuntimeError, + match=r"Qiskit circuit export does not support scalar parameter operation 'math\.sqrt'", + ): + program.to_qiskit() + + assert program.ir == source_ir + + +def test_same_name_free_and_bound_parameters_are_rejected() -> None: + """Require free and lexically bound parameters to have unique names.""" + global_parameter = Parameter("theta") + loop_parameter = Parameter("theta") + body = QuantumCircuit(1) + body.ry(global_parameter, 0) + circuit = QuantumCircuit(1) + with pytest.warns(UserWarning, match="loop_parameter was not found"): + circuit.for_loop(range(2), loop_parameter, body, [0], [], label=None) + source_data = list(circuit.data) + + with pytest.raises(RuntimeError, match="distinct parameters with the same name"): + QCProgram.from_qiskit(circuit) + + assert list(circuit.data) == source_data + assert circuit.parameters == {global_parameter} + + +def test_bound_parameter_names_are_unique_across_scopes() -> None: + """Reject same-name binders in separate lexical scopes.""" + first = Parameter("index") + first_body = QuantumCircuit(1) + first_body.rx(first, 0) + second = Parameter("index") + second_body = QuantumCircuit(1) + second_body.ry(second, 0) + circuit = QuantumCircuit(1) + circuit.for_loop(range(2), first, first_body, [0], [], label=None) + circuit.for_loop(range(2), second, second_body, [0], [], label=None) + source_data = list(circuit.data) + + with pytest.raises(RuntimeError, match="distinct parameters with the same name"): + QCProgram.from_qiskit(circuit) + + assert list(circuit.data) == source_data + assert not circuit.parameters + + +def test_duplicate_named_symbolic_inputs_are_invalid_qc_ir() -> None: + """Reject duplicate program input names when parsing QC IR.""" + with pytest.raises(RuntimeError, match="MLIR operation failed"): + QCProgram.from_mlir_str( + """module { + func.func @main( + %first: f64 {mqt.input_name = "theta"}, + %second: f64 {mqt.input_name = "theta"} + ) attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + qc.rx(%first) %q : !qc.qubit + qc.rz(%second) %q : !qc.qubit + qc.dealloc %q : !qc.qubit + return + } +} +""" + ) + + +def test_parameter_and_register_names_must_be_unique() -> None: + """Reject a parameter and register that share one program name.""" + theta = Parameter("theta") + register = QuantumRegister(1, "theta") + circuit = QuantumCircuit(register) + circuit.rx(theta, register[0]) + source_data = list(circuit.data) + + with pytest.raises(RuntimeError, match="unique parameter and register names"): + QCProgram.from_qiskit(circuit) + + assert list(circuit.data) == source_data + + with pytest.raises(RuntimeError, match="MLIR operation failed"): + QCProgram.from_mlir_str( + """module { + func.func @main(%theta: f64 {mqt.input_name = "theta"}) attributes {mqt.entry_point} { + %q = memref.alloc() {mqt.register_name = "theta"} : memref<1x!qc.qubit> + memref.dealloc %q : memref<1x!qc.qubit> + return + } +} +""" + ) + + +def test_parameter_names_with_null_characters_fail_closed() -> None: + """Reject names that the Qiskit C API would silently truncate.""" + parameter = Parameter("before\0after") + circuit = QuantumCircuit(1) + circuit.rz(parameter, 0) + source_data = list(circuit.data) + + with pytest.raises(RuntimeError, match="names cannot contain null characters"): + QCProgram.from_qiskit(circuit) + + assert list(circuit.data) == source_data + + with pytest.raises(RuntimeError, match="MLIR operation failed"): + QCProgram.from_mlir_str( + r"""module { + func.func @main(%theta: f64 {mqt.input_name = "before\00after"}) attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + qc.rz(%theta) %q : !qc.qubit + qc.dealloc %q : !qc.qubit + return + } +} +""" + ) + + +def test_named_symbolic_input_exports_to_qiskit() -> None: + """Reconstruct a direct Qiskit parameter from a named f64 input.""" symbolic = QCProgram.from_mlir_str( """module { - func.func @main(%theta: f64) attributes {passthrough = ["entry_point"]} { + func.func @main(%theta: f64 {mqt.input_name = "theta"}) attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + qc.rx(%theta) %q : !qc.qubit + qc.dealloc %q : !qc.qubit + return + } +} +""" + ) + restored = symbolic.to_qiskit() + + assert [parameter.name for parameter in restored.parameters] == ["theta"] + assert restored.data[0].operation.params[0] == next(iter(restored.parameters)) + + +def test_unused_named_symbolic_input_fails_export_without_mutation() -> None: + """Reject a compiler input that would disappear from the Qiskit circuit.""" + program = QCProgram.from_mlir_str( + """module { + func.func @main(%theta: f64 {mqt.input_name = "theta"}) attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + qc.x %q : !qc.qubit + qc.dealloc %q : !qc.qubit + return + } +} +""" + ) + source_ir = program.ir + + with pytest.raises(RuntimeError, match="cannot preserve unused named f64 program input 'theta'"): + program.to_qiskit() + + assert program.ir == source_ir + + +def test_unnamed_runtime_input_is_rejected_on_export() -> None: + """Do not infer source semantics for arbitrary runtime inputs.""" + runtime = QCProgram.from_mlir_str( + """module { + func.func @main(%theta: f64) attributes {mqt.entry_point} { %q = qc.alloc : !qc.qubit qc.rx(%theta) %q : !qc.qubit qc.dealloc %q : !qc.qubit @@ -981,8 +1521,30 @@ def test_flat_export_rejects_symbolic_inputs() -> None: } """ ) - with pytest.raises(RuntimeError, match="symbolic or runtime inputs"): - symbolic.to_qiskit() + + with pytest.raises(RuntimeError, match="requires named f64 program inputs"): + runtime.to_qiskit() + + +def test_named_non_f64_runtime_input_is_rejected_on_export() -> None: + """Reject a named compiler input whose type cannot represent a parameter.""" + runtime = QCProgram.from_mlir_str( + """module { + func.func @main(%count: i64 {mqt.input_name = "count"}) attributes {mqt.entry_point} { + %q = qc.alloc : !qc.qubit + qc.x %q : !qc.qubit + qc.dealloc %q : !qc.qubit + return + } +} +""" + ) + source_ir = runtime.ir + + with pytest.raises(RuntimeError, match="requires named f64 program inputs"): + runtime.to_qiskit() + + assert runtime.ir == source_ir def test_target_aware_qiskit_export_maps_sparse_site_ids() -> None: @@ -993,7 +1555,7 @@ def test_target_aware_qiskit_export_maps_sparse_site_ids() -> None: ) program = QCProgram.from_mlir_str( """module { - func.func @main() attributes {passthrough = ["entry_point"]} { + func.func @main() attributes {mqt.entry_point} { %q = qc.static 4294967296 : !qc.qubit qc.x %q : !qc.qubit return @@ -1019,7 +1581,7 @@ def test_target_aware_qiskit_export_rejects_unknown_site() -> None: ) program = QCProgram.from_mlir_str( """module { - func.func @main() attributes {passthrough = ["entry_point"]} { + func.func @main() attributes {mqt.entry_point} { %q = qc.static 30 : !qc.qubit qc.x %q : !qc.qubit return @@ -1051,7 +1613,7 @@ def test_target_aware_qiskit_export_rejects_dynamic_qubits(allocation: str) -> N target = CompilerTarget(2) program = QCProgram.from_mlir_str( f"""module {{ - func.func @main() attributes {{passthrough = ["entry_point"]}} {{ + func.func @main() attributes {{mqt.entry_point}} {{ {allocation} return }}