diff --git a/.agent/plans/selected-payload-target-environment.md b/.agent/plans/selected-payload-target-environment.md new file mode 100644 index 0000000000..625c6ef82b --- /dev/null +++ b/.agent/plans/selected-payload-target-environment.md @@ -0,0 +1,70 @@ +# Independent compiler capability prototype + +Status: implemented. + +## Scope and release boundary + +Core #2219 owns the compiler-only representation of selected program execution +capabilities and the corresponding target environment. It has no dependency on +the QDMI 1.4 adoption branch. Core PR #2162 adds control-flow legalization. Core +PR #2227 is the separate QDMI integration layer. Settled compiler-target and +typed attribute support already landed through #2218, #2323, and #2215. + +The compiler-only payload model targets Core 4.0. Core #2365 and QDMI #523 track +the separate QDMI 1.4 adaptation for Core 4.1 and do not gate this prototype. +Rebase mechanics do not settle format identity, operation sets, execution +guarantees, classical capabilities, or opaque-program semantics. + +## Preserved behavior + +Target inference rejects unknown topology or gate sets. Retain fixed and +variadic operation arities, arbitrary controlled DDSIM gates, and zero-arity +global phase. Retain current QCO linearity checks, reusable-function boundaries, +and SDK input support. Do not reintroduce superseded target-inference commits. + +The canonical pipeline keeps target-aware decomposition and deterministic +placement for all-to-all connectivity, with routing only for explicit graphs. +Mapping, native synthesis, and conformance consume the validated module +environment through MLIR's analysis manager. Placement and decomposition retain +their current target-taking factories. The pipeline builder receives the +selected environment, attaches it at entry, and seeds the analysis with its +prepared target. Standalone passes decode the module attribute on demand. The +environment is immutable during compilation. The typed pair has no unused DLTI +extension or query layer. + +## Implementation + +The prototype types and cached analysis live in +`mlir/Compiler/TargetEnvironment.h` and `TargetEnvironment.cpp`; typed metadata +belongs to the MQT dialect. `mlir/lib/Compiler/Pipeline.cpp` owns pipeline +execution. Keep `Programs.cpp` focused on the program representation. + +Bindings and `mqt-cc` accept a selected environment, while untargeted output +selection stays separate. No unreleased QDMI APIs or provider SDK dependencies +are introduced. Capability records remain prototype vocabulary until the design +tracker settles their semantics. + +## Validation + +Run the independent release build, compiler/mapping/synthesis/MQT IR tests, +command-line checks, Python compiler and QDMI regressions, generated stubs, +lint, and C++ lint. Explicitly cover missing environments, invalidation after +metadata changes, variadic gates and global phase, unsupported output without +consuming input, and preservation of current linearity checks. + +The prior capability-snapshot validation passed the release build and 3,879 +native tests, with one existing optional-device skip. All 558 targeted Python +tests passed with the superconducting reference device enabled. Stub generation +and C++ lint passed. + +## Audit decisions and validation + +The selected environment is the pipeline's only input. Its initialization pass +attaches the typed pair and seeds the analysis with the prepared immutable +target; standalone passes decode IR on demand. Regression tests cover shared +target storage, cache retention and invalidation, and replacement of stale +metadata. The unused DLTI extension and query layer is removed. + +The optimized native build passed all 3,204 configured tests, with one existing +optional-device skip. Repository lint passed. C++ lint covers whole changed +files in the PR diff. diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cd42f2dd1..0dbc98e813 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,10 +38,11 @@ releases may include breaking changes. direct lowering and dense-array helpers for supported compiler inputs ([#1915], [#1973], [#2077], [#2078], [#2079], [#2334]) ([**@simon1hofmann**], [**@burgholzer**]) -- ✨ Add immutable MLIR compiler targets, QDMI device integration, ordered - operation applicability, directional native synthesis, and target compilation - through C++, Python, and `mqt-cc` ([#1687], [#1993], [#1999], [#2049], - [#2285]) ([**@MatthiasReumann**], [**@simon1hofmann**], [**@burgholzer**]) +- ✨ Add immutable MLIR compiler targets, selected payload specifications, QDMI + device integration, ordered operation applicability, directional native + synthesis, and target compilation through C++, Python, and `mqt-cc` ([#2285], + [#2219], [#2049], [#1999], [#1993], [#1687]) ([**@MatthiasReumann**], + [**@simon1hofmann**], [**@burgholzer**]) #### Import and export @@ -964,6 +965,7 @@ for previous changelogs._ [#2228]: https://github.com/munich-quantum-toolkit/core/pull/2228 [#2224]: https://github.com/munich-quantum-toolkit/core/pull/2224 [#2220]: https://github.com/munich-quantum-toolkit/core/pull/2220 +[#2219]: https://github.com/munich-quantum-toolkit/core/pull/2219 [#2218]: https://github.com/munich-quantum-toolkit/core/pull/2218 [#2217]: https://github.com/munich-quantum-toolkit/core/pull/2217 [#2216]: https://github.com/munich-quantum-toolkit/core/pull/2216 diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 8082aefb7a..024aa35764 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -15,6 +15,7 @@ #include "mlir/Compiler/Programs.h" #include "mlir/Compiler/QDMIAdapter.h" #include "mlir/Compiler/Target.h" +#include "mlir/Compiler/TargetEnvironment.h" #include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/QCO/Utils/DDFunctionality.h" #include "mlir/bench/Generate.h" @@ -310,12 +311,26 @@ programFromInput(const nb::object& program, const bool inplace) { /// Run the coordinated default pipeline and return a typed program. [[nodiscard]] static mlir::CompilerProgram compileProgram(const nb::object& program, const mlir::ProgramFormat output, - const bool inplace, const mlir::CompilerTarget* const target, - const std::string& qcoPipeline, const bool enableTiming, - const bool enableStatistics) { + const bool inplace, const std::string& qcoPipeline, + const bool enableTiming, const bool enableStatistics) { return takeResult(mlir::runDefaultPipeline(programFromInput(program, inplace), - output, target, qcoPipeline, - enableTiming, enableStatistics)); + output, qcoPipeline, enableTiming, + enableStatistics)); +} + +/// Compile for one target environment and return its selected payload. +[[nodiscard]] static mlir::CompilerProgram +compileProgramForTarget(const nb::object& program, const bool inplace, + const mlir::TargetEnvironment& environment, + const bool enableTiming, const bool enableStatistics) { + auto output = environment.payloadSpecification().compilerOutput(); + if (!output) { + const auto message = llvm::toString(output.takeError()); + throw nb::value_error(message.c_str()); + } + return takeResult(mlir::runDefaultPipeline(programFromInput(program, inplace), + environment, enableTiming, + enableStatistics)); } template @@ -475,6 +490,70 @@ NB_MODULE(MQT_CORE_MODULE_NAME, m) { .value("QIR_ADAPTIVE", mlir::ProgramFormat::QIRAdaptive, "QIR for the Adaptive Profile."); + nb::enum_(m, "PayloadEncoding", + "Payload representation encoding.") + .value("TEXT", mlir::PayloadEncoding::Text) + .value("BINARY", mlir::PayloadEncoding::Binary); + + nb::class_(m, "PayloadFormat", "Exact payload identity.") + .def(nb::init(), + "format_id"_a, "version"_a, "profile"_a = "", + "encoding"_a = mlir::PayloadEncoding::Text) + .def_rw("format_id", &mlir::PayloadFormat::id) + .def_rw("version", &mlir::PayloadFormat::version) + .def_rw("profile", &mlir::PayloadFormat::profile) + .def_rw("encoding", &mlir::PayloadFormat::encoding); + + nb::class_(m, "ProgramConstraint", + "One payload capability constraint.") + .def(nb::init(), "constraint_id"_a, "value"_a) + .def_rw("constraint_id", &mlir::ProgramConstraint::id) + .def_rw("value", &mlir::ProgramConstraint::value); + + nb::class_(m, "ProgramCapability", + "One payload execution capability.") + .def(nb::init>(), + "capability_id"_a, "value"_a = 0, + "constraints"_a = std::vector{}) + .def_rw("capability_id", &mlir::ProgramCapability::id) + .def_rw("value", &mlir::ProgramCapability::value) + .def_rw("constraints", &mlir::ProgramCapability::constraints); + + nb::class_(m, "PayloadSpecification", + "Selected payload execution contract.") + .def( + "__init__", + [](mlir::PayloadSpecification& self, mlir::PayloadFormat format, + std::vector capabilities, + const bool optionalCapabilitiesKnown) { + constructFromExpected(self, mlir::PayloadSpecification::create( + std::move(format), + std::move(capabilities), + optionalCapabilitiesKnown)); + }, + "payload_format"_a, + "capabilities"_a = std::vector{}, + "optional_capabilities_known"_a = false) + .def_prop_ro( + "format", + [](const mlir::PayloadSpecification& environment) { + return environment.format(); + }, + "The exact selected payload format.") + .def_prop_ro( + "capabilities", + [](const mlir::PayloadSpecification& environment) { + return std::vector( + environment.capabilities().begin(), + environment.capabilities().end()); + }, + "The effective payload capabilities.") + .def_prop_ro("optional_capabilities_known", + &mlir::PayloadSpecification::optionalCapabilitiesKnown, + "Whether optional capability metadata is complete."); + auto compilerTarget = nb::class_( m, "CompilerTarget", R"pb(Immutable MLIR compiler target. @@ -927,6 +1006,17 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); "name"_a, "arity"_a, "num_parameters"_a = nb::none(), "sites"_a = nb::none(), "Whether the target supports an operation."); + nb::class_( + m, "TargetEnvironment", + "A compiler target and its selected payload specification.") + .def(nb::init(), + "target"_a, "payload_specification"_a) + .def_prop_ro("target", &mlir::TargetEnvironment::target, + "The compiler target.") + .def_prop_ro("payload_specification", + &mlir::TargetEnvironment::payloadSpecification, + "The selected payload specification."); + auto program = nb::class_( m, "Program", R"pb(Base class for a typed MLIR compiler program. @@ -1139,7 +1229,7 @@ operations.)pb"); "must be at least 3; default 3 means wider than two-qubit).") .def("compile_for_target", &BooleanMemberAdapter<&mlir::QCOProgram::compileForTarget>::call, - "target"_a, nb::kw_only(), "enable_timing"_a = false, + "target_environment"_a, nb::kw_only(), "enable_timing"_a = false, "enable_statistics"_a = false, "Compile this QCO program for the target in place. Do not rely on " "its contents if compilation fails.") @@ -1355,8 +1445,8 @@ contracts.)pb"); m.def("compile_program", &compileProgram, "program"_a, nb::kw_only(), "output"_a = mlir::ProgramFormat::QC, "inplace"_a = false, - "target"_a = nb::none(), "qco_pipeline"_a = "mqt-qco-default", - "enable_timing"_a = false, "enable_statistics"_a = false, + "qco_pipeline"_a = "mqt-qco-default", "enable_timing"_a = false, + "enable_statistics"_a = false, R"pb( Run the coordinated default MQT compiler pipeline. @@ -1370,16 +1460,34 @@ directly to construct a custom pipeline stage by stage. program: Source text, a file path, a Qiskit circuit, or a typed compiler program. output: The requested output stage of the compiler pipeline. inplace: Whether a typed input program may be consumed. - target: An optional compiler target for decomposition, mapping, and native - synthesis. A target requires optimized QCO, QC, or QIR output. qco_pipeline: The QCO optimization pipeline to run. A custom pipeline - cannot be combined with a target. + cannot be combined with target compilation. enable_timing: Whether to collect pass timing information. enable_statistics: Whether to collect pass statistics. Returns: A typed compiler program for the requested output format. )pb"); + + m.def("compile_program", &compileProgramForTarget, "program"_a, nb::kw_only(), + "inplace"_a = false, "target_environment"_a, "enable_timing"_a = false, + "enable_statistics"_a = false, + R"pb( +Compile a program for a target and return the selected executable payload. + +The payload specification determines the output format. Typed program inputs +are copied by default; set ``inplace=True`` to consume them. + +Args: + program: Source text, a file path, a Qiskit circuit, or a typed compiler program. + inplace: Whether a typed input program may be consumed. + target_environment: The compiler target and selected payload specification. + enable_timing: Whether to collect pass timing information. + enable_statistics: Whether to collect pass statistics. + +Returns: + A typed compiler program for the selected payload format. +)pb"); } } // namespace mqt diff --git a/bindings/patterns.txt b/bindings/patterns.txt index 46db30f7e4..36c4f84323 100644 --- a/bindings/patterns.txt +++ b/bindings/patterns.txt @@ -168,7 +168,6 @@ mqt.core.mlir.compile_program: *, output: Literal[OutputFormat.QC, OutputFormat.QC_IMPORT] = ..., inplace: bool = False, - target: CompilerTarget | None = None, qco_pipeline: str = "mqt-qco-default", enable_timing: bool = False, enable_statistics: bool = False, @@ -185,7 +184,6 @@ mqt.core.mlir.compile_program: *, output: Literal[OutputFormat.QCO, OutputFormat.QCO_OPTIMIZED], inplace: bool = False, - target: CompilerTarget | None = None, qco_pipeline: str = "mqt-qco-default", enable_timing: bool = False, enable_statistics: bool = False, @@ -218,7 +216,6 @@ mqt.core.mlir.compile_program: *, output: Literal[OutputFormat.JEFF], inplace: bool = False, - target: CompilerTarget | None = None, qco_pipeline: str = "mqt-qco-default", enable_timing: bool = False, enable_statistics: bool = False, @@ -235,7 +232,6 @@ mqt.core.mlir.compile_program: *, output: Literal[OutputFormat.QIR_BASE, OutputFormat.QIR_ADAPTIVE], inplace: bool = False, - target: CompilerTarget | None = None, qco_pipeline: str = "mqt-qco-default", enable_timing: bool = False, enable_statistics: bool = False, @@ -252,9 +248,24 @@ mqt.core.mlir.compile_program: *, output: OutputFormat, inplace: bool = False, - target: CompilerTarget | None = None, qco_pipeline: str = "mqt-qco-default", enable_timing: bool = False, enable_statistics: bool = False, ) -> QCProgram | QCOProgram | OpenQASMProgram | JeffProgram | QIRProgram: \doc + @overload + def compile_program( + program: str + | os.PathLike[str] + | qiskit.circuit.QuantumCircuit + | QCProgram + | QCOProgram + | JeffProgram + | OpenQASMProgram, + *, + inplace: bool = False, + target_environment: TargetEnvironment, + enable_timing: bool = False, + enable_statistics: bool = False, + ) -> OpenQASMProgram | QIRProgram: + \doc diff --git a/docs/glossary.md b/docs/glossary.md index c2ecdb889b..598cb84ef3 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -172,11 +172,25 @@ compiler target a compiler pipeline may use for one destination. It is a snapshot used for compilation, not a live device connection. +target environment + A compiler target paired with the selected payload specification for one + compilation. It combines hardware facts with the selected output contract. + +selected payload specification + The exact format, encoding, and effective execution capabilities selected for + a compiled program. It does not describe every format accepted by a device. + payload The program IR on which a transform, schedule, or target-specific action operates. Use a more specific term when the exact object, such as a function or circuit, matters. +execution payload + The serialized program submitted for execution. This is distinct from the + MLIR transform dialect's payload IR. A payload format identifies its + representation; execution capabilities state what that representation may + contain for the selected target. + static Known while compiling the program. Static does not necessarily mean a C++ object with static storage duration. diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index 63c0635be4..e09d0930b1 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -15,18 +15,46 @@ stored, copied cheaply, and reused for multiple compilations. Open a configured QDMI device and snapshot it as a compiler target: ```python -from mqt.core.mlir import CompilerTarget, OutputFormat, compile_program +from mqt.core.mlir import ( + CompilerTarget, + PayloadFormat, + PayloadEncoding, + PayloadSpecification, + TargetEnvironment, + compile_program, +) -target = CompilerTarget.from_device_id("mqt.sc.iqm.garnet") +target = CompilerTarget.from_device_id("mqt.ddsim.default") +payload = PayloadSpecification(PayloadFormat("qir", "2.1", "base", PayloadEncoding.BINARY)) +environment = TargetEnvironment(target, payload) compiled = compile_program( "bell.qasm", - target=target, - output=OutputFormat.QCO_OPTIMIZED, + target_environment=environment, ) ``` -Target compilation accepts optimized QCO, QC, or QIR output and uses the -canonical QCO pipeline; it cannot be combined with a custom `qco_pipeline`. +The payload specification identifies the exact representation selected for the +device. MQT Core derives the compiler output from that specification and uses +the canonical QCO pipeline. The targeted overload therefore accepts one +`TargetEnvironment` and no independent output or custom pipeline. MQT Core's +QDMI adapter does not yet translate QDMI program-format and feature metadata, so +callers must construct the payload specification from the device documentation. + +DDSIM accepts the QIR payload used here. The bundled SC devices, such as +`mqt.sc.iqm.garnet`, provide hardware models for compilation only; a model's +gate set does not imply that it accepts an executable payload. + +The example has no reported execution capabilities. A producer must add every +effective capability, including the selected format's baseline. Set +`optional_capabilities_known=True` only when the producer also knows that the +list contains every optional device capability. + +Payload versions accept one to three numeric components. A +`PayloadSpecification` fills omitted components with zero: `"2.1"` becomes +`"2.1.0"`, and `"3"` becomes `"3.0.0"`. These are exact versions, not ranges; +`"2"` means `"2.0.0"` and does not select QIR 2.1. Leading zeros, prerelease +suffixes, and version ranges are rejected. The same rules apply when reading the +typed `#mqt.payload_spec` attribute. The target can also be constructed directly. Connectivity and native-operation support are required: @@ -90,17 +118,21 @@ Target synthesis preserves a native `gphase`. If the target does not support `gphase`, target synthesis preserves relative phase effects and removes only the unobservable global phase of the entry point. -Use {py:meth}`~mqt.core.mlir.QCOProgram.compile_for_target` to apply target -compilation to an existing QCO program. Compilation runs in place. If a pass -fails, earlier passes may already have changed the program. Copy the program -before compilation if the caller must preserve the input. For pass-level -benchmarking, the C++ API exposes separate factories for pre-routing -optimization, deterministic placement, topology-aware mapping, native synthesis, -and conformance verification. Target compilation uses compact placement on -all-to-all targets and the mapper only when the target has an explicit coupling -graph. The high-level program API registers the required inliner extensions; -callers that populate the low-level target pipeline directly must register -inliner extensions for every callable dialect in their context. +Use {py:meth}`~mqt.core.mlir.QCOProgram.compile_for_target` with the target +environment to apply target compilation to an existing QCO program. Compilation +runs in place. If a pass fails, the environment and earlier pass changes remain +on the program. Copy the program before compilation if the caller must preserve +the input. The pipeline takes one `TargetEnvironment`, replaces any existing +`mqt.target_env` module attribute, and shares the prepared target with all +target passes without rebuilding its connectivity tables. The selected +environment must remain unchanged during pipeline execution. Standalone passes +decode the typed module attribute once through a cached analysis. The mapping, +native-synthesis, and conformance factories also work in textual MLIR pass +pipelines. Target compilation keeps deterministic placement on all-to-all +targets and uses mapping only for explicit topology. The high-level program API +registers the required inliner extensions; callers that populate the low-level +target pipeline directly must register inliner extensions for every callable +dialect in their context. Target compilation preserves quantum operations even when their final qubit values are not measured or returned. This supports measurement-free programs, @@ -119,20 +151,24 @@ mqt-cc --qdmi-list-devices Select a device when compiling: ```console -mqt-cc --qdmi-device=mqt.sc.iqm.garnet \ - --emit=qco-optimized input.qasm +mqt-cc --qdmi-device=mqt.ddsim.default \ + --payload-spec='#mqt.payload_spec, capabilities = [], optional_capabilities_known = false>' \ + -o output.bc input.qasm ``` An explicit registry file can be selected before device discovery: ```console mqt-cc --qdmi-config=/path/to/qdmi.json \ - --qdmi-device=example.device input.qasm + --qdmi-device=example.device \ + --payload-spec='#mqt.payload_spec, capabilities = [], optional_capabilities_known = false>' \ + input.qasm ``` -Target compilation produces optimized QCO, QC, or QIR. It cannot be combined -with a custom `--passes` pipeline because the canonical target pipeline owns the -required pass ordering. +The payload specification selects the emitted format and encoding. For targeted +QIR, the selected encoding takes precedence over the output filename extension. +Target compilation rejects `--emit` and custom `--passes` pipelines because the +target contract owns the output and required pass ordering. ## C++ source-tree API @@ -142,22 +178,35 @@ device ID and the compiler-owned target: ```cpp #include "mlir/Compiler/QDMIAdapter.h" #include "mlir/Compiler/Programs.h" +#include "mlir/Compiler/TargetEnvironment.h" #include #include -auto target = mlir::compilerTargetFromDeviceId("mqt.sc.iqm.garnet"); +auto target = mlir::compilerTargetFromDeviceId("mqt.ddsim.default"); if (!target) { llvm::errs() << "Failed to create compiler target: " << llvm::toString(target.takeError()) << '\n'; return 1; } +auto payload = mlir::PayloadSpecification::create({ + .id = "qir", + .version = "2.1", + .profile = "base", + .encoding = mlir::PayloadEncoding::Binary, +}); +if (!payload) { + llvm::errs() << llvm::toString(payload.takeError()) << '\n'; + return 1; +} +mlir::TargetEnvironment environment(*target, *payload); + auto qc = mlir::QCProgram::fromQASMFile("input.qasm"); if (!qc) { return 1; } auto qco = std::move(*qc).intoQCO(); -if (!qco || !qco->compileForTarget(*target)) { +if (!qco || !qco->compileForTarget(environment)) { return 1; } ``` diff --git a/docs/qdmi/ddsim_device.md b/docs/qdmi/ddsim_device.md index 06619f6efe..dcc2d5ed06 100644 --- a/docs/qdmi/ddsim_device.md +++ b/docs/qdmi/ddsim_device.md @@ -61,16 +61,23 @@ The compiler can snapshot the DDSIM device as an all-to-all target, compile a program to QIR, and submit the resulting bitcode to the same device: ```python -from mqt.core.mlir import CompilerTarget, OutputFormat, compile_program +from mqt.core.mlir import ( + CompilerTarget, + PayloadFormat, + PayloadEncoding, + PayloadSpecification, + TargetEnvironment, + compile_program, +) from mqt.core.qdmi import ProgramFormat from mqt.core.qdmi.driver import open_device device = open_device("mqt.ddsim.default") target = CompilerTarget.from_device(device) +payload = PayloadSpecification(PayloadFormat("qir", "2.1.0", "base", PayloadEncoding.BINARY)) program = compile_program( "bell.qasm", - target=target, - output=OutputFormat.QIR_BASE, + target_environment=TargetEnvironment(target, payload), ) job = device.submit_job( diff --git a/mlir/include/mlir/Compiler/Programs.h b/mlir/include/mlir/Compiler/Programs.h index 2ac278ccc6..c264e67b70 100644 --- a/mlir/include/mlir/Compiler/Programs.h +++ b/mlir/include/mlir/Compiler/Programs.h @@ -33,7 +33,7 @@ class QCOProgram; class JeffProgram; class OpenQASMProgram; class QIRProgram; -class CompilerTarget; +class TargetEnvironment; /** * @brief The QIR profile represented by a QIR program. @@ -281,7 +281,7 @@ class QCOProgram final : public Program { /// Compile this program for a target in place. /// /// Do not rely on the program contents if compilation fails. - [[nodiscard]] bool compileForTarget(const CompilerTarget& target, + [[nodiscard]] bool compileForTarget(const TargetEnvironment& environment, bool enableTiming = false, bool enableStatistics = false); @@ -381,8 +381,16 @@ using CompilerProgram = std::variant runDefaultPipeline(CompilerInput&& program, ProgramFormat output, - const CompilerTarget* target = nullptr, std::string_view qcoPipeline = "mqt-qco-default", bool enableTiming = false, bool enableStatistics = false); +/// Run the coordinated default compiler pipeline for a target. +/// +/// The supplied program is consumed. Call `copy()` before this function +/// when the source program must remain available for another pipeline branch. +[[nodiscard]] std::optional +runDefaultPipeline(CompilerInput&& program, + const TargetEnvironment& environment, + bool enableTiming = false, bool enableStatistics = false); + } // namespace mlir diff --git a/mlir/include/mlir/Compiler/TargetCompilation.h b/mlir/include/mlir/Compiler/TargetCompilation.h index 10d4cd6233..d80c01f8b3 100644 --- a/mlir/include/mlir/Compiler/TargetCompilation.h +++ b/mlir/include/mlir/Compiler/TargetCompilation.h @@ -12,7 +12,7 @@ namespace mlir { -class CompilerTarget; +class TargetEnvironment; class OpPassManager; /// Populate the canonical compiler-target pipeline. @@ -22,7 +22,10 @@ class OpPassManager; /// synthesizes native operations, performs a final local cleanup, and verifies /// target conformance. The context that runs this low-level pipeline must /// register inliner extensions for its callable dialects. +/// The supplied environment is authoritative: the pipeline attaches it to the +/// module and shares its prepared target with every target-dependent pass. +/// The environment must remain unchanged during pipeline execution. void populateTargetCompilationPipeline(OpPassManager& pm, - const CompilerTarget& target); + const TargetEnvironment& environment); } // namespace mlir diff --git a/mlir/include/mlir/Compiler/TargetEnvironment.h b/mlir/include/mlir/Compiler/TargetEnvironment.h new file mode 100644 index 0000000000..a5f2739cde --- /dev/null +++ b/mlir/include/mlir/Compiler/TargetEnvironment.h @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mlir/Compiler/Target.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace mlir { + +class MLIRContext; +enum class ProgramFormat : uint8_t; + +namespace mqt { +class PayloadSpecAttr; +class TargetEnvAttr; +} // namespace mqt + +/// Payload representation encoding. +enum class PayloadEncoding : uint8_t { Text, Binary }; + +/// Exact identity of one executable payload representation. +struct PayloadFormat { + std::string id; + std::string version; + std::string profile; + PayloadEncoding encoding = PayloadEncoding::Text; + + friend bool operator==(const PayloadFormat&, const PayloadFormat&) = default; +}; + +/// One typed constraint on a payload capability. +struct ProgramConstraint { + std::string id; + uint64_t value = 0; + + friend bool operator==(const ProgramConstraint&, + const ProgramConstraint&) = default; +}; + +/// One extensible payload execution capability. +struct ProgramCapability { + std::string id; + uint64_t value = 0; + std::vector constraints; + + friend bool operator==(const ProgramCapability&, + const ProgramCapability&) = default; +}; + +/// Context-free selected execution payload contract. +/// +/// Producers must include every effective capability, including +/// payload-format baselines. The knowledge bit states whether the list also +/// contains all optional device capabilities. +class PayloadSpecification { +public: + /// Create and validate a selected payload specification. + /// + /// Numeric versions accept one to three components; omitted components are + /// zero, and the stored version always uses major.minor.patch. + [[nodiscard]] static llvm::Expected + create(PayloadFormat format, std::vector capabilities = {}, + bool optionalCapabilitiesKnown = false); + + /// Reconstruct a context-free value from its typed MLIR attribute. + [[nodiscard]] static llvm::Expected + create(mqt::PayloadSpecAttr attribute); + + /// Return the exact payload format. + [[nodiscard]] const PayloadFormat& format() const noexcept; + + /// Return the compiler output selected by the payload format. + [[nodiscard]] llvm::Expected compilerOutput() const; + + /// Return effective payload capabilities in reported order. + [[nodiscard]] llvm::ArrayRef capabilities() const noexcept; + + /// Return whether optional capability metadata is complete. + [[nodiscard]] bool optionalCapabilitiesKnown() const noexcept; + + /// Materialize the selected contract as a typed MLIR attribute. + [[nodiscard]] mqt::PayloadSpecAttr materialize(MLIRContext& context) const; + +private: + PayloadSpecification(PayloadFormat format, + std::vector capabilities, + bool optionalCapabilitiesKnown); + + PayloadFormat format_; + std::vector capabilities_; + bool optionalCapabilitiesKnown_; +}; + +/// Context-free hardware target and selected payload specification. +class TargetEnvironment { +public: + TargetEnvironment(const CompilerTarget& target, PayloadSpecification payload); + + /// Reconstruct the context-free pair from its typed MLIR attribute. + [[nodiscard]] static llvm::Expected + create(mqt::TargetEnvAttr attribute); + + /// Return the compiler target. + [[nodiscard]] const CompilerTarget& target() const noexcept; + + /// Return the selected payload specification. + [[nodiscard]] const PayloadSpecification& + payloadSpecification() const noexcept; + + /// Materialize the pair as a typed MLIR attribute. + [[nodiscard]] mqt::TargetEnvAttr materialize(MLIRContext& context) const; + +private: + CompilerTarget target_; + PayloadSpecification payloadSpecification_; +}; + +/// Attach the canonical typed target environment to a module. +void attachTargetEnvironment(ModuleOp moduleOp, + const TargetEnvironment& environment); + +/// Cached, validated view of a module's canonical target environment. +class TargetEnvironmentAnalysis { +public: + explicit TargetEnvironmentAnalysis(Operation* operation); + + /// Attach and cache a prepared environment without rebuilding its target. + void initialize(const TargetEnvironment& environment); + + /// Return whether the module contains a valid target environment. + [[nodiscard]] explicit operator bool() const noexcept; + + /// Return the target environment. Requires a valid analysis. + [[nodiscard]] const TargetEnvironment& environment() const noexcept; + + /// Return the validation error, or an empty string for a valid analysis. + [[nodiscard]] llvm::StringRef error() const noexcept; + + /// Keep the cached values while the canonical attribute is unchanged. + [[nodiscard]] bool + isInvalidated(const AnalysisManager::PreservedAnalyses& analyses) const; + +private: + /// Decode IR only when no prepared environment was supplied. + void resolve() const; + + ModuleOp moduleOp_; + Attribute attribute_; + mutable std::optional environment_; + mutable std::string error_; +}; + +} // namespace mlir diff --git a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td index 6607a81fec..442d15a0bb 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td @@ -45,6 +45,8 @@ def MQTDialect : Dialect { guarantee traps. Unitary operations may retain effects such as a scoped global-phase contribution. `#mqt.compilation_target` records compiler-target facts as typed IR. + `mqt.target_env` records compiler-target facts and the selected payload + execution contract. }]; let discardableAttrs = (ins "::mlir::StringAttr":$input_name, @@ -219,4 +221,106 @@ def CompilationTargetAttr : MQTAttr<"CompilationTarget", "compilation_target"> { let genVerifyDecl = 1; } +def PayloadEncoding : I32EnumAttr<"PayloadEncoding", "Payload encoding", + [I32EnumAttrCase<"Text", 0, "text">, + I32EnumAttrCase<"Binary", 1, "binary">]> { + let cppNamespace = "::mlir::mqt"; + let genSpecializedAttr = 0; +} + +def PayloadFormatAttr : MQTAttr<"PayloadFormat", "payload_format"> { + let summary = "Exact payload identity"; + let description = [{ + Identifies a payload by format ID, exact version, optional profile ID, and + encoding. Every field takes part in identity. An empty profile denotes a + payload without a named profile. + + The following identity selects binary QIR 2.1 with the base profile: + + ```mlir + #mqt.payload_format + ``` + }]; + let parameters = (ins "StringAttr":$id, "StringAttr":$version, + "StringAttr":$profile, EnumParameter:$encoding); + let assemblyFormat = "`<` struct(params) `>`"; + let genVerifyDecl = 1; +} + +def ProgramConstraintAttr : MQTAttr<"ProgramConstraint", "program_constraint"> { + let summary = "One constraint on a payload capability"; + let description = [{ + Records one extensible constraint ID and its constraint-specific value. + }]; + let parameters = (ins "StringAttr":$id, "uint64_t":$value); + let assemblyFormat = "`<` struct(params) `>`"; + let genVerifyDecl = 1; +} + +def ProgramCapabilityAttr : MQTAttr<"ProgramCapability", "program_capability"> { + let summary = "One payload execution capability"; + let description = [{ + Records one extensible capability ID, its feature-specific value, and all + constraints on that capability. Unknown IDs remain valid so newer + producers can round-trip through older consumers. + }]; + let parameters = (ins "StringAttr":$id, "uint64_t":$value, + MQTArrayRefParameter<"ProgramConstraintAttr">:$constraints); + let assemblyFormat = "`<` struct(params) `>`"; + let genVerifyDecl = 1; +} + +def PayloadSpecAttr : MQTAttr<"PayloadSpec", "payload_spec"> { + let summary = "Selected payload execution contract"; + let description = [{ + Records the exact payload and its effective capabilities. Producers expand + format baselines before creating this attribute. The capability list + remains available when optional capability metadata is unknown; + `optional_capabilities_known` records whether that optional metadata is + complete. + + The following producer-defined format reports bounded forward branching. + Its optional capability metadata is incomplete: + + ```mlir + #mqt.payload_spec< + format = , + capabilities = []>], + optional_capabilities_known = false> + ``` + }]; + let parameters = (ins "PayloadFormatAttr":$format, + MQTArrayRefParameter<"ProgramCapabilityAttr">:$capabilities, + "bool":$optional_capabilities_known); + let assemblyFormat = "`<` struct(params) `>`"; + let genVerifyDecl = 1; +} + +def TargetEnvAttr : MQTAttr<"TargetEnv", "target_env"> { + let summary = "Selected compilation and payload specification"; + let description = [{ + Combines typed compiler-target facts with the selected payload contract. + + The following environment pairs a one-site target with a producer-defined + payload: + + ```mlir + #mqt.target_env< + compilation_target = #mqt.compilation_target< + sites = [], connectivity = all_to_all, couplings = [], + native_operations = unrestricted, operations = []>, + payload_specification = #mqt.payload_spec< + format = , + capabilities = [], optional_capabilities_known = false>> + ``` + }]; + let parameters = (ins "CompilationTargetAttr":$compilation_target, + "PayloadSpecAttr":$payload_specification); + let assemblyFormat = "`<` struct(params) `>`"; +} + #endif // MLIR_DIALECT_MQT_IR_MQTDIALECT_TD diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h b/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h index 6ef068926a..3a4fd328df 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h @@ -25,9 +25,5 @@ namespace qco { /// Create a deterministic placement pass for a compiler target. std::unique_ptr createPlacementPass(const CompilerTarget& target); -/// Create a mapping pass for a compiler target with explicit topology. -std::unique_ptr createMappingPass(const CompilerTarget& target, - MappingPassOptions options); - } // namespace qco } // namespace mlir diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h index 25eb69810f..c9c5e569eb 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h @@ -47,15 +47,4 @@ namespace mlir::qco { createDecomposeMultiControlled(const CompilerTarget& target, uint64_t minQubits = 3); -/// Create post-routing synthesis for one immutable compiler target. -/// Each qubit must have a known static site. Structured branch exits must agree -/// on sites and loop backedges must preserve their entry sites. -/// The input may be modified on failure. -[[nodiscard]] std::unique_ptr -createTargetNativeSynthesis(const CompilerTarget& target); - -/// Create the final mapped-operation verifier, requiring known static sites. -[[nodiscard]] std::unique_ptr -createVerifyTargetConformance(const CompilerTarget& target); - } // namespace mlir::qco diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 86b538f5da..85e88d0cb6 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -183,6 +183,35 @@ def MappingPass : Pass<"place-and-route", "mlir::ModuleOp"> { "The number of inserted SWAPs">]; } +def TargetNativeSynthesis : Pass<"target-native-synthesis", "mlir::ModuleOp"> { + let dependentDialects = ["mlir::qco::QCODialect", + "::mlir::arith::ArithDialect", + "::mlir::math::MathDialect"]; + let summary = "Synthesize operations for the native target basis"; + let description = [{ + Reads the typed `mqt.target_env` module attribute and lowers non-native + unitary operations to one complete synthesis basis supported throughout the + compiler target. Unknown native-operation metadata is valid when no + surviving unitary operation needs it. Otherwise, the pass fails when the + metadata is unknown, no complete basis exists, or an operation has no + compile-time unitary matrix and cannot be synthesized as a parameterized + single-qubit gate. + }]; +} + +def VerifyTargetConformance + : Pass<"verify-target-conformance", "mlir::ModuleOp"> { + let dependentDialects = ["mlir::qco::QCODialect"]; + let summary = "Verify that a mapped program conforms to its compiler target"; + let description = [{ + Reads the typed `mqt.target_env` module attribute and verifies that every + qubit is assigned to a target site and every remaining quantum operation is + native for the compiler target. Unknown native-operation metadata is valid + when the surviving program contains no unitary, measurement, or reset + operation that needs the metadata. + }]; +} + //===----------------------------------------------------------------------===// // Optimization Passes //===----------------------------------------------------------------------===// diff --git a/mlir/lib/Compiler/CMakeLists.txt b/mlir/lib/Compiler/CMakeLists.txt index bd592f2b5b..d29747cd43 100644 --- a/mlir/lib/Compiler/CMakeLists.txt +++ b/mlir/lib/Compiler/CMakeLists.txt @@ -11,18 +11,27 @@ add_mlir_library( MQTCompilerTarget PARTIAL_SOURCES_INTENDED Target.cpp + TargetEnvironment.cpp ADDITIONAL_HEADER_DIRS ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler LINK_LIBS PUBLIC MLIRIR MLIRMQTDialect + MLIRPass MLIRQCODialect) mqt_mlir_target_use_project_options(MQTCompilerTarget) -target_sources(MQTCompilerTarget PUBLIC FILE_SET HEADERS BASE_DIRS ${MQT_MLIR_SOURCE_INCLUDE_DIR} - FILES ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler/Target.h) +target_sources( + MQTCompilerTarget + PUBLIC FILE_SET + HEADERS + BASE_DIRS + ${MQT_MLIR_SOURCE_INCLUDE_DIR} + FILES + ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler/Target.h + ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler/TargetEnvironment.h) # Build the optional QDMI-to-compiler-target adapter set(LLVM_REQUIRES_EH ON) diff --git a/mlir/lib/Compiler/Pipeline.cpp b/mlir/lib/Compiler/Pipeline.cpp index a44694f72e..b1bba6d82c 100644 --- a/mlir/lib/Compiler/Pipeline.cpp +++ b/mlir/lib/Compiler/Pipeline.cpp @@ -10,6 +10,7 @@ #include "mlir/Compiler/Programs.h" #include "mlir/Compiler/TargetCompilation.h" +#include "mlir/Compiler/TargetEnvironment.h" #include "mlir/Conversion/JeffToQCO/JeffToQCO.h" #include "mlir/Conversion/QCOToJeff/QCOToJeff.h" #include "mlir/Conversion/QCOToQC/QCOToQC.h" @@ -33,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -226,12 +228,12 @@ bool QCOProgram::decomposeMultiControlled(uint64_t minQubits) { "failed to decompose multi-controlled gates")); } -bool QCOProgram::compileForTarget(const CompilerTarget& target, +bool QCOProgram::compileForTarget(const TargetEnvironment& environment, bool enableTiming, bool enableStatistics) { return succeeded(runQCOTransformPasses( mod(), - [&target](OpPassManager& pm) { - populateTargetCompilationPipeline(pm, target); + [&environment](OpPassManager& pm) { + populateTargetCompilationPipeline(pm, environment); }, "failed to compile the QCO program for the target", enableTiming, enableStatistics)); @@ -418,23 +420,11 @@ bool QIRProgram::writeBitcode(const std::filesystem::path& path) const { // Pipeline //===----------------------------------------------------------------------===// -std::optional -runDefaultPipeline(CompilerInput&& program, ProgramFormat output, - const CompilerTarget* target, std::string_view qcoPipeline, - bool enableTiming, bool enableStatistics) { - if (target != nullptr && - (output == ProgramFormat::QCImport || output == ProgramFormat::QCO || - output == ProgramFormat::Jeff)) { - llvm::errs() - << "a compiler target requires QCOOptimized, QC, OpenQASM3, or QIR " - "output.\n"; - return std::nullopt; - } - if (target != nullptr && qcoPipeline != "mqt-qco-default") { - llvm::errs() << "a custom QCO pass pipeline cannot be combined with a " - "compiler target.\n"; - return std::nullopt; - } +[[nodiscard]] static std::optional +runDefaultPipelineImpl(CompilerInput&& program, ProgramFormat output, + const TargetEnvironment* environment, + std::string_view qcoPipeline, bool enableTiming, + bool enableStatistics) { if ((output == ProgramFormat::QCImport || output == ProgramFormat::QCO) && qcoPipeline != "mqt-qco-default") { llvm::errs() << "a custom QCO pass pipeline cannot be used with an output " @@ -482,7 +472,7 @@ runDefaultPipeline(CompilerInput&& program, ProgramFormat output, return CompilerProgram(std::move(*qco)); } - if (target == nullptr && + if (environment == nullptr && (output == ProgramFormat::QIRBase || output == ProgramFormat::QIRAdaptive) && failed(runQCOTransformPasses( @@ -492,8 +482,8 @@ runDefaultPipeline(CompilerInput&& program, ProgramFormat output, return std::nullopt; } - if (target != nullptr) { - if (!qco->compileForTarget(*target, enableTiming, enableStatistics)) { + if (environment != nullptr) { + if (!qco->compileForTarget(*environment, enableTiming, enableStatistics)) { return std::nullopt; } } else { @@ -532,4 +522,27 @@ runDefaultPipeline(CompilerInput&& program, ProgramFormat output, return std::move(*qc).intoQIR(profile); } +std::optional runDefaultPipeline(CompilerInput&& program, + ProgramFormat output, + std::string_view qcoPipeline, + bool enableTiming, + bool enableStatistics) { + return runDefaultPipelineImpl(std::move(program), output, nullptr, + qcoPipeline, enableTiming, enableStatistics); +} + +std::optional +runDefaultPipeline(CompilerInput&& program, + const TargetEnvironment& environment, bool enableTiming, + bool enableStatistics) { + auto output = environment.payloadSpecification().compilerOutput(); + if (!output) { + llvm::errs() << llvm::toString(output.takeError()) << '\n'; + return std::nullopt; + } + return runDefaultPipelineImpl(std::move(program), *output, &environment, + "mqt-qco-default", enableTiming, + enableStatistics); +} + } // namespace mlir diff --git a/mlir/lib/Compiler/TargetCompilation.cpp b/mlir/lib/Compiler/TargetCompilation.cpp index 1895da149d..50b108ed9d 100644 --- a/mlir/lib/Compiler/TargetCompilation.cpp +++ b/mlir/lib/Compiler/TargetCompilation.cpp @@ -11,17 +11,46 @@ #include "mlir/Compiler/TargetCompilation.h" #include "mlir/Compiler/Target.h" +#include "mlir/Compiler/TargetEnvironment.h" #include "mlir/Dialect/QCO/Transforms/Mapping/Mapping.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Support/Passes.h" +#include #include #include +#include +#include + namespace mlir { +namespace { + +class InitializeTargetEnvironmentPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(InitializeTargetEnvironmentPass) + + explicit InitializeTargetEnvironmentPass(TargetEnvironment environment) + : environment_(std::move(environment)) {} + +protected: + void runOnOperation() override { + getAnalysis().initialize(environment_); + markAnalysesPreserved(); + } + +private: + TargetEnvironment environment_; +}; + +} /* namespace */ void populateTargetCompilationPipeline(OpPassManager& pm, - const CompilerTarget& target) { + const TargetEnvironment& environment) { + pm.addPass(std::make_unique(environment)); + const auto& target = environment.target(); pm.addPass(createInlinerPass()); populateQCOCleanupPipeline(pm); pm.addPass(qco::createDecomposeMultiControlled(target)); @@ -29,16 +58,16 @@ void populateTargetCompilationPipeline(OpPassManager& pm, pm.addPass(qco::createFuseTwoQubitGates()); switch (target.connectivityKind()) { case CompilerTarget::Connectivity::Kind::Explicit: - pm.addPass(qco::createMappingPass(target, qco::MappingPassOptions{})); + pm.addPass(qco::createMappingPass(qco::MappingPassOptions{})); break; case CompilerTarget::Connectivity::Kind::AllToAll: pm.addPass(qco::createPlacementPass(target)); break; } populateQCOCleanupPipeline(pm); - pm.addPass(qco::createTargetNativeSynthesis(target)); + pm.addPass(qco::createTargetNativeSynthesis()); pm.addPass(createCSEPass()); - pm.addPass(qco::createVerifyTargetConformance(target)); + pm.addPass(qco::createVerifyTargetConformance()); } } // namespace mlir diff --git a/mlir/lib/Compiler/TargetEnvironment.cpp b/mlir/lib/Compiler/TargetEnvironment.cpp new file mode 100644 index 0000000000..2d7918eef0 --- /dev/null +++ b/mlir/lib/Compiler/TargetEnvironment.cpp @@ -0,0 +1,324 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Compiler/TargetEnvironment.h" + +#include "mlir/Compiler/Programs.h" +#include "mlir/Compiler/Target.h" +#include "mlir/Dialect/MQT/IR/MQTAttributes.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace mlir { + +[[nodiscard]] static llvm::Error invalidPayload(const llvm::Twine& message) { + return llvm::createStringError(llvm::errc::invalid_argument, + "Invalid payload specification: " + message); +} + +[[nodiscard]] static std::optional +normalizePayloadVersion(llvm::StringRef version) { + llvm::VersionTuple parsed; + if (parsed.tryParse(version) || parsed.getBuild() || + parsed.getAsString() != version) { + return std::nullopt; + } + return llvm::VersionTuple(parsed.getMajor(), parsed.getMinor().value_or(0), + parsed.getSubminor().value_or(0)) + .getAsString(); +} + +[[nodiscard]] static bool containsNull(const llvm::StringRef value) { + return value.contains('\0'); +} + +llvm::Expected +PayloadSpecification::create(PayloadFormat format, + std::vector capabilities, + const bool optionalCapabilitiesKnown) { + if (format.id.empty() || format.version.empty()) { + return invalidPayload("Payload format requires an ID and version"); + } + if (containsNull(format.id) || containsNull(format.version) || + containsNull(format.profile)) { + return invalidPayload( + "Payload format fields must not contain null characters"); + } + auto version = normalizePayloadVersion(format.version); + if (!version) { + return invalidPayload( + "Payload format version must use major[.minor[.patch]]"); + } + format.version = std::move(*version); + switch (format.encoding) { + case PayloadEncoding::Text: + case PayloadEncoding::Binary: + break; + default: + return invalidPayload("Payload format encoding is invalid"); + } + + llvm::SmallDenseSet> seenCapabilities; + seenCapabilities.reserve(capabilities.size()); + for (const ProgramCapability& capability : capabilities) { + if (capability.id.empty()) { + return invalidPayload("Program capability ID must not be empty"); + } + if (containsNull(capability.id)) { + return invalidPayload( + "Program capability ID must not contain a null character"); + } + const auto capabilityKey = + std::pair(llvm::StringRef(capability.id), capability.value); + if (!seenCapabilities.insert(capabilityKey).second) { + return invalidPayload("Payload specification contains a duplicate " + "capability ID/value pair"); + } + + llvm::SmallDenseSet seenConstraints; + seenConstraints.reserve(capability.constraints.size()); + for (const ProgramConstraint& constraint : capability.constraints) { + if (constraint.id.empty()) { + return invalidPayload("Program constraint ID must not be empty"); + } + if (containsNull(constraint.id)) { + return invalidPayload( + "Program constraint ID must not contain a null character"); + } + if (!seenConstraints.insert(constraint.id).second) { + return invalidPayload( + "Program capability contains a duplicate constraint ID"); + } + } + } + + return PayloadSpecification(std::move(format), std::move(capabilities), + optionalCapabilitiesKnown); +} + +llvm::Expected +PayloadSpecification::create(const mqt::PayloadSpecAttr attribute) { + if (!attribute) { + return invalidPayload("Payload specification attribute must not be null"); + } + const auto formatAttr = attribute.getFormat(); + PayloadEncoding encoding = PayloadEncoding::Text; + switch (formatAttr.getEncoding()) { + case mqt::PayloadEncoding::Text: + encoding = PayloadEncoding::Text; + break; + case mqt::PayloadEncoding::Binary: + encoding = PayloadEncoding::Binary; + break; + default: + return invalidPayload("Payload format encoding is invalid"); + } + PayloadFormat format{ + .id = formatAttr.getId().getValue().str(), + .version = formatAttr.getVersion().getValue().str(), + .profile = formatAttr.getProfile().getValue().str(), + .encoding = encoding, + }; + + std::vector capabilities; + capabilities.reserve(attribute.getCapabilities().size()); + for (const mqt::ProgramCapabilityAttr capabilityAttr : + attribute.getCapabilities()) { + std::vector constraints; + constraints.reserve(capabilityAttr.getConstraints().size()); + for (const mqt::ProgramConstraintAttr constraintAttr : + capabilityAttr.getConstraints()) { + constraints.emplace_back(constraintAttr.getId().getValue().str(), + constraintAttr.getValue()); + } + capabilities.push_back({ + .id = capabilityAttr.getId().getValue().str(), + .value = capabilityAttr.getValue(), + .constraints = std::move(constraints), + }); + } + return create(std::move(format), std::move(capabilities), + attribute.getOptionalCapabilitiesKnown()); +} + +PayloadSpecification::PayloadSpecification( + PayloadFormat format, std::vector capabilities, + const bool optionalCapabilitiesKnown) + : format_(std::move(format)), capabilities_(std::move(capabilities)), + optionalCapabilitiesKnown_(optionalCapabilitiesKnown) {} + +const PayloadFormat& PayloadSpecification::format() const noexcept { + return format_; +} + +llvm::Expected PayloadSpecification::compilerOutput() const { + if (format_.id == "openqasm" && format_.version == "3.0.0" && + format_.profile.empty() && format_.encoding == PayloadEncoding::Text) { + return ProgramFormat::OpenQASM3; + } + if (format_.id == "qir" && format_.version == "2.1.0") { + if (format_.profile == "base") { + return ProgramFormat::QIRBase; + } + if (format_.profile == "adaptive") { + return ProgramFormat::QIRAdaptive; + } + } + return invalidPayload("MQT Compiler cannot emit the selected payload format"); +} + +llvm::ArrayRef +PayloadSpecification::capabilities() const noexcept { + return capabilities_; +} + +bool PayloadSpecification::optionalCapabilitiesKnown() const noexcept { + return optionalCapabilitiesKnown_; +} + +mqt::PayloadSpecAttr +PayloadSpecification::materialize(MLIRContext& context) const { + const auto format = mqt::PayloadFormatAttr::get( + &context, StringAttr::get(&context, format_.id), + StringAttr::get(&context, format_.version), + StringAttr::get(&context, format_.profile), + format_.encoding == PayloadEncoding::Binary ? mqt::PayloadEncoding::Binary + : mqt::PayloadEncoding::Text); + + llvm::SmallVector capabilities; + capabilities.reserve(capabilities_.size()); + for (const ProgramCapability& capability : capabilities_) { + llvm::SmallVector constraints; + constraints.reserve(capability.constraints.size()); + for (const ProgramConstraint& constraint : capability.constraints) { + constraints.emplace_back(mqt::ProgramConstraintAttr::get( + &context, StringAttr::get(&context, constraint.id), + constraint.value)); + } + capabilities.emplace_back(mqt::ProgramCapabilityAttr::get( + &context, StringAttr::get(&context, capability.id), capability.value, + constraints)); + } + return mqt::PayloadSpecAttr::get(&context, format, capabilities, + optionalCapabilitiesKnown_); +} + +TargetEnvironment::TargetEnvironment(const CompilerTarget& target, + PayloadSpecification payload) + : target_(target), payloadSpecification_(std::move(payload)) {} + +llvm::Expected +TargetEnvironment::create(const mqt::TargetEnvAttr attribute) { + if (!attribute) { + return llvm::createStringError(llvm::errc::invalid_argument, + "Target environment must not be null"); + } + auto target = CompilerTarget::create(attribute.getCompilationTarget()); + if (!target) { + return target.takeError(); + } + auto payload = + PayloadSpecification::create(attribute.getPayloadSpecification()); + if (!payload) { + return payload.takeError(); + } + return TargetEnvironment(*target, std::move(*payload)); +} + +const CompilerTarget& TargetEnvironment::target() const noexcept { + return target_; +} + +const PayloadSpecification& +TargetEnvironment::payloadSpecification() const noexcept { + return payloadSpecification_; +} + +mqt::TargetEnvAttr TargetEnvironment::materialize(MLIRContext& context) const { + return mqt::TargetEnvAttr::get(&context, target_.materialize(context), + payloadSpecification_.materialize(context)); +} + +void attachTargetEnvironment(ModuleOp moduleOp, + const TargetEnvironment& environment) { + MLIRContext& context = *moduleOp.getContext(); + moduleOp->setAttr(mqt::TargetEnvAttr::name, environment.materialize(context)); +} + +TargetEnvironmentAnalysis::TargetEnvironmentAnalysis(Operation* operation) + : moduleOp_(cast(operation)), + attribute_(moduleOp_->getAttrOfType( + mqt::TargetEnvAttr::name)) {} + +void TargetEnvironmentAnalysis::initialize( + const TargetEnvironment& environment) { + attachTargetEnvironment(moduleOp_, environment); + attribute_ = moduleOp_->getAttr(mqt::TargetEnvAttr::name); + environment_ = environment; + error_.clear(); +} + +void TargetEnvironmentAnalysis::resolve() const { + if (environment_ || !error_.empty()) { + return; + } + if (!attribute_) { + error_ = "module does not contain mqt.target_env"; + return; + } + auto environment = + TargetEnvironment::create(llvm::cast(attribute_)); + if (!environment) { + error_ = llvm::toString(environment.takeError()); + return; + } + environment_.emplace(std::move(*environment)); +} + +TargetEnvironmentAnalysis::operator bool() const noexcept { + resolve(); + return environment_.has_value(); +} + +const TargetEnvironment& +TargetEnvironmentAnalysis::environment() const noexcept { + resolve(); + assert(environment_.has_value()); + return *environment_; +} + +llvm::StringRef TargetEnvironmentAnalysis::error() const noexcept { + resolve(); + return error_; +} + +bool TargetEnvironmentAnalysis::isInvalidated( + const AnalysisManager::PreservedAnalyses& /*analyses*/) const { + return moduleOp_->getAttr(mqt::TargetEnvAttr::name) != attribute_; +} + +} // namespace mlir diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index 804575a7df..7fd6c7eaad 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -26,6 +26,7 @@ #include #include // IWYU pragma: keep #include +#include #include #include #include @@ -65,6 +66,88 @@ void MQTDialect::initialize() { #define GET_ATTRDEF_CLASSES #include "mlir/Dialect/MQT/IR/MQTAttributes.cpp.inc" +[[nodiscard]] static bool isCanonicalPayloadVersion(const StringRef version) { + llvm::VersionTuple parsed; + return !parsed.tryParse(version) && !parsed.getBuild() && + parsed.getAsString() == version; +} + +LogicalResult +PayloadFormatAttr::verify(const function_ref emitError, + const StringAttr id, const StringAttr version, + const StringAttr profile, + const PayloadEncoding /*encoding*/) { + if (id.getValue().empty() || version.getValue().empty()) { + return emitError() << "payload format requires an ID and version"; + } + if (id.getValue().contains('\0') || version.getValue().contains('\0') || + profile.getValue().contains('\0')) { + return emitError() + << "payload format fields must not contain null characters"; + } + if (!isCanonicalPayloadVersion(version.getValue())) { + return emitError() + << "payload format version must use major[.minor[.patch]]"; + } + return success(); +} + +LogicalResult ProgramConstraintAttr::verify( + const function_ref emitError, const StringAttr id, + const uint64_t /*value*/) { + if (id.getValue().empty()) { + return emitError() << "program constraint ID must not be empty"; + } + if (id.getValue().contains('\0')) { + return emitError() << "program constraint ID must not contain a null " + "character"; + } + return success(); +} + +LogicalResult ProgramCapabilityAttr::verify( + const function_ref emitError, const StringAttr id, + const uint64_t /*value*/, + const ArrayRef constraints) { + if (id.getValue().empty()) { + return emitError() << "program capability ID must not be empty"; + } + if (id.getValue().contains('\0')) { + return emitError() + << "program capability ID must not contain a null character"; + } + + llvm::SmallDenseSet seen; + seen.reserve(constraints.size()); + for (const ProgramConstraintAttr constraint : constraints) { + if (!seen.insert(constraint.getId().getValue()).second) { + return emitError() << "program capability contains duplicate constraint '" + << constraint.getId().getValue() << "'"; + } + } + return success(); +} + +LogicalResult +PayloadSpecAttr::verify(const function_ref emitError, + const PayloadFormatAttr /*format*/, + const ArrayRef capabilities, + const bool /*optionalCapabilitiesKnown*/) { + llvm::SmallDenseSet> seen; + seen.reserve(capabilities.size()); + for (const ProgramCapabilityAttr capability : capabilities) { + const auto key = + std::pair(capability.getId().getValue(), capability.getValue()); + if (!seen.insert(key).second) { + return emitError() + << "payload specification contains duplicate capability '" + << capability.getId().getValue() << "' with value " + << capability.getValue(); + } + } + return success(); +} + LogicalResult DurationUnitAttr::verify(const function_ref emitError, const StringAttr unit, const FloatAttr scaleFactor) { @@ -630,6 +713,19 @@ verifyRegisterName(Operation* operation, const NamedAttribute attribute) { LogicalResult MQTDialect::verifyOperationAttribute(Operation* operation, const NamedAttribute attribute) { + if (attribute.getName() == TargetEnvAttr::name) { + if (!isa(operation)) { + return operation->emitError() + << "attribute '" << attribute.getName().getValue() + << "' is only valid on a module"; + } + if (!isa(attribute.getValue())) { + return operation->emitError() + << "attribute '" << attribute.getName().getValue() + << "' must be an mqt target environment"; + } + return success(); + } if (attribute.getName() == EntryPointAttrHelper::getNameStr()) { return verifyEntryPoint(operation, attribute); } diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 72ddfb0aa3..b243345472 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -11,10 +11,12 @@ #include "mlir/Dialect/QCO/Transforms/Mapping/Mapping.h" #include "mlir/Compiler/Target.h" +#include "mlir/Compiler/TargetEnvironment.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" +#include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/Drivers.h" #include "mlir/Dialect/QCO/Utils/Graph.h" #include "mlir/Dialect/QCO/Utils/Layout.h" @@ -45,7 +47,6 @@ #include #include #include -#include #include #include @@ -564,22 +565,23 @@ struct MappingPass : impl::MappingPassBase { explicit MappingPass(const MappingPassOptions& options) : MappingPassBase(options) {} - /// Construct mapping for a compiler target. - explicit MappingPass(const CompilerTarget& compilerTarget, - const MappingPassOptions& options) - : MappingPassBase(options), target(compilerTarget) {} - protected: void runOnOperation() override { assert(alpha > 0 && "expected alpha > 0"); assert(niterations > 0 && "expected niterations > 0"); assert(ntrials > 0 && "expected ntrials > 0"); - if (!target) { - llvm::reportFatalUsageError("No compiler target specified!"); + auto moduleOp = getOperation(); + const auto& environment = getAnalysis(); + if (!environment) { + moduleOp.emitError() + << "place-and-route requires a valid mqt.target_env: " + << environment.error(); + signalPassFailure(); + return; } + target = &environment.environment().target(); - auto moduleOp = getOperation(); if (target->connectivityKind() != CompilerTarget::Connectivity::Kind::Explicit) { moduleOp.emitError() @@ -1732,7 +1734,7 @@ struct MappingPass : impl::MappingPassBase { return stats; } - std::optional target; + const CompilerTarget* target = nullptr; }; } // namespace @@ -1741,9 +1743,4 @@ std::unique_ptr createPlacementPass(const CompilerTarget& target) { return std::make_unique(target); } -std::unique_ptr createMappingPass(const CompilerTarget& target, - MappingPassOptions options) { - return std::make_unique(target, options); -} - } // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index 92a5fdc6e9..a10c7a4c2c 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -9,6 +9,7 @@ */ #include "mlir/Compiler/Target.h" +#include "mlir/Compiler/TargetEnvironment.h" #include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Transforms/GlobalPhaseNormalization.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" @@ -56,6 +57,10 @@ namespace mlir::qco { using decomposition::decomposeUnitary2QWeyl; using decomposition::emitUnitary2QWeyl; +#define GEN_PASS_DEF_TARGETNATIVESYNTHESIS +#define GEN_PASS_DEF_VERIFYTARGETCONFORMANCE +#include "mlir/Dialect/QCO/Transforms/Passes.h.inc" + namespace { /// Composed unitary and metadata for a fusable two-qubit run. @@ -581,23 +586,24 @@ struct FuseTwoQubitGatesPass final }; struct TargetNativeSynthesisPass final - : PassWrapper> { - MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TargetNativeSynthesisPass) - - explicit TargetNativeSynthesisPass(const CompilerTarget& targetIn) - : target(targetIn) {} - - void getDependentDialects(DialectRegistry& registry) const override { - registry.insert(); - } + : impl::TargetNativeSynthesisBase { protected: void runOnOperation() override { + ModuleOp moduleOp = getOperation(); + const auto& environment = getAnalysis(); + if (!environment) { + moduleOp.emitError() + << "target-native synthesis requires a valid mqt.target_env: " + << environment.error(); + signalPassFailure(); + return; + } + const CompilerTarget& target = environment.environment().target(); if (target.nativeOperationsKind() == CompilerTarget::NativeOperations::Kind::Unrestricted) { return; } - ModuleOp moduleOp = getOperation(); const auto targetBasis = target.synthesisBasis(); if (failed(prepareGlobalPhases(moduleOp, target))) { signalPassFailure(); @@ -633,25 +639,29 @@ struct TargetNativeSynthesisPass final signalPassFailure(); } } - - CompilerTarget target; }; struct VerifyTargetConformancePass final - : PassWrapper> { - MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(VerifyTargetConformancePass) - - explicit VerifyTargetConformancePass(const CompilerTarget& targetIn) - : target(targetIn) {} + : impl::VerifyTargetConformanceBase { protected: void runOnOperation() override { - auto sites = collectStaticSites(getOperation()); + ModuleOp moduleOp = getOperation(); + const auto& environment = getAnalysis(); + if (!environment) { + moduleOp.emitError() + << "target conformance requires a valid mqt.target_env: " + << environment.error(); + signalPassFailure(); + return; + } + const CompilerTarget& target = environment.environment().target(); + auto sites = collectStaticSites(moduleOp); if (failed(sites)) { signalPassFailure(); return; } - WalkResult result = getOperation()->walk([&](Operation* operation) { + WalkResult result = moduleOp->walk([&](Operation* operation) { if (auto staticOp = dyn_cast(operation)) { const auto site = static_cast(staticOp.getIndex()); @@ -689,8 +699,6 @@ struct VerifyTargetConformancePass final signalPassFailure(); } } - - CompilerTarget target; }; } // namespace @@ -699,14 +707,4 @@ std::unique_ptr createFuseTwoQubitGates() { return std::make_unique(); } -std::unique_ptr -createTargetNativeSynthesis(const CompilerTarget& target) { - return std::make_unique(target); -} - -std::unique_ptr -createVerifyTargetConformance(const CompilerTarget& target) { - return std::make_unique(target); -} - } // namespace mlir::qco diff --git a/mlir/lib/Support/Passes.cpp b/mlir/lib/Support/Passes.cpp index 76c78d8a33..7bf7cbab8a 100644 --- a/mlir/lib/Support/Passes.cpp +++ b/mlir/lib/Support/Passes.cpp @@ -57,10 +57,13 @@ void registerMQTCompilerPasses() { qco::registerMeasurementLifting(); qco::registerMergeSingleQubitRotationGates(); qco::registerPauliTwirl2QGates(); + qco::registerMappingPass(); qco::registerQuantumLoopUnroll(); qco::registerRemoveDeadGates(); qco::registerReplaceClassicalControls(); qco::registerReuseQubits(); + qco::registerTargetNativeSynthesis(); + qco::registerVerifyTargetConformance(); mqt::registerNormalizeGlobalPhases(); mqt::registerUnrollModifiers(); PassPipelineRegistration<>("mqt-qco-default", diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index 213c254c8e..68d9871060 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -8,8 +8,11 @@ * Licensed under the MIT License */ +#include "mlir/Compiler/Programs.h" #include "mlir/Compiler/QDMIAdapter.h" +#include "mlir/Compiler/Target.h" #include "mlir/Compiler/TargetCompilation.h" +#include "mlir/Compiler/TargetEnvironment.h" #include "mlir/Conversion/JeffToQCO/JeffToQCO.h" #include "mlir/Conversion/QCOToJeff/QCOToJeff.h" #include "mlir/Conversion/QCOToQC/QCOToQC.h" @@ -17,6 +20,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/MQTAttributes.h" #include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/MQT/Transforms/Passes.h" #include "mlir/Dialect/QC/IR/QCDialect.h" @@ -43,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -88,12 +93,12 @@ static llvm::cl::opt inputFormat( llvm::cl::desc("Input format: auto, jeff, mlir, or qasm (default: auto)"), llvm::cl::value_desc("format"), llvm::cl::init("auto")); -static llvm::cl::opt - outputFilename("o", - llvm::cl::desc("Output filename (for QIR, - and .ll write " - "textual LLVM IR; .bc and other names write " - "LLVM bitcode)"), - llvm::cl::value_desc("filename"), llvm::cl::init("-")); +static llvm::cl::opt outputFilename( + "o", + llvm::cl::desc("Output filename (for untargeted QIR, - and .ll write " + "textual LLVM IR; .bc and other names write LLVM " + "bitcode)"), + llvm::cl::value_desc("filename"), llvm::cl::init("-")); static llvm::cl::opt outputFormat( "emit", @@ -112,6 +117,11 @@ static llvm::cl::opt qdmiDevice( llvm::cl::desc("Compile for the QDMI device with this stable ID"), llvm::cl::value_desc("id"), llvm::cl::init("")); +static llvm::cl::opt payloadSpecification( + "payload-spec", + llvm::cl::desc("Selected payload as a typed #mqt.payload_spec attribute"), + llvm::cl::value_desc("attribute"), llvm::cl::init("")); + static llvm::cl::opt qdmiConfig( "qdmi-config", llvm::cl::desc("Use an explicit QDMI registry configuration file"), @@ -340,7 +350,9 @@ static LogicalResult writeJeffOutput(ModuleOp mod, const StringRef filename) { * @brief Write a module to an output file. */ template -static LogicalResult writeOutput(ModuleType mod, StringRef filename) { +static LogicalResult +writeOutput(ModuleType mod, StringRef filename, + const std::optional qirEncoding = std::nullopt) { std::string errorMessage; const auto output = openOutputFile(filename, &errorMessage); if (!output) { @@ -355,7 +367,11 @@ static LogicalResult writeOutput(ModuleType mod, StringRef filename) { writeBytecodeToFile(mod, output->os()); } } else if constexpr (std::is_same_v) { - if (filename == "-" || llvm::sys::path::extension(filename) == ".ll") { + const auto writeText = + qirEncoding + ? *qirEncoding == PayloadEncoding::Text + : filename == "-" || llvm::sys::path::extension(filename) == ".ll"; + if (writeText) { mod->print(output->os(), nullptr); } else { llvm::WriteBitcodeToFile(*mod, output->os()); @@ -393,6 +409,15 @@ static int runCompiler(int argc, char** argv) { qdmiListDevices && !qdmiDevice.empty(), "--qdmi-list-devices cannot be combined with --qdmi-device.") .failed() || + reportQDMIErrorIf( + qdmiDevice.empty() != payloadSpecification.empty(), + "--qdmi-device and --payload-spec must be provided together.") + .failed() || + reportQDMIErrorIf( + !qdmiDevice.empty() && outputFormat.getNumOccurrences() != 0, + "--emit cannot be combined with --qdmi-device; --payload-spec " + "selects the output.") + .failed() || reportQDMIErrorIf( !qdmiConfig.empty() && !qdmiListDevices && qdmiDevice.empty(), "--qdmi-config requires --qdmi-device or --qdmi-list-devices.") @@ -418,7 +443,7 @@ static int runCompiler(int argc, char** argv) { << inputFilename << "'. Use --input-format.\n"; return 1; } - const auto parsedOutputFormat = parseOutputFormat(outputFormat); + auto parsedOutputFormat = parseOutputFormat(outputFormat); if (!parsedOutputFormat) { llvm::errs() << "Unknown output format '" << outputFormat << "'.\n"; return 1; @@ -426,14 +451,7 @@ static int runCompiler(int argc, char** argv) { std::optional compilerTarget; if (!qdmiDevice.empty()) { - if (reportQDMIErrorIf( - *parsedOutputFormat == OutputFormat::QCImport || - *parsedOutputFormat == OutputFormat::QCO || - *parsedOutputFormat == OutputFormat::Jeff, - "--qdmi-device requires qco-optimized, qc/mlir, qir-base, or " - "qir-adaptive output.") - .failed() || - reportQDMIErrorIf(passPipeline.hasAnyOccurrences(), + if (reportQDMIErrorIf(passPipeline.hasAnyOccurrences(), "--qdmi-device cannot be combined with --passes.") .failed() || reportQDMIErrorIf( @@ -470,6 +488,49 @@ static int runCompiler(int argc, char** argv) { MLIRContext context(registry); context.loadAllAvailableDialects(); + std::optional selectedPayload; + if (!payloadSpecification.empty()) { + const auto attribute = parseAttribute(payloadSpecification, &context); + const auto payloadAttr = + dyn_cast_if_present(attribute); + if (!payloadAttr) { + llvm::errs() + << "--payload-spec must be a valid #mqt.payload_spec attribute.\n"; + return 1; + } + auto payload = PayloadSpecification::create(payloadAttr); + if (!payload) { + llvm::errs() << "Invalid --payload-spec: " + << llvm::toString(payload.takeError()) << '\n'; + return 1; + } + selectedPayload.emplace(std::move(*payload)); + } + + std::optional targetEnvironment; + if (compilerTarget) { + auto compilerOutput = selectedPayload->compilerOutput(); + if (!compilerOutput) { + llvm::errs() << llvm::toString(compilerOutput.takeError()) << '\n'; + return 1; + } + switch (*compilerOutput) { + case ProgramFormat::OpenQASM3: + parsedOutputFormat = OutputFormat::OpenQASM3; + break; + case ProgramFormat::QIRBase: + parsedOutputFormat = OutputFormat::QIRBase; + break; + case ProgramFormat::QIRAdaptive: + parsedOutputFormat = OutputFormat::QIRAdaptive; + break; + default: + llvm_unreachable("Unsupported target compiler output"); + } + targetEnvironment.emplace(std::move(*compilerTarget), + std::move(*selectedPayload)); + } + ParsedProgram program; switch (*parsedInputFormat) { case InputFormat::MLIR: @@ -544,8 +605,8 @@ static int runCompiler(int argc, char** argv) { *parsedOutputFormat == OutputFormat::QIRAdaptive)) { pm.addPass(createInlinerPass()); } - if (compilerTarget) { - populateTargetCompilationPipeline(pm, *compilerTarget); + if (targetEnvironment) { + populateTargetCompilationPipeline(pm, *targetEnvironment); return success(); } populateQCOCleanupPipeline(pm); @@ -638,7 +699,13 @@ static int runCompiler(int argc, char** argv) { return 1; } qir::normalizeQIRModuleFlags(*llvmMod); - if (writeOutput(llvmMod.get(), outputFilename).failed()) { + const auto qirEncoding = + targetEnvironment + ? std::optional( + targetEnvironment->payloadSpecification().format().encoding) + : std::nullopt; + if (writeOutput(llvmMod.get(), outputFilename, qirEncoding) + .failed()) { return 1; } } else if (writeOutput(program.mod.get(), outputFilename) diff --git a/mlir/unittests/Compiler/mqt-cc/verify_qir_output.cmake b/mlir/unittests/Compiler/mqt-cc/verify_qir_output.cmake index adf88650d6..3e840787ff 100644 --- a/mlir/unittests/Compiler/mqt-cc/verify_qir_output.cmake +++ b/mlir/unittests/Compiler/mqt-cc/verify_qir_output.cmake @@ -17,6 +17,21 @@ function(run_command description) endif() endfunction() +function(run_command_expect_failure description expected_error) + execute_process( + COMMAND ${ARGN} + RESULT_VARIABLE result + OUTPUT_VARIABLE output + ERROR_VARIABLE error) + if(result EQUAL 0) + message(FATAL_ERROR "${description} unexpectedly succeeded") + endif() + string(FIND "${output}${error}" "${expected_error}" error_position) + if(error_position EQUAL -1) + message(FATAL_ERROR "${description} did not report '${expected_error}':\n${output}${error}") + endif() +endfunction() + function(require_profile filename expected_profile) file(READ "${filename}" llvm_ir) string(FIND "${llvm_ir}" "\"qir_profiles\"=\"${expected_profile}\"" profile_position) @@ -33,9 +48,74 @@ function(require_textual_llvm_ir filename) endif() endfunction() +function(require_bitcode filename) + file( + READ "${filename}" bitcode_magic + OFFSET 0 + LIMIT 4 + HEX) + string(TOLOWER "${bitcode_magic}" bitcode_magic) + if(NOT bitcode_magic STREQUAL "4243c0de") + message(FATAL_ERROR "${filename} does not start with the LLVM bitcode magic") + endif() +endfunction() + file(REMOVE_RECURSE "${OUTPUT_DIR}") file(MAKE_DIRECTORY "${OUTPUT_DIR}") +set(binary_payload_specification + "#mqt.payload_spec, capabilities = [], optional_capabilities_known = false>" +) +set(text_payload_specification + "#mqt.payload_spec, capabilities = [], optional_capabilities_known = false>" +) +run_command_expect_failure( + "mqt-cc target without payload specification" + "--qdmi-device and --payload-spec must be provided together" "${MQT_CC}" "${INPUT_FILE}" + "--qdmi-device=mqt.sc.iqm.garnet") +run_command_expect_failure( + "mqt-cc invalid payload specification" + "--payload-spec must be a valid #mqt.payload_spec attribute" "${MQT_CC}" "${INPUT_FILE}" + "--qdmi-device=mqt.sc.iqm.garnet" "--payload-spec=#mqt.payload_spec<>") +run_command_expect_failure( + "mqt-cc target with explicit output" + "--emit cannot be combined with --qdmi-device" + "${MQT_CC}" + "${INPUT_FILE}" + "--qdmi-device=mqt.sc.iqm.garnet" + "--payload-spec=${binary_payload_specification}" + "--emit=qir-base") + +set(target_bitcode_file "${OUTPUT_DIR}/target-binary.ll") +set(target_disassembled_file "${OUTPUT_DIR}/target-binary-disassembled.ll") +run_command( + "mqt-cc binary payload target compilation" + "${MQT_CC}" + "${INPUT_FILE}" + "--qdmi-device=mqt.sc.iqm.garnet" + "--payload-spec=${binary_payload_specification}" + -o + "${target_bitcode_file}") +require_bitcode("${target_bitcode_file}") +run_command("llvm-dis target bitcode validation" "${LLVM_DIS}" "${target_bitcode_file}" -o + "${target_disassembled_file}") +require_profile("${target_disassembled_file}" "base_profile") + +set(target_text_file "${OUTPUT_DIR}/target-text.bc") +set(target_assembled_file "${OUTPUT_DIR}/target-text-assembled.bc") +run_command( + "mqt-cc text payload target compilation" + "${MQT_CC}" + "${INPUT_FILE}" + "--qdmi-device=mqt.sc.iqm.garnet" + "--payload-spec=${text_payload_specification}" + -o + "${target_text_file}") +require_textual_llvm_ir("${target_text_file}") +run_command("llvm-as target text validation" "${LLVM_AS}" "${target_text_file}" -o + "${target_assembled_file}") +require_profile("${target_text_file}" "base_profile") + foreach(profile IN ITEMS base adaptive) set(expected_profile "${profile}_profile") set(text_file "${OUTPUT_DIR}/${profile}.ll") @@ -52,15 +132,7 @@ foreach(profile IN ITEMS base adaptive) run_command("mqt-cc ${profile} bitcode generation" "${MQT_CC}" "${INPUT_FILE}" "--emit=qir-${profile}" -o "${bitcode_file}") - file( - READ "${bitcode_file}" bitcode_magic - OFFSET 0 - LIMIT 4 - HEX) - string(TOLOWER "${bitcode_magic}" bitcode_magic) - if(NOT bitcode_magic STREQUAL "4243c0de") - message(FATAL_ERROR "${bitcode_file} does not start with the LLVM bitcode magic") - endif() + require_bitcode("${bitcode_file}") run_command("llvm-dis ${profile} bitcode validation" "${LLVM_DIS}" "${bitcode_file}" -o "${disassembled_file}") require_profile("${disassembled_file}" "${expected_profile}") diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 23be48efec..d3d42aad27 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -13,7 +13,9 @@ #include "mlir/Compiler/Programs.h" #include "mlir/Compiler/QDMIAdapter.h" #include "mlir/Compiler/Target.h" +#include "mlir/Compiler/TargetEnvironment.h" #include "mlir/Dialect/CBit/IR/CBitDialect.h" +#include "mlir/Dialect/MQT/IR/MQTAttributes.h" #include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" #include "mlir/Dialect/QC/IR/QCDialect.h" @@ -138,8 +140,8 @@ class CompilerPipelineTest // NOLINTNEXTLINE(readability-identifier-naming) void SetUp() override { DialectRegistry registry; - registry.insert(); @@ -247,6 +249,29 @@ makeCZTarget(std::initializer_list singleQubitGates) { CompilerTarget::NativeOperations::fromOperations(operations))); } +[[nodiscard]] static PayloadSpecification makePayloadSpecification() { + return llvm::cantFail(PayloadSpecification::create( + { + .id = "qir", + .version = "2.1.0", + .profile = "base", + .encoding = PayloadEncoding::Binary, + }, + { + { + .id = "forward-branching", + .constraints = + { + { + .id = "max-control-flow-nesting-depth", + .value = 8, + }, + }, + }, + }, + true)); +} + TEST_P(CompilerPipelineTest, EndToEndPipeline) { const auto& testCase = GetParam(); const auto name = " (" + testCase.name + ")"; @@ -267,7 +292,7 @@ TEST_P(CompilerPipelineTest, EndToEndPipeline) { auto compiled = runDefaultPipeline( CompilerInput{std::move(*input)}, testCase.convertToQIR ? ProgramFormat::QIRAdaptive : ProgramFormat::QC, - nullptr, testCase.qcoPipeline); + testCase.qcoPipeline); ASSERT_TRUE(compiled); OwningOpRef expected; @@ -475,9 +500,9 @@ h q; )"; auto input = QCProgram::fromQASMString(qasm); ASSERT_TRUE(input); - auto result = runDefaultPipeline(CompilerInput{std::move(*input)}, - ProgramFormat::QCOOptimized, nullptr, - "hadamard-lifting"); + auto result = + runDefaultPipeline(CompilerInput{std::move(*input)}, + ProgramFormat::QCOOptimized, "hadamard-lifting"); ASSERT_TRUE(result); EXPECT_FALSE(std::get(*result).str().empty()); } @@ -1102,7 +1127,8 @@ TEST_F(CompilerPipelineTest, EmptyCompiledProgramsRoundTrip) { ASSERT_TRUE(qc); auto qco = std::move(*qc).intoQCO(); ASSERT_TRUE(qco); - ASSERT_TRUE(qco->compileForTarget(makeCZTarget({{"sx", 0}, {"rz", 1}}))); + ASSERT_TRUE(qco->compileForTarget(TargetEnvironment( + makeCZTarget({{"sx", 0}, {"rz", 1}}), makePayloadSpecification()))); ASSERT_TRUE(succeeded(verify(qco->module()))); qco->module().walk([](Operation* operation) { const auto dialect = operation->getName().getDialectNamespace(); @@ -1575,7 +1601,8 @@ TEST_F(CompilerPipelineTest, JeffBinaryRoundTripPreservesReusableFunctions) { EXPECT_TRUE(std::get(*output).llvmIR()); } auto targeted = restored->copy(); - ASSERT_TRUE(targeted.compileForTarget(makeSparseUCZTarget(true))); + ASSERT_TRUE(targeted.compileForTarget(TargetEnvironment( + makeSparseUCZTarget(true), makePayloadSpecification()))); EXPECT_EQ(llvm::range_size(targeted.module().getOps()), 1); auto qc = std::move(*restored).intoQC(); ASSERT_TRUE(qc); @@ -1785,11 +1812,21 @@ TEST_F(CompilerPipelineTest, QCOProgramCompilesForTarget) { ASSERT_TRUE(qco); const auto target = makeSparseUCZTarget(true); - ASSERT_TRUE(qco->compileForTarget(target)); + const auto payload = makePayloadSpecification(); + const TargetEnvironment targetEnvironment(target, payload); + /// The supplied environment must replace stale metadata before all passes. + attachTargetEnvironment( + qco->module(), TargetEnvironment(makeSparseUCZTarget(false), payload)); + ASSERT_TRUE(qco->compileForTarget(targetEnvironment)); auto compiled = parseRecordedModule(qco->str()); - ASSERT_TRUE(compiled); + ASSERT_TRUE(compiled) << qco->str(); EXPECT_TRUE(verify(*compiled).succeeded()); + const auto environment = (*compiled)->getAttrOfType( + mlir::mqt::TargetEnvAttr::name); + ASSERT_TRUE(environment); + EXPECT_EQ(environment.getPayloadSpecification(), + payload.materialize(*context)); size_t numStatic = 0; size_t numDynamic = 0; @@ -1818,7 +1855,33 @@ TEST_F(CompilerPipelineTest, QCOProgramCompilesForTarget) { ASSERT_TRUE(unsupportedQC); auto unsupportedQCO = std::move(*unsupportedQC).intoQCO(); ASSERT_TRUE(unsupportedQCO); - EXPECT_FALSE(unsupportedQCO->compileForTarget(makeSparseUCZTarget(false))); + EXPECT_FALSE(unsupportedQCO->compileForTarget( + TargetEnvironment(makeSparseUCZTarget(false), payload))); + EXPECT_TRUE( + unsupportedQCO->module()->hasAttr(mlir::mqt::TargetEnvAttr::name)); +} + +/// Test: target passes use the canonical environment in textual form. +TEST_F(CompilerPipelineTest, TargetPassesRunFromTextualPipeline) { + constexpr llvm::StringLiteral source = R"(OPENQASM 3.0; +include "stdgates.inc"; +qubit q; +x q; +)"; + auto qc = QCProgram::fromQASMString(source); + ASSERT_TRUE(qc); + auto qco = std::move(*qc).intoQCO(); + ASSERT_TRUE(qco); + + const auto target = llvm::cantFail( + CompilerTarget::create(1, CompilerTarget::Connectivity::fromCouplings({}), + CompilerTarget::NativeOperations::unrestricted())); + attachTargetEnvironment( + qco->module(), TargetEnvironment(target, makePayloadSpecification())); + EXPECT_TRUE( + qco->runPassPipeline("place-and-route{ntrials=1},target-native-synthesis," + "verify-target-conformance")); + EXPECT_NE(qco->str().find("qco.static"), std::string::npos); } TEST_F(CompilerPipelineTest, TargetCompilationInlinesReusableFunctions) { @@ -1846,7 +1909,8 @@ TEST_F(CompilerPipelineTest, TargetCompilationInlinesReusableFunctions) { auto program = QCOProgram::fromModule(ownedContext, std::move(moduleOp)); ASSERT_TRUE(program); - ASSERT_TRUE(program->compileForTarget(makeSparseUCZTarget(false))); + ASSERT_TRUE(program->compileForTarget(TargetEnvironment( + makeSparseUCZTarget(false), makePayloadSpecification()))); EXPECT_FALSE(program->module().lookupSymbol("flip")); size_t calls = 0; program->module().walk([&](qco::CallOp) { ++calls; }); @@ -1901,7 +1965,8 @@ TEST_F(CompilerPipelineTest, 2, CompilerTarget::Connectivity::fromCouplings({{0, 1}}), CompilerTarget::NativeOperations::fromOperations(operations))); - ASSERT_TRUE(program->compileForTarget(target)); + ASSERT_TRUE(program->compileForTarget( + TargetEnvironment(target, makePayloadSpecification()))); const std::string before = program->str(); EXPECT_EQ(before.find("func.func private @forward"), std::string::npos); EXPECT_NE(before.find("qco.if"), std::string::npos); @@ -1940,7 +2005,8 @@ gphase(0.5); const auto target = llvm::cantFail(compilerTargetFromDeviceId("mqt.ddsim.default")); - ASSERT_TRUE(qco->compileForTarget(target)); + ASSERT_TRUE(qco->compileForTarget( + TargetEnvironment(target, makePayloadSpecification()))); auto compiled = parseRecordedModule(qco->str()); ASSERT_TRUE(compiled); @@ -1990,7 +2056,8 @@ cx q[1], q[0]; 2, CompilerTarget::Connectivity::fromCouplings({{0, 1}}), CompilerTarget::NativeOperations::fromOperations(operations))); - ASSERT_TRUE(qco->compileForTarget(target)); + ASSERT_TRUE(qco->compileForTarget( + TargetEnvironment(target, makePayloadSpecification()))); auto compiled = parseRecordedModule(qco->str()); ASSERT_TRUE(compiled); EXPECT_TRUE(verify(*compiled).succeeded()); @@ -2085,7 +2152,8 @@ TEST_F(CompilerPipelineTest, QCOProgramCompilesDynamicRunForSupportedTargets) { ASSERT_TRUE(testCase.target.synthesisBasis()); ASSERT_EQ(testCase.target.synthesisBasis()->singleQubit, testCase.resolvedBasis); - ASSERT_TRUE(program->compileForTarget(testCase.target)); + ASSERT_TRUE(program->compileForTarget( + TargetEnvironment(testCase.target, makePayloadSpecification()))); auto compiled = parseRecordedModule(program->str()); ASSERT_TRUE(compiled); @@ -2142,7 +2210,8 @@ TEST_F(CompilerPipelineTest, QCOProgramMergesDynamicRunInNativeCtrlBody) { auto program = QCOProgram::fromMLIRString(source); ASSERT_TRUE(program); - ASSERT_TRUE(program->compileForTarget(target)); + ASSERT_TRUE(program->compileForTarget( + TargetEnvironment(target, makePayloadSpecification()))); auto compiled = parseRecordedModule(program->str()); ASSERT_TRUE(compiled); @@ -2183,10 +2252,11 @@ c = measure q; const auto target = llvm::cantFail(CompilerTarget::create( std::move(sites), CompilerTarget::Connectivity::allToAll(), CompilerTarget::NativeOperations::unrestricted())); - ASSERT_TRUE(qco->compileForTarget(target)); + ASSERT_TRUE(qco->compileForTarget( + TargetEnvironment(target, makePayloadSpecification()))); auto compiled = parseRecordedModule(qco->str()); - ASSERT_TRUE(compiled); + ASSERT_TRUE(compiled) << qco->str(); EXPECT_TRUE(verify(*compiled).succeeded()); llvm::SmallVector staticSites; @@ -2221,7 +2291,8 @@ h q[1]; ASSERT_TRUE(qc); auto program = std::move(*qc).intoQCO(); ASSERT_TRUE(program); - ASSERT_TRUE(program->compileForTarget(target)); + ASSERT_TRUE(program->compileForTarget( + TargetEnvironment(target, makePayloadSpecification()))); auto moduleOp = parseRecordedModule(program->str()); ASSERT_TRUE(moduleOp); EXPECT_TRUE(verify(*moduleOp).succeeded()); @@ -2239,27 +2310,19 @@ h q[1]; EXPECT_EQ(staticQubits, 2U); } -// Test: the default pipeline accepts an optional compiler target. -TEST_F(CompilerPipelineTest, DefaultPipelineCompilesForTarget) { +/// Test: the payload specification selects the targeted output. +TEST_F(CompilerPipelineTest, DefaultPipelineDerivesTargetOutput) { auto input = QCProgram::fromQASMString(qasm::multipleControlledX); ASSERT_TRUE(input); const auto target = makeSparseUCZTarget(true); + const TargetEnvironment environment(target, makePayloadSpecification()); - auto result = runDefaultPipeline(CompilerInput{std::move(*input)}, - ProgramFormat::QCOOptimized, &target); + auto result = + runDefaultPipeline(CompilerInput{std::move(*input)}, environment); ASSERT_TRUE(result); - ASSERT_TRUE(std::holds_alternative(*result)); - const auto& qco = std::get(*result); - EXPECT_NE(qco.str().find("qco.static"), std::string::npos); - EXPECT_EQ(qco.str().find("qco.swap"), std::string::npos); - - auto qirInput = QCProgram::fromQASMString(qasm::multipleControlledX); - ASSERT_TRUE(qirInput); - auto qirResult = runDefaultPipeline(CompilerInput{std::move(*qirInput)}, - ProgramFormat::QIRBase, &target); - ASSERT_TRUE(qirResult); - ASSERT_TRUE(std::holds_alternative(*qirResult)); - const auto& qir = std::get(*qirResult); + ASSERT_TRUE(std::holds_alternative(*result)); + const auto& qir = std::get(*result); + EXPECT_EQ(qir.profile(), QIRProfile::Base); auto qirModule = parseRecordedModule(qir.str()); ASSERT_TRUE(qirModule); std::vector qirSiteIds; @@ -2341,7 +2404,7 @@ h q; auto profiledInput = QCProgram::fromQASMString(qasm); ASSERT_TRUE(profiledInput); auto profiled = runDefaultPipeline(CompilerInput{std::move(*profiledInput)}, - ProgramFormat::QCOOptimized, nullptr, + ProgramFormat::QCOOptimized, "mqt-qco-default", true, true); ASSERT_TRUE(profiled); EXPECT_TRUE(std::holds_alternative(*profiled)); @@ -2350,28 +2413,7 @@ h q; ASSERT_TRUE(customPipelineInput); EXPECT_FALSE(runDefaultPipeline( CompilerInput{std::move(*customPipelineInput)}, ProgramFormat::QCO, - nullptr, "builtin.module(merge-single-qubit-rotation-gates)")); - - const auto target = llvm::cantFail( - CompilerTarget::create(1, CompilerTarget::Connectivity::allToAll(), - CompilerTarget::NativeOperations::unrestricted())); - auto targetedImport = QCProgram::fromQASMString(qasm); - auto targetedRawQCO = QCProgram::fromQASMString(qasm); - auto targetedJeff = QCProgram::fromQASMString(qasm); - auto targetedCustom = QCProgram::fromQASMString(qasm); - ASSERT_TRUE(targetedImport); - ASSERT_TRUE(targetedRawQCO); - ASSERT_TRUE(targetedJeff); - ASSERT_TRUE(targetedCustom); - EXPECT_FALSE(runDefaultPipeline(CompilerInput{std::move(*targetedImport)}, - ProgramFormat::QCImport, &target)); - EXPECT_FALSE(runDefaultPipeline(CompilerInput{std::move(*targetedRawQCO)}, - ProgramFormat::QCO, &target)); - EXPECT_FALSE(runDefaultPipeline(CompilerInput{std::move(*targetedJeff)}, - ProgramFormat::Jeff, &target)); - EXPECT_FALSE(runDefaultPipeline(CompilerInput{std::move(*targetedCustom)}, - ProgramFormat::QCOOptimized, &target, - "hadamard-lifting")); + "builtin.module(merge-single-qubit-rotation-gates)")); auto base = compile(ProgramFormat::QIRBase); ASSERT_TRUE(base); @@ -2398,7 +2440,7 @@ h q; EXPECT_FALSE( runDefaultPipeline(CompilerInput{qco->copy()}, ProgramFormat::QCImport)); EXPECT_FALSE(runDefaultPipeline(CompilerInput{qco->copy()}, - ProgramFormat::QCImport, nullptr, + ProgramFormat::QCImport, "merge-single-qubit-rotation-gates")); auto fromQCO = runDefaultPipeline(CompilerInput{std::move(*qco)}, ProgramFormat::QC); @@ -2417,9 +2459,36 @@ h q; EXPECT_TRUE(std::holds_alternative(*fromJeff)); } -// Test: QCOProgram::decomposeMultiControlled runs the pass on MCX. -// -// Correctness of the decomposition is tested in a dedicated suite. +TEST_F(CompilerPipelineTest, UnsupportedTargetOutputPreservesInput) { + constexpr llvm::StringLiteral source = R"(OPENQASM 3.0; +include "stdgates.inc"; +qubit q; +h q; +)"; + auto input = QCProgram::fromQASMString(source); + ASSERT_TRUE(input); + CompilerInput program{std::move(*input)}; + const auto original = std::get(program).str(); + const auto payload = llvm::cantFail( + PayloadSpecification::create({.id = "unsupported", .version = "1.0.0"})); + const TargetEnvironment environment( + llvm::cantFail(CompilerTarget::create( + 1, CompilerTarget::Connectivity::allToAll(), + CompilerTarget::NativeOperations::unrestricted())), + payload); + + EXPECT_FALSE(runDefaultPipeline(std::move(program), environment)); + // The call binds an rvalue reference, but the unsupported output is rejected + // before the function consumes the input. Inspecting the input verifies that + // preservation contract. + // NOLINTNEXTLINE(bugprone-use-after-move) + ASSERT_TRUE(std::holds_alternative(program)); + EXPECT_EQ(std::get(program).str(), original); +} + +/// Test: QCOProgram::decomposeMultiControlled runs the pass on MCX. +/// +/// Correctness of the decomposition is tested in a dedicated suite. TEST_F(CompilerPipelineTest, DecomposeMultiControlledPass) { auto moduleOp = mlir::qc::QCProgramBuilder::build( context.get(), mlir::qc::multipleControlledX); diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index a986c6f647..87b473f58a 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -8,7 +8,9 @@ * Licensed under the MIT License */ +#include "mlir/Compiler/Programs.h" #include "mlir/Compiler/Target.h" +#include "mlir/Compiler/TargetEnvironment.h" #include "mlir/Dialect/MQT/IR/MQTAttributes.h" #include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" @@ -19,11 +21,16 @@ #include #include #include +#include #include #include +#include #include +#include #include #include +#include +#include #include #include @@ -63,6 +70,217 @@ using Site = Target::Site; using SiteId = Target::SiteId; using SiteTuple = Target::SiteTuple; +TEST(PayloadSpecificationTest, ValidatesAndRoundTripsTypedAttribute) { + mlir::MLIRContext context; + context.loadDialect(); + + const auto payload = valid(mlir::PayloadSpecification::create( + { + .id = "vendor.ir", + .version = "4.2.0", + .profile = "dynamic", + .encoding = mlir::PayloadEncoding::Binary, + }, + { + { + .id = "forward-branching", + .constraints = + { + { + .id = "max-control-flow-nesting-depth", + .value = 8, + }, + }, + }, + }, + true)); + const auto attribute = payload.materialize(context); + const auto reconstructed = + valid(mlir::PayloadSpecification::create(attribute)); + + EXPECT_EQ(reconstructed.format(), payload.format()); + EXPECT_EQ(reconstructed.capabilities(), payload.capabilities()); + EXPECT_TRUE(reconstructed.optionalCapabilitiesKnown()); + EXPECT_EQ(reconstructed.materialize(context), attribute); + + expectInvalid( + mlir::PayloadSpecification::create(mlir::mqt::PayloadSpecAttr{}), + "Invalid payload specification: Payload specification attribute must " + "not be null"); + expectInvalid(mlir::PayloadSpecification::create( + {.id = "qir", .version = "2.1.0.1", .profile = "base"}), + "Invalid payload specification: Payload format version must " + "use major[.minor[.patch]]"); + expectInvalid( + mlir::PayloadSpecification::create({.id = "", .version = "2.1.0"}), + "Invalid payload specification: Payload format requires an ID and " + "version"); + expectInvalid( + mlir::PayloadSpecification::create( + {.id = std::string("qir\0", 4), .version = "2.1.0"}), + "Invalid payload specification: Payload format fields must not contain " + "null characters"); + expectInvalid( + mlir::PayloadSpecification::create({.id = "qir", .version = "2.1.0"}, + {{.id = ""}}), + "Invalid payload specification: Program capability ID must not be " + "empty"); + expectInvalid( + mlir::PayloadSpecification::create({.id = "qir", .version = "2.1.0"}, + {{.id = std::string("x\0", 2)}}), + "Invalid payload specification: Program capability ID must not contain " + "a null character"); + expectInvalid( + mlir::PayloadSpecification::create( + {.id = "qir", .version = "2.1.0"}, + {{.id = "capability", .constraints = {{.id = ""}}}}), + "Invalid payload specification: Program constraint ID must not be " + "empty"); + expectInvalid( + mlir::PayloadSpecification::create( + {.id = "qir", .version = "2.1.0"}, + { + { + .id = "capability", + .constraints = {{.id = std::string("x\0", 2)}}, + }, + }), + "Invalid payload specification: Program constraint ID must not contain " + "a null character"); + expectInvalid( + mlir::PayloadSpecification::create( + {.id = "qir", .version = "2.1.0", .profile = "base"}, + { + { + .id = "integer-computation", + .constraints = {{.id = "width"}, {.id = "width"}}, + }, + }), + "Invalid payload specification: Program capability contains a duplicate " + "constraint ID"); + expectInvalid(mlir::PayloadSpecification::create( + {.id = "qir", .version = "2.1.0", .profile = "base"}, + { + {.id = "integer-computation", .value = 64}, + {.id = "integer-computation", .value = 64}, + }), + "Invalid payload specification: Payload specification contains " + "a duplicate capability ID/value pair"); +} + +TEST(PayloadSpecificationTest, NormalizesExactVersionComponents) { + for (const auto& [input, expected] : std::array{ + std::pair{"2", "2.0.0"}, + std::pair{"2.1", "2.1.0"}, + std::pair{"2.1.3", "2.1.3"}, + }) { + SCOPED_TRACE(input); + const auto payload = valid(mlir::PayloadSpecification::create( + {.id = "qir", .version = input, .profile = "base"})); + EXPECT_EQ(payload.format().version, expected); + } + + const auto qir = valid(mlir::PayloadSpecification::create( + {.id = "qir", .version = "2.1", .profile = "base"})); + EXPECT_EQ(valid(qir.compilerOutput()), mlir::ProgramFormat::QIRBase); + const auto qasm = valid( + mlir::PayloadSpecification::create({.id = "openqasm", .version = "3"})); + EXPECT_EQ(valid(qasm.compilerOutput()), mlir::ProgramFormat::OpenQASM3); + const auto exactMajor = valid(mlir::PayloadSpecification::create( + {.id = "qir", .version = "2", .profile = "base"})); + expectInvalid(exactMajor.compilerOutput(), + "Invalid payload specification: MQT Compiler cannot emit the " + "selected payload format"); +} + +TEST(PayloadSpecificationTest, NormalizesTypedVersionShorthand) { + mlir::MLIRContext context; + context.loadDialect(); + const auto attribute = mlir::dyn_cast_if_present( + mlir::parseAttribute(R"mlir(#mqt.payload_spec< + format = , + capabilities = [], optional_capabilities_known = false>)mlir", + &context)); + ASSERT_TRUE(attribute); + const auto payload = valid(mlir::PayloadSpecification::create(attribute)); + EXPECT_EQ(payload.format().version, "2.1.0"); + EXPECT_EQ(valid(payload.compilerOutput()), mlir::ProgramFormat::QIRBase); + EXPECT_EQ(payload.materialize(context).getFormat().getVersion().getValue(), + "2.1.0"); +} + +TEST(TargetEnvironmentTest, ReusesPreparedTargetStorage) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::OwningOpRef moduleOp = + mlir::ModuleOp::create(mlir::UnknownLoc::get(&context)); + const auto target = + valid(Target::create(3, Connectivity::fromCouplings({{0, 1}, {1, 2}}), + NativeOperations::unrestricted())); + const mlir::TargetEnvironment environment( + target, valid(mlir::PayloadSpecification::create( + {.id = "qir", .version = "2.1.0", .profile = "base"}))); + mlir::ModuleAnalysisManager moduleAnalysisManager(moduleOp.get(), nullptr); + mlir::AnalysisManager analysisManager = moduleAnalysisManager; + auto& analysis = + analysisManager.getAnalysis(); + analysis.initialize(environment); + ASSERT_TRUE(analysis); + EXPECT_EQ(analysis.environment().target().sites().data(), + target.sites().data()); + EXPECT_EQ(analysis.environment().target().couplings().data(), + target.couplings().data()); + EXPECT_EQ((*moduleOp)->getAttr(mlir::mqt::TargetEnvAttr::name), + environment.materialize(context)); + mlir::AnalysisManager::PreservedAnalyses preserved; + analysisManager.invalidate(preserved); + ASSERT_TRUE( + analysisManager.getCachedAnalysis()); + EXPECT_EQ(analysisManager.getAnalysis() + .environment() + .target() + .sites() + .data(), + target.sites().data()); +} + +TEST(TargetEnvironmentTest, InvalidatesCachedAnalysisAfterAttributeChange) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::OwningOpRef module = + mlir::ModuleOp::create(mlir::UnknownLoc::get(&context)); + const auto payload = valid(mlir::PayloadSpecification::create( + {.id = "qir", .version = "2.1.0", .profile = "base"})); + mlir::attachTargetEnvironment( + *module, mlir::TargetEnvironment( + valid(Target::create(1, Connectivity::allToAll(), + NativeOperations::unrestricted())), + payload)); + mlir::ModuleAnalysisManager moduleAnalysisManager(module.get(), nullptr); + mlir::AnalysisManager analysisManager = moduleAnalysisManager; + + const auto& initial = + analysisManager.getAnalysis(); + ASSERT_TRUE(initial); + EXPECT_EQ(initial.environment().target().numSites(), 1); + + mlir::attachTargetEnvironment( + *module, mlir::TargetEnvironment( + valid(Target::create(2, Connectivity::allToAll(), + NativeOperations::unrestricted())), + payload)); + mlir::AnalysisManager::PreservedAnalyses preserved; + preserved.preserve(); + analysisManager.invalidate(preserved); + EXPECT_FALSE( + analysisManager.getCachedAnalysis()); + + const auto& updated = + analysisManager.getAnalysis(); + ASSERT_TRUE(updated); + EXPECT_EQ(updated.environment().target().numSites(), 2); +} + TEST(CompilerTargetTest, ConstructsDetailedNamedTargetAndSharesStorage) { std::vector sites; sites.emplace_back(valid(Site::create(7, "left", 100, 80))); diff --git a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp index f282a53bf8..ff7e104b9e 100644 --- a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp +++ b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp @@ -62,6 +62,13 @@ class MQTIRTest : public ::testing::Test { return parseAttribute(source, context.get()); } + [[nodiscard]] OwningOpRef roundTrip(ModuleOp moduleOp) const { + std::string printed; + llvm::raw_string_ostream stream(printed); + moduleOp.print(stream); + return parse(printed); + } + [[nodiscard]] Attribute roundTrip(const Attribute attribute) const { std::string printed; llvm::raw_string_ostream stream(printed); @@ -712,4 +719,134 @@ TEST_F(MQTIRTest, RejectsUnknownMQTAttributes) { } )mlir")); } +TEST_F(MQTIRTest, RoundTripsTypedTargetEnvironment) { + auto moduleOp = parse(R"mlir( + module attributes { + mqt.target_env = #mqt.target_env< + compilation_target = #mqt.compilation_target< + name = "device", + sites = [, + ], + duration_unit = #mqt.duration_unit, + connectivity = explicit, + couplings = [], + native_operations = explicit, + operations = [, + num_parameters = 0, + site_tuples = [<[10, 20], duration = 50, + fidelity = 9.900000e-01 : f64>], + duration = 60, fidelity = 9.800000e-01 : f64>]>, + payload_specification = #mqt.payload_spec< + format = #mqt.payload_format, + capabilities = []>], + optional_capabilities_known = false>> + } { + func.func @main() { return } + } + )mlir"); + ASSERT_TRUE(moduleOp); + + const auto targetEnv = + (*moduleOp)->getAttrOfType(mqt::TargetEnvAttr::name); + ASSERT_TRUE(targetEnv); + const auto compilationTarget = targetEnv.getCompilationTarget(); + EXPECT_EQ(compilationTarget.getName().getValue(), "device"); + ASSERT_EQ(compilationTarget.getSites().size(), 2U); + EXPECT_EQ(compilationTarget.getSites()[0].getId(), 10); + EXPECT_EQ(compilationTarget.getSites()[1].getId(), 20); + EXPECT_EQ(compilationTarget.getConnectivity(), + mqt::ConnectivityKind::Explicit); + EXPECT_EQ(compilationTarget.getNativeOperations(), + mqt::NativeOperationsKind::Explicit); + ASSERT_EQ(compilationTarget.getOperations().size(), 1U); + const auto operationSites = compilationTarget.getOperations() + .front() + .getSiteTuples() + .front() + .getSites(); + ASSERT_EQ(operationSites.size(), 2U); + EXPECT_EQ(operationSites[0], 10); + EXPECT_EQ(operationSites[1], 20); + + const auto payloadSpecification = targetEnv.getPayloadSpecification(); + EXPECT_EQ(payloadSpecification.getFormat().getId().getValue(), "vendor-ir"); + EXPECT_EQ(payloadSpecification.getFormat().getVersion().getValue(), "4.2.0"); + EXPECT_EQ(payloadSpecification.getFormat().getProfile().getValue(), + "dynamic"); + EXPECT_EQ(payloadSpecification.getFormat().getEncoding(), + mqt::PayloadEncoding::Binary); + EXPECT_FALSE(payloadSpecification.getOptionalCapabilitiesKnown()); + ASSERT_EQ(payloadSpecification.getCapabilities().size(), 1U); + ASSERT_EQ( + payloadSpecification.getCapabilities().front().getConstraints().size(), + 1U); + + const auto reparsed = roundTrip(*moduleOp); + ASSERT_TRUE(reparsed); + EXPECT_EQ((*reparsed)->getAttr(mqt::TargetEnvAttr::name), targetEnv); +} + +TEST_F(MQTIRTest, RepresentsEmptyPayloadCapabilities) { + const auto payload = dyn_cast_if_present(parseAttr( + R"mlir(#mqt.payload_spec, + capabilities = [], optional_capabilities_known = true>)mlir")); + ASSERT_TRUE(payload); + EXPECT_TRUE(payload.getCapabilities().empty()); + EXPECT_TRUE(payload.getOptionalCapabilitiesKnown()); +} + +TEST_F(MQTIRTest, RejectsInvalidPayloadContracts) { + EXPECT_FALSE(parseAttr(R"mlir(#mqt.payload_format)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.payload_format)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.payload_format)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.payload_format)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.payload_format)mlir")); + EXPECT_FALSE( + parseAttr(R"mlir(#mqt.program_constraint)mlir")); + EXPECT_FALSE(parseAttr( + R"mlir(#mqt.program_constraint)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.program_capability)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.program_capability)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.program_capability, + ]>)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.payload_spec< + format = #mqt.payload_format, + capabilities = [, + ], + optional_capabilities_known = true>)mlir")); +} + +TEST_F(MQTIRTest, RejectsTargetEnvironmentOutsideModule) { + EXPECT_FALSE(parse(R"mlir( + module attributes {mqt.target_env = "invalid"} {} + )mlir")); + EXPECT_FALSE(parse(R"mlir( + module { + func.func @main() attributes { + mqt.target_env = #mqt.target_env< + compilation_target = #mqt.compilation_target< + sites = [], connectivity = all_to_all, couplings = [], + native_operations = unrestricted, operations = []>, + payload_specification = #mqt.payload_spec< + format = #mqt.payload_format, + capabilities = [], optional_capabilities_known = false>> + } { return } + } + )mlir")); +} + } // namespace diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index 1a4d1560a3..0e8e51081c 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/Compiler/TargetEnvironment.h" #include "mlir/Dialect/CBit/IR/CBitDialect.h" #include "mlir/Dialect/CBit/IR/CBitOps.h" #include "mlir/Dialect/MQT/IR/MQTDialect.h" @@ -78,6 +79,17 @@ static std::string printModule(ModuleOp moduleOp) { return result; } +static void attachTestEnvironment(ModuleOp moduleOp, + const CompilerTarget& target) { + static const auto PAYLOAD = [] { + PayloadFormat format; + format.id = "test.payload"; + format.version = "1.0.0"; + return llvm::cantFail(PayloadSpecification::create(std::move(format))); + }(); + attachTargetEnvironment(moduleOp, TargetEnvironment(target, PAYLOAD)); +} + static SmallVector getQubitValues(ValueRange values) { return llvm::filter_to_vector( values, [](Value value) { return isa(value.getType()); }); @@ -319,8 +331,9 @@ class MappingPassFixture : public testing::Test { static LogicalResult runPass(ModuleOp m, const CompilerTarget& target, const MappingPassOptions& options) { + attachTestEnvironment(m, target); PassManager pm(m->getContext()); - pm.addPass(createMappingPass(target, options)); + pm.addPass(createMappingPass(options)); if (failed(pm.run(m))) { return failure(); } @@ -345,6 +358,27 @@ class MappingPassTest : public MappingPassFixture, }; // namespace +TEST_F(MappingPassFixture, RequiresTypedTargetEnvironment) { + QCOProgramBuilder builder(context.get()); + builder.initialize(); + auto moduleOp = builder.finalize(); + + std::string diagnostics; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + diagnostics += '\n'; + return success(); + }); + PassManager pm(context.get()); + pm.addPass(createMappingPass(MappingPassOptions{.ntrials = 1})); + EXPECT_TRUE(failed(pm.run(moduleOp.get()))); + EXPECT_NE(diagnostics.find("place-and-route requires a valid " + "mqt.target_env: module does not contain " + "mqt.target_env"), + std::string::npos) + << diagnostics; +} + TEST_F(MappingPassFixture, MapTopologyOnlyWithEmptyOperationSet) { constexpr int64_t size = 3; @@ -568,6 +602,7 @@ TEST_F(MappingPassFixture, RejectNonExplicitTopologyBeforeMutation) { auto qubit = builder.h(builder.allocQubit()); builder.sink(qubit); auto moduleOp = builder.finalize(); + attachTestEnvironment(moduleOp.get(), target); const auto before = printModule(moduleOp.get()); std::string diagnostics; @@ -694,9 +729,9 @@ TEST_F(MappingPassFixture, PreserveStoredRegisterControlDuringRouting) { const auto target = llvm::cantFail( CompilerTarget::create(3, Connectivity::fromCouplings({{0, 1}, {1, 2}}), NativeOperations::unrestricted())); + attachTestEnvironment(moduleOp.get(), target); PassManager mappingPm(context.get()); - mappingPm.addPass( - createMappingPass(target, MappingPassOptions{.ntrials = 1})); + mappingPm.addPass(createMappingPass(MappingPassOptions{.ntrials = 1})); ASSERT_TRUE(succeeded(mappingPm.run(moduleOp.get()))); ASSERT_TRUE(succeeded(verify(*moduleOp))); EXPECT_TRUE(isExecutable(getEntryPoint(moduleOp.get()), target)); @@ -869,10 +904,11 @@ TEST_P(MappingPassTest, MapProgramAfterQubitReuse) { builder.sink(q1); auto m = builder.finalize({bit0, bit1}); + attachTestEnvironment(m.get(), target); PassManager pm(context.get()); pm.addPass(createReuseQubits()); pm.addPass(createCanonicalizerPass()); - pm.addPass(createMappingPass(target, MappingPassOptions{.ntrials = 1})); + pm.addPass(createMappingPass(MappingPassOptions{.ntrials = 1})); pm.addPass(createCanonicalizerPass()); ASSERT_TRUE(pm.run(m.get()).succeeded()); ASSERT_TRUE(succeeded(verify(*m))); @@ -1105,7 +1141,7 @@ TEST_P(MappingPassTest, MapLoopBasedGHZByUnrolling) { PassManager pm(context.get()); pm.addNestedPass(createQuantumLoopUnroll()); populateQCOCleanupPipeline(pm); - pm.addPass(createMappingPass(target, MappingPassOptions{})); + pm.addPass(createMappingPass(MappingPassOptions{})); QCOProgramBuilder builder(context.get()); builder.initialize(SmallVector(size, builder.getI1Type())); @@ -1131,6 +1167,7 @@ TEST_P(MappingPassTest, MapLoopBasedGHZByUnrolling) { builder.qtensorDealloc(tensor); auto m = builder.finalize(bits); + attachTestEnvironment(m.get(), target); ASSERT_TRUE(pm.run(m.get()).succeeded()); ASSERT_TRUE(succeeded(verify(*m))); EXPECT_TRUE(isExecutable(getEntryPoint(m.get()), target)); diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp index 08e887cd58..faed1d6c90 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -11,6 +11,7 @@ #include "dd/DDDefinitions.hpp" #include "dd/Package.hpp" #include "mlir/Compiler/Target.h" +#include "mlir/Compiler/TargetEnvironment.h" #include "mlir/Dialect/MQT/IR/MQTDialect.h" #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" @@ -157,6 +158,26 @@ runPass(ModuleOp module, std::unique_ptr pass) { return manager.run(module); } +[[nodiscard]] static mlir::PayloadSpecification makePayloadSpecification() { + mlir::PayloadFormat format; + format.id = "mqt.test.payload"; + format.version = "1.0.0"; + format.encoding = mlir::PayloadEncoding::Binary; + return valid(mlir::PayloadSpecification::create(std::move(format))); +} + +static void attachTestEnvironment(ModuleOp module, const Target& target) { + mlir::attachTargetEnvironment( + module, mlir::TargetEnvironment(target, makePayloadSpecification())); +} + +[[nodiscard]] static mlir::LogicalResult +runTargetPass(ModuleOp module, const Target& target, + std::unique_ptr pass) { + attachTestEnvironment(module, target); + return runPass(module, std::move(pass)); +} + [[nodiscard]] static Target makeUCxTarget(std::optional> sites = std::nullopt) { if (!sites) { @@ -236,8 +257,8 @@ class TargetSynthesisTest : public testing::Test { void SetUp() override { mlir::DialectRegistry registry; registry.insert(); + mlir::mqt::MQTDialect, mlir::qco::QCODialect, + mlir::qtensor::QTensorDialect, mlir::scf::SCFDialect>(); context = std::make_unique(); context->appendDialectRegistry(registry); context->loadAllAvailableDialects(); @@ -261,6 +282,13 @@ class TargetSynthesisTest : public testing::Test { return diagnostics; } + [[nodiscard]] std::string + expectTargetFailure(ModuleOp module, const Target& target, + std::unique_ptr pass) const { + attachTestEnvironment(module, target); + return expectFailure(module, std::move(pass)); + } + std::unique_ptr context; }; @@ -270,8 +298,8 @@ TEST(TargetSynthesisPassContract, FactoriesAreIndependentlyConstructible) { const auto target = valid(Target::create(2, Connectivity::allToAll(), NativeOperations::unrestricted())); auto fusion = mlir::qco::createFuseTwoQubitGates(); - auto synthesis = mlir::qco::createTargetNativeSynthesis(target); - auto conformance = mlir::qco::createVerifyTargetConformance(target); + auto synthesis = mlir::qco::createTargetNativeSynthesis(); + auto conformance = mlir::qco::createVerifyTargetConformance(); ASSERT_NE(fusion, nullptr); ASSERT_NE(synthesis, nullptr); @@ -288,6 +316,31 @@ TEST(TargetSynthesisPassContract, FactoriesAreIndependentlyConstructible) { mlir::arith::ArithDialect::getDialectNamespace())); } +TEST_F(TargetSynthesisTest, TargetPassesRequireTypedEnvironment) { + const auto buildClassical = [&] { + return build( + [](QCOProgramBuilder& builder) { return builder.intConstant(0); }); + }; + + auto synthesisModule = buildClassical(); + auto diagnostics = + expectFailure(*synthesisModule, mlir::qco::createTargetNativeSynthesis()); + EXPECT_NE(diagnostics.find("target-native synthesis requires a valid " + "mqt.target_env: module does not contain " + "mqt.target_env"), + std::string::npos) + << diagnostics; + + auto conformanceModule = buildClassical(); + diagnostics = expectFailure(*conformanceModule, + mlir::qco::createVerifyTargetConformance()); + EXPECT_NE(diagnostics.find("target conformance requires a valid " + "mqt.target_env: module does not contain " + "mqt.target_env"), + std::string::npos) + << diagnostics; +} + TEST_F(TargetSynthesisTest, TwoQubitGateFusionRequiresStrictImprovement) { const auto adjacentCx = [](QCOProgramBuilder& builder) { const auto q0Input = builder.staticQubit(0); @@ -424,12 +477,12 @@ TEST_F(TargetSynthesisTest, TargetNativeSynthesisRemovesOrdinarySwap) { auto synthesized = build(swap); const auto target = makeUCxTarget(); - ASSERT_TRUE(mlir::succeeded( - runPass(*synthesized, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *synthesized, target, mlir::qco::createTargetNativeSynthesis()))); EXPECT_EQ(countOps(*synthesized), 0U); EXPECT_GT(countOps(*synthesized), 0U); - ASSERT_TRUE(mlir::succeeded( - runPass(*synthesized, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *synthesized, target, mlir::qco::createVerifyTargetConformance()))); ASSERT_TRUE(mlir::succeeded(mlir::verify(*synthesized))); expectEquivalent(expected, synthesized); } @@ -443,13 +496,14 @@ TEST_F(TargetSynthesisTest, return builder.intConstant(0); }); const auto target = makeOneWayUCxTarget(); + attachTestEnvironment(*module, target); const auto before = printModule(*module); - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *module, target, mlir::qco::createTargetNativeSynthesis()))); EXPECT_EQ(printModule(*module), before); - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *module, target, mlir::qco::createVerifyTargetConformance()))); } TEST_F(TargetSynthesisTest, @@ -467,11 +521,11 @@ TEST_F(TargetSynthesisTest, const auto before = printModule(*synthesized); const auto target = makeOneWayUCxTarget(); - ASSERT_TRUE(mlir::succeeded( - runPass(*synthesized, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *synthesized, target, mlir::qco::createTargetNativeSynthesis()))); EXPECT_NE(printModule(*synthesized), before); - ASSERT_TRUE(mlir::succeeded( - runPass(*synthesized, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *synthesized, target, mlir::qco::createVerifyTargetConformance()))); ASSERT_TRUE(mlir::succeeded(mlir::verify(*synthesized))); expectEquivalent(expected, synthesized); } @@ -487,17 +541,16 @@ TEST_F(TargetSynthesisTest, MappingLeavesDirectionRepairToSynthesis) { const auto target = makeOneWayUCxTarget(Connectivity::fromCouplings({{0, 1}})); ASSERT_TRUE(mlir::succeeded( - runPass(*moduleOp, - mlir::qco::createMappingPass( - target, mlir::qco::MappingPassOptions{ - .niterations = 1, .ntrials = 1, .seed = 42})))); + runTargetPass(*moduleOp, target, + mlir::qco::createMappingPass(mlir::qco::MappingPassOptions{ + .niterations = 1, .ntrials = 1, .seed = 42})))); EXPECT_EQ(countOps(*moduleOp), 0U); auto expected = mlir::OwningOpRef(moduleOp->clone()); - ASSERT_TRUE(mlir::succeeded( - runPass(*moduleOp, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *moduleOp, target, mlir::qco::createTargetNativeSynthesis()))); EXPECT_EQ(countOps(*moduleOp), 2U); - ASSERT_TRUE(mlir::succeeded( - runPass(*moduleOp, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *moduleOp, target, mlir::qco::createVerifyTargetConformance()))); expectEquivalent(expected, moduleOp); } @@ -526,9 +579,10 @@ TEST_F(TargetSynthesisTest, RejectsUnknownSitesWithoutWideningNativeSupport) { ASSERT_TRUE(mlir::succeeded(mlir::qco::verifyLinearity(*moduleOp))); const auto target = makeOneWayUCxTarget(); for (auto pass : {false, true}) { - const auto diagnostics = expectFailure( - *moduleOp, pass ? mlir::qco::createTargetNativeSynthesis(target) - : mlir::qco::createVerifyTargetConformance(target)); + const auto diagnostics = + expectTargetFailure(*moduleOp, target, + pass ? mlir::qco::createTargetNativeSynthesis() + : mlir::qco::createVerifyTargetConformance()); EXPECT_NE(diagnostics.find("static sites"), std::string::npos); } } @@ -540,8 +594,9 @@ TEST_F(TargetSynthesisTest, ConformanceRejectsUnsupportedEntanglerDirection) { [[maybe_unused]] const auto [q0, q1] = builder.cx(q0Input, q1Input); return builder.intConstant(0); }); - const auto diagnostics = expectFailure( - *module, mlir::qco::createVerifyTargetConformance(makeOneWayUCxTarget())); + const auto target = makeOneWayUCxTarget(); + const auto diagnostics = expectTargetFailure( + *module, target, mlir::qco::createVerifyTargetConformance()); EXPECT_NE(diagnostics.find("target does not support operation 'qco.ctrl'"), std::string::npos) << diagnostics; @@ -564,10 +619,10 @@ TEST_F(TargetSynthesisTest, }); const auto target = makeOneWayUCxTarget(); - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *module, target, mlir::qco::createTargetNativeSynthesis()))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *module, target, mlir::qco::createVerifyTargetConformance()))); ASSERT_TRUE(mlir::succeeded(mlir::verify(*module))); } @@ -618,11 +673,11 @@ TEST_F(TargetSynthesisTest, ASSERT_TRUE(module); const auto target = makeOneWayUCxTarget(); - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *module, target, mlir::qco::createTargetNativeSynthesis()))); EXPECT_EQ(countOps(*module), 0U); - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *module, target, mlir::qco::createVerifyTargetConformance()))); ASSERT_TRUE(mlir::succeeded(mlir::verify(*module))); } @@ -646,8 +701,8 @@ TEST_F(TargetSynthesisTest, }); const auto target = makeOneWayUCxTarget(); - const auto diagnostics = - expectFailure(*module, mlir::qco::createTargetNativeSynthesis(target)); + const auto diagnostics = expectTargetFailure( + *module, target, mlir::qco::createTargetNativeSynthesis()); EXPECT_NE(diagnostics.find("consistent static sites"), std::string::npos); } @@ -678,8 +733,9 @@ TEST_F(TargetSynthesisTest, TargetNativeSynthesisRejectsAmbiguousBranchSites) { context.get()); ASSERT_TRUE(module); - const auto diagnostics = expectFailure( - *module, mlir::qco::createTargetNativeSynthesis(makeOneWayUCxTarget())); + const auto target = makeOneWayUCxTarget(); + const auto diagnostics = expectTargetFailure( + *module, target, mlir::qco::createTargetNativeSynthesis()); EXPECT_NE(diagnostics.find("consistent static sites"), std::string::npos) << diagnostics; } @@ -720,9 +776,9 @@ TEST_F(TargetSynthesisTest, RejectsLoopCarriedSitePermutations) { auto moduleOp = mlir::parseSourceString(source, context.get()); ASSERT_TRUE(moduleOp); ASSERT_TRUE(mlir::succeeded(mlir::verify(*moduleOp))); - const auto diagnostics = expectFailure( - *moduleOp, - mlir::qco::createTargetNativeSynthesis(makeOneWayUCxTarget())); + const auto target = makeOneWayUCxTarget(); + const auto diagnostics = expectTargetFailure( + *moduleOp, target, mlir::qco::createTargetNativeSynthesis()); EXPECT_NE(diagnostics.find("consistent static sites"), std::string::npos); } } @@ -754,12 +810,13 @@ TEST_F(TargetSynthesisTest, WhileResultsMayDifferFromLoopEntrySites) { ASSERT_TRUE(moduleOp); ASSERT_TRUE(mlir::succeeded(mlir::verify(*moduleOp))); const auto target = makeOneWayUCxTarget(); + attachTestEnvironment(*moduleOp, target); const auto before = printModule(*moduleOp); - ASSERT_TRUE(mlir::succeeded( - runPass(*moduleOp, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *moduleOp, target, mlir::qco::createTargetNativeSynthesis()))); EXPECT_EQ(printModule(*moduleOp), before); - ASSERT_TRUE(mlir::succeeded( - runPass(*moduleOp, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *moduleOp, target, mlir::qco::createVerifyTargetConformance()))); } TEST_F(TargetSynthesisTest, AcceptsMatchingBranchSitePermutations) { @@ -774,10 +831,10 @@ TEST_F(TargetSynthesisTest, AcceptsMatchingBranchSitePermutations) { return builder.intConstant(0); }); const auto target = makeOneWayUCxTarget(); - ASSERT_TRUE(mlir::succeeded( - runPass(*moduleOp, mlir::qco::createTargetNativeSynthesis(target)))); - ASSERT_TRUE(mlir::succeeded( - runPass(*moduleOp, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *moduleOp, target, mlir::qco::createTargetNativeSynthesis()))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *moduleOp, target, mlir::qco::createVerifyTargetConformance()))); } TEST_F(TargetSynthesisTest, @@ -794,8 +851,8 @@ TEST_F(TargetSynthesisTest, valid(Operation::create("cx", 2, 0)), }))); - const auto diagnostics = - expectFailure(*module, mlir::qco::createTargetNativeSynthesis(target)); + const auto diagnostics = expectTargetFailure( + *module, target, mlir::qco::createTargetNativeSynthesis()); EXPECT_NE(diagnostics.find("no usable synthesis basis"), std::string::npos) << diagnostics; } @@ -820,8 +877,8 @@ TEST_F(TargetSynthesisTest, valid(Operation::create("gphase", 0, 1)), }))); - const auto diagnostics = - expectFailure(*module, mlir::qco::createTargetNativeSynthesis(target)); + const auto diagnostics = expectTargetFailure( + *module, target, mlir::qco::createTargetNativeSynthesis()); EXPECT_NE(diagnostics.find( "no supported synthesis-basis placement is known for its " "static sites"), @@ -840,11 +897,11 @@ TEST_F(TargetSynthesisTest, auto synthesized = build(hadamard); const auto target = makeUCxTarget(); - ASSERT_TRUE(mlir::succeeded( - runPass(*synthesized, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *synthesized, target, mlir::qco::createTargetNativeSynthesis()))); EXPECT_EQ(countOps(*synthesized), 0U); - ASSERT_TRUE(mlir::succeeded( - runPass(*synthesized, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *synthesized, target, mlir::qco::createVerifyTargetConformance()))); expectEquivalent(expected, synthesized); } @@ -864,8 +921,8 @@ TEST_F(TargetSynthesisTest, ASSERT_TRUE(moduleOp); const auto target = makeUCxTarget(); - ASSERT_TRUE(mlir::succeeded( - runPass(*moduleOp, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *moduleOp, target, mlir::qco::createTargetNativeSynthesis()))); EXPECT_EQ(countOps(*moduleOp), 0U); EXPECT_EQ(countOps(*moduleOp), 0U); EXPECT_EQ(countOps(*moduleOp), 2U); @@ -875,8 +932,8 @@ TEST_F(TargetSynthesisTest, EXPECT_EQ(countOps(*moduleOp), 0U); EXPECT_EQ(countOps(*moduleOp), 0U); EXPECT_EQ(countOps(*moduleOp), 0U); - ASSERT_TRUE(mlir::succeeded( - runPass(*moduleOp, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *moduleOp, target, mlir::qco::createVerifyTargetConformance()))); } TEST_F(TargetSynthesisTest, SingleQubitSynthesisNeedsNoEntangler) { @@ -898,11 +955,11 @@ TEST_F(TargetSynthesisTest, SingleQubitSynthesisNeedsNoEntangler) { }))); ASSERT_TRUE(mlir::succeeded(mlir::verify(*synthesized))); - ASSERT_TRUE(mlir::succeeded( - runPass(*synthesized, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *synthesized, target, mlir::qco::createTargetNativeSynthesis()))); ASSERT_TRUE(mlir::succeeded(mlir::verify(*synthesized))); - ASSERT_TRUE(mlir::succeeded( - runPass(*synthesized, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *synthesized, target, mlir::qco::createVerifyTargetConformance()))); expectEquivalent(expected, synthesized); } @@ -922,12 +979,12 @@ TEST_F(TargetSynthesisTest, RuntimeSingleQubitSynthesisNeedsNoEntangler) { 1, Connectivity::allToAll(), NativeOperations::fromOperations({valid(Operation::create("u", 1, 3))}))); - ASSERT_TRUE(mlir::succeeded( - runPass(*moduleOp, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *moduleOp, target, mlir::qco::createTargetNativeSynthesis()))); ASSERT_TRUE(mlir::succeeded(mlir::verify(*moduleOp))); EXPECT_EQ(countOps(*moduleOp), 0U); - ASSERT_TRUE(mlir::succeeded( - runPass(*moduleOp, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *moduleOp, target, mlir::qco::createVerifyTargetConformance()))); } TEST_F(TargetSynthesisTest, TwoQubitSynthesisRequiresEntangler) { @@ -940,11 +997,12 @@ TEST_F(TargetSynthesisTest, TwoQubitSynthesisRequiresEntangler) { const auto target = valid(Target::create( 2, Connectivity::allToAll(), NativeOperations::fromOperations({valid(Operation::create("u", 1, 3))}))); + attachTestEnvironment(*moduleOp, target); ASSERT_TRUE(mlir::succeeded(mlir::verify(*moduleOp))); const auto before = printModule(*moduleOp); const auto diagnostics = - expectFailure(*moduleOp, mlir::qco::createTargetNativeSynthesis(target)); + expectFailure(*moduleOp, mlir::qco::createTargetNativeSynthesis()); EXPECT_NE(diagnostics.find("no usable two-qubit entangler"), std::string::npos) @@ -953,6 +1011,42 @@ TEST_F(TargetSynthesisTest, TwoQubitSynthesisRequiresEntangler) { EXPECT_TRUE(mlir::succeeded(mlir::verify(*moduleOp))); } +TEST(TargetSynthesisPassContract, LoadsMathDialectForRuntimeSynthesis) { + mlir::DialectRegistry registry; + registry.insert(); + mlir::MLIRContext context(registry); + context.getOrLoadDialect(); + auto moduleOp = mlir::parseSourceString(R"mlir( + module { + func.func @main(%theta: f64) -> !qco.qubit { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.rz(%theta) %q0 : !qco.qubit -> !qco.qubit + return %q1 : !qco.qubit + } + } + )mlir", + &context); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*moduleOp))); + const auto target = + valid(Target::create(2, Connectivity::allToAll(), + NativeOperations::fromOperations({ + valid(Operation::create("r", 1, 2)), + valid(Operation::create("cx", 2, 0)), + valid(Operation::create("gphase", 0, 1)), + }))); + + EXPECT_EQ(context.getLoadedDialect(), nullptr); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *moduleOp, target, mlir::qco::createTargetNativeSynthesis()))); + EXPECT_EQ(countOps(*moduleOp), 0U); + EXPECT_GT(countOps(*moduleOp), 0U); + EXPECT_TRUE(mlir::succeeded(mlir::verify(*moduleOp))); + EXPECT_TRUE(mlir::succeeded(runTargetPass( + *moduleOp, target, mlir::qco::createVerifyTargetConformance()))); +} + TEST_F(TargetSynthesisTest, DenseUnitaryHasAsymmetricTwoQubitDDSemantics) { const auto denseCx = [](QCOProgramBuilder& builder) { auto q0 = builder.staticQubit(0); @@ -989,11 +1083,11 @@ TEST_F(TargetSynthesisTest, auto synthesizedX = build(denseX); const auto target = makeUCxTarget(); - ASSERT_TRUE(mlir::succeeded( - runPass(*synthesizedX, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *synthesizedX, target, mlir::qco::createTargetNativeSynthesis()))); EXPECT_EQ(countOps(*synthesizedX), 0U); - ASSERT_TRUE(mlir::succeeded(runPass( - *synthesizedX, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *synthesizedX, target, mlir::qco::createVerifyTargetConformance()))); expectEquivalent(expectedX, synthesizedX); const auto denseCx = [](QCOProgramBuilder& builder) { @@ -1011,11 +1105,11 @@ TEST_F(TargetSynthesisTest, auto expectedCx = build(cxReference); auto synthesizedCx = build(denseCx); - ASSERT_TRUE(mlir::succeeded( - runPass(*synthesizedCx, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *synthesizedCx, target, mlir::qco::createTargetNativeSynthesis()))); EXPECT_EQ(countOps(*synthesizedCx), 0U); - ASSERT_TRUE(mlir::succeeded(runPass( - *synthesizedCx, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *synthesizedCx, target, mlir::qco::createVerifyTargetConformance()))); expectEquivalent(expectedCx, synthesizedCx); } @@ -1031,12 +1125,13 @@ TEST_F(TargetSynthesisTest, TargetNativeSynthesisPreservesNativeSwap) { NativeOperations::fromOperations( {valid(Operation::create("swap", 2, 0))}))); ASSERT_FALSE(swapTarget.synthesisBasis()); + attachTestEnvironment(*module, swapTarget); const auto before = printModule(*module); - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createTargetNativeSynthesis(swapTarget)))); - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createVerifyTargetConformance(swapTarget)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *module, swapTarget, mlir::qco::createTargetNativeSynthesis()))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *module, swapTarget, mlir::qco::createVerifyTargetConformance()))); EXPECT_EQ(countOps(*module), 1U); EXPECT_EQ(printModule(*module), before); } @@ -1057,11 +1152,11 @@ TEST_F(TargetSynthesisTest, TargetNativeSynthesisPreservesNativeGlobalPhase) { valid(Operation::create("gphase", 0, 1)), }))); - ASSERT_TRUE(mlir::succeeded( - runPass(*synthesized, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *synthesized, target, mlir::qco::createTargetNativeSynthesis()))); EXPECT_EQ(countOps(*synthesized), 1U); - ASSERT_TRUE(mlir::succeeded( - runPass(*synthesized, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *synthesized, target, mlir::qco::createVerifyTargetConformance()))); expectEquivalent(expected, synthesized); } @@ -1089,8 +1184,8 @@ TEST_F(TargetSynthesisTest, const auto target = valid(Target::create( 1, Connectivity::allToAll(), NativeOperations::fromOperations({}))); - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *module, target, mlir::qco::createTargetNativeSynthesis()))); EXPECT_EQ(countOps(*module), 1U); EXPECT_EQ(llvm::range_size(functions[0].getOps()), 1U); EXPECT_EQ(llvm::range_size(functions[1].getOps()), 0U); @@ -1110,13 +1205,13 @@ TEST_F(TargetSynthesisTest, 1, Connectivity::allToAll(), NativeOperations::fromOperations({valid(Operation::create("p", 1, 1))}))); - ASSERT_TRUE(mlir::succeeded( - runPass(*synthesized, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *synthesized, target, mlir::qco::createTargetNativeSynthesis()))); EXPECT_EQ(countOps(*synthesized), 0U); EXPECT_EQ(countOps(*synthesized), 0U); EXPECT_EQ(countOps(*synthesized), 1U); - ASSERT_TRUE(mlir::succeeded( - runPass(*synthesized, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *synthesized, target, mlir::qco::createVerifyTargetConformance()))); expectEquivalent(expected, synthesized); } @@ -1139,12 +1234,12 @@ TEST_F(TargetSynthesisTest, TargetNativeSynthesisUsesHomogeneousCapability) { ASSERT_TRUE(target.synthesisBasis()); ASSERT_EQ(target.synthesisBasis()->entangler, Target::GateKind::CZ); - ASSERT_TRUE(mlir::succeeded( - runPass(*synthesized, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *synthesized, target, mlir::qco::createTargetNativeSynthesis()))); EXPECT_EQ(countOps(*synthesized), 0U); EXPECT_GT(countOps(*synthesized), 0U); - ASSERT_TRUE(mlir::succeeded( - runPass(*synthesized, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *synthesized, target, mlir::qco::createVerifyTargetConformance()))); ASSERT_TRUE(mlir::succeeded(mlir::verify(*synthesized))); expectEquivalent(expected, synthesized); } @@ -1159,12 +1254,13 @@ TEST_F(TargetSynthesisTest, }); const auto permissive = valid(Target::create( 1, Connectivity::allToAll(), NativeOperations::unrestricted())); + attachTestEnvironment(*module, permissive); const auto before = printModule(*module); - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createTargetNativeSynthesis(permissive)))); - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createVerifyTargetConformance(permissive)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *module, permissive, mlir::qco::createTargetNativeSynthesis()))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *module, permissive, mlir::qco::createVerifyTargetConformance()))); EXPECT_EQ(printModule(*module), before); } @@ -1180,12 +1276,13 @@ TEST_F(TargetSynthesisTest, NativePowShellHidesItsImplementationBody) { NativeOperations::fromOperations( {valid(Operation::create("pow", 1, 1))}))); ASSERT_FALSE(powOnly.synthesisBasis()); + attachTestEnvironment(*module, powOnly); const auto before = printModule(*module); - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createTargetNativeSynthesis(powOnly)))); - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createVerifyTargetConformance(powOnly)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *module, powOnly, mlir::qco::createTargetNativeSynthesis()))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *module, powOnly, mlir::qco::createVerifyTargetConformance()))); EXPECT_EQ(printModule(*module), before); } @@ -1216,8 +1313,9 @@ TEST_F(TargetSynthesisTest, RejectsUnsupportedMultiTargetControlShell) { ASSERT_TRUE(moduleOp); ASSERT_TRUE(mlir::succeeded(mlir::verify(*moduleOp))); ASSERT_TRUE(mlir::succeeded(mlir::qco::verifyLinearity(*moduleOp))); - const auto diagnostics = expectFailure( - *moduleOp, mlir::qco::createTargetNativeSynthesis(makeUCxTarget())); + const auto target = makeUCxTarget(); + const auto diagnostics = expectTargetFailure( + *moduleOp, target, mlir::qco::createTargetNativeSynthesis()); EXPECT_NE(diagnostics.find("unitary matrix is not available"), std::string::npos); } @@ -1233,11 +1331,12 @@ TEST_F(TargetSynthesisTest, MissingBasisIsDiagnosedOnlyWhenLoweringIsNeeded) { qubit = builder.h(qubit); return builder.intConstant(0); }); + attachTestEnvironment(*supported, hOnly); const auto before = printModule(*supported); - ASSERT_TRUE(mlir::succeeded( - runPass(*supported, mlir::qco::createTargetNativeSynthesis(hOnly)))); - ASSERT_TRUE(mlir::succeeded( - runPass(*supported, mlir::qco::createVerifyTargetConformance(hOnly)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *supported, hOnly, mlir::qco::createTargetNativeSynthesis()))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *supported, hOnly, mlir::qco::createVerifyTargetConformance()))); EXPECT_EQ(printModule(*supported), before); auto unsupported = build([](QCOProgramBuilder& builder) { @@ -1245,8 +1344,8 @@ TEST_F(TargetSynthesisTest, MissingBasisIsDiagnosedOnlyWhenLoweringIsNeeded) { qubit = builder.x(qubit); return builder.intConstant(0); }); - const auto diagnostics = expectFailure( - *unsupported, mlir::qco::createTargetNativeSynthesis(hOnly)); + const auto diagnostics = expectTargetFailure( + *unsupported, hOnly, mlir::qco::createTargetNativeSynthesis()); EXPECT_NE(diagnostics.find("target-native synthesis cannot lower operation " "'qco.x'"), std::string::npos) @@ -1274,12 +1373,13 @@ TEST_F(TargetSynthesisTest, SupportedRuntimeParameterizedGateStaysUntouched) { valid(Operation::create("u", 1, 3)), valid(Operation::create("rxx", 2, 1)), }))); + attachTestEnvironment(*module, target); const auto before = printModule(*module); - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *module, target, mlir::qco::createTargetNativeSynthesis()))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *module, target, mlir::qco::createVerifyTargetConformance()))); EXPECT_EQ(printModule(*module), before); } @@ -1300,10 +1400,10 @@ TEST_F(TargetSynthesisTest, const auto target = makeOneWayRxxTarget(); ASSERT_FALSE(target.synthesisBasis()); - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *module, target, mlir::qco::createTargetNativeSynthesis()))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *module, target, mlir::qco::createVerifyTargetConformance()))); ASSERT_TRUE(mlir::succeeded(mlir::verify(*module))); RXXOp rxx; @@ -1337,8 +1437,9 @@ TEST_F(TargetSynthesisTest, )mlir", context.get()); ASSERT_TRUE(module); - const auto diagnostics = expectFailure( - *module, mlir::qco::createTargetNativeSynthesis(makeUCxTarget())); + const auto target = makeUCxTarget(); + const auto diagnostics = expectTargetFailure( + *module, target, mlir::qco::createTargetNativeSynthesis()); EXPECT_NE(diagnostics.find("target-native synthesis cannot lower operation " "'qco.rxx'"), std::string::npos) @@ -1365,9 +1466,9 @@ TEST_F(TargetSynthesisTest, )mlir", context.get()); ASSERT_TRUE(module); - - const auto diagnostics = expectFailure( - *module, mlir::qco::createTargetNativeSynthesis(makeUCxTarget())); + const auto target = makeUCxTarget(); + const auto diagnostics = expectTargetFailure( + *module, target, mlir::qco::createTargetNativeSynthesis()); EXPECT_NE(diagnostics.find("unitary matrix is not available"), std::string::npos); } @@ -1387,10 +1488,10 @@ TEST_F(TargetSynthesisTest, std::tie(q20, q10) = builder.cx(q20, q10); return builder.intConstant(0); }); - ASSERT_TRUE(mlir::succeeded( - runPass(*reversed, mlir::qco::createTargetNativeSynthesis(target)))); - ASSERT_TRUE(mlir::succeeded( - runPass(*reversed, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *reversed, target, mlir::qco::createTargetNativeSynthesis()))); + ASSERT_TRUE(mlir::succeeded(runTargetPass( + *reversed, target, mlir::qco::createVerifyTargetConformance()))); auto unknownSite = build([](QCOProgramBuilder& builder) { const auto q30Input = builder.staticQubit(30); @@ -1398,8 +1499,8 @@ TEST_F(TargetSynthesisTest, [[maybe_unused]] auto [q30, q20] = builder.cx(q30Input, q20Input); return builder.intConstant(0); }); - const auto diagnostics = expectFailure( - *unknownSite, mlir::qco::createVerifyTargetConformance(target)); + const auto diagnostics = expectTargetFailure( + *unknownSite, target, mlir::qco::createVerifyTargetConformance()); EXPECT_NE(diagnostics.find("target does not contain static site 30"), std::string::npos) << diagnostics; @@ -1411,8 +1512,8 @@ TEST_F(TargetSynthesisTest, ConformanceRejectsDynamicAllocations) { NativeOperations::fromOperations({valid(Operation::create("x", 1, 0))}))); const auto expectDynamicAllocationFailure = [&](OwningOpRef module) { - const auto diagnostics = expectFailure( - *module, mlir::qco::createVerifyTargetConformance(target)); + const auto diagnostics = expectTargetFailure( + *module, target, mlir::qco::createVerifyTargetConformance()); EXPECT_NE( diagnostics.find("requires qubits to be assigned to qco.static"), std::string::npos) @@ -1446,8 +1547,8 @@ TEST_F(TargetSynthesisTest, ConformanceRejectsQuantumFunctionInputs) { 1, Connectivity::allToAll(), NativeOperations::fromOperations({valid(Operation::create("x", 1, 0))}))); - const auto diagnostics = - expectFailure(*module, mlir::qco::createVerifyTargetConformance(target)); + const auto diagnostics = expectTargetFailure( + *module, target, mlir::qco::createVerifyTargetConformance()); EXPECT_NE(diagnostics.find("requires quantum function inputs to be assigned " "to qco.static target sites"), std::string::npos) @@ -1459,8 +1560,8 @@ TEST_F(TargetSynthesisTest, ConformanceChecksTypeArityAndParameters) { OwningOpRef module, const std::string& operation, const std::string& details) { - const auto diagnostics = expectFailure( - *module, mlir::qco::createVerifyTargetConformance(target)); + const auto diagnostics = expectTargetFailure( + *module, target, mlir::qco::createVerifyTargetConformance()); EXPECT_NE(diagnostics.find(operation), std::string::npos) << diagnostics; EXPECT_NE(diagnostics.find(details), std::string::npos) << diagnostics; }; @@ -1514,8 +1615,8 @@ TEST_F(TargetSynthesisTest, ConformanceChecksNonUnitaryCapabilities) { const auto xOnly = valid(Target::create( 1, Connectivity::allToAll(), NativeOperations::fromOperations({valid(Operation::create("x", 1, 0))}))); - const auto diagnostics = - expectFailure(*module, mlir::qco::createVerifyTargetConformance(xOnly)); + const auto diagnostics = expectTargetFailure( + *module, xOnly, mlir::qco::createVerifyTargetConformance()); EXPECT_NE(diagnostics.find("'qco.measure' with arity 1 and 0 parameter(s)"), std::string::npos) << diagnostics; diff --git a/python/mqt/core/mlir.pyi b/python/mqt/core/mlir.pyi index 9f389b5a86..e608caaa80 100644 --- a/python/mqt/core/mlir.pyi +++ b/python/mqt/core/mlir.pyi @@ -59,6 +59,87 @@ class OutputFormat(enum.Enum): QIR_ADAPTIVE = 7 """QIR for the Adaptive Profile.""" +class PayloadEncoding(enum.Enum): + """Payload representation encoding.""" + + TEXT = 0 + + BINARY = 1 + +class PayloadFormat: + """Exact payload identity.""" + + def __init__( + self, format_id: str, version: str, profile: str = "", encoding: PayloadEncoding = PayloadEncoding.TEXT + ) -> None: ... + @property + def format_id(self) -> str: ... + @format_id.setter + def format_id(self, arg: str, /) -> None: ... + @property + def version(self) -> str: ... + @version.setter + def version(self, arg: str, /) -> None: ... + @property + def profile(self) -> str: ... + @profile.setter + def profile(self, arg: str, /) -> None: ... + @property + def encoding(self) -> PayloadEncoding: ... + @encoding.setter + def encoding(self, arg: PayloadEncoding, /) -> None: ... + +class ProgramConstraint: + """One payload capability constraint.""" + + def __init__(self, constraint_id: str, value: int) -> None: ... + @property + def constraint_id(self) -> str: ... + @constraint_id.setter + def constraint_id(self, arg: str, /) -> None: ... + @property + def value(self) -> int: ... + @value.setter + def value(self, arg: int, /) -> None: ... + +class ProgramCapability: + """One payload execution capability.""" + + def __init__(self, capability_id: str, value: int = 0, constraints: Sequence[ProgramConstraint] = []) -> None: ... + @property + def capability_id(self) -> str: ... + @capability_id.setter + def capability_id(self, arg: str, /) -> None: ... + @property + def value(self) -> int: ... + @value.setter + def value(self, arg: int, /) -> None: ... + @property + def constraints(self) -> list[ProgramConstraint]: ... + @constraints.setter + def constraints(self, arg: Sequence[ProgramConstraint], /) -> None: ... + +class PayloadSpecification: + """Selected payload execution contract.""" + + def __init__( + self, + payload_format: PayloadFormat, + capabilities: Sequence[ProgramCapability] = [], + optional_capabilities_known: bool = False, + ) -> None: ... + @property + def format(self) -> PayloadFormat: + """The exact selected payload format.""" + + @property + def capabilities(self) -> list[ProgramCapability]: + """The effective payload capabilities.""" + + @property + def optional_capabilities_known(self) -> bool: + """Whether optional capability metadata is complete.""" + class CompilerTarget: """Immutable MLIR compiler target. @@ -390,6 +471,18 @@ class CompilerTarget: ) -> bool: """Whether the target supports an operation.""" +class TargetEnvironment: + """A compiler target and its selected payload specification.""" + + def __init__(self, target: CompilerTarget, payload_specification: PayloadSpecification) -> None: ... + @property + def target(self) -> CompilerTarget: + """The compiler target.""" + + @property + def payload_specification(self) -> PayloadSpecification: + """The selected payload specification.""" + class Program: """Base class for a typed MLIR compiler program. @@ -541,7 +634,7 @@ class QCOProgram(Program): """Decompose controlled X/Z/SWAP gates, qco.rccx, and constant-angle phase gates that act on at least min_qubits qubits (min_qubits must be at least 3; default 3 means wider than two-qubit).""" def compile_for_target( - self, target: CompilerTarget, *, enable_timing: bool = False, enable_statistics: bool = False + self, target_environment: TargetEnvironment, *, enable_timing: bool = False, enable_statistics: bool = False ) -> None: """Compile this QCO program for the target in place. Do not rely on its contents if compilation fails.""" @@ -758,7 +851,6 @@ def compile_program( *, output: Literal[OutputFormat.QC, OutputFormat.QC_IMPORT] = ..., inplace: bool = False, - target: CompilerTarget | None = None, qco_pipeline: str = "mqt-qco-default", enable_timing: bool = False, enable_statistics: bool = False, @@ -775,7 +867,6 @@ def compile_program( *, output: Literal[OutputFormat.QCO, OutputFormat.QCO_OPTIMIZED], inplace: bool = False, - target: CompilerTarget | None = None, qco_pipeline: str = "mqt-qco-default", enable_timing: bool = False, enable_statistics: bool = False, @@ -808,7 +899,6 @@ def compile_program( *, output: Literal[OutputFormat.JEFF], inplace: bool = False, - target: CompilerTarget | None = None, qco_pipeline: str = "mqt-qco-default", enable_timing: bool = False, enable_statistics: bool = False, @@ -825,7 +915,6 @@ def compile_program( *, output: Literal[OutputFormat.QIR_BASE, OutputFormat.QIR_ADAPTIVE], inplace: bool = False, - target: CompilerTarget | None = None, qco_pipeline: str = "mqt-qco-default", enable_timing: bool = False, enable_statistics: bool = False, @@ -842,7 +931,6 @@ def compile_program( *, output: OutputFormat, inplace: bool = False, - target: CompilerTarget | None = None, qco_pipeline: str = "mqt-qco-default", enable_timing: bool = False, enable_statistics: bool = False, @@ -859,13 +947,42 @@ def compile_program( program: Source text, a file path, a Qiskit circuit, or a typed compiler program. output: The requested output stage of the compiler pipeline. inplace: Whether a typed input program may be consumed. - target: An optional compiler target for decomposition, mapping, and native - synthesis. A target requires optimized QCO, QC, or QIR output. qco_pipeline: The QCO optimization pipeline to run. A custom pipeline - cannot be combined with a target. + cannot be combined with target compilation. enable_timing: Whether to collect pass timing information. enable_statistics: Whether to collect pass statistics. Returns: A typed compiler program for the requested output format. """ + +@overload +def compile_program( + program: str + | os.PathLike[str] + | qiskit.circuit.QuantumCircuit + | QCProgram + | QCOProgram + | JeffProgram + | OpenQASMProgram, + *, + inplace: bool = False, + target_environment: TargetEnvironment, + enable_timing: bool = False, + enable_statistics: bool = False, +) -> OpenQASMProgram | QIRProgram: + """Compile a program for a target and return the selected executable payload. + + The payload specification determines the output format. Typed program inputs + are copied by default; set ``inplace=True`` to consume them. + + Args: + program: Source text, a file path, a Qiskit circuit, or a typed compiler program. + inplace: Whether a typed input program may be consumed. + target_environment: The compiler target and selected payload specification. + enable_timing: Whether to collect pass timing information. + enable_statistics: Whether to collect pass statistics. + + Returns: + A typed compiler program for the selected payload format. + """ diff --git a/test/python/qdmi/test_qdmi.py b/test/python/qdmi/test_qdmi.py index 79ed627385..ca0161bedb 100644 --- a/test/python/qdmi/test_qdmi.py +++ b/test/python/qdmi/test_qdmi.py @@ -21,7 +21,17 @@ import pytest from packaging import version -from mqt.core.mlir import CompilerTarget, OutputFormat, compile_program +from mqt.core.mlir import ( + CompilerTarget, + OutputFormat, + PayloadEncoding, + PayloadFormat, + PayloadSpecification, + QIRProfile, + QIRProgram, + TargetEnvironment, + compile_program, +) from mqt.core.qdmi import ( CustomProperty, Device, @@ -556,7 +566,10 @@ def test_device_executes_qir_program(ddsim_device: Device) -> None: c = measure q; """ target = CompilerTarget.from_device(ddsim_device) - program = compile_program(qasm3_program, output=OutputFormat.QIR_BASE, target=target) + payload = PayloadSpecification(PayloadFormat("qir", "2.1.0", "base", PayloadEncoding.TEXT)) + program = compile_program(qasm3_program, target_environment=TargetEnvironment(target, payload)) + assert isinstance(program, QIRProgram) + assert program.profile == QIRProfile.BASE assert ProgramFormat.QIR_BASE_STRING in ddsim_device.supported_program_formats() job = ddsim_device.submit_job(program.llvm_ir, ProgramFormat.QIR_BASE_STRING, num_shots=1024) @@ -593,7 +606,9 @@ def test_device_executes_controlled_qir_with_exact_phase(ddsim_device: Device) - expected = quantum_info.Statevector.from_instruction(circuit).data target = CompilerTarget.from_device(ddsim_device) - program = compile_program(circuit, output=OutputFormat.QIR_BASE, target=target) + payload = PayloadSpecification(PayloadFormat("qir", "2.1.0", "base", PayloadEncoding.TEXT)) + program = compile_program(circuit, target_environment=TargetEnvironment(target, payload)) + assert isinstance(program, QIRProgram) job = ddsim_device.submit_job(program.llvm_ir, ProgramFormat.QIR_BASE_STRING, num_shots=0) job.wait() diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index e3e5935d78..58cf3bb378 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -27,10 +27,16 @@ JeffProgram, OpenQASMProgram, OutputFormat, + PayloadEncoding, + PayloadFormat, + PayloadSpecification, + ProgramCapability, + ProgramConstraint, QCOProgram, QCProgram, QIRProfile, QIRProgram, + TargetEnvironment, compile_program, ) from mqt.core.qdmi.driver import open_device @@ -75,6 +81,24 @@ """ +def _test_payload_specification() -> PayloadSpecification: + """Return one explicit selected payload contract for target tests.""" + return PayloadSpecification( + PayloadFormat("qir", "2.1.0", "base", PayloadEncoding.BINARY), + [ProgramCapability("forward-branching", 0, [ProgramConstraint("max-control-flow-nesting-depth", 8)])], + optional_capabilities_known=True, + ) + + +def _test_target_environment(target: CompilerTarget) -> TargetEnvironment: + """Pair a compiler target with the test payload specification. + + Returns: + The complete target environment. + """ + return TargetEnvironment(target, _test_payload_specification()) + + def _assert_bell_program(program: QCProgram, *, measured: bool = False) -> None: """Check the semantics of a translated Bell-state program.""" assert program.is_valid @@ -365,7 +389,7 @@ def test_empty_compiled_program_round_trips_through_mlir() -> None: connectivity=CompilerTarget.Connectivity.all_to_all(), native_operations=CompilerTarget.NativeOperations.unrestricted(), ) - program.compile_for_target(target) + program.compile_for_target(_test_target_environment(target)) assert "qco." not in program.ir qc = QCOProgram.from_mlir_str(program.ir).to_qc() @@ -388,22 +412,42 @@ def garnet_target() -> CompilerTarget: def test_compile_program_for_qdmi_target(garnet_target: CompilerTarget) -> None: """Compile through the canonical target pipeline for a QDMI device.""" - result = compile_program( - QASM_STRING, - output=OutputFormat.QCO_OPTIMIZED, - target=garnet_target, - ) + result = compile_program(QASM_STRING, target_environment=_test_target_environment(garnet_target)) - assert isinstance(result, QCOProgram) - static_sites = {int(site) for site in re.findall(r"qco\.static (\d+)", result.ir)} + assert isinstance(result, QIRProgram) + assert result.profile == QIRProfile.BASE + + mapped = compile_program(QASM_STRING, output=OutputFormat.QCO) + assert isinstance(mapped, QCOProgram) + mapped.compile_for_target(_test_target_environment(garnet_target)) + static_sites = {int(site) for site in re.findall(r"qco\.static (\d+)", mapped.ir)} assert len(static_sites) == 2 assert static_sites <= {site.id for site in garnet_target.sites} - assert "qco.r(" in result.ir - assert "qco.ctrl" in result.ir - assert "qco.z " in result.ir - assert result.ir.count("qco.measure") == 2 - assert "qco.rx" not in result.ir - assert "qco.ry" not in result.ir + assert "qco.r(" in mapped.ir + assert "qco.ctrl" in mapped.ir + assert "qco.z " in mapped.ir + assert mapped.ir.count("qco.measure") == 2 + assert "qco.rx" not in mapped.ir + assert "qco.ry" not in mapped.ir + + +def test_compile_program_rejects_unsupported_target_payload_without_consuming_input() -> None: + """Reject an unsupported selected payload before consuming typed input.""" + program = compile_program(QASM_STRING, output=OutputFormat.QCO) + assert isinstance(program, QCOProgram) + environment = TargetEnvironment( + CompilerTarget( + 1, + connectivity=CompilerTarget.Connectivity.all_to_all(), + native_operations=CompilerTarget.NativeOperations.unrestricted(), + ), + PayloadSpecification(PayloadFormat("example.payload", "1.0.0")), + ) + + with pytest.raises(ValueError, match="cannot emit the selected payload format"): + compile_program(program, inplace=True, target_environment=environment) + + assert program.is_valid def test_qco_program_compiles_for_direct_sparse_target() -> None: @@ -428,7 +472,7 @@ def test_qco_program_compiles_for_direct_sparse_target() -> None: qco = compile_program(QASM_STRING, output=OutputFormat.QCO) assert isinstance(qco, QCOProgram) - qco.compile_for_target(target) + qco.compile_for_target(_test_target_environment(target)) assert {int(site) for site in re.findall(r"qco\.static (\d+)", qco.ir)} == {10, 20} assert "qco.u(" in qco.ir @@ -459,7 +503,7 @@ def test_target_compiles_single_qubit_gates_without_entangler(num_sites: int) -> source.ry(0.123, site) program = QCProgram.from_qiskit(source).to_qco() - program.compile_for_target(target) + program.compile_for_target(_test_target_environment(target)) assert program.is_valid result = program.to_qc().to_qiskit(target=target) @@ -475,12 +519,9 @@ def test_target_compilation_exports_canonical_physical_qiskit_circuit() -> None: connectivity=CompilerTarget.Connectivity.all_to_all(), native_operations=CompilerTarget.NativeOperations.unrestricted(), ) - mapped = compile_program( - QASM_STRING, - output=OutputFormat.QCO_OPTIMIZED, - target=target, - ) + mapped = compile_program(QASM_STRING, output=OutputFormat.QCO) assert isinstance(mapped, QCOProgram) + mapped.compile_for_target(_test_target_environment(target)) assert 0 < mapped.ir.count("qco.static") < target.num_sites qc = mapped.to_qc(copy=True) @@ -582,6 +623,57 @@ def test_compiler_target_accepts_plain_site_tuples(arity: int | CompilerTarget.O CompilerTarget.Operation("cx", arity, 0, site_tuples=[(0,)]) +def test_payload_specification_preserves_python_api() -> None: + """Construct and validate one context-free selected payload contract.""" + payload_format = PayloadFormat("qir", "2.1.0", "base", PayloadEncoding.BINARY) + constraint = ProgramConstraint("max-control-flow-nesting-depth", 8) + capability = ProgramCapability("forward-branching", 0, [constraint]) + environment = PayloadSpecification( + payload_format, + [capability], + optional_capabilities_known=True, + ) + + assert environment.format.format_id == "qir" + assert environment.format.version == "2.1.0" + assert environment.format.profile == "base" + assert environment.format.encoding == PayloadEncoding.BINARY + assert environment.capabilities[0].capability_id == "forward-branching" + assert environment.capabilities[0].value == 0 + assert environment.capabilities[0].constraints[0].constraint_id == "max-control-flow-nesting-depth" + assert environment.capabilities[0].constraints[0].value == 8 + assert environment.optional_capabilities_known + + payload_format.version = "9.9.9" + exposed_descriptor = environment.format + exposed_descriptor.version = "8.8.8" + capability.value = 1 + assert environment.format.version == "2.1.0" + assert environment.capabilities[0].value == 0 + + with pytest.raises(ValueError, match=r"major\[\.minor\[\.patch\]\]"): + PayloadSpecification(PayloadFormat("qir", "2.1.0.1", "base")) + + +@pytest.mark.parametrize( + ("format_id", "version", "profile", "expected_version", "expected_type"), + [("qir", "2.1", "base", "2.1.0", QIRProgram), ("openqasm", "3", "", "3.0.0", OpenQASMProgram)], +) +def test_target_compilation_accepts_exact_version_shorthand( + format_id: str, version: str, profile: str, expected_version: str, expected_type: type +) -> None: + """Normalize a shortened version before selecting the compiler output.""" + payload = PayloadSpecification(PayloadFormat(format_id, version, profile)) + assert payload.format.version == expected_version + target = CompilerTarget( + 2, + connectivity=CompilerTarget.Connectivity.all_to_all(), + native_operations=CompilerTarget.NativeOperations.unrestricted(), + ) + result = compile_program(QASM_STRING, target_environment=TargetEnvironment(target, payload)) + assert isinstance(result, expected_type) + + def test_compiler_target_construction_preserves_validation_errors() -> None: """Translate explicit C++ construction errors to Python ``ValueError``.""" with pytest.raises(TypeError): diff --git a/test/python/test_mlir_qiskit_translation.py b/test/python/test_mlir_qiskit_translation.py index ba95b3c1f1..e69dea7c0f 100644 --- a/test/python/test_mlir_qiskit_translation.py +++ b/test/python/test_mlir_qiskit_translation.py @@ -41,7 +41,16 @@ from qiskit.quantum_info import Operator, random_unitary from qiskit_support import supports_qiskit_translation -from mqt.core.mlir import CompilerTarget, QCProgram, compile_program, sample +from mqt.core.mlir import ( + CompilerTarget, + PayloadEncoding, + PayloadFormat, + PayloadSpecification, + QCProgram, + TargetEnvironment, + compile_program, + sample, +) if TYPE_CHECKING: from collections.abc import Callable @@ -52,6 +61,20 @@ pytest.skip(f"No registered Qiskit adapter for {qiskit.__version__}", allow_module_level=True) +def _test_payload_specification() -> PayloadSpecification: + """Return the selected payload contract for target tests.""" + return PayloadSpecification(PayloadFormat("qir", "2.1.0", "base", PayloadEncoding.BINARY)) + + +def _test_target_environment(target: CompilerTarget) -> TargetEnvironment: + """Pair a compiler target with the test payload specification. + + Returns: + The complete target environment. + """ + return TargetEnvironment(target, _test_payload_specification()) + + STANDARD_GATES = ( library.IGate(), library.XGate(), @@ -231,7 +254,7 @@ def test_two_qubit_dense_unitary_compiles_to_target_basis() -> None: ) program = QCProgram.from_qiskit(circuit).to_qco(copy=True) - program.compile_for_target(target) + program.compile_for_target(_test_target_environment(target)) restored = program.to_qc(copy=True).to_qiskit(target=target) assert "qco.unitary" not in program.ir @@ -546,7 +569,7 @@ def test_target_compiled_openqasm2_measurements_export() -> None: """ ) mapped = program.to_qco(copy=True) - mapped.compile_for_target(target) + mapped.compile_for_target(_test_target_environment(target)) restored = mapped.to_qc(copy=True).to_qiskit(target=target)