From 5b4c1a46b4fd8e6f774019e7329e841e10946cab Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Sun, 23 Aug 2026 18:39:01 +0000 Subject: [PATCH 01/11] =?UTF-8?q?=E2=9C=A8=20Record=20typed=20compiler=20t?= =?UTF-8?q?arget=20environments=20in=20MQT=20IR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Record complete compiler-target facts and one exact payload execution contract as typed module metadata. Assisted-by: GPT-5.6 Sol via Codex --- .agent/plans/typed-target-environment.md | 211 ++++++++++++++++++ .../mlir/Dialect/MQT/IR/MQTAttributes.h | 1 + .../include/mlir/Dialect/MQT/IR/MQTDialect.td | 89 ++++++++ mlir/lib/Dialect/MQT/IR/CMakeLists.txt | 1 + mlir/lib/Dialect/MQT/IR/MQTDialect.cpp | 151 +++++++++++++ mlir/unittests/Dialect/MQT/IR/CMakeLists.txt | 1 + mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp | 165 +++++++++++++- 7 files changed, 618 insertions(+), 1 deletion(-) create mode 100644 .agent/plans/typed-target-environment.md diff --git a/.agent/plans/typed-target-environment.md b/.agent/plans/typed-target-environment.md new file mode 100644 index 0000000000..b646db7891 --- /dev/null +++ b/.agent/plans/typed-target-environment.md @@ -0,0 +1,211 @@ +# Record complete compiler target environments in MLIR + +This ExecPlan is a living document. The sections `Progress`, +`Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must +be kept up to date as work proceeds. + +This ExecPlan must be maintained in accordance with `.agent/PLANS.md` from the +repository root. + +## Purpose / Big Picture + +An MLIR module must carry enough target information to reproduce and inspect a +compilation without hidden C++ pass state. After this change, the module-level +`mqt.target_env` attribute records both the immutable hardware target and the +exact selected payload environment. A textual MLIR round trip and a +`CompilerTarget` round trip demonstrate that no source metadata or derived cache +is lost. + +## Progress + +- [x] (2026-08-23 17:36Z) Rebased the existing payload-attribute foundation on + the explicit compiler-target fact model. +- [x] (2026-08-23 17:36Z) Inspected the compiler target, MQT dialect, build + boundaries, existing payload attributes, and focused tests. +- [x] (2026-08-23 18:04Z) Defined and verified the typed target-environment + attribute graph. +- [x] (2026-08-23 18:04Z) Added lossless `CompilerTarget` materialization and + reconstruction. +- [x] (2026-08-23 18:04Z) Added structural, textual round-trip, DLTI-query, and + C++ target round-trip tests. +- [x] (2026-08-23 18:09Z) Built the focused targets, generated dialect + documentation, and ran clang-tidy and the full lint session. +- [x] (2026-08-23 18:12Z) Restacked and updated pull request #2215. + +## Surprises & Discoveries + +- Observation: A single optional capability list cannot retain a known payload + baseline when provider-specific optional metadata is unknown. Evidence: the + old `TargetEnvAttr` used list absence for all unknown metadata, so a producer + could not record baseline capabilities and unknown optional capabilities at + the same time. +- Observation: The generic DLTI operation query selects one attached query + interface and can be ambiguous if a module also has a data-layout attribute. + Evidence: MLIR's DLTI query helper walks attached attributes rather than + selecting `mqt.target_env` by its canonical name. + +## Decision Log + +- Decision: `mqt.target_env` contains one typed compilation target and one typed + payload environment. Rationale: mapping and synthesis depend on hardware + facts, while control-flow legalization and terminal lowering depend on the + selected payload; both are required to replay compilation. Date/Author: + 2026-08-23, Codex. +- Decision: Keep `CompilerTarget` as a context-free immutable C++ snapshot and + convert it at the compiler-to-IR boundary. Rationale: public target objects + remain cheap to copy and do not depend on an MLIR context. Date/Author: + 2026-08-23, Codex. +- Decision: Store source facts only. Rationale: gate bases, adjacency, shortest + paths, canonical names, and other caches can be recomputed by the validated + C++ constructor. Date/Author: 2026-08-23, Codex. +- Decision: Do not connect this change to the work-in-progress QDMI v1.4 API. + Rationale: the attribute schema is provider-neutral, and the later QDMI + adapter can map stable descriptors and features into it without making the MQT + dialect depend on QDMI. Date/Author: 2026-08-23, Codex. +- Decision: Use direct typed lookup by the canonical module attribute name as + the authoritative access path. Rationale: DLTI remains useful for nested + extension queries but is not an unambiguous module-level selector when other + DLTI attributes are present. Date/Author: 2026-08-23, Codex. + +## Outcomes & Retrospective + +The typed target schema and C++ conversion are implemented and published in pull +request #2215. The focused MQT IR and full compiler suites, dialect +documentation, clang-tidy, and lint pass. + +## Context and Orientation + +`mlir/include/mlir/Compiler/Target.h` and `mlir/lib/Compiler/Target.cpp` define +and validate `CompilerTarget`. The value contains ordered sites, optional timing +metadata, connectivity whose state is unknown, all-to-all, or explicit, and +native operations whose state is unknown, unrestricted, or explicit. Its private +storage also contains derived routing and synthesis caches; those caches must +not appear in textual IR. + +`mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td` defines shared MQT attributes. +The current payload foundation records an exact descriptor and extensible +capability IDs. `mlir/lib/Dialect/MQT/IR/MQTDialect.cpp` implements structural +verification and enforces that `mqt.target_env` is attached only to a module. +`mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp` parses and verifies textual MQT +IR. + +A payload descriptor is the exact tuple of format ID, semantic version, profile, +and text or binary encoding. A capability is a positive execution guarantee +scoped to that descriptor. A constraint is a typed upper bound or other +condition on one capability. Different constraints on one capability are +conjunctive. An empty constraint list means unrestricted support for that +capability. + +## Plan of Work + +Replace the payload-only target attribute graph in +`mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td` with typed attributes for a +payload descriptor, program constraints, program capabilities, a payload +environment, duration units, sites, site tuples, operations, connectivity, +native operations, a compilation target, and the combined target environment. +Use enums for closed knowledge states and string IDs for extensible capabilities +and constraints. Verify descriptor versions as canonical `major.minor.patch` +semantic versions. A payload environment always stores its complete known +effective capability list and separately records whether optional capability +metadata is complete. + +The compilation-target attributes must preserve the exact source facts accepted +by `CompilerTarget`. Unknown, unrestricted, and explicit states must remain +distinct even when their associated lists are empty. Verify invalid kind/list +combinations, duplicate IDs and tuples, missing duration units, bad site +references, and non-finite or out-of-range numeric metadata. Let the C++ target +constructor remain the final semantic validator instead of duplicating derived +graph checks in the dialect. + +Make the combined target environment implement `DLTIQueryInterface`. Expose the +typed compilation target and payload environment under reserved MQT query keys, +then delegate provider- or dialect-namespaced keys to an optional `dlti.map`. +Reject unnamespaced extension keys and collisions with reserved keys. Keep +direct `ModuleOp::getAttrOfType(TargetEnvAttr::name)` lookup as +the compiler contract. + +Add conversion methods in `mlir/include/mlir/Compiler/Target.h` and +`mlir/lib/Compiler/Target.cpp`. Materialization creates an +`mqt::CompilationTargetAttr` in a supplied `MLIRContext`. Reconstruction reads +the typed attributes and calls existing validated `CompilerTarget::create` +factories. Link `MQTCompilerTarget` to `MLIRMQTDialect`; do not add QDMI or a +new bridge library. + +Extend the MQT IR and compiler target tests. One textual module must contain a +complete target with ordered sparse site IDs, explicit topology, constrained and +unconstrained payload capabilities, site-specific operation metadata, and a +namespaced extension. Additional tests cover unknown and unrestricted target +facts, known-empty versus incomplete optional capability metadata, malformed +semantic versions and target records, module-only placement, DLTI queries, and +all `CompilerTarget` to attribute to `CompilerTarget` round trips. + +## Concrete Steps + +Run all commands from the repository root. Build the dialect and compiler test +targets with: + + cmake --build --preset release --target mqt-core-mlir-unittest-mqt-ir mqt-core-mlir-unittests-compiler mlir-doc + +Run the focused binaries: + + ./build/release/mlir/unittests/Dialect/MQT/IR/mqt-core-mlir-unittest-mqt-ir + ./build/release/mlir/unittests/Compiler/mqt-core-mlir-unittests-compiler --gtest_filter='CompilerTarget.*' + +Finish with: + + uvx nox -s lint + git diff --check + +All tests must report zero failures. The generated dialect documentation must +build without warnings from the new attribute definitions. + +## Validation and Acceptance + +Acceptance requires a parsed and printed `mqt.target_env` whose compilation +target and payload environment are structurally equal after round trip. A +context-free `CompilerTarget` with sparse site IDs, explicit couplings, native +operations, timing, and fidelity must produce a compilation-target attribute +that reconstructs to the same public facts. Separate targets with unknown, +unrestricted, and explicit facts must remain different after reconstruction. + +Invalid semantic versions, duplicate constraint IDs, invalid kind/list pairs, +unknown site references, timing without a duration unit, unnamespaced extension +keys, and non-module attachment must fail with specific diagnostics. A direct +DLTI query on the attribute must return both reserved MQT values and a nested +extension value. + +## Idempotence and Recovery + +Builds, tests, formatting, and documentation generation are safe to repeat. The +work is stacked on the compiler-target prerequisite and does not change the QDMI +dependency. Preserve the recorded pre-stack backup ref before rewriting the +published pull-request branch. Use the exact recorded remote commit as the lease +when publication is ready. + +## Artifacts and Notes + +The payload completeness distinction is observable as follows: + + capabilities = [], optional_capabilities_known = true + +means that no optional capability is supported, while: + + capabilities = [baseline capability], optional_capabilities_known = false + +keeps the known baseline and states that provider-specific optional metadata is +not available. + +## Interfaces and Dependencies + +Use ODS `AttrDef` types and generated parsers and printers. Use LLVM containers +inside MLIR implementation code. Use `mlir::DLTIQueryInterface` only as the +query layer; do not implement `DataLayoutSpecInterface` or +`TargetSystemSpecInterface`. Keep format, profile, capability, constraint, and +extension IDs extensible strings. Use typed enums only for payload encoding, +connectivity state, and native-operation state. Add no generic dictionary +snapshot, target technology enum, capability-policy helper, QDMI header, or +derived compiler cache. + +Revision note: This plan expands the payload-only pull request after the +compiler target gained explicit knowledge states. It records the complete target +environment and keeps QDMI integration as a later adapter change. diff --git a/mlir/include/mlir/Dialect/MQT/IR/MQTAttributes.h b/mlir/include/mlir/Dialect/MQT/IR/MQTAttributes.h index 1cdaaefcae..f19956c036 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/MQTAttributes.h +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTAttributes.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include diff --git a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td index 6607a81fec..89b2fe4878 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td @@ -12,6 +12,7 @@ include "mlir/IR/AttrTypeBase.td" include "mlir/IR/EnumAttr.td" include "mlir/IR/OpBase.td" +include "mlir/Interfaces/DataLayoutInterfaces.td" def MQTDialect : Dialect { let name = "mqt"; @@ -45,6 +46,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 +222,90 @@ 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 PayloadDescriptorAttr + : MQTAttr<"PayloadDescriptor", "payload_descriptor"> { + 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. + }]; + 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 PayloadEnvAttr : MQTAttr<"PayloadEnv", "payload_env"> { + let summary = "Selected payload execution contract"; + let description = [{ + Records the exact payload and its effective capabilities. Producers expand + descriptor 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. + }]; + let parameters = (ins "PayloadDescriptorAttr":$descriptor, + MQTArrayRefParameter<"ProgramCapabilityAttr">:$capabilities, + "bool":$optional_capabilities_known); + let assemblyFormat = "`<` struct(params) `>`"; + let genVerifyDecl = 1; +} + +def TargetEnvAttr + : MQTAttr<"TargetEnv", "target_env", [DLTIQueryInterface]> { + let summary = "Selected compilation and payload environment"; + let description = [{ + Combines typed compiler-target facts with the selected payload contract. + Optional DLTI extensions carry provider-specific facts. Direct access to + the typed fields is authoritative. + }]; + let parameters = (ins "CompilationTargetAttr":$compilation_target, + "PayloadEnvAttr":$payload_env, + OptionalParameter<"::mlir::MapAttr">:$extensions); + let assemblyFormat = "`<` struct(params) `>`"; + let genVerifyDecl = 1; + let extraClassDeclaration = [{ + static constexpr ::llvm::StringLiteral kCompilationTargetKey = + "mqt.compilation_target"; + static constexpr ::llvm::StringLiteral kPayloadEnvKey = "mqt.payload_env"; + + ::mlir::FailureOr<::mlir::Attribute> + query(::mlir::DataLayoutEntryKey key) const; + }]; +} + #endif // MLIR_DIALECT_MQT_IR_MQTDIALECT_TD diff --git a/mlir/lib/Dialect/MQT/IR/CMakeLists.txt b/mlir/lib/Dialect/MQT/IR/CMakeLists.txt index d30513cca9..03cf59f80e 100644 --- a/mlir/lib/Dialect/MQT/IR/CMakeLists.txt +++ b/mlir/lib/Dialect/MQT/IR/CMakeLists.txt @@ -17,6 +17,7 @@ add_mlir_dialect_library( PRIVATE LLVMSupport MLIRCBitDialect + MLIRDLTIDialect MLIRFuncDialect MLIRIR MLIRMemRefDialect diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index 804575a7df..fd1d472ddf 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -26,6 +26,8 @@ #include #include // IWYU pragma: keep #include +#include +#include #include #include #include @@ -38,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -65,6 +68,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.getMinor() && + parsed.getSubminor() && !parsed.getBuild() && + parsed.getAsString() == version; +} + +LogicalResult PayloadDescriptorAttr::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 descriptor requires an ID and version"; + } + if (id.getValue().contains('\0') || version.getValue().contains('\0') || + profile.getValue().contains('\0')) { + return emitError() + << "payload descriptor fields must not contain null characters"; + } + if (!isCanonicalPayloadVersion(version.getValue())) { + return emitError() + << "payload descriptor version must use canonical 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 +PayloadEnvAttr::verify(const function_ref emitError, + const PayloadDescriptorAttr /*descriptor*/, + 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 environment 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) { @@ -285,6 +370,59 @@ LogicalResult CompilationTargetAttr::verify( return success(); } +[[nodiscard]] static bool isNamespacedExtensionKey(const StringRef key) { + SmallVector components; + key.split(components, '.'); + return components.size() > 1 && + llvm::none_of(components, [](const StringRef component) { + return component.empty(); + }); +} + +LogicalResult +TargetEnvAttr::verify(const function_ref emitError, + const CompilationTargetAttr /*compilationTarget*/, + const PayloadEnvAttr /*payloadEnv*/, + const MapAttr extensions) { + if (!extensions) { + return success(); + } + for (const DataLayoutEntryInterface entry : extensions.getEntries()) { + const auto key = entry.getKey().dyn_cast(); + if (!key) { + return emitError() + << "target environment extension keys must be identifiers"; + } + const auto value = key.getValue(); + if (!isNamespacedExtensionKey(value)) { + return emitError() << "target environment extension key '" << value + << "' must be provider or dialect namespaced"; + } + if (value == kCompilationTargetKey || value == kPayloadEnvKey) { + return emitError() << "target environment extension key '" << value + << "' is reserved by MQT"; + } + } + return success(); +} + +FailureOr TargetEnvAttr::query(const DataLayoutEntryKey key) const { + const auto identifier = key.dyn_cast(); + if (!identifier) { + return failure(); + } + if (identifier.getValue() == kCompilationTargetKey) { + return getCompilationTarget(); + } + if (identifier.getValue() == kPayloadEnvKey) { + return getPayloadEnv(); + } + if (MapAttr extensions = getExtensions()) { + return extensions.query(key); + } + return failure(); +} + [[nodiscard]] static LogicalResult verifyEntryPoint(Operation* operation, const NamedAttribute attribute) { if (!isa(attribute.getValue())) { @@ -630,6 +768,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/unittests/Dialect/MQT/IR/CMakeLists.txt b/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt index cf0a00d560..d918bb9038 100644 --- a/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt +++ b/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt @@ -13,6 +13,7 @@ target_link_libraries( PRIVATE GTest::gtest_main MLIRArithDialect MLIRCBitDialect + MLIRDLTIDialect MLIRFuncDialect MLIRMemRefDialect MLIRMQTDialect diff --git a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp index f282a53bf8..7929544dc7 100644 --- a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp +++ b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp @@ -22,6 +22,8 @@ #include #include #include +#include +#include #include #include #include @@ -47,7 +49,7 @@ class MQTIRTest : public ::testing::Test { void SetUp() override { DialectRegistry registry; - registry.insert(); context = std::make_unique(registry); @@ -62,6 +64,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 +721,158 @@ 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 = [], + duration = 60, fidelity = 9.800000e-01 : f64>]>, + payload_env = #mqt.payload_env< + descriptor = #mqt.payload_descriptor, + capabilities = []>], + optional_capabilities_known = false>, + extensions = #dlti.map<"vendor.queue_depth" = 8 : i64>> + } { + 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 payloadEnv = targetEnv.getPayloadEnv(); + EXPECT_EQ(payloadEnv.getDescriptor().getId().getValue(), "vendor-ir"); + EXPECT_EQ(payloadEnv.getDescriptor().getVersion().getValue(), "4.2.0"); + EXPECT_EQ(payloadEnv.getDescriptor().getProfile().getValue(), "dynamic"); + EXPECT_EQ(payloadEnv.getDescriptor().getEncoding(), + mqt::PayloadEncoding::Binary); + EXPECT_FALSE(payloadEnv.getOptionalCapabilitiesKnown()); + ASSERT_EQ(payloadEnv.getCapabilities().size(), 1U); + ASSERT_EQ(payloadEnv.getCapabilities().front().getConstraints().size(), 1U); + + auto query = cast(targetEnv); + auto targetResult = query.query(StringAttr::get( + context.get(), mqt::TargetEnvAttr::kCompilationTargetKey)); + ASSERT_TRUE(succeeded(targetResult)); + EXPECT_EQ(*targetResult, compilationTarget); + auto payloadResult = query.query( + StringAttr::get(context.get(), mqt::TargetEnvAttr::kPayloadEnvKey)); + ASSERT_TRUE(succeeded(payloadResult)); + EXPECT_EQ(*payloadResult, payloadEnv); + auto extensionResult = + query.query(StringAttr::get(context.get(), "vendor.queue_depth")); + ASSERT_TRUE(succeeded(extensionResult)); + EXPECT_EQ(cast(*extensionResult).getInt(), 8); + EXPECT_TRUE(failed(query.query(IntegerType::get(context.get(), 32)))); + + const auto reparsed = roundTrip(*moduleOp); + ASSERT_TRUE(reparsed); + EXPECT_EQ((*reparsed)->getAttr(mqt::TargetEnvAttr::name), targetEnv); +} + +TEST_F(MQTIRTest, RejectsInvalidPayloadContracts) { + EXPECT_FALSE(parseAttr(R"mlir(#mqt.payload_descriptor)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.payload_descriptor)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.payload_descriptor)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.payload_descriptor)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.payload_descriptor)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_env< + descriptor = #mqt.payload_descriptor, + capabilities = [, + ], + optional_capabilities_known = true>)mlir")); +} + +TEST_F(MQTIRTest, RejectsInvalidTargetEnvironmentExtensions) { + EXPECT_FALSE(parseAttr(R"mlir(#mqt.target_env< + compilation_target = #mqt.compilation_target< + sites = [], connectivity = all_to_all, couplings = [], + native_operations = unrestricted, operations = []>, + payload_env = #mqt.payload_env< + descriptor = #mqt.payload_descriptor, capabilities = [], + optional_capabilities_known = false>, + extensions = #dlti.map<"unnamespaced" = 1 : i64>>)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.target_env< + compilation_target = #mqt.compilation_target< + sites = [], connectivity = all_to_all, couplings = [], + native_operations = unrestricted, operations = []>, + payload_env = #mqt.payload_env< + descriptor = #mqt.payload_descriptor, capabilities = [], + optional_capabilities_known = false>, + extensions = #dlti.map<"mqt.payload_env" = 1 : i64>>)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_env = #mqt.payload_env< + descriptor = #mqt.payload_descriptor, + capabilities = [], optional_capabilities_known = false>> + } { return } + } + )mlir")); +} + } // namespace From 741c2cdf0c034a99eb5404b2857f672af6e1f7cc Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Sun, 23 Aug 2026 19:08:53 +0000 Subject: [PATCH 02/11] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Define=20target=20co?= =?UTF-8?q?mpilation=20with=20payload=20specifications?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Represent the selected hardware target and payload specification as one validated target environment. Cache that value through the MLIR analysis manager for mapping, native synthesis, and conformance. Derive targeted compiler output from the selected payload format in the C++, Python, and mqt-cc APIs. Keep untargeted output selection independent. Assisted-by: GPT-5.6 Sol via Codex --- .../selected-payload-target-environment.md | 55 +++ .agent/plans/typed-target-environment.md | 211 ----------- CHANGELOG.md | 10 +- bindings/mlir/register_mlir.cpp | 130 ++++++- bindings/patterns.txt | 21 +- docs/glossary.md | 14 + docs/mlir/target_compilation.md | 78 +++-- docs/qdmi/ddsim_device.md | 13 +- mlir/include/mlir/Compiler/Programs.h | 14 +- .../include/mlir/Compiler/TargetEnvironment.h | 163 +++++++++ .../include/mlir/Dialect/MQT/IR/MQTDialect.td | 32 +- .../Dialect/QCO/Transforms/Mapping/Mapping.h | 4 - .../mlir/Dialect/QCO/Transforms/Passes.h | 13 +- .../mlir/Dialect/QCO/Transforms/Passes.td | 27 ++ mlir/lib/Compiler/CMakeLists.txt | 13 +- mlir/lib/Compiler/Pipeline.cpp | 60 ++-- mlir/lib/Compiler/TargetCompilation.cpp | 6 +- mlir/lib/Compiler/TargetEnvironment.cpp | 301 ++++++++++++++++ mlir/lib/Dialect/MQT/IR/MQTDialect.cpp | 33 +- .../QCO/Transforms/Mapping/Mapping.cpp | 27 +- .../NativeSynthesis/TargetSynthesis.cpp | 60 ++-- mlir/lib/Support/Passes.cpp | 3 + mlir/tools/mqt-cc/mqt-cc.cpp | 108 ++++-- .../Compiler/mqt-cc/verify_qir_output.cmake | 90 ++++- .../Compiler/test_compiler_pipeline.cpp | 189 ++++++---- .../Compiler/test_compiler_target.cpp | 140 ++++++++ mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp | 77 ++-- .../QCO/Transforms/Mapping/test_mapping.cpp | 43 ++- .../NativeSynthesis/test_target_synthesis.cpp | 330 +++++++++++------- python/mqt/core/mlir.pyi | 135 ++++++- test/python/qdmi/test_qdmi.py | 21 +- test/python/test_mlir.py | 111 +++++- test/python/test_mlir_qiskit_translation.py | 29 +- 33 files changed, 1885 insertions(+), 676 deletions(-) create mode 100644 .agent/plans/selected-payload-target-environment.md delete mode 100644 .agent/plans/typed-target-environment.md create mode 100644 mlir/include/mlir/Compiler/TargetEnvironment.h create mode 100644 mlir/lib/Compiler/TargetEnvironment.cpp diff --git a/.agent/plans/selected-payload-target-environment.md b/.agent/plans/selected-payload-target-environment.md new file mode 100644 index 0000000000..4988517238 --- /dev/null +++ b/.agent/plans/selected-payload-target-environment.md @@ -0,0 +1,55 @@ +# Independent compiler capability prototype + +Status: independently rebased and locally validated; contract design remains +gated. + +## 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 remaining payload model is a non-blocking Core 4.1 candidate. Core #2365 and +QDMI #523 must record the contract decisions before this prototype is considered +merge-ready. 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 same +validated target that was attached to the module. + +## 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 release build passes, with 3,879 native tests passing and one existing +optional-device test skipping. All 558 targeted Python tests pass with the +superconducting reference device enabled. Stub generation and C++ lint pass. diff --git a/.agent/plans/typed-target-environment.md b/.agent/plans/typed-target-environment.md deleted file mode 100644 index b646db7891..0000000000 --- a/.agent/plans/typed-target-environment.md +++ /dev/null @@ -1,211 +0,0 @@ -# Record complete compiler target environments in MLIR - -This ExecPlan is a living document. The sections `Progress`, -`Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must -be kept up to date as work proceeds. - -This ExecPlan must be maintained in accordance with `.agent/PLANS.md` from the -repository root. - -## Purpose / Big Picture - -An MLIR module must carry enough target information to reproduce and inspect a -compilation without hidden C++ pass state. After this change, the module-level -`mqt.target_env` attribute records both the immutable hardware target and the -exact selected payload environment. A textual MLIR round trip and a -`CompilerTarget` round trip demonstrate that no source metadata or derived cache -is lost. - -## Progress - -- [x] (2026-08-23 17:36Z) Rebased the existing payload-attribute foundation on - the explicit compiler-target fact model. -- [x] (2026-08-23 17:36Z) Inspected the compiler target, MQT dialect, build - boundaries, existing payload attributes, and focused tests. -- [x] (2026-08-23 18:04Z) Defined and verified the typed target-environment - attribute graph. -- [x] (2026-08-23 18:04Z) Added lossless `CompilerTarget` materialization and - reconstruction. -- [x] (2026-08-23 18:04Z) Added structural, textual round-trip, DLTI-query, and - C++ target round-trip tests. -- [x] (2026-08-23 18:09Z) Built the focused targets, generated dialect - documentation, and ran clang-tidy and the full lint session. -- [x] (2026-08-23 18:12Z) Restacked and updated pull request #2215. - -## Surprises & Discoveries - -- Observation: A single optional capability list cannot retain a known payload - baseline when provider-specific optional metadata is unknown. Evidence: the - old `TargetEnvAttr` used list absence for all unknown metadata, so a producer - could not record baseline capabilities and unknown optional capabilities at - the same time. -- Observation: The generic DLTI operation query selects one attached query - interface and can be ambiguous if a module also has a data-layout attribute. - Evidence: MLIR's DLTI query helper walks attached attributes rather than - selecting `mqt.target_env` by its canonical name. - -## Decision Log - -- Decision: `mqt.target_env` contains one typed compilation target and one typed - payload environment. Rationale: mapping and synthesis depend on hardware - facts, while control-flow legalization and terminal lowering depend on the - selected payload; both are required to replay compilation. Date/Author: - 2026-08-23, Codex. -- Decision: Keep `CompilerTarget` as a context-free immutable C++ snapshot and - convert it at the compiler-to-IR boundary. Rationale: public target objects - remain cheap to copy and do not depend on an MLIR context. Date/Author: - 2026-08-23, Codex. -- Decision: Store source facts only. Rationale: gate bases, adjacency, shortest - paths, canonical names, and other caches can be recomputed by the validated - C++ constructor. Date/Author: 2026-08-23, Codex. -- Decision: Do not connect this change to the work-in-progress QDMI v1.4 API. - Rationale: the attribute schema is provider-neutral, and the later QDMI - adapter can map stable descriptors and features into it without making the MQT - dialect depend on QDMI. Date/Author: 2026-08-23, Codex. -- Decision: Use direct typed lookup by the canonical module attribute name as - the authoritative access path. Rationale: DLTI remains useful for nested - extension queries but is not an unambiguous module-level selector when other - DLTI attributes are present. Date/Author: 2026-08-23, Codex. - -## Outcomes & Retrospective - -The typed target schema and C++ conversion are implemented and published in pull -request #2215. The focused MQT IR and full compiler suites, dialect -documentation, clang-tidy, and lint pass. - -## Context and Orientation - -`mlir/include/mlir/Compiler/Target.h` and `mlir/lib/Compiler/Target.cpp` define -and validate `CompilerTarget`. The value contains ordered sites, optional timing -metadata, connectivity whose state is unknown, all-to-all, or explicit, and -native operations whose state is unknown, unrestricted, or explicit. Its private -storage also contains derived routing and synthesis caches; those caches must -not appear in textual IR. - -`mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td` defines shared MQT attributes. -The current payload foundation records an exact descriptor and extensible -capability IDs. `mlir/lib/Dialect/MQT/IR/MQTDialect.cpp` implements structural -verification and enforces that `mqt.target_env` is attached only to a module. -`mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp` parses and verifies textual MQT -IR. - -A payload descriptor is the exact tuple of format ID, semantic version, profile, -and text or binary encoding. A capability is a positive execution guarantee -scoped to that descriptor. A constraint is a typed upper bound or other -condition on one capability. Different constraints on one capability are -conjunctive. An empty constraint list means unrestricted support for that -capability. - -## Plan of Work - -Replace the payload-only target attribute graph in -`mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td` with typed attributes for a -payload descriptor, program constraints, program capabilities, a payload -environment, duration units, sites, site tuples, operations, connectivity, -native operations, a compilation target, and the combined target environment. -Use enums for closed knowledge states and string IDs for extensible capabilities -and constraints. Verify descriptor versions as canonical `major.minor.patch` -semantic versions. A payload environment always stores its complete known -effective capability list and separately records whether optional capability -metadata is complete. - -The compilation-target attributes must preserve the exact source facts accepted -by `CompilerTarget`. Unknown, unrestricted, and explicit states must remain -distinct even when their associated lists are empty. Verify invalid kind/list -combinations, duplicate IDs and tuples, missing duration units, bad site -references, and non-finite or out-of-range numeric metadata. Let the C++ target -constructor remain the final semantic validator instead of duplicating derived -graph checks in the dialect. - -Make the combined target environment implement `DLTIQueryInterface`. Expose the -typed compilation target and payload environment under reserved MQT query keys, -then delegate provider- or dialect-namespaced keys to an optional `dlti.map`. -Reject unnamespaced extension keys and collisions with reserved keys. Keep -direct `ModuleOp::getAttrOfType(TargetEnvAttr::name)` lookup as -the compiler contract. - -Add conversion methods in `mlir/include/mlir/Compiler/Target.h` and -`mlir/lib/Compiler/Target.cpp`. Materialization creates an -`mqt::CompilationTargetAttr` in a supplied `MLIRContext`. Reconstruction reads -the typed attributes and calls existing validated `CompilerTarget::create` -factories. Link `MQTCompilerTarget` to `MLIRMQTDialect`; do not add QDMI or a -new bridge library. - -Extend the MQT IR and compiler target tests. One textual module must contain a -complete target with ordered sparse site IDs, explicit topology, constrained and -unconstrained payload capabilities, site-specific operation metadata, and a -namespaced extension. Additional tests cover unknown and unrestricted target -facts, known-empty versus incomplete optional capability metadata, malformed -semantic versions and target records, module-only placement, DLTI queries, and -all `CompilerTarget` to attribute to `CompilerTarget` round trips. - -## Concrete Steps - -Run all commands from the repository root. Build the dialect and compiler test -targets with: - - cmake --build --preset release --target mqt-core-mlir-unittest-mqt-ir mqt-core-mlir-unittests-compiler mlir-doc - -Run the focused binaries: - - ./build/release/mlir/unittests/Dialect/MQT/IR/mqt-core-mlir-unittest-mqt-ir - ./build/release/mlir/unittests/Compiler/mqt-core-mlir-unittests-compiler --gtest_filter='CompilerTarget.*' - -Finish with: - - uvx nox -s lint - git diff --check - -All tests must report zero failures. The generated dialect documentation must -build without warnings from the new attribute definitions. - -## Validation and Acceptance - -Acceptance requires a parsed and printed `mqt.target_env` whose compilation -target and payload environment are structurally equal after round trip. A -context-free `CompilerTarget` with sparse site IDs, explicit couplings, native -operations, timing, and fidelity must produce a compilation-target attribute -that reconstructs to the same public facts. Separate targets with unknown, -unrestricted, and explicit facts must remain different after reconstruction. - -Invalid semantic versions, duplicate constraint IDs, invalid kind/list pairs, -unknown site references, timing without a duration unit, unnamespaced extension -keys, and non-module attachment must fail with specific diagnostics. A direct -DLTI query on the attribute must return both reserved MQT values and a nested -extension value. - -## Idempotence and Recovery - -Builds, tests, formatting, and documentation generation are safe to repeat. The -work is stacked on the compiler-target prerequisite and does not change the QDMI -dependency. Preserve the recorded pre-stack backup ref before rewriting the -published pull-request branch. Use the exact recorded remote commit as the lease -when publication is ready. - -## Artifacts and Notes - -The payload completeness distinction is observable as follows: - - capabilities = [], optional_capabilities_known = true - -means that no optional capability is supported, while: - - capabilities = [baseline capability], optional_capabilities_known = false - -keeps the known baseline and states that provider-specific optional metadata is -not available. - -## Interfaces and Dependencies - -Use ODS `AttrDef` types and generated parsers and printers. Use LLVM containers -inside MLIR implementation code. Use `mlir::DLTIQueryInterface` only as the -query layer; do not implement `DataLayoutSpecInterface` or -`TargetSystemSpecInterface`. Keep format, profile, capability, constraint, and -extension IDs extensible strings. Use typed enums only for payload encoding, -connectivity state, and native-operation state. Add no generic dictionary -snapshot, target technology enum, capability-policy helper, QDMI header, or -derived compiler cache. - -Revision note: This plan expands the payload-only pull request after the -compiler target gained explicit knowledge states. It records the complete target -environment and keeps QDMI integration as a later adapter change. 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..acb1222284 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -15,18 +15,35 @@ 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") +payload = PayloadSpecification(PayloadFormat("qir", "2.1.0", "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. + +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. The target can also be constructed directly. Connectivity and native-operation support are required: @@ -90,17 +107,17 @@ 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 target passes read the typed `mqt.target_env` module attribute. +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, @@ -120,19 +137,23 @@ Select a device when compiling: ```console mqt-cc --qdmi-device=mqt.sc.iqm.garnet \ - --emit=qco-optimized input.qasm + --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,6 +163,7 @@ 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 @@ -152,12 +174,24 @@ if (!target) { return 1; } +auto payload = mlir::PayloadSpecification::create({ + .id = "qir", + .version = "2.1.0", + .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/TargetEnvironment.h b/mlir/include/mlir/Compiler/TargetEnvironment.h new file mode 100644 index 0000000000..a990eb1cf4 --- /dev/null +++ b/mlir/include/mlir/Compiler/TargetEnvironment.h @@ -0,0 +1,163 @@ +/* + * 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 payload execution 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. + [[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); + + /// 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: + ModuleOp moduleOp_; + Attribute attribute_; + std::optional environment_; + 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 89b2fe4878..1f6d76e134 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td @@ -222,16 +222,14 @@ def CompilationTargetAttr : MQTAttr<"CompilationTarget", "compilation_target"> { let genVerifyDecl = 1; } -def PayloadEncoding - : I32EnumAttr<"PayloadEncoding", "Payload encoding", - [I32EnumAttrCase<"Text", 0, "text">, - I32EnumAttrCase<"Binary", 1, "binary">]> { +def PayloadEncoding : I32EnumAttr<"PayloadEncoding", "Payload encoding", + [I32EnumAttrCase<"Text", 0, "text">, + I32EnumAttrCase<"Binary", 1, "binary">]> { let cppNamespace = "::mlir::mqt"; let genSpecializedAttr = 0; } -def PayloadDescriptorAttr - : MQTAttr<"PayloadDescriptor", "payload_descriptor"> { +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 @@ -244,8 +242,7 @@ def PayloadDescriptorAttr let genVerifyDecl = 1; } -def ProgramConstraintAttr - : MQTAttr<"ProgramConstraint", "program_constraint"> { +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. @@ -255,8 +252,7 @@ def ProgramConstraintAttr let genVerifyDecl = 1; } -def ProgramCapabilityAttr - : MQTAttr<"ProgramCapability", "program_capability"> { +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 @@ -269,39 +265,39 @@ def ProgramCapabilityAttr let genVerifyDecl = 1; } -def PayloadEnvAttr : MQTAttr<"PayloadEnv", "payload_env"> { +def PayloadSpecAttr : MQTAttr<"PayloadSpec", "payload_spec"> { let summary = "Selected payload execution contract"; let description = [{ Records the exact payload and its effective capabilities. Producers expand - descriptor baselines before creating this attribute. The capability list + 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. }]; - let parameters = (ins "PayloadDescriptorAttr":$descriptor, + 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", [DLTIQueryInterface]> { - let summary = "Selected compilation and payload environment"; +def TargetEnvAttr : MQTAttr<"TargetEnv", "target_env", [DLTIQueryInterface]> { + let summary = "Selected compilation and payload specification"; let description = [{ Combines typed compiler-target facts with the selected payload contract. Optional DLTI extensions carry provider-specific facts. Direct access to the typed fields is authoritative. }]; let parameters = (ins "CompilationTargetAttr":$compilation_target, - "PayloadEnvAttr":$payload_env, + "PayloadSpecAttr":$payload_specification, OptionalParameter<"::mlir::MapAttr">:$extensions); let assemblyFormat = "`<` struct(params) `>`"; let genVerifyDecl = 1; let extraClassDeclaration = [{ static constexpr ::llvm::StringLiteral kCompilationTargetKey = "mqt.compilation_target"; - static constexpr ::llvm::StringLiteral kPayloadEnvKey = "mqt.payload_env"; + static constexpr ::llvm::StringLiteral kPayloadSpecificationKey = + "mqt.payload_specification"; ::mlir::FailureOr<::mlir::Attribute> query(::mlir::DataLayoutEntryKey key) const; 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..c9b4013e6c 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h @@ -19,7 +19,7 @@ namespace mlir { class CompilerTarget; -} // namespace mlir +} namespace mlir::qco { @@ -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..2f2dede6a7 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -183,6 +183,33 @@ 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"]; + 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. + }]; +} + +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..32c1883b52 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,13 @@ 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) { + attachTargetEnvironment(mod(), environment); return succeeded(runQCOTransformPasses( mod(), - [&target](OpPassManager& pm) { - populateTargetCompilationPipeline(pm, target); + [&environment](OpPassManager& pm) { + populateTargetCompilationPipeline(pm, environment.target()); }, "failed to compile the QCO program for the target", enableTiming, enableStatistics)); @@ -418,23 +421,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 +473,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 +483,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 +523,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..c176d54629 100644 --- a/mlir/lib/Compiler/TargetCompilation.cpp +++ b/mlir/lib/Compiler/TargetCompilation.cpp @@ -29,16 +29,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..ee197e5c14 --- /dev/null +++ b/mlir/lib/Compiler/TargetEnvironment.cpp @@ -0,0 +1,301 @@ +/* + * 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 + +namespace mlir { + +[[nodiscard]] static llvm::Error invalidPayload(const llvm::Twine& message) { + return llvm::createStringError(llvm::errc::invalid_argument, + "Invalid payload specification: " + message); +} + +[[nodiscard]] static bool isCanonicalVersion(const llvm::StringRef version) { + llvm::VersionTuple parsed; + return !parsed.tryParse(version) && parsed.getMinor() && + parsed.getSubminor() && !parsed.getBuild() && + parsed.getAsString() == version; +} + +[[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"); + } + if (!isCanonicalVersion(format.version)) { + return invalidPayload( + "Payload format version must use canonical major.minor.patch"); + } + 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)) { + 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 { + return environment_.has_value(); +} + +const TargetEnvironment& +TargetEnvironmentAnalysis::environment() const noexcept { + assert(environment_.has_value()); + return *environment_; +} + +llvm::StringRef TargetEnvironmentAnalysis::error() const noexcept { + 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 fd1d472ddf..b5213ccda9 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -75,21 +75,22 @@ void MQTDialect::initialize() { parsed.getAsString() == version; } -LogicalResult PayloadDescriptorAttr::verify( - const function_ref emitError, const StringAttr id, - const StringAttr version, const StringAttr profile, - const PayloadEncoding /*encoding*/) { +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 descriptor requires an ID and version"; + 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 descriptor fields must not contain null characters"; + << "payload format fields must not contain null characters"; } if (!isCanonicalPayloadVersion(version.getValue())) { return emitError() - << "payload descriptor version must use canonical major.minor.patch"; + << "payload format version must use canonical major.minor.patch"; } return success(); } @@ -131,10 +132,10 @@ LogicalResult ProgramCapabilityAttr::verify( } LogicalResult -PayloadEnvAttr::verify(const function_ref emitError, - const PayloadDescriptorAttr /*descriptor*/, - const ArrayRef capabilities, - const bool /*optionalCapabilitiesKnown*/) { +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) { @@ -142,7 +143,7 @@ PayloadEnvAttr::verify(const function_ref emitError, std::pair(capability.getId().getValue(), capability.getValue()); if (!seen.insert(key).second) { return emitError() - << "payload environment contains duplicate capability '" + << "payload specification contains duplicate capability '" << capability.getId().getValue() << "' with value " << capability.getValue(); } @@ -382,7 +383,7 @@ LogicalResult CompilationTargetAttr::verify( LogicalResult TargetEnvAttr::verify(const function_ref emitError, const CompilationTargetAttr /*compilationTarget*/, - const PayloadEnvAttr /*payloadEnv*/, + const PayloadSpecAttr /*payloadSpecification*/, const MapAttr extensions) { if (!extensions) { return success(); @@ -398,7 +399,7 @@ TargetEnvAttr::verify(const function_ref emitError, return emitError() << "target environment extension key '" << value << "' must be provider or dialect namespaced"; } - if (value == kCompilationTargetKey || value == kPayloadEnvKey) { + if (value == kCompilationTargetKey || value == kPayloadSpecificationKey) { return emitError() << "target environment extension key '" << value << "' is reserved by MQT"; } @@ -414,8 +415,8 @@ FailureOr TargetEnvAttr::query(const DataLayoutEntryKey key) const { if (identifier.getValue() == kCompilationTargetKey) { return getCompilationTarget(); } - if (identifier.getValue() == kPayloadEnvKey) { - return getPayloadEnv(); + if (identifier.getValue() == kPayloadSpecificationKey) { + return getPayloadSpecification(); } if (MapAttr extensions = getExtensions()) { return extensions.query(key); 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..d5dcd56acf 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,9 @@ static int runCompiler(int argc, char** argv) { *parsedOutputFormat == OutputFormat::QIRAdaptive)) { pm.addPass(createInlinerPass()); } - if (compilerTarget) { - populateTargetCompilationPipeline(pm, *compilerTarget); + if (targetEnvironment) { + attachTargetEnvironment(*program.mod, *targetEnvironment); + populateTargetCompilationPipeline(pm, targetEnvironment->target()); return success(); } populateQCOCleanupPipeline(pm); @@ -638,7 +700,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..6cf1a8faea 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()); } @@ -1575,7 +1600,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 +1811,18 @@ 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); + 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 +1851,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 +1905,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 +1961,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 +2001,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 +2052,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 +2148,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 +2206,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 +2248,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 +2287,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 +2306,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 +2400,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 +2409,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 +2436,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 +2455,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..d578f62461 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -9,6 +9,7 @@ */ #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" @@ -21,9 +22,13 @@ #include #include #include +#include #include +#include #include #include +#include +#include #include #include @@ -63,6 +68,141 @@ 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", .profile = "base"}), + "Invalid payload specification: Payload format version must " + "use canonical 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(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 7929544dc7..f3e9117022 100644 --- a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp +++ b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp @@ -23,7 +23,6 @@ #include #include #include -#include #include #include #include @@ -33,6 +32,7 @@ #include #include #include +#include #include #include @@ -49,9 +49,9 @@ class MQTIRTest : public ::testing::Test { void SetUp() override { DialectRegistry registry; - registry.insert(); + registry.insert(); context = std::make_unique(registry); context->loadAllAvailableDialects(); } @@ -736,11 +736,11 @@ TEST_F(MQTIRTest, RoundTripsTypedTargetEnvironment) { native_operations = explicit, operations = [, num_parameters = 0, - site_tuples = [], duration = 60, fidelity = 9.800000e-01 : f64>]>, - payload_env = #mqt.payload_env< - descriptor = #mqt.payload_descriptor, capabilities = []>], @@ -774,25 +774,28 @@ TEST_F(MQTIRTest, RoundTripsTypedTargetEnvironment) { EXPECT_EQ(operationSites[0], 10); EXPECT_EQ(operationSites[1], 20); - const auto payloadEnv = targetEnv.getPayloadEnv(); - EXPECT_EQ(payloadEnv.getDescriptor().getId().getValue(), "vendor-ir"); - EXPECT_EQ(payloadEnv.getDescriptor().getVersion().getValue(), "4.2.0"); - EXPECT_EQ(payloadEnv.getDescriptor().getProfile().getValue(), "dynamic"); - EXPECT_EQ(payloadEnv.getDescriptor().getEncoding(), + 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(payloadEnv.getOptionalCapabilitiesKnown()); - ASSERT_EQ(payloadEnv.getCapabilities().size(), 1U); - ASSERT_EQ(payloadEnv.getCapabilities().front().getConstraints().size(), 1U); + EXPECT_FALSE(payloadSpecification.getOptionalCapabilitiesKnown()); + ASSERT_EQ(payloadSpecification.getCapabilities().size(), 1U); + ASSERT_EQ( + payloadSpecification.getCapabilities().front().getConstraints().size(), + 1U); auto query = cast(targetEnv); auto targetResult = query.query(StringAttr::get( context.get(), mqt::TargetEnvAttr::kCompilationTargetKey)); ASSERT_TRUE(succeeded(targetResult)); EXPECT_EQ(*targetResult, compilationTarget); - auto payloadResult = query.query( - StringAttr::get(context.get(), mqt::TargetEnvAttr::kPayloadEnvKey)); + auto payloadResult = query.query(StringAttr::get( + context.get(), mqt::TargetEnvAttr::kPayloadSpecificationKey)); ASSERT_TRUE(succeeded(payloadResult)); - EXPECT_EQ(*payloadResult, payloadEnv); + EXPECT_EQ(*payloadResult, payloadSpecification); auto extensionResult = query.query(StringAttr::get(context.get(), "vendor.queue_depth")); ASSERT_TRUE(succeeded(extensionResult)); @@ -804,16 +807,26 @@ TEST_F(MQTIRTest, RoundTripsTypedTargetEnvironment) { 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_descriptor)mlir")); - EXPECT_FALSE(parseAttr(R"mlir(#mqt.payload_descriptor)mlir")); - EXPECT_FALSE(parseAttr(R"mlir(#mqt.payload_descriptor)mlir")); - EXPECT_FALSE(parseAttr(R"mlir(#mqt.payload_descriptor)mlir")); - EXPECT_FALSE(parseAttr(R"mlir(#mqt.payload_descriptor)mlir")); EXPECT_FALSE( parseAttr(R"mlir(#mqt.program_constraint)mlir")); @@ -826,8 +839,8 @@ TEST_F(MQTIRTest, RejectsInvalidPayloadContracts) { EXPECT_FALSE(parseAttr(R"mlir(#mqt.program_capability, ]>)mlir")); - EXPECT_FALSE(parseAttr(R"mlir(#mqt.payload_env< - descriptor = #mqt.payload_descriptor, capabilities = [, ], @@ -839,8 +852,8 @@ TEST_F(MQTIRTest, RejectsInvalidTargetEnvironmentExtensions) { compilation_target = #mqt.compilation_target< sites = [], connectivity = all_to_all, couplings = [], native_operations = unrestricted, operations = []>, - payload_env = #mqt.payload_env< - descriptor = #mqt.payload_descriptor, capabilities = [], optional_capabilities_known = false>, extensions = #dlti.map<"unnamespaced" = 1 : i64>>)mlir")); @@ -848,11 +861,11 @@ TEST_F(MQTIRTest, RejectsInvalidTargetEnvironmentExtensions) { compilation_target = #mqt.compilation_target< sites = [], connectivity = all_to_all, couplings = [], native_operations = unrestricted, operations = []>, - payload_env = #mqt.payload_env< - descriptor = #mqt.payload_descriptor, capabilities = [], optional_capabilities_known = false>, - extensions = #dlti.map<"mqt.payload_env" = 1 : i64>>)mlir")); + extensions = #dlti.map<"mqt.payload_specification" = 1 : i64>>)mlir")); } TEST_F(MQTIRTest, RejectsTargetEnvironmentOutsideModule) { @@ -866,8 +879,8 @@ TEST_F(MQTIRTest, RejectsTargetEnvironmentOutsideModule) { compilation_target = #mqt.compilation_target< sites = [], connectivity = all_to_all, couplings = [], native_operations = unrestricted, operations = []>, - payload_env = #mqt.payload_env< - descriptor = #mqt.payload_descriptor, capabilities = [], optional_capabilities_known = false>> } { return } diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index 1a4d1560a3..b6fe6de3f9 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; @@ -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..4a472e2341 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) { @@ -989,11 +1046,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 +1068,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 +1088,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 +1115,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 +1147,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 +1168,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 +1197,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 +1217,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 +1239,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 +1276,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 +1294,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 +1307,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 +1336,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 +1363,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 +1400,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 +1429,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 +1451,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 +1462,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 +1475,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 +1510,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 +1523,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 +1578,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..b550ed83c2 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 @@ -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 @@ -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,38 @@ 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"canonical major\.minor\.patch"): + PayloadSpecification(PayloadFormat("qir", "2.1", "base")) + + 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) From 8ccedc173bf6893c023d74d83a778ac33c9a44c1 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Sun, 6 Sep 2026 20:14:09 +0200 Subject: [PATCH 03/11] =?UTF-8?q?=F0=9F=93=9D=20Align=20payload=20model=20?= =?UTF-8?q?release=20target?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../selected-payload-target-environment.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/.agent/plans/selected-payload-target-environment.md b/.agent/plans/selected-payload-target-environment.md index 4988517238..d214442405 100644 --- a/.agent/plans/selected-payload-target-environment.md +++ b/.agent/plans/selected-payload-target-environment.md @@ -1,7 +1,7 @@ # Independent compiler capability prototype -Status: independently rebased and locally validated; contract design remains -gated. +Status: independently rebased and locally validated; ready for human contract +review. ## Scope and release boundary @@ -11,10 +11,10 @@ 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 remaining payload model is a non-blocking Core 4.1 candidate. Core #2365 and -QDMI #523 must record the contract decisions before this prototype is considered -merge-ready. Rebase mechanics do not settle format identity, operation sets, -execution guarantees, classical capabilities, or opaque-program semantics. +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 @@ -50,6 +50,7 @@ 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 release build passes, with 3,879 native tests passing and one existing -optional-device test skipping. All 558 targeted Python tests pass with the -superconducting reference device enabled. Stub generation and C++ lint pass. +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. From 20984b37b098111a53f122fb5f5e9059e2aa0771 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 7 Sep 2026 12:48:08 +0200 Subject: [PATCH 04/11] =?UTF-8?q?=F0=9F=90=9B=20Load=20math=20for=20target?= =?UTF-8?q?=20native=20synthesis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Declare the dialect used by runtime-angle synthesis and cover a fresh context that does not preload it. Assisted-by: GPT-6 via Codex --- .../mlir/Dialect/QCO/Transforms/Passes.td | 6 ++-- .../NativeSynthesis/test_target_synthesis.cpp | 36 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 2f2dede6a7..85e88d0cb6 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -185,7 +185,8 @@ def MappingPass : Pass<"place-and-route", "mlir::ModuleOp"> { def TargetNativeSynthesis : Pass<"target-native-synthesis", "mlir::ModuleOp"> { let dependentDialects = ["mlir::qco::QCODialect", - "::mlir::arith::ArithDialect"]; + "::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 @@ -193,7 +194,8 @@ def TargetNativeSynthesis : Pass<"target-native-synthesis", "mlir::ModuleOp"> { 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. + compile-time unitary matrix and cannot be synthesized as a parameterized + single-qubit gate. }]; } 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 4a472e2341..6f2548bc8d 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -1010,6 +1010,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); From ff704c1acf89a9dadbfac595bbc6124f3cdbd869 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 7 Sep 2026 12:48:45 +0200 Subject: [PATCH 05/11] =?UTF-8?q?=E2=9C=A8=20Accept=20exact=20payload=20ve?= =?UTF-8?q?rsion=20shorthand?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zero-fill omitted numeric components at the payload snapshot boundary. Accept the same syntax in typed MLIR and keep compiler output selection exact. Assisted-by: GPT-6 via Codex --- docs/mlir/target_compilation.md | 7 +++ .../include/mlir/Compiler/TargetEnvironment.h | 2 + mlir/lib/Compiler/TargetEnvironment.cpp | 20 +++++--- mlir/lib/Dialect/MQT/IR/MQTDialect.cpp | 5 +- .../Compiler/test_compiler_target.cpp | 47 ++++++++++++++++++- mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp | 2 +- test/python/test_mlir.py | 23 ++++++++- 7 files changed, 92 insertions(+), 14 deletions(-) diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index acb1222284..8215eae270 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -45,6 +45,13 @@ 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: diff --git a/mlir/include/mlir/Compiler/TargetEnvironment.h b/mlir/include/mlir/Compiler/TargetEnvironment.h index a990eb1cf4..4ff93cd017 100644 --- a/mlir/include/mlir/Compiler/TargetEnvironment.h +++ b/mlir/include/mlir/Compiler/TargetEnvironment.h @@ -74,6 +74,8 @@ struct ProgramCapability { 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); diff --git a/mlir/lib/Compiler/TargetEnvironment.cpp b/mlir/lib/Compiler/TargetEnvironment.cpp index ee197e5c14..d6726f8bde 100644 --- a/mlir/lib/Compiler/TargetEnvironment.cpp +++ b/mlir/lib/Compiler/TargetEnvironment.cpp @@ -29,6 +29,7 @@ #include #include +#include #include #include @@ -39,11 +40,16 @@ namespace mlir { "Invalid payload specification: " + message); } -[[nodiscard]] static bool isCanonicalVersion(const llvm::StringRef version) { +[[nodiscard]] static std::optional +normalizePayloadVersion(llvm::StringRef version) { llvm::VersionTuple parsed; - return !parsed.tryParse(version) && parsed.getMinor() && - parsed.getSubminor() && !parsed.getBuild() && - parsed.getAsString() == version; + 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) { @@ -62,10 +68,12 @@ PayloadSpecification::create(PayloadFormat format, return invalidPayload( "Payload format fields must not contain null characters"); } - if (!isCanonicalVersion(format.version)) { + auto version = normalizePayloadVersion(format.version); + if (!version) { return invalidPayload( - "Payload format version must use canonical major.minor.patch"); + "Payload format version must use major[.minor[.patch]]"); } + format.version = std::move(*version); switch (format.encoding) { case PayloadEncoding::Text: case PayloadEncoding::Binary: diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index b5213ccda9..b47204b76a 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -70,8 +70,7 @@ void MQTDialect::initialize() { [[nodiscard]] static bool isCanonicalPayloadVersion(const StringRef version) { llvm::VersionTuple parsed; - return !parsed.tryParse(version) && parsed.getMinor() && - parsed.getSubminor() && !parsed.getBuild() && + return !parsed.tryParse(version) && !parsed.getBuild() && parsed.getAsString() == version; } @@ -90,7 +89,7 @@ PayloadFormatAttr::verify(const function_ref emitError, } if (!isCanonicalPayloadVersion(version.getValue())) { return emitError() - << "payload format version must use canonical major.minor.patch"; + << "payload format version must use major[.minor[.patch]]"; } return success(); } diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index d578f62461..ebb543e184 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -8,6 +8,7 @@ * 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" @@ -20,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -106,9 +108,9 @@ TEST(PayloadSpecificationTest, ValidatesAndRoundTripsTypedAttribute) { "Invalid payload specification: Payload specification attribute must " "not be null"); expectInvalid(mlir::PayloadSpecification::create( - {.id = "qir", .version = "2.1", .profile = "base"}), + {.id = "qir", .version = "2.1.0.1", .profile = "base"}), "Invalid payload specification: Payload format version must " - "use canonical major.minor.patch"); + "use major[.minor[.patch]]"); expectInvalid( mlir::PayloadSpecification::create({.id = "", .version = "2.1.0"}), "Invalid payload specification: Payload format requires an ID and " @@ -166,6 +168,47 @@ TEST(PayloadSpecificationTest, ValidatesAndRoundTripsTypedAttribute) { "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, InvalidatesCachedAnalysisAfterAttributeChange) { mlir::MLIRContext context; context.loadDialect(); diff --git a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp index f3e9117022..1d6f7d777f 100644 --- a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp +++ b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp @@ -823,7 +823,7 @@ TEST_F(MQTIRTest, RejectsInvalidPayloadContracts) { EXPECT_FALSE(parseAttr(R"mlir(#mqt.payload_format)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.payload_format)mlir")); + version = "2.1.0.1", profile = "base", encoding = text>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.payload_format)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.payload_format None: assert environment.format.version == "2.1.0" assert environment.capabilities[0].value == 0 - with pytest.raises(ValueError, match=r"canonical major\.minor\.patch"): - PayloadSpecification(PayloadFormat("qir", "2.1", "base")) + 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: From 321d8dd347a9e7834a45b2f440433e179eff2644 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 7 Sep 2026 12:49:20 +0200 Subject: [PATCH 06/11] =?UTF-8?q?=F0=9F=93=9D=20Clarify=20target=20and=20p?= =?UTF-8?q?ayload=20prerequisites?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use DDSIM for executable QIR examples and state the environment required by the C++ pipeline builder. Assisted-by: GPT-6 via Codex --- docs/mlir/target_compilation.md | 30 +++++++++++-------- .../include/mlir/Compiler/TargetCompilation.h | 3 ++ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index 8215eae270..bcf4f785b6 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -24,8 +24,8 @@ from mqt.core.mlir import ( compile_program, ) -target = CompilerTarget.from_device_id("mqt.sc.iqm.garnet") -payload = PayloadSpecification(PayloadFormat("qir", "2.1.0", "base", PayloadEncoding.BINARY)) +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", @@ -40,6 +40,10 @@ the canonical QCO pipeline. The targeted overload therefore accepts one 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 @@ -49,8 +53,8 @@ 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. +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: @@ -121,10 +125,10 @@ on the program. Copy the program before compilation if the caller must preserve the input. The target passes read the typed `mqt.target_env` module attribute. 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. +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, @@ -143,8 +147,8 @@ mqt-cc --qdmi-list-devices Select a device when compiling: ```console -mqt-cc --qdmi-device=mqt.sc.iqm.garnet \ - --payload-spec='#mqt.payload_spec, capabilities = [], optional_capabilities_known = false>' \ +mqt-cc --qdmi-device=mqt.ddsim.default \ + --payload-spec='#mqt.payload_spec, capabilities = [], optional_capabilities_known = false>' \ -o output.bc input.qasm ``` @@ -153,7 +157,7 @@ An explicit registry file can be selected before device discovery: ```console mqt-cc --qdmi-config=/path/to/qdmi.json \ --qdmi-device=example.device \ - --payload-spec='#mqt.payload_spec, capabilities = [], optional_capabilities_known = false>' \ + --payload-spec='#mqt.payload_spec, capabilities = [], optional_capabilities_known = false>' \ input.qasm ``` @@ -174,7 +178,7 @@ device ID and the compiler-owned target: #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'; @@ -183,7 +187,7 @@ if (!target) { auto payload = mlir::PayloadSpecification::create({ .id = "qir", - .version = "2.1.0", + .version = "2.1", .profile = "base", .encoding = mlir::PayloadEncoding::Binary, }); diff --git a/mlir/include/mlir/Compiler/TargetCompilation.h b/mlir/include/mlir/Compiler/TargetCompilation.h index 10d4cd6233..913cf4d61f 100644 --- a/mlir/include/mlir/Compiler/TargetCompilation.h +++ b/mlir/include/mlir/Compiler/TargetCompilation.h @@ -22,6 +22,9 @@ 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 input module must have an attached `mqt.target_env` whose compiler +/// target matches `target`. Use `attachTargetEnvironment` before running the +/// pipeline. void populateTargetCompilationPipeline(OpPassManager& pm, const CompilerTarget& target); From 4c05ee9aeb0d2b44385eb4744eb9bd3e8b99da53 Mon Sep 17 00:00:00 2001 From: simon1hofmann <119581649+simon1hofmann@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:43:49 +0200 Subject: [PATCH 07/11] Apply batched suggestions from code review Co-authored-by: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Signed-off-by: simon1hofmann <119581649+simon1hofmann@users.noreply.github.com> --- mlir/include/mlir/Compiler/TargetEnvironment.h | 3 ++- mlir/unittests/Compiler/test_compiler_pipeline.cpp | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/mlir/include/mlir/Compiler/TargetEnvironment.h b/mlir/include/mlir/Compiler/TargetEnvironment.h index 4ff93cd017..bcb629b5c9 100644 --- a/mlir/include/mlir/Compiler/TargetEnvironment.h +++ b/mlir/include/mlir/Compiler/TargetEnvironment.h @@ -66,7 +66,7 @@ struct ProgramCapability { const ProgramCapability&) = default; }; -/// Context-free selected payload execution contract. +/// Context-free selected execution payload contract. /// /// Producers must include every effective capability, including /// payload-format baselines. The knowledge bit states whether the list also @@ -74,6 +74,7 @@ struct ProgramCapability { 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 diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 6cf1a8faea..1d03ffef90 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -2474,10 +2474,10 @@ h q; 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) + // 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); } From 7ba95341d3c82304871855875eafbda5f6f0d801 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 7 Sep 2026 15:58:36 +0200 Subject: [PATCH 08/11] =?UTF-8?q?=F0=9F=8E=A8=20Restore=20QCO=20namespace-?= =?UTF-8?q?closing=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: OpenAI via Codex --- mlir/include/mlir/Dialect/QCO/Transforms/Passes.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h index c9b4013e6c..c9c5e569eb 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h @@ -19,7 +19,7 @@ namespace mlir { class CompilerTarget; -} +} // namespace mlir namespace mlir::qco { From 95e5fecff69b8f1ce41d17f3b1b50c18da3b9164 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 7 Sep 2026 15:58:36 +0200 Subject: [PATCH 09/11] =?UTF-8?q?=F0=9F=93=9D=20Add=20payload=20and=20targ?= =?UTF-8?q?et=20environment=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: OpenAI via Codex --- .../include/mlir/Dialect/MQT/IR/MQTDialect.td | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td index 1f6d76e134..5084081ae6 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td @@ -235,6 +235,13 @@ def PayloadFormatAttr : MQTAttr<"PayloadFormat", "payload_format"> { 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); @@ -273,6 +280,18 @@ def PayloadSpecAttr : MQTAttr<"PayloadSpec", "payload_spec"> { 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, @@ -287,6 +306,20 @@ def TargetEnvAttr : MQTAttr<"TargetEnv", "target_env", [DLTIQueryInterface]> { Combines typed compiler-target facts with the selected payload contract. Optional DLTI extensions carry provider-specific facts. Direct access to the typed fields is authoritative. + + 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, From 8a050a0a6f3cdf5df2d6e621ce032f5af0c27f9e Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 8 Sep 2026 09:25:14 +0200 Subject: [PATCH 10/11] =?UTF-8?q?=F0=9F=A7=AA=20Adapt=20new=20target=20tes?= =?UTF-8?q?ts=20to=20payload=20environments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preserve upstream regressions after rebasing onto main by using the existing target-environment helpers. Assisted-by: OpenAI via Codex --- .../Compiler/test_compiler_pipeline.cpp | 3 ++- .../QCO/Transforms/Mapping/test_mapping.cpp | 4 ++-- .../NativeSynthesis/test_target_synthesis.cpp | 19 ++++++++++--------- test/python/test_mlir.py | 4 ++-- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 1d03ffef90..d361c09727 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -1127,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(); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index b6fe6de3f9..0e8e51081c 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -729,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)); 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 6f2548bc8d..faed1d6c90 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -955,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); } @@ -979,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) { @@ -997,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) diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index 6ae9553490..58cf3bb378 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -389,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() @@ -503,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) From 0555f269d7fa25e2bd560eb9d71d5f6b9cf97ca6 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Tue, 8 Sep 2026 15:27:33 +0000 Subject: [PATCH 11/11] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Share=20the=20select?= =?UTF-8?q?ed=20target=20environment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the pipeline take one environment and reuse its prepared target in the cached analysis. Standalone passes decode the typed pair on demand. Remove the unused DLTI extension and query layer. Assisted-by: GPT-6 via Codex --- .../selected-payload-target-environment.md | 22 ++++++-- docs/mlir/target_compilation.md | 18 +++--- .../include/mlir/Compiler/TargetCompilation.h | 10 ++-- .../include/mlir/Compiler/TargetEnvironment.h | 10 +++- .../mlir/Dialect/MQT/IR/MQTAttributes.h | 1 - .../include/mlir/Dialect/MQT/IR/MQTDialect.td | 18 +----- mlir/lib/Compiler/Pipeline.cpp | 3 +- mlir/lib/Compiler/TargetCompilation.cpp | 31 ++++++++++- mlir/lib/Compiler/TargetEnvironment.cpp | 21 ++++++- mlir/lib/Dialect/MQT/IR/CMakeLists.txt | 1 - mlir/lib/Dialect/MQT/IR/MQTDialect.cpp | 55 ------------------- mlir/tools/mqt-cc/mqt-cc.cpp | 3 +- .../Compiler/test_compiler_pipeline.cpp | 3 + .../Compiler/test_compiler_target.cpp | 35 ++++++++++++ mlir/unittests/Dialect/MQT/IR/CMakeLists.txt | 1 - mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp | 47 ++-------------- 16 files changed, 136 insertions(+), 143 deletions(-) diff --git a/.agent/plans/selected-payload-target-environment.md b/.agent/plans/selected-payload-target-environment.md index d214442405..625c6ef82b 100644 --- a/.agent/plans/selected-payload-target-environment.md +++ b/.agent/plans/selected-payload-target-environment.md @@ -1,7 +1,6 @@ # Independent compiler capability prototype -Status: independently rebased and locally validated; ready for human contract -review. +Status: implemented. ## Scope and release boundary @@ -27,8 +26,11 @@ 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 same -validated target that was attached to the module. +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 @@ -54,3 +56,15 @@ 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/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index bcf4f785b6..e09d0930b1 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -122,13 +122,17 @@ 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 target passes read the typed `mqt.target_env` module attribute. -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. +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, diff --git a/mlir/include/mlir/Compiler/TargetCompilation.h b/mlir/include/mlir/Compiler/TargetCompilation.h index 913cf4d61f..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,10 +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 input module must have an attached `mqt.target_env` whose compiler -/// target matches `target`. Use `attachTargetEnvironment` before running the -/// pipeline. +/// 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 index bcb629b5c9..a5f2739cde 100644 --- a/mlir/include/mlir/Compiler/TargetEnvironment.h +++ b/mlir/include/mlir/Compiler/TargetEnvironment.h @@ -143,6 +143,9 @@ 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; @@ -157,10 +160,13 @@ class TargetEnvironmentAnalysis { isInvalidated(const AnalysisManager::PreservedAnalyses& analyses) const; private: + /// Decode IR only when no prepared environment was supplied. + void resolve() const; + ModuleOp moduleOp_; Attribute attribute_; - std::optional environment_; - std::string error_; + mutable std::optional environment_; + mutable std::string error_; }; } // namespace mlir diff --git a/mlir/include/mlir/Dialect/MQT/IR/MQTAttributes.h b/mlir/include/mlir/Dialect/MQT/IR/MQTAttributes.h index f19956c036..1cdaaefcae 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/MQTAttributes.h +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTAttributes.h @@ -18,7 +18,6 @@ #include #include #include -#include #include #include #include diff --git a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td index 5084081ae6..442d15a0bb 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td @@ -12,7 +12,6 @@ include "mlir/IR/AttrTypeBase.td" include "mlir/IR/EnumAttr.td" include "mlir/IR/OpBase.td" -include "mlir/Interfaces/DataLayoutInterfaces.td" def MQTDialect : Dialect { let name = "mqt"; @@ -300,12 +299,10 @@ def PayloadSpecAttr : MQTAttr<"PayloadSpec", "payload_spec"> { let genVerifyDecl = 1; } -def TargetEnvAttr : MQTAttr<"TargetEnv", "target_env", [DLTIQueryInterface]> { +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. - Optional DLTI extensions carry provider-specific facts. Direct access to - the typed fields is authoritative. The following environment pairs a one-site target with a producer-defined payload: @@ -322,19 +319,8 @@ def TargetEnvAttr : MQTAttr<"TargetEnv", "target_env", [DLTIQueryInterface]> { ``` }]; let parameters = (ins "CompilationTargetAttr":$compilation_target, - "PayloadSpecAttr":$payload_specification, - OptionalParameter<"::mlir::MapAttr">:$extensions); + "PayloadSpecAttr":$payload_specification); let assemblyFormat = "`<` struct(params) `>`"; - let genVerifyDecl = 1; - let extraClassDeclaration = [{ - static constexpr ::llvm::StringLiteral kCompilationTargetKey = - "mqt.compilation_target"; - static constexpr ::llvm::StringLiteral kPayloadSpecificationKey = - "mqt.payload_specification"; - - ::mlir::FailureOr<::mlir::Attribute> - query(::mlir::DataLayoutEntryKey key) const; - }]; } #endif // MLIR_DIALECT_MQT_IR_MQTDIALECT_TD diff --git a/mlir/lib/Compiler/Pipeline.cpp b/mlir/lib/Compiler/Pipeline.cpp index 32c1883b52..b1bba6d82c 100644 --- a/mlir/lib/Compiler/Pipeline.cpp +++ b/mlir/lib/Compiler/Pipeline.cpp @@ -230,11 +230,10 @@ bool QCOProgram::decomposeMultiControlled(uint64_t minQubits) { bool QCOProgram::compileForTarget(const TargetEnvironment& environment, bool enableTiming, bool enableStatistics) { - attachTargetEnvironment(mod(), environment); return succeeded(runQCOTransformPasses( mod(), [&environment](OpPassManager& pm) { - populateTargetCompilationPipeline(pm, environment.target()); + populateTargetCompilationPipeline(pm, environment); }, "failed to compile the QCO program for the target", enableTiming, enableStatistics)); diff --git a/mlir/lib/Compiler/TargetCompilation.cpp b/mlir/lib/Compiler/TargetCompilation.cpp index c176d54629..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)); diff --git a/mlir/lib/Compiler/TargetEnvironment.cpp b/mlir/lib/Compiler/TargetEnvironment.cpp index d6726f8bde..2d7918eef0 100644 --- a/mlir/lib/Compiler/TargetEnvironment.cpp +++ b/mlir/lib/Compiler/TargetEnvironment.cpp @@ -260,8 +260,7 @@ TargetEnvironment::payloadSpecification() const noexcept { mqt::TargetEnvAttr TargetEnvironment::materialize(MLIRContext& context) const { return mqt::TargetEnvAttr::get(&context, target_.materialize(context), - payloadSpecification_.materialize(context), - {}); + payloadSpecification_.materialize(context)); } void attachTargetEnvironment(ModuleOp moduleOp, @@ -273,7 +272,20 @@ void attachTargetEnvironment(ModuleOp moduleOp, TargetEnvironmentAnalysis::TargetEnvironmentAnalysis(Operation* operation) : moduleOp_(cast(operation)), attribute_(moduleOp_->getAttrOfType( - mqt::TargetEnvAttr::name)) { + 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; @@ -288,16 +300,19 @@ TargetEnvironmentAnalysis::TargetEnvironmentAnalysis(Operation* operation) } 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_; } diff --git a/mlir/lib/Dialect/MQT/IR/CMakeLists.txt b/mlir/lib/Dialect/MQT/IR/CMakeLists.txt index 03cf59f80e..d30513cca9 100644 --- a/mlir/lib/Dialect/MQT/IR/CMakeLists.txt +++ b/mlir/lib/Dialect/MQT/IR/CMakeLists.txt @@ -17,7 +17,6 @@ add_mlir_dialect_library( PRIVATE LLVMSupport MLIRCBitDialect - MLIRDLTIDialect MLIRFuncDialect MLIRIR MLIRMemRefDialect diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index b47204b76a..7fd6c7eaad 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -27,7 +27,6 @@ #include // IWYU pragma: keep #include #include -#include #include #include #include @@ -40,7 +39,6 @@ #include #include #include -#include #include #include #include @@ -370,59 +368,6 @@ LogicalResult CompilationTargetAttr::verify( return success(); } -[[nodiscard]] static bool isNamespacedExtensionKey(const StringRef key) { - SmallVector components; - key.split(components, '.'); - return components.size() > 1 && - llvm::none_of(components, [](const StringRef component) { - return component.empty(); - }); -} - -LogicalResult -TargetEnvAttr::verify(const function_ref emitError, - const CompilationTargetAttr /*compilationTarget*/, - const PayloadSpecAttr /*payloadSpecification*/, - const MapAttr extensions) { - if (!extensions) { - return success(); - } - for (const DataLayoutEntryInterface entry : extensions.getEntries()) { - const auto key = entry.getKey().dyn_cast(); - if (!key) { - return emitError() - << "target environment extension keys must be identifiers"; - } - const auto value = key.getValue(); - if (!isNamespacedExtensionKey(value)) { - return emitError() << "target environment extension key '" << value - << "' must be provider or dialect namespaced"; - } - if (value == kCompilationTargetKey || value == kPayloadSpecificationKey) { - return emitError() << "target environment extension key '" << value - << "' is reserved by MQT"; - } - } - return success(); -} - -FailureOr TargetEnvAttr::query(const DataLayoutEntryKey key) const { - const auto identifier = key.dyn_cast(); - if (!identifier) { - return failure(); - } - if (identifier.getValue() == kCompilationTargetKey) { - return getCompilationTarget(); - } - if (identifier.getValue() == kPayloadSpecificationKey) { - return getPayloadSpecification(); - } - if (MapAttr extensions = getExtensions()) { - return extensions.query(key); - } - return failure(); -} - [[nodiscard]] static LogicalResult verifyEntryPoint(Operation* operation, const NamedAttribute attribute) { if (!isa(attribute.getValue())) { diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index d5dcd56acf..68d9871060 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -606,8 +606,7 @@ static int runCompiler(int argc, char** argv) { pm.addPass(createInlinerPass()); } if (targetEnvironment) { - attachTargetEnvironment(*program.mod, *targetEnvironment); - populateTargetCompilationPipeline(pm, targetEnvironment->target()); + populateTargetCompilationPipeline(pm, *targetEnvironment); return success(); } populateQCOCleanupPipeline(pm); diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index d361c09727..d3d42aad27 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -1814,6 +1814,9 @@ TEST_F(CompilerPipelineTest, QCOProgramCompilesForTarget) { const auto target = makeSparseUCZTarget(true); 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()); diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index ebb543e184..87b473f58a 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -209,6 +209,41 @@ TEST(PayloadSpecificationTest, NormalizesTypedVersionShorthand) { "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(); diff --git a/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt b/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt index d918bb9038..cf0a00d560 100644 --- a/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt +++ b/mlir/unittests/Dialect/MQT/IR/CMakeLists.txt @@ -13,7 +13,6 @@ target_link_libraries( PRIVATE GTest::gtest_main MLIRArithDialect MLIRCBitDialect - MLIRDLTIDialect MLIRFuncDialect MLIRMemRefDialect MLIRMQTDialect diff --git a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp index 1d6f7d777f..ff7e104b9e 100644 --- a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp +++ b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp @@ -22,7 +22,6 @@ #include #include #include -#include #include #include #include @@ -32,7 +31,6 @@ #include #include #include -#include #include #include @@ -49,9 +47,9 @@ class MQTIRTest : public ::testing::Test { void SetUp() override { DialectRegistry registry; - registry.insert(); + registry.insert(); context = std::make_unique(registry); context->loadAllAvailableDialects(); } @@ -744,8 +742,7 @@ TEST_F(MQTIRTest, RoundTripsTypedTargetEnvironment) { version = "4.2.0", profile = "dynamic", encoding = binary>, capabilities = []>], - optional_capabilities_known = false>, - extensions = #dlti.map<"vendor.queue_depth" = 8 : i64>> + optional_capabilities_known = false>> } { func.func @main() { return } } @@ -787,21 +784,6 @@ TEST_F(MQTIRTest, RoundTripsTypedTargetEnvironment) { payloadSpecification.getCapabilities().front().getConstraints().size(), 1U); - auto query = cast(targetEnv); - auto targetResult = query.query(StringAttr::get( - context.get(), mqt::TargetEnvAttr::kCompilationTargetKey)); - ASSERT_TRUE(succeeded(targetResult)); - EXPECT_EQ(*targetResult, compilationTarget); - auto payloadResult = query.query(StringAttr::get( - context.get(), mqt::TargetEnvAttr::kPayloadSpecificationKey)); - ASSERT_TRUE(succeeded(payloadResult)); - EXPECT_EQ(*payloadResult, payloadSpecification); - auto extensionResult = - query.query(StringAttr::get(context.get(), "vendor.queue_depth")); - ASSERT_TRUE(succeeded(extensionResult)); - EXPECT_EQ(cast(*extensionResult).getInt(), 8); - EXPECT_TRUE(failed(query.query(IntegerType::get(context.get(), 32)))); - const auto reparsed = roundTrip(*moduleOp); ASSERT_TRUE(reparsed); EXPECT_EQ((*reparsed)->getAttr(mqt::TargetEnvAttr::name), targetEnv); @@ -847,27 +829,6 @@ TEST_F(MQTIRTest, RejectsInvalidPayloadContracts) { optional_capabilities_known = true>)mlir")); } -TEST_F(MQTIRTest, RejectsInvalidTargetEnvironmentExtensions) { - EXPECT_FALSE(parseAttr(R"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 = #mqt.payload_format, capabilities = [], - optional_capabilities_known = false>, - extensions = #dlti.map<"unnamespaced" = 1 : i64>>)mlir")); - EXPECT_FALSE(parseAttr(R"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 = #mqt.payload_format, capabilities = [], - optional_capabilities_known = false>, - extensions = #dlti.map<"mqt.payload_specification" = 1 : i64>>)mlir")); -} - TEST_F(MQTIRTest, RejectsTargetEnvironmentOutsideModule) { EXPECT_FALSE(parse(R"mlir( module attributes {mqt.target_env = "invalid"} {}