From ebc32451d6ecf71e23bfb48131c461170fc021f8 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 19 Aug 2026 16:02:10 +0200 Subject: [PATCH 01/17] =?UTF-8?q?=E2=9C=A8=20Support=20symbolic=20Qiskit?= =?UTF-8?q?=20parameters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-5.6 via Codex Signed-off-by: Simon Hofmann --- .agent/plans/qiskit-circuit-translation.md | 67 ++- .agent/plans/qiskit-symbolic-parameters.md | 292 ++++++++++ CHANGELOG.md | 3 +- bindings/mlir/qiskit/Qiskit2_5.cpp | 595 ++++++++++++++++++-- bindings/mlir/qiskit/QiskitExport.cpp | 386 +++++++++++-- bindings/mlir/qiskit/QiskitImport.cpp | 389 ++++++++++--- bindings/mlir/qiskit/QiskitTranslation.h | 39 +- docs/mlir/python_compiler_collection.md | 21 +- mlir/include/mlir/Dialect/Utils/Utils.h | 3 + mlir/lib/Compiler/CMakeLists.txt | 1 + mlir/lib/Compiler/Programs.cpp | 5 +- test/python/test_mlir_qiskit_translation.py | 581 ++++++++++++++++++- 12 files changed, 2168 insertions(+), 214 deletions(-) create mode 100644 .agent/plans/qiskit-symbolic-parameters.md 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..01a2cabd29 --- /dev/null +++ b/.agent/plans/qiskit-symbolic-parameters.md @@ -0,0 +1,292 @@ +# 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. Lexically bound `for`-loop values remain distinct from free parameters, +even when their displayed names match. + +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 identity rather than + 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 identity 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. + +## 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. The importer must compare canonical scalar symbol + metadata and reject such aliases before creating a module. +- 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 identities. +- 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. + +## 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: Key all parameters by their Qiskit identity during import and by + their compiler SSA value during export. Use `mqt.input_name` for the public + scalar name. Rationale: identity prevents lexical capture without storing a + frontend object in MLIR. Date/Author: 2026-08-18 / Codex. +- Decision: Preserve symbol sharing but do not preserve Qiskit's original UUID + across a round trip. Rationale: the compiler input is the frontend-neutral + identity. The writer creates exactly one Qiskit symbol for each input and + reuses it throughout gates and global phase. Date/Author: 2026-08-18 / 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 identity 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. + +## Outcomes & Retrospective + +The scalar implementation is complete. Shared direct symbols, bounded real +expression trees, parameterized definitions, identity-safe loop bindings, and +global phase passed the original focused validation. Unused named inputs now +fail before writer allocation rather than disappearing. The split branch builds +and passes all 158 Qiskit translation tests after #2158 merged. + +## 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`, declared in +`mlir/include/mlir/Dialect/Utils/Utils.h`, for the stable public name of each +compiler input. 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 the loop parameter's Qiskit identity. 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 so its UUID is +preserved. + +Next, change `QiskitImport.cpp` to validate every tree leaf by identity and to +emit each supported node as an `f64` Arith or Math value. Register the Math +dialect in the import context. Key both local and global parameter maps by +identity. 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, finite 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 identity. + +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. Import a `for` loop whose +binder has the same displayed name as a distinct free symbol used in its body. +The gate must use the free function argument, not the loop induction value. + +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 with a kind, +finite numeric value or symbol name and identity, and zero, one, or two child +pointers. `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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 765aed4872..28f7e68b5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/qiskit/Qiskit2_5.cpp b/bindings/mlir/qiskit/Qiskit2_5.cpp index 314b301776..f92a74b5f0 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,302 @@ 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 {.kind = ParameterKind::Number, .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 {.kind = ParameterKind::Number, .number = number}; } - if (nb::hasattr(parameter, "name")) { - return {.number = std::nullopt, - .text = pythonStringAttribute( - parameter, "name", - "Qiskit modifier exponent has an invalid symbol name")}; + + 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 {.kind = ParameterKind::Number, .number = complexNumber.real()}; } - 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)); + if (!nb::hasattr(parameter, "name") || !nb::hasattr(parameter, "uuid")) { + throw std::runtime_error( + "Qiskit parameter expression contains an unsupported operand"); + } + auto name = pythonStringAttribute( + parameter, "name", "Qiskit parameter has an invalid symbol name"); + auto identity = + pythonText(pythonAttribute(parameter, "uuid", + "Qiskit parameter has no stable identity"), + "Qiskit parameter has an invalid stable identity"); + 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"); + } + if (identity.empty()) { + throw std::runtime_error("Qiskit parameter has an empty stable identity"); + } + if (identity.find('\0') != std::string::npos) { + throw std::runtime_error( + "Qiskit parameter identities cannot contain null characters"); + } + Parameter result{.kind = ParameterKind::Symbol, + .text = std::move(name), + .identity = std::move(identity)}; + 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 ParameterKind kind, + Parameter operand) { + return {.kind = kind, + .left = std::make_shared(std::move(operand))}; +} + +[[nodiscard]] Parameter makeBinaryParameter(const ParameterKind kind, + Parameter lhs, Parameter rhs) { + return {.kind = kind, + .left = std::make_shared(std::move(lhs)), + .right = std::make_shared(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]] ParameterKind unaryParameterKind(const std::string_view opcode) { + if (opcode == "NEG") { + return ParameterKind::Negate; + } + if (opcode == "SIN") { + return ParameterKind::Sin; + } + if (opcode == "COS") { + return ParameterKind::Cos; + } + if (opcode == "TAN") { + return ParameterKind::Tan; + } + if (opcode == "ASIN") { + return ParameterKind::ArcSin; + } + if (opcode == "ACOS") { + return ParameterKind::ArcCos; + } + if (opcode == "ATAN") { + return ParameterKind::ArcTan; + } + if (opcode == "EXP") { + return ParameterKind::Exp; + } + if (opcode == "LOG") { + return ParameterKind::Log; + } + if (opcode == "ABS") { + return ParameterKind::Abs; + } + return ParameterKind::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]] ParameterKind binaryParameterKind(const std::string_view opcode) { + if (opcode == "ADD") { + return ParameterKind::Add; + } + if (opcode == "SUB" || opcode == "RSUB") { + return ParameterKind::Subtract; + } + if (opcode == "MUL") { + return ParameterKind::Multiply; + } + if (opcode == "DIV" || opcode == "RDIV") { + return ParameterKind::Divide; + } + return ParameterKind::Power; +} + +[[nodiscard]] Parameter normalizePythonParameter(const nb::handle parameter) { + if (nb::hasattr(parameter, "name") && nb::hasattr(parameter, "uuid")) { + 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 +773,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 +790,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 +808,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 +986,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 +1049,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 +1171,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 +1375,38 @@ 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"); } - result.parameter = symbol.name; + if (symbol.name == nullptr) { + throwPythonError("Qiskit failed to read a loop-parameter 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]); + if (parameter.kind != ParameterKind::Symbol) { + throw std::runtime_error("Qiskit for-loop parameter is not a symbol"); + } + if (parameter.text != 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 +1513,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 +1547,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 +1660,151 @@ 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 (parameter.kind == ParameterKind::Number) { + if (parameter.left != nullptr || parameter.right != nullptr) { + throw std::runtime_error( + "numeric parameter expression node has operands"); + } + ownedParameters.emplace_back( + std::make_unique(parameter.number)); + return ownedParameters.back()->get(); + } + if (parameter.kind == ParameterKind::Symbol) { + if (parameter.left != nullptr || parameter.right != nullptr) { + throw std::runtime_error( + "symbolic parameter expression node has operands"); + } + if (parameter.identity.empty()) { + throw std::runtime_error( + "cannot export a symbolic parameter without a stable identity"); + } + if (parameter.text.empty()) { + throw std::runtime_error( + "cannot export a symbolic parameter without a name"); + } + const auto found = symbols_.find(parameter.identity); + if (found != symbols_.end()) { + if (found->second.name != parameter.text) { + throw std::runtime_error( + "one symbolic parameter identity has conflicting metadata"); + } + return found->second.parameter->get(); + } + auto [inserted, success] = + symbols_.emplace(parameter.identity, + Symbol{.name = parameter.text, + .parameter = std::make_unique( + parameter.text)}); + static_cast(success); + return inserted->second.parameter->get(); + } + + const auto unary = parameter.kind == ParameterKind::Negate || + parameter.kind == ParameterKind::Sin || + parameter.kind == ParameterKind::Cos || + parameter.kind == ParameterKind::Tan || + parameter.kind == ParameterKind::ArcSin || + parameter.kind == ParameterKind::ArcCos || + parameter.kind == ParameterKind::ArcTan || + parameter.kind == ParameterKind::Exp || + parameter.kind == ParameterKind::Log || + parameter.kind == ParameterKind::Abs || + parameter.kind == ParameterKind::Conjugate; + if (parameter.left == nullptr || (unary && parameter.right != nullptr) || + (!unary && parameter.right == nullptr)) { + throw std::runtime_error("parameter expression has invalid operands"); + } + const auto* left = nativeParameter(*parameter.left, ownedParameters, + nodeCount, depth + 1U); + const QkParam* right = nullptr; + if (!unary) { + right = nativeParameter(*parameter.right, ownedParameters, nodeCount, + depth + 1U); + } + auto output = std::make_unique(); + QkExitCode result = QkExitCode_Success; + switch (parameter.kind) { + case ParameterKind::Number: + case ParameterKind::Symbol: + throw std::runtime_error("invalid parameter expression node"); + case ParameterKind::Add: + result = qk_param_add(output->getMutable(), left, right); + break; + case ParameterKind::Subtract: + result = qk_param_sub(output->getMutable(), left, right); + break; + case ParameterKind::Multiply: + result = qk_param_mul(output->getMutable(), left, right); + break; + case ParameterKind::Divide: + result = qk_param_div(output->getMutable(), left, right); + break; + case ParameterKind::Power: + result = qk_param_pow(output->getMutable(), left, right); + break; + case ParameterKind::Negate: + result = qk_param_neg(output->getMutable(), left); + break; + case ParameterKind::Sin: + result = qk_param_sin(output->getMutable(), left); + break; + case ParameterKind::Cos: + result = qk_param_cos(output->getMutable(), left); + break; + case ParameterKind::Tan: + result = qk_param_tan(output->getMutable(), left); + break; + case ParameterKind::ArcSin: + result = qk_param_asin(output->getMutable(), left); + break; + case ParameterKind::ArcCos: + result = qk_param_acos(output->getMutable(), left); + break; + case ParameterKind::ArcTan: + result = qk_param_atan(output->getMutable(), left); + break; + case ParameterKind::Exp: + result = qk_param_exp(output->getMutable(), left); + break; + case ParameterKind::Log: + result = qk_param_log(output->getMutable(), left); + break; + case ParameterKind::Abs: + result = qk_param_abs(output->getMutable(), left); + break; + case ParameterKind::Conjugate: + result = qk_param_conjugate(output->getMutable(), left); + break; + } + checkExitCode(result, "constructing a parameter expression"); + const auto* value = output->get(); + ownedParameters.push_back(std::move(output)); + return value; + } + + struct Symbol { + std::string name; + std::unique_ptr parameter; + }; + 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..a96c35e725 100644 --- a/bindings/mlir/qiskit/QiskitExport.cpp +++ b/bindings/mlir/qiskit/QiskitExport.cpp @@ -26,9 +26,12 @@ #include #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,225 @@ 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 {.kind = ParameterKind::Number, .number = value}; +} + +[[nodiscard]] Parameter unaryParameter(const ParameterKind kind, + Parameter operand) { + return {.kind = kind, + .left = std::make_shared(std::move(operand))}; +} + +[[nodiscard]] Parameter binaryParameter(const ParameterKind kind, + Parameter left, Parameter right) { + return {.kind = kind, + .left = std::make_shared(std::move(left)), + .right = std::make_shared(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"); } - return *number; + auto result = numberParameter(*number); + parameters.try_emplace(value, result); + return result; } - throw std::runtime_error( - "Qiskit circuit export supports only numeric parameters"); + 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 ParameterKind kind) { + if (operation->getNumOperands() != 1U) { + throw std::runtime_error("QC parameter operation '" + + operation->getName().getStringRef().str() + + "' has invalid arity"); + } + return unaryParameter(kind, + exportParameterImpl(operation->getOperand(0), + parameters, depth + 1U, nodes)); + }; + const auto binary = [&](const ParameterKind 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(ParameterKind::Add); + } else if (llvm::isa(*operation)) { + result = binary(ParameterKind::Subtract); + } else if (llvm::isa(*operation)) { + result = binary(ParameterKind::Multiply); + } else if (llvm::isa(*operation)) { + result = binary(ParameterKind::Divide); + } else if (llvm::isa(*operation)) { + result = binary(ParameterKind::Power); + } else if (llvm::isa(*operation)) { + result = unary(ParameterKind::Negate); + } else if (llvm::isa(*operation)) { + result = unary(ParameterKind::Sin); + } else if (llvm::isa(*operation)) { + result = unary(ParameterKind::Cos); + } else if (llvm::isa(*operation)) { + result = unary(ParameterKind::Tan); + } else if (llvm::isa(*operation)) { + result = unary(ParameterKind::ArcSin); + } else if (llvm::isa(*operation)) { + result = unary(ParameterKind::ArcCos); + } else if (llvm::isa(*operation)) { + result = unary(ParameterKind::ArcTan); + } else if (llvm::isa(*operation)) { + result = unary(ParameterKind::Exp); + } else if (llvm::isa(*operation)) { + result = unary(ParameterKind::Log); + } else if (llvm::isa(*operation)) { + result = unary(ParameterKind::Abs); + } else { + throw std::runtime_error( + "Qiskit circuit export does not support scalar parameter operation '" + + operation->getName().getStringRef().str() + "'"); + } + 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(); + } + const auto requireLeaf = [&] { + if (parameter.left || parameter.right) { + throw std::runtime_error( + "QC parameter-expression leaf has unexpected operands"); + } + }; + const auto requireUnary = [&] { + if (!parameter.left || parameter.right) { + throw std::runtime_error( + "QC unary parameter expression has invalid operands"); + } + validateExportParameterImpl(*parameter.left, depth + 1U, nodes); + }; + const auto requireBinary = [&] { + if (!parameter.left || !parameter.right) { + throw std::runtime_error( + "QC binary parameter expression has missing operands"); + } + validateExportParameterImpl(*parameter.left, depth + 1U, nodes); + validateExportParameterImpl(*parameter.right, depth + 1U, nodes); + }; + switch (parameter.kind) { + case ParameterKind::Number: + requireLeaf(); + if (!std::isfinite(parameter.number)) { + throw std::runtime_error("cannot export a non-finite QC parameter"); + } + return; + case ParameterKind::Symbol: + requireLeaf(); + if (parameter.text.empty() || parameter.identity.empty()) { + throw std::runtime_error( + "QC parameter symbol has invalid identity metadata"); + } + if (parameter.text.find('\0') != std::string::npos || + parameter.identity.find('\0') != std::string::npos) { + throw std::runtime_error( + "QC parameter symbol metadata contains a null character"); + } + return; + case ParameterKind::Add: + case ParameterKind::Subtract: + case ParameterKind::Multiply: + case ParameterKind::Divide: + case ParameterKind::Power: + requireBinary(); + return; + case ParameterKind::Negate: + case ParameterKind::Sin: + case ParameterKind::Cos: + case ParameterKind::Tan: + case ParameterKind::ArcSin: + case ParameterKind::ArcCos: + case ParameterKind::ArcTan: + case ParameterKind::Exp: + case ParameterKind::Log: + case ParameterKind::Abs: + case ParameterKind::Conjugate: + requireUnary(); + return; + } + throw std::runtime_error("unknown QC parameter expression kind"); +} + +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 +346,104 @@ struct ExportState { std::vector instructions; std::vector quantumRegisters; std::vector classicalRegisters; - double globalPhase = 0.0; + ExportedParameters parameters; + std::vector inputParameters; + Parameter globalPhase{.kind = ParameterKind::Number, .number = 0.0}; uint32_t numQubits = 0; uint32_t numClbits = 0; }; +void collectParameterIdentities(const Parameter& parameter, + llvm::StringSet<>& identities) { + if (parameter.kind == ParameterKind::Symbol) { + identities.insert(parameter.identity); + return; + } + if (parameter.left) { + collectParameterIdentities(*parameter.left, identities); + } + if (parameter.right) { + collectParameterIdentities(*parameter.right, identities); + } +} + +void validateExportParameters(const ExportState& state) { + llvm::StringSet<> usedIdentities; + const auto validate = [&](const Parameter& parameter) { + validateExportParameter(parameter); + collectParameterIdentities(parameter, usedIdentities); + }; + validate(state.globalPhase); + for (const auto& instruction : state.instructions) { + for (const auto& parameter : instruction.parameters) { + validate(parameter); + } + } + for (const auto& input : state.inputParameters) { + if (!usedIdentities.contains(input.identity)) { + throw std::runtime_error( + "Qiskit circuit export cannot preserve unused named f64 program " + "input '" + + input.text + "'"); + } + } +} + +void collectParameters(mlir::func::FuncOp function, ExportState& state) { + llvm::StringSet<> names; + for (const auto [index, argument] : + llvm::enumerate(function.getArguments())) { + const auto name = function.getArgAttrOfType( + index, mlir::utils::INPUT_NAME_ATTR); + if (!argument.getType().isF64() || !name || name.getValue().empty()) { + throw std::runtime_error( + "Qiskit circuit export requires named f64 program inputs"); + } + if (name.getValue().contains('\0')) { + throw std::runtime_error( + "Qiskit circuit export does not support parameter names with null " + "characters"); + } + if (!names.insert(name.getValue()).second) { + throw std::runtime_error( + "Qiskit circuit export requires unique parameter names"); + } + Parameter parameter{ + .kind = ParameterKind::Symbol, + .text = name.str(), + .identity = "input:" + std::to_string(index), + }; + state.parameters[argument] = parameter; + state.inputParameters.push_back(std::move(parameter)); + } +} + +void addGlobalPhase(ExportState& state, const Parameter& phase) { + if (phase.kind == ParameterKind::Number) { + if (!std::isfinite(phase.number)) { + throw std::runtime_error( + "QC global phase cannot be represented by Qiskit"); + } + if (state.globalPhase.kind == ParameterKind::Number) { + state.globalPhase.number += phase.number; + if (!std::isfinite(state.globalPhase.number)) { + throw std::runtime_error( + "QC global phase cannot be represented by Qiskit"); + } + return; + } + if (std::abs(phase.number) <= mlir::utils::TOLERANCE) { + return; + } + } else if (state.globalPhase.kind == ParameterKind::Number && + std::abs(state.globalPhase.number) <= mlir::utils::TOLERANCE) { + state.globalPhase = phase; + return; + } + state.globalPhase = + binaryParameter(ParameterKind::Add, std::move(state.globalPhase), phase); +} + [[nodiscard]] std::vector mapQubits(const mlir::ValueRange values, const llvm::DenseMap& qubits) { @@ -161,7 +462,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 +473,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 +565,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( + ParameterKind::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(ParameterKind::Negate, std::move(parameters[0])), + unaryParameter(ParameterKind::Negate, std::move(parameters[2])), + unaryParameter(ParameterKind::Negate, std::move(parameters[1]))}; return; } throw std::runtime_error( @@ -279,7 +584,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 +604,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 +636,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 +647,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); + if (exponent.kind != ParameterKind::Number || + (exponent.number != 1.0 && exponent.number != -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 (exponent.number == -1.0) { invertGate(result); } return result; @@ -381,7 +692,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; } @@ -567,7 +878,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 +910,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 +954,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 +1005,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..4934f48b5a 100644 --- a/bindings/mlir/qiskit/QiskitImport.cpp +++ b/bindings/mlir/qiskit/QiskitImport.cpp @@ -21,6 +21,7 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include "mlir/Dialect/Utils/DenseUnitary.h" +#include "mlir/Dialect/Utils/Utils.h" #include #include @@ -37,8 +38,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -75,19 +78,36 @@ 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::func::FuncDialect, mlir::math::MathDialect, + mlir::scf::SCFDialect, mlir::LLVM::LLVMDialect, + mlir::memref::MemRefDialect, mlir::jeff::JeffDialect>(); mlir::registerBuiltinDialectTranslation(registry); mlir::registerLLVMDialectTranslation(registry); auto context = std::make_shared(registry); @@ -95,44 +115,225 @@ 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(); + } + const auto requireLeft = [&]() -> const Parameter& { + if (!parameter.left) { + throw std::runtime_error( + "Qiskit parameter expression has a missing operand"); + } + return *parameter.left; + }; + const auto requireRight = [&]() -> const Parameter& { + if (!parameter.right) { + throw std::runtime_error( + "Qiskit parameter expression has a missing operand"); + } + return *parameter.right; + }; + switch (parameter.kind) { + case ParameterKind::Number: + if (parameter.left || parameter.right) { + throw std::runtime_error( + "Qiskit parameter-expression leaf has unexpected operands"); + } + if (!std::isfinite(parameter.number)) { throw std::runtime_error("Qiskit returned a non-finite parameter"); } return; - } - if (localParameters.contains(parameter.text)) { + case ParameterKind::Symbol: + if (parameter.left || parameter.right) { + throw std::runtime_error( + "Qiskit parameter-expression leaf has unexpected operands"); + } + if (parameter.identity.empty() || parameter.text.empty()) { + throw std::runtime_error( + "Qiskit returned a parameter with invalid symbol metadata"); + } + if (const auto local = localParameters.find(parameter.identity); + local != localParameters.end()) { + if (parameter.text != local->second.text) { + throw std::runtime_error("Qiskit parameter symbol '" + parameter.text + + "' aliases local symbol '" + + local->second.text + + "' with inconsistent metadata"); + } + return; + } + if (const auto free = freeParameters.find(parameter.identity); + free != freeParameters.end()) { + if (parameter.text != free->second.text) { + throw std::runtime_error("Qiskit parameter symbol '" + parameter.text + + "' aliases free symbol '" + free->second.text + + "' with inconsistent metadata"); + } + return; + } + throw std::runtime_error("Qiskit parameter symbol '" + parameter.text + + "' is not defined in this circuit scope"); + case ParameterKind::Add: + case ParameterKind::Subtract: + case ParameterKind::Multiply: + case ParameterKind::Divide: + case ParameterKind::Power: + validateParameterImpl(requireLeft(), localParameters, freeParameters, + depth + 1U, nodes); + validateParameterImpl(requireRight(), localParameters, freeParameters, + depth + 1U, nodes); + return; + case ParameterKind::Negate: + case ParameterKind::Sin: + case ParameterKind::Cos: + case ParameterKind::Tan: + case ParameterKind::ArcSin: + case ParameterKind::ArcCos: + case ParameterKind::ArcTan: + case ParameterKind::Exp: + case ParameterKind::Log: + case ParameterKind::Abs: + case ParameterKind::Conjugate: + if (parameter.right) { + throw std::runtime_error( + "Qiskit unary parameter expression has invalid operands"); + } + validateParameterImpl(requireLeft(), localParameters, freeParameters, + depth + 1U, nodes); return; } - throw std::runtime_error( - "Qiskit circuit import does not support free symbolic parameter '" + - parameter.text + "'"); + 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)) { +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(); + } + const auto requireLeft = [&]() -> const Parameter& { + if (!parameter.left) { + throw std::runtime_error( + "Qiskit parameter expression has a missing operand"); + } + return *parameter.left; + }; + const auto requireRight = [&]() -> const Parameter& { + if (!parameter.right) { + throw std::runtime_error( + "Qiskit parameter expression has a missing operand"); + } + return *parameter.right; + }; + switch (parameter.kind) { + case ParameterKind::Number: + if (!std::isfinite(parameter.number)) { throw std::runtime_error("Qiskit returned a non-finite parameter"); } - return *parameter.number; + return parameter.number; + case ParameterKind::Symbol: + if (!parameter.identity.empty()) { + if (const auto local = localParameters.find(parameter.identity); + local != localParameters.end()) { + return local->second; + } + if (const auto global = globalParameters.find(parameter.identity); + global != globalParameters.end()) { + return global->second; + } + } + throw std::runtime_error("Qiskit parameter symbol '" + parameter.text + + "' is not defined in this circuit scope"); + case ParameterKind::Conjugate: + // QC scalar parameters are real-valued, so conjugation is the identity. + return parameterValueImpl(builder, requireLeft(), localParameters, + globalParameters, depth + 1U, nodes); + default: + break; + } + + const auto left = materializeParameterValue( + builder, parameterValueImpl(builder, requireLeft(), localParameters, + globalParameters, depth + 1U, nodes)); + switch (parameter.kind) { + case ParameterKind::Negate: + return mlir::arith::NegFOp::create(builder, left).getResult(); + case ParameterKind::Sin: + return mlir::math::SinOp::create(builder, left).getResult(); + case ParameterKind::Cos: + return mlir::math::CosOp::create(builder, left).getResult(); + case ParameterKind::Tan: + return mlir::math::TanOp::create(builder, left).getResult(); + case ParameterKind::ArcSin: + return mlir::math::AsinOp::create(builder, left).getResult(); + case ParameterKind::ArcCos: + return mlir::math::AcosOp::create(builder, left).getResult(); + case ParameterKind::ArcTan: + return mlir::math::AtanOp::create(builder, left).getResult(); + case ParameterKind::Exp: + return mlir::math::ExpOp::create(builder, left).getResult(); + case ParameterKind::Log: + return mlir::math::LogOp::create(builder, left).getResult(); + case ParameterKind::Abs: + return mlir::math::AbsFOp::create(builder, left).getResult(); + default: + break; + } + + const auto right = materializeParameterValue( + builder, parameterValueImpl(builder, requireRight(), localParameters, + globalParameters, depth + 1U, nodes)); + switch (parameter.kind) { + case ParameterKind::Add: + return mlir::arith::AddFOp::create(builder, left, right).getResult(); + case ParameterKind::Subtract: + return mlir::arith::SubFOp::create(builder, left, right).getResult(); + case ParameterKind::Multiply: + return mlir::arith::MulFOp::create(builder, left, right).getResult(); + case ParameterKind::Divide: + return mlir::arith::DivFOp::create(builder, left, right).getResult(); + case ParameterKind::Power: + return mlir::math::PowFOp::create(builder, left, right).getResult(); + default: + break; } - return parameterValue(parameter.text, localParameters); + 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 +458,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 +481,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 +508,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 +522,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 +563,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 +577,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 +971,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 +1028,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 +1057,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 +1112,7 @@ void translateControlFlow(mlir::qc::QCProgramBuilder& builder, auto parameters = localParameters; if (loop.parameter) { requireExactLoopParameter(value); - parameters[*loop.parameter] = + parameters[loop.parameter->identity] = floatConstant(builder, static_cast(value)); } translateBlock(*body, parameters); @@ -919,7 +1127,7 @@ void translateControlFlow(mlir::qc::QCProgramBuilder& builder, builder.scfFor(0, count, 1, [&](const mlir::Value iteration) { auto parameters = localParameters; if (loop.parameter) { - parameters[*loop.parameter] = + parameters[loop.parameter->identity] = loopParameterValue(builder, iteration, loop); } translateBlock(*body, parameters); @@ -988,9 +1196,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 +1238,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 +1247,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 +1292,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 +1301,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,7 +1441,8 @@ expansionSummary(const CircuitReader& circuit, ExpansionCountState& state, } void validateCircuit(const CircuitReader& circuit, - const llvm::StringSet<>& localParameters, + const ValidationParameters& localParameters, + const ValidationParameters& freeParameters, uint32_t rootQubits, uint32_t rootClbits, size_t definitionDepth, size_t controlFlowDepth); @@ -1293,7 +1505,8 @@ void validateTarget(const ClassicalTarget& target, const uint32_t rootClbits) { } void validateControlFlow(const ControlFlowReader& controlFlow, - llvm::StringSet<> localParameters, + ValidationParameters localParameters, + const ValidationParameters& freeParameters, const uint32_t rootQubits, const uint32_t rootClbits, const size_t definitionDepth, const size_t controlFlowDepth) { @@ -1353,7 +1566,12 @@ void validateControlFlow(const ControlFlowReader& controlFlow, } } if (loop.parameter) { - localParameters.insert(*loop.parameter); + if (loop.parameter->kind != ParameterKind::Symbol || + loop.parameter->identity.empty() || loop.parameter->text.empty()) { + throw std::runtime_error( + "Qiskit for-loop parameter has invalid symbol metadata"); + } + localParameters[loop.parameter->identity] = *loop.parameter; } break; } @@ -1408,13 +1626,14 @@ 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, rootQubits, + rootClbits, definitionDepth, controlFlowDepth + 1U); } } void validateDefinition(const CircuitReader& circuit, const size_t index, - const llvm::StringSet<>& localParameters, + const ValidationParameters& localParameters, + const ValidationParameters& freeParameters, const size_t definitionDepth, const size_t controlFlowDepth) { if (definitionDepth >= MAX_DEFINITION_DEPTH) { @@ -1422,13 +1641,14 @@ 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, + definition->numQubits(), definition->numClbits(), + definitionDepth + 1U, controlFlowDepth); } void validateCircuit(const CircuitReader& circuit, - const llvm::StringSet<>& localParameters, + const ValidationParameters& localParameters, + const ValidationParameters& freeParameters, const uint32_t rootQubits, const uint32_t rootClbits, const size_t definitionDepth, const size_t controlFlowDepth) { @@ -1441,7 +1661,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 +1678,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 +1711,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, + definitionDepth, controlFlowDepth); break; case OperationKind::Unknown: if (!instruction.modifiers.empty()) { @@ -1500,8 +1720,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, + definitionDepth, controlFlowDepth); break; case OperationKind::Barrier: if (!instruction.parameters.empty() || !instruction.clbits.empty()) { @@ -1526,8 +1746,9 @@ void validateCircuit(const CircuitReader& circuit, break; case OperationKind::ControlFlow: { const auto controlFlow = circuit.controlFlow(index); - validateControlFlow(*controlFlow, localParameters, rootQubits, rootClbits, - definitionDepth, controlFlowDepth); + validateControlFlow(*controlFlow, localParameters, freeParameters, + rootQubits, rootClbits, definitionDepth, + controlFlowDepth); break; } case OperationKind::Delay: @@ -1541,10 +1762,30 @@ 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<> freeParameterNames; + for (const auto& parameter : freeParameters) { + if (parameter.kind != ParameterKind::Symbol || parameter.text.empty() || + parameter.identity.empty()) { + throw std::runtime_error( + "Qiskit circuit returned an invalid free parameter"); + } + if (!freeParameterSymbols.try_emplace(parameter.identity, parameter) + .second) { + throw std::runtime_error( + "Qiskit circuit returned a duplicate parameter identity"); + } + if (!freeParameterNames.insert(parameter.text).second) { + throw std::runtime_error( + "Qiskit circuit contains distinct parameters with the same name"); + } + } ExpansionCountState expansion; static_cast(expansionSummary(*view, expansion)); - validateCircuit(*view, {}, view->numQubits(), view->numClbits(), 0U, 0U); + validateCircuit(*view, {}, freeParameterSymbols, view->numQubits(), + view->numClbits(), 0U, 0U); const auto quantumRegisters = circuitRegisters(*view, true); const auto classicalRegisters = circuitRegisters(*view, false); const auto looseQubits = @@ -1568,6 +1809,26 @@ 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 llvm::SmallVector argumentAttributes{ + builder.getNamedAttr(mlir::utils::INPUT_NAME_ATTR, + builder.getStringAttr(parameter.text))}; + 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[parameter.identity] = function.getArgument(index); + } llvm::SmallVector qubits; qubits.reserve(view->numQubits()); @@ -1607,7 +1868,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..e90ccf69f1 100644 --- a/bindings/mlir/qiskit/QiskitTranslation.h +++ b/bindings/mlir/qiskit/QiskitTranslation.h @@ -48,9 +48,38 @@ struct Register { validateRegisterLayout(const std::vector& registers, uint32_t total, std::string_view kind); +inline constexpr size_t MAX_PARAMETER_EXPRESSION_DEPTH = 64U; +inline constexpr size_t MAX_PARAMETER_EXPRESSION_NODES = 4096U; + +enum class ParameterKind : uint8_t { + Number, + Symbol, + Add, + Subtract, + Multiply, + Divide, + Power, + Negate, + Sin, + Cos, + Tan, + ArcSin, + ArcCos, + ArcTan, + Exp, + Log, + Abs, + Conjugate, +}; + +/** One normalized scalar parameter-expression tree. */ struct Parameter { - std::optional number = 0.0; + ParameterKind kind = ParameterKind::Number; + double number = 0.0; std::string text; + std::string identity; + std::shared_ptr left; + std::shared_ptr right; }; enum class GateModifierKind : uint8_t { @@ -169,7 +198,7 @@ struct Loop { int64_t stop = 0; int64_t step = 1; std::vector values; - std::optional parameter; + std::optional parameter; }; struct SwitchCase { @@ -196,6 +225,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 +270,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/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/Dialect/Utils/Utils.h b/mlir/include/mlir/Dialect/Utils/Utils.h index a4b9b6dc24..feab5bad12 100644 --- a/mlir/include/mlir/Dialect/Utils/Utils.h +++ b/mlir/include/mlir/Dialect/Utils/Utils.h @@ -46,6 +46,9 @@ namespace mlir::utils { inline constexpr llvm::StringLiteral QUBIT_REGISTER_NAME_ATTR = "mqt.qubit_register_name"; +/// Attribute used to retain the source-level name of a scalar program input. +inline constexpr llvm::StringLiteral INPUT_NAME_ATTR = "mqt.input_name"; + /// Check if a floating-point value is an integer. [[nodiscard]] inline bool isIntegerExponent(double r) { return r == std::floor(r) && std::isfinite(r); diff --git a/mlir/lib/Compiler/CMakeLists.txt b/mlir/lib/Compiler/CMakeLists.txt index 566c7b8697..59552438be 100644 --- a/mlir/lib/Compiler/CMakeLists.txt +++ b/mlir/lib/Compiler/CMakeLists.txt @@ -75,6 +75,7 @@ add_mlir_library( MLIRTargetLLVMIRExport MLIRBuiltinToLLVMIRTranslation MLIRLLVMToLLVMIRTranslation + MLIRMathDialect MQTCompilerTarget MQT::MLIRSupport) diff --git a/mlir/lib/Compiler/Programs.cpp b/mlir/lib/Compiler/Programs.cpp index b73d58ff3f..116939f8dc 100644 --- a/mlir/lib/Compiler/Programs.cpp +++ b/mlir/lib/Compiler/Programs.cpp @@ -46,6 +46,7 @@ #include #include #include +#include #include #include #include @@ -85,8 +86,8 @@ namespace mlir { DialectRegistry registry; registry.insert(); registerBuiltinDialectTranslation(registry); registerLLVMDialectTranslation(registry); diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index 40c918150c..f6ed06d610 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -14,6 +14,8 @@ import re import subprocess import sys +from typing import TYPE_CHECKING +from uuid import uuid4 import numpy as np import pytest @@ -27,6 +29,8 @@ Gate, InverseModifier, Parameter, + ParameterExpression, + ParameterVector, PowerModifier, Qubit, library, @@ -37,6 +41,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): @@ -704,7 +711,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 +722,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 +842,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 +862,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,10 +1051,470 @@ 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 identity 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 {passthrough = ["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: + lines = [ + "module {", + ' func.func @main(%theta: f64 {mqt.input_name = "theta"}) attributes {passthrough = ["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 {passthrough = ["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_global_and_loop_parameters_use_identity_not_name() -> None: + """Do not capture a same-name global symbol as a loop induction value.""" + 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) + + program = QCProgram.from_qiskit(circuit) + + input_match = re.search(r"func\.func @main\((%[^: ]+): f64 \{[^}]*mqt\.input_name = \"theta\"[^}]*\}", program.ir) + assert input_match is not None + assert f"qc.ry({input_match.group(1)})" in program.ir + assert list(circuit.data) == source_data + assert circuit.parameters == {global_parameter} + + +def test_conflicting_free_parameter_uuid_alias_fails_without_mutation() -> None: + """Reject differently named free symbols that share one stable identity.""" + identity = uuid4() + canonical = Parameter("canonical", uuid=identity) + alias = Parameter("alias", uuid=identity) + circuit = QuantumCircuit(1) + circuit.rx(canonical, 0) + circuit.rz(alias, 0) + source_data = list(circuit.data) + + with pytest.raises( + RuntimeError, + match="parameter symbol 'alias' aliases free symbol 'canonical' with inconsistent metadata", + ): + QCProgram.from_qiskit(circuit) + + assert list(circuit.data) == source_data + assert circuit.parameters == {canonical} + + +def test_conflicting_local_parameter_uuid_alias_fails_without_mutation() -> None: + """Resolve a shared identity against the active lexical loop binding.""" + identity = uuid4() + global_parameter = Parameter("global", uuid=identity) + loop_parameter = Parameter("local", uuid=identity) + body = QuantumCircuit(1) + body.ry(global_parameter, 0) + circuit = QuantumCircuit(1) + circuit.for_loop(range(2), loop_parameter, body, [0], [], label=None) + source_data = list(circuit.data) + + with pytest.raises( + RuntimeError, + match="parameter symbol 'global' aliases local symbol 'local' with inconsistent metadata", + ): + QCProgram.from_qiskit(circuit) + + assert list(circuit.data) == source_data + assert circuit.parameters == {global_parameter} + + +def test_duplicate_named_symbolic_inputs_fail_closed_without_mutation() -> None: + """Reject ambiguous Qiskit parameter names before changing the source IR.""" + program = QCProgram.from_mlir_str( + """module { + func.func @main( + %first: f64 {mqt.input_name = "theta"}, + %second: f64 {mqt.input_name = "theta"} + ) attributes {passthrough = ["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 + } +} +""" + ) + source_ir = program.ir + + with pytest.raises(RuntimeError, match="requires unique parameter names"): + program.to_qiskit() + + assert program.ir == source_ir + + +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 + + program = QCProgram.from_mlir_str( + r"""module { + func.func @main(%theta: f64 {mqt.input_name = "before\00after"}) attributes {passthrough = ["entry_point"]} { + %q = qc.alloc : !qc.qubit + qc.rz(%theta) %q : !qc.qubit + qc.dealloc %q : !qc.qubit + return + } +} +""" + ) + source_ir = program.ir + + with pytest.raises(RuntimeError, match="names with null characters"): + program.to_qiskit() + + assert program.ir == source_ir + + +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 {mqt.input_name = "theta"}) attributes {passthrough = ["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 {passthrough = ["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 {passthrough = ["entry_point"]} { %q = qc.alloc : !qc.qubit qc.rx(%theta) %q : !qc.qubit @@ -981,8 +1524,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 {passthrough = ["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: From cd5c89ab042e19299763b66052c7edc8f993e860 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 20 Aug 2026 10:27:19 +0200 Subject: [PATCH 02/17] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bindings/mlir/qiskit/QiskitExport.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/bindings/mlir/qiskit/QiskitExport.cpp b/bindings/mlir/qiskit/QiskitExport.cpp index a96c35e725..4ff1f2af48 100644 --- a/bindings/mlir/qiskit/QiskitExport.cpp +++ b/bindings/mlir/qiskit/QiskitExport.cpp @@ -26,7 +26,6 @@ #include #include #include -#include #include #include #include From e6e4b919cc1e02465034d4680bb552c0d804a0a7 Mon Sep 17 00:00:00 2001 From: simon1hofmann <119581649+simon1hofmann@users.noreply.github.com> Date: Thu, 20 Aug 2026 10:46:06 +0200 Subject: [PATCH 03/17] Update test/python/test_mlir_qiskit_translation.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: simon1hofmann <119581649+simon1hofmann@users.noreply.github.com> --- test/python/test_mlir_qiskit_translation.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index f6ed06d610..44cca12151 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -1281,11 +1281,15 @@ def test_manual_arith_and_math_parameter_expression_exports_to_qiskit() -> None: 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 {passthrough = ["entry_point"]} {', " %q = qc.alloc : !qc.qubit", - ] values = [] for index in range(term_count): value = f"%term{index}" From 302e26b284298f0fe6e6d8606c5f43fadc63f616 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 20 Aug 2026 10:49:15 +0200 Subject: [PATCH 04/17] =?UTF-8?q?=F0=9F=90=87=20Restore=20line=20deleted?= =?UTF-8?q?=20by=20CodeRabbit=20suggestion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/python/test_mlir_qiskit_translation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index 44cca12151..aab8966932 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -1290,6 +1290,7 @@ def _wide_parameter_expression_program(term_count: int) -> QCProgram: "module {", ' func.func @main(%theta: f64 {mqt.input_name = "theta"}) attributes {passthrough = ["entry_point"]} {', " %q = qc.alloc : !qc.qubit", + ] values = [] for index in range(term_count): value = f"%term{index}" From 0c7965573ad5ef3739e50845c05626f2d7fa44ff Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Thu, 20 Aug 2026 11:13:09 +0000 Subject: [PATCH 05/17] =?UTF-8?q?=E2=9C=A8=20Add=20the=20MQT=20metadata=20?= =?UTF-8?q?dialect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Define typed discardable attributes for program-input and qubit-register names. Verify their placement, string values, and per-function uniqueness through the owning dialect. Assisted-by: Codex --- .agent/plans/qiskit-symbolic-parameters.md | 81 +++++++-- mlir/include/mlir/Dialect/CMakeLists.txt | 1 + mlir/include/mlir/Dialect/MQT/CMakeLists.txt | 9 + .../mlir/Dialect/MQT/IR/CMakeLists.txt | 14 ++ mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h | 17 ++ .../include/mlir/Dialect/MQT/IR/MQTDialect.td | 36 ++++ mlir/lib/Dialect/CMakeLists.txt | 1 + mlir/lib/Dialect/MQT/CMakeLists.txt | 9 + mlir/lib/Dialect/MQT/IR/CMakeLists.txt | 43 +++++ mlir/lib/Dialect/MQT/IR/MQTDialect.cpp | 154 ++++++++++++++++++ mlir/unittests/Dialect/CMakeLists.txt | 1 + mlir/unittests/Dialect/MQT/CMakeLists.txt | 9 + mlir/unittests/Dialect/MQT/IR/CMakeLists.txt | 24 +++ mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp | 154 ++++++++++++++++++ 14 files changed, 535 insertions(+), 18 deletions(-) create mode 100644 mlir/include/mlir/Dialect/MQT/CMakeLists.txt create mode 100644 mlir/include/mlir/Dialect/MQT/IR/CMakeLists.txt create mode 100644 mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h create mode 100644 mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td create mode 100644 mlir/lib/Dialect/MQT/CMakeLists.txt create mode 100644 mlir/lib/Dialect/MQT/IR/CMakeLists.txt create mode 100644 mlir/lib/Dialect/MQT/IR/MQTDialect.cpp create mode 100644 mlir/unittests/Dialect/MQT/CMakeLists.txt create mode 100644 mlir/unittests/Dialect/MQT/IR/CMakeLists.txt create mode 100644 mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp diff --git a/.agent/plans/qiskit-symbolic-parameters.md b/.agent/plans/qiskit-symbolic-parameters.md index 01a2cabd29..65a704e263 100644 --- a/.agent/plans/qiskit-symbolic-parameters.md +++ b/.agent/plans/qiskit-symbolic-parameters.md @@ -14,8 +14,8 @@ 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. Lexically bound `for`-loop values remain distinct from free parameters, -even when their displayed names match. +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 @@ -64,6 +64,20 @@ partially constructed output circuit after a failure. Exact - [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. +- [ ] Replace raw `mqt.*` string constants with generated dialect helpers and + preserve compatible metadata through QC/QCO conversions and allocation + rewrites. +- [ ] Replace Qiskit UUID-based parameter identity with the supported unique-name + contract and reject all free/bound name collisions before module creation. +- [ ] Replace the nullable parameter-expression node with a closed variant so + malformed node states cannot be constructed. +- [ ] Reject non-finite constant gate parameters in the shared QC and QCO gate + verifier rather than in individual import/export paths. +- [ ] Run focused dialect, conversion, compiler, Qiskit, documentation, stub, + and lint validation; inspect every signed commit and the final diff. ## Surprises & Discoveries @@ -92,6 +106,13 @@ partially constructed output circuit after a failure. Exact - 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. ## Decision Log @@ -105,10 +126,12 @@ partially constructed output circuit after a failure. Exact 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: Key all parameters by their Qiskit identity during import and by - their compiler SSA value during export. Use `mqt.input_name` for the public - scalar name. Rationale: identity prevents lexical capture without storing a - frontend object in MLIR. 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 another identity are ambiguous and + outside the supported source contract; UUID/name mismatch objects are also + invalid input rather than an IR requirement. Date/Author: 2026-08-20 / Codex. - Decision: Preserve symbol sharing but do not preserve Qiskit's original UUID across a round trip. Rationale: the compiler input is the frontend-neutral identity. The writer creates exactly one Qiskit symbol for each input and @@ -127,6 +150,20 @@ partially constructed output circuit after a failure. Exact 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.qubit_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: 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. ## Outcomes & Retrospective @@ -151,12 +188,13 @@ 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`, declared in -`mlir/include/mlir/Dialect/Utils/Utils.h`, for the stable public name of each -compiler input. The compiler representation uses `arith.addf`, `arith.subf`, +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 +and `mqt.qubit_register_name`; 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 the loop parameter's Qiskit identity. It is not a function input. +keyed by its unique source name. It is not a function input. ## Plan of Work @@ -170,12 +208,14 @@ excessive depth, and excessive node count before returning to generic import. Read a `for` parameter through the public control-flow operation so its UUID is preserved. -Next, change `QiskitImport.cpp` to validate every tree leaf by identity and to +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. Key both local and global parameter maps by -identity. 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. +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, finite constants, and the supported Arith and Math operations recursively. Cache each SSA result @@ -229,9 +269,9 @@ 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. Import a `for` loop whose -binder has the same displayed name as a distinct free symbol used in its body. -The gate must use the free function argument, not the loop induction value. +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 @@ -290,3 +330,8 @@ 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/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..b6ae6efd5a --- /dev/null +++ b/mlir/include/mlir/Dialect/MQT/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Chair for Design Automation, TUM +# Copyright (c) 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..c73acbfbed --- /dev/null +++ b/mlir/include/mlir/Dialect/MQT/IR/CMakeLists.txt @@ -0,0 +1,14 @@ +# Copyright (c) 2026 Chair for Design Automation, TUM +# Copyright (c) 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_public_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..38bf648fbe --- /dev/null +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2026 Chair for Design Automation, TUM + * Copyright (c) 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mlir/Dialect/MQT/IR/MQTDialect.h.inc" // IWYU pragma: export + +#include +#include +#include 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..ef56c5685e --- /dev/null +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td @@ -0,0 +1,36 @@ +// Copyright (c) 2026 Chair for Design Automation, TUM +// Copyright (c) 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/DialectBase.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.qubit_register_name` records the source-level name of a rank-one qubit + register allocation. + }]; + + let discardableAttrs = (ins "::mlir::StringAttr":$input_name, + "::mlir::StringAttr":$qubit_register_name); + + let hasOperationAttrVerify = 1; + let hasRegionArgAttrVerify = 1; + let hasRegionResultAttrVerify = 1; +} + +#endif // MLIR_DIALECT_MQT_IR_MQTDIALECT_TD 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..b6ae6efd5a --- /dev/null +++ b/mlir/lib/Dialect/MQT/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Chair for Design Automation, TUM +# Copyright (c) 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..c3b19ebfbb --- /dev/null +++ b/mlir/lib/Dialect/MQT/IR/CMakeLists.txt @@ -0,0 +1,43 @@ +# Copyright (c) 2026 Chair for Design Automation, TUM +# Copyright (c) 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 + 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..cbf091ce7b --- /dev/null +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2026 Chair for Design Automation, TUM + * Copyright (c) 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/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 + +using namespace mlir; +using namespace mlir::mqt; + +#include "mlir/Dialect/MQT/IR/MQTDialect.cpp.inc" + +void MQTDialect::initialize() {} + +namespace { +[[nodiscard]] 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]] bool isQubitRegisterAllocation(Operation* operation) { + if (auto alloc = dyn_cast(operation)) { + const auto type = alloc.getType(); + return type.getRank() == 1 && isa(type.getElementType()); + } + if (auto alloc = dyn_cast(operation)) { + const auto type = cast(alloc.getType()); + return type.getRank() == 1 && isa(type.getElementType()); + } + return false; +} + +[[nodiscard]] LogicalResult +verifyQubitRegisterName(Operation* operation, const NamedAttribute attribute) { + if (failed(verifyName(operation, attribute))) { + return failure(); + } + if (!isQubitRegisterAllocation(operation)) { + return operation->emitError() + << "attribute '" << attribute.getName().getValue() + << "' requires a rank-one qubit 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 (Operation& candidate : function.getFunctionBody().front()) { + if (&candidate == operation) { + continue; + } + if (candidate.getAttrOfType(attribute.getName()) == name) { + return operation->emitError() + << "duplicate qubit register name '" << name.getValue() << "'"; + } + } + return success(); +} +} // namespace + +LogicalResult +MQTDialect::verifyOperationAttribute(Operation* operation, + const NamedAttribute attribute) { + if (attribute.getName() == QubitRegisterNameAttrHelper::getNameStr()) { + return verifyQubitRegisterName(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 input 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"; +} 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..b6ae6efd5a --- /dev/null +++ b/mlir/unittests/Dialect/MQT/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Chair for Design Automation, TUM +# Copyright (c) 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..57ec7a2c77 --- /dev/null +++ b/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt @@ -0,0 +1,24 @@ +# Copyright (c) 2026 Chair for Design Automation, TUM +# Copyright (c) 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 + 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..241aee8e87 --- /dev/null +++ b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2026 Chair for Design Automation, TUM + * Copyright (c) 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/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, AcceptsProgramInputAndQubitRegisterNames) { + EXPECT_TRUE(parse(R"mlir( + module { + func.func @qc(%theta: f64 {mqt.input_name = "theta"}) { + %reg = memref.alloc() {mqt.qubit_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.qubit_register_name = "r"} + : tensor<2x!qco.qubit> + 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, RejectsInvalidQubitRegisterOwners) { + EXPECT_FALSE(parse(R"mlir( + module { + func.func @main() { + %reg = memref.alloc() {mqt.qubit_register_name = "bits"} + : memref<2xi1> + return + } + } + )mlir")); + EXPECT_FALSE(parse(R"mlir( + module { + func.func @main(%arg: f64 {mqt.qubit_register_name = "q"}) { + return + } + } + )mlir")); +} + +TEST_F(MQTIRTest, RejectsDuplicateQubitRegisterNames) { + EXPECT_FALSE(parse(R"mlir( + module { + func.func @main() { + %lhs = memref.alloc() {mqt.qubit_register_name = "q"} + : memref<1x!qc.qubit> + %rhs = memref.alloc() {mqt.qubit_register_name = "q"} + : memref<2x!qc.qubit> + return + } + } + )mlir")); +} + +TEST_F(MQTIRTest, RejectsUnknownMQTAttributes) { + EXPECT_FALSE(parse(R"mlir( + module { + func.func @main() attributes {mqt.unknown} { return } + } + )mlir")); +} +} // namespace From 4c0c9b6d7e86568835f51d5d73caa1bfc3070605 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Thu, 20 Aug 2026 11:32:09 +0000 Subject: [PATCH 06/17] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Adopt=20shared=20MQT?= =?UTF-8?q?=20metadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace raw mqt attribute keys with generated dialect helpers and load the metadata dialect in every compiler and translation context. Preserve all discardable metadata through QC/QCO allocation conversions and register-shrink rewrites. Validate the shared contract while parsing QC IR. Assisted-by: Codex --- .agent/plans/qiskit-symbolic-parameters.md | 55 +++++++------ bindings/mlir/CMakeLists.txt | 1 + bindings/mlir/qiskit/QiskitExport.cpp | 6 +- bindings/mlir/qiskit/QiskitImport.cpp | 19 +++-- mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h | 12 ++- mlir/include/mlir/Dialect/Utils/Utils.h | 7 -- mlir/lib/Compiler/CMakeLists.txt | 1 + mlir/lib/Compiler/Programs.cpp | 5 +- mlir/lib/Conversion/QCOToQC/CMakeLists.txt | 1 + mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | 5 +- mlir/lib/Conversion/QCToQCO/CMakeLists.txt | 1 + mlir/lib/Conversion/QCToQCO/QCToQCO.cpp | 5 +- mlir/lib/Dialect/QC/Builder/CMakeLists.txt | 1 + .../Dialect/QC/Builder/QCProgramBuilder.cpp | 7 +- .../QC/Transforms/ShrinkQubitRegisters.cpp | 1 + .../lib/Dialect/QC/Translation/CMakeLists.txt | 1 + .../QC/Translation/TranslateQCToOpenQASM3.cpp | 3 +- mlir/lib/Dialect/QCO/Builder/CMakeLists.txt | 1 + .../Dialect/QCO/Builder/QCOProgramBuilder.cpp | 9 ++- .../QTensor/Transforms/ShrinkRegisters.cpp | 1 + mlir/tools/mqt-cc/CMakeLists.txt | 1 + mlir/tools/mqt-cc/mqt-cc.cpp | 12 +-- .../Conversion/QCOToQC/test_qco_to_qc.cpp | 38 ++++----- .../Conversion/QCQCORoundTrip/CMakeLists.txt | 1 + .../QCQCORoundTrip/test_qc_qco_round_trip.cpp | 43 +++++++++- .../Conversion/QCToQCO/test_qc_to_qco.cpp | 39 ++++----- mlir/unittests/Dialect/QC/CMakeLists.txt | 1 + .../Dialect/QC/Transforms/CMakeLists.txt | 15 ++++ .../QC/Transforms/test_qc_transforms.cpp | 74 +++++++++++++++++ .../QC/Translation/test_qasm3_translation.cpp | 22 +++--- mlir/unittests/Dialect/QTensor/CMakeLists.txt | 1 + .../Dialect/QTensor/Transforms/CMakeLists.txt | 22 ++++++ .../Transforms/test_qtensor_transforms.cpp | 79 +++++++++++++++++++ test/python/test_mlir_qiskit_translation.py | 30 +++---- 34 files changed, 384 insertions(+), 136 deletions(-) create mode 100644 mlir/unittests/Dialect/QC/Transforms/CMakeLists.txt create mode 100644 mlir/unittests/Dialect/QC/Transforms/test_qc_transforms.cpp create mode 100644 mlir/unittests/Dialect/QTensor/Transforms/CMakeLists.txt create mode 100644 mlir/unittests/Dialect/QTensor/Transforms/test_qtensor_transforms.cpp diff --git a/.agent/plans/qiskit-symbolic-parameters.md b/.agent/plans/qiskit-symbolic-parameters.md index 65a704e263..b2f01a62f7 100644 --- a/.agent/plans/qiskit-symbolic-parameters.md +++ b/.agent/plans/qiskit-symbolic-parameters.md @@ -65,13 +65,14 @@ partially constructed output circuit after a failure. Exact 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. -- [ ] Replace raw `mqt.*` string constants with generated dialect helpers and - preserve compatible metadata through QC/QCO conversions and allocation - rewrites. -- [ ] Replace Qiskit UUID-based parameter identity with the supported unique-name - contract and reject all free/bound name collisions before module creation. + 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. +- [ ] Replace Qiskit UUID-based parameter identity with the supported + unique-name contract and reject all free/bound name collisions before + module creation. - [ ] Replace the nullable parameter-expression node with a closed variant so malformed node states cannot be constructed. - [ ] Reject non-finite constant gate parameters in the shared QC and QCO gate @@ -113,6 +114,11 @@ partially constructed output circuit after a failure. Exact - 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. ## Decision Log @@ -152,18 +158,18 @@ partially constructed output circuit after a failure. Exact Codex. - Decision: Define `mqt.input_name` and `mqt.qubit_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. + 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: 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. + 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. + metadata without source-format-specific key handling. Date/Author: 2026-08-20 + / Codex. ## Outcomes & Retrospective @@ -208,14 +214,13 @@ excessive depth, and excessive node count before returning to generic import. Read a `for` parameter through the public control-flow operation so its UUID is preserved. -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. +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, finite constants, and the supported Arith and Math operations recursively. Cache each SSA result @@ -269,9 +274,9 @@ 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. +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 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/QiskitExport.cpp b/bindings/mlir/qiskit/QiskitExport.cpp index 4ff1f2af48..b013822f8d 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" @@ -393,7 +394,7 @@ void collectParameters(mlir::func::FuncOp function, ExportState& state) { for (const auto [index, argument] : llvm::enumerate(function.getArguments())) { const auto name = function.getArgAttrOfType( - index, mlir::utils::INPUT_NAME_ATTR); + index, mlir::mqt::MQTDialect::InputNameAttrHelper::getNameStr()); if (!argument.getType().isF64() || !name || name.getValue().empty()) { throw std::runtime_error( "Qiskit circuit export requires named f64 program inputs"); @@ -753,7 +754,8 @@ 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::QubitRegisterNameAttrHelper:: + getNameStr())) { Register reg{.name = name.str()}; reg.bits.resize(size); std::iota(reg.bits.begin(), reg.bits.end(), state.numQubits); diff --git a/bindings/mlir/qiskit/QiskitImport.cpp b/bindings/mlir/qiskit/QiskitImport.cpp index 4934f48b5a..f1cada71d6 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" @@ -102,12 +103,13 @@ constexpr size_t MAX_EXPANDED_OPERATIONS = 10'000'000U; [[nodiscard]] std::shared_ptr createContext() { mlir::DialectRegistry registry; - registry.insert(); + registry.insert(); mlir::registerBuiltinDialectTranslation(registry); mlir::registerLLVMDialectTranslation(registry); auto context = std::make_shared(registry); @@ -1814,8 +1816,9 @@ mlir::QCProgram importCircuit(const nb::handle circuit) { GlobalParameters globalParameters; for (const auto& parameter : freeParameters) { const llvm::SmallVector argumentAttributes{ - builder.getNamedAttr(mlir::utils::INPUT_NAME_ATTR, - builder.getStringAttr(parameter.text))}; + builder.getNamedAttr( + mlir::mqt::MQTDialect::InputNameAttrHelper::getNameStr(), + builder.getStringAttr(parameter.text))}; const auto index = function.getNumArguments(); // MLIR types are handles. Converting FloatType to Type keeps the same // storage and does not slice object state. diff --git a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h index 38bf648fbe..8521d6e692 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h @@ -1,6 +1,6 @@ /* - * Copyright (c) 2026 Chair for Design Automation, TUM - * Copyright (c) 2026 Munich Quantum Software Company GmbH + * 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 @@ -10,8 +10,12 @@ #pragma once -#include "mlir/Dialect/MQT/IR/MQTDialect.h.inc" // IWYU pragma: export - #include #include #include + +//===----------------------------------------------------------------------===// +// Dialect +//===----------------------------------------------------------------------===// + +#include "mlir/Dialect/MQT/IR/MQTDialect.h.inc" // IWYU pragma: export diff --git a/mlir/include/mlir/Dialect/Utils/Utils.h b/mlir/include/mlir/Dialect/Utils/Utils.h index feab5bad12..1a1e8e6702 100644 --- a/mlir/include/mlir/Dialect/Utils/Utils.h +++ b/mlir/include/mlir/Dialect/Utils/Utils.h @@ -42,13 +42,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"; - -/// Attribute used to retain the source-level name of a scalar program input. -inline constexpr llvm::StringLiteral INPUT_NAME_ATTR = "mqt.input_name"; - /// Check if a floating-point value is an integer. [[nodiscard]] inline bool isIntegerExponent(double r) { return r == std::floor(r) && std::isfinite(r); diff --git a/mlir/lib/Compiler/CMakeLists.txt b/mlir/lib/Compiler/CMakeLists.txt index 59552438be..08fa49cf9e 100644 --- a/mlir/lib/Compiler/CMakeLists.txt +++ b/mlir/lib/Compiler/CMakeLists.txt @@ -76,6 +76,7 @@ add_mlir_library( MLIRBuiltinToLLVMIRTranslation MLIRLLVMToLLVMIRTranslation MLIRMathDialect + MLIRMQTDialect MQTCompilerTarget MQT::MLIRSupport) diff --git a/mlir/lib/Compiler/Programs.cpp b/mlir/lib/Compiler/Programs.cpp index 116939f8dc..a5780696dc 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" @@ -84,8 +85,8 @@ namespace mlir { [[nodiscard]] static std::shared_ptr createCompilerContext() { DialectRegistry registry; - registry.insert(); 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..07f5eea0eb 100644 --- a/mlir/lib/Conversion/QCOToQC/QCOToQC.cpp +++ b/mlir/lib/Conversion/QCOToQC/QCOToQC.cpp @@ -280,7 +280,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 +289,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..53b8a61275 100644 --- a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp +++ b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp @@ -840,7 +840,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 +849,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/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..b4f03a1a63 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()}); } @@ -147,7 +148,9 @@ Value QCProgramBuilder::allocQubitRegisterStorage(const int64_t size, 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() + ->getQubitRegisterNameAttrHelper() + .setAttr(alloc, getStringAttr(name)); } auto memref = alloc.getResult(); allocatedQregs.insert(memref); 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..75f4f40c61 100644 --- a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp +++ b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.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/QCInterfaces.h" #include "mlir/Dialect/QC/IR/QCOps.h" @@ -324,7 +325,7 @@ class OpenQASMEmitter { } StringRef requested; if (const auto attr = alloc->getAttrOfType( - utils::QUBIT_REGISTER_NAME_ATTR)) { + mqt::MQTDialect::QubitRegisterNameAttrHelper::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..c05789614b 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,7 +56,8 @@ 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()}); } @@ -149,8 +151,9 @@ QCOProgramBuilder::allocQubitRegister(const int64_t size, auto qtensor = qtensorAlloc(size); if (!name.empty()) { - qtensor.getDefiningOp()->setAttr(QUBIT_REGISTER_NAME_ATTR, - getStringAttr(name)); + ctx->getLoadedDialect() + ->getQubitRegisterNameAttrHelper() + .setAttr(qtensor.getDefiningOp(), getStringAttr(name)); } SmallVector qubits; 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/QCOToQC/test_qco_to_qc.cpp b/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp index 8159d40c98..6155f06d15 100644 --- a/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp +++ b/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp @@ -10,6 +10,7 @@ #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" @@ -47,8 +48,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 +57,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 +89,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,17 +108,17 @@ TEST(QCOToQCRegressionTest, RetainsQubitRegisterName) { } }); ASSERT_TRUE(allocation); - const auto name = - allocation->getAttrOfType(utils::QUBIT_REGISTER_NAME_ATTR); + const auto name = allocation->getAttrOfType( + mlir::mqt::MQTDialect::QubitRegisterNameAttrHelper::getNameStr()); ASSERT_TRUE(name); EXPECT_EQ(name.getValue(), "named_qubits"); } TEST(QCOToQCRegressionTest, RetainsDynamicQubitRegisterName) { DialectRegistry registry; - registry.insert(); + registry.insert(); MLIRContext context(registry); context.loadAllAvailableDialects(); @@ -142,8 +143,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::QubitRegisterNameAttrHelper::getNameStr()); ASSERT_TRUE(name); EXPECT_EQ(name.getValue(), "named_qubits"); } @@ -377,9 +378,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 +397,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..b6787c32e5 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 @@ -45,9 +47,10 @@ class QCQCORoundTripTest : public testing::Test { QCQCORoundTripTest() { DialectRegistry registry; - registry.insert(); + registry + .insert(); context.appendDialectRegistry(registry); context.loadAllAvailableDialects(); } @@ -71,6 +74,40 @@ class QCQCORoundTripTest : public testing::Test { } // namespace +TEST_F(QCQCORoundTripTest, PreservesSharedMQTMetadata) { + constexpr llvm::StringLiteral source = R"mlir( +module { + func.func @main(%theta: f64 {mqt.input_name = "theta"}) + attributes {passthrough = ["entry_point"]} { + %reg = memref.alloc() {mqt.qubit_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); + const auto inputName = function.getArgAttrOfType( + 0, mlir::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( + mlir::mqt::MQTDialect::QubitRegisterNameAttrHelper::getNameStr()); + ASSERT_TRUE(registerName); + EXPECT_EQ(registerName.getValue(), "q"); +} + TEST_F(QCQCORoundTripTest, PreservesClassicalRegistersWithoutConversion) { constexpr llvm::StringLiteral source = R"mlir( module { diff --git a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp index 2a490c2b7d..80918636e1 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" @@ -66,8 +67,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 +78,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 +91,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 +117,10 @@ class QCToQCORegressionTest : public testing::Test { QCToQCORegressionTest() { DialectRegistry registry; - registry.insert(); + registry + .insert(); context.appendDialectRegistry(registry); context.loadAllAvailableDialects(); } @@ -601,8 +604,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::QubitRegisterNameAttrHelper::getNameStr()); ASSERT_TRUE(name); EXPECT_EQ(name.getValue(), "named_qubits"); } @@ -627,8 +630,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::QubitRegisterNameAttrHelper::getNameStr()); ASSERT_TRUE(name); EXPECT_EQ(name.getValue(), "named_qubits"); } @@ -1442,9 +1445,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 +1471,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/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/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..c777781565 --- /dev/null +++ b/mlir/unittests/Dialect/QC/Transforms/test_qc_transforms.cpp @@ -0,0 +1,74 @@ +/* + * 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 + +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.qubit_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::QubitRegisterNameAttrHelper::getNameStr()), + StringAttr::get(&context, "q")); +} +} // namespace diff --git a/mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp b/mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp index e33d9ab555..5c7581ee77 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" @@ -62,7 +63,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 +73,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 +83,10 @@ class QASM3TranslationTest void SetUp() override { DialectRegistry registry; - registry - .insert(); + registry.insert(); context = std::make_unique(); context->appendDialectRegistry(registry); context->loadAllAvailableDialects(); @@ -908,7 +909,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 +923,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); }); @@ -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::QubitRegisterNameAttrHelper::getNameStr()); ASSERT_TRUE(name); EXPECT_EQ(name.getValue(), "named_qubits"); } 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..2838d00308 --- /dev/null +++ b/mlir/unittests/Dialect/QTensor/Transforms/test_qtensor_transforms.cpp @@ -0,0 +1,79 @@ +/* + * 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 + +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.qubit_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::QubitRegisterNameAttrHelper::getNameStr()), + StringAttr::get(&context, "q")); +} +} // namespace diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index aab8966932..4de23edd2a 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -1420,10 +1420,11 @@ def test_conflicting_local_parameter_uuid_alias_fails_without_mutation() -> None assert circuit.parameters == {global_parameter} -def test_duplicate_named_symbolic_inputs_fail_closed_without_mutation() -> None: - """Reject ambiguous Qiskit parameter names before changing the source IR.""" - program = QCProgram.from_mlir_str( - """module { +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"} @@ -1436,13 +1437,7 @@ def test_duplicate_named_symbolic_inputs_fail_closed_without_mutation() -> None: } } """ - ) - source_ir = program.ir - - with pytest.raises(RuntimeError, match="requires unique parameter names"): - program.to_qiskit() - - assert program.ir == source_ir + ) def test_parameter_names_with_null_characters_fail_closed() -> None: @@ -1457,8 +1452,9 @@ def test_parameter_names_with_null_characters_fail_closed() -> None: assert list(circuit.data) == source_data - program = QCProgram.from_mlir_str( - r"""module { + 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 {passthrough = ["entry_point"]} { %q = qc.alloc : !qc.qubit qc.rz(%theta) %q : !qc.qubit @@ -1467,13 +1463,7 @@ def test_parameter_names_with_null_characters_fail_closed() -> None: } } """ - ) - source_ir = program.ir - - with pytest.raises(RuntimeError, match="names with null characters"): - program.to_qiskit() - - assert program.ir == source_ir + ) def test_named_symbolic_input_exports_to_qiskit() -> None: From 2ee2857267564df08db851fcef060b17aeaaf63d Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Thu, 20 Aug 2026 11:38:31 +0000 Subject: [PATCH 07/17] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Key=20Qiskit=20param?= =?UTF-8?q?eters=20by=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove Qiskit UUIDs from the normalized parameter representation and key import, export, and writer state by the supported unique source name. Reject collisions across free and lexically bound parameters during preflight, including binders in separate scopes. Assisted-by: Codex --- .agent/plans/qiskit-symbolic-parameters.md | 65 +++++++-------- bindings/mlir/qiskit/Qiskit2_5.cpp | 47 +++-------- bindings/mlir/qiskit/QiskitExport.cpp | 39 +++------ bindings/mlir/qiskit/QiskitImport.cpp | 89 +++++++++------------ bindings/mlir/qiskit/QiskitTranslation.h | 1 - test/python/test_mlir_qiskit_translation.py | 58 ++++---------- 6 files changed, 109 insertions(+), 190 deletions(-) diff --git a/.agent/plans/qiskit-symbolic-parameters.md b/.agent/plans/qiskit-symbolic-parameters.md index b2f01a62f7..af966ea4d5 100644 --- a/.agent/plans/qiskit-symbolic-parameters.md +++ b/.agent/plans/qiskit-symbolic-parameters.md @@ -35,13 +35,12 @@ partially constructed output circuit after a failure. Exact 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 identity rather than - name. + 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 identity through nested control + 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 @@ -70,9 +69,9 @@ partially constructed output circuit after a failure. Exact - [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. -- [ ] 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:36Z) Replace Qiskit UUID-based parameter identity with the + supported unique-name contract and reject all free/bound name collisions + before module creation. - [ ] Replace the nullable parameter-expression node with a closed variant so malformed node states cannot be constructed. - [ ] Reject non-finite constant gate parameters in the shared QC and QCO gate @@ -92,12 +91,13 @@ partially constructed output circuit after a failure. Exact 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. The importer must compare canonical scalar symbol - metadata and reject such aliases before creating a module. + 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 identities. + 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. @@ -135,13 +135,12 @@ partially constructed output circuit after a failure. Exact - 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 another identity are ambiguous and - outside the supported source contract; UUID/name mismatch objects are also - invalid input rather than an IR requirement. Date/Author: 2026-08-20 / Codex. -- Decision: Preserve symbol sharing but do not preserve Qiskit's original UUID - across a round trip. Rationale: the compiler input is the frontend-neutral - identity. The writer creates exactly one Qiskit symbol for each input and - reuses it throughout gates and global phase. Date/Author: 2026-08-18 / Codex. + 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 @@ -151,11 +150,10 @@ partially constructed output circuit after a failure. Exact 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 identity 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: 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.qubit_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 @@ -174,10 +172,10 @@ partially constructed output circuit after a failure. Exact ## Outcomes & Retrospective The scalar implementation is complete. Shared direct symbols, bounded real -expression trees, parameterized definitions, identity-safe loop bindings, and -global phase passed the original focused validation. Unused named inputs now -fail before writer allocation rather than disappearing. The split branch builds -and passes all 158 Qiskit translation tests after #2158 merged. +expression trees, parameterized definitions, name-safe loop bindings, and global +phase passed the original focused validation. Unused named inputs now fail +before writer allocation rather than disappearing. The split branch builds and +passes all 158 Qiskit translation tests after #2158 merged. ## Context and Orientation @@ -211,8 +209,8 @@ 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 so its UUID is -preserved. +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 @@ -229,7 +227,7 @@ 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 identity. +input name. Finally, add focused Python regressions for direct and shared symbols, nested binary and unary expressions, reverse operators, partial binding, global phase, @@ -322,12 +320,11 @@ validation. ## Interfaces and Dependencies `Parameter` in `QiskitTranslation.h` is a copyable immutable tree with a kind, -finite numeric value or symbol name and identity, and zero, one, or two child -pointers. `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. +finite numeric value or symbol name, and zero, one, or two child pointers. +`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 diff --git a/bindings/mlir/qiskit/Qiskit2_5.cpp b/bindings/mlir/qiskit/Qiskit2_5.cpp index f92a74b5f0..02e6eca21b 100644 --- a/bindings/mlir/qiskit/Qiskit2_5.cpp +++ b/bindings/mlir/qiskit/Qiskit2_5.cpp @@ -208,16 +208,12 @@ normalizePythonParameterLeaf(const nb::handle parameter) { return {.kind = ParameterKind::Number, .number = complexNumber.real()}; } - if (!nb::hasattr(parameter, "name") || !nb::hasattr(parameter, "uuid")) { + 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"); - auto identity = - pythonText(pythonAttribute(parameter, "uuid", - "Qiskit parameter has no stable identity"), - "Qiskit parameter has an invalid stable identity"); if (name.empty()) { throw std::runtime_error("Qiskit parameter has an empty symbol name"); } @@ -225,16 +221,7 @@ normalizePythonParameterLeaf(const nb::handle parameter) { throw std::runtime_error( "Qiskit parameter names cannot contain null characters"); } - if (identity.empty()) { - throw std::runtime_error("Qiskit parameter has an empty stable identity"); - } - if (identity.find('\0') != std::string::npos) { - throw std::runtime_error( - "Qiskit parameter identities cannot contain null characters"); - } - Parameter result{.kind = ParameterKind::Symbol, - .text = std::move(name), - .identity = std::move(identity)}; + Parameter result{.kind = ParameterKind::Symbol, .text = std::move(name)}; const auto vectorElement = nb::module_::import_("qiskit.circuit").attr("ParameterVectorElement"); if (nb::isinstance(parameter, vectorElement)) { @@ -374,7 +361,7 @@ takeParameterExpressionOperand(const nb::handle operand, } [[nodiscard]] Parameter normalizePythonParameter(const nb::handle parameter) { - if (nb::hasattr(parameter, "name") && nb::hasattr(parameter, "uuid")) { + if (nb::hasattr(parameter, "name")) { return normalizePythonParameterLeaf(parameter); } @@ -1689,29 +1676,18 @@ class NativeCircuitWriter final : public CircuitWriter { throw std::runtime_error( "symbolic parameter expression node has operands"); } - if (parameter.identity.empty()) { - throw std::runtime_error( - "cannot export a symbolic parameter without a stable identity"); - } if (parameter.text.empty()) { throw std::runtime_error( "cannot export a symbolic parameter without a name"); } - const auto found = symbols_.find(parameter.identity); + const auto found = symbols_.find(parameter.text); if (found != symbols_.end()) { - if (found->second.name != parameter.text) { - throw std::runtime_error( - "one symbolic parameter identity has conflicting metadata"); - } - return found->second.parameter->get(); + return found->second->get(); } - auto [inserted, success] = - symbols_.emplace(parameter.identity, - Symbol{.name = parameter.text, - .parameter = std::make_unique( - parameter.text)}); + auto [inserted, success] = symbols_.emplace( + parameter.text, std::make_unique(parameter.text)); static_cast(success); - return inserted->second.parameter->get(); + return inserted->second->get(); } const auto unary = parameter.kind == ParameterKind::Negate || @@ -1797,14 +1773,9 @@ class NativeCircuitWriter final : public CircuitWriter { return value; } - struct Symbol { - std::string name; - std::unique_ptr parameter; - }; - QkCircuit* circuit_ = nullptr; std::vector pendingControlledUnitaries_; - std::unordered_map symbols_; + std::unordered_map> symbols_; }; class NativeTranslation final : public VersionedTranslation { diff --git a/bindings/mlir/qiskit/QiskitExport.cpp b/bindings/mlir/qiskit/QiskitExport.cpp index b013822f8d..a514133de7 100644 --- a/bindings/mlir/qiskit/QiskitExport.cpp +++ b/bindings/mlir/qiskit/QiskitExport.cpp @@ -245,14 +245,12 @@ void validateExportParameterImpl(const Parameter& parameter, const size_t depth, return; case ParameterKind::Symbol: requireLeaf(); - if (parameter.text.empty() || parameter.identity.empty()) { - throw std::runtime_error( - "QC parameter symbol has invalid identity metadata"); + if (parameter.text.empty()) { + throw std::runtime_error("QC parameter symbol has an invalid name"); } - if (parameter.text.find('\0') != std::string::npos || - parameter.identity.find('\0') != std::string::npos) { + if (parameter.text.find('\0') != std::string::npos) { throw std::runtime_error( - "QC parameter symbol metadata contains a null character"); + "QC parameter symbol name contains a null character"); } return; case ParameterKind::Add: @@ -353,25 +351,25 @@ struct ExportState { uint32_t numClbits = 0; }; -void collectParameterIdentities(const Parameter& parameter, - llvm::StringSet<>& identities) { +void collectParameterNames(const Parameter& parameter, + llvm::StringSet<>& names) { if (parameter.kind == ParameterKind::Symbol) { - identities.insert(parameter.identity); + names.insert(parameter.text); return; } if (parameter.left) { - collectParameterIdentities(*parameter.left, identities); + collectParameterNames(*parameter.left, names); } if (parameter.right) { - collectParameterIdentities(*parameter.right, identities); + collectParameterNames(*parameter.right, names); } } void validateExportParameters(const ExportState& state) { - llvm::StringSet<> usedIdentities; + llvm::StringSet<> usedNames; const auto validate = [&](const Parameter& parameter) { validateExportParameter(parameter); - collectParameterIdentities(parameter, usedIdentities); + collectParameterNames(parameter, usedNames); }; validate(state.globalPhase); for (const auto& instruction : state.instructions) { @@ -380,7 +378,7 @@ void validateExportParameters(const ExportState& state) { } } for (const auto& input : state.inputParameters) { - if (!usedIdentities.contains(input.identity)) { + if (!usedNames.contains(input.text)) { throw std::runtime_error( "Qiskit circuit export cannot preserve unused named f64 program " "input '" + @@ -390,28 +388,17 @@ void validateExportParameters(const ExportState& state) { } void collectParameters(mlir::func::FuncOp function, ExportState& state) { - llvm::StringSet<> names; 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 || name.getValue().empty()) { + if (!argument.getType().isF64() || !name) { throw std::runtime_error( "Qiskit circuit export requires named f64 program inputs"); } - if (name.getValue().contains('\0')) { - throw std::runtime_error( - "Qiskit circuit export does not support parameter names with null " - "characters"); - } - if (!names.insert(name.getValue()).second) { - throw std::runtime_error( - "Qiskit circuit export requires unique parameter names"); - } Parameter parameter{ .kind = ParameterKind::Symbol, .text = name.str(), - .identity = "input:" + std::to_string(index), }; state.parameters[argument] = parameter; state.inputParameters.push_back(std::move(parameter)); diff --git a/bindings/mlir/qiskit/QiskitImport.cpp b/bindings/mlir/qiskit/QiskitImport.cpp index f1cada71d6..b46de15900 100644 --- a/bindings/mlir/qiskit/QiskitImport.cpp +++ b/bindings/mlir/qiskit/QiskitImport.cpp @@ -156,27 +156,14 @@ void validateParameterImpl(const Parameter& parameter, throw std::runtime_error( "Qiskit parameter-expression leaf has unexpected operands"); } - if (parameter.identity.empty() || parameter.text.empty()) { + if (parameter.text.empty()) { throw std::runtime_error( "Qiskit returned a parameter with invalid symbol metadata"); } - if (const auto local = localParameters.find(parameter.identity); - local != localParameters.end()) { - if (parameter.text != local->second.text) { - throw std::runtime_error("Qiskit parameter symbol '" + parameter.text + - "' aliases local symbol '" + - local->second.text + - "' with inconsistent metadata"); - } + if (localParameters.contains(parameter.text)) { return; } - if (const auto free = freeParameters.find(parameter.identity); - free != freeParameters.end()) { - if (parameter.text != free->second.text) { - throw std::runtime_error("Qiskit parameter symbol '" + parameter.text + - "' aliases free symbol '" + free->second.text + - "' with inconsistent metadata"); - } + if (freeParameters.contains(parameter.text)) { return; } throw std::runtime_error("Qiskit parameter symbol '" + parameter.text + @@ -261,15 +248,13 @@ parameterValueImpl(mlir::qc::QCProgramBuilder& builder, } return parameter.number; case ParameterKind::Symbol: - if (!parameter.identity.empty()) { - if (const auto local = localParameters.find(parameter.identity); - local != localParameters.end()) { - return local->second; - } - if (const auto global = globalParameters.find(parameter.identity); - global != globalParameters.end()) { - return global->second; - } + if (const auto local = localParameters.find(parameter.text); + local != localParameters.end()) { + return local->second; + } + if (const auto global = globalParameters.find(parameter.text); + global != globalParameters.end()) { + return global->second; } throw std::runtime_error("Qiskit parameter symbol '" + parameter.text + "' is not defined in this circuit scope"); @@ -1114,7 +1099,7 @@ void translateControlFlow(mlir::qc::QCProgramBuilder& builder, auto parameters = localParameters; if (loop.parameter) { requireExactLoopParameter(value); - parameters[loop.parameter->identity] = + parameters[loop.parameter->text] = floatConstant(builder, static_cast(value)); } translateBlock(*body, parameters); @@ -1129,7 +1114,7 @@ void translateControlFlow(mlir::qc::QCProgramBuilder& builder, builder.scfFor(0, count, 1, [&](const mlir::Value iteration) { auto parameters = localParameters; if (loop.parameter) { - parameters[loop.parameter->identity] = + parameters[loop.parameter->text] = loopParameterValue(builder, iteration, loop); } translateBlock(*body, parameters); @@ -1445,8 +1430,9 @@ expansionSummary(const CircuitReader& circuit, ExpansionCountState& state, void validateCircuit(const CircuitReader& circuit, const ValidationParameters& localParameters, const ValidationParameters& freeParameters, - uint32_t rootQubits, uint32_t rootClbits, - size_t definitionDepth, size_t controlFlowDepth); + 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 && @@ -1509,6 +1495,7 @@ void validateTarget(const ClassicalTarget& target, const uint32_t rootClbits) { void validateControlFlow(const ControlFlowReader& controlFlow, ValidationParameters localParameters, const ValidationParameters& freeParameters, + llvm::StringSet<>& parameterNames, const uint32_t rootQubits, const uint32_t rootClbits, const size_t definitionDepth, const size_t controlFlowDepth) { @@ -1569,11 +1556,15 @@ void validateControlFlow(const ControlFlowReader& controlFlow, } if (loop.parameter) { if (loop.parameter->kind != ParameterKind::Symbol || - loop.parameter->identity.empty() || loop.parameter->text.empty()) { + loop.parameter->text.empty()) { throw std::runtime_error( "Qiskit for-loop parameter has invalid symbol metadata"); } - localParameters[loop.parameter->identity] = *loop.parameter; + if (!parameterNames.insert(loop.parameter->text).second) { + throw std::runtime_error( + "Qiskit circuit contains distinct parameters with the same name"); + } + localParameters[loop.parameter->text] = *loop.parameter; } break; } @@ -1628,14 +1619,16 @@ void validateControlFlow(const ControlFlowReader& controlFlow, throw std::runtime_error( "Qiskit control-flow block operands do not match its bit mapping"); } - validateCircuit(*block, localParameters, freeParameters, 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 ValidationParameters& localParameters, const ValidationParameters& freeParameters, + llvm::StringSet<>& parameterNames, const size_t definitionDepth, const size_t controlFlowDepth) { if (definitionDepth >= MAX_DEFINITION_DEPTH) { @@ -1643,7 +1636,7 @@ 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, freeParameters, + validateCircuit(*definition, localParameters, freeParameters, parameterNames, definition->numQubits(), definition->numClbits(), definitionDepth + 1U, controlFlowDepth); } @@ -1651,6 +1644,7 @@ void validateDefinition(const CircuitReader& circuit, const size_t index, void validateCircuit(const CircuitReader& circuit, 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) { @@ -1714,7 +1708,7 @@ void validateCircuit(const CircuitReader& circuit, "instructions"); } validateDefinition(circuit, index, localParameters, freeParameters, - definitionDepth, controlFlowDepth); + parameterNames, definitionDepth, controlFlowDepth); break; case OperationKind::Unknown: if (!instruction.modifiers.empty()) { @@ -1723,7 +1717,7 @@ void validateCircuit(const CircuitReader& circuit, "instructions"); } validateDefinition(circuit, index, localParameters, freeParameters, - definitionDepth, controlFlowDepth); + parameterNames, definitionDepth, controlFlowDepth); break; case OperationKind::Barrier: if (!instruction.parameters.empty() || !instruction.clbits.empty()) { @@ -1749,8 +1743,8 @@ void validateCircuit(const CircuitReader& circuit, case OperationKind::ControlFlow: { const auto controlFlow = circuit.controlFlow(index); validateControlFlow(*controlFlow, localParameters, freeParameters, - rootQubits, rootClbits, definitionDepth, - controlFlowDepth); + parameterNames, rootQubits, rootClbits, + definitionDepth, controlFlowDepth); break; } case OperationKind::Delay: @@ -1766,28 +1760,23 @@ mlir::QCProgram importCircuit(const nb::handle circuit) { auto view = translation->openCircuit(circuit); const auto freeParameters = view->parameters(); ValidationParameters freeParameterSymbols; - llvm::StringSet<> freeParameterNames; + llvm::StringSet<> parameterNames; for (const auto& parameter : freeParameters) { - if (parameter.kind != ParameterKind::Symbol || parameter.text.empty() || - parameter.identity.empty()) { + if (parameter.kind != ParameterKind::Symbol || parameter.text.empty()) { throw std::runtime_error( "Qiskit circuit returned an invalid free parameter"); } - if (!freeParameterSymbols.try_emplace(parameter.identity, parameter) - .second) { - throw std::runtime_error( - "Qiskit circuit returned a duplicate parameter identity"); - } - if (!freeParameterNames.insert(parameter.text).second) { + if (!parameterNames.insert(parameter.text).second) { throw std::runtime_error( "Qiskit circuit contains distinct parameters with the same name"); } + freeParameterSymbols.try_emplace(parameter.text, parameter); } ExpansionCountState expansion; static_cast(expansionSummary(*view, expansion)); - validateCircuit(*view, {}, freeParameterSymbols, 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); const auto looseQubits = @@ -1830,7 +1819,7 @@ mlir::QCProgram importCircuit(const nb::handle circuit) { throw std::runtime_error( "failed to create a compiler input for a Qiskit parameter"); } - globalParameters[parameter.identity] = function.getArgument(index); + globalParameters[parameter.text] = function.getArgument(index); } llvm::SmallVector qubits; diff --git a/bindings/mlir/qiskit/QiskitTranslation.h b/bindings/mlir/qiskit/QiskitTranslation.h index e90ccf69f1..64974136c3 100644 --- a/bindings/mlir/qiskit/QiskitTranslation.h +++ b/bindings/mlir/qiskit/QiskitTranslation.h @@ -77,7 +77,6 @@ struct Parameter { ParameterKind kind = ParameterKind::Number; double number = 0.0; std::string text; - std::string identity; std::shared_ptr left; std::shared_ptr right; }; diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index 4de23edd2a..2833a9a2ee 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -15,7 +15,6 @@ import subprocess import sys from typing import TYPE_CHECKING -from uuid import uuid4 import numpy as np import pytest @@ -1197,7 +1196,7 @@ def test_symbolic_unary_function_round_trip( def test_partially_bound_symbolic_expression_round_trip() -> None: - """Keep the unbound identity after partially binding an expression.""" + """Keep the unbound parameter after partially binding an expression.""" theta = Parameter("theta") phi = Parameter("phi") angle = (theta * phi + phi.sin()).bind({theta: 0.5}) @@ -1359,8 +1358,8 @@ def test_unsupported_scalar_operation_fails_export_without_mutation() -> None: assert program.ir == source_ir -def test_same_name_global_and_loop_parameters_use_identity_not_name() -> None: - """Do not capture a same-name global symbol as a loop induction value.""" +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) @@ -1370,54 +1369,31 @@ def test_same_name_global_and_loop_parameters_use_identity_not_name() -> None: circuit.for_loop(range(2), loop_parameter, body, [0], [], label=None) source_data = list(circuit.data) - program = QCProgram.from_qiskit(circuit) - - input_match = re.search(r"func\.func @main\((%[^: ]+): f64 \{[^}]*mqt\.input_name = \"theta\"[^}]*\}", program.ir) - assert input_match is not None - assert f"qc.ry({input_match.group(1)})" in program.ir - assert list(circuit.data) == source_data - assert circuit.parameters == {global_parameter} - - -def test_conflicting_free_parameter_uuid_alias_fails_without_mutation() -> None: - """Reject differently named free symbols that share one stable identity.""" - identity = uuid4() - canonical = Parameter("canonical", uuid=identity) - alias = Parameter("alias", uuid=identity) - circuit = QuantumCircuit(1) - circuit.rx(canonical, 0) - circuit.rz(alias, 0) - source_data = list(circuit.data) - - with pytest.raises( - RuntimeError, - match="parameter symbol 'alias' aliases free symbol 'canonical' with inconsistent metadata", - ): + with pytest.raises(RuntimeError, match="distinct parameters with the same name"): QCProgram.from_qiskit(circuit) assert list(circuit.data) == source_data - assert circuit.parameters == {canonical} + assert circuit.parameters == {global_parameter} -def test_conflicting_local_parameter_uuid_alias_fails_without_mutation() -> None: - """Resolve a shared identity against the active lexical loop binding.""" - identity = uuid4() - global_parameter = Parameter("global", uuid=identity) - loop_parameter = Parameter("local", uuid=identity) - body = QuantumCircuit(1) - body.ry(global_parameter, 0) +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), loop_parameter, body, [0], [], label=None) + 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="parameter symbol 'global' aliases local symbol 'local' with inconsistent metadata", - ): + 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} + assert not circuit.parameters def test_duplicate_named_symbolic_inputs_are_invalid_qc_ir() -> None: From f1f648a72c943082a9c6c895c6e2eb5b97f46dfb Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Thu, 20 Aug 2026 11:45:22 +0000 Subject: [PATCH 08/17] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Close=20Qiskit=20par?= =?UTF-8?q?ameter=20expression=20states?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Represent normalized parameters as a private variant of number, symbol, unary, and binary nodes with separate operator enums. Construct expression nodes through factories with required operands, then remove redundant malformed-tree checks from import, export, and native Qiskit writing. Assisted-by: Codex --- .agent/plans/qiskit-symbolic-parameters.md | 22 +- bindings/mlir/qiskit/Qiskit2_5.cpp | 218 +++++++++---------- bindings/mlir/qiskit/QiskitExport.cpp | 174 ++++++--------- bindings/mlir/qiskit/QiskitImport.cpp | 238 +++++++++------------ bindings/mlir/qiskit/QiskitTranslation.h | 91 ++++++-- 5 files changed, 363 insertions(+), 380 deletions(-) diff --git a/.agent/plans/qiskit-symbolic-parameters.md b/.agent/plans/qiskit-symbolic-parameters.md index af966ea4d5..f159e13c90 100644 --- a/.agent/plans/qiskit-symbolic-parameters.md +++ b/.agent/plans/qiskit-symbolic-parameters.md @@ -72,8 +72,8 @@ partially constructed output circuit after a failure. Exact - [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. -- [ ] Replace the nullable parameter-expression node with a closed variant so - malformed node states cannot be constructed. +- [x] (2026-08-20 11:44Z) Replace the nullable parameter-expression node with a + closed variant so malformed node states cannot be constructed. - [ ] Reject non-finite constant gate parameters in the shared QC and QCO gate verifier rather than in individual import/export paths. - [ ] Run focused dialect, conversion, compiler, Qiskit, documentation, stub, @@ -168,6 +168,11 @@ partially constructed output circuit after a failure. Exact 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. ## Outcomes & Retrospective @@ -319,12 +324,13 @@ validation. ## Interfaces and Dependencies -`Parameter` in `QiskitTranslation.h` is a copyable immutable tree with a kind, -finite numeric value or symbol name, and zero, one, or two child pointers. -`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. +`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 diff --git a/bindings/mlir/qiskit/Qiskit2_5.cpp b/bindings/mlir/qiskit/Qiskit2_5.cpp index 02e6eca21b..c5c32c434a 100644 --- a/bindings/mlir/qiskit/Qiskit2_5.cpp +++ b/bindings/mlir/qiskit/Qiskit2_5.cpp @@ -176,7 +176,7 @@ QkExitCode addParameterizedGate(QkCircuit* circuit, const QkGate gate, const auto isNumber = qk_param_equal(parameter, numeric); qk_param_free(numeric); if (isNumber) { - return {.kind = ParameterKind::Number, .number = number}; + return Parameter::number(number); } } throw std::runtime_error( @@ -191,7 +191,7 @@ normalizePythonParameterLeaf(const nb::handle parameter) { if (!std::isfinite(number)) { throw std::runtime_error("Qiskit returned a non-finite parameter"); } - return {.kind = ParameterKind::Number, .number = number}; + return Parameter::number(number); } std::complex complexNumber; @@ -205,7 +205,7 @@ normalizePythonParameterLeaf(const nb::handle parameter) { "Qiskit parameter expressions with complex values are not " "supported"); } - return {.kind = ParameterKind::Number, .number = complexNumber.real()}; + return Parameter::number(complexNumber.real()); } if (!nb::hasattr(parameter, "name")) { @@ -221,7 +221,7 @@ normalizePythonParameterLeaf(const nb::handle parameter) { throw std::runtime_error( "Qiskit parameter names cannot contain null characters"); } - Parameter result{.kind = ParameterKind::Symbol, .text = std::move(name)}; + auto result = Parameter::symbol(std::move(name)); const auto vectorElement = nb::module_::import_("qiskit.circuit").attr("ParameterVectorElement"); if (nb::isinstance(parameter, vectorElement)) { @@ -272,17 +272,14 @@ takeParameterExpressionOperand(const nb::handle operand, return {.value = normalizePythonParameterLeaf(operand)}; } -[[nodiscard]] Parameter makeUnaryParameter(const ParameterKind kind, +[[nodiscard]] Parameter makeUnaryParameter(const UnaryParameterKind kind, Parameter operand) { - return {.kind = kind, - .left = std::make_shared(std::move(operand))}; + return Parameter::unary(kind, std::move(operand)); } -[[nodiscard]] Parameter makeBinaryParameter(const ParameterKind kind, +[[nodiscard]] Parameter makeBinaryParameter(const BinaryParameterKind kind, Parameter lhs, Parameter rhs) { - return {.kind = kind, - .left = std::make_shared(std::move(lhs)), - .right = std::make_shared(std::move(rhs))}; + return Parameter::binary(kind, std::move(lhs), std::move(rhs)); } [[nodiscard]] std::string parameterOpcode(const nb::handle replayEntry) { @@ -304,38 +301,39 @@ takeParameterExpressionOperand(const nb::handle operand, opcode == "ABS" || opcode == "CONJ" || opcode == "CONJUGATE"; } -[[nodiscard]] ParameterKind unaryParameterKind(const std::string_view opcode) { +[[nodiscard]] UnaryParameterKind +unaryParameterKind(const std::string_view opcode) { if (opcode == "NEG") { - return ParameterKind::Negate; + return UnaryParameterKind::Negate; } if (opcode == "SIN") { - return ParameterKind::Sin; + return UnaryParameterKind::Sin; } if (opcode == "COS") { - return ParameterKind::Cos; + return UnaryParameterKind::Cos; } if (opcode == "TAN") { - return ParameterKind::Tan; + return UnaryParameterKind::Tan; } if (opcode == "ASIN") { - return ParameterKind::ArcSin; + return UnaryParameterKind::ArcSin; } if (opcode == "ACOS") { - return ParameterKind::ArcCos; + return UnaryParameterKind::ArcCos; } if (opcode == "ATAN") { - return ParameterKind::ArcTan; + return UnaryParameterKind::ArcTan; } if (opcode == "EXP") { - return ParameterKind::Exp; + return UnaryParameterKind::Exp; } if (opcode == "LOG") { - return ParameterKind::Log; + return UnaryParameterKind::Log; } if (opcode == "ABS") { - return ParameterKind::Abs; + return UnaryParameterKind::Abs; } - return ParameterKind::Conjugate; + return UnaryParameterKind::Conjugate; } [[nodiscard]] bool isBinaryParameterOpcode(const std::string_view opcode) { @@ -344,20 +342,21 @@ takeParameterExpressionOperand(const nb::handle operand, opcode == "RDIV" || opcode == "RPOW"; } -[[nodiscard]] ParameterKind binaryParameterKind(const std::string_view opcode) { +[[nodiscard]] BinaryParameterKind +binaryParameterKind(const std::string_view opcode) { if (opcode == "ADD") { - return ParameterKind::Add; + return BinaryParameterKind::Add; } if (opcode == "SUB" || opcode == "RSUB") { - return ParameterKind::Subtract; + return BinaryParameterKind::Subtract; } if (opcode == "MUL") { - return ParameterKind::Multiply; + return BinaryParameterKind::Multiply; } if (opcode == "DIV" || opcode == "RDIV") { - return ParameterKind::Divide; + return BinaryParameterKind::Divide; } - return ParameterKind::Power; + return BinaryParameterKind::Power; } [[nodiscard]] Parameter normalizePythonParameter(const nb::handle parameter) { @@ -1383,10 +1382,11 @@ class NativeControlFlowReader final : public ControlFlowReader { "Qiskit for-loop operation has no loop parameter"); } auto parameter = normalizePythonParameter(parameters[1]); - if (parameter.kind != ParameterKind::Symbol) { + const auto* parameterSymbol = parameter.getSymbol(); + if (parameterSymbol == nullptr) { throw std::runtime_error("Qiskit for-loop parameter is not a symbol"); } - if (parameter.text != nativeName) { + if (parameterSymbol->name != nativeName) { throw std::runtime_error( "Qiskit Python and native loop-parameter names do not match"); } @@ -1662,110 +1662,90 @@ class NativeCircuitWriter final : public CircuitWriter { if (depth > MAX_PARAMETER_EXPRESSION_DEPTH) { throwParameterExpressionDepthError(); } - if (parameter.kind == ParameterKind::Number) { - if (parameter.left != nullptr || parameter.right != nullptr) { - throw std::runtime_error( - "numeric parameter expression node has operands"); - } + if (const auto* number = parameter.getNumber()) { ownedParameters.emplace_back( - std::make_unique(parameter.number)); + std::make_unique(number->value)); return ownedParameters.back()->get(); } - if (parameter.kind == ParameterKind::Symbol) { - if (parameter.left != nullptr || parameter.right != nullptr) { - throw std::runtime_error( - "symbolic parameter expression node has operands"); - } - if (parameter.text.empty()) { + 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(parameter.text); + const auto found = symbols_.find(symbol->name); if (found != symbols_.end()) { return found->second->get(); } auto [inserted, success] = symbols_.emplace( - parameter.text, std::make_unique(parameter.text)); + symbol->name, std::make_unique(symbol->name)); static_cast(success); return inserted->second->get(); } - const auto unary = parameter.kind == ParameterKind::Negate || - parameter.kind == ParameterKind::Sin || - parameter.kind == ParameterKind::Cos || - parameter.kind == ParameterKind::Tan || - parameter.kind == ParameterKind::ArcSin || - parameter.kind == ParameterKind::ArcCos || - parameter.kind == ParameterKind::ArcTan || - parameter.kind == ParameterKind::Exp || - parameter.kind == ParameterKind::Log || - parameter.kind == ParameterKind::Abs || - parameter.kind == ParameterKind::Conjugate; - if (parameter.left == nullptr || (unary && parameter.right != nullptr) || - (!unary && parameter.right == nullptr)) { - throw std::runtime_error("parameter expression has invalid operands"); - } - const auto* left = nativeParameter(*parameter.left, ownedParameters, - nodeCount, depth + 1U); - const QkParam* right = nullptr; - if (!unary) { - right = nativeParameter(*parameter.right, ownedParameters, nodeCount, - depth + 1U); - } auto output = std::make_unique(); QkExitCode result = QkExitCode_Success; - switch (parameter.kind) { - case ParameterKind::Number: - case ParameterKind::Symbol: - throw std::runtime_error("invalid parameter expression node"); - case ParameterKind::Add: - result = qk_param_add(output->getMutable(), left, right); - break; - case ParameterKind::Subtract: - result = qk_param_sub(output->getMutable(), left, right); - break; - case ParameterKind::Multiply: - result = qk_param_mul(output->getMutable(), left, right); - break; - case ParameterKind::Divide: - result = qk_param_div(output->getMutable(), left, right); - break; - case ParameterKind::Power: - result = qk_param_pow(output->getMutable(), left, right); - break; - case ParameterKind::Negate: - result = qk_param_neg(output->getMutable(), left); - break; - case ParameterKind::Sin: - result = qk_param_sin(output->getMutable(), left); - break; - case ParameterKind::Cos: - result = qk_param_cos(output->getMutable(), left); - break; - case ParameterKind::Tan: - result = qk_param_tan(output->getMutable(), left); - break; - case ParameterKind::ArcSin: - result = qk_param_asin(output->getMutable(), left); - break; - case ParameterKind::ArcCos: - result = qk_param_acos(output->getMutable(), left); - break; - case ParameterKind::ArcTan: - result = qk_param_atan(output->getMutable(), left); - break; - case ParameterKind::Exp: - result = qk_param_exp(output->getMutable(), left); - break; - case ParameterKind::Log: - result = qk_param_log(output->getMutable(), left); - break; - case ParameterKind::Abs: - result = qk_param_abs(output->getMutable(), left); - break; - case ParameterKind::Conjugate: - result = qk_param_conjugate(output->getMutable(), left); - break; + 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(); diff --git a/bindings/mlir/qiskit/QiskitExport.cpp b/bindings/mlir/qiskit/QiskitExport.cpp index a514133de7..eb3ea7ad8c 100644 --- a/bindings/mlir/qiskit/QiskitExport.cpp +++ b/bindings/mlir/qiskit/QiskitExport.cpp @@ -92,20 +92,17 @@ using ExportedParameters = llvm::DenseMap; } [[nodiscard]] Parameter numberParameter(const double value) { - return {.kind = ParameterKind::Number, .number = value}; + return Parameter::number(value); } -[[nodiscard]] Parameter unaryParameter(const ParameterKind kind, +[[nodiscard]] Parameter unaryParameter(const UnaryParameterKind kind, Parameter operand) { - return {.kind = kind, - .left = std::make_shared(std::move(operand))}; + return Parameter::unary(kind, std::move(operand)); } -[[nodiscard]] Parameter binaryParameter(const ParameterKind kind, +[[nodiscard]] Parameter binaryParameter(const BinaryParameterKind kind, Parameter left, Parameter right) { - return {.kind = kind, - .left = std::make_shared(std::move(left)), - .right = std::make_shared(std::move(right))}; + return Parameter::binary(kind, std::move(left), std::move(right)); } [[nodiscard]] Parameter exportParameterImpl(mlir::Value value, @@ -138,7 +135,7 @@ using ExportedParameters = llvm::DenseMap; throw std::runtime_error( "Qiskit circuit export cannot resolve an unnamed scalar parameter"); } - const auto unary = [&](const ParameterKind kind) { + const auto unary = [&](const UnaryParameterKind kind) { if (operation->getNumOperands() != 1U) { throw std::runtime_error("QC parameter operation '" + operation->getName().getStringRef().str() + @@ -148,7 +145,7 @@ using ExportedParameters = llvm::DenseMap; exportParameterImpl(operation->getOperand(0), parameters, depth + 1U, nodes)); }; - const auto binary = [&](const ParameterKind kind) { + const auto binary = [&](const BinaryParameterKind kind) { if (operation->getNumOperands() != 2U) { throw std::runtime_error("QC parameter operation '" + operation->getName().getStringRef().str() + @@ -163,35 +160,35 @@ using ExportedParameters = llvm::DenseMap; Parameter result; if (llvm::isa(*operation)) { - result = binary(ParameterKind::Add); + result = binary(BinaryParameterKind::Add); } else if (llvm::isa(*operation)) { - result = binary(ParameterKind::Subtract); + result = binary(BinaryParameterKind::Subtract); } else if (llvm::isa(*operation)) { - result = binary(ParameterKind::Multiply); + result = binary(BinaryParameterKind::Multiply); } else if (llvm::isa(*operation)) { - result = binary(ParameterKind::Divide); + result = binary(BinaryParameterKind::Divide); } else if (llvm::isa(*operation)) { - result = binary(ParameterKind::Power); + result = binary(BinaryParameterKind::Power); } else if (llvm::isa(*operation)) { - result = unary(ParameterKind::Negate); + result = unary(UnaryParameterKind::Negate); } else if (llvm::isa(*operation)) { - result = unary(ParameterKind::Sin); + result = unary(UnaryParameterKind::Sin); } else if (llvm::isa(*operation)) { - result = unary(ParameterKind::Cos); + result = unary(UnaryParameterKind::Cos); } else if (llvm::isa(*operation)) { - result = unary(ParameterKind::Tan); + result = unary(UnaryParameterKind::Tan); } else if (llvm::isa(*operation)) { - result = unary(ParameterKind::ArcSin); + result = unary(UnaryParameterKind::ArcSin); } else if (llvm::isa(*operation)) { - result = unary(ParameterKind::ArcCos); + result = unary(UnaryParameterKind::ArcCos); } else if (llvm::isa(*operation)) { - result = unary(ParameterKind::ArcTan); + result = unary(UnaryParameterKind::ArcTan); } else if (llvm::isa(*operation)) { - result = unary(ParameterKind::Exp); + result = unary(UnaryParameterKind::Exp); } else if (llvm::isa(*operation)) { - result = unary(ParameterKind::Log); + result = unary(UnaryParameterKind::Log); } else if (llvm::isa(*operation)) { - result = unary(ParameterKind::Abs); + result = unary(UnaryParameterKind::Abs); } else { throw std::runtime_error( "Qiskit circuit export does not support scalar parameter operation '" + @@ -215,66 +212,32 @@ void validateExportParameterImpl(const Parameter& parameter, const size_t depth, if (++nodes > MAX_PARAMETER_EXPRESSION_NODES) { throwExportedParameterExpressionSizeError(); } - const auto requireLeaf = [&] { - if (parameter.left || parameter.right) { - throw std::runtime_error( - "QC parameter-expression leaf has unexpected operands"); - } - }; - const auto requireUnary = [&] { - if (!parameter.left || parameter.right) { - throw std::runtime_error( - "QC unary parameter expression has invalid operands"); - } - validateExportParameterImpl(*parameter.left, depth + 1U, nodes); - }; - const auto requireBinary = [&] { - if (!parameter.left || !parameter.right) { - throw std::runtime_error( - "QC binary parameter expression has missing operands"); - } - validateExportParameterImpl(*parameter.left, depth + 1U, nodes); - validateExportParameterImpl(*parameter.right, depth + 1U, nodes); - }; - switch (parameter.kind) { - case ParameterKind::Number: - requireLeaf(); - if (!std::isfinite(parameter.number)) { + if (const auto* number = parameter.getNumber()) { + if (!std::isfinite(number->value)) { throw std::runtime_error("cannot export a non-finite QC parameter"); } return; - case ParameterKind::Symbol: - requireLeaf(); - if (parameter.text.empty()) { + } + if (const auto* symbol = parameter.getSymbol()) { + if (symbol->name.empty()) { throw std::runtime_error("QC parameter symbol has an invalid name"); } - if (parameter.text.find('\0') != std::string::npos) { + if (symbol->name.find('\0') != std::string::npos) { throw std::runtime_error( "QC parameter symbol name contains a null character"); } return; - case ParameterKind::Add: - case ParameterKind::Subtract: - case ParameterKind::Multiply: - case ParameterKind::Divide: - case ParameterKind::Power: - requireBinary(); + } + if (const auto* unary = parameter.getUnary()) { + validateExportParameterImpl(*unary->operand, depth + 1U, nodes); return; - case ParameterKind::Negate: - case ParameterKind::Sin: - case ParameterKind::Cos: - case ParameterKind::Tan: - case ParameterKind::ArcSin: - case ParameterKind::ArcCos: - case ParameterKind::ArcTan: - case ParameterKind::Exp: - case ParameterKind::Log: - case ParameterKind::Abs: - case ParameterKind::Conjugate: - requireUnary(); + } + 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 kind"); + throw std::runtime_error("unknown QC parameter expression"); } void validateExportParameter(const Parameter& parameter) { @@ -346,22 +309,24 @@ struct ExportState { std::vector classicalRegisters; ExportedParameters parameters; std::vector inputParameters; - Parameter globalPhase{.kind = ParameterKind::Number, .number = 0.0}; + Parameter globalPhase; uint32_t numQubits = 0; uint32_t numClbits = 0; }; void collectParameterNames(const Parameter& parameter, llvm::StringSet<>& names) { - if (parameter.kind == ParameterKind::Symbol) { - names.insert(parameter.text); + if (const auto* symbol = parameter.getSymbol()) { + names.insert(symbol->name); return; } - if (parameter.left) { - collectParameterNames(*parameter.left, names); + if (const auto* unary = parameter.getUnary()) { + collectParameterNames(*unary->operand, names); + return; } - if (parameter.right) { - collectParameterNames(*parameter.right, names); + if (const auto* binary = parameter.getBinary()) { + collectParameterNames(*binary->left, names); + collectParameterNames(*binary->right, names); } } @@ -378,11 +343,15 @@ void validateExportParameters(const ExportState& state) { } } for (const auto& input : state.inputParameters) { - if (!usedNames.contains(input.text)) { + 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 '" + - input.text + "'"); + symbol->name + "'"); } } } @@ -396,39 +365,38 @@ void collectParameters(mlir::func::FuncOp function, ExportState& state) { throw std::runtime_error( "Qiskit circuit export requires named f64 program inputs"); } - Parameter parameter{ - .kind = ParameterKind::Symbol, - .text = name.str(), - }; + 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 (phase.kind == ParameterKind::Number) { - if (!std::isfinite(phase.number)) { + if (const auto* number = phase.getNumber()) { + if (!std::isfinite(number->value)) { throw std::runtime_error( "QC global phase cannot be represented by Qiskit"); } - if (state.globalPhase.kind == ParameterKind::Number) { - state.globalPhase.number += phase.number; - if (!std::isfinite(state.globalPhase.number)) { + 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(phase.number) <= mlir::utils::TOLERANCE) { + if (std::abs(number->value) <= mlir::utils::TOLERANCE) { return; } - } else if (state.globalPhase.kind == ParameterKind::Number && - std::abs(state.globalPhase.number) <= mlir::utils::TOLERANCE) { + } else if (const auto* globalNumber = state.globalPhase.getNumber(); + globalNumber != nullptr && + std::abs(globalNumber->value) <= mlir::utils::TOLERANCE) { state.globalPhase = phase; return; } - state.globalPhase = - binaryParameter(ParameterKind::Add, std::move(state.globalPhase), phase); + state.globalPhase = binaryParameter(BinaryParameterKind::Add, + std::move(state.globalPhase), phase); } [[nodiscard]] std::vector @@ -553,16 +521,16 @@ void invertGate(ExportedInstruction& instruction) { throw std::runtime_error("QC inverse modifier has invalid arity"); } instruction.parameters.front() = unaryParameter( - ParameterKind::Negate, std::move(instruction.parameters.front())); + UnaryParameterKind::Negate, std::move(instruction.parameters.front())); return; } if (instruction.gate.gate == Gate::U3 && instruction.parameters.size() == 3U) { auto parameters = std::move(instruction.parameters); instruction.parameters = { - unaryParameter(ParameterKind::Negate, std::move(parameters[0])), - unaryParameter(ParameterKind::Negate, std::move(parameters[2])), - unaryParameter(ParameterKind::Negate, std::move(parameters[1]))}; + 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( @@ -635,8 +603,8 @@ collectUnitaryInstruction(mlir::Operation& operation, "QC power export requires one standard gate in the modifier body"); } const auto exponent = exportParameter(power.getExponent(), parameters); - if (exponent.kind != ParameterKind::Number || - (exponent.number != 1.0 && exponent.number != -1.0)) { + 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"); } @@ -644,7 +612,7 @@ collectUnitaryInstruction(mlir::Operation& operation, modifierQubitMap(qubits, power.getRegion().front(), power.getQubits()); auto result = collectUnitaryInstruction(*bodyOperations.front(), nestedMap, parameters); - if (exponent.number == -1.0) { + if (number->value == -1.0) { invertGate(result); } return result; diff --git a/bindings/mlir/qiskit/QiskitImport.cpp b/bindings/mlir/qiskit/QiskitImport.cpp index b46de15900..e2b37d6f0b 100644 --- a/bindings/mlir/qiskit/QiskitImport.cpp +++ b/bindings/mlir/qiskit/QiskitImport.cpp @@ -127,73 +127,35 @@ void validateParameterImpl(const Parameter& parameter, if (++nodes > MAX_PARAMETER_EXPRESSION_NODES) { throwImportedParameterExpressionSizeError(); } - const auto requireLeft = [&]() -> const Parameter& { - if (!parameter.left) { - throw std::runtime_error( - "Qiskit parameter expression has a missing operand"); - } - return *parameter.left; - }; - const auto requireRight = [&]() -> const Parameter& { - if (!parameter.right) { - throw std::runtime_error( - "Qiskit parameter expression has a missing operand"); - } - return *parameter.right; - }; - switch (parameter.kind) { - case ParameterKind::Number: - if (parameter.left || parameter.right) { - throw std::runtime_error( - "Qiskit parameter-expression leaf has unexpected operands"); - } - if (!std::isfinite(parameter.number)) { + if (const auto* number = parameter.getNumber()) { + if (!std::isfinite(number->value)) { throw std::runtime_error("Qiskit returned a non-finite parameter"); } return; - case ParameterKind::Symbol: - if (parameter.left || parameter.right) { - throw std::runtime_error( - "Qiskit parameter-expression leaf has unexpected operands"); - } - if (parameter.text.empty()) { + } + 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(parameter.text)) { + if (localParameters.contains(symbol->name)) { return; } - if (freeParameters.contains(parameter.text)) { + if (freeParameters.contains(symbol->name)) { return; } - throw std::runtime_error("Qiskit parameter symbol '" + parameter.text + + throw std::runtime_error("Qiskit parameter symbol '" + symbol->name + "' is not defined in this circuit scope"); - case ParameterKind::Add: - case ParameterKind::Subtract: - case ParameterKind::Multiply: - case ParameterKind::Divide: - case ParameterKind::Power: - validateParameterImpl(requireLeft(), localParameters, freeParameters, - depth + 1U, nodes); - validateParameterImpl(requireRight(), localParameters, freeParameters, + } + if (const auto* unary = parameter.getUnary()) { + validateParameterImpl(*unary->operand, localParameters, freeParameters, depth + 1U, nodes); return; - case ParameterKind::Negate: - case ParameterKind::Sin: - case ParameterKind::Cos: - case ParameterKind::Tan: - case ParameterKind::ArcSin: - case ParameterKind::ArcCos: - case ParameterKind::ArcTan: - case ParameterKind::Exp: - case ParameterKind::Log: - case ParameterKind::Abs: - case ParameterKind::Conjugate: - if (parameter.right) { - throw std::runtime_error( - "Qiskit unary parameter expression has invalid operands"); - } - validateParameterImpl(requireLeft(), localParameters, freeParameters, + } + if (const auto* binary = parameter.getBinary()) { + validateParameterImpl(*binary->left, localParameters, freeParameters, + depth + 1U, nodes); + validateParameterImpl(*binary->right, localParameters, freeParameters, depth + 1U, nodes); return; } @@ -227,89 +189,79 @@ parameterValueImpl(mlir::qc::QCProgramBuilder& builder, if (++nodes > MAX_PARAMETER_EXPRESSION_NODES) { throwImportedParameterExpressionSizeError(); } - const auto requireLeft = [&]() -> const Parameter& { - if (!parameter.left) { - throw std::runtime_error( - "Qiskit parameter expression has a missing operand"); - } - return *parameter.left; - }; - const auto requireRight = [&]() -> const Parameter& { - if (!parameter.right) { - throw std::runtime_error( - "Qiskit parameter expression has a missing operand"); - } - return *parameter.right; - }; - switch (parameter.kind) { - case ParameterKind::Number: - if (!std::isfinite(parameter.number)) { + if (const auto* number = parameter.getNumber()) { + if (!std::isfinite(number->value)) { throw std::runtime_error("Qiskit returned a non-finite parameter"); } - return parameter.number; - case ParameterKind::Symbol: - if (const auto local = localParameters.find(parameter.text); + 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(parameter.text); + if (const auto global = globalParameters.find(symbol->name); global != globalParameters.end()) { return global->second; } - throw std::runtime_error("Qiskit parameter symbol '" + parameter.text + + throw std::runtime_error("Qiskit parameter symbol '" + symbol->name + "' is not defined in this circuit scope"); - case ParameterKind::Conjugate: - // QC scalar parameters are real-valued, so conjugation is the identity. - return parameterValueImpl(builder, requireLeft(), localParameters, - globalParameters, depth + 1U, nodes); - default: - break; } - const auto left = materializeParameterValue( - builder, parameterValueImpl(builder, requireLeft(), localParameters, - globalParameters, depth + 1U, nodes)); - switch (parameter.kind) { - case ParameterKind::Negate: - return mlir::arith::NegFOp::create(builder, left).getResult(); - case ParameterKind::Sin: - return mlir::math::SinOp::create(builder, left).getResult(); - case ParameterKind::Cos: - return mlir::math::CosOp::create(builder, left).getResult(); - case ParameterKind::Tan: - return mlir::math::TanOp::create(builder, left).getResult(); - case ParameterKind::ArcSin: - return mlir::math::AsinOp::create(builder, left).getResult(); - case ParameterKind::ArcCos: - return mlir::math::AcosOp::create(builder, left).getResult(); - case ParameterKind::ArcTan: - return mlir::math::AtanOp::create(builder, left).getResult(); - case ParameterKind::Exp: - return mlir::math::ExpOp::create(builder, left).getResult(); - case ParameterKind::Log: - return mlir::math::LogOp::create(builder, left).getResult(); - case ParameterKind::Abs: - return mlir::math::AbsFOp::create(builder, left).getResult(); - default: - break; + 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; + } } - const auto right = materializeParameterValue( - builder, parameterValueImpl(builder, requireRight(), localParameters, - globalParameters, depth + 1U, nodes)); - switch (parameter.kind) { - case ParameterKind::Add: - return mlir::arith::AddFOp::create(builder, left, right).getResult(); - case ParameterKind::Subtract: - return mlir::arith::SubFOp::create(builder, left, right).getResult(); - case ParameterKind::Multiply: - return mlir::arith::MulFOp::create(builder, left, right).getResult(); - case ParameterKind::Divide: - return mlir::arith::DivFOp::create(builder, left, right).getResult(); - case ParameterKind::Power: - return mlir::math::PowFOp::create(builder, left, right).getResult(); - default: - break; + 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"); } @@ -1099,7 +1051,12 @@ void translateControlFlow(mlir::qc::QCProgramBuilder& builder, auto parameters = localParameters; if (loop.parameter) { requireExactLoopParameter(value); - parameters[loop.parameter->text] = + 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); @@ -1114,8 +1071,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->text] = - 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); }); @@ -1555,16 +1515,16 @@ void validateControlFlow(const ControlFlowReader& controlFlow, } } if (loop.parameter) { - if (loop.parameter->kind != ParameterKind::Symbol || - loop.parameter->text.empty()) { + 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(loop.parameter->text).second) { + if (!parameterNames.insert(symbol->name).second) { throw std::runtime_error( "Qiskit circuit contains distinct parameters with the same name"); } - localParameters[loop.parameter->text] = *loop.parameter; + localParameters[symbol->name] = *loop.parameter; } break; } @@ -1762,15 +1722,16 @@ mlir::QCProgram importCircuit(const nb::handle circuit) { ValidationParameters freeParameterSymbols; llvm::StringSet<> parameterNames; for (const auto& parameter : freeParameters) { - if (parameter.kind != ParameterKind::Symbol || parameter.text.empty()) { + 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(parameter.text).second) { + if (!parameterNames.insert(symbol->name).second) { throw std::runtime_error( "Qiskit circuit contains distinct parameters with the same name"); } - freeParameterSymbols.try_emplace(parameter.text, parameter); + freeParameterSymbols.try_emplace(symbol->name, parameter); } ExpansionCountState expansion; @@ -1804,10 +1765,15 @@ mlir::QCProgram importCircuit(const nb::handle circuit) { 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(parameter.text))}; + 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. @@ -1819,7 +1785,7 @@ mlir::QCProgram importCircuit(const nb::handle circuit) { throw std::runtime_error( "failed to create a compiler input for a Qiskit parameter"); } - globalParameters[parameter.text] = function.getArgument(index); + globalParameters[symbol->name] = function.getArgument(index); } llvm::SmallVector qubits; diff --git a/bindings/mlir/qiskit/QiskitTranslation.h b/bindings/mlir/qiskit/QiskitTranslation.h index 64974136c3..af57c46c42 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 { @@ -51,14 +53,7 @@ validateRegisterLayout(const std::vector& registers, uint32_t total, inline constexpr size_t MAX_PARAMETER_EXPRESSION_DEPTH = 64U; inline constexpr size_t MAX_PARAMETER_EXPRESSION_NODES = 4096U; -enum class ParameterKind : uint8_t { - Number, - Symbol, - Add, - Subtract, - Multiply, - Divide, - Power, +enum class UnaryParameterKind : uint8_t { Negate, Sin, Cos, @@ -72,13 +67,81 @@ enum class ParameterKind : uint8_t { Conjugate, }; +enum class BinaryParameterKind : uint8_t { + Add, + Subtract, + Multiply, + Divide, + Power, +}; + /** One normalized scalar parameter-expression tree. */ -struct Parameter { - ParameterKind kind = ParameterKind::Number; - double number = 0.0; - std::string text; - std::shared_ptr left; - std::shared_ptr right; +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, std::make_shared(std::move(operand))}); + } + + [[nodiscard]] static Parameter binary(const BinaryParameterKind operation, + Parameter left, Parameter right) { + return Parameter( + Binary{operation, std::make_shared(std::move(left)), + 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 { From d1649b70b76e9b70c30c48b683e4b92ee36da25b Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Thu, 20 Aug 2026 11:59:31 +0000 Subject: [PATCH 09/17] =?UTF-8?q?=E2=9C=85=20Verify=20finite=20unitary=20p?= =?UTF-8?q?arameters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the static finiteness contract into the shared QC and QCO unitary interface verifiers. Remove redundant Qiskit boundary checks while preserving source preflight and global-phase overflow protection. Assisted-by: Codex --- .agent/plans/qiskit-symbolic-parameters.md | 28 ++++++++---- bindings/mlir/qiskit/QiskitExport.cpp | 12 +----- bindings/mlir/qiskit/QiskitImport.cpp | 3 -- .../include/mlir/Dialect/QC/IR/QCInterfaces.h | 6 +++ .../mlir/Dialect/QC/IR/QCInterfaces.td | 8 ++++ .../mlir/Dialect/QCO/IR/QCOInterfaces.h | 6 +++ .../mlir/Dialect/QCO/IR/QCOInterfaces.td | 8 ++++ mlir/include/mlir/Dialect/Utils/Utils.h | 33 ++++++++++++++ .../IR/Operations/StandardGates/GPhaseOp.cpp | 9 ++-- mlir/lib/Dialect/QC/IR/QCOps.cpp | 5 +++ .../IR/Operations/StandardGates/GPhaseOp.cpp | 9 ++-- mlir/lib/Dialect/QCO/IR/QCOOps.cpp | 5 +++ mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp | 43 +++++++++++++++++++ mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp | 42 ++++++++++++++++++ .../Utils/test_global_phase_normalization.cpp | 29 ------------- 15 files changed, 185 insertions(+), 61 deletions(-) diff --git a/.agent/plans/qiskit-symbolic-parameters.md b/.agent/plans/qiskit-symbolic-parameters.md index f159e13c90..55bd19ce8a 100644 --- a/.agent/plans/qiskit-symbolic-parameters.md +++ b/.agent/plans/qiskit-symbolic-parameters.md @@ -74,8 +74,9 @@ partially constructed output circuit after a failure. Exact 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. -- [ ] Reject non-finite constant gate parameters in the shared QC and QCO gate - verifier rather than in individual import/export paths. +- [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. - [ ] Run focused dialect, conversion, compiler, Qiskit, documentation, stub, and lint validation; inspect every signed commit and the final diff. @@ -119,6 +120,10 @@ partially constructed output circuit after a failure. Exact 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. ## Decision Log @@ -173,6 +178,13 @@ partially constructed output circuit after a failure. Exact 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 @@ -225,12 +237,12 @@ 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, finite 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 +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. diff --git a/bindings/mlir/qiskit/QiskitExport.cpp b/bindings/mlir/qiskit/QiskitExport.cpp index eb3ea7ad8c..cb42da3d72 100644 --- a/bindings/mlir/qiskit/QiskitExport.cpp +++ b/bindings/mlir/qiskit/QiskitExport.cpp @@ -118,9 +118,6 @@ using ExportedParameters = llvm::DenseMap; 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; @@ -212,10 +209,7 @@ void validateExportParameterImpl(const Parameter& parameter, const size_t depth, if (++nodes > MAX_PARAMETER_EXPRESSION_NODES) { throwExportedParameterExpressionSizeError(); } - if (const auto* number = parameter.getNumber()) { - if (!std::isfinite(number->value)) { - throw std::runtime_error("cannot export a non-finite QC parameter"); - } + if (parameter.getNumber() != nullptr) { return; } if (const auto* symbol = parameter.getSymbol()) { @@ -373,10 +367,6 @@ void collectParameters(mlir::func::FuncOp function, ExportState& state) { void addGlobalPhase(ExportState& state, const Parameter& phase) { if (const auto* number = phase.getNumber()) { - if (!std::isfinite(number->value)) { - throw std::runtime_error( - "QC global phase cannot be represented by Qiskit"); - } if (const auto* globalNumber = state.globalPhase.getNumber()) { const auto sum = globalNumber->value + number->value; if (!std::isfinite(sum)) { diff --git a/bindings/mlir/qiskit/QiskitImport.cpp b/bindings/mlir/qiskit/QiskitImport.cpp index e2b37d6f0b..68a9ccb7a0 100644 --- a/bindings/mlir/qiskit/QiskitImport.cpp +++ b/bindings/mlir/qiskit/QiskitImport.cpp @@ -190,9 +190,6 @@ parameterValueImpl(mlir::qc::QCProgramBuilder& builder, throwImportedParameterExpressionSizeError(); } if (const auto* number = parameter.getNumber()) { - if (!std::isfinite(number->value)) { - throw std::runtime_error("Qiskit returned a non-finite parameter"); - } return number->value; } if (const auto* symbol = parameter.getSymbol()) { 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/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/Utils/Utils.h b/mlir/include/mlir/Dialect/Utils/Utils.h index 1a1e8e6702..144a0e36d2 100644 --- a/mlir/include/mlir/Dialect/Utils/Utils.h +++ b/mlir/include/mlir/Dialect/Utils/Utils.h @@ -11,6 +11,7 @@ #pragma once #include +#include #include #include #include @@ -30,6 +31,7 @@ #include #include #include +#include #include #include @@ -225,6 +227,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) { + llvm::DenseMap> constantCache; + llvm::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. * diff --git a/mlir/lib/Dialect/QC/IR/Operations/StandardGates/GPhaseOp.cpp b/mlir/lib/Dialect/QC/IR/Operations/StandardGates/GPhaseOp.cpp index 931b002a47..111759e9a0 100644 --- a/mlir/lib/Dialect/QC/IR/Operations/StandardGates/GPhaseOp.cpp +++ b/mlir/lib/Dialect/QC/IR/Operations/StandardGates/GPhaseOp.cpp @@ -28,11 +28,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/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/unittests/Dialect/QC/IR/test_qc_ir.cpp b/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp index 72f1874f5e..b8c8372820 100644 --- a/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp +++ b/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include @@ -292,6 +293,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()))); diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp index 5d4751f8da..8bc4a67c8b 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp @@ -333,6 +333,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 }; 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 { From 9ec198dccb77975cea50b2b098fec4e3fec87366 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Thu, 20 Aug 2026 12:13:50 +0000 Subject: [PATCH 10/17] =?UTF-8?q?=F0=9F=93=9D=20Complete=20MQT=20metadata?= =?UTF-8?q?=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the operation-free MQT dialect visible to the MLIR documentation generator through OpBase.td. Normalize the new-file license headers and record the final validation results. Assisted-by: Codex --- .agent/plans/qiskit-symbolic-parameters.md | 24 +++++++++++++------ mlir/include/mlir/Dialect/MQT/CMakeLists.txt | 4 ++-- .../mlir/Dialect/MQT/IR/CMakeLists.txt | 4 ++-- .../include/mlir/Dialect/MQT/IR/MQTDialect.td | 6 ++--- mlir/lib/Dialect/MQT/CMakeLists.txt | 4 ++-- mlir/lib/Dialect/MQT/IR/CMakeLists.txt | 4 ++-- mlir/lib/Dialect/MQT/IR/MQTDialect.cpp | 4 ++-- mlir/unittests/Dialect/MQT/CMakeLists.txt | 4 ++-- mlir/unittests/Dialect/MQT/IR/CMakeLists.txt | 4 ++-- mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp | 4 ++-- 10 files changed, 36 insertions(+), 26 deletions(-) diff --git a/.agent/plans/qiskit-symbolic-parameters.md b/.agent/plans/qiskit-symbolic-parameters.md index 55bd19ce8a..128aa8251c 100644 --- a/.agent/plans/qiskit-symbolic-parameters.md +++ b/.agent/plans/qiskit-symbolic-parameters.md @@ -77,8 +77,9 @@ partially constructed output circuit after a failure. Exact - [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. -- [ ] Run focused dialect, conversion, compiler, Qiskit, documentation, stub, - and lint validation; inspect every signed commit and the final diff. +- [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. ## Surprises & Discoveries @@ -124,6 +125,9 @@ partially constructed output circuit after a failure. Exact 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. ## Decision Log @@ -188,11 +192,17 @@ partially constructed output circuit after a failure. Exact ## Outcomes & Retrospective -The scalar implementation is complete. Shared direct symbols, bounded real -expression trees, parameterized definitions, name-safe loop bindings, and global -phase passed the original focused validation. Unused named inputs now fail -before writer allocation rather than disappearing. The split branch builds and -passes all 158 Qiskit translation tests after #2158 merged. +The scalar implementation is complete. The shared MQT metadata dialect owns +input and qubit-register names, and 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,128 configured tests pass; one QDMI test is +skipped by its environment guard. All 157 Qiskit translation tests pass after a +fresh extension build. MLIR and Sphinx documentation, stub generation, and the +repository lint suite also pass. ## Context and Orientation diff --git a/mlir/include/mlir/Dialect/MQT/CMakeLists.txt b/mlir/include/mlir/Dialect/MQT/CMakeLists.txt index b6ae6efd5a..b181a84fed 100644 --- a/mlir/include/mlir/Dialect/MQT/CMakeLists.txt +++ b/mlir/include/mlir/Dialect/MQT/CMakeLists.txt @@ -1,5 +1,5 @@ -# Copyright (c) 2026 Chair for Design Automation, TUM -# Copyright (c) 2026 Munich Quantum Software Company GmbH +# 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 diff --git a/mlir/include/mlir/Dialect/MQT/IR/CMakeLists.txt b/mlir/include/mlir/Dialect/MQT/IR/CMakeLists.txt index c73acbfbed..d2b3e87d42 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/CMakeLists.txt +++ b/mlir/include/mlir/Dialect/MQT/IR/CMakeLists.txt @@ -1,5 +1,5 @@ -# Copyright (c) 2026 Chair for Design Automation, TUM -# Copyright (c) 2026 Munich Quantum Software Company GmbH +# 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 diff --git a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td index ef56c5685e..958e6b1c3b 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td @@ -1,5 +1,5 @@ -// Copyright (c) 2026 Chair for Design Automation, TUM -// Copyright (c) 2026 Munich Quantum Software Company GmbH +// 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 @@ -9,7 +9,7 @@ #ifndef MLIR_DIALECT_MQT_IR_MQTDIALECT_TD #define MLIR_DIALECT_MQT_IR_MQTDIALECT_TD -include "mlir/IR/DialectBase.td" +include "mlir/IR/OpBase.td" def MQTDialect : Dialect { let name = "mqt"; diff --git a/mlir/lib/Dialect/MQT/CMakeLists.txt b/mlir/lib/Dialect/MQT/CMakeLists.txt index b6ae6efd5a..b181a84fed 100644 --- a/mlir/lib/Dialect/MQT/CMakeLists.txt +++ b/mlir/lib/Dialect/MQT/CMakeLists.txt @@ -1,5 +1,5 @@ -# Copyright (c) 2026 Chair for Design Automation, TUM -# Copyright (c) 2026 Munich Quantum Software Company GmbH +# 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 diff --git a/mlir/lib/Dialect/MQT/IR/CMakeLists.txt b/mlir/lib/Dialect/MQT/IR/CMakeLists.txt index c3b19ebfbb..f4be1eb7e9 100644 --- a/mlir/lib/Dialect/MQT/IR/CMakeLists.txt +++ b/mlir/lib/Dialect/MQT/IR/CMakeLists.txt @@ -1,5 +1,5 @@ -# Copyright (c) 2026 Chair for Design Automation, TUM -# Copyright (c) 2026 Munich Quantum Software Company GmbH +# 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 diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index cbf091ce7b..ee63bf2964 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -1,6 +1,6 @@ /* - * Copyright (c) 2026 Chair for Design Automation, TUM - * Copyright (c) 2026 Munich Quantum Software Company GmbH + * 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 diff --git a/mlir/unittests/Dialect/MQT/CMakeLists.txt b/mlir/unittests/Dialect/MQT/CMakeLists.txt index b6ae6efd5a..b181a84fed 100644 --- a/mlir/unittests/Dialect/MQT/CMakeLists.txt +++ b/mlir/unittests/Dialect/MQT/CMakeLists.txt @@ -1,5 +1,5 @@ -# Copyright (c) 2026 Chair for Design Automation, TUM -# Copyright (c) 2026 Munich Quantum Software Company GmbH +# 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 diff --git a/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt b/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt index 57ec7a2c77..382c246f38 100644 --- a/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt +++ b/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt @@ -1,5 +1,5 @@ -# Copyright (c) 2026 Chair for Design Automation, TUM -# Copyright (c) 2026 Munich Quantum Software Company GmbH +# 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 diff --git a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp index 241aee8e87..98a70d8711 100644 --- a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp +++ b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp @@ -1,6 +1,6 @@ /* - * Copyright (c) 2026 Chair for Design Automation, TUM - * Copyright (c) 2026 Munich Quantum Software Company GmbH + * 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 From 135ab32a3f1477fdcd85b95cfac308ebe665f73c Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Thu, 20 Aug 2026 12:52:20 +0000 Subject: [PATCH 11/17] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Unify=20quantum=20an?= =?UTF-8?q?d=20classical=20register=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the CBit source-name field and the qubit-specific metadata with one mqt.register_name contract. Verify one function-wide namespace across inputs and registers, preserve register names through CBit lowering, and reject Qiskit name collisions before IR construction. Assisted-by: Codex --- .agent/plans/qiskit-symbolic-parameters.md | 23 +++++--- bindings/mlir/qiskit/QiskitExport.cpp | 6 +-- bindings/mlir/qiskit/QiskitImport.cpp | 7 +++ mlir/include/mlir/Dialect/CBit/IR/CBitOps.td | 14 +++-- .../include/mlir/Dialect/MQT/IR/MQTDialect.td | 7 +-- .../Dialect/QC/Builder/QCProgramBuilder.h | 7 +-- .../Dialect/QCO/Builder/QCOProgramBuilder.h | 7 +-- .../Conversion/CBitToMemRef/CBitToMemRef.cpp | 1 + mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp | 4 +- .../QCToQIR/QIRCommon/CMakeLists.txt | 1 + .../QCToQIR/QIRCommon/QIRCommon.cpp | 4 +- mlir/lib/Dialect/MQT/IR/CMakeLists.txt | 1 + mlir/lib/Dialect/MQT/IR/MQTDialect.cpp | 44 ++++++++++++---- .../Dialect/QC/Builder/QCProgramBuilder.cpp | 16 +++--- .../QC/Translation/TranslateQCToOpenQASM3.cpp | 8 +-- .../Dialect/QCO/Builder/QCOProgramBuilder.cpp | 16 +++--- .../Conversion/CBitToMemRef/CMakeLists.txt | 1 + .../CBitToMemRef/test_cbit_to_memref.cpp | 17 ++++-- .../Conversion/QCOToQC/test_qco_to_qc.cpp | 6 +-- .../QCQCORoundTrip/test_qc_qco_round_trip.cpp | 22 +++++--- .../Conversion/QCToQCO/test_qc_to_qco.cpp | 8 +-- mlir/unittests/Dialect/CBit/IR/CMakeLists.txt | 1 + .../Dialect/CBit/IR/test_cbit_ir.cpp | 12 +++-- mlir/unittests/Dialect/MQT/IR/CMakeLists.txt | 1 + mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp | 52 ++++++++++++++----- mlir/unittests/Dialect/QC/IR/CMakeLists.txt | 11 +++- mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp | 24 ++++----- .../QC/Transforms/test_qc_transforms.cpp | 4 +- .../Translation/test_openqasm3_emission.cpp | 8 +-- .../QC/Translation/test_qasm3_translation.cpp | 5 +- mlir/unittests/Dialect/QCO/IR/CMakeLists.txt | 1 + mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp | 24 ++++----- .../Transforms/test_qtensor_transforms.cpp | 4 +- test/python/test_mlir_qiskit_translation.py | 36 +++++++++++-- 34 files changed, 261 insertions(+), 142 deletions(-) diff --git a/.agent/plans/qiskit-symbolic-parameters.md b/.agent/plans/qiskit-symbolic-parameters.md index 128aa8251c..b8631a367b 100644 --- a/.agent/plans/qiskit-symbolic-parameters.md +++ b/.agent/plans/qiskit-symbolic-parameters.md @@ -80,6 +80,9 @@ partially constructed output circuit after a failure. Exact - [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. ## Surprises & Discoveries @@ -128,6 +131,9 @@ partially constructed output circuit after a failure. Exact - 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. ## Decision Log @@ -163,12 +169,17 @@ partially constructed output circuit after a failure. Exact 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.qubit_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: 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: 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 diff --git a/bindings/mlir/qiskit/QiskitExport.cpp b/bindings/mlir/qiskit/QiskitExport.cpp index cb42da3d72..f4c02e0c11 100644 --- a/bindings/mlir/qiskit/QiskitExport.cpp +++ b/bindings/mlir/qiskit/QiskitExport.cpp @@ -699,8 +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::mqt::MQTDialect::QubitRegisterNameAttrHelper:: - getNameStr())) { + mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr())) { Register reg{.name = name.str()}; reg.bits.resize(size); std::iota(reg.bits.begin(), reg.bits.end(), state.numQubits); @@ -764,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); diff --git a/bindings/mlir/qiskit/QiskitImport.cpp b/bindings/mlir/qiskit/QiskitImport.cpp index 68a9ccb7a0..a65652974a 100644 --- a/bindings/mlir/qiskit/QiskitImport.cpp +++ b/bindings/mlir/qiskit/QiskitImport.cpp @@ -1737,6 +1737,13 @@ mlir::QCProgram importCircuit(const nb::handle circuit) { 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( 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/MQT/IR/MQTDialect.td b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td index 958e6b1c3b..b4308499b6 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td @@ -21,12 +21,13 @@ def MQTDialect : Dialect { across quantum dialect conversions. It defines no operations or types. `mqt.input_name` records the source-level name of a function input. - `mqt.qubit_register_name` records the source-level name of a rank-one qubit - register allocation. + `mqt.register_name` records the source-level name of a quantum or classical + register allocation. Input and register names share one function-wide + namespace. }]; let discardableAttrs = (ins "::mlir::StringAttr":$input_name, - "::mlir::StringAttr":$qubit_register_name); + "::mlir::StringAttr":$register_name); let hasOperationAttrVerify = 1; let hasRegionArgAttrVerify = 1; diff --git a/mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h b/mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h index 0cc189a502..7724d27350 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 @@ -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/QCO/Builder/QCOProgramBuilder.h b/mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h index 6f087b86ba..d8c0acef0f 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 @@ -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/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/JeffToQCO.cpp b/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp index a24b62e9eb..450ed6c08f 100644 --- a/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp +++ b/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp @@ -336,8 +336,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(); } }; 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..557f619b03 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.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/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/IR/QCOps.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" @@ -467,7 +468,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( + ::mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr())) { reg.label = name.str(); } const auto size = allocOp.getResult().getType().getWidth(); diff --git a/mlir/lib/Dialect/MQT/IR/CMakeLists.txt b/mlir/lib/Dialect/MQT/IR/CMakeLists.txt index f4be1eb7e9..871f7e2283 100644 --- a/mlir/lib/Dialect/MQT/IR/CMakeLists.txt +++ b/mlir/lib/Dialect/MQT/IR/CMakeLists.txt @@ -15,6 +15,7 @@ add_mlir_dialect_library( MLIRMQTDialectIncGen LINK_LIBS PRIVATE + MLIRCBitDialect MLIRFuncDialect MLIRIR MLIRMemRefDialect diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index ee63bf2964..db26ca6dcf 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -10,6 +10,7 @@ #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" @@ -52,10 +53,14 @@ namespace { return success(); } -[[nodiscard]] bool isQubitRegisterAllocation(Operation* operation) { +[[nodiscard]] 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()); + return type.getRank() == 1 && (isa(type.getElementType()) || + type.getElementType().isInteger(1)); } if (auto alloc = dyn_cast(operation)) { const auto type = cast(alloc.getType()); @@ -64,15 +69,15 @@ namespace { return false; } -[[nodiscard]] LogicalResult -verifyQubitRegisterName(Operation* operation, const NamedAttribute attribute) { +[[nodiscard]] LogicalResult verifyRegisterName(Operation* operation, + const NamedAttribute attribute) { if (failed(verifyName(operation, attribute))) { return failure(); } - if (!isQubitRegisterAllocation(operation)) { + if (!isRegisterAllocation(operation)) { return operation->emitError() << "attribute '" << attribute.getName().getValue() - << "' requires a rank-one qubit register allocation"; + << "' requires a rank-one quantum or classical register allocation"; } auto function = operation->getParentOfType(); @@ -84,13 +89,21 @@ verifyQubitRegisterName(Operation* operation, const NamedAttribute attribute) { } 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(attribute.getName()) == name) { + if (candidate.getAttrOfType( + MQTDialect::RegisterNameAttrHelper::getNameStr()) == name) { return operation->emitError() - << "duplicate qubit register name '" << name.getValue() << "'"; + << "duplicate program name '" << name.getValue() << "'"; } } return success(); @@ -100,8 +113,8 @@ verifyQubitRegisterName(Operation* operation, const NamedAttribute attribute) { LogicalResult MQTDialect::verifyOperationAttribute(Operation* operation, const NamedAttribute attribute) { - if (attribute.getName() == QubitRegisterNameAttrHelper::getNameStr()) { - return verifyQubitRegisterName(operation, attribute); + if (attribute.getName() == RegisterNameAttrHelper::getNameStr()) { + return verifyRegisterName(operation, attribute); } if (attribute.getName() == InputNameAttrHelper::getNameStr()) { return operation->emitError() @@ -139,7 +152,16 @@ LogicalResult MQTDialect::verifyRegionArgAttribute( if (function.getArgAttrOfType(index, attribute.getName()) == name) { return operation->emitError() - << "duplicate input name '" << name.getValue() << "'"; + << "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(); diff --git a/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp b/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp index b4f03a1a63..1dbd18a06a 100644 --- a/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp +++ b/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp @@ -141,15 +141,11 @@ 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()) { ctx->getLoadedDialect() - ->getQubitRegisterNameAttrHelper() + ->getRegisterNameAttrHelper() .setAttr(alloc, getStringAttr(name)); } auto memref = alloc.getResult(); @@ -172,9 +168,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/Translation/TranslateQCToOpenQASM3.cpp b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp index 75f4f40c61..47a3d12292 100644 --- a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp +++ b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp @@ -297,9 +297,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), @@ -325,7 +325,7 @@ class OpenQASMEmitter { } StringRef requested; if (const auto attr = alloc->getAttrOfType( - mqt::MQTDialect::QubitRegisterNameAttrHelper::getNameStr())) { + mqt::MQTDialect::RegisterNameAttrHelper::getNameStr())) { requested = attr.getValue(); } Resource resource{.kind = ResourceKind::Qubit, diff --git a/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp b/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp index c05789614b..5004bbd277 100644 --- a/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp +++ b/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp @@ -145,14 +145,10 @@ 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()) { ctx->getLoadedDialect() - ->getQubitRegisterNameAttrHelper() + ->getRegisterNameAttrHelper() .setAttr(qtensor.getDefiningOp(), getStringAttr(name)); } @@ -177,9 +173,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/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/QCOToQC/test_qco_to_qc.cpp b/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp index 6155f06d15..f1af599f66 100644 --- a/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp +++ b/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp @@ -109,7 +109,7 @@ TEST(QCOToQCRegressionTest, RetainsQubitRegisterName) { }); ASSERT_TRUE(allocation); const auto name = allocation->getAttrOfType( - mlir::mqt::MQTDialect::QubitRegisterNameAttrHelper::getNameStr()); + mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()); ASSERT_TRUE(name); EXPECT_EQ(name.getValue(), "named_qubits"); } @@ -125,7 +125,7 @@ TEST(QCOToQCRegressionTest, RetainsDynamicQubitRegisterName) { 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 + %reg = qtensor.alloc(%size) {mqt.register_name = "named_qubits"} : tensor qtensor.dealloc %reg : tensor return } @@ -144,7 +144,7 @@ module { EXPECT_EQ(allocation.getDynamicSizes().front(), allocation->getBlock()->getArgument(0)); const auto name = allocation->getAttrOfType( - mlir::mqt::MQTDialect::QubitRegisterNameAttrHelper::getNameStr()); + mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()); ASSERT_TRUE(name); EXPECT_EQ(name.getValue(), "named_qubits"); } 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 b6787c32e5..81f193626d 100644 --- a/mlir/unittests/Conversion/QCQCORoundTrip/test_qc_qco_round_trip.cpp +++ b/mlir/unittests/Conversion/QCQCORoundTrip/test_qc_qco_round_trip.cpp @@ -79,7 +79,7 @@ TEST_F(QCQCORoundTripTest, PreservesSharedMQTMetadata) { module { func.func @main(%theta: f64 {mqt.input_name = "theta"}) attributes {passthrough = ["entry_point"]} { - %reg = memref.alloc() {mqt.qubit_register_name = "q"} + %reg = memref.alloc() {mqt.register_name = "q"} : memref<2x!qc.qubit> memref.dealloc %reg : memref<2x!qc.qubit> return @@ -103,7 +103,7 @@ module { moduleOp->walk([&](memref::AllocOp op) { allocation = op; }); ASSERT_TRUE(allocation); const auto registerName = allocation->getAttrOfType( - mlir::mqt::MQTDialect::QubitRegisterNameAttrHelper::getNameStr()); + mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()); ASSERT_TRUE(registerName); EXPECT_EQ(registerName.getValue(), "q"); } @@ -114,8 +114,8 @@ module { func.func @main() -> (!cbit.reg<2>, !cbit.reg<1>) attributes {passthrough = ["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> @@ -142,10 +142,20 @@ 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( + ::mlir::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( + ::mlir::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()); diff --git a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp index 80918636e1..d28e509419 100644 --- a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp +++ b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp @@ -605,7 +605,7 @@ TEST_F(QCToQCORegressionTest, RetainsQubitRegisterName) { moduleOp->walk([&](qtensor::AllocOp op) { allocation = op; }); ASSERT_TRUE(allocation); const auto name = allocation->getAttrOfType( - mlir::mqt::MQTDialect::QubitRegisterNameAttrHelper::getNameStr()); + mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()); ASSERT_TRUE(name); EXPECT_EQ(name.getValue(), "named_qubits"); } @@ -614,7 +614,7 @@ 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 + %reg = memref.alloc(%size) {mqt.register_name = "named_qubits"} : memref memref.dealloc %reg : memref return } @@ -631,7 +631,7 @@ module { EXPECT_TRUE(allocation.getResult().getType().isDynamicDim(0)); EXPECT_EQ(allocation.getSize(), allocation->getBlock()->getArgument(0)); const auto name = allocation->getAttrOfType( - mlir::mqt::MQTDialect::QubitRegisterNameAttrHelper::getNameStr()); + mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()); ASSERT_TRUE(name); EXPECT_EQ(name.getValue(), "named_qubits"); } @@ -1109,7 +1109,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, 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/MQT/IR/CMakeLists.txt b/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt index 382c246f38..cf0a00d560 100644 --- a/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt +++ b/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt @@ -12,6 +12,7 @@ target_link_libraries( ${mqt_ir_target} PRIVATE GTest::gtest_main MLIRArithDialect + MLIRCBitDialect MLIRFuncDialect MLIRMemRefDialect MLIRMQTDialect diff --git a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp index 98a70d8711..f5cc5aba92 100644 --- a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp +++ b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp @@ -13,6 +13,7 @@ * @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" @@ -40,7 +41,7 @@ class MQTIRTest : public ::testing::Test { void SetUp() override { DialectRegistry registry; - registry.insert(); context = std::make_unique(registry); @@ -52,20 +53,29 @@ class MQTIRTest : public ::testing::Test { } }; -TEST_F(MQTIRTest, AcceptsProgramInputAndQubitRegisterNames) { +TEST_F(MQTIRTest, AcceptsProgramInputAndRegisterNames) { EXPECT_TRUE(parse(R"mlir( module { func.func @qc(%theta: f64 {mqt.input_name = "theta"}) { - %reg = memref.alloc() {mqt.qubit_register_name = "q"} + %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.qubit_register_name = "r"} + %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")); } @@ -111,33 +121,51 @@ TEST_F(MQTIRTest, RejectsInputNameOnOperation) { )mlir")); } -TEST_F(MQTIRTest, RejectsInvalidQubitRegisterOwners) { +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.qubit_register_name = "bits"} - : memref<2xi1> + %reg = memref.alloc() {mqt.register_name = "values"} + : memref<2xf64> return } } )mlir")); EXPECT_FALSE(parse(R"mlir( module { - func.func @main(%arg: f64 {mqt.qubit_register_name = "q"}) { + func.func @main(%arg: f64 {mqt.register_name = "q"}) { return } } )mlir")); } -TEST_F(MQTIRTest, RejectsDuplicateQubitRegisterNames) { +TEST_F(MQTIRTest, RejectsDuplicateProgramNames) { EXPECT_FALSE(parse(R"mlir( module { func.func @main() { - %lhs = memref.alloc() {mqt.qubit_register_name = "q"} + %lhs = memref.alloc() {mqt.register_name = "state"} : memref<1x!qc.qubit> - %rhs = memref.alloc() {mqt.qubit_register_name = "q"} - : memref<2x!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 } } 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 b8c8372820..bed8f6d466 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" @@ -150,17 +151,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( { @@ -216,10 +206,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) { @@ -596,7 +592,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/test_qc_transforms.cpp b/mlir/unittests/Dialect/QC/Transforms/test_qc_transforms.cpp index c777781565..28a6c80e70 100644 --- a/mlir/unittests/Dialect/QC/Transforms/test_qc_transforms.cpp +++ b/mlir/unittests/Dialect/QC/Transforms/test_qc_transforms.cpp @@ -46,7 +46,7 @@ TEST(QCTransformsTest, ShrinkQubitRegistersPreservesMetadata) { module { func.func @main() { %c1 = arith.constant 1 : index - %reg = memref.alloc() {mqt.qubit_register_name = "q"} + %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 @@ -68,7 +68,7 @@ TEST(QCTransformsTest, ShrinkQubitRegistersPreservesMetadata) { ASSERT_TRUE(allocation); EXPECT_EQ(allocation.getType().getShape(), ArrayRef{1}); EXPECT_EQ(allocation->getAttrOfType( - mqt::MQTDialect::QubitRegisterNameAttrHelper::getNameStr()), + 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..9986bcebba 100644 --- a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp +++ b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp @@ -88,7 +88,7 @@ TEST(OpenQASM3EmissionTest, PreservesMeasurementOrderBeforeDelayedStore) { attributes {passthrough = ["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> } @@ -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 5c7581ee77..d6af89d426 100644 --- a/mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp +++ b/mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp @@ -1007,7 +1007,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"); } @@ -1059,7 +1060,7 @@ qubit[2] named_qubits; }); ASSERT_TRUE(qubitRegister); const auto name = qubitRegister->getAttrOfType( - mlir::mqt::MQTDialect::QubitRegisterNameAttrHelper::getNameStr()); + 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 8bc4a67c8b..31fc1ef0a6 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" @@ -237,17 +238,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 +294,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) { @@ -464,7 +460,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, diff --git a/mlir/unittests/Dialect/QTensor/Transforms/test_qtensor_transforms.cpp b/mlir/unittests/Dialect/QTensor/Transforms/test_qtensor_transforms.cpp index 2838d00308..4020e95d07 100644 --- a/mlir/unittests/Dialect/QTensor/Transforms/test_qtensor_transforms.cpp +++ b/mlir/unittests/Dialect/QTensor/Transforms/test_qtensor_transforms.cpp @@ -48,7 +48,7 @@ TEST(QTensorTransformsTest, ShrinkToFitPreservesMetadata) { func.func @main() { %c1 = arith.constant 1 : index %c3 = arith.constant 3 : index - %reg = qtensor.alloc(%c3) {mqt.qubit_register_name = "q"} + %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 @@ -73,7 +73,7 @@ TEST(QTensorTransformsTest, ShrinkToFitPreservesMetadata) { EXPECT_EQ(cast(allocation.getType()).getShape(), ArrayRef{1}); EXPECT_EQ(allocation->getAttrOfType( - mqt::MQTDialect::QubitRegisterNameAttrHelper::getNameStr()), + mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()), StringAttr::get(&context, "q")); } } // namespace diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index 2833a9a2ee..94a927c57b 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -445,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)] @@ -550,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"] @@ -598,8 +598,8 @@ def test_qiskit_export_excludes_internal_cbit_registers() -> None: """module { func.func @main() -> !cbit.reg<1> attributes {passthrough = ["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> } @@ -1416,6 +1416,32 @@ def test_duplicate_named_symbolic_inputs_are_invalid_qc_ir() -> None: ) +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 {passthrough = ["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") From d49ce2094aea6e2760ddac9e340d5d0a05267d32 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Thu, 20 Aug 2026 13:09:46 +0000 Subject: [PATCH 12/17] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Model=20program=20en?= =?UTF-8?q?try=20points=20in=20MQT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace high-level LLVM passthrough strings with the verified mqt.entry_point attribute. Preserve it across QC/QCO and jeff conversions, then lower it to QIR passthrough metadata at the LLVM boundary. Assisted-by: Codex --- .agent/plans/qiskit-symbolic-parameters.md | 52 +++++++++++++------ .../mlir/Conversion/JeffToQCO/JeffToQCO.td | 2 +- .../mlir/Conversion/QCOToJeff/QCOToJeff.td | 2 +- .../QCToQIR/QIRAdaptive/QCToQIRAdaptive.td | 4 +- .../Conversion/QCToQIR/QIRBase/QCToQIRBase.td | 4 +- mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h | 7 +++ .../include/mlir/Dialect/MQT/IR/MQTDialect.td | 4 +- .../Dialect/QC/Builder/QCProgramBuilder.h | 4 +- .../Dialect/QCO/Builder/QCOProgramBuilder.h | 4 +- .../include/mlir/Dialect/QIR/Utils/QIRUtils.h | 6 +-- mlir/include/mlir/Dialect/Utils/Utils.h | 32 ------------ mlir/lib/Conversion/JeffToQCO/CMakeLists.txt | 1 + mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp | 10 ++-- mlir/lib/Conversion/QCOToJeff/CMakeLists.txt | 1 + mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp | 20 +++---- .../QCToQIR/QIRAdaptive/CMakeLists.txt | 1 + .../QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp | 3 +- .../Conversion/QCToQIR/QIRBase/CMakeLists.txt | 1 + .../QCToQIR/QIRBase/QCToQIRBase.cpp | 3 +- .../QCToQIR/QIRCommon/QIRCommon.cpp | 11 +--- mlir/lib/Dialect/MQT/IR/MQTDialect.cpp | 40 ++++++++++++++ .../Dialect/QC/Builder/QCProgramBuilder.cpp | 7 ++- .../Dialect/QCO/Builder/QCOProgramBuilder.cpp | 7 ++- .../lib/Dialect/QCO/Transforms/CMakeLists.txt | 1 + .../QCO/Transforms/Mapping/Mapping.cpp | 5 +- .../QIR/Transforms/AttachQIRAttributes.cpp | 2 + .../lib/Dialect/QIR/Transforms/CMakeLists.txt | 1 + mlir/lib/Dialect/QIR/Utils/CMakeLists.txt | 1 + mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp | 6 ++- .../JeffRoundTrip/test_jeff_round_trip.cpp | 43 ++++++++------- .../Conversion/QCOToQC/test_qco_to_qc.cpp | 10 ++-- .../QCQCORoundTrip/test_qc_qco_round_trip.cpp | 12 +++-- .../Conversion/QCToQCO/test_qc_to_qco.cpp | 42 +++++++-------- mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp | 39 ++++++++++++++ .../Translation/test_openqasm3_emission.cpp | 6 +-- mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp | 4 +- .../QCO/Transforms/Mapping/CMakeLists.txt | 1 + .../QCO/Transforms/Mapping/test_mapping.cpp | 24 +++++---- .../Optimizations/test_qco_reuse_qubits.cpp | 4 +- test/python/test_mlir.py | 4 +- test/python/test_mlir_qiskit_translation.py | 38 +++++++------- 41 files changed, 277 insertions(+), 192 deletions(-) diff --git a/.agent/plans/qiskit-symbolic-parameters.md b/.agent/plans/qiskit-symbolic-parameters.md index b8631a367b..e252f6a45e 100644 --- a/.agent/plans/qiskit-symbolic-parameters.md +++ b/.agent/plans/qiskit-symbolic-parameters.md @@ -83,6 +83,15 @@ partially constructed output circuit after a failure. Exact - [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. ## Surprises & Discoveries @@ -134,6 +143,10 @@ partially constructed output circuit after a failure. Exact - 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 @@ -180,6 +193,12 @@ partially constructed output circuit after a failure. Exact 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: 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 @@ -204,16 +223,17 @@ partially constructed output circuit after a failure. Exact ## Outcomes & Retrospective The scalar implementation is complete. The shared MQT metadata dialect owns -input and qubit-register names, and 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,128 configured tests pass; one QDMI test is -skipped by its environment guard. All 157 Qiskit translation tests pass after a -fresh extension build. MLIR and Sphinx documentation, stub generation, and the -repository lint suite also pass. +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 @@ -231,12 +251,12 @@ 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 -and `mqt.qubit_register_name`; 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. +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 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/QCOToJeff/QCOToJeff.td b/mlir/include/mlir/Conversion/QCOToJeff/QCOToJeff.td index d057b31a57..dafd61baab 100644 --- a/mlir/include/mlir/Conversion/QCOToJeff/QCOToJeff.td +++ b/mlir/include/mlir/Conversion/QCOToJeff/QCOToJeff.td @@ -21,5 +21,5 @@ def QCOToJeff : Pass<"qco-to-jeff"> { As the index is not preserved in `jeff`, it is not possible to round-tripping static qubits. }]; - let dependentDialects = ["mlir::jeff::JeffDialect"]; + let dependentDialects = ["mlir::jeff::JeffDialect", "mlir::mqt::MQTDialect"]; } diff --git a/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td b/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td index 7abd899a79..98b4f2d418 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: @@ -35,5 +35,5 @@ def QCToQIRAdaptive : Pass<"qc-to-qir-adaptive"> { - Non-quantum dialects are lowered via MLIR's built-in conversions. }]; - let dependentDialects = ["mlir::LLVM::LLVMDialect"]; + let dependentDialects = ["mlir::LLVM::LLVMDialect", "mlir::mqt::MQTDialect"]; } diff --git a/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td b/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td index ad37812fb2..7e12798df8 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). @@ -37,5 +37,5 @@ def QCToQIRBase : Pass<"qc-to-qir-base"> { - Non-quantum dialects are lowered via MLIR's built-in conversions. }]; - let dependentDialects = ["mlir::LLVM::LLVMDialect"]; + let dependentDialects = ["mlir::LLVM::LLVMDialect", "mlir::mqt::MQTDialect"]; } diff --git a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h index 8521d6e692..d54d24dbe0 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h @@ -10,7 +10,9 @@ #pragma once +#include #include +#include #include #include @@ -19,3 +21,8 @@ //===----------------------------------------------------------------------===// #include "mlir/Dialect/MQT/IR/MQTDialect.h.inc" // IWYU pragma: export + +namespace mlir::mqt { +/// 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 index b4308499b6..5471ec3976 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td @@ -24,10 +24,12 @@ def MQTDialect : Dialect { `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::StringAttr":$register_name, "::mlir::UnitAttr":$entry_point); let hasOperationAttrVerify = 1; let hasRegionArgAttrVerify = 1; diff --git a/mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h b/mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h index 7724d27350..a15de4e5cd 100644 --- a/mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h +++ b/mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h @@ -77,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(); @@ -88,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); diff --git a/mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h b/mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h index d8c0acef0f..1a68e7e5aa 100644 --- a/mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h +++ b/mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h @@ -87,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(); @@ -98,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); 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 144a0e36d2..ab2edd0309 100644 --- a/mlir/include/mlir/Dialect/Utils/Utils.h +++ b/mlir/include/mlir/Dialect/Utils/Utils.h @@ -18,7 +18,6 @@ #include #include #include -#include #include #include #include @@ -494,35 +493,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/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 450ed6c08f..1bb11399df 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" @@ -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,8 @@ struct ConvertJeffMainToQCO final : OpConversionPattern { resultTypes.push_back(rewriter.getI64Type()); } rewriter.modifyOpInPlace(op, [&] { - op->setAttr("passthrough", rewriter.getArrayAttr( - {rewriter.getStringAttr("entry_point")})); + op->setAttr(mqt::MQTDialect::EntryPointAttrHelper::getNameStr(), + rewriter.getUnitAttr()); op.setType(rewriter.getFunctionType(inputTypes, resultTypes)); for (const auto& [argument, type] : llvm::zip_equal(block->getArguments(), inputTypes)) { @@ -1303,7 +1304,8 @@ struct JeffToQCO final : impl::JeffToQCOBase { target.addDynamicallyLegalOp([&](func::FuncOp op) { return (op.getSymName() != getEntryPointName(module) || - op->hasAttr("passthrough")) && + op->hasAttr( + mqt::MQTDialect::EntryPointAttrHelper::getNameStr())) && 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..a4c22378ec 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" @@ -1644,7 +1645,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 +1658,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 (!op->hasAttr(mqt::MQTDialect::EntryPointAttrHelper::getNameStr())) { return failure(); } @@ -1699,7 +1692,7 @@ struct ConvertQCOMainToJeff final : StatefulOpConversionPattern { llvm::zip_equal(block->getArguments(), newInputs)) { argument.setType(type); } - op->removeAttr("passthrough"); + op->removeAttr(mqt::MQTDialect::EntryPointAttrHelper::getNameStr()); rewriter.finalizeOpModification(op); return success(); @@ -1882,8 +1875,9 @@ struct QCOToJeff final : impl::QCOToJeffBase { scf::SCFDialect, memref::MemRefDialect>(); target.addLegalDialect(); - target.addDynamicallyLegalOp( - [](func::FuncOp op) { return !op->hasAttr("passthrough"); }); + target.addDynamicallyLegalOp([](func::FuncOp op) { + return !op->hasAttr(mqt::MQTDialect::EntryPointAttrHelper::getNameStr()); + }); target.addDynamicallyLegalOp([](func::ReturnOp op) { return llvm::none_of(op.getOperandTypes(), [](Type type) { return isa(type); 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..59c4c64a19 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp @@ -14,6 +14,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/QIR/Utils/QIRUtils.h" @@ -730,7 +731,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..d7c3d576b2 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp @@ -13,6 +13,7 @@ #include "mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.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/QIR/Utils/QIRUtils.h" @@ -494,7 +495,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/QIRCommon.cpp b/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp index 557f619b03..8ea774e2d1 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp @@ -436,16 +436,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 (!funcOp->hasAttr(mqt::MQTDialect::EntryPointAttrHelper::getNameStr())) { return; } diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index db26ca6dcf..479e75b1b7 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -32,6 +32,34 @@ using namespace mlir::mqt; void MQTDialect::initialize() {} namespace { +[[nodiscard]] 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 && + candidate.hasAttr(MQTDialect::EntryPointAttrHelper::getNameStr())) { + return operation->emitError() + << "module must contain at most one program entry point"; + } + } + return success(); +} + [[nodiscard]] LogicalResult verifyName(Operation* operation, const NamedAttribute attribute) { const auto name = dyn_cast(attribute.getValue()); @@ -113,6 +141,9 @@ namespace { 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); } @@ -174,3 +205,12 @@ LogicalResult MQTDialect::verifyRegionResultAttribute( << "attribute '" << attribute.getName().getValue() << "' is not valid on a region result"; } + +func::FuncOp mlir::mqt::getEntryPoint(ModuleOp moduleOp) { + for (auto function : moduleOp.getOps()) { + if (function->hasAttr(MQTDialect::EntryPointAttrHelper::getNameStr())) { + return function; + } + } + return nullptr; +} diff --git a/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp b/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp index 1dbd18a06a..63a71e18d2 100644 --- a/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp +++ b/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp @@ -61,9 +61,8 @@ 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})); + ctx->getLoadedDialect()->getEntryPointAttrHelper().setAttr( + mainFunc, getUnitAttr()); // Create entry block and set insertion point auto& entryBlock = mainFunc.getBody().emplaceBlock(); @@ -71,7 +70,7 @@ void QCProgramBuilder::initialize(TypeRange returnTypes) { } void QCProgramBuilder::retype(TypeRange returnTypes) { - auto mainFunc = getEntryPoint(mlir::cast(module)); + auto mainFunc = mqt::getEntryPoint(mlir::cast(module)); if (!mainFunc) { llvm::reportFatalUsageError("Main function not found for retyping"); } diff --git a/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp b/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp index 5004bbd277..ff5793bd17 100644 --- a/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp +++ b/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp @@ -70,9 +70,8 @@ void QCOProgramBuilder::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})); + ctx->getLoadedDialect()->getEntryPointAttrHelper().setAttr( + mainFunc, getUnitAttr()); // Create entry block and set insertion point auto& entryBlock = mainFunc.getBody().emplaceBlock(); @@ -80,7 +79,7 @@ void QCOProgramBuilder::initialize(TypeRange returnTypes) { } void QCOProgramBuilder::retype(TypeRange returnTypes) { - auto mainFunc = getEntryPoint(mlir::cast(module)); + auto mainFunc = mqt::getEntryPoint(mlir::cast(module)); if (!mainFunc) { llvm::reportFatalUsageError("Main function not found for retyping"); } 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..88e9ba7eda 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" @@ -358,7 +359,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 +411,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..09012dd5c5 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)); + main->removeAttr(mqt::MQTDialect::EntryPointAttrHelper::getNameStr()); 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..7db48a677c 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 (funcOp->hasAttr(mqt::MQTDialect::EntryPointAttrHelper::getNameStr())) { + return funcOp; + } auto passthrough = funcOp->getAttrOfType("passthrough"); if (!passthrough) { continue; diff --git a/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp b/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp index d380dac45d..9863e12d7f 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,8 @@ 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( + main->hasAttr(mlir::mqt::MQTDialect::EntryPointAttrHelper::getNameStr())); auto cregType = cbit::RegisterType::get(&context, 1); ASSERT_EQ(main.getFunctionType().getNumResults(), 1); EXPECT_EQ(main.getFunctionType().getResult(0), cregType); @@ -433,8 +434,9 @@ TEST(JeffRoundTripRegressionTest, RestoresEntryPointWithObservableResults) { TEST(JeffRoundTripRegressionTest, ConvertsJeffBitArraysDirectlyToCBit) { DialectRegistry registry; - registry.insert(); + registry.insert(); MLIRContext context(registry); context.loadAllAvailableDialects(); auto program = @@ -470,15 +472,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 +517,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 f1af599f66..8b14f6732e 100644 --- a/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp +++ b/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp @@ -124,7 +124,7 @@ TEST(QCOToQCRegressionTest, RetainsDynamicQubitRegisterName) { constexpr llvm::StringLiteral source = R"mlir( module { - func.func @main(%size: index) attributes {passthrough = ["entry_point"]} { + func.func @main(%size: index) attributes {mqt.entry_point} { %reg = qtensor.alloc(%size) {mqt.register_name = "named_qubits"} : tensor qtensor.dealloc %reg : tensor return @@ -176,7 +176,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 @@ -230,7 +230,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) { @@ -285,7 +285,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 @@ -332,7 +332,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) 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 81f193626d..03d82d0213 100644 --- a/mlir/unittests/Conversion/QCQCORoundTrip/test_qc_qco_round_trip.cpp +++ b/mlir/unittests/Conversion/QCQCORoundTrip/test_qc_qco_round_trip.cpp @@ -78,7 +78,7 @@ TEST_F(QCQCORoundTripTest, PreservesSharedMQTMetadata) { constexpr llvm::StringLiteral source = R"mlir( module { func.func @main(%theta: f64 {mqt.input_name = "theta"}) - attributes {passthrough = ["entry_point"]} { + attributes {mqt.entry_point} { %reg = memref.alloc() {mqt.register_name = "q"} : memref<2x!qc.qubit> memref.dealloc %reg : memref<2x!qc.qubit> @@ -94,6 +94,8 @@ module { auto function = moduleOp->lookupSymbol("main"); ASSERT_TRUE(function); + EXPECT_TRUE(function->hasAttr( + mlir::mqt::MQTDialect::EntryPointAttrHelper::getNameStr())); const auto inputName = function.getArgAttrOfType( 0, mlir::mqt::MQTDialect::InputNameAttrHelper::getNameStr()); ASSERT_TRUE(inputName); @@ -112,7 +114,7 @@ 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) {mqt.register_name = "zero"} : !cbit.reg<2> %undefined = cbit.alloc(#cbit.init) {mqt.register_name = "undefined"} : !cbit.reg<1> @@ -170,7 +172,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 @@ -209,7 +211,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 { @@ -249,7 +251,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 d28e509419..1ac5f1f62a 100644 --- a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp +++ b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp @@ -225,7 +225,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 @@ -265,7 +265,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 @@ -320,7 +320,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 @@ -360,7 +360,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 { @@ -408,7 +408,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 { @@ -437,7 +437,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 @@ -492,7 +492,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 { @@ -550,7 +550,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> @@ -613,7 +613,7 @@ TEST_F(QCToQCORegressionTest, RetainsQubitRegisterName) { TEST_F(QCToQCORegressionTest, RetainsDynamicQubitRegisterName) { constexpr llvm::StringLiteral source = R"mlir( module { - func.func @main(%size: index) attributes {passthrough = ["entry_point"]} { + func.func @main(%size: index) attributes {mqt.entry_point} { %reg = memref.alloc(%size) {mqt.register_name = "named_qubits"} : memref memref.dealloc %reg : memref return @@ -640,7 +640,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> @@ -669,7 +669,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 @@ -701,7 +701,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 @@ -735,7 +735,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 } @@ -744,7 +744,7 @@ module { R"mlir( module { func.func @main(%reg: memref<1x!qc.qubit>) - attributes {passthrough = ["entry_point"]} { + attributes {mqt.entry_point} { return } } @@ -752,7 +752,7 @@ module { R"mlir( module { func.func @main(%reg: memref<*x!qc.qubit>) - attributes {passthrough = ["entry_point"]} { + attributes {mqt.entry_point} { return } } @@ -788,7 +788,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 @@ -801,7 +801,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> @@ -842,7 +842,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 @@ -1354,7 +1354,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 @@ -1385,7 +1385,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> @@ -1414,7 +1414,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 diff --git a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp index f5cc5aba92..3eb02b1543 100644 --- a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp +++ b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp @@ -80,6 +80,45 @@ TEST_F(MQTIRTest, AcceptsProgramInputAndRegisterNames) { )mlir")); } +TEST_F(MQTIRTest, AcceptsAndFindsEntryPoint) { + auto moduleOp = parse(R"mlir( + module { + func.func @helper() { return } + func.func @main() attributes {mqt.entry_point} { return } + } + )mlir"); + ASSERT_TRUE(moduleOp); + EXPECT_EQ(mqt::getEntryPoint(*moduleOp).getSymName(), "main"); +} + +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 { diff --git a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp index 9986bcebba..c4c7b2e57a 100644 --- a/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp +++ b/mlir/unittests/Dialect/QC/Translation/test_openqasm3_emission.cpp @@ -85,7 +85,7 @@ 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) {mqt.register_name = "c"} @@ -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 diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp index 31fc1ef0a6..01d1d42ee0 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp @@ -704,7 +704,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 @@ -967,7 +967,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..9f4e20380b 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" @@ -61,6 +62,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 +298,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 +599,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 +637,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 +985,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 +1050,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 +1094,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 +1145,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 +1204,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 +1270,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 +1332,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/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 94a927c57b..e89e19f9db 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -230,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 @@ -489,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 @@ -563,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 @@ -596,7 +596,7 @@ 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) {mqt.register_name = "output"} : !cbit.reg<1> %internal = cbit.alloc(#cbit.init) {mqt.register_name = "internal"} : !cbit.reg<2> @@ -627,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 @@ -650,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 @@ -1259,7 +1259,7 @@ 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 {passthrough = ["entry_point"]} { + 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 @@ -1287,7 +1287,7 @@ def _wide_parameter_expression_program(term_count: int) -> QCProgram: """ lines = [ "module {", - ' func.func @main(%theta: f64 {mqt.input_name = "theta"}) attributes {passthrough = ["entry_point"]} {', + ' func.func @main(%theta: f64 {mqt.input_name = "theta"}) attributes {mqt.entry_point} {', " %q = qc.alloc : !qc.qubit", ] values = [] @@ -1337,7 +1337,7 @@ 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 {passthrough = ["entry_point"]} { + 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 @@ -1404,7 +1404,7 @@ def test_duplicate_named_symbolic_inputs_are_invalid_qc_ir() -> None: func.func @main( %first: f64 {mqt.input_name = "theta"}, %second: f64 {mqt.input_name = "theta"} - ) attributes {passthrough = ["entry_point"]} { + ) attributes {mqt.entry_point} { %q = qc.alloc : !qc.qubit qc.rx(%first) %q : !qc.qubit qc.rz(%second) %q : !qc.qubit @@ -1432,7 +1432,7 @@ def test_parameter_and_register_names_must_be_unique() -> None: with pytest.raises(RuntimeError, match="MLIR operation failed"): QCProgram.from_mlir_str( """module { - func.func @main(%theta: f64 {mqt.input_name = "theta"}) attributes {passthrough = ["entry_point"]} { + 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 @@ -1457,7 +1457,7 @@ def test_parameter_names_with_null_characters_fail_closed() -> None: 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 {passthrough = ["entry_point"]} { + 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 @@ -1472,7 +1472,7 @@ 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 {mqt.input_name = "theta"}) 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 @@ -1491,7 +1491,7 @@ 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 {passthrough = ["entry_point"]} { + 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 @@ -1512,7 +1512,7 @@ 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 {passthrough = ["entry_point"]} { + 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 @@ -1530,7 +1530,7 @@ 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 {passthrough = ["entry_point"]} { + 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 @@ -1555,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 @@ -1581,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 @@ -1613,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 }} From 946aa33a6dfd3e89158831b918ca1ac5890bdfd5 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Thu, 20 Aug 2026 13:47:35 +0000 Subject: [PATCH 13/17] =?UTF-8?q?=F0=9F=90=9B=20Register=20MQT=20dialect?= =?UTF-8?q?=20TableGen=20headers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the generated MQT dialect declarations to MLIR's global header target. This ensures clean parallel builds generate MQTDialect.h.inc before compiling any dialect consumer. Assisted-by: Codex --- mlir/include/mlir/Dialect/MQT/IR/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlir/include/mlir/Dialect/MQT/IR/CMakeLists.txt b/mlir/include/mlir/Dialect/MQT/IR/CMakeLists.txt index d2b3e87d42..d0e04e4d20 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/CMakeLists.txt +++ b/mlir/include/mlir/Dialect/MQT/IR/CMakeLists.txt @@ -9,6 +9,6 @@ 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_public_tablegen_target(MLIRMQTDialectIncGen) +add_mlir_dialect_tablegen_target(MLIRMQTDialectIncGen) add_mlir_doc(MQTDialect MQTDialect Dialects/ -gen-dialect-doc -dialect=mqt) From 826470cfaa16761745bf3c5f3a2dbc49a6162c7c Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Thu, 20 Aug 2026 14:44:02 +0000 Subject: [PATCH 14/17] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20MQT=20met?= =?UTF-8?q?adata=20integration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Centralize entry-point metadata access in the MQT dialect, restrict pass dependencies to produced dialects, and remove redundant namespace qualifiers where unambiguous. Assisted-by: Codex --- .agent/plans/qiskit-symbolic-parameters.md | 11 +++++++ .../mlir/Conversion/QCOToJeff/QCOToJeff.td | 2 +- .../QCToQIR/QIRAdaptive/QCToQIRAdaptive.td | 2 +- .../Conversion/QCToQIR/QIRBase/QCToQIRBase.td | 2 +- mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h | 9 +++++ mlir/include/mlir/Dialect/Utils/Utils.h | 13 ++++---- mlir/lib/Compiler/Programs.cpp | 4 +-- mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp | 6 ++-- mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp | 11 +++---- .../QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp | 2 +- .../QCToQIR/QIRBase/QCToQIRBase.cpp | 2 +- .../QCToQIR/QIRCommon/QIRCommon.cpp | 4 +-- mlir/lib/Dialect/MQT/IR/MQTDialect.cpp | 19 +++++++++-- .../Dialect/QC/Builder/QCProgramBuilder.cpp | 5 ++- .../Dialect/QCO/Builder/QCOProgramBuilder.cpp | 7 ++-- .../QIR/Transforms/AttachQIRAttributes.cpp | 2 +- mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp | 2 +- .../JeffRoundTrip/test_jeff_round_trip.cpp | 3 +- .../QCQCORoundTrip/test_qc_qco_round_trip.cpp | 33 +++++++++---------- mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp | 17 ++++++++-- 20 files changed, 96 insertions(+), 60 deletions(-) diff --git a/.agent/plans/qiskit-symbolic-parameters.md b/.agent/plans/qiskit-symbolic-parameters.md index e252f6a45e..b43566dea8 100644 --- a/.agent/plans/qiskit-symbolic-parameters.md +++ b/.agent/plans/qiskit-symbolic-parameters.md @@ -92,6 +92,12 @@ partially constructed output circuit after a failure. Exact 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 @@ -199,6 +205,11 @@ partially constructed output circuit after a failure. Exact 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 diff --git a/mlir/include/mlir/Conversion/QCOToJeff/QCOToJeff.td b/mlir/include/mlir/Conversion/QCOToJeff/QCOToJeff.td index dafd61baab..d057b31a57 100644 --- a/mlir/include/mlir/Conversion/QCOToJeff/QCOToJeff.td +++ b/mlir/include/mlir/Conversion/QCOToJeff/QCOToJeff.td @@ -21,5 +21,5 @@ def QCOToJeff : Pass<"qco-to-jeff"> { As the index is not preserved in `jeff`, it is not possible to round-tripping static qubits. }]; - let dependentDialects = ["mlir::jeff::JeffDialect", "mlir::mqt::MQTDialect"]; + let dependentDialects = ["mlir::jeff::JeffDialect"]; } diff --git a/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td b/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td index 98b4f2d418..a56d0b263e 100644 --- a/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td +++ b/mlir/include/mlir/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.td @@ -35,5 +35,5 @@ def QCToQIRAdaptive : Pass<"qc-to-qir-adaptive"> { - Non-quantum dialects are lowered via MLIR's built-in conversions. }]; - let dependentDialects = ["mlir::LLVM::LLVMDialect", "mlir::mqt::MQTDialect"]; + let dependentDialects = ["mlir::LLVM::LLVMDialect"]; } diff --git a/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td b/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td index 7e12798df8..6d15fbb842 100644 --- a/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td +++ b/mlir/include/mlir/Conversion/QCToQIR/QIRBase/QCToQIRBase.td @@ -37,5 +37,5 @@ def QCToQIRBase : Pass<"qc-to-qir-base"> { - Non-quantum dialects are lowered via MLIR's built-in conversions. }]; - let dependentDialects = ["mlir::LLVM::LLVMDialect", "mlir::mqt::MQTDialect"]; + let dependentDialects = ["mlir::LLVM::LLVMDialect"]; } diff --git a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h index d54d24dbe0..7cf2b9604a 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.h @@ -23,6 +23,15 @@ #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/Utils/Utils.h b/mlir/include/mlir/Dialect/Utils/Utils.h index ab2edd0309..ed7afbd38d 100644 --- a/mlir/include/mlir/Dialect/Utils/Utils.h +++ b/mlir/include/mlir/Dialect/Utils/Utils.h @@ -166,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; } @@ -196,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); @@ -210,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); } @@ -230,8 +229,8 @@ valueToConstantAttr(Value value, /// expression is finite. [[nodiscard]] inline LogicalResult verifyFiniteConstantParameters(Operation* op, const ValueRange parameters) { - llvm::DenseMap> constantCache; - llvm::DenseSet visited; + DenseMap> constantCache; + DenseSet visited; for (const auto [index, parameter] : llvm::enumerate(parameters)) { SmallVector worklist{parameter}; while (!worklist.empty()) { diff --git a/mlir/lib/Compiler/Programs.cpp b/mlir/lib/Compiler/Programs.cpp index a5780696dc..4828575d56 100644 --- a/mlir/lib/Compiler/Programs.cpp +++ b/mlir/lib/Compiler/Programs.cpp @@ -328,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 { @@ -399,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/JeffToQCO/JeffToQCO.cpp b/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp index 1bb11399df..b274b6d7d8 100644 --- a/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp +++ b/mlir/lib/Conversion/JeffToQCO/JeffToQCO.cpp @@ -1230,8 +1230,7 @@ struct ConvertJeffMainToQCO final : OpConversionPattern { resultTypes.push_back(rewriter.getI64Type()); } rewriter.modifyOpInPlace(op, [&] { - op->setAttr(mqt::MQTDialect::EntryPointAttrHelper::getNameStr(), - rewriter.getUnitAttr()); + mqt::setEntryPoint(op); op.setType(rewriter.getFunctionType(inputTypes, resultTypes)); for (const auto& [argument, type] : llvm::zip_equal(block->getArguments(), inputTypes)) { @@ -1304,8 +1303,7 @@ struct JeffToQCO final : impl::JeffToQCOBase { target.addDynamicallyLegalOp([&](func::FuncOp op) { return (op.getSymName() != getEntryPointName(module) || - op->hasAttr( - mqt::MQTDialect::EntryPointAttrHelper::getNameStr())) && + mqt::isEntryPoint(op)) && typeConverter.isSignatureLegal(op.getFunctionType()) && typeConverter.isLegal(&op.getBody()); }); diff --git a/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp b/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp index a4c22378ec..fe6463eb8b 100644 --- a/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp +++ b/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp @@ -1658,7 +1658,7 @@ struct ConvertQCOMainToJeff final : StatefulOpConversionPattern { LogicalResult matchAndRewrite(func::FuncOp op, OpAdaptor /*adaptor*/, ConversionPatternRewriter& rewriter) const override { - if (!op->hasAttr(mqt::MQTDialect::EntryPointAttrHelper::getNameStr())) { + if (!mqt::isEntryPoint(op)) { return failure(); } @@ -1692,7 +1692,7 @@ struct ConvertQCOMainToJeff final : StatefulOpConversionPattern { llvm::zip_equal(block->getArguments(), newInputs)) { argument.setType(type); } - op->removeAttr(mqt::MQTDialect::EntryPointAttrHelper::getNameStr()); + mqt::removeEntryPoint(op); rewriter.finalizeOpModification(op); return success(); @@ -1856,7 +1856,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; } @@ -1875,9 +1875,8 @@ struct QCOToJeff final : impl::QCOToJeffBase { scf::SCFDialect, memref::MemRefDialect>(); target.addLegalDialect(); - target.addDynamicallyLegalOp([](func::FuncOp op) { - return !op->hasAttr(mqt::MQTDialect::EntryPointAttrHelper::getNameStr()); - }); + target.addDynamicallyLegalOp( + [](func::FuncOp op) { return !mqt::isEntryPoint(op); }); target.addDynamicallyLegalOp([](func::ReturnOp op) { return llvm::none_of(op.getOperandTypes(), [](Type type) { return isa(type); diff --git a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp index 59c4c64a19..cd850dc61d 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp @@ -686,7 +686,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; } diff --git a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp index d7c3d576b2..9d4526b0a0 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp @@ -463,7 +463,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; } diff --git a/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp b/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp index 8ea774e2d1..433a9e2f94 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp @@ -436,7 +436,7 @@ LogicalResult prepareClassicalResults(Operation* moduleOp, bool hasInvalidMemory = false; SmallVector consumedStores; moduleOp->walk([&](func::FuncOp funcOp) { - if (!funcOp->hasAttr(mqt::MQTDialect::EntryPointAttrHelper::getNameStr())) { + if (!mqt::isEntryPoint(funcOp)) { return; } @@ -460,7 +460,7 @@ LogicalResult prepareClassicalResults(Operation* moduleOp, auto& reg = state.cregs[it->second]; reg.record = false; if (const auto name = allocOp->getAttrOfType( - ::mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr())) { + mqt::MQTDialect::RegisterNameAttrHelper::getNameStr())) { reg.label = name.str(); } const auto size = allocOp.getResult().getType().getWidth(); diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index 479e75b1b7..0ad1bf8a9d 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -51,8 +51,7 @@ namespace { } for (Operation& candidate : moduleOp.getBody()->getOperations()) { - if (&candidate != operation && - candidate.hasAttr(MQTDialect::EntryPointAttrHelper::getNameStr())) { + if (&candidate != operation && isEntryPoint(&candidate)) { return operation->emitError() << "module must contain at most one program entry point"; } @@ -206,9 +205,23 @@ LogicalResult MQTDialect::verifyRegionResultAttribute( << "' 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 (function->hasAttr(MQTDialect::EntryPointAttrHelper::getNameStr())) { + if (isEntryPoint(function)) { return function; } } diff --git a/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp b/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp index 63a71e18d2..5034956482 100644 --- a/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp +++ b/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp @@ -61,8 +61,7 @@ void QCProgramBuilder::initialize(TypeRange returnTypes) { auto funcType = getFunctionType({}, returnTypes); auto mainFunc = func::FuncOp::create(*this, "main", funcType); - ctx->getLoadedDialect()->getEntryPointAttrHelper().setAttr( - mainFunc, getUnitAttr()); + 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 = mqt::getEntryPoint(mlir::cast(module)); + auto mainFunc = mqt::getEntryPoint(cast(module)); if (!mainFunc) { llvm::reportFatalUsageError("Main function not found for retyping"); } diff --git a/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp b/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp index ff5793bd17..e0056cce32 100644 --- a/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp +++ b/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp @@ -64,14 +64,13 @@ 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); - ctx->getLoadedDialect()->getEntryPointAttrHelper().setAttr( - mainFunc, getUnitAttr()); + mqt::setEntryPoint(mainFunc); // Create entry block and set insertion point auto& entryBlock = mainFunc.getBody().emplaceBlock(); @@ -79,7 +78,7 @@ void QCOProgramBuilder::initialize(TypeRange returnTypes) { } void QCOProgramBuilder::retype(TypeRange returnTypes) { - auto mainFunc = mqt::getEntryPoint(mlir::cast(module)); + auto mainFunc = mqt::getEntryPoint(cast(module)); if (!mainFunc) { llvm::reportFatalUsageError("Main function not found for retyping"); } diff --git a/mlir/lib/Dialect/QIR/Transforms/AttachQIRAttributes.cpp b/mlir/lib/Dialect/QIR/Transforms/AttachQIRAttributes.cpp index 09012dd5c5..c46d81b188 100644 --- a/mlir/lib/Dialect/QIR/Transforms/AttachQIRAttributes.cpp +++ b/mlir/lib/Dialect/QIR/Transforms/AttachQIRAttributes.cpp @@ -120,7 +120,7 @@ struct QIRSetAttributesAndMetadata final {"required_num_results", std::to_string(metadata.numResults)})}; main->setAttr("passthrough", rewriter.getArrayAttr(attributes)); - main->removeAttr(mqt::MQTDialect::EntryPointAttrHelper::getNameStr()); + mqt::removeEntryPoint(main); rewriter.setInsertionPointToEnd(m.getBody()); diff --git a/mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp b/mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp index 7db48a677c..3367544635 100644 --- a/mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp +++ b/mlir/lib/Dialect/QIR/Utils/QIRUtils.cpp @@ -335,7 +335,7 @@ LLVM::LLVMFuncOp getMainFunction(Operation* op) { } for (const auto funcOp : moduleOp.getOps()) { - if (funcOp->hasAttr(mqt::MQTDialect::EntryPointAttrHelper::getNameStr())) { + if (mqt::isEntryPoint(funcOp)) { return funcOp; } auto passthrough = funcOp->getAttrOfType("passthrough"); diff --git a/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp b/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp index 9863e12d7f..268f60c9c6 100644 --- a/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp +++ b/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp @@ -422,8 +422,7 @@ TEST(JeffRoundTripRegressionTest, RestoresEntryPointWithObservableResults) { ASSERT_TRUE(succeeded(convertJeffToQCO(*program))); auto main = program->lookupSymbol("main"); ASSERT_TRUE(main); - EXPECT_TRUE( - main->hasAttr(mlir::mqt::MQTDialect::EntryPointAttrHelper::getNameStr())); + 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); 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 03d82d0213..71845f87d1 100644 --- a/mlir/unittests/Conversion/QCQCORoundTrip/test_qc_qco_round_trip.cpp +++ b/mlir/unittests/Conversion/QCQCORoundTrip/test_qc_qco_round_trip.cpp @@ -48,7 +48,7 @@ class QCQCORoundTripTest : public testing::Test { QCQCORoundTripTest() { DialectRegistry registry; registry - .insert(); context.appendDialectRegistry(registry); @@ -75,7 +75,7 @@ class QCQCORoundTripTest : public testing::Test { } // namespace TEST_F(QCQCORoundTripTest, PreservesSharedMQTMetadata) { - constexpr llvm::StringLiteral source = R"mlir( + constexpr StringLiteral source = R"mlir( module { func.func @main(%theta: f64 {mqt.input_name = "theta"}) attributes {mqt.entry_point} { @@ -94,10 +94,9 @@ module { auto function = moduleOp->lookupSymbol("main"); ASSERT_TRUE(function); - EXPECT_TRUE(function->hasAttr( - mlir::mqt::MQTDialect::EntryPointAttrHelper::getNameStr())); + EXPECT_TRUE(mqt::isEntryPoint(function)); const auto inputName = function.getArgAttrOfType( - 0, mlir::mqt::MQTDialect::InputNameAttrHelper::getNameStr()); + 0, mqt::MQTDialect::InputNameAttrHelper::getNameStr()); ASSERT_TRUE(inputName); EXPECT_EQ(inputName.getValue(), "theta"); @@ -105,7 +104,7 @@ module { moduleOp->walk([&](memref::AllocOp op) { allocation = op; }); ASSERT_TRUE(allocation); const auto registerName = allocation->getAttrOfType( - mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()); + mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()); ASSERT_TRUE(registerName); EXPECT_EQ(registerName.getValue(), "q"); } @@ -144,20 +143,18 @@ module { ASSERT_EQ(loads.size(), 1); ASSERT_EQ(stores.size(), 2); EXPECT_EQ(allocations[0].getInitialization(), cbit::Initialization::Zero); - EXPECT_EQ( - allocations[0] - ->getAttrOfType( - ::mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()) - .getValue(), - "zero"); + EXPECT_EQ(allocations[0] + ->getAttrOfType( + mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()) + .getValue(), + "zero"); EXPECT_EQ(allocations[1].getInitialization(), cbit::Initialization::Undefined); - EXPECT_EQ( - allocations[1] - ->getAttrOfType( - ::mlir::mqt::MQTDialect::RegisterNameAttrHelper::getNameStr()) - .getValue(), - "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()); diff --git a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp index 3eb02b1543..6099633f98 100644 --- a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp +++ b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp @@ -80,7 +80,7 @@ TEST_F(MQTIRTest, AcceptsProgramInputAndRegisterNames) { )mlir")); } -TEST_F(MQTIRTest, AcceptsAndFindsEntryPoint) { +TEST_F(MQTIRTest, ManagesAndFindsEntryPoint) { auto moduleOp = parse(R"mlir( module { func.func @helper() { return } @@ -88,7 +88,20 @@ TEST_F(MQTIRTest, AcceptsAndFindsEntryPoint) { } )mlir"); ASSERT_TRUE(moduleOp); - EXPECT_EQ(mqt::getEntryPoint(*moduleOp).getSymName(), "main"); + 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) { From 8f4a13ec19b59a1a5ba32204f924e20a35e60bc6 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Thu, 20 Aug 2026 14:44:26 +0000 Subject: [PATCH 15/17] =?UTF-8?q?=F0=9F=93=9D=20Document=20the=20MQT=20met?= =?UTF-8?q?adata=20dialect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose the generated MQT dialect reference through the compiler collection documentation and its table of contents. Assisted-by: Codex --- docs/mlir/MQT.md | 7 +++++++ docs/mlir/index.md | 3 +++ 2 files changed, 10 insertions(+) create mode 100644 docs/mlir/MQT.md 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 From 6f919c2e311955eac5666d5c48e74a95e3515aaf Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Thu, 20 Aug 2026 14:44:31 +0000 Subject: [PATCH 16/17] =?UTF-8?q?=F0=9F=93=9D=20Add=20PR=202150=20to=20the?= =?UTF-8?q?=20compiler=20launch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reference symbolic Qiskit parameters from the general MQT Compiler Collection launch entry. Assisted-by: Codex --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28f7e68b5d..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 From f4ff4849f1618eeb5165f971e306ab20c8f7a468 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Thu, 20 Aug 2026 16:25:44 +0000 Subject: [PATCH 17/17] =?UTF-8?q?=F0=9F=94=A7=20Fix=20clang-tidy=20diagnos?= =?UTF-8?q?tics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use direct includes, designated initializers, and static file-local helpers across the symbolic Qiskit parameter changes. Assisted-by: Codex --- bindings/mlir/qiskit/QiskitImport.cpp | 1 - bindings/mlir/qiskit/QiskitTranslation.h | 10 ++++++---- mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp | 1 - mlir/lib/Conversion/QCOToQC/QCOToQC.cpp | 1 - mlir/lib/Conversion/QCToQCO/QCToQCO.cpp | 1 - .../QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp | 1 - .../QCToQIR/QIRBase/QCToQIRBase.cpp | 1 - .../QCToQIR/QIRCommon/QIRCommon.cpp | 1 - mlir/lib/Dialect/MQT/IR/MQTDialect.cpp | 19 ++++++++++--------- .../IR/Operations/StandardGates/GPhaseOp.cpp | 1 + .../QC/Translation/TranslateQCToOpenQASM3.cpp | 1 - .../QCO/Transforms/Mapping/Mapping.cpp | 2 -- .../Conversion/QCOToQC/test_qco_to_qc.cpp | 1 - .../QCQCORoundTrip/test_qc_qco_round_trip.cpp | 1 + .../Conversion/QCToQCO/test_qc_to_qco.cpp | 1 - mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp | 1 - .../QC/Transforms/test_qc_transforms.cpp | 2 ++ .../QC/Translation/test_qasm3_translation.cpp | 1 - mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp | 1 - .../QCO/Transforms/Mapping/test_mapping.cpp | 2 -- .../Transforms/test_qtensor_transforms.cpp | 2 ++ 21 files changed, 22 insertions(+), 30 deletions(-) diff --git a/bindings/mlir/qiskit/QiskitImport.cpp b/bindings/mlir/qiskit/QiskitImport.cpp index a65652974a..98e406d692 100644 --- a/bindings/mlir/qiskit/QiskitImport.cpp +++ b/bindings/mlir/qiskit/QiskitImport.cpp @@ -22,7 +22,6 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include "mlir/Dialect/Utils/DenseUnitary.h" -#include "mlir/Dialect/Utils/Utils.h" #include #include diff --git a/bindings/mlir/qiskit/QiskitTranslation.h b/bindings/mlir/qiskit/QiskitTranslation.h index af57c46c42..67c02b66e9 100644 --- a/bindings/mlir/qiskit/QiskitTranslation.h +++ b/bindings/mlir/qiskit/QiskitTranslation.h @@ -110,14 +110,16 @@ class Parameter { [[nodiscard]] static Parameter unary(const UnaryParameterKind operation, Parameter operand) { return Parameter(Unary{ - operation, std::make_shared(std::move(operand))}); + .operation = operation, + .operand = std::make_shared(std::move(operand))}); } [[nodiscard]] static Parameter binary(const BinaryParameterKind operation, Parameter left, Parameter right) { return Parameter( - Binary{operation, std::make_shared(std::move(left)), - std::make_shared(std::move(right))}); + Binary{.operation = operation, + .left = std::make_shared(std::move(left)), + .right = std::make_shared(std::move(right))}); } [[nodiscard]] const Number* getNumber() const { @@ -153,7 +155,7 @@ enum class GateModifierKind : uint8_t { struct GateModifier { GateModifierKind kind = GateModifierKind::Inverse; uint32_t numControls = 0; - Parameter exponent{}; + Parameter exponent; }; struct StandardGateMapping { diff --git a/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp b/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp index fe6463eb8b..ca29ebec3e 100644 --- a/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp +++ b/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp @@ -26,7 +26,6 @@ #include #include #include -#include #include #include #include diff --git a/mlir/lib/Conversion/QCOToQC/QCOToQC.cpp b/mlir/lib/Conversion/QCOToQC/QCOToQC.cpp index 07f5eea0eb..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 diff --git a/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp b/mlir/lib/Conversion/QCToQCO/QCToQCO.cpp index 53b8a61275..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 diff --git a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp index cd850dc61d..9294657c50 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp @@ -14,7 +14,6 @@ #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/QIR/Utils/QIRUtils.h" diff --git a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp index 9d4526b0a0..87d02a9d21 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp @@ -13,7 +13,6 @@ #include "mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.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/QIR/Utils/QIRUtils.h" diff --git a/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp b/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp index 433a9e2f94..c9e619b779 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp @@ -17,7 +17,6 @@ #include "mlir/Dialect/QC/IR/QCOps.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" -#include #include #include #include diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index 0ad1bf8a9d..f1da715d93 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -15,8 +15,11 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" +#include #include +#include #include +#include #include #include #include @@ -31,9 +34,8 @@ using namespace mlir::mqt; void MQTDialect::initialize() {} -namespace { -[[nodiscard]] LogicalResult verifyEntryPoint(Operation* operation, - const NamedAttribute attribute) { +[[nodiscard]] static LogicalResult +verifyEntryPoint(Operation* operation, const NamedAttribute attribute) { if (!isa(attribute.getValue())) { return operation->emitError() << "attribute '" << attribute.getName().getValue() @@ -59,8 +61,8 @@ namespace { return success(); } -[[nodiscard]] LogicalResult verifyName(Operation* operation, - const NamedAttribute attribute) { +[[nodiscard]] static LogicalResult verifyName(Operation* operation, + const NamedAttribute attribute) { const auto name = dyn_cast(attribute.getValue()); if (!name) { return operation->emitError() @@ -80,7 +82,7 @@ namespace { return success(); } -[[nodiscard]] bool isRegisterAllocation(Operation* operation) { +[[nodiscard]] static bool isRegisterAllocation(Operation* operation) { if (isa(operation)) { return true; } @@ -96,8 +98,8 @@ namespace { return false; } -[[nodiscard]] LogicalResult verifyRegisterName(Operation* operation, - const NamedAttribute attribute) { +[[nodiscard]] static LogicalResult +verifyRegisterName(Operation* operation, const NamedAttribute attribute) { if (failed(verifyName(operation, attribute))) { return failure(); } @@ -135,7 +137,6 @@ namespace { } return success(); } -} // namespace LogicalResult MQTDialect::verifyOperationAttribute(Operation* operation, diff --git a/mlir/lib/Dialect/QC/IR/Operations/StandardGates/GPhaseOp.cpp b/mlir/lib/Dialect/QC/IR/Operations/StandardGates/GPhaseOp.cpp index 111759e9a0..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; diff --git a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp index 47a3d12292..4ed1f88b1b 100644 --- a/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp +++ b/mlir/lib/Dialect/QC/Translation/TranslateQCToOpenQASM3.cpp @@ -17,7 +17,6 @@ #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/IR/QCInterfaces.h" #include "mlir/Dialect/QC/IR/QCOps.h" -#include "mlir/Dialect/Utils/Utils.h" #include "mlir/Target/OpenQASM/GateCatalog.h" #include diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 88e9ba7eda..ef372562c2 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -21,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 @@ -65,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" diff --git a/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp b/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp index 8b14f6732e..9e48ae88aa 100644 --- a/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp +++ b/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp @@ -16,7 +16,6 @@ #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" 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 71845f87d1..cda3572613 100644 --- a/mlir/unittests/Conversion/QCQCORoundTrip/test_qc_qco_round_trip.cpp +++ b/mlir/unittests/Conversion/QCQCORoundTrip/test_qc_qco_round_trip.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include diff --git a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp index 1ac5f1f62a..8e3b233ef8 100644 --- a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp +++ b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp @@ -22,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" diff --git a/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp b/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp index bed8f6d466..751b68bbb2 100644 --- a/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp +++ b/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp @@ -55,7 +55,6 @@ #include #include #include -#include using namespace mlir; using namespace mlir::qc; diff --git a/mlir/unittests/Dialect/QC/Transforms/test_qc_transforms.cpp b/mlir/unittests/Dialect/QC/Transforms/test_qc_transforms.cpp index 28a6c80e70..7a9ae0d66c 100644 --- a/mlir/unittests/Dialect/QC/Transforms/test_qc_transforms.cpp +++ b/mlir/unittests/Dialect/QC/Transforms/test_qc_transforms.cpp @@ -32,6 +32,8 @@ #include #include +#include + using namespace mlir; namespace { diff --git a/mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp b/mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp index d6af89d426..2a51a5ff3e 100644 --- a/mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp +++ b/mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp @@ -21,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" diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp index 01d1d42ee0..7ddfa81cdc 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp @@ -57,7 +57,6 @@ #include #include #include -#include #include #include diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index 9f4e20380b..012a02a52c 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -18,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 @@ -61,7 +60,6 @@ using namespace mlir; using namespace mlir::qco; -using namespace mlir::utils; using mlir::mqt::getEntryPoint; static SmallVector getQubitValues(ValueRange values) { diff --git a/mlir/unittests/Dialect/QTensor/Transforms/test_qtensor_transforms.cpp b/mlir/unittests/Dialect/QTensor/Transforms/test_qtensor_transforms.cpp index 4020e95d07..b16c3a8ed1 100644 --- a/mlir/unittests/Dialect/QTensor/Transforms/test_qtensor_transforms.cpp +++ b/mlir/unittests/Dialect/QTensor/Transforms/test_qtensor_transforms.cpp @@ -33,6 +33,8 @@ #include #include +#include + using namespace mlir; namespace {