From 7b49142a2987482144c940f151315c1febec5905 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Sat, 25 Jul 2026 00:39:45 +0200 Subject: [PATCH 1/9] =?UTF-8?q?=E2=9C=A8=20Add=20configurable=20QDMI=20dev?= =?UTF-8?q?ice=20integration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-5.6 via Codex --- .github/workflows/ci.yml | 3 + .gitignore | 1 + .pre-commit-config.yaml | 3 + CMakeLists.txt | 23 + bindings/fomac/fomac.cpp | 229 +- cmake/AddMQTQDMIDevice.cmake | 145 + cmake/mqt-core-config.cmake.in | 1 + include/mqt-core/fomac/FoMaC.hpp | 120 +- include/mqt-core/qdmi/common/Common.hpp | 4 + include/mqt-core/qdmi/driver/Driver.hpp | 114 +- noxfile.py | 22 +- pyproject.toml | 17 +- python/mqt/core/fomac.pyi | 139 +- src/CMakeLists.txt | 1 + src/fomac/FoMaC.cpp | 52 +- src/qdmi/devices/dd/CMakeLists.txt | 6 +- src/qdmi/devices/na/CMakeLists.txt | 6 +- src/qdmi/devices/sc/CMakeLists.txt | 6 +- src/qdmi/driver/CMakeLists.txt | 43 +- src/qdmi/driver/DeviceRegistry.cpp | 523 + src/qdmi/driver/DeviceRegistry.hpp | 38 + src/qdmi/driver/Driver.cpp | 251 +- test/fomac/CMakeLists.txt | 20 +- test/fomac/test_fomac.cpp | 37 +- test/na/fomac/CMakeLists.txt | 20 +- test/python/fomac/test_fomac.py | 65 +- test/qdmi/CMakeLists.txt | 1 + test/qdmi/driver/CMakeLists.txt | 77 +- .../driver/imported_device/CMakeLists.txt | 28 + test/qdmi/driver/metadata_device.cpp | 11 + test/qdmi/driver/session_device.cpp | 238 + test/qdmi/driver/test_driver.cpp | 336 +- test/qdmi/registry/CMakeLists.txt | 15 + test/qdmi/registry/test_device_registry.cpp | 414 + vendor/tomlplusplus/README.md | 10 + vendor/tomlplusplus/toml.hpp | 17899 ++++++++++++++++ 36 files changed, 20535 insertions(+), 383 deletions(-) create mode 100644 cmake/AddMQTQDMIDevice.cmake create mode 100644 src/qdmi/driver/DeviceRegistry.cpp create mode 100644 src/qdmi/driver/DeviceRegistry.hpp create mode 100644 test/qdmi/driver/imported_device/CMakeLists.txt create mode 100644 test/qdmi/driver/metadata_device.cpp create mode 100644 test/qdmi/driver/session_device.cpp create mode 100644 test/qdmi/registry/CMakeLists.txt create mode 100644 test/qdmi/registry/test_device_registry.cpp create mode 100644 vendor/tomlplusplus/README.md create mode 100644 vendor/tomlplusplus/toml.hpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9732aa6bec..1b4468d4cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -165,6 +165,9 @@ jobs: setup-python: true install-pkgs: "nanobind==2.13.0" cpp-linter-extra-args: "-std=c++20" + # The vendored toml++ header is checked upstream and is not maintained + # according to MQT Core's clang-tidy configuration. + cpp-linter-ignore-extra: "vendor/**" setup-mlir: true llvm-version: 22.1.7 diff --git a/.gitignore b/.gitignore index 9a03636339..3bcb81aff4 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ coverage.xml .pytest_cache/ cover/ *.profraw +cmake_test_discovery_*.json # Translations *.mo diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 03e5c409e9..5fe3af5723 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -74,6 +74,7 @@ repos: rev: v1.48.0 hooks: - id: typos + exclude: ^vendor/ priority: 3 ## Check license headers @@ -81,6 +82,7 @@ repos: rev: v2.9.0 hooks: - id: license-tools + exclude: ^vendor/ priority: 4 ## Format BibTeX files with bibtex-tidy @@ -136,6 +138,7 @@ repos: hooks: - id: clang-format types_or: [c++, c, cuda] + exclude: ^vendor/ priority: 5 - id: clang-format name: clang-format (TableGen) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0bc895e4c7..e310053d24 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,7 @@ include(PreventInSourceBuilds) include(PackageAddTest) include(Cache) include(AddMQTCoreLibrary) +include(AddMQTQDMIDevice) option(BUILD_MQT_CORE_BINDINGS "Build the MQT Core Python bindings" OFF) if(BUILD_MQT_CORE_BINDINGS) @@ -169,6 +170,28 @@ if(BUILD_MQT_CORE_MLIR) endif() endif() +if(BUILD_MQT_CORE_BINDINGS) + set(MQT_CORE_WHEEL_TARGETS + mqt-core-ir + mqt-core-algorithms + mqt-core-circuit-optimizer + mqt-core-dd + mqt-core-zx + mqt-core-na + mqt-core-ir-bindings + mqt-core-dd-bindings + mqt-core-fomac-bindings + mqt-core-na-bindings + mqt-core-qdmi-ddsim-device + mqt-core-qdmi-na-device + mqt-core-qdmi-sc-device) + if(BUILD_MQT_CORE_MLIR) + list(APPEND MQT_CORE_WHEEL_TARGETS mqt-core-mlir-bindings) + endif() + add_custom_target(mqt-core-wheel) + add_dependencies(mqt-core-wheel ${MQT_CORE_WHEEL_TARGETS}) +endif() + if(PROJECT_IS_TOP_LEVEL) if(NOT TARGET mqt-core-uninstall) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/cmake/cmake_uninstall.cmake.in diff --git a/bindings/fomac/fomac.cpp b/bindings/fomac/fomac.cpp index fcdfd0ddf8..0898d88102 100644 --- a/bindings/fomac/fomac.cpp +++ b/bindings/fomac/fomac.cpp @@ -14,16 +14,18 @@ #include #include -#include // NOLINT(misc-include-cleaner) -#include // NOLINT(misc-include-cleaner) -#include // NOLINT(misc-include-cleaner) -#include // NOLINT(misc-include-cleaner) -#include // NOLINT(misc-include-cleaner) -#include // NOLINT(misc-include-cleaner) -#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) #include #include +#include #include #include #include @@ -70,6 +72,28 @@ template throw nb::type_error( "value_type must be exactly str, bool, int, float, or bytes"); } + +[[nodiscard]] auto makeDeviceSessionConfig( + std::optional baseUrl, std::optional token, + std::optional authFile, + std::optional authUrl, std::optional username, + std::optional password, std::optional custom1, + std::optional custom2, std::optional custom3, + std::optional custom4, std::optional custom5) + -> qdmi::DeviceSessionConfig { + return {.baseUrl = std::move(baseUrl), + .token = std::move(token), + .authFile = std::move(authFile), + .authUrl = std::move(authUrl), + .username = std::move(username), + .password = std::move(password), + .custom1 = std::move(custom1), + .custom2 = std::move(custom2), + .custom3 = std::move(custom3), + .custom4 = std::move(custom4), + .custom5 = std::move(custom5)}; +} + } // namespace NB_MODULE(MQT_CORE_MODULE_NAME, m) { @@ -77,13 +101,13 @@ NB_MODULE(MQT_CORE_MODULE_NAME, m) { auto session = nb::class_( m, "Session", R"pb(A FoMaC session for managing QDMI devices. -Allows creating isolated sessions with independent authentication settings. +Allows creating isolated sessions with separate authentication settings. All authentication parameters are optional and can be provided as keyword arguments to the constructor.)pb"); session.def( "__init__", [](fomac::Session* self, std::optional token, - std::optional authFile, + std::optional authFile, std::optional authUrl, std::optional username, std::optional password, @@ -570,80 +594,153 @@ when the custom slot is unsupported.)pb"); operation.def(nb::self != nb::self, nb::sig("def __ne__(self, arg: object, /) -> bool")); - // Module-level function to add dynamic device libraries - m.def( - "add_dynamic_device_library", - [](const std::string& libraryPath, const std::string& prefix, - const std::optional& baseUrl = std::nullopt, - const std::optional& token = std::nullopt, - const std::optional& authFile = std::nullopt, - const std::optional& authUrl = std::nullopt, - const std::optional& username = std::nullopt, - const std::optional& password = std::nullopt, - const std::optional& custom1 = std::nullopt, - const std::optional& custom2 = std::nullopt, - const std::optional& custom3 = std::nullopt, - const std::optional& custom4 = std::nullopt, - const std::optional& custom5 = - std::nullopt) -> fomac::Device { - const qdmi::DeviceSessionConfig config{.baseUrl = baseUrl, - .token = token, - .authFile = authFile, - .authUrl = authUrl, - .username = username, - .password = password, - .custom1 = custom1, - .custom2 = custom2, - .custom3 = custom3, - .custom4 = custom4, - .custom5 = custom5}; - auto* const qdmiDevice = qdmi::Driver::get().addDynamicDeviceLibrary( - libraryPath, prefix, config); - return fomac::Session::createSessionlessDevice(qdmiDevice); - }, - "library_path"_a, "prefix"_a, nb::kw_only(), "base_url"_a = std::nullopt, - "token"_a = std::nullopt, "auth_file"_a = std::nullopt, - "auth_url"_a = std::nullopt, "username"_a = std::nullopt, - "password"_a = std::nullopt, "custom1"_a = std::nullopt, - "custom2"_a = std::nullopt, "custom3"_a = std::nullopt, - "custom4"_a = std::nullopt, "custom5"_a = std::nullopt, - R"pb(Load a dynamic device library into the QDMI driver. - -This function loads a shared library (.so, .dll, or .dylib) that implements a QDMI device interface and makes it available for use in sessions. + nb::class_( + m, "DeviceDefinition", + R"pb(A stable QDMI device registration that can be stored before loading.)pb") + .def( + "__init__", + [](qdmi::DeviceDefinition* self, std::string deviceId, + std::string libraryPath, std::string prefix, + const std::optional& baseUrl = std::nullopt, + const std::optional& token = std::nullopt, + const std::optional& authFile = + std::nullopt, + const std::optional& authUrl = std::nullopt, + const std::optional& username = std::nullopt, + const std::optional& password = std::nullopt, + const std::optional& custom1 = std::nullopt, + const std::optional& custom2 = std::nullopt, + const std::optional& custom3 = std::nullopt, + const std::optional& custom4 = std::nullopt, + const std::optional& custom5 = std::nullopt) { + new (self) qdmi::DeviceDefinition{ + .id = std::move(deviceId), + .library = std::move(libraryPath), + .prefix = std::move(prefix), + .session = makeDeviceSessionConfig( + baseUrl, token, authFile, authUrl, username, password, + custom1, custom2, custom3, custom4, custom5)}; + }, + "device_id"_a, "library_path"_a, "prefix"_a, nb::kw_only(), + "base_url"_a = std::nullopt, "token"_a = std::nullopt, + "auth_file"_a = std::nullopt, "auth_url"_a = std::nullopt, + "username"_a = std::nullopt, "password"_a = std::nullopt, + "custom1"_a = std::nullopt, "custom2"_a = std::nullopt, + "custom3"_a = std::nullopt, "custom4"_a = std::nullopt, + "custom5"_a = std::nullopt, + R"pb(Create a device definition without loading its native library. Args: - library_path: Path to the shared library file to load. - prefix: Function prefix used by the library (e.g., "MY_DEVICE"). + device_id: Stable identifier used by :func:`open_device`. + library_path: Path to the shared QDMI device library. + prefix: Function prefix used by the library (for example, ``MY_DEVICE``). base_url: Optional base URL for the device API endpoint. token: Optional authentication token. - auth_file: Optional path to authentication file. + auth_file: Optional path to an authentication file. auth_url: Optional authentication server URL. - username: Optional username for authentication. - password: Optional password for authentication. + username: Optional authentication username. + password: Optional authentication password. custom1: Optional custom configuration parameter 1. custom2: Optional custom configuration parameter 2. custom3: Optional custom configuration parameter 3. custom4: Optional custom configuration parameter 4. - custom5: Optional custom configuration parameter 5. + custom5: Optional custom configuration parameter 5.)pb") + .def_ro("device_id", &qdmi::DeviceDefinition::id, + R"pb(Stable identifier used to open the device.)pb") + .def_prop_ro( + "library_path", + [](const qdmi::DeviceDefinition& definition) { + return definition.library.string(); + }, + R"pb(Path to the native QDMI device library.)pb") + .def_ro("prefix", &qdmi::DeviceDefinition::prefix, + R"pb(Prefix used for the QDMI device interface functions.)pb"); + + m.def( + "register_device", + [](qdmi::DeviceDefinition definition, const bool replace) { + qdmi::Driver::get().registerDevice(std::move(definition), replace); + }, + "definition"_a, nb::kw_only(), "replace"_a = false, + R"pb(Register a QDMI device definition without loading its library. + +Args: + definition: Definition to validate and store. + replace: Replace an existing definition if it has not been opened. + +Raises: + ValueError: If the definition is invalid or its ID is already registered. + RuntimeError: If replacing an already opened ID.)pb"); + + m.def( + "register_device_if_absent", + [](qdmi::DeviceDefinition definition) { + return qdmi::Driver::get().registerDeviceIfAbsent( + std::move(definition)); + }, + "definition"_a, + R"pb(Register a valid QDMI device definition if its ID is absent. + +An existing ID is the only ignored condition. Invalid definitions still raise. + +Args: + definition: Definition to validate and store. Returns: - Device: The newly loaded device that can be used to create backends. + bool: Whether the definition was inserted. Raises: - RuntimeError: If library loading fails or configuration is invalid. + ValueError: If the definition is invalid.)pb"); -Examples: - Load a device library with configuration: + m.def( + "open_device", + [](const std::string& deviceId, std::optional baseUrl, + std::optional token, + std::optional authFile, + std::optional authUrl, + std::optional username, + std::optional password, + std::optional custom1, std::optional custom2, + std::optional custom3, std::optional custom4, + std::optional custom5) { + const auto overrides = makeDeviceSessionConfig( + std::move(baseUrl), std::move(token), std::move(authFile), + std::move(authUrl), std::move(username), std::move(password), + std::move(custom1), std::move(custom2), std::move(custom3), + std::move(custom4), std::move(custom5)); + return fomac::Session::openDevice(deviceId, overrides); + }, + "device_id"_a, nb::kw_only(), "base_url"_a = std::nullopt, + "token"_a = std::nullopt, "auth_file"_a = std::nullopt, + "auth_url"_a = std::nullopt, "username"_a = std::nullopt, + "password"_a = std::nullopt, "custom1"_a = std::nullopt, + "custom2"_a = std::nullopt, "custom3"_a = std::nullopt, + "custom4"_a = std::nullopt, "custom5"_a = std::nullopt, + R"pb(Open a registered QDMI device by stable ID. - >>> import mqt.core.fomac as fomac - >>> device = fomac.add_dynamic_device_library( - ... "/path/to/libmy_device.so", "MY_DEVICE", base_url="http://localhost:8080", custom1="API_V2" - ... ) +Every call creates a fresh device session while keeping the stable registration +unchanged. Opening the device loads trusted native device code. - Now the device can be used directly: +Args: + device_id: Stable ID of a registered device. + base_url: Optional base URL override for the device API endpoint. + token: Optional authentication token override. + auth_file: Optional authentication-file override. + auth_url: Optional authentication server URL override. + username: Optional authentication username override. + password: Optional authentication password override. + custom1: Optional custom configuration parameter 1 override. + custom2: Optional custom configuration parameter 2 override. + custom3: Optional custom configuration parameter 3 override. + custom4: Optional custom configuration parameter 4 override. + custom5: Optional custom configuration parameter 5 override. - >>> from mqt.core.plugins.qiskit import QDMIBackend - >>> backend = QDMIBackend(device=device))pb"); +Returns: + Device: The opened device, ready for direct backend construction. + +Raises: + IndexError: If the ID is not registered. + RuntimeError: If the device library cannot be loaded or initialized.)pb"); } } // namespace mqt diff --git a/cmake/AddMQTQDMIDevice.cmake b/cmake/AddMQTQDMIDevice.cmake new file mode 100644 index 0000000000..c539002e8e --- /dev/null +++ b/cmake/AddMQTQDMIDevice.cmake @@ -0,0 +1,145 @@ +# 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(GNUInstallDirs) + +function(_mqt_qdmi_json_escape result value) + string(REPLACE "\\" "\\\\" escaped "${value}") + string(REPLACE "\"" "\\\"" escaped "${escaped}") + string(REPLACE "\n" "\\n" escaped "${escaped}") + string(REPLACE "\r" "\\r" escaped "${escaped}") + string(REPLACE "\t" "\\t" escaped "${escaped}") + set(${result} + "${escaped}" + PARENT_SCOPE) +endfunction() + +# Configure and register a relocatable built-in QDMI device. The generated fragment is emitted +# beside the runtime library in both build and install trees. +function(mqt_configure_qdmi_device target) + cmake_parse_arguments(ARG "" "ID;PREFIX" "" ${ARGN}) + if(NOT TARGET ${target}) + message(FATAL_ERROR "Unknown QDMI device target: ${target}") + endif() + if(NOT ARG_ID OR NOT ARG_PREFIX) + message(FATAL_ERROR "mqt_configure_qdmi_device requires ID and PREFIX") + endif() + + set_target_properties( + ${target} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}") + target_compile_definitions(${target} PRIVATE QDMI_VERSION="${QDMI_VERSION}" + ${ARG_PREFIX}_QDMI_device_EXPORTS) + _mqt_qdmi_json_escape(device_id "${ARG_ID}") + _mqt_qdmi_json_escape(device_prefix "${ARG_PREFIX}") + + set(fragment "${CMAKE_CURRENT_BINARY_DIR}/$/${target}.qdmi.json") + file( + GENERATE + OUTPUT "${fragment}" + CONTENT + "{\n \"schema-version\": 1,\n \"qdmi\": {\n \"devices\": [\n {\n \"id\": \"${device_id}\",\n \"library\": \"$\",\n \"prefix\": \"${device_prefix}\",\n \"enabled\": true\n }\n ]\n }\n}\n" + ) + + add_custom_command( + TARGET ${target} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${fragment}" + "$/${target}.qdmi.json") + set_target_properties( + ${target} + PROPERTIES MQT_QDMI_DEVICE_ID "${ARG_ID}" + MQT_QDMI_DEVICE_PREFIX "${ARG_PREFIX}" + MQT_QDMI_MANIFEST_NAME "${target}.qdmi.json") + set_property(GLOBAL APPEND PROPERTY MQT_QDMI_DEVICE_TARGETS ${target}) + set_property( + TARGET ${target} + APPEND + PROPERTY EXPORT_PROPERTIES MQT_QDMI_DEVICE_ID MQT_QDMI_DEVICE_PREFIX MQT_QDMI_MANIFEST_NAME) + if(WIN32) + # Shared-library targets are runtime artifacts on Windows and are installed under bin. Keep the + # fragment beside the DLL so its relative path resolves. + set(fragment_install_dir ${CMAKE_INSTALL_BINDIR}) + else() + set(fragment_install_dir ${CMAKE_INSTALL_LIBDIR}) + endif() + set(install_arguments) + if(MQT_CORE_TARGET_NAME) + list(APPEND install_arguments COMPONENT ${MQT_CORE_TARGET_NAME}_Runtime) + endif() + install( + FILES "${fragment}" + DESTINATION ${fragment_install_dir} + ${install_arguments}) +endfunction() + +# Return every QDMI device registered through mqt_configure_qdmi_device. +function(mqt_get_qdmi_device_targets result) + get_property(devices GLOBAL PROPERTY MQT_QDMI_DEVICE_TARGETS) + set(${result} + ${devices} + PARENT_SCOPE) +endfunction() + +# Copy QDMI device libraries and their manifests beside a static consumer executable. +function(mqt_copy_qdmi_runtime target) + if(NOT TARGET ${target}) + message(FATAL_ERROR "Unknown QDMI runtime consumer target: ${target}") + endif() + set(devices ${ARGN}) + if(NOT devices) + mqt_get_qdmi_device_targets(devices) + endif() + if(NOT devices) + message(FATAL_ERROR "mqt_copy_qdmi_runtime requires at least one QDMI device target") + endif() + foreach(device IN LISTS devices) + if(NOT TARGET ${device}) + message(FATAL_ERROR "Unknown QDMI device target: ${device}") + endif() + get_target_property(device_target ${device} ALIASED_TARGET) + if(NOT device_target) + set(device_target ${device}) + endif() + get_target_property(manifest_name ${device_target} MQT_QDMI_MANIFEST_NAME) + if(NOT manifest_name) + get_target_property(device_id ${device_target} MQT_QDMI_DEVICE_ID) + get_target_property(device_prefix ${device_target} MQT_QDMI_DEVICE_PREFIX) + if(NOT device_id OR NOT device_prefix) + message( + FATAL_ERROR + "QDMI device target '${device}' must define either MQT_QDMI_MANIFEST_NAME or both MQT_QDMI_DEVICE_ID and MQT_QDMI_DEVICE_PREFIX" + ) + endif() + _mqt_qdmi_json_escape(device_id "${device_id}") + _mqt_qdmi_json_escape(device_prefix "${device_prefix}") + string(MAKE_C_IDENTIFIER "${target}-${device}" manifest_stem) + set(manifest_name "${manifest_stem}.qdmi.json") + set(manifest "${CMAKE_CURRENT_BINARY_DIR}/$/${manifest_name}") + file( + GENERATE + OUTPUT "${manifest}" + CONTENT + "{\n \"schema-version\": 1,\n \"qdmi\": {\n \"devices\": [\n {\n \"id\": \"${device_id}\",\n \"library\": \"$\",\n \"prefix\": \"${device_prefix}\",\n \"enabled\": true\n }\n ]\n }\n}\n" + ) + else() + set(manifest "$/${manifest_name}") + endif() + get_target_property(device_imported ${device_target} IMPORTED) + if(NOT device_imported) + add_dependencies(${target} ${device}) + endif() + add_custom_command( + TARGET ${target} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" + "$" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${manifest}" + "$/${manifest_name}") + endforeach() +endfunction() diff --git a/cmake/mqt-core-config.cmake.in b/cmake/mqt-core-config.cmake.in index f8d1982f46..0855dc9466 100644 --- a/cmake/mqt-core-config.cmake.in +++ b/cmake/mqt-core-config.cmake.in @@ -34,6 +34,7 @@ if(TARGET MQT::Core) endif() include("${CMAKE_CURRENT_LIST_DIR}/AddMQTPythonBinding.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/AddMQTQDMIDevice.cmake") include("${CMAKE_CURRENT_LIST_DIR}/Cache.cmake") include("${CMAKE_CURRENT_LIST_DIR}/PackageAddTest.cmake") include("${CMAKE_CURRENT_LIST_DIR}/PreventInSourceBuilds.cmake") diff --git a/include/mqt-core/fomac/FoMaC.hpp b/include/mqt-core/fomac/FoMaC.hpp index 25f07ce68f..9aa6713410 100644 --- a/include/mqt-core/fomac/FoMaC.hpp +++ b/include/mqt-core/fomac/FoMaC.hpp @@ -15,6 +15,7 @@ #pragma once #include "qdmi/common/Common.hpp" +#include "qdmi/driver/Driver.hpp" #include "qdmi/types.h" #include @@ -25,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -312,7 +314,7 @@ struct SessionConfig { /// Authentication token std::optional token; /// Path to file containing authentication information - std::optional authFile; + std::optional authFile; /// URL to authentication server std::optional authUrl; /// Username for authentication @@ -355,6 +357,16 @@ class Session { */ [[nodiscard]] static Device createSessionlessDevice(QDMI_Device device); + /** + * @brief Opens a registered QDMI device as a fresh device session. + * @param id Stable registered device ID. + * @param overrides Session values that replace registered defaults. + * @return An owning device wrapper for the new session. + */ + [[nodiscard]] static Device + openDevice(std::string_view id, + const qdmi::DeviceSessionConfig& overrides = {}); + /** * @brief Constructs a new QDMI Session with optional authentication. * @param config Optional session configuration containing authentication @@ -405,7 +417,7 @@ static_assert(std::is_move_assignable()); class Device { public: // NOLINTNEXTLINE(google-explicit-constructor, *-explicit-conversions) - operator QDMI_Device() const { return device_; } + operator QDMI_Device() const { return device_.get(); } /// @see QDMI_DEVICE_PROPERTY_NAME [[nodiscard]] std::string getName() const; @@ -498,8 +510,8 @@ class Device { const auto qdmiProperty = detail::toDeviceProperty(property); return detail::queryCustomValue( [this, qdmiProperty](const size_t size, void* value, size_t* sizeRet) { - return QDMI_device_query_device_property(device_, qdmiProperty, size, - value, sizeRet); + return QDMI_device_query_device_property(device_.get(), qdmiProperty, + size, value, sizeRet); }, "custom device property " + std::to_string(static_cast(property))); @@ -521,7 +533,15 @@ class Device { * @brief Constructs a Device object from a QDMI_Device handle. * @param device The QDMI_Device handle to wrap. */ - explicit Device(QDMI_Device device) : device_(device) {} + explicit Device(QDMI_Device device) + : device_(device, [](QDMI_Device_impl_d*) {}) {} + + /** + * @brief Constructs a wrapper that retains an owning session. + * @param device The QDMI device handle to wrap. + */ + explicit Device(std::shared_ptr device) + : device_(std::move(device)) {} /// Query a device property. template @@ -531,8 +551,8 @@ class Device { if constexpr (string_or_optional_string) { size_t size = 0; - auto result = - QDMI_device_query_device_property(device_, prop, 0, nullptr, &size); + auto result = QDMI_device_query_device_property(device_.get(), prop, 0, + nullptr, &size); if constexpr (is_optional) { if (result == QDMI_ERROR_NOTSUPPORTED) { @@ -542,15 +562,15 @@ class Device { qdmi::throwIfError(result, msg); std::string value(size - 1, '\0'); - result = QDMI_device_query_device_property(device_, prop, size, + result = QDMI_device_query_device_property(device_.get(), prop, size, value.data(), nullptr); qdmi::throwIfError(result, msg); return value; } else if constexpr (maybe_optional_size_constructible_contiguous_range< T>) { size_t size = 0; - auto result = - QDMI_device_query_device_property(device_, prop, 0, nullptr, &size); + auto result = QDMI_device_query_device_property(device_.get(), prop, 0, + nullptr, &size); if constexpr (is_optional) { if (result == QDMI_ERROR_NOTSUPPORTED) { @@ -561,14 +581,14 @@ class Device { qdmi::throwIfError(result, msg); remove_optional_t value( size / sizeof(typename remove_optional_t::value_type)); - result = QDMI_device_query_device_property(device_, prop, size, + result = QDMI_device_query_device_property(device_.get(), prop, size, value.data(), nullptr); qdmi::throwIfError(result, msg); return value; } else { remove_optional_t value{}; const auto result = QDMI_device_query_device_property( - device_, prop, sizeof(remove_optional_t), &value, nullptr); + device_.get(), prop, sizeof(remove_optional_t), &value, nullptr); if constexpr (is_optional) { if (result == QDMI_ERROR_NOTSUPPORTED) { @@ -585,7 +605,7 @@ class Device { const CustomJobParameter& value); /// @brief The underlying device pointer. - QDMI_Device device_; + std::shared_ptr device_; friend class Session; }; @@ -602,6 +622,10 @@ class Device { */ class Job { public: + Job(Job&&) noexcept = default; + + auto operator=(Job&& other) noexcept -> Job&; + // NOLINTNEXTLINE(google-explicit-constructor, *-explicit-conversions) operator QDMI_Job() const { return job_.get(); } @@ -720,8 +744,16 @@ class Job { /** * @brief Constructs a Job object from a QDMI_Job handle. * @param job The QDMI_Job handle to wrap. + * @param device The device that owns the job. + */ + explicit Job(QDMI_Job job, std::shared_ptr device) + : device_(std::move(device)), job_(job, QDMI_job_free) {} + + /** + * @brief Ownership of the device session that owns the job. + * @note Declared before `job_` so the job is freed before its device. */ - explicit Job(QDMI_Job job) : job_(job, QDMI_job_free) {} + std::shared_ptr device_; std::unique_ptr job_{ nullptr, QDMI_job_free}; @@ -801,8 +833,8 @@ class Site { const auto qdmiProperty = detail::toSiteProperty(property); return detail::queryCustomValue( [this, qdmiProperty](const size_t size, void* value, size_t* sizeRet) { - return QDMI_device_query_site_property(*device_, site_, qdmiProperty, - size, value, sizeRet); + return QDMI_device_query_site_property( + device_.get(), site_, qdmiProperty, size, value, sizeRet); }, "custom site property " + std::to_string(static_cast(property))); @@ -813,18 +845,19 @@ class Site { private: /** * @brief Constructs a Site object from a QDMI_Site handle. - * @param device The device that owns the site. + * @param device The QDMI device handle that owns the site. * @param site The QDMI_Site handle to wrap. */ - Site(const Device* device, QDMI_Site site) : device_(device), site_(site) {} + Site(std::shared_ptr device, QDMI_Site site) + : device_(std::move(device)), site_(site) {} /// Query a site property. template [[nodiscard]] T queryProperty(const QDMI_Site_Property prop) const { if constexpr (string_or_optional_string) { size_t size = 0; - const auto result = QDMI_device_query_site_property(*device_, site_, prop, - 0, nullptr, &size); + const auto result = QDMI_device_query_site_property( + device_.get(), site_, prop, 0, nullptr, &size); if constexpr (is_optional) { if (result == QDMI_ERROR_NOTSUPPORTED) { return std::nullopt; @@ -833,15 +866,16 @@ class Site { qdmi::throwIfError(result, std::string("Querying size") + qdmi::toString(prop)); std::string value(size - 1, '\0'); - qdmi::throwIfError(QDMI_device_query_site_property(*device_, site_, prop, - size, value.data(), - nullptr), + qdmi::throwIfError(QDMI_device_query_site_property(device_.get(), site_, + prop, size, + value.data(), nullptr), std::string("Querying ") + qdmi::toString(prop)); return value; } else { remove_optional_t value{}; const auto result = QDMI_device_query_site_property( - *device_, site_, prop, sizeof(remove_optional_t), &value, nullptr); + device_.get(), site_, prop, sizeof(remove_optional_t), &value, + nullptr); if constexpr (is_optional) { if (result == QDMI_ERROR_NOTSUPPORTED) { return std::nullopt; @@ -853,8 +887,8 @@ class Site { } } - /// @brief A pointer to the device that owns the site. - Device const* device_; + /// @brief The QDMI device handle that owns the site. + std::shared_ptr device_; /// @brief The underlying QDMI_Site object. QDMI_Site site_; @@ -967,7 +1001,7 @@ class Operation { [this, qdmiProperty, &qdmiSites, ¶ms](const size_t size, void* value, size_t* sizeRet) { return QDMI_device_query_operation_property( - *device_, operation_, qdmiSites.size(), qdmiSites.data(), + device_.get(), operation_, qdmiSites.size(), qdmiSites.data(), params.size(), params.data(), qdmiProperty, size, value, sizeRet); }, "custom operation property " + @@ -979,11 +1013,12 @@ class Operation { private: /** * @brief Constructs an Operation object from a QDMI_Operation handle. - * @param device The device that owns the site. + * @param device The QDMI device handle that owns the operation. * @param operation The QDMI_Operation handle to wrap. */ - Operation(const Device* device, QDMI_Operation operation) - : device_(device), operation_(operation) {} + Operation(std::shared_ptr device, + QDMI_Operation operation) + : device_(std::move(device)), operation_(operation) {} /// Query an operation property. template @@ -999,8 +1034,8 @@ class Operation { if constexpr (string_or_optional_string) { size_t size = 0; auto result = QDMI_device_query_operation_property( - *device_, operation_, sites.size(), qdmiSites.data(), params.size(), - params.data(), prop, 0, nullptr, &size); + device_.get(), operation_, sites.size(), qdmiSites.data(), + params.size(), params.data(), prop, 0, nullptr, &size); if constexpr (is_optional) { if (result == QDMI_ERROR_NOTSUPPORTED) { return std::nullopt; @@ -1009,16 +1044,16 @@ class Operation { qdmi::throwIfError(result, msg); std::string value(size - 1, '\0'); result = QDMI_device_query_operation_property( - *device_, operation_, sites.size(), qdmiSites.data(), params.size(), - params.data(), prop, size, value.data(), nullptr); + device_.get(), operation_, sites.size(), qdmiSites.data(), + params.size(), params.data(), prop, size, value.data(), nullptr); qdmi::throwIfError(result, msg); return value; } else if constexpr (maybe_optional_size_constructible_contiguous_range< T>) { size_t size = 0; auto result = QDMI_device_query_operation_property( - *device_, operation_, sites.size(), qdmiSites.data(), params.size(), - params.data(), prop, 0, nullptr, &size); + device_.get(), operation_, sites.size(), qdmiSites.data(), + params.size(), params.data(), prop, 0, nullptr, &size); if constexpr (is_optional) { if (result == QDMI_ERROR_NOTSUPPORTED) { return std::nullopt; @@ -1028,15 +1063,16 @@ class Operation { remove_optional_t value( size / sizeof(typename remove_optional_t::value_type)); result = QDMI_device_query_operation_property( - *device_, operation_, sites.size(), qdmiSites.data(), params.size(), - params.data(), prop, size, value.data(), nullptr); + device_.get(), operation_, sites.size(), qdmiSites.data(), + params.size(), params.data(), prop, size, value.data(), nullptr); qdmi::throwIfError(result, msg); return value; } else { remove_optional_t value{}; const auto result = QDMI_device_query_operation_property( - *device_, operation_, sites.size(), qdmiSites.data(), params.size(), - params.data(), prop, sizeof(remove_optional_t), &value, nullptr); + device_.get(), operation_, sites.size(), qdmiSites.data(), + params.size(), params.data(), prop, sizeof(remove_optional_t), + &value, nullptr); if constexpr (is_optional) { if (result == QDMI_ERROR_NOTSUPPORTED) { return std::nullopt; @@ -1047,8 +1083,8 @@ class Operation { } } - /// @brief A pointer to the device that owns the operation. - Device const* device_; + /// @brief The QDMI device handle that owns the operation. + std::shared_ptr device_; /// @brief The underlying QDMI_Operation object. QDMI_Operation operation_; diff --git a/include/mqt-core/qdmi/common/Common.hpp b/include/mqt-core/qdmi/common/Common.hpp index c3586e6013..0e21332eea 100644 --- a/include/mqt-core/qdmi/common/Common.hpp +++ b/include/mqt-core/qdmi/common/Common.hpp @@ -249,6 +249,8 @@ constexpr auto toString(const QDMI_Device_Session_Parameter param) -> const return "USERNAME"; case QDMI_DEVICE_SESSION_PARAMETER_PASSWORD: return "PASSWORD"; + case QDMI_DEVICE_SESSION_PARAMETER_CHILDDEVICE: + return "CHILD DEVICE"; case QDMI_DEVICE_SESSION_PARAMETER_MAX: return "MAX"; case QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1: @@ -386,6 +388,8 @@ constexpr auto toString(const QDMI_Device_Property prop) -> const char* { return "PULSE SUPPORT"; case QDMI_DEVICE_PROPERTY_SUPPORTEDPROGRAMFORMATS: return "SUPPORTED PROGRAM FORMATS"; + case QDMI_DEVICE_PROPERTY_CHILDDEVICES: + return "CHILD DEVICES"; case QDMI_DEVICE_PROPERTY_MAX: return "MAX"; case QDMI_DEVICE_PROPERTY_CUSTOM1: diff --git a/include/mqt-core/qdmi/driver/Driver.hpp b/include/mqt-core/qdmi/driver/Driver.hpp index 27c7f864bf..a423d36550 100644 --- a/include/mqt-core/qdmi/driver/Driver.hpp +++ b/include/mqt-core/qdmi/driver/Driver.hpp @@ -21,12 +21,19 @@ #include #include +#include #include #include #include +#include #include +#include #include +namespace fomac { +class Session; +} + namespace qdmi { /** @@ -40,7 +47,7 @@ struct DeviceSessionConfig { /// Authentication token std::optional token; /// Path to file containing authentication information - std::optional authFile; + std::optional authFile; /// URL to authentication server std::optional authUrl; /// Username for authentication @@ -59,6 +66,22 @@ struct DeviceSessionConfig { std::optional custom5; }; +/** + * @brief Stable registration for a QDMI device. + * @details Registration records this metadata without loading the native + * library. Use @ref Driver::open to create the corresponding device session. + */ +struct DeviceDefinition { + /// Stable identifier used to open this device. + std::string id; + /// Path to the native QDMI device library. + std::filesystem::path library; + /// Prefix used for the QDMI device interface functions. + std::string prefix; + /// Parameters applied before the device session is initialized. + DeviceSessionConfig session; +}; + /** * @brief Definition of the device library. * @details The device library contains function pointers to the QDMI @@ -351,14 +374,16 @@ struct QDMI_Session_impl_d { private: /// @brief The status of the session. qdmi::SessionStatus status_ = qdmi::SessionStatus::ALLOCATED; - /// @brief A pointer to the list of all devices. - const std::vector>* devices_; + /// @brief Snapshot of devices visible when this session was allocated. + std::vector devices_; public: /// @brief Constructor for the QDMI session. explicit QDMI_Session_impl_d( - const std::vector>& devices) - : devices_(&devices) {} + const std::vector>& devices); + + /// @brief Constructor from an explicit device-handle snapshot. + explicit QDMI_Session_impl_d(const std::vector& devices); /** * @brief Initializes the session. @@ -384,14 +409,15 @@ struct QDMI_Session_impl_d { namespace qdmi { /** * @brief The MQT QDMI driver class. - * @details This driver loads all statically known and linked QDMI device - * libraries. Additional devices can be added dynamically. + * @details This driver discovers configured QDMI device definitions and opens + * their libraries. Additional definitions can be registered at runtime. * @note This class is a singleton that manages the QDMI libraries and * sessions. It is responsible for loading the libraries, allocating sessions, * and providing access to the devices. */ class Driver final : public Singleton { friend class Singleton; + friend class fomac::Session; /// @brief Private constructor to enforce the singleton pattern. Driver(); @@ -401,6 +427,24 @@ class Driver final : public Singleton { */ std::vector> devices_; + /// @brief Registered definitions in stable registration order. + std::vector definitions_; + + /// @brief IDs disabled by the highest-precedence configuration source. + std::unordered_set disabledDeviceIds_; + + /// @brief Initially discovered definitions visible to the client API. + std::vector clientDefinitionIds_; + + /// @brief Materialized devices exposed through the client API. + std::vector clientDevices_; + + /// @brief Whether the configured client device catalog has been opened. + bool clientCatalogMaterialized_ = false; + + /// @brief Opened devices indexed by their stable registration ID. + std::unordered_map openedDevices_; + /** * @brief Map of sessions to their corresponding unique pointers to * QDMI_Session_impl_d objects. @@ -408,24 +452,48 @@ class Driver final : public Singleton { std::unordered_map> sessions_; + /// Opens the initially configured definitions for the client API. + void materializeClientCatalog(); + + /// Opens a fresh device session with per-call overrides. + auto openFresh(std::string_view id, const DeviceSessionConfig& overrides) + -> std::shared_ptr; + public: /** - * @brief Loads a dynamic device library and adds it to the driver. - * - * @param libName The path to the dynamic library to load. - * @param prefix The prefix used for the device interface functions in the - * library. - * @param config Configuration for device session parameters. - * - * @return A pointer to the newly created device. - * - * @throws std::runtime_error If the device cannot be initialized. - * @throws std::bad_alloc If memory allocation fails during the process. - */ - auto addDynamicDeviceLibrary(const std::string& libName, - const std::string& prefix, - const DeviceSessionConfig& config = {}) - -> QDMI_Device; + * @returns the process-wide Driver instance. + * @details This out-of-line accessor keeps static-library consumers from + * instantiating separate singleton storage in different translation units. + */ + [[nodiscard]] static auto get() -> Driver&; + + /** + * @brief Registers a device definition without loading its library. + * @param definition The definition to validate and store. + * @param replace Whether an existing unopened definition may be replaced. + * @throws std::invalid_argument If the definition is incomplete or its ID is + * already registered. + * @throws std::runtime_error If replacing an already opened definition. + */ + void registerDevice(DeviceDefinition definition, bool replace = false); + + /** + * @brief Registers a device definition unless its ID is already present. + * @param definition The definition to validate and store. + * @returns Whether the definition was inserted. + * @throws std::invalid_argument If the definition is incomplete. + * @details An existing ID is the only ignored condition. The complete + * definition is validated before checking for that ID. + */ + auto registerDeviceIfAbsent(DeviceDefinition definition) -> bool; + + /** + * @brief Opens the registered device with the given stable ID. + * @returns The existing device handle when the ID is already open. + * @throws std::out_of_range If the ID is unknown. + * @throws std::runtime_error If loading or session initialization fails. + */ + auto open(std::string_view id) -> QDMI_Device; /** * @brief Allocates a new session. diff --git a/noxfile.py b/noxfile.py index 4ef2142aae..e7cfbd4533 100755 --- a/noxfile.py +++ b/noxfile.py @@ -210,6 +210,17 @@ def stubs(session: nox.Session) -> None: package_root = Path(__file__).parent / "python" / "mqt" / "core" + modules = ["mqt.core.ir", "mqt.core.dd", "mqt.core.fomac", "mqt.core.na"] + mlir_available = session.run( + "python", + "-c", + "import importlib.util; print(importlib.util.find_spec('mqt.core.mlir') is not None)", + silent=True, + ) + if mlir_available and mlir_available.strip() == "True": + modules.append("mqt.core.mlir") + module_args = [arg for module in modules for arg in ("--module", module)] + session.run( "python", "-m", @@ -218,16 +229,7 @@ def stubs(session: nox.Session) -> None: "--include-private", "--output-dir", str(package_root), - "--module", - "mqt.core.ir", - "--module", - "mqt.core.dd", - "--module", - "mqt.core.fomac", - "--module", - "mqt.core.mlir", - "--module", - "mqt.core.na", + *module_args, "--pattern-file", "bindings/patterns.txt", ) diff --git a/pyproject.toml b/pyproject.toml index 2e10aceb71..6f904ce45f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,22 +84,7 @@ ninja.version = ">=1.10" build-dir = "build/{wheel_tag}/{build_type}" # All the targets to build -build.targets = [ - "mqt-core-ir", - "mqt-core-algorithms", - "mqt-core-circuit-optimizer", - "mqt-core-dd", - "mqt-core-zx", - "mqt-core-na", - "mqt-core-ir-bindings", - "mqt-core-dd-bindings", - "mqt-core-fomac-bindings", - "mqt-core-na-bindings", - "mqt-core-mlir-bindings", - "mqt-core-qdmi-ddsim-device", - "mqt-core-qdmi-na-device", - "mqt-core-qdmi-sc-device", -] +build.targets = ["mqt-core-wheel"] install.components = [ "mqt-core_Runtime", diff --git a/python/mqt/core/fomac.pyi b/python/mqt/core/fomac.pyi index 3e8bd79642..71cd51094b 100644 --- a/python/mqt/core/fomac.pyi +++ b/python/mqt/core/fomac.pyi @@ -7,13 +7,14 @@ # Licensed under the MIT License import enum +import os from collections.abc import Sequence from typing import overload class Session: """A FoMaC session for managing QDMI devices. - Allows creating isolated sessions with independent authentication settings. + Allows creating isolated sessions with separate authentication settings. All authentication parameters are optional and can be provided as keyword arguments to the constructor. """ @@ -21,7 +22,7 @@ class Session: self, *, token: str | None = None, - auth_file: str | None = None, + auth_file: str | os.PathLike | None = None, auth_url: str | None = None, username: str | None = None, password: str | None = None, @@ -511,13 +512,91 @@ class Device: def __eq__(self, arg: object, /) -> bool: ... def __ne__(self, arg: object, /) -> bool: ... -def add_dynamic_device_library( - library_path: str, - prefix: str, +class DeviceDefinition: + """A stable QDMI device registration that can be stored before loading.""" + + def __init__( + self, + device_id: str, + library_path: str, + prefix: str, + *, + base_url: str | None = None, + token: str | None = None, + auth_file: str | os.PathLike | None = None, + auth_url: str | None = None, + username: str | None = None, + password: str | None = None, + custom1: str | None = None, + custom2: str | None = None, + custom3: str | None = None, + custom4: str | None = None, + custom5: str | None = None, + ) -> None: + """Create a device definition without loading its native library. + + Args: + device_id: Stable identifier used by :func:`open_device`. + library_path: Path to the shared QDMI device library. + prefix: Function prefix used by the library (for example, ``MY_DEVICE``). + base_url: Optional base URL for the device API endpoint. + token: Optional authentication token. + auth_file: Optional path to an authentication file. + auth_url: Optional authentication server URL. + username: Optional authentication username. + password: Optional authentication password. + custom1: Optional custom configuration parameter 1. + custom2: Optional custom configuration parameter 2. + custom3: Optional custom configuration parameter 3. + custom4: Optional custom configuration parameter 4. + custom5: Optional custom configuration parameter 5. + """ + + @property + def device_id(self) -> str: + """Stable identifier used to open the device.""" + + @property + def library_path(self) -> str: + """Path to the native QDMI device library.""" + + @property + def prefix(self) -> str: + """Prefix used for the QDMI device interface functions.""" + +def register_device(definition: DeviceDefinition, *, replace: bool = False) -> None: + """Register a QDMI device definition without loading its library. + + Args: + definition: Definition to validate and store. + replace: Replace an existing definition if it has not been opened. + + Raises: + ValueError: If the definition is invalid or its ID is already registered. + RuntimeError: If replacing an already opened ID. + """ + +def register_device_if_absent(definition: DeviceDefinition) -> bool: + """Register a valid QDMI device definition if its ID is absent. + + An existing ID is the only ignored condition. Invalid definitions still raise. + + Args: + definition: Definition to validate and store. + + Returns: + bool: Whether the definition was inserted. + + Raises: + ValueError: If the definition is invalid. + """ + +def open_device( + device_id: str, *, base_url: str | None = None, token: str | None = None, - auth_file: str | None = None, + auth_file: str | os.PathLike | None = None, auth_url: str | None = None, username: str | None = None, password: str | None = None, @@ -527,41 +606,29 @@ def add_dynamic_device_library( custom4: str | None = None, custom5: str | None = None, ) -> Device: - """Load a dynamic device library into the QDMI driver. + """Open a registered QDMI device by stable ID. - This function loads a shared library (.so, .dll, or .dylib) that implements a QDMI device interface and makes it available for use in sessions. + Every call creates a fresh device session while keeping the stable registration + unchanged. Opening the device loads trusted native device code. Args: - library_path: Path to the shared library file to load. - prefix: Function prefix used by the library (e.g., "MY_DEVICE"). - base_url: Optional base URL for the device API endpoint. - token: Optional authentication token. - auth_file: Optional path to authentication file. - auth_url: Optional authentication server URL. - username: Optional username for authentication. - password: Optional password for authentication. - custom1: Optional custom configuration parameter 1. - custom2: Optional custom configuration parameter 2. - custom3: Optional custom configuration parameter 3. - custom4: Optional custom configuration parameter 4. - custom5: Optional custom configuration parameter 5. + device_id: Stable ID of a registered device. + base_url: Optional base URL override for the device API endpoint. + token: Optional authentication token override. + auth_file: Optional authentication-file override. + auth_url: Optional authentication server URL override. + username: Optional authentication username override. + password: Optional authentication password override. + custom1: Optional custom configuration parameter 1 override. + custom2: Optional custom configuration parameter 2 override. + custom3: Optional custom configuration parameter 3 override. + custom4: Optional custom configuration parameter 4 override. + custom5: Optional custom configuration parameter 5 override. Returns: - Device: The newly loaded device that can be used to create backends. + Device: The opened device, ready for direct backend construction. Raises: - RuntimeError: If library loading fails or configuration is invalid. - - Examples: - Load a device library with configuration: - - >>> import mqt.core.fomac as fomac - >>> device = fomac.add_dynamic_device_library( - ... "/path/to/libmy_device.so", "MY_DEVICE", base_url="http://localhost:8080", custom1="API_V2" - ... ) - - Now the device can be used directly: - - >>> from mqt.core.plugins.qiskit import QDMIBackend - >>> backend = QDMIBackend(device=device) + IndexError: If the ID is not registered. + RuntimeError: If the device library cannot be loaded or initialized. """ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 095203ac15..08161ca5bc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -129,6 +129,7 @@ if(MQT_CORE_INSTALL) install( FILES ${PROJECT_SOURCE_DIR}/cmake/AddMQTPythonBinding.cmake + ${PROJECT_SOURCE_DIR}/cmake/AddMQTQDMIDevice.cmake ${PROJECT_SOURCE_DIR}/cmake/Cache.cmake ${PROJECT_SOURCE_DIR}/cmake/FindGMP.cmake ${PROJECT_SOURCE_DIR}/cmake/PackageAddTest.cmake diff --git a/src/fomac/FoMaC.cpp b/src/fomac/FoMaC.cpp index 4b42160f9b..1130bf168d 100644 --- a/src/fomac/FoMaC.cpp +++ b/src/fomac/FoMaC.cpp @@ -134,10 +134,9 @@ std::optional> Operation::getSites() const { } std::vector returnedSites; returnedSites.reserve(qdmiSites->size()); - std::ranges::transform(*qdmiSites, std::back_inserter(returnedSites), - [device = device_](const QDMI_Site& site) -> Site { - return {device, site}; - }); + std::ranges::transform( + *qdmiSites, std::back_inserter(returnedSites), + [this](const QDMI_Site& site) -> Site { return {device_, site}; }); return returnedSites; } std::optional>> @@ -199,7 +198,7 @@ std::vector Device::getSites() const { sites.reserve(qdmiSites.size()); std::ranges::transform( qdmiSites, std::back_inserter(sites), - [this](const QDMI_Site& site) -> Site { return {this, site}; }); + [this](const QDMI_Site& site) -> Site { return {device_, site}; }); return sites; } @@ -227,7 +226,7 @@ std::vector Device::getOperations() const { operations.reserve(qdmiOperations.size()); std::ranges::transform( qdmiOperations, std::back_inserter(operations), - [this](const QDMI_Operation& op) -> Operation { return {this, op}; }); + [this](const QDMI_Operation& op) -> Operation { return {device_, op}; }); return operations; } @@ -245,8 +244,10 @@ Device::getCouplingMap() const { std::ranges::transform(*qdmiCouplingMap, std::back_inserter(couplingMap), [this](const std::pair& pair) -> std::pair { - return {Site{this, pair.first}, - Site{this, pair.second}}; + return { + Site{device_, pair.first}, + Site{device_, pair.second}, + }; }); return couplingMap; } @@ -289,7 +290,7 @@ std::vector Device::getSupportedProgramFormats() const { std::vector Device::getChildDevices() const { size_t size = 0; auto result = QDMI_device_query_device_property( - device_, QDMI_DEVICE_PROPERTY_CHILDDEVICES, 0, nullptr, &size); + device_.get(), QDMI_DEVICE_PROPERTY_CHILDDEVICES, 0, nullptr, &size); if (result == QDMI_ERROR_NOTSUPPORTED) { return {}; } @@ -301,7 +302,7 @@ std::vector Device::getChildDevices() const { std::vector handles(size / sizeof(QDMI_Device)); if (size != 0) { result = QDMI_device_query_device_property( - device_, QDMI_DEVICE_PROPERTY_CHILDDEVICES, size, + device_.get(), QDMI_DEVICE_PROPERTY_CHILDDEVICES, size, static_cast(handles.data()), nullptr); qdmi::throwIfError(result, "Querying child devices"); } @@ -310,7 +311,9 @@ std::vector Device::getChildDevices() const { devices.reserve(handles.size()); std::ranges::transform( handles, std::back_inserter(devices), - [](QDMI_Device_impl_d* const handle) { return Device(handle); }); + [this](QDMI_Device_impl_d* const handle) { + return Device(std::shared_ptr(device_, handle)); + }); return devices; } @@ -322,8 +325,9 @@ Job Device::submitJob(const std::string& program, const std::optional& custom4, const std::optional& custom5) const { QDMI_Job job = nullptr; - qdmi::throwIfError(QDMI_device_create_job(device_, &job), "Creating job"); - Job jobWrapper{job}; + qdmi::throwIfError(QDMI_device_create_job(device_.get(), &job), + "Creating job"); + Job jobWrapper{job, device_}; qdmi::throwIfError(QDMI_job_set_parameter(jobWrapper, QDMI_JOB_PARAMETER_PROGRAMFORMAT, @@ -402,6 +406,16 @@ void Job::cancel() const { qdmi::throwIfError(QDMI_job_cancel(job_.get()), "Cancelling job"); } +auto Job::operator=(Job&& other) noexcept -> Job& { + if (this != &other) { + // Release the current job while its owning device session is still alive. + job_.reset(); + device_ = std::move(other.device_); + job_ = std::move(other.job_); + } + return *this; +} + std::string Job::getId() const { size_t size = 0; qdmi::throwIfError(QDMI_job_query_property(job_.get(), QDMI_JOB_PROPERTY_ID, @@ -685,6 +699,11 @@ Device Session::createSessionlessDevice(QDMI_Device device) { return Device(device); } +Device Session::openDevice(const std::string_view id, + const qdmi::DeviceSessionConfig& overrides) { + return Device(qdmi::Driver::get().openFresh(id, overrides)); +} + Session::Session(const SessionConfig& config) { session_ = [] { QDMI_Session session = nullptr; @@ -720,7 +739,7 @@ Session::Session(const SessionConfig& config) { if (config.authFile) { if (!std::filesystem::exists(*config.authFile)) { throw std::runtime_error("Authentication file does not exist: " + - *config.authFile); + config.authFile->string()); } } // Validate URL format for authUrl @@ -757,7 +776,10 @@ Session::Session(const SessionConfig& config) { // Set session parameters setParameter(config.token, QDMI_SESSION_PARAMETER_TOKEN); - setParameter(config.authFile, QDMI_SESSION_PARAMETER_AUTHFILE); + if (config.authFile) { + const std::optional authFile = config.authFile->string(); + setParameter(authFile, QDMI_SESSION_PARAMETER_AUTHFILE); + } setParameter(config.authUrl, QDMI_SESSION_PARAMETER_AUTHURL); setParameter(config.username, QDMI_SESSION_PARAMETER_USERNAME); setParameter(config.password, QDMI_SESSION_PARAMETER_PASSWORD); diff --git a/src/qdmi/devices/dd/CMakeLists.txt b/src/qdmi/devices/dd/CMakeLists.txt index ee4a2a3458..966028aaba 100644 --- a/src/qdmi/devices/dd/CMakeLists.txt +++ b/src/qdmi/devices/dd/CMakeLists.txt @@ -45,11 +45,7 @@ if(NOT TARGET ${TARGET_NAME}) target_compile_definitions(${TARGET_NAME} PRIVATE BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR) endif() - # Make QDMI version available and ensure symbols are exported when building the library - target_compile_definitions(${TARGET_NAME} PRIVATE QDMI_VERSION="${QDMI_VERSION}" - ${QDMI_PREFIX}_QDMI_device_EXPORTS) - - # Add to list of MQT Core targets + mqt_configure_qdmi_device(${TARGET_NAME} ID mqt.ddsim.default PREFIX ${QDMI_PREFIX}) list(APPEND MQT_CORE_TARGETS ${TARGET_NAME}) endif() diff --git a/src/qdmi/devices/na/CMakeLists.txt b/src/qdmi/devices/na/CMakeLists.txt index fb77c9164c..6e10993b4b 100644 --- a/src/qdmi/devices/na/CMakeLists.txt +++ b/src/qdmi/devices/na/CMakeLists.txt @@ -123,11 +123,7 @@ if(NOT TARGET ${TARGET_NAME}) # add link libraries target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQDMICommon spdlog::spdlog) - # Make QDMI version available and ensure symbols are properly exported when building the library - target_compile_definitions(${TARGET_NAME} PRIVATE QDMI_VERSION="${QDMI_VERSION}" - ${QDMI_PREFIX}_QDMI_device_EXPORTS) - - # add to list of MQT core targets + mqt_configure_qdmi_device(${TARGET_NAME} ID mqt.na.default PREFIX ${QDMI_PREFIX}) list(APPEND MQT_CORE_TARGETS ${TARGET_NAME}) endif() diff --git a/src/qdmi/devices/sc/CMakeLists.txt b/src/qdmi/devices/sc/CMakeLists.txt index d6650f8799..22e78bfdb9 100644 --- a/src/qdmi/devices/sc/CMakeLists.txt +++ b/src/qdmi/devices/sc/CMakeLists.txt @@ -120,11 +120,7 @@ if(NOT TARGET ${TARGET_NAME}) # add link libraries target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQDMICommon spdlog::spdlog) - # Make QDMI version available and ensure symbols are properly exported when building the library - target_compile_definitions(${TARGET_NAME} PRIVATE QDMI_VERSION="${QDMI_VERSION}" - ${QDMI_PREFIX}_QDMI_device_EXPORTS) - - # add to list of MQT core targets + mqt_configure_qdmi_device(${TARGET_NAME} ID mqt.sc.default PREFIX ${QDMI_PREFIX}) list(APPEND MQT_CORE_TARGETS ${TARGET_NAME}) endif() diff --git a/src/qdmi/driver/CMakeLists.txt b/src/qdmi/driver/CMakeLists.txt index 3712a4b386..c2981e9411 100644 --- a/src/qdmi/driver/CMakeLists.txt +++ b/src/qdmi/driver/CMakeLists.txt @@ -13,7 +13,7 @@ if(NOT TARGET ${TARGET_NAME}) add_mqt_core_library(${TARGET_NAME} ALIAS_NAME QDMIDriver) # Add sources to target - target_sources(${TARGET_NAME} PRIVATE Driver.cpp) + target_sources(${TARGET_NAME} PRIVATE DeviceRegistry.cpp Driver.cpp) # Add headers using file sets target_sources(${TARGET_NAME} PUBLIC FILE_SET HEADERS BASE_DIRS ${MQT_CORE_INCLUDE_BUILD_DIR} @@ -23,41 +23,22 @@ if(NOT TARGET ${TARGET_NAME}) target_link_libraries( ${TARGET_NAME} PUBLIC qdmi::qdmi MQT::CoreQDMICommon - PRIVATE qdmi::qdmi_project_warnings spdlog::spdlog ${CMAKE_DL_LIBS}) + PRIVATE qdmi::qdmi_project_warnings nlohmann_json::nlohmann_json spdlog::spdlog + ${CMAKE_DL_LIBS}) + target_include_directories(${TARGET_NAME} SYSTEM + PRIVATE ${PROJECT_SOURCE_DIR}/vendor/tomlplusplus) - add_dependencies(${TARGET_NAME} MQT::CoreQDMINaDevice MQT::CoreQDMIScDevice - MQT::CoreQDMI_DDSIM_Device) - target_compile_definitions( - ${TARGET_NAME} - PRIVATE - "DYN_DEV_LIBS=std::pair{\"$\", \"MQT_NA\"}, std::pair{\"$\", \"MQT_SC\"}, std::pair{\"$\", \"MQT_DDSIM\"}" - ) + mqt_get_qdmi_device_targets(QDMI_DEVICE_TARGETS) # Ensure the driver can find the device libraries at runtime if(WIN32) - # On Windows, we need to copy the device DLLs to the library directory - add_custom_command( - TARGET ${TARGET_NAME} - PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ - $) - add_custom_command( - TARGET ${TARGET_NAME} - PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ - $) - add_custom_command( - TARGET ${TARGET_NAME} - PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ - $) + mqt_copy_qdmi_runtime(${TARGET_NAME} ${QDMI_DEVICE_TARGETS}) else() - target_link_options( - ${TARGET_NAME} - INTERFACE - $> - $> - $>) + add_dependencies(${TARGET_NAME} ${QDMI_DEVICE_TARGETS}) + foreach(device IN LISTS QDMI_DEVICE_TARGETS) + target_link_options(${TARGET_NAME} INTERFACE + $>) + endforeach() endif() # add to list of MQT core targets diff --git a/src/qdmi/driver/DeviceRegistry.cpp b/src/qdmi/driver/DeviceRegistry.cpp new file mode 100644 index 0000000000..bc3537a683 --- /dev/null +++ b/src/qdmi/driver/DeviceRegistry.cpp @@ -0,0 +1,523 @@ +/* + * 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 "DeviceRegistry.hpp" + +#include "qdmi/driver/Driver.hpp" + +#include // NOLINT(misc-include-cleaner) +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace qdmi::detail { +namespace { +using Json = nlohmann::json; // NOLINT(misc-include-cleaner) + +struct SessionPatch { + std::optional baseUrl; + std::optional token; + std::optional authFile; + std::optional authUrl; + std::optional username; + std::optional password; + std::optional custom1; + std::optional custom2; + std::optional custom3; + std::optional custom4; + std::optional custom5; +}; + +struct DefinitionPatch { + std::string id; + std::optional library; + std::optional prefix; + std::optional enabled; + SessionPatch session; + std::filesystem::path source; +}; + +[[nodiscard]] auto sourceLabel(const std::filesystem::path& source, + const std::string_view path) -> std::string { + return source.string() + ":" + std::string(path); +} + +void requireObject(const Json& value, const std::filesystem::path& source, + const std::string_view path) { + if (!value.is_object()) { + throw std::invalid_argument(sourceLabel(source, path) + + " must be an object"); + } +} + +void rejectUnknownKeys(const Json& value, + const std::initializer_list allowed, + const std::filesystem::path& source, + const std::string_view path) { + const std::set known(allowed); + for (const auto& [key, unused] : value.items()) { + static_cast(unused); + if (!known.contains(key)) { + throw std::invalid_argument(sourceLabel(source, path) + + " contains unknown key '" + key + "'"); + } + } +} + +[[nodiscard]] auto optionalString(const Json& value, const std::string& key, + const std::filesystem::path& source, + const std::string& path) + -> std::optional { + const auto it = value.find(key); + if (it == value.end()) { + return std::nullopt; + } + if (!it->is_string()) { + throw std::invalid_argument(sourceLabel(source, path + "." + key) + + " must be a string"); + } + return it->get(); +} + +[[nodiscard]] auto resolvePath(std::filesystem::path path, + const std::filesystem::path& base) + -> std::filesystem::path { + if (path.is_relative()) { + path = base / path; + } + return path.lexically_normal(); +} + +[[nodiscard]] auto absolutePath(const std::filesystem::path& path) + -> std::filesystem::path { + if (path.empty()) { + return {}; + } + return std::filesystem::absolute(path).lexically_normal(); +} + +[[nodiscard]] auto +parseSessionPatch(const Json& value, const std::filesystem::path& source, + const std::string& path, const std::filesystem::path& base) + -> SessionPatch { + requireObject(value, source, path); + rejectUnknownKeys(value, + {"base-url", "token", "auth-file", "auth-url", "username", + "password", "custom1", "custom2", "custom3", "custom4", + "custom5"}, + source, path); + SessionPatch patch; + patch.baseUrl = optionalString(value, "base-url", source, path); + patch.token = optionalString(value, "token", source, path); + patch.authUrl = optionalString(value, "auth-url", source, path); + patch.username = optionalString(value, "username", source, path); + patch.password = optionalString(value, "password", source, path); + patch.custom1 = optionalString(value, "custom1", source, path); + patch.custom2 = optionalString(value, "custom2", source, path); + patch.custom3 = optionalString(value, "custom3", source, path); + patch.custom4 = optionalString(value, "custom4", source, path); + patch.custom5 = optionalString(value, "custom5", source, path); + if (auto authFile = optionalString(value, "auth-file", source, path)) { + patch.authFile = resolvePath(*authFile, base); + } + return patch; +} + +[[nodiscard]] auto +parseDevicePatch(const Json& value, const std::filesystem::path& source, + const std::string& path, const std::filesystem::path& base) + -> DefinitionPatch { + requireObject(value, source, path); + rejectUnknownKeys(value, {"id", "library", "prefix", "enabled", "session"}, + source, path); + const auto id = optionalString(value, "id", source, path); + if (!id || id->empty()) { + throw std::invalid_argument(sourceLabel(source, path + ".id") + + " must be a non-empty string"); + } + DefinitionPatch patch; + patch.id = *id; + patch.source = source; + if (auto library = optionalString(value, "library", source, path)) { + patch.library = resolvePath(*library, base); + } + patch.prefix = optionalString(value, "prefix", source, path); + if (const auto it = value.find("enabled"); it != value.end()) { + if (!it->is_boolean()) { + throw std::invalid_argument(sourceLabel(source, path + ".enabled") + + " must be a boolean"); + } + patch.enabled = it->get(); + } + if (const auto it = value.find("session"); it != value.end()) { + patch.session = parseSessionPatch(*it, source, path + ".session", base); + } + return patch; +} + +[[nodiscard]] auto parseConfiguration(const Json& root, + const std::filesystem::path& source, + const std::filesystem::path& base) + -> std::vector { + requireObject(root, source, "$"); + rejectUnknownKeys(root, {"schema-version", "qdmi"}, source, "$"); + const auto version = root.find("schema-version"); + if (version == root.end() || !version->is_number_integer() || + version->get() != 1) { + throw std::invalid_argument(sourceLabel(source, "$.schema-version") + + " must be the integer 1"); + } + const auto qdmiConfig = root.find("qdmi"); + if (qdmiConfig == root.end()) { + return {}; + } + requireObject(*qdmiConfig, source, "$.qdmi"); + rejectUnknownKeys(*qdmiConfig, {"devices"}, source, "$.qdmi"); + const auto devices = qdmiConfig->find("devices"); + if (devices == qdmiConfig->end()) { + return {}; + } + if (!devices->is_array()) { + throw std::invalid_argument(sourceLabel(source, "$.qdmi.devices") + + " must be an array"); + } + std::set ids; + std::vector patches; + patches.reserve(devices->size()); + for (size_t i = 0; i < devices->size(); ++i) { + auto patch = + parseDevicePatch((*devices)[i], source, + "$.qdmi.devices[" + std::to_string(i) + "]", base); + if (!ids.emplace(patch.id).second) { + throw std::invalid_argument(sourceLabel(source, "$.qdmi.devices") + + " contains duplicate id '" + patch.id + "'"); + } + patches.emplace_back(std::move(patch)); + } + return patches; +} + +[[nodiscard]] auto readJson(const std::filesystem::path& path) -> Json { + std::ifstream stream(path); + if (!stream) { + throw std::runtime_error("Cannot open QDMI configuration file: " + + path.string()); + } + try { + return Json::parse(stream); + } catch (const Json::parse_error& error) { + throw std::invalid_argument(path.string() + + ": invalid JSON: " + error.what()); + } +} + +[[nodiscard]] auto readPyproject(const std::filesystem::path& path) + -> std::optional { + try { + const auto table = toml::parse_file(path.string()); + const auto* qdmiTable = table["tool"]["qdmi"].as_table(); + if (qdmiTable == nullptr) { + return std::nullopt; + } + std::ostringstream formatted; + formatted << toml::json_formatter{*qdmiTable}; + return Json{{"schema-version", 1}, {"qdmi", Json::parse(formatted.str())}}; + } catch (const toml::parse_error& error) { + throw std::invalid_argument( + path.string() + ": invalid TOML: " + std::string(error.description())); + } +} + +template +void mergeOptional(std::optional& target, const std::optional& source) { + if (source) { + target = source; + } +} + +void mergeSession(SessionPatch& target, const SessionPatch& source) { + mergeOptional(target.baseUrl, source.baseUrl); + mergeOptional(target.token, source.token); + mergeOptional(target.authFile, source.authFile); + mergeOptional(target.authUrl, source.authUrl); + mergeOptional(target.username, source.username); + mergeOptional(target.password, source.password); + mergeOptional(target.custom1, source.custom1); + mergeOptional(target.custom2, source.custom2); + mergeOptional(target.custom3, source.custom3); + mergeOptional(target.custom4, source.custom4); + mergeOptional(target.custom5, source.custom5); +} + +void mergePatch(DefinitionPatch& target, const DefinitionPatch& source) { + mergeOptional(target.library, source.library); + mergeOptional(target.prefix, source.prefix); + mergeOptional(target.enabled, source.enabled); + mergeSession(target.session, source.session); + target.source = source.source; +} + +[[nodiscard]] auto moduleDirectory() -> std::filesystem::path { +#ifdef _WIN32 + HMODULE module = nullptr; + if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&moduleDirectory), + &module) == 0) { + return {}; + } + std::wstring buffer(MAX_PATH, L'\0'); + while (true) { + const auto size = GetModuleFileNameW(module, buffer.data(), + static_cast(buffer.size())); + if (size == 0) { + return {}; + } + if (size < buffer.size()) { + buffer.resize(size); + return std::filesystem::path(buffer).parent_path(); + } + buffer.resize(buffer.size() * 2); + } +#else + Dl_info info{}; + if (dladdr(reinterpret_cast(&moduleDirectory), &info) == 0 || + info.dli_fname == nullptr) { + return {}; + } + return std::filesystem::path(info.dli_fname).parent_path(); +#endif +} + +[[nodiscard]] auto environment(const char* name) -> std::optional { +#ifdef _WIN32 + char* raw = nullptr; + size_t size = 0; + if (_dupenv_s(&raw, &size, name) != 0 || raw == nullptr) { + return std::nullopt; + } + const std::unique_ptr value(raw, &std::free); + if (*value == '\0') { + return std::nullopt; + } + return std::string(value.get()); +#else + if (const auto* value = std::getenv(name); + value != nullptr && *value != '\0') { + return std::string(value); + } + return std::nullopt; +#endif +} + +void appendIfFile(std::vector& files, + const std::filesystem::path& path) { + const auto absolute = absolutePath(path); + if (absolute.empty()) { + return; + } + std::error_code error; + if (std::filesystem::is_regular_file(absolute, error)) { + files.emplace_back(absolute); + } +} + +void appendFragments(std::vector& files, + const std::filesystem::path& directory) { + const auto absolute = absolutePath(directory); + if (absolute.empty()) { + return; + } + std::error_code error; + if (!std::filesystem::is_directory(absolute, error)) { + return; + } + std::vector found; + for (const auto& entry : std::filesystem::directory_iterator(absolute)) { + if (entry.is_regular_file() && + entry.path().filename().string().ends_with(".qdmi.json")) { + found.emplace_back(entry.path()); + } + } + std::ranges::sort(found); + files.insert(files.end(), found.begin(), found.end()); +} + +[[nodiscard]] auto nearestProjectConfiguration(std::filesystem::path directory) + -> std::optional { + while (!directory.empty()) { + auto dedicated = directory / "qdmi.json"; + if (std::filesystem::is_regular_file(dedicated)) { + return dedicated; + } + auto pyproject = directory / "pyproject.toml"; + if (std::filesystem::is_regular_file(pyproject)) { + if (readPyproject(pyproject)) { + return pyproject; + } + } + const auto parent = directory.parent_path(); + if (parent == directory) { + break; + } + directory = parent; + } + return std::nullopt; +} + +[[nodiscard]] auto discoverFiles() -> std::vector { + std::vector files; + const auto root = moduleDirectory(); + appendFragments(files, root); + appendFragments(files, root / "bin"); + appendFragments(files, root / "lib"); + appendFragments(files, root / "mqt-core" / "qdmi"); + appendFragments(files, root / "qdmi"); + + std::optional explicitFile; + if (auto value = environment("MQT_CORE_QDMI_CONFIG_FILE")) { + explicitFile = *value; + } + if (explicitFile) { + const auto resolved = + resolvePath(*explicitFile, std::filesystem::current_path()); + if (!std::filesystem::is_regular_file(resolved)) { + throw std::runtime_error("Explicit QDMI configuration file does not " + "exist: " + + resolved.string()); + } + files.emplace_back(resolved); + return files; + } + +#ifdef _WIN32 + if (auto programData = environment("PROGRAMDATA")) { + appendIfFile(files, std::filesystem::path(*programData) / "mqt-core" / + "qdmi.json"); + } + if (auto appData = environment("APPDATA")) { + appendIfFile(files, + std::filesystem::path(*appData) / "mqt-core" / "qdmi.json"); + } +#else + appendIfFile(files, "/etc/mqt-core/qdmi.json"); + if (auto xdg = environment("XDG_CONFIG_HOME")) { + appendIfFile(files, std::filesystem::path(*xdg) / "mqt-core" / "qdmi.json"); + } else if (auto home = environment("HOME")) { + appendIfFile(files, std::filesystem::path(*home) / ".config" / "mqt-core" / + "qdmi.json"); + } +#endif + if (auto project = + nearestProjectConfiguration(std::filesystem::current_path())) { + files.emplace_back(std::move(*project)); + } + return files; +} + +[[nodiscard]] auto materialize(const DefinitionPatch& patch) + -> std::optional { + if (!patch.enabled.value_or(true)) { + return std::nullopt; + } + if (!patch.library || patch.library->empty()) { + throw std::invalid_argument(patch.source.string() + ": enabled device '" + + patch.id + "' is missing library"); + } + if (!patch.prefix || patch.prefix->empty()) { + throw std::invalid_argument(patch.source.string() + ": enabled device '" + + patch.id + "' is missing prefix"); + } + qdmi::DeviceDefinition definition; + definition.id = patch.id; + definition.library = *patch.library; + definition.prefix = *patch.prefix; + definition.session.baseUrl = patch.session.baseUrl; + definition.session.token = patch.session.token; + if (patch.session.authFile) { + definition.session.authFile = patch.session.authFile; + } + definition.session.authUrl = patch.session.authUrl; + definition.session.username = patch.session.username; + definition.session.password = patch.session.password; + definition.session.custom1 = patch.session.custom1; + definition.session.custom2 = patch.session.custom2; + definition.session.custom3 = patch.session.custom3; + definition.session.custom4 = patch.session.custom4; + definition.session.custom5 = patch.session.custom5; + return definition; +} + +} // namespace + +DeviceRegistry::DeviceRegistry() { + std::map merged; + const auto mergePatches = [&merged](std::vector patches) { + for (auto& patch : patches) { + if (auto it = merged.find(patch.id); it != merged.end()) { + mergePatch(it->second, patch); + } else { + merged.emplace(patch.id, std::move(patch)); + } + } + }; + + for (const auto& file : discoverFiles()) { + if (file.filename() == "pyproject.toml") { + if (auto config = readPyproject(file)) { + mergePatches(parseConfiguration(*config, file, file.parent_path())); + } + } else { + mergePatches( + parseConfiguration(readJson(file), file, file.parent_path())); + } + } + const auto inlineBase = std::filesystem::current_path(); + if (auto inlineJson = environment("MQT_CORE_QDMI_CONFIG_JSON")) { + try { + mergePatches(parseConfiguration( + Json::parse(*inlineJson), "", inlineBase)); + } catch (const Json::parse_error& error) { + throw std::invalid_argument( + std::string(": invalid JSON: ") + + error.what()); + } + } + for (auto& [unused, patch] : merged) { + static_cast(unused); + if (!patch.enabled.value_or(true)) { + disabledIds_.emplace_back(std::move(patch.id)); + } else if (auto definition = materialize(patch)) { + definitions_.emplace_back(std::move(*definition)); + } + } +} + +} // namespace qdmi::detail diff --git a/src/qdmi/driver/DeviceRegistry.hpp b/src/qdmi/driver/DeviceRegistry.hpp new file mode 100644 index 0000000000..83061f2f62 --- /dev/null +++ b/src/qdmi/driver/DeviceRegistry.hpp @@ -0,0 +1,38 @@ +/* + * 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 "qdmi/driver/Driver.hpp" + +#include +#include + +namespace qdmi::detail { + +/// Discovers configured QDMI devices without loading their libraries. +class DeviceRegistry { +public: + DeviceRegistry(); + + [[nodiscard]] const std::vector& definitions() const { + return definitions_; + } + + [[nodiscard]] const std::vector& disabledIds() const { + return disabledIds_; + } + +private: + std::vector definitions_; + std::vector disabledIds_; +}; + +} // namespace qdmi::detail diff --git a/src/qdmi/driver/Driver.cpp b/src/qdmi/driver/Driver.cpp index 2562f338c3..9bc695e16b 100644 --- a/src/qdmi/driver/Driver.cpp +++ b/src/qdmi/driver/Driver.cpp @@ -10,6 +10,7 @@ #include "qdmi/driver/Driver.hpp" +#include "DeviceRegistry.hpp" #include "qdmi/common/Common.hpp" #include @@ -17,24 +18,27 @@ #include #include -#include #include #include #include #include +#include +#include +#include #include +#include #include #include #include #include +#include +#include #include #include #include #ifdef _WIN32 #include - -#include #else #include #endif // _WIN32 @@ -74,15 +78,13 @@ namespace { /// directory if no path is specified. [[nodiscard]] auto loadDeviceLibrary(const std::string& libName) -> HMODULE { const auto requested = std::filesystem::path(libName); - - if (requested.has_parent_path()) { - // A directory component was supplied: Directly load the library. - return LoadLibraryW(requested.wstring().c_str()); - } - - // Bare filename: resolve relative to the driver's own directory so that - // builtin device DLLs installed next to the driver are found reliably. - const auto path = getDriverDirectory() / requested; + // Bare filenames are resolved relative to the Driver. Configured paths are + // already absolute or relative to their declaring file. + const auto path = requested.has_parent_path() + ? requested + : getDriverDirectory() / requested; + // Search beside the device DLL for its dependencies. This is required for + // device implementations such as DDSIM in an installed Python wheel. return LoadLibraryExW(path.wstring().c_str(), nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); @@ -145,12 +147,14 @@ DynamicDeviceLibrary::DynamicDeviceLibrary(const std::string& libName, LOAD_DYNAMIC_SYMBOL(device_session_query_site_property) LOAD_DYNAMIC_SYMBOL(device_session_query_operation_property) // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) - } catch (const std::exception&) { + // Initialize the device library only after every required symbol is + // available. + throwIfError(device_initialize(), "Failed to initialize device library"); + } catch (...) { DL_CLOSE(libHandle_); + libHandle_ = nullptr; throw; } - // initialize the device - device_initialize(); } DynamicDeviceLibrary::~DynamicDeviceLibrary() { @@ -164,6 +168,66 @@ DynamicDeviceLibrary::~DynamicDeviceLibrary() { } } +namespace { +struct DynamicLibraryCache { + std::mutex mutex; + std::map, + std::weak_ptr> + libraries; +}; + +[[nodiscard]] auto dynamicLibraryCache() -> DynamicLibraryCache& { + static DynamicLibraryCache cache; + return cache; +} + +[[nodiscard]] auto getDynamicDeviceLibrary(const std::string& libName, + const std::string& prefix) + -> std::shared_ptr { + auto& cache = dynamicLibraryCache(); + const std::scoped_lock lock(cache.mutex); + std::error_code error; + auto canonicalPath = std::filesystem::weakly_canonical( + std::filesystem::absolute(std::filesystem::path(libName), error), error); + if (error) { + canonicalPath = std::filesystem::path(libName).lexically_normal(); + } + const auto key = std::pair{canonicalPath.string(), prefix}; + if (const auto library = cache.libraries[key].lock()) { + return library; + } + auto library = std::make_shared(libName, prefix); + cache.libraries[key] = library; + return library; +} + +template +void applyOverride(std::optional& value, + const std::optional& overrideValue) { + if (overrideValue) { + value = overrideValue; + } +} + +[[nodiscard]] auto mergeSessionConfig(const DeviceSessionConfig& defaults, + const DeviceSessionConfig& overrides) + -> DeviceSessionConfig { + auto merged = defaults; + applyOverride(merged.baseUrl, overrides.baseUrl); + applyOverride(merged.token, overrides.token); + applyOverride(merged.authFile, overrides.authFile); + applyOverride(merged.authUrl, overrides.authUrl); + applyOverride(merged.username, overrides.username); + applyOverride(merged.password, overrides.password); + applyOverride(merged.custom1, overrides.custom1); + applyOverride(merged.custom2, overrides.custom2); + applyOverride(merged.custom3, overrides.custom3); + applyOverride(merged.custom4, overrides.custom4); + applyOverride(merged.custom5, overrides.custom5); + return merged; +} +} // namespace + #undef DL_OPEN #undef DL_SYM #undef DL_CLOSE @@ -205,7 +269,10 @@ QDMI_Device_impl_d::QDMI_Device_impl_d( setParameter(config.baseUrl, QDMI_DEVICE_SESSION_PARAMETER_BASEURL); setParameter(config.token, QDMI_DEVICE_SESSION_PARAMETER_TOKEN); - setParameter(config.authFile, QDMI_DEVICE_SESSION_PARAMETER_AUTHFILE); + if (config.authFile) { + const std::optional authFile = config.authFile->string(); + setParameter(authFile, QDMI_DEVICE_SESSION_PARAMETER_AUTHFILE); + } setParameter(config.authUrl, QDMI_DEVICE_SESSION_PARAMETER_AUTHURL); setParameter(config.username, QDMI_DEVICE_SESSION_PARAMETER_USERNAME); setParameter(config.password, QDMI_DEVICE_SESSION_PARAMETER_PASSWORD); @@ -379,6 +446,17 @@ namespace { } } // namespace +QDMI_Session_impl_d::QDMI_Session_impl_d( + const std::vector>& devices) { + devices_.reserve(devices.size()); + std::ranges::transform(devices, std::back_inserter(devices_), + [](const auto& device) { return device.get(); }); +} + +QDMI_Session_impl_d::QDMI_Session_impl_d( + const std::vector& devices) + : devices_(devices) {} + QDMI_Job_impl_d::~QDMI_Job_impl_d() { device_->getLibrary().device_job_free(deviceJob_); } @@ -481,14 +559,14 @@ auto QDMI_Session_impl_d::querySessionProperty(QDMI_Session_Property prop, } if (prop == QDMI_SESSION_PROPERTY_DEVICES) { if (value != nullptr) { - if (size < devices_->size() * sizeof(QDMI_Device)) { + if (size < devices_.size() * sizeof(QDMI_Device)) { return QDMI_ERROR_INVALIDARGUMENT; } - memcpy(value, static_cast(devices_->data()), - devices_->size() * sizeof(QDMI_Device)); + memcpy(value, static_cast(devices_.data()), + devices_.size() * sizeof(QDMI_Device)); } if (sizeRet != nullptr) { - *sizeRet = devices_->size() * sizeof(QDMI_Device); + *sizeRet = devices_.size() * sizeof(QDMI_Device); } return QDMI_SUCCESS; } @@ -496,31 +574,138 @@ auto QDMI_Session_impl_d::querySessionProperty(QDMI_Session_Property prop, } namespace qdmi { +namespace { +void validateDefinition(const DeviceDefinition& definition) { + if (definition.id.empty()) { + throw std::invalid_argument("Device definition ID must not be empty"); + } + if (definition.library.empty()) { + throw std::invalid_argument("Device definition library must not be empty"); + } + if (definition.prefix.empty()) { + throw std::invalid_argument("Device definition prefix must not be empty"); + } +} +} // namespace + +auto Driver::get() -> Driver& { + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + static auto* instance = new Driver(); + return *instance; +} + Driver::Driver() { - for (const auto& [lib, prefix] : std::array{DYN_DEV_LIBS}) { - try { - addDynamicDeviceLibrary(lib, prefix); - } catch (const std::exception& ex) { - SPDLOG_WARN("Skipping builtin QDMI device library '{}': {}", lib, - ex.what()); + const detail::DeviceRegistry registry; + disabledDeviceIds_.insert(registry.disabledIds().begin(), + registry.disabledIds().end()); + for (const auto& definition : registry.definitions()) { + registerDevice(definition); + clientDefinitionIds_.emplace_back(definition.id); + } +} + +void Driver::registerDevice(DeviceDefinition definition, const bool replace) { + validateDefinition(definition); + if (disabledDeviceIds_.contains(definition.id)) { + if (!replace) { + throw std::invalid_argument("QDMI device ID '" + definition.id + + "' is disabled by configuration"); } + disabledDeviceIds_.erase(definition.id); + } + const auto existing = + std::ranges::find(definitions_, definition.id, &DeviceDefinition::id); + if (existing == definitions_.end()) { + definitions_.emplace_back(std::move(definition)); + return; } + if (!replace) { + throw std::invalid_argument("QDMI device ID '" + definition.id + + "' is already registered"); + } + if (openedDevices_.contains(definition.id)) { + throw std::runtime_error("Cannot replace opened QDMI device ID '" + + definition.id + "'"); + } + *existing = std::move(definition); } -auto Driver::addDynamicDeviceLibrary(const std::string& libName, - const std::string& prefix, - const DeviceSessionConfig& config) - -> QDMI_Device { +auto Driver::registerDeviceIfAbsent(DeviceDefinition definition) -> bool { + validateDefinition(definition); + if (disabledDeviceIds_.contains(definition.id) || + std::ranges::find(definitions_, definition.id, &DeviceDefinition::id) != + definitions_.end()) { + return false; + } + definitions_.emplace_back(std::move(definition)); + return true; +} + +auto Driver::open(const std::string_view id) -> QDMI_Device { + if (disabledDeviceIds_.contains(std::string(id))) { + throw std::runtime_error("QDMI device ID '" + std::string(id) + + "' is disabled by configuration"); + } + if (const auto opened = openedDevices_.find(std::string(id)); + opened != openedDevices_.end()) { + return opened->second; + } + const auto definition = + std::ranges::find(definitions_, id, &DeviceDefinition::id); + if (definition == definitions_.end()) { + throw std::out_of_range("Unknown QDMI device ID '" + std::string(id) + "'"); + } devices_.emplace_back(std::make_unique( - std::make_shared(libName, prefix), config)); - return devices_.back().get(); + getDynamicDeviceLibrary(definition->library.string(), definition->prefix), + definition->session)); + auto* const device = devices_.back().get(); + openedDevices_.emplace(definition->id, device); + return device; +} + +auto Driver::openFresh(const std::string_view id, + const DeviceSessionConfig& overrides) + -> std::shared_ptr { + if (disabledDeviceIds_.contains(std::string(id))) { + throw std::runtime_error("QDMI device ID '" + std::string(id) + + "' is disabled by configuration"); + } + const auto definition = + std::ranges::find(definitions_, id, &DeviceDefinition::id); + if (definition == definitions_.end()) { + throw std::out_of_range("Unknown QDMI device ID '" + std::string(id) + "'"); + } + return std::make_shared( + getDynamicDeviceLibrary(definition->library.string(), definition->prefix), + mergeSessionConfig(definition->session, overrides)); +} + +void Driver::materializeClientCatalog() { + if (clientCatalogMaterialized_) { + return; + } + clientCatalogMaterialized_ = true; + for (const auto& id : clientDefinitionIds_) { + try { + clientDevices_.emplace_back(open(id)); + } catch (const std::exception& ex) { + const auto definition = + std::ranges::find(definitions_, id, &DeviceDefinition::id); + const auto library = definition == definitions_.end() + ? std::string("") + : definition->library.string(); + SPDLOG_WARN("Skipping configured QDMI device '{}' from '{}': {}", id, + library, ex.what()); + } + } } auto Driver::sessionAlloc(QDMI_Session* session) -> int { if (session == nullptr) { return QDMI_ERROR_INVALIDARGUMENT; } - auto uniqueSession = std::make_unique(devices_); + materializeClientCatalog(); + auto uniqueSession = std::make_unique(clientDevices_); *session = sessions_.emplace(uniqueSession.get(), std::move(uniqueSession)) .first->first; return QDMI_SUCCESS; diff --git a/test/fomac/CMakeLists.txt b/test/fomac/CMakeLists.txt index dd6542126f..5d435c3927 100644 --- a/test/fomac/CMakeLists.txt +++ b/test/fomac/CMakeLists.txt @@ -10,23 +10,5 @@ set(TARGET_NAME mqt-core-fomac-test) if(TARGET MQT::CoreFoMaC) package_add_test(${TARGET_NAME} MQT::CoreFoMaC test_fomac.cpp) - - if(WIN32) - # On Windows, we need to copy the device DLLs to the test directory - add_custom_command( - TARGET ${TARGET_NAME} - PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ - $) - add_custom_command( - TARGET ${TARGET_NAME} - PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ - $) - add_custom_command( - TARGET ${TARGET_NAME} - PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ - $) - endif() + mqt_copy_qdmi_runtime(${TARGET_NAME}) endif() diff --git a/test/fomac/test_fomac.cpp b/test/fomac/test_fomac.cpp index 935091d847..8182e89d35 100644 --- a/test/fomac/test_fomac.cpp +++ b/test/fomac/test_fomac.cpp @@ -346,6 +346,8 @@ TEST(FoMaCTest, DevicePropertyToString) { "MIN ATOM DISTANCE"); EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_SUPPORTEDPROGRAMFORMATS), "SUPPORTED PROGRAM FORMATS"); + EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_CHILDDEVICES), + "CHILD DEVICES"); EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_MAX), "MAX"); EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_CUSTOM1), "CUSTOM1"); EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_CUSTOM2), "CUSTOM2"); @@ -358,6 +360,11 @@ TEST(FoMaCTest, SessionPropertyToString) { EXPECT_STREQ(qdmi::toString(QDMI_SESSION_PROPERTY_DEVICES), "DEVICES"); } +TEST(FoMaCTest, DeviceSessionParameterToString) { + EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_SESSION_PARAMETER_CHILDDEVICE), + "CHILD DEVICE"); +} + TEST(FoMaCTest, ThrowIfError) { EXPECT_NO_THROW(qdmi::throwIfError(QDMI_SUCCESS, "Test")); EXPECT_NO_THROW(qdmi::throwIfError(QDMI_WARN_GENERAL, "Test")); @@ -1090,7 +1097,7 @@ TEST(AuthenticationTest, SessionConstructionWithAuthFile) { } SessionConfig config2; - config2.authFile = tmpPath.string(); + config2.authFile = tmpPath; EXPECT_NO_THROW({ const Session session(config2); }); // Clean up @@ -1220,6 +1227,34 @@ TEST(AuthenticationTest, SessionMultipleInstances) { EXPECT_EQ(devices1.size(), devices2.size()); } +TEST(DeviceOwnershipTest, SiteKeepsFreshSessionAlive) { + const auto site = [] { + auto device = Session::openDevice("mqt.na.default"); + return device.getSites().front(); + }(); + + EXPECT_EQ(site.getIndex(), 0); +} + +TEST(DeviceOwnershipTest, OperationKeepsFreshSessionAlive) { + const auto operation = [] { + auto device = Session::openDevice("mqt.na.default"); + return device.getOperations().front(); + }(); + + EXPECT_FALSE(operation.getName().empty()); +} + +TEST(DeviceOwnershipTest, SiteFromOperationKeepsFreshSessionAlive) { + const auto site = [] { + auto device = Session::openDevice("mqt.na.default"); + const auto operation = device.getOperations().front(); + return operation.getSites().value().front(); + }(); + + EXPECT_TRUE(site.isZone()); +} + namespace { // Helper function to get all devices for parameterized tests auto getDevices() -> std::vector { diff --git a/test/na/fomac/CMakeLists.txt b/test/na/fomac/CMakeLists.txt index b30219f250..05228857df 100644 --- a/test/na/fomac/CMakeLists.txt +++ b/test/na/fomac/CMakeLists.txt @@ -14,23 +14,5 @@ if(TARGET MQT::CoreNAFoMaC) # Set the device json path target_compile_definitions(${TARGET_NAME} PRIVATE NA_DEVICE_JSON="${PROJECT_SOURCE_DIR}/json/na/device.json") - - if(WIN32) - # On Windows, we need to copy the device DLLs to the test directory - add_custom_command( - TARGET ${TARGET_NAME} - PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ - $) - add_custom_command( - TARGET ${TARGET_NAME} - PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ - $) - add_custom_command( - TARGET ${TARGET_NAME} - PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ - $) - endif() + mqt_copy_qdmi_runtime(${TARGET_NAME}) endif() diff --git a/test/python/fomac/test_fomac.py b/test/python/fomac/test_fomac.py index 6cdc5c63d9..3886c5f5f9 100644 --- a/test/python/fomac/test_fomac.py +++ b/test/python/fomac/test_fomac.py @@ -16,7 +16,17 @@ import pytest -from mqt.core.fomac import CustomProperty, Device, Job, ProgramFormat, Session, add_dynamic_device_library +from mqt.core.fomac import ( + CustomProperty, + Device, + DeviceDefinition, + Job, + ProgramFormat, + Session, + open_device, + register_device, + register_device_if_absent, +) CustomValueType = type[str] | type[bool] | type[int] | type[float] | type[bytes] @@ -767,9 +777,11 @@ def test_session_construction_with_auth_file() -> None: tmp_path = tmp_file.name try: - # Existing file should be accepted (validation passes, parameter may be skipped) - session = Session(auth_file=tmp_path) - assert session is not None + # Both string and pathlib paths should be accepted. + string_session = Session(auth_file=tmp_path) + path_session = Session(auth_file=Path(tmp_path)) + assert string_session is not None + assert path_session is not None finally: # Clean up Path(tmp_path).unlink(missing_ok=True) @@ -893,7 +905,46 @@ def test_session_multiple_instances() -> None: assert len(devices1) == len(devices2) -def test_add_dynamic_device_library_nonexistent_library() -> None: - """Test that loading a non-existent library raises an error.""" +def test_register_device_does_not_load_nonexistent_library() -> None: + """Registration stores metadata and opening performs native loading.""" + definition = DeviceDefinition("python.missing", "/nonexistent/lib.so", "PREFIX") + assert definition.device_id == "python.missing" + assert definition.library_path == "/nonexistent/lib.so" + assert definition.prefix == "PREFIX" + register_device(definition) with pytest.raises(RuntimeError): - add_dynamic_device_library("/nonexistent/lib.so", "PREFIX") + open_device("python.missing") + + +def test_register_device_if_absent_only_ignores_existing_id() -> None: + """Idempotent registration still validates duplicate definitions.""" + definition = DeviceDefinition("python.if-absent", "/nonexistent/device.so", "PREFIX") + assert register_device_if_absent(definition) + assert not register_device_if_absent(definition) + with pytest.raises(ValueError, match="library must not be empty"): + register_device_if_absent(DeviceDefinition("python.if-absent", "", "PREFIX")) + + +def test_open_device_rejects_unknown_id() -> None: + """Opening requires a stable registered ID.""" + with pytest.raises(IndexError, match="Unknown QDMI device ID"): + open_device("python.unknown") + + +def test_open_device_creates_a_fresh_session() -> None: + """Stable-ID opens should return separately owned sessions.""" + first = open_device("mqt.na.default") + second = open_device("mqt.na.default") + assert first != second + + +def test_site_keeps_fresh_session_alive() -> None: + """A site should remain usable after its device wrapper is destroyed.""" + site = open_device("mqt.na.default").sites()[0] + assert site.index() == 0 + + +def test_operation_keeps_fresh_session_alive() -> None: + """An operation should remain usable after its device wrapper is destroyed.""" + operation = open_device("mqt.na.default").operations()[0] + assert operation.name() diff --git a/test/qdmi/CMakeLists.txt b/test/qdmi/CMakeLists.txt index 595fc88d15..a1af9424f3 100644 --- a/test/qdmi/CMakeLists.txt +++ b/test/qdmi/CMakeLists.txt @@ -8,3 +8,4 @@ add_subdirectory(devices) add_subdirectory(driver) +add_subdirectory(registry) diff --git a/test/qdmi/driver/CMakeLists.txt b/test/qdmi/driver/CMakeLists.txt index 6fc3089a23..bec8f47243 100644 --- a/test/qdmi/driver/CMakeLists.txt +++ b/test/qdmi/driver/CMakeLists.txt @@ -9,30 +9,71 @@ set(TARGET_NAME mqt-core-qdmi-driver-test) if(TARGET MQT::CoreQDMIDriver) + add_library(mqt-core-qdmi-metadata-device SHARED metadata_device.cpp) + set_target_properties( + mqt-core-qdmi-metadata-device + PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/device" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/device" + MQT_QDMI_DEVICE_ID "test.metadata-only" + MQT_QDMI_DEVICE_PREFIX "TEST_METADATA") + set_property( + TARGET mqt-core-qdmi-metadata-device + APPEND + PROPERTY EXPORT_PROPERTIES MQT_QDMI_DEVICE_ID MQT_QDMI_DEVICE_PREFIX) + set(metadata_device_export + "${CMAKE_CURRENT_BINARY_DIR}/mqt-core-qdmi-metadata-device-targets.cmake") + export(TARGETS mqt-core-qdmi-metadata-device FILE "${metadata_device_export}") + + add_library(mqt-core-qdmi-session-device SHARED session_device.cpp) + target_link_libraries(mqt-core-qdmi-session-device PRIVATE qdmi::qdmi) + set_target_properties( + mqt-core-qdmi-session-device + PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/device" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/device") + package_add_test(${TARGET_NAME} MQT::CoreQDMIDriver test_driver.cpp) target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreFoMaC) + set(config_file "${CMAKE_CURRENT_BINARY_DIR}/$/configured-devices.json") + file( + GENERATE + OUTPUT "${config_file}" + CONTENT + "{\n \"schema-version\": 1,\n \"qdmi\": {\n \"devices\": [\n {\"id\": \"mqt.na.default\", \"library\": \"$\", \"prefix\": \"MQT_NA\"},\n {\"id\": \"mqt.sc.default\", \"library\": \"$\", \"prefix\": \"MQT_SC\"},\n {\"id\": \"mqt.ddsim.default\", \"library\": \"$\", \"prefix\": \"MQT_DDSIM\"},\n {\"id\": \"test.disabled\", \"enabled\": false},\n {\"id\": \"broken.example\", \"library\": \"missing-device-library\", \"prefix\": \"BROKEN\"}\n ]\n }\n}\n" + ) + string(MAKE_C_IDENTIFIER "${TARGET_NAME}-mqt-core-qdmi-metadata-device" metadata_manifest_stem) + set(metadata_manifest_file + "$/${metadata_manifest_stem}.qdmi.json") target_compile_definitions( ${TARGET_NAME} PRIVATE - "DYN_DEV_LIBS=std::array{ std::pair{\"$\", \"MQT_NA\"}, std::pair{\"$\", \"MQT_SC\"}, std::pair{\"$\", \"MQT_DDSIM\"} }" + "MQT_CORE_QDMI_TEST_CONFIG_FILE=\"${config_file}\"" + "MQT_CORE_QDMI_METADATA_MANIFEST=\"${metadata_manifest_file}\"" + "MQT_CORE_QDMI_SESSION_DEVICE=\"$\"" + "TEST_DEVICE_LIBRARIES=std::array{ std::pair{\"$\", \"MQT_NA\"}, std::pair{\"$\", \"MQT_SC\"}, std::pair{\"$\", \"MQT_DDSIM\"} }" ) - if(WIN32) - # On Windows, we need to copy the device DLLs to the test directory - add_custom_command( - TARGET ${TARGET_NAME} - PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ - $) - add_custom_command( - TARGET ${TARGET_NAME} - PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ - $) - add_custom_command( - TARGET ${TARGET_NAME} - PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ - $) + mqt_copy_qdmi_runtime(${TARGET_NAME}) + mqt_copy_qdmi_runtime(${TARGET_NAME} mqt-core-qdmi-metadata-device) + add_dependencies(${TARGET_NAME} mqt-core-qdmi-session-device) + + set(imported_device_build_dir "${CMAKE_CURRENT_BINARY_DIR}/imported-device-consumer") + set(imported_device_configure_command + ${CMAKE_COMMAND} -S "${CMAKE_CURRENT_SOURCE_DIR}/imported_device" -B + "${imported_device_build_dir}" -G "${CMAKE_GENERATOR}" + "-DMQT_CORE_QDMI_DEVICE_TARGETS=${metadata_device_export}" + "-DMQT_CORE_QDMI_HELPER=${PROJECT_SOURCE_DIR}/cmake/AddMQTQDMIDevice.cmake") + if(CMAKE_GENERATOR_PLATFORM) + list(APPEND imported_device_configure_command -A "${CMAKE_GENERATOR_PLATFORM}") + endif() + if(CMAKE_GENERATOR_TOOLSET) + list(APPEND imported_device_configure_command -T "${CMAKE_GENERATOR_TOOLSET}") endif() + add_test(NAME mqt-core-qdmi-imported-device-configure + COMMAND ${imported_device_configure_command}) + add_test(NAME mqt-core-qdmi-imported-device-build + COMMAND ${CMAKE_COMMAND} --build "${imported_device_build_dir}" --config $) + set_tests_properties(mqt-core-qdmi-imported-device-configure + PROPERTIES FIXTURES_SETUP mqt-core-qdmi-imported-device) + set_tests_properties(mqt-core-qdmi-imported-device-build PROPERTIES FIXTURES_REQUIRED + mqt-core-qdmi-imported-device) endif() diff --git a/test/qdmi/driver/imported_device/CMakeLists.txt b/test/qdmi/driver/imported_device/CMakeLists.txt new file mode 100644 index 0000000000..49043d90d8 --- /dev/null +++ b/test/qdmi/driver/imported_device/CMakeLists.txt @@ -0,0 +1,28 @@ +# 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 + +cmake_minimum_required(VERSION 3.24) +project(mqt-core-qdmi-imported-device-test LANGUAGES CXX) + +if(NOT MQT_CORE_QDMI_DEVICE_TARGETS OR NOT MQT_CORE_QDMI_HELPER) + message(FATAL_ERROR "Device targets and the QDMI helper are required") +endif() + +include("${MQT_CORE_QDMI_DEVICE_TARGETS}") +include("${MQT_CORE_QDMI_HELPER}") + +set(device mqt-core-qdmi-metadata-device) +get_target_property(device_id ${device} MQT_QDMI_DEVICE_ID) +get_target_property(device_prefix ${device} MQT_QDMI_DEVICE_PREFIX) +if(NOT device_id STREQUAL "test.metadata-only" OR NOT device_prefix STREQUAL "TEST_METADATA") + message(FATAL_ERROR "Exported QDMI device metadata was not preserved") +endif() + +file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/main.cpp" "int main() { return 0; }\n") +add_executable(imported-device-consumer "${CMAKE_CURRENT_BINARY_DIR}/main.cpp") +mqt_copy_qdmi_runtime(imported-device-consumer ${device}) diff --git a/test/qdmi/driver/metadata_device.cpp b/test/qdmi/driver/metadata_device.cpp new file mode 100644 index 0000000000..6bc4f9e826 --- /dev/null +++ b/test/qdmi/driver/metadata_device.cpp @@ -0,0 +1,11 @@ +/* + * 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 + */ + +extern "C" void mqtCoreQdmiMetadataDeviceMarker() {} diff --git a/test/qdmi/driver/session_device.cpp b/test/qdmi/driver/session_device.cpp new file mode 100644 index 0000000000..435f52436c --- /dev/null +++ b/test/qdmi/driver/session_device.cpp @@ -0,0 +1,238 @@ +/* + * 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 + +#include +#include +#include +#include +#include +#include + +struct QDMI_Child_Device_impl_d {}; + +struct QDMI_Device_Session_impl_d { + std::unordered_map parameters; + QDMI_Child_Device child = nullptr; + bool initialized = false; +}; + +struct QDMI_Device_Job_impl_d { + QDMI_Device_Session session = nullptr; +}; + +namespace { +std::atomic_size_t activeSessions = 0; + +[[nodiscard]] auto parameter(const QDMI_Device_Session session, + const QDMI_Device_Session_Parameter key) + -> std::string { + if (const auto entry = session->parameters.find(key); + entry != session->parameters.end()) { + return entry->second; + } + return ""; +} + +[[nodiscard]] auto childDeviceHandle() -> QDMI_Child_Device { + static QDMI_Child_Device_impl_d child; + return &child; +} + +auto queryString(const std::string& result, const size_t size, void* value, + size_t* sizeRet) -> int { + const auto required = result.size() + 1; + if (sizeRet != nullptr) { + *sizeRet = required; + } + if (value == nullptr) { + return QDMI_SUCCESS; + } + if (size < required) { + return QDMI_ERROR_INVALIDARGUMENT; + } + std::memcpy(value, result.c_str(), required); + return QDMI_SUCCESS; +} +} // namespace + +extern "C" int TEST_SESSION_QDMI_device_initialize() { return QDMI_SUCCESS; } + +extern "C" int TEST_SESSION_QDMI_device_finalize() { return QDMI_SUCCESS; } + +extern "C" int +TEST_SESSION_QDMI_device_session_alloc(QDMI_Device_Session* session) { + if (session == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + *session = new (std::nothrow) QDMI_Device_Session_impl_d; + if (*session == nullptr) { + return QDMI_ERROR_OUTOFMEM; + } + ++activeSessions; + return QDMI_SUCCESS; +} + +extern "C" int TEST_SESSION_QDMI_device_session_set_parameter( + QDMI_Device_Session session, const QDMI_Device_Session_Parameter param, + const size_t size, const void* value) { + if (session == nullptr || (value != nullptr && size == 0)) { + return QDMI_ERROR_INVALIDARGUMENT; + } + if (session->initialized) { + return QDMI_ERROR_BADSTATE; + } + if (param == QDMI_DEVICE_SESSION_PARAMETER_CHILDDEVICE) { + if (value == nullptr || size != sizeof(QDMI_Child_Device)) { + return QDMI_ERROR_INVALIDARGUMENT; + } + QDMI_Child_Device child = nullptr; + std::memcpy(&child, value, sizeof(child)); + if (child != childDeviceHandle()) { + return QDMI_ERROR_INVALIDARGUMENT; + } + session->child = child; + return QDMI_SUCCESS; + } + if (value != nullptr) { + session->parameters[param] = static_cast(value); + } + return QDMI_SUCCESS; +} + +extern "C" int +TEST_SESSION_QDMI_device_session_init(QDMI_Device_Session session) { + if (session == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + if (session->initialized) { + return QDMI_ERROR_BADSTATE; + } + session->initialized = true; + return QDMI_SUCCESS; +} + +extern "C" void +TEST_SESSION_QDMI_device_session_free(QDMI_Device_Session session) { + if (session != nullptr) { + --activeSessions; + delete session; + } +} + +extern "C" int TEST_SESSION_QDMI_device_session_query_device_property( + QDMI_Device_Session session, const QDMI_Device_Property prop, + const size_t size, void* value, size_t* sizeRet) { + if (session == nullptr || !session->initialized) { + return QDMI_ERROR_BADSTATE; + } + if (prop == QDMI_DEVICE_PROPERTY_CHILDDEVICES) { + if (session->child != nullptr || + parameter(session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM5) != + "with-child") { + return QDMI_ERROR_NOTSUPPORTED; + } + constexpr auto required = sizeof(QDMI_Child_Device); + if (sizeRet != nullptr) { + *sizeRet = required; + } + if (value == nullptr) { + return QDMI_SUCCESS; + } + if (size < required) { + return QDMI_ERROR_INVALIDARGUMENT; + } + const auto child = childDeviceHandle(); + std::memcpy(value, &child, required); + return QDMI_SUCCESS; + } + if (prop != QDMI_DEVICE_PROPERTY_NAME) { + return QDMI_ERROR_NOTSUPPORTED; + } + if (session->child != nullptr) { + return queryString("child;active=" + std::to_string(activeSessions.load()), + size, value, sizeRet); + } + const auto name = + "base=" + parameter(session, QDMI_DEVICE_SESSION_PARAMETER_BASEURL) + + ";token=" + parameter(session, QDMI_DEVICE_SESSION_PARAMETER_TOKEN) + + ";custom1=" + parameter(session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1) + + ";custom2=" + parameter(session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM2) + + ";active=" + std::to_string(activeSessions.load()); + return queryString(name, size, value, sizeRet); +} + +extern "C" int TEST_SESSION_QDMI_device_session_query_site_property( + QDMI_Device_Session, QDMI_Site, QDMI_Site_Property, size_t, void*, + size_t*) { + return QDMI_ERROR_NOTSUPPORTED; +} + +extern "C" int TEST_SESSION_QDMI_device_session_query_operation_property( + QDMI_Device_Session, QDMI_Operation, size_t, const QDMI_Site*, size_t, + const double*, QDMI_Operation_Property, size_t, void*, size_t*) { + return QDMI_ERROR_NOTSUPPORTED; +} + +extern "C" int +TEST_SESSION_QDMI_device_session_create_device_job(QDMI_Device_Session session, + QDMI_Device_Job* job) { + if (session == nullptr || !session->initialized || job == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + *job = new (std::nothrow) QDMI_Device_Job_impl_d{session}; + return *job == nullptr ? QDMI_ERROR_OUTOFMEM : QDMI_SUCCESS; +} + +extern "C" int TEST_SESSION_QDMI_device_job_set_parameter( + QDMI_Device_Job job, QDMI_Device_Job_Parameter, size_t, const void*) { + return job == nullptr ? QDMI_ERROR_INVALIDARGUMENT : QDMI_SUCCESS; +} + +extern "C" int TEST_SESSION_QDMI_device_job_query_property( + QDMI_Device_Job job, const QDMI_Device_Job_Property prop, const size_t size, + void* value, size_t* sizeRet) { + if (job == nullptr || job->session == nullptr || + prop != QDMI_DEVICE_JOB_PROPERTY_ID) { + return QDMI_ERROR_INVALIDARGUMENT; + } + return queryString("session-job", size, value, sizeRet); +} + +extern "C" int TEST_SESSION_QDMI_device_job_submit(QDMI_Device_Job job) { + if (job == nullptr || job->session == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + return QDMI_SUCCESS; +} + +extern "C" int TEST_SESSION_QDMI_device_job_cancel(QDMI_Device_Job) { + return QDMI_ERROR_NOTSUPPORTED; +} + +extern "C" int TEST_SESSION_QDMI_device_job_check(QDMI_Device_Job, + QDMI_Job_Status*) { + return QDMI_ERROR_NOTSUPPORTED; +} + +extern "C" int TEST_SESSION_QDMI_device_job_wait(QDMI_Device_Job, size_t) { + return QDMI_ERROR_NOTSUPPORTED; +} + +extern "C" int TEST_SESSION_QDMI_device_job_get_results(QDMI_Device_Job, + QDMI_Job_Result, size_t, + void*, size_t*) { + return QDMI_ERROR_NOTSUPPORTED; +} + +extern "C" void TEST_SESSION_QDMI_device_job_free(QDMI_Device_Job job) { + delete job; +} diff --git a/test/qdmi/driver/test_driver.cpp b/test/qdmi/driver/test_driver.cpp index 9c3737fdf6..0bcee662cb 100644 --- a/test/qdmi/driver/test_driver.cpp +++ b/test/qdmi/driver/test_driver.cpp @@ -20,8 +20,13 @@ #include #include #include +#include #include +#include +#include +#include #include +#include #include #include #include @@ -54,6 +59,25 @@ namespace qc { namespace { +struct ConfiguredDriverEnvironment { + ConfiguredDriverEnvironment() noexcept { +#ifdef _WIN32 + if (_putenv_s("MQT_CORE_QDMI_CONFIG_FILE", + MQT_CORE_QDMI_TEST_CONFIG_FILE) != 0) { +#else + // POSIX exposes setenv through , but include-cleaner does not + // associate the global declaration with that C++ header. + // NOLINTNEXTLINE(misc-include-cleaner) + if (setenv("MQT_CORE_QDMI_CONFIG_FILE", MQT_CORE_QDMI_TEST_CONFIG_FILE, + 1) != 0) { +#endif + std::abort(); + } + } +}; + +const ConfiguredDriverEnvironment CONFIGURED_DRIVER_ENVIRONMENT; + class ChildDeviceLibrary final : public qdmi::DeviceLibrary { struct Child { size_t id; @@ -229,6 +253,18 @@ class ChildDeviceLibrary final : public qdmi::DeviceLibrary { return name; } +[[nodiscard]] auto openTestDevice(const std::string& library, + const std::string& prefix, + const qdmi::DeviceSessionConfig& session = {}) + -> QDMI_Device { + static size_t nextId = 0; + auto& driver = qdmi::Driver::get(); + const auto id = "test.runtime." + std::to_string(nextId++); + driver.registerDevice( + {.id = id, .library = library, .prefix = prefix, .session = session}); + return driver.open(id); +} + class DriverTest : public testing::TestWithParam { protected: QDMI_Session session = nullptr; @@ -786,6 +822,17 @@ TEST_P(DriverTest, QueryNeedsCalibration) { constexpr std::array DEVICES{"MQT NA Default QDMI Device", "MQT Core DDSIM QDMI Device", "MQT SC Default QDMI Device"}; + +void registerSessionTestDevice() { + static_cast(qdmi::Driver::get().registerDeviceIfAbsent( + {.id = "test.session-overrides", + .library = MQT_CORE_QDMI_SESSION_DEVICE, + .prefix = "TEST_SESSION", + .session = {.baseUrl = "registered-base", + .token = "registered-token", + .custom1 = "registered-custom"}})); +} + // Instantiate the test suite with different parameters INSTANTIATE_TEST_SUITE_P( // Custom instantiation name @@ -804,26 +851,241 @@ INSTANTIATE_TEST_SUITE_P( return name; }); -TEST(DeviceSessionConfigTest, AddDynamicDeviceLibraryWithBaseUrl) { +TEST(ConfiguredDriverTest, ConstructionRegistersWithoutOpeningDevices) { + const auto [library, prefix] = TEST_DEVICE_LIBRARIES.front(); + EXPECT_NO_THROW(qdmi::Driver::get().registerDevice( + {.id = "mqt.na.default", .library = library, .prefix = prefix}, true)); +} + +TEST(ConfiguredDriverTest, ExposesWorkingDefinitionsAndIsolatesFailures) { + QDMI_Session session = nullptr; + ASSERT_EQ(QDMI_session_alloc(&session), QDMI_SUCCESS); + ASSERT_EQ(QDMI_session_init(session), QDMI_SUCCESS); + + size_t size = 0; + ASSERT_EQ(QDMI_session_query_session_property( + session, QDMI_SESSION_PROPERTY_DEVICES, 0, nullptr, &size), + QDMI_SUCCESS); + ASSERT_EQ(size, 3 * sizeof(QDMI_Device)); + std::array devices{}; + ASSERT_EQ(QDMI_session_query_session_property( + session, QDMI_SESSION_PROPERTY_DEVICES, size, + static_cast(devices.data()), nullptr), + QDMI_SUCCESS); + + std::vector names; + std::ranges::transform(devices, std::back_inserter(names), queryName); + EXPECT_THAT(names, + testing::UnorderedElementsAre("MQT NA Default QDMI Device", + "MQT Core DDSIM QDMI Device", + "MQT SC Default QDMI Device")); + QDMI_session_free(session); +} + +TEST(DeviceRegistrationTest, ValidatesDuplicatesAndReplacement) { + auto& driver = qdmi::Driver::get(); + EXPECT_THROW(driver.registerDevice({}), std::invalid_argument); + EXPECT_THROW(driver.open("test.unknown"), std::out_of_range); + + const auto [library, prefix] = TEST_DEVICE_LIBRARIES.front(); + const qdmi::DeviceDefinition original{ + .id = "test.replaceable", .library = library, .prefix = prefix}; + driver.registerDevice(original); + EXPECT_THROW(driver.registerDevice(original), std::invalid_argument); + + auto replacement = original; + replacement.session.custom1 = "replacement"; + EXPECT_NO_THROW(driver.registerDevice(replacement, true)); + auto* const opened = driver.open(original.id); + ASSERT_NE(opened, nullptr); + EXPECT_EQ(driver.open(original.id), opened); + EXPECT_THROW(driver.registerDevice(original, true), std::runtime_error); + EXPECT_NO_THROW(driver.registerDevice( + {.id = "test.upserted", .library = library, .prefix = prefix}, true)); + EXPECT_NE(driver.open("test.upserted"), nullptr); +} + +TEST(DeviceRegistrationTest, RegistersOnlyWhenIdIsAbsent) { + auto& driver = qdmi::Driver::get(); + const auto [library, prefix] = TEST_DEVICE_LIBRARIES.front(); + const qdmi::DeviceDefinition definition{ + .id = "test.insert-if-absent", .library = library, .prefix = prefix}; + EXPECT_TRUE(driver.registerDeviceIfAbsent(definition)); + EXPECT_FALSE(driver.registerDeviceIfAbsent(definition)); + + auto invalidDuplicate = definition; + invalidDuplicate.library.clear(); + EXPECT_THROW(static_cast( + driver.registerDeviceIfAbsent(std::move(invalidDuplicate))), + std::invalid_argument); + + const qdmi::DeviceDefinition disabled{ + .id = "test.disabled", .library = library, .prefix = prefix}; + EXPECT_FALSE(driver.registerDeviceIfAbsent(disabled)); + EXPECT_THROW(static_cast(driver.open(disabled.id)), std::runtime_error); + EXPECT_THROW(driver.registerDevice(disabled), std::invalid_argument); +} + +TEST(DeviceRegistrationTest, RegistrationDoesNotLoadLibraries) { + auto& driver = qdmi::Driver::get(); + driver.registerDevice({.id = "test.missing-library", + .library = "/nonexistent/device-library", + .prefix = "MISSING"}); + EXPECT_THROW(static_cast(driver.open("test.missing-library")), + std::runtime_error); +} + +TEST(DeviceRegistrationTest, SynthesizesManifestForMetadataOnlyTarget) { + std::ifstream manifest(MQT_CORE_QDMI_METADATA_MANIFEST); + ASSERT_TRUE(manifest); + const std::string contents{std::istreambuf_iterator(manifest), + std::istreambuf_iterator()}; + EXPECT_THAT(contents, testing::HasSubstr("\"id\": \"test.metadata-only\"")); + EXPECT_THAT(contents, testing::HasSubstr("\"prefix\": \"TEST_METADATA\"")); + EXPECT_THAT(contents, testing::HasSubstr("mqt-core-qdmi-metadata-device")); +} + +TEST(DeviceRegistrationTest, + FreshOverridesMergeValuesOwnTheirSessionAndStayOutOfCatalog) { + registerSessionTestDevice(); + + const auto clientCatalogSize = [] { + QDMI_Session session = nullptr; + if (QDMI_session_alloc(&session) != QDMI_SUCCESS || + QDMI_session_init(session) != QDMI_SUCCESS) { + throw std::runtime_error("Failed to create QDMI test session"); + } + size_t size = 0; + const auto status = QDMI_session_query_session_property( + session, QDMI_SESSION_PROPERTY_DEVICES, 0, nullptr, &size); + QDMI_session_free(session); + if (status != QDMI_SUCCESS) { + throw std::runtime_error("Failed to query QDMI device catalog"); + } + return size; + }; + + const auto catalogSizeBefore = clientCatalogSize(); + { + qdmi::DeviceSessionConfig overrides; + overrides.token = "override-token"; + overrides.custom2 = "override-custom"; + auto device = + fomac::Session::openDevice("test.session-overrides", overrides); + EXPECT_EQ(device.getName(), + "base=registered-base;token=override-token;custom1=" + "registered-custom;custom2=override-custom;active=1"); + EXPECT_EQ(clientCatalogSize(), catalogSizeBefore); + } + + qdmi::DeviceSessionConfig probeOverrides; + probeOverrides.token = "probe-token"; + const auto probe = + fomac::Session::openDevice("test.session-overrides", probeOverrides); + EXPECT_THAT(queryName(probe), testing::HasSubstr("active=1")); + EXPECT_EQ(clientCatalogSize(), catalogSizeBefore); +} + +TEST(DeviceRegistrationTest, FreshOpenCreatesDistinctSessions) { + registerSessionTestDevice(); + const auto first = fomac::Session::openDevice("test.session-overrides"); + const auto second = fomac::Session::openDevice("test.session-overrides"); + EXPECT_NE(first, second); +} + +TEST(DeviceRegistrationTest, FreshJobRetainsItsDeviceSession) { + registerSessionTestDevice(); + std::optional job; + { + auto device = fomac::Session::openDevice("test.session-overrides"); + job.emplace( + device.submitJob("OPENQASM 2.0;", QDMI_PROGRAM_FORMAT_QASM2, 1)); + } + + ASSERT_TRUE(job.has_value()); + EXPECT_EQ(job->getId(), "session-job"); + job.reset(); + + const auto probe = fomac::Session::openDevice("test.session-overrides"); + EXPECT_THAT(queryName(probe), testing::HasSubstr("active=1")); +} + +TEST(DeviceRegistrationTest, FreshChildDeviceRetainsItsRootSession) { + registerSessionTestDevice(); + std::optional child; + { + qdmi::DeviceSessionConfig overrides; + overrides.custom5 = "with-child"; + auto root = fomac::Session::openDevice("test.session-overrides", overrides); + auto children = root.getChildDevices(); + ASSERT_EQ(children.size(), 1); + child.emplace(std::move(children.front())); + } + + ASSERT_TRUE(child.has_value()); + EXPECT_EQ(child->getName(), "child;active=2"); + child.reset(); + + const auto probe = fomac::Session::openDevice("test.session-overrides"); + EXPECT_THAT(queryName(probe), testing::HasSubstr("active=1")); +} + +TEST(DeviceRegistrationTest, RuntimeRegistrationsStayOutOfClientCatalog) { + auto& driver = qdmi::Driver::get(); + QDMI_Session existingSession = nullptr; + ASSERT_EQ(QDMI_session_alloc(&existingSession), QDMI_SUCCESS); + ASSERT_EQ(QDMI_session_init(existingSession), QDMI_SUCCESS); + size_t originalSize = 0; + ASSERT_EQ(QDMI_session_query_session_property(existingSession, + QDMI_SESSION_PROPERTY_DEVICES, + 0, nullptr, &originalSize), + QDMI_SUCCESS); + + const auto [library, prefix] = TEST_DEVICE_LIBRARIES.front(); + driver.registerDevice( + {.id = "test.snapshot", .library = library, .prefix = prefix}); + ASSERT_NE(driver.open("test.snapshot"), nullptr); + + size_t existingSize = 0; + EXPECT_EQ(QDMI_session_query_session_property(existingSession, + QDMI_SESSION_PROPERTY_DEVICES, + 0, nullptr, &existingSize), + QDMI_SUCCESS); + EXPECT_EQ(existingSize, originalSize); + + QDMI_Session newSession = nullptr; + ASSERT_EQ(QDMI_session_alloc(&newSession), QDMI_SUCCESS); + ASSERT_EQ(QDMI_session_init(newSession), QDMI_SUCCESS); + size_t newSize = 0; + EXPECT_EQ(QDMI_session_query_session_property(newSession, + QDMI_SESSION_PROPERTY_DEVICES, + 0, nullptr, &newSize), + QDMI_SUCCESS); + EXPECT_EQ(newSize, originalSize); + QDMI_session_free(newSession); + QDMI_session_free(existingSession); +} + +TEST(DeviceSessionConfigTest, OpenWithBaseUrl) { qdmi::DeviceSessionConfig config; config.baseUrl = "http://localhost:8080"; - for (const auto& [lib, prefix] : DYN_DEV_LIBS) { + for (const auto& [lib, prefix] : TEST_DEVICE_LIBRARIES) { EXPECT_NO_THROW( - { qdmi::Driver::get().addDynamicDeviceLibrary(lib, prefix, config); }); + { static_cast(openTestDevice(lib, prefix, config)); }); } } -TEST(DeviceSessionConfigTest, AddDynamicDeviceLibraryWithCustomParameters) { +TEST(DeviceSessionConfigTest, OpenWithCustomParameters) { qdmi::DeviceSessionConfig config; config.custom1 = "RESONANCE_COCOS_V1"; config.custom2 = "test_value"; config.baseUrl = "http://localhost:9090"; - for (const auto& [lib, prefix] : DYN_DEV_LIBS) { + for (const auto& [lib, prefix] : TEST_DEVICE_LIBRARIES) { // Custom parameters may fail with validation errors or succeed/return false try { - qdmi::Driver::get().addDynamicDeviceLibrary(lib, prefix, config); + static_cast(openTestDevice(lib, prefix, config)); SUCCEED() << "Library loaded or already loaded"; } catch (const std::runtime_error& e) { // Custom parameters may be rejected with INVALIDARGUMENT @@ -838,42 +1100,42 @@ TEST(DeviceSessionConfigTest, AddDynamicDeviceLibraryWithCustomParameters) { } } -TEST(DeviceSessionConfigTest, AddDynamicDeviceLibraryWithAuthToken) { +TEST(DeviceSessionConfigTest, OpenWithAuthToken) { qdmi::DeviceSessionConfig config; config.token = "test_token_123"; config.baseUrl = "https://api.example.com"; - for (const auto& [lib, prefix] : DYN_DEV_LIBS) { + for (const auto& [lib, prefix] : TEST_DEVICE_LIBRARIES) { EXPECT_NO_THROW( - { qdmi::Driver::get().addDynamicDeviceLibrary(lib, prefix, config); }); + { static_cast(openTestDevice(lib, prefix, config)); }); } } -TEST(DeviceSessionConfigTest, AddDynamicDeviceLibraryWithAuthFile) { +TEST(DeviceSessionConfigTest, OpenWithAuthFile) { qdmi::DeviceSessionConfig config; config.authFile = "/nonexistent/auth.json"; - for (const auto& [lib, prefix] : DYN_DEV_LIBS) { + for (const auto& [lib, prefix] : TEST_DEVICE_LIBRARIES) { // This should not throw even with non-existent file because // if the auth file parameter is not supported, it's skipped EXPECT_NO_THROW( - { qdmi::Driver::get().addDynamicDeviceLibrary(lib, prefix, config); }); + { static_cast(openTestDevice(lib, prefix, config)); }); } } -TEST(DeviceSessionConfigTest, AddDynamicDeviceLibraryWithUsernamePassword) { +TEST(DeviceSessionConfigTest, OpenWithUsernamePassword) { qdmi::DeviceSessionConfig config; config.authUrl = "https://auth.example.com"; config.username = "quantum_user"; config.password = "secret_password"; - for (const auto& [lib, prefix] : DYN_DEV_LIBS) { + for (const auto& [lib, prefix] : TEST_DEVICE_LIBRARIES) { EXPECT_NO_THROW( - { qdmi::Driver::get().addDynamicDeviceLibrary(lib, prefix, config); }); + { static_cast(openTestDevice(lib, prefix, config)); }); } } -TEST(DeviceSessionConfigTest, AddDynamicDeviceLibraryWithAllParameters) { +TEST(DeviceSessionConfigTest, OpenWithAllParameters) { qdmi::DeviceSessionConfig config; config.baseUrl = "http://localhost:8080"; config.token = "test_token"; @@ -886,9 +1148,9 @@ TEST(DeviceSessionConfigTest, AddDynamicDeviceLibraryWithAllParameters) { config.custom4 = "value4"; config.custom5 = "value5"; - for (const auto& [lib, prefix] : DYN_DEV_LIBS) { + for (const auto& [lib, prefix] : TEST_DEVICE_LIBRARIES) { try { - qdmi::Driver::get().addDynamicDeviceLibrary(lib, prefix, config); + static_cast(openTestDevice(lib, prefix, config)); SUCCEED() << "Library loaded or already loaded"; } catch (const std::runtime_error& e) { // Custom parameters may be rejected with INVALIDARGUMENT @@ -906,16 +1168,15 @@ TEST(DeviceSessionConfigTest, AddDynamicDeviceLibraryWithAllParameters) { TEST(DeviceSessionConfigTest, IdempotentLoadingWithDifferentConfigs) { // This test is explicitly not part of the fixture because this would // automatically load the default config and the respective libraries. - if constexpr (DYN_DEV_LIBS.empty()) { + if constexpr (TEST_DEVICE_LIBRARIES.empty()) { GTEST_SKIP() << "No dynamic device libraries to test"; } - auto& driver = qdmi::Driver::get(); - for (const auto& [lib, prefix] : DYN_DEV_LIBS) { + for (const auto& [lib, prefix] : TEST_DEVICE_LIBRARIES) { // Config 1: baseUrl { qdmi::DeviceSessionConfig config; config.baseUrl = "http://localhost:8080"; - EXPECT_NO_THROW(driver.addDynamicDeviceLibrary(lib, prefix, config);); + EXPECT_NO_THROW(static_cast(openTestDevice(lib, prefix, config));); } // Config 2: different baseUrl and custom parameters @@ -923,7 +1184,7 @@ TEST(DeviceSessionConfigTest, IdempotentLoadingWithDifferentConfigs) { qdmi::DeviceSessionConfig config; config.baseUrl = "http://localhost:9090"; config.custom1 = "API_V2"; - EXPECT_NO_THROW(driver.addDynamicDeviceLibrary(lib, prefix, config);); + EXPECT_NO_THROW(static_cast(openTestDevice(lib, prefix, config));); } // Config 3: authentication parameters @@ -931,24 +1192,35 @@ TEST(DeviceSessionConfigTest, IdempotentLoadingWithDifferentConfigs) { qdmi::DeviceSessionConfig config; config.token = "new_token"; config.authUrl = "https://auth.example.com"; - EXPECT_NO_THROW(driver.addDynamicDeviceLibrary(lib, prefix, config);); + EXPECT_NO_THROW(static_cast(openTestDevice(lib, prefix, config));); } } } -TEST(DynamicDeviceLibraryTest, addDynamicDeviceLibraryReturnsDevice) { - // Test that addDynamicDeviceLibrary returns a valid device pointer - if constexpr (DYN_DEV_LIBS.empty()) { +TEST(DynamicDeviceLibraryTest, ReusesLibraryWithFreshDeviceSessions) { + const auto [library, prefix] = TEST_DEVICE_LIBRARIES.front(); + auto* const first = + openTestDevice(library, prefix, {.custom1 = "first-session"}); + const auto equivalentLibrary = std::filesystem::path(library).parent_path() / + "." / + std::filesystem::path(library).filename(); + auto* const second = openTestDevice(equivalentLibrary.string(), prefix, + {.custom1 = "second-session"}); + + ASSERT_NE(first, second); + EXPECT_EQ(&first->getLibrary(), &second->getLibrary()); +} + +TEST(DynamicDeviceLibraryTest, OpenReturnsDevice) { + if constexpr (TEST_DEVICE_LIBRARIES.empty()) { GTEST_SKIP() << "No dynamic device libraries configured for testing."; } - auto& driver = qdmi::Driver::get(); - for (const auto& [lib, prefix] : DYN_DEV_LIBS) { + for (const auto& [lib, prefix] : TEST_DEVICE_LIBRARIES) { const qdmi::DeviceSessionConfig config; QDMI_Device device = nullptr; - ASSERT_NO_THROW( - { device = driver.addDynamicDeviceLibrary(lib, prefix, config); }); + ASSERT_NO_THROW({ device = openTestDevice(lib, prefix, config); }); ASSERT_NE(device, nullptr) - << "addDynamicDeviceLibrary should return a non-null device pointer"; + << "open should return a non-null device pointer"; // Verify the device is valid by querying its name size_t size = 0; diff --git a/test/qdmi/registry/CMakeLists.txt b/test/qdmi/registry/CMakeLists.txt new file mode 100644 index 0000000000..25e4f84e34 --- /dev/null +++ b/test/qdmi/registry/CMakeLists.txt @@ -0,0 +1,15 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +set(TARGET_NAME mqt-core-qdmi-registry-test) + +if(TARGET MQT::CoreQDMIDriver) + package_add_test(${TARGET_NAME} MQT::CoreQDMIDriver test_device_registry.cpp) + target_include_directories(${TARGET_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/src/qdmi/driver) + mqt_copy_qdmi_runtime(${TARGET_NAME}) +endif() diff --git a/test/qdmi/registry/test_device_registry.cpp b/test/qdmi/registry/test_device_registry.cpp new file mode 100644 index 0000000000..627bce2836 --- /dev/null +++ b/test/qdmi/registry/test_device_registry.cpp @@ -0,0 +1,414 @@ +/* + * 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 "DeviceRegistry.hpp" +#include "qdmi/driver/Driver.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +class TemporaryDirectory { +public: + TemporaryDirectory() { + path_ = std::filesystem::temp_directory_path() / + ("mqt-core-qdmi-registry-test-" + + std::to_string(std::random_device{}())); + std::filesystem::remove_all(path_); + std::filesystem::create_directories(path_); + } + + ~TemporaryDirectory() { std::filesystem::remove_all(path_); } + + [[nodiscard]] const std::filesystem::path& path() const { return path_; } + + [[nodiscard]] std::filesystem::path + write(const std::filesystem::path& relative, + const std::string& contents) const { + const auto path = path_ / relative; + std::filesystem::create_directories(path.parent_path()); + std::ofstream output(path); + output << contents; + return path; + } + +private: + std::filesystem::path path_; +}; + +class ScopedEnvironmentVariable { +public: + ScopedEnvironmentVariable(std::string name, const std::string& value) + : name_(std::move(name)) { + if (const auto* previous = std::getenv(name_.c_str()); + previous != nullptr) { + previous_ = previous; + } + set(value); + } + + ~ScopedEnvironmentVariable() { + if (previous_) { + static_cast(setWithoutChecking(*previous_)); + } else { +#ifdef _WIN32 + static_cast(_putenv_s(name_.c_str(), "")); +#else + // NOLINTNEXTLINE(misc-include-cleaner) + static_cast(unsetenv(name_.c_str())); +#endif + } + } + + ScopedEnvironmentVariable(const ScopedEnvironmentVariable&) = delete; + ScopedEnvironmentVariable& + operator=(const ScopedEnvironmentVariable&) = delete; + ScopedEnvironmentVariable(ScopedEnvironmentVariable&&) = delete; + ScopedEnvironmentVariable& operator=(ScopedEnvironmentVariable&&) = delete; + +private: + void set(const std::string& value) const { + if (!setWithoutChecking(value)) { + throw std::runtime_error("Failed to set environment variable " + name_); + } + } + + [[nodiscard]] bool setWithoutChecking(const std::string& value) const { +#ifdef _WIN32 + return _putenv_s(name_.c_str(), value.c_str()) == 0; +#else + // NOLINTNEXTLINE(misc-include-cleaner) + return setenv(name_.c_str(), value.c_str(), 1) == 0; +#endif + } + + std::string name_; + std::optional previous_; +}; + +class ScopedCurrentPath { +public: + explicit ScopedCurrentPath(const std::filesystem::path& path) + : previous_(std::filesystem::current_path()) { + std::filesystem::current_path(path); + } + ~ScopedCurrentPath() { std::filesystem::current_path(previous_); } + + ScopedCurrentPath(const ScopedCurrentPath&) = delete; + ScopedCurrentPath& operator=(const ScopedCurrentPath&) = delete; + ScopedCurrentPath(ScopedCurrentPath&&) = delete; + ScopedCurrentPath& operator=(ScopedCurrentPath&&) = delete; + +private: + std::filesystem::path previous_; +}; + +[[nodiscard]] auto findDefinition(const qdmi::detail::DeviceRegistry& registry, + const std::string_view id) + -> const qdmi::DeviceDefinition* { + const auto& definitions = registry.definitions(); + const auto found = + std::ranges::find(definitions, id, &qdmi::DeviceDefinition::id); + return found == definitions.end() ? nullptr : &*found; +} + +[[nodiscard]] auto emptyConfig(const TemporaryDirectory& directory) + -> ScopedEnvironmentVariable { + const auto path = + directory.write("empty.json", R"({"schema-version": 1, "qdmi": {}})"); + return {"MQT_CORE_QDMI_CONFIG_FILE", path.string()}; +} + +TEST(DeviceRegistry, ParsesEnvironmentConfigurationWithoutLoadingLibraries) { + const TemporaryDirectory directory; + const ScopedCurrentPath currentPath(directory.path()); + const auto configFile = emptyConfig(directory); + const ScopedEnvironmentVariable configJson("MQT_CORE_QDMI_CONFIG_JSON", R"({ + "schema-version": 1, + "qdmi": {"devices": [{ + "id": "example.device", "library": "libexample.so", "prefix": "EXAMPLE", + "session": {"auth-file": "secret.json", "custom1": "value"} + }]} + })"); + + const qdmi::detail::DeviceRegistry registry; + const auto* definition = findDefinition(registry, "example.device"); + ASSERT_NE(definition, nullptr); + EXPECT_EQ(std::filesystem::weakly_canonical(definition->library), + std::filesystem::weakly_canonical(directory.path()) / + "libexample.so"); + ASSERT_TRUE(definition->session.authFile.has_value()); + EXPECT_EQ(std::filesystem::weakly_canonical(*definition->session.authFile), + std::filesystem::weakly_canonical(directory.path()) / + "secret.json"); + EXPECT_EQ(definition->session.custom1, "value"); +} + +TEST(DeviceRegistry, RejectsDuplicateIdsAndUnsupportedKeys) { + const TemporaryDirectory directory; + const auto configFile = emptyConfig(directory); + { + const ScopedEnvironmentVariable configJson("MQT_CORE_QDMI_CONFIG_JSON", R"({ + "schema-version": 1, + "qdmi": {"devices": [ + {"id": "duplicate", "library": "one", "prefix": "ONE"}, + {"id": "duplicate", "library": "two", "prefix": "TWO"} + ]} + })"); + EXPECT_THROW(static_cast(qdmi::detail::DeviceRegistry()), + std::invalid_argument); + } + { + const ScopedEnvironmentVariable configJson("MQT_CORE_QDMI_CONFIG_JSON", R"({ + "schema-version": 1, + "qdmi": {"device-config": {"model": "unused"}} + })"); + EXPECT_THROW(static_cast(qdmi::detail::DeviceRegistry()), + std::invalid_argument); + } +} + +TEST(DeviceRegistry, MergesEnvironmentJsonOverExplicitFile) { + const TemporaryDirectory directory; + const auto path = directory.write("environment.json", R"({ + "schema-version": 1, + "qdmi": {"devices": [{ + "id": "environment", "library": "file.so", "prefix": "FILE", + "session": {"custom1": "from-file", "custom2": "preserved"} + }]} + })"); + const ScopedEnvironmentVariable configFile("MQT_CORE_QDMI_CONFIG_FILE", + path.string()); + const ScopedEnvironmentVariable configJson("MQT_CORE_QDMI_CONFIG_JSON", R"({ + "schema-version": 1, + "qdmi": {"devices": [{ + "id": "environment", "session": {"custom1": "from-json"} + }]} + })"); + + const qdmi::detail::DeviceRegistry registry; + const auto* definition = findDefinition(registry, "environment"); + ASSERT_NE(definition, nullptr); + EXPECT_EQ(definition->library, directory.path() / "file.so"); + EXPECT_EQ(definition->prefix, "FILE"); + EXPECT_EQ(definition->session.custom1, "from-json"); + EXPECT_EQ(definition->session.custom2, "preserved"); +} + +TEST(DeviceRegistry, DisabledEnvironmentEntryMasksExplicitDefinition) { + const TemporaryDirectory directory; + const auto path = directory.write("complete.json", R"({ + "schema-version": 1, + "qdmi": {"devices": [ + {"id": "masked", "library": "device.so", "prefix": "DEVICE"} + ]} + })"); + const ScopedEnvironmentVariable configFile("MQT_CORE_QDMI_CONFIG_FILE", + path.string()); + const ScopedEnvironmentVariable configJson("MQT_CORE_QDMI_CONFIG_JSON", R"({ + "schema-version": 1, + "qdmi": {"devices": [{"id": "masked", "enabled": false}]} + })"); + + const qdmi::detail::DeviceRegistry registry; + EXPECT_EQ(findDefinition(registry, "masked"), nullptr); + ASSERT_EQ(registry.disabledIds().size(), 1); + EXPECT_EQ(registry.disabledIds().front(), "masked"); +} + +TEST(DeviceRegistry, ResolvesRelativeConfigurationPathsBeforeCwdChanges) { + const TemporaryDirectory directory; + directory.write("config/device.json", R"({ + "schema-version": 1, + "qdmi": {"devices": [{ + "id": "relative", "library": "libdevice.so", "prefix": "RELATIVE", + "session": {"auth-file": "auth.json"} + }]} + })"); + + std::filesystem::path library; + std::filesystem::path authFile; + { + const ScopedCurrentPath currentPath(directory.path()); + const ScopedEnvironmentVariable configFile("MQT_CORE_QDMI_CONFIG_FILE", + "config/device.json"); + const ScopedEnvironmentVariable configJson("MQT_CORE_QDMI_CONFIG_JSON", ""); + const qdmi::detail::DeviceRegistry registry; + const auto* definition = findDefinition(registry, "relative"); + ASSERT_NE(definition, nullptr); + library = definition->library; + ASSERT_TRUE(definition->session.authFile.has_value()); + authFile = *definition->session.authFile; + } + + EXPECT_TRUE(library.is_absolute()); + EXPECT_TRUE(authFile.is_absolute()); + EXPECT_EQ(std::filesystem::weakly_canonical(library), + std::filesystem::weakly_canonical(directory.path()) / "config" / + "libdevice.so"); + EXPECT_EQ(std::filesystem::weakly_canonical(authFile), + std::filesystem::weakly_canonical(directory.path()) / "config" / + "auth.json"); +} + +TEST(DeviceRegistry, DiscoversGeneratedBuildTreeManifests) { + const TemporaryDirectory directory; + const auto configFile = emptyConfig(directory); + const ScopedEnvironmentVariable configJson("MQT_CORE_QDMI_CONFIG_JSON", ""); + + const qdmi::detail::DeviceRegistry registry; + ASSERT_EQ(registry.definitions().size(), 3); + EXPECT_EQ(registry.definitions().at(0).id, "mqt.ddsim.default"); + EXPECT_EQ(registry.definitions().at(1).id, "mqt.na.default"); + EXPECT_EQ(registry.definitions().at(2).id, "mqt.sc.default"); + for (const auto& definition : registry.definitions()) { + EXPECT_TRUE(std::filesystem::is_regular_file(definition.library)); + } +} + +TEST(DeviceRegistry, ReadsProjectConfigurationFromPyprojectToml) { + const TemporaryDirectory directory; + directory.write("pyproject.toml", R"( + [tool.qdmi] + devices = [{ id = "toml", library = "device.so", prefix = "TOML" }] + )"); + const ScopedCurrentPath currentPath(directory.path()); + const ScopedEnvironmentVariable configFile("MQT_CORE_QDMI_CONFIG_FILE", ""); + const ScopedEnvironmentVariable configJson("MQT_CORE_QDMI_CONFIG_JSON", ""); +#ifdef _WIN32 + const ScopedEnvironmentVariable userConfig("APPDATA", + directory.path().string()); +#else + const ScopedEnvironmentVariable userConfig("XDG_CONFIG_HOME", + directory.path().string()); +#endif + + const qdmi::detail::DeviceRegistry registry; + const auto* definition = findDefinition(registry, "toml"); + ASSERT_NE(definition, nullptr); + EXPECT_EQ(std::filesystem::weakly_canonical(definition->library), + std::filesystem::weakly_canonical(directory.path()) / "device.so"); +} + +TEST(DeviceRegistry, DedicatedProjectFileWinsOverPyproject) { + const TemporaryDirectory directory; + directory.write("pyproject.toml", R"( + [tool.qdmi] + devices = [{ id = "toml", library = "toml.so", prefix = "TOML" }] + )"); + directory.write("qdmi.json", R"({ + "schema-version": 1, + "qdmi": {"devices": [ + {"id": "json", "library": "json.so", "prefix": "JSON"} + ]} + })"); + const ScopedCurrentPath currentPath(directory.path()); + const ScopedEnvironmentVariable configFile("MQT_CORE_QDMI_CONFIG_FILE", ""); + const ScopedEnvironmentVariable configJson("MQT_CORE_QDMI_CONFIG_JSON", ""); + + const qdmi::detail::DeviceRegistry registry; + EXPECT_NE(findDefinition(registry, "json"), nullptr); + EXPECT_EQ(findDefinition(registry, "toml"), nullptr); +} + +TEST(DeviceRegistry, MergesProjectConfigurationOverUserConfiguration) { + const TemporaryDirectory directory; + directory.write("user/mqt-core/qdmi.json", R"({ + "schema-version": 1, + "qdmi": {"devices": [{ + "id": "layered", "library": "user.so", "prefix": "USER", + "session": {"custom1": "user-default"} + }]} + })"); + directory.write("project/qdmi.json", R"({ + "schema-version": 1, + "qdmi": {"devices": [{"id": "layered", "prefix": "PROJECT"}]} + })"); + const ScopedCurrentPath currentPath(directory.path() / "project"); + const ScopedEnvironmentVariable configFile("MQT_CORE_QDMI_CONFIG_FILE", ""); + const ScopedEnvironmentVariable configJson("MQT_CORE_QDMI_CONFIG_JSON", ""); +#ifdef _WIN32 + const ScopedEnvironmentVariable programData("PROGRAMDATA", + directory.path().string()); + const ScopedEnvironmentVariable userConfig( + "APPDATA", (directory.path() / "user").string()); +#else + const ScopedEnvironmentVariable userConfig( + "XDG_CONFIG_HOME", (directory.path() / "user").string()); +#endif + + const qdmi::detail::DeviceRegistry registry; + const auto* definition = findDefinition(registry, "layered"); + ASSERT_NE(definition, nullptr); + EXPECT_EQ(definition->library, + directory.path() / "user" / "mqt-core" / "user.so"); + EXPECT_EQ(definition->prefix, "PROJECT"); + EXPECT_EQ(definition->session.custom1, "user-default"); +} + +TEST(DeviceRegistry, ReportsInvalidDocumentsAndDefinitionTypes) { + const TemporaryDirectory directory; + const auto configFile = emptyConfig(directory); + for ( + const auto* document : { + R"({})", + R"({"schema-version": 2, "qdmi": {}})", + R"({"schema-version": 1, "qdmi": {"devices": {}}})", + R"({"schema-version": 1, "qdmi": {"devices": [{"id": 4}]}})", + R"({"schema-version": 1, "qdmi": {"devices": [{"id": "invalid", "library": "device", "prefix": "P", "enabled": "yes"}]}})", + R"({"schema-version": 1, "qdmi": {"devices": [{"id": "invalid", "library": "device", "prefix": "P", "session": {"token": 42}}]}})", + R"({"schema-version": 1, "qdmi": {"devices": [{"id": "missing", "prefix": "P"}]}})", + R"({"schema-version": 1, "qdmi": {"devices": [{"id": "unknown", "library": "device", "prefix": "P", "unexpected": true}]}})", + }) { + const ScopedEnvironmentVariable configJson("MQT_CORE_QDMI_CONFIG_JSON", + document); + EXPECT_THROW(static_cast(qdmi::detail::DeviceRegistry()), + std::invalid_argument); + } +} + +TEST(DeviceRegistry, ReportsInvalidExplicitJsonAndToml) { + const TemporaryDirectory directory; + { + const ScopedEnvironmentVariable configFile( + "MQT_CORE_QDMI_CONFIG_FILE", + (directory.path() / "missing.json").string()); + EXPECT_THROW(static_cast(qdmi::detail::DeviceRegistry()), + std::runtime_error); + } + { + const auto invalid = directory.write("invalid.json", "{"); + const ScopedEnvironmentVariable configFile("MQT_CORE_QDMI_CONFIG_FILE", + invalid.string()); + EXPECT_THROW(static_cast(qdmi::detail::DeviceRegistry()), + std::invalid_argument); + } + { + directory.write("pyproject.toml", "[tool.qdmi\n"); + const ScopedCurrentPath currentPath(directory.path()); + const ScopedEnvironmentVariable configFile("MQT_CORE_QDMI_CONFIG_FILE", ""); + EXPECT_THROW(static_cast(qdmi::detail::DeviceRegistry()), + std::invalid_argument); + } +} + +} // namespace diff --git a/vendor/tomlplusplus/README.md b/vendor/tomlplusplus/README.md new file mode 100644 index 0000000000..6e5384047b --- /dev/null +++ b/vendor/tomlplusplus/README.md @@ -0,0 +1,10 @@ +# toml++ single-header distribution + +`toml.hpp` is the upstream single-header distribution of +[toml++](https://github.com/marzer/tomlplusplus), pinned to commit +`a43ad3787293f4a46b1d70c0924b5a25d10e79fc` from 24 May 2026. + +The unmodified file has SHA-256 digest +`e8a56bd3ae26d71c7414615f4d85bce5072077b2e1ca455289211170a541e026`. Its preamble +contains the MIT license, as prescribed for upstream's single-header +distribution. diff --git a/vendor/tomlplusplus/toml.hpp b/vendor/tomlplusplus/toml.hpp new file mode 100644 index 0000000000..caf87c4c21 --- /dev/null +++ b/vendor/tomlplusplus/toml.hpp @@ -0,0 +1,17899 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// toml++ v3.4.0 +// https://github.com/marzer/tomlplusplus +// SPDX-License-Identifier: MIT +// +//---------------------------------------------------------------------------------------------------------------------- +// +// - THIS FILE WAS ASSEMBLED FROM MULTIPLE HEADER FILES BY A SCRIPT - PLEASE DON'T EDIT IT DIRECTLY - +// +// If you wish to submit a contribution to toml++, hooray and thanks! Before you crack on, please be aware that this +// file was assembled from a number of smaller files by a python script, and code contributions should not be made +// against it directly. You should instead make your changes in the relevant source file(s). The file names of the files +// that contributed to this header can be found at the beginnings and ends of the corresponding sections of this file. +// +//---------------------------------------------------------------------------------------------------------------------- +// +// TOML Language Specifications: +// latest: https://github.com/toml-lang/toml/blob/master/README.md +// v1.0.0: https://toml.io/en/v1.0.0 +// v0.5.0: https://toml.io/en/v0.5.0 +// changelog: https://github.com/toml-lang/toml/blob/master/CHANGELOG.md +// +//---------------------------------------------------------------------------------------------------------------------- +// +// MIT License +// +// Copyright (c) Mark Gillard +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +// Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +//---------------------------------------------------------------------------------------------------------------------- +#ifndef TOMLPLUSPLUS_HPP +#define TOMLPLUSPLUS_HPP + +#define INCLUDE_TOMLPLUSPLUS_H // old guard name used pre-v3 +#define TOMLPLUSPLUS_H // guard name used in the legacy toml.h + +//******** impl/preprocessor.hpp ************************************************************************************* + +#ifndef __cplusplus +#error toml++ is a C++ library. +#endif + +#ifndef TOML_CPP +#ifdef _MSVC_LANG +#if _MSVC_LANG > __cplusplus +#define TOML_CPP _MSVC_LANG +#endif +#endif +#ifndef TOML_CPP +#define TOML_CPP __cplusplus +#endif +#if TOML_CPP >= 202900L +#undef TOML_CPP +#define TOML_CPP 29 +#elif TOML_CPP >= 202600L +#undef TOML_CPP +#define TOML_CPP 26 +#elif TOML_CPP >= 202302L +#undef TOML_CPP +#define TOML_CPP 23 +#elif TOML_CPP >= 202002L +#undef TOML_CPP +#define TOML_CPP 20 +#elif TOML_CPP >= 201703L +#undef TOML_CPP +#define TOML_CPP 17 +#elif TOML_CPP >= 201402L +#undef TOML_CPP +#define TOML_CPP 14 +#elif TOML_CPP >= 201103L +#undef TOML_CPP +#define TOML_CPP 11 +#else +#undef TOML_CPP +#define TOML_CPP 0 +#endif +#endif + +#if !TOML_CPP +#error toml++ requires C++17 or higher. For a pre-C++11 TOML library see https://github.com/ToruNiina/Boost.toml +#elif TOML_CPP < 17 +#error toml++ requires C++17 or higher. For a C++11 TOML library see https://github.com/ToruNiina/toml11 +#endif + +#ifndef TOML_MAKE_VERSION +#define TOML_MAKE_VERSION(major, minor, patch) (((major)*10000) + ((minor)*100) + ((patch))) +#endif + +#ifndef TOML_INTELLISENSE +#ifdef __INTELLISENSE__ +#define TOML_INTELLISENSE 1 +#else +#define TOML_INTELLISENSE 0 +#endif +#endif + +#ifndef TOML_DOXYGEN +#if defined(DOXYGEN) || defined(__DOXYGEN) || defined(__DOXYGEN__) || defined(__doxygen__) || defined(__POXY__) \ + || defined(__poxy__) +#define TOML_DOXYGEN 1 +#else +#define TOML_DOXYGEN 0 +#endif +#endif + +#ifndef TOML_CLANG +#ifdef __clang__ +#define TOML_CLANG __clang_major__ +#else +#define TOML_CLANG 0 +#endif + +// special handling for apple clang; see: +// - https://github.com/marzer/tomlplusplus/issues/189 +// - https://en.wikipedia.org/wiki/Xcode +// - +// https://stackoverflow.com/questions/19387043/how-can-i-reliably-detect-the-version-of-clang-at-preprocessing-time +#if TOML_CLANG && defined(__apple_build_version__) +#undef TOML_CLANG +#define TOML_CLANG_VERSION TOML_MAKE_VERSION(__clang_major__, __clang_minor__, __clang_patchlevel__) +#if TOML_CLANG_VERSION >= TOML_MAKE_VERSION(15, 0, 0) +#define TOML_CLANG 16 +#elif TOML_CLANG_VERSION >= TOML_MAKE_VERSION(14, 3, 0) +#define TOML_CLANG 15 +#elif TOML_CLANG_VERSION >= TOML_MAKE_VERSION(14, 0, 0) +#define TOML_CLANG 14 +#elif TOML_CLANG_VERSION >= TOML_MAKE_VERSION(13, 1, 6) +#define TOML_CLANG 13 +#elif TOML_CLANG_VERSION >= TOML_MAKE_VERSION(13, 0, 0) +#define TOML_CLANG 12 +#elif TOML_CLANG_VERSION >= TOML_MAKE_VERSION(12, 0, 5) +#define TOML_CLANG 11 +#elif TOML_CLANG_VERSION >= TOML_MAKE_VERSION(12, 0, 0) +#define TOML_CLANG 10 +#elif TOML_CLANG_VERSION >= TOML_MAKE_VERSION(11, 0, 3) +#define TOML_CLANG 9 +#elif TOML_CLANG_VERSION >= TOML_MAKE_VERSION(11, 0, 0) +#define TOML_CLANG 8 +#elif TOML_CLANG_VERSION >= TOML_MAKE_VERSION(10, 0, 1) +#define TOML_CLANG 7 +#else +#define TOML_CLANG 6 // not strictly correct but doesn't matter below this +#endif +#undef TOML_CLANG_VERSION +#endif +#endif + +#ifndef TOML_ICC +#ifdef __INTEL_COMPILER +#define TOML_ICC __INTEL_COMPILER +#ifdef __ICL +#define TOML_ICC_CL TOML_ICC +#else +#define TOML_ICC_CL 0 +#endif +#else +#define TOML_ICC 0 +#define TOML_ICC_CL 0 +#endif +#endif + +#ifndef TOML_MSVC_LIKE +#ifdef _MSC_VER +#define TOML_MSVC_LIKE _MSC_VER +#else +#define TOML_MSVC_LIKE 0 +#endif +#endif + +#ifndef TOML_MSVC +#if TOML_MSVC_LIKE && !TOML_CLANG && !TOML_ICC +#define TOML_MSVC TOML_MSVC_LIKE +#else +#define TOML_MSVC 0 +#endif +#endif + +#ifndef TOML_GCC_LIKE +#ifdef __GNUC__ +#define TOML_GCC_LIKE __GNUC__ +#else +#define TOML_GCC_LIKE 0 +#endif +#endif + +#ifndef TOML_GCC +#if TOML_GCC_LIKE && !TOML_CLANG && !TOML_ICC +#define TOML_GCC TOML_GCC_LIKE +#else +#define TOML_GCC 0 +#endif +#endif + +#ifndef TOML_CUDA +#if defined(__CUDACC__) || defined(__CUDA_ARCH__) || defined(__CUDA_LIBDEVICE__) +#define TOML_CUDA 1 +#else +#define TOML_CUDA 0 +#endif +#endif + +#ifndef TOML_NVCC +#ifdef __NVCOMPILER_MAJOR__ +#define TOML_NVCC __NVCOMPILER_MAJOR__ +#else +#define TOML_NVCC 0 +#endif +#endif + +#ifndef TOML_ARCH_ITANIUM +#if defined(__ia64__) || defined(__ia64) || defined(_IA64) || defined(__IA64__) || defined(_M_IA64) +#define TOML_ARCH_ITANIUM 1 +#define TOML_ARCH_BITNESS 64 +#else +#define TOML_ARCH_ITANIUM 0 +#endif +#endif + +#ifndef TOML_ARCH_AMD64 +#if defined(__amd64__) || defined(__amd64) || defined(__x86_64__) || defined(__x86_64) || defined(_M_AMD64) +#define TOML_ARCH_AMD64 1 +#define TOML_ARCH_BITNESS 64 +#else +#define TOML_ARCH_AMD64 0 +#endif +#endif + +#ifndef TOML_ARCH_X86 +#if defined(__i386__) || defined(_M_IX86) +#define TOML_ARCH_X86 1 +#define TOML_ARCH_BITNESS 32 +#else +#define TOML_ARCH_X86 0 +#endif +#endif + +#ifndef TOML_ARCH_ARM +#if defined(__aarch64__) || defined(__ARM_ARCH_ISA_A64) || defined(_M_ARM64) || defined(__ARM_64BIT_STATE) \ + || defined(_M_ARM64EC) +#define TOML_ARCH_ARM32 0 +#define TOML_ARCH_ARM64 1 +#define TOML_ARCH_ARM 1 +#define TOML_ARCH_BITNESS 64 +#elif defined(__arm__) || defined(_M_ARM) || defined(__ARM_32BIT_STATE) +#define TOML_ARCH_ARM32 1 +#define TOML_ARCH_ARM64 0 +#define TOML_ARCH_ARM 1 +#define TOML_ARCH_BITNESS 32 +#else +#define TOML_ARCH_ARM32 0 +#define TOML_ARCH_ARM64 0 +#define TOML_ARCH_ARM 0 +#endif +#endif + +#ifndef TOML_ARCH_BITNESS +#define TOML_ARCH_BITNESS 0 +#endif + +#ifndef TOML_ARCH_X64 +#if TOML_ARCH_BITNESS == 64 +#define TOML_ARCH_X64 1 +#else +#define TOML_ARCH_X64 0 +#endif +#endif + +#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) || defined(__CYGWIN__) +#define TOML_WINDOWS 1 +#else +#define TOML_WINDOWS 0 +#endif + +#ifdef __unix__ +#define TOML_UNIX 1 +#else +#define TOML_UNIX 0 +#endif + +#ifdef __linux__ +#define TOML_LINUX 1 +#else +#define TOML_LINUX 0 +#endif + +// TOML_HAS_INCLUDE +#ifndef TOML_HAS_INCLUDE +#ifdef __has_include +#define TOML_HAS_INCLUDE(header) __has_include(header) +#else +#define TOML_HAS_INCLUDE(header) 0 +#endif +#endif + +// TOML_HAS_BUILTIN +#ifndef TOML_HAS_BUILTIN +#ifdef __has_builtin +#define TOML_HAS_BUILTIN(name) __has_builtin(name) +#else +#define TOML_HAS_BUILTIN(name) 0 +#endif +#endif + +// TOML_HAS_FEATURE +#ifndef TOML_HAS_FEATURE +#ifdef __has_feature +#define TOML_HAS_FEATURE(name) __has_feature(name) +#else +#define TOML_HAS_FEATURE(name) 0 +#endif +#endif + +// TOML_HAS_ATTR +#ifndef TOML_HAS_ATTR +#ifdef __has_attribute +#define TOML_HAS_ATTR(attr) __has_attribute(attr) +#else +#define TOML_HAS_ATTR(attr) 0 +#endif +#endif + +// TOML_HAS_CPP_ATTR +#ifndef TOML_HAS_CPP_ATTR +#ifdef __has_cpp_attribute +#define TOML_HAS_CPP_ATTR(attr) __has_cpp_attribute(attr) +#else +#define TOML_HAS_CPP_ATTR(attr) 0 +#endif +#endif + +// TOML_ATTR (gnu attributes) +#ifndef TOML_ATTR +#if TOML_CLANG || TOML_GCC_LIKE +#define TOML_ATTR(...) __attribute__((__VA_ARGS__)) +#else +#define TOML_ATTR(...) +#endif +#endif + +// TOML_DECLSPEC (msvc attributes) +#ifndef TOML_DECLSPEC +#if TOML_MSVC_LIKE +#define TOML_DECLSPEC(...) __declspec(__VA_ARGS__) +#else +#define TOML_DECLSPEC(...) +#endif +#endif + +// TOML_COMPILER_HAS_EXCEPTIONS +#ifndef TOML_COMPILER_HAS_EXCEPTIONS +#if defined(__EXCEPTIONS) || defined(_CPPUNWIND) || defined(__cpp_exceptions) +#define TOML_COMPILER_HAS_EXCEPTIONS 1 +#else +#define TOML_COMPILER_HAS_EXCEPTIONS 0 +#endif +#endif + +// TOML_COMPILER_HAS_RTTI +#ifndef TOML_COMPILER_HAS_RTTI +#if defined(_CPPRTTI) || defined(__GXX_RTTI) || TOML_HAS_FEATURE(cxx_rtti) +#define TOML_COMPILER_HAS_RTTI 1 +#else +#define TOML_COMPILER_HAS_RTTI 0 +#endif +#endif + +// TOML_CONCAT +#define TOML_CONCAT_1(x, y) x##y +#define TOML_CONCAT(x, y) TOML_CONCAT_1(x, y) + +// TOML_MAKE_STRING +#define TOML_MAKE_STRING_1(s) #s +#define TOML_MAKE_STRING(s) TOML_MAKE_STRING_1(s) + +// TOML_PRAGMA_XXXX (compiler-specific pragmas) +#if TOML_CLANG +#define TOML_PRAGMA_CLANG(decl) _Pragma(TOML_MAKE_STRING(clang decl)) +#else +#define TOML_PRAGMA_CLANG(decl) +#endif +#if TOML_CLANG >= 8 +#define TOML_PRAGMA_CLANG_GE_8(decl) TOML_PRAGMA_CLANG(decl) +#else +#define TOML_PRAGMA_CLANG_GE_8(decl) +#endif +#if TOML_CLANG >= 9 +#define TOML_PRAGMA_CLANG_GE_9(decl) TOML_PRAGMA_CLANG(decl) +#else +#define TOML_PRAGMA_CLANG_GE_9(decl) +#endif +#if TOML_CLANG >= 10 +#define TOML_PRAGMA_CLANG_GE_10(decl) TOML_PRAGMA_CLANG(decl) +#else +#define TOML_PRAGMA_CLANG_GE_10(decl) +#endif +#if TOML_CLANG >= 11 +#define TOML_PRAGMA_CLANG_GE_11(decl) TOML_PRAGMA_CLANG(decl) +#else +#define TOML_PRAGMA_CLANG_GE_11(decl) +#endif +#if TOML_GCC +#define TOML_PRAGMA_GCC(decl) _Pragma(TOML_MAKE_STRING(GCC decl)) +#else +#define TOML_PRAGMA_GCC(decl) +#endif +#if TOML_MSVC +#define TOML_PRAGMA_MSVC(...) __pragma(__VA_ARGS__) +#else +#define TOML_PRAGMA_MSVC(...) +#endif +#if TOML_ICC +#define TOML_PRAGMA_ICC(...) __pragma(__VA_ARGS__) +#else +#define TOML_PRAGMA_ICC(...) +#endif + +// TOML_ALWAYS_INLINE +#ifndef TOML_ALWAYS_INLINE +#ifdef _MSC_VER +#define TOML_ALWAYS_INLINE __forceinline +#elif TOML_GCC || TOML_CLANG || TOML_HAS_ATTR(__always_inline__) +#define TOML_ALWAYS_INLINE \ + TOML_ATTR(__always_inline__) \ + inline +#else +#define TOML_ALWAYS_INLINE inline +#endif +#endif + +// TOML_NEVER_INLINE +#ifndef TOML_NEVER_INLINE +#ifdef _MSC_VER +#define TOML_NEVER_INLINE TOML_DECLSPEC(noinline) +#elif TOML_CUDA // https://gitlab.gnome.org/GNOME/glib/-/issues/2555 +#define TOML_NEVER_INLINE TOML_ATTR(noinline) +#else +#if TOML_GCC || TOML_CLANG || TOML_HAS_ATTR(__noinline__) +#define TOML_NEVER_INLINE TOML_ATTR(__noinline__) +#endif +#endif +#ifndef TOML_NEVER_INLINE +#define TOML_NEVER_INLINE +#endif +#endif + +// MSVC attributes +#ifndef TOML_ABSTRACT_INTERFACE +#define TOML_ABSTRACT_INTERFACE TOML_DECLSPEC(novtable) +#endif +#ifndef TOML_EMPTY_BASES +#define TOML_EMPTY_BASES TOML_DECLSPEC(empty_bases) +#endif + +// TOML_TRIVIAL_ABI +#ifndef TOML_TRIVIAL_ABI +#if TOML_CLANG || TOML_HAS_ATTR(__trivial_abi__) +#define TOML_TRIVIAL_ABI TOML_ATTR(__trivial_abi__) +#else +#define TOML_TRIVIAL_ABI +#endif +#endif + +// TOML_NODISCARD +#ifndef TOML_NODISCARD +#if TOML_CPP >= 17 && TOML_HAS_CPP_ATTR(nodiscard) >= 201603 +#define TOML_NODISCARD [[nodiscard]] +#elif TOML_CLANG || TOML_GCC || TOML_HAS_ATTR(__warn_unused_result__) +#define TOML_NODISCARD TOML_ATTR(__warn_unused_result__) +#else +#define TOML_NODISCARD +#endif +#endif + +// TOML_NODISCARD_CTOR +#ifndef TOML_NODISCARD_CTOR +#if TOML_CPP >= 17 && TOML_HAS_CPP_ATTR(nodiscard) >= 201907 +#define TOML_NODISCARD_CTOR [[nodiscard]] +#else +#define TOML_NODISCARD_CTOR +#endif +#endif + +// pure + const +#ifndef TOML_PURE +#ifdef NDEBUG +#define TOML_PURE \ + TOML_DECLSPEC(noalias) \ + TOML_ATTR(pure) +#else +#define TOML_PURE +#endif +#endif +#ifndef TOML_CONST +#ifdef NDEBUG +#define TOML_CONST \ + TOML_DECLSPEC(noalias) \ + TOML_ATTR(const) +#else +#define TOML_CONST +#endif +#endif +#ifndef TOML_INLINE_GETTER +#define TOML_INLINE_GETTER \ + TOML_NODISCARD \ + TOML_ALWAYS_INLINE +#endif +#ifndef TOML_PURE_GETTER +#define TOML_PURE_GETTER \ + TOML_NODISCARD \ + TOML_PURE +#endif +#ifndef TOML_PURE_INLINE_GETTER +#define TOML_PURE_INLINE_GETTER \ + TOML_NODISCARD \ + TOML_ALWAYS_INLINE \ + TOML_PURE +#endif +#ifndef TOML_CONST_GETTER +#define TOML_CONST_GETTER \ + TOML_NODISCARD \ + TOML_CONST +#endif +#ifndef TOML_CONST_INLINE_GETTER +#define TOML_CONST_INLINE_GETTER \ + TOML_NODISCARD \ + TOML_ALWAYS_INLINE \ + TOML_CONST +#endif + +// TOML_ASSUME +#ifndef TOML_ASSUME +#ifdef _MSC_VER +#define TOML_ASSUME(expr) __assume(expr) +#elif TOML_ICC || TOML_CLANG || TOML_HAS_BUILTIN(__builtin_assume) +#define TOML_ASSUME(expr) __builtin_assume(expr) +#elif TOML_HAS_CPP_ATTR(assume) >= 202207 +#define TOML_ASSUME(expr) [[assume(expr)]] +#elif TOML_HAS_ATTR(__assume__) +#define TOML_ASSUME(expr) __attribute__((__assume__(expr))) +#else +#define TOML_ASSUME(expr) static_cast(0) +#endif +#endif + +// TOML_UNREACHABLE +#ifndef TOML_UNREACHABLE +#ifdef _MSC_VER +#define TOML_UNREACHABLE __assume(0) +#elif TOML_ICC || TOML_CLANG || TOML_GCC || TOML_HAS_BUILTIN(__builtin_unreachable) +#define TOML_UNREACHABLE __builtin_unreachable() +#else +#define TOML_UNREACHABLE static_cast(0) +#endif +#endif + +// TOML_LIKELY +#if TOML_CPP >= 20 && TOML_HAS_CPP_ATTR(likely) >= 201803 +#define TOML_LIKELY(...) (__VA_ARGS__) [[likely]] +#define TOML_LIKELY_CASE [[likely]] +#elif TOML_GCC || TOML_CLANG || TOML_HAS_BUILTIN(__builtin_expect) +#define TOML_LIKELY(...) (__builtin_expect(!!(__VA_ARGS__), 1)) +#else +#define TOML_LIKELY(...) (__VA_ARGS__) +#endif +#ifndef TOML_LIKELY_CASE +#define TOML_LIKELY_CASE +#endif + +// TOML_UNLIKELY +#if TOML_CPP >= 20 && TOML_HAS_CPP_ATTR(unlikely) >= 201803 +#define TOML_UNLIKELY(...) (__VA_ARGS__) [[unlikely]] +#define TOML_UNLIKELY_CASE [[unlikely]] +#elif TOML_GCC || TOML_CLANG || TOML_HAS_BUILTIN(__builtin_expect) +#define TOML_UNLIKELY(...) (__builtin_expect(!!(__VA_ARGS__), 0)) +#else +#define TOML_UNLIKELY(...) (__VA_ARGS__) +#endif +#ifndef TOML_UNLIKELY_CASE +#define TOML_UNLIKELY_CASE +#endif + +// TOML_FLAGS_ENUM +#if TOML_CLANG || TOML_HAS_ATTR(flag_enum) +#define TOML_FLAGS_ENUM __attribute__((flag_enum)) +#else +#define TOML_FLAGS_ENUM +#endif + +// TOML_OPEN_ENUM + TOML_CLOSED_ENUM +#if TOML_CLANG || TOML_HAS_ATTR(enum_extensibility) +#define TOML_OPEN_ENUM __attribute__((enum_extensibility(open))) +#define TOML_CLOSED_ENUM __attribute__((enum_extensibility(closed))) +#else +#define TOML_OPEN_ENUM +#define TOML_CLOSED_ENUM +#endif + +// TOML_OPEN_FLAGS_ENUM + TOML_CLOSED_FLAGS_ENUM +#define TOML_OPEN_FLAGS_ENUM TOML_OPEN_ENUM TOML_FLAGS_ENUM +#define TOML_CLOSED_FLAGS_ENUM TOML_CLOSED_ENUM TOML_FLAGS_ENUM + +// TOML_MAKE_FLAGS +#define TOML_MAKE_FLAGS_2(T, op, linkage) \ + TOML_CONST_INLINE_GETTER \ + linkage constexpr T operator op(T lhs, T rhs) noexcept \ + { \ + using under = std::underlying_type_t; \ + return static_cast(static_cast(lhs) op static_cast(rhs)); \ + } \ + \ + linkage constexpr T& operator TOML_CONCAT(op, =)(T & lhs, T rhs) noexcept \ + { \ + return lhs = (lhs op rhs); \ + } \ + \ + static_assert(true) +#define TOML_MAKE_FLAGS_1(T, linkage) \ + static_assert(std::is_enum_v); \ + \ + TOML_MAKE_FLAGS_2(T, &, linkage); \ + TOML_MAKE_FLAGS_2(T, |, linkage); \ + TOML_MAKE_FLAGS_2(T, ^, linkage); \ + \ + TOML_CONST_INLINE_GETTER \ + linkage constexpr T operator~(T val) noexcept \ + { \ + using under = std::underlying_type_t; \ + return static_cast(~static_cast(val)); \ + } \ + \ + TOML_CONST_INLINE_GETTER \ + linkage constexpr bool operator!(T val) noexcept \ + { \ + using under = std::underlying_type_t; \ + return !static_cast(val); \ + } \ + \ + static_assert(true) +#define TOML_MAKE_FLAGS(T) TOML_MAKE_FLAGS_1(T, ) + +#define TOML_UNUSED(...) static_cast(__VA_ARGS__) + +#define TOML_DELETE_DEFAULTS(T) \ + T(const T&) = delete; \ + T(T&&) = delete; \ + T& operator=(const T&) = delete; \ + T& operator=(T&&) = delete + +#define TOML_ASYMMETRICAL_EQUALITY_OPS(LHS, RHS, ...) \ + __VA_ARGS__ TOML_NODISCARD \ + friend bool operator==(RHS rhs, LHS lhs) noexcept \ + { \ + return lhs == rhs; \ + } \ + __VA_ARGS__ TOML_NODISCARD \ + friend bool operator!=(LHS lhs, RHS rhs) noexcept \ + { \ + return !(lhs == rhs); \ + } \ + __VA_ARGS__ TOML_NODISCARD \ + friend bool operator!=(RHS rhs, LHS lhs) noexcept \ + { \ + return !(lhs == rhs); \ + } \ + static_assert(true) + +#define TOML_EVAL_BOOL_1(T, F) T +#define TOML_EVAL_BOOL_0(T, F) F + +#if !defined(__POXY__) && !defined(POXY_IMPLEMENTATION_DETAIL) +#define POXY_IMPLEMENTATION_DETAIL(...) __VA_ARGS__ +#endif + +// COMPILER-SPECIFIC WARNING MANAGEMENT + +#if TOML_CLANG + +#define TOML_PUSH_WARNINGS \ + TOML_PRAGMA_CLANG(diagnostic push) \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wunknown-warning-option") \ + static_assert(true) + +#define TOML_DISABLE_SWITCH_WARNINGS \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wswitch") \ + static_assert(true) + +#define TOML_DISABLE_ARITHMETIC_WARNINGS \ + TOML_PRAGMA_CLANG_GE_10(diagnostic ignored "-Wimplicit-int-float-conversion") \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wfloat-equal") \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wdouble-promotion") \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wchar-subscripts") \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wshift-sign-overflow") \ + static_assert(true) + +#define TOML_DISABLE_SPAM_WARNINGS \ + TOML_PRAGMA_CLANG_GE_8(diagnostic ignored "-Wdefaulted-function-deleted") \ + TOML_PRAGMA_CLANG_GE_9(diagnostic ignored "-Wctad-maybe-unsupported") \ + TOML_PRAGMA_CLANG_GE_10(diagnostic ignored "-Wzero-as-null-pointer-constant") \ + TOML_PRAGMA_CLANG_GE_11(diagnostic ignored "-Wsuggest-destructor-override") \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wweak-vtables") \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wweak-template-vtables") \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wdouble-promotion") \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wchar-subscripts") \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wmissing-field-initializers") \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wpadded") \ + static_assert(true) + +#define TOML_POP_WARNINGS \ + TOML_PRAGMA_CLANG(diagnostic pop) \ + static_assert(true) + +#define TOML_DISABLE_WARNINGS \ + TOML_PRAGMA_CLANG(diagnostic push) \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Weverything") \ + static_assert(true, "") + +#define TOML_ENABLE_WARNINGS \ + TOML_PRAGMA_CLANG(diagnostic pop) \ + static_assert(true) + +#define TOML_SIMPLE_STATIC_ASSERT_MESSAGES 1 + +#elif TOML_MSVC + +#define TOML_PUSH_WARNINGS \ + __pragma(warning(push)) \ + static_assert(true) + +#if TOML_HAS_INCLUDE() +#pragma warning(push, 0) +#include +#pragma warning(pop) +#define TOML_DISABLE_CODE_ANALYSIS_WARNINGS \ + __pragma(warning(disable : ALL_CODE_ANALYSIS_WARNINGS)) \ + static_assert(true) +#else +#define TOML_DISABLE_CODE_ANALYSIS_WARNINGS static_assert(true) +#endif + +#define TOML_DISABLE_SWITCH_WARNINGS \ + __pragma(warning(disable : 4061)) \ + __pragma(warning(disable : 4062)) \ + __pragma(warning(disable : 4063)) \ + __pragma(warning(disable : 5262)) /* switch-case implicit fallthrough (false-positive) */ \ + __pragma(warning(disable : 26819)) /* cg: unannotated fallthrough */ \ + static_assert(true) + +#define TOML_DISABLE_SPAM_WARNINGS \ + __pragma(warning(disable : 4127)) /* conditional expr is constant */ \ + __pragma(warning(disable : 4324)) /* structure was padded due to alignment specifier */ \ + __pragma(warning(disable : 4348)) \ + __pragma(warning(disable : 4464)) /* relative include path contains '..' */ \ + __pragma(warning(disable : 4505)) /* unreferenced local function removed */ \ + __pragma(warning(disable : 4514)) /* unreferenced inline function has been removed */ \ + __pragma(warning(disable : 4582)) /* constructor is not implicitly called */ \ + __pragma(warning(disable : 4619)) /* there is no warning number 'XXXX' */ \ + __pragma(warning(disable : 4623)) /* default constructor was implicitly defined as deleted */ \ + __pragma(warning(disable : 4625)) /* copy constructor was implicitly defined as deleted */ \ + __pragma(warning(disable : 4626)) /* assignment operator was implicitly defined as deleted */ \ + __pragma(warning(disable : 4710)) /* function not inlined */ \ + __pragma(warning(disable : 4711)) /* function selected for automatic expansion */ \ + __pragma(warning(disable : 4820)) /* N bytes padding added */ \ + __pragma(warning(disable : 5026)) /* move constructor was implicitly defined as deleted */ \ + __pragma(warning(disable : 5027)) /* move assignment operator was implicitly defined as deleted */ \ + __pragma(warning(disable : 5039)) /* potentially throwing function passed to 'extern "C"' function */ \ + __pragma(warning(disable : 5045)) /* Compiler will insert Spectre mitigation */ \ + __pragma(warning(disable : 5264)) /* const variable is not used (false-positive) */ \ + __pragma(warning(disable : 26451)) \ + __pragma(warning(disable : 26490)) \ + __pragma(warning(disable : 26495)) \ + __pragma(warning(disable : 26812)) \ + __pragma(warning(disable : 26819)) \ + static_assert(true) + +#define TOML_DISABLE_ARITHMETIC_WARNINGS \ + __pragma(warning(disable : 4365)) /* argument signed/unsigned mismatch */ \ + __pragma(warning(disable : 4738)) /* storing 32-bit float result in memory */ \ + __pragma(warning(disable : 5219)) /* implicit conversion from integral to float */ \ + static_assert(true) + +#define TOML_POP_WARNINGS \ + __pragma(warning(pop)) \ + static_assert(true) + +#define TOML_DISABLE_WARNINGS \ + __pragma(warning(push, 0)) \ + __pragma(warning(disable : 4348)) \ + __pragma(warning(disable : 4668)) \ + __pragma(warning(disable : 5105)) \ + __pragma(warning(disable : 5264)) \ + TOML_DISABLE_CODE_ANALYSIS_WARNINGS; \ + TOML_DISABLE_SWITCH_WARNINGS; \ + TOML_DISABLE_SPAM_WARNINGS; \ + TOML_DISABLE_ARITHMETIC_WARNINGS; \ + static_assert(true) + +#define TOML_ENABLE_WARNINGS TOML_POP_WARNINGS + +#elif TOML_ICC + +#define TOML_PUSH_WARNINGS \ + __pragma(warning(push)) \ + static_assert(true) + +#define TOML_DISABLE_SPAM_WARNINGS \ + __pragma(warning(disable : 82)) /* storage class is not first */ \ + __pragma(warning(disable : 111)) /* statement unreachable (false-positive) */ \ + __pragma(warning(disable : 869)) /* unreferenced parameter */ \ + __pragma(warning(disable : 1011)) /* missing return (false-positive) */ \ + __pragma(warning(disable : 2261)) /* assume expr side-effects discarded */ \ + static_assert(true) + +#define TOML_POP_WARNINGS \ + __pragma(warning(pop)) \ + static_assert(true) + +#define TOML_DISABLE_WARNINGS \ + __pragma(warning(push, 0)) \ + TOML_DISABLE_SPAM_WARNINGS + +#define TOML_ENABLE_WARNINGS \ + __pragma(warning(pop)) \ + static_assert(true) + +#elif TOML_GCC + +#define TOML_PUSH_WARNINGS \ + TOML_PRAGMA_GCC(diagnostic push) \ + static_assert(true) + +#define TOML_DISABLE_SWITCH_WARNINGS \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wswitch") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wswitch-enum") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wswitch-default") \ + static_assert(true) + +#define TOML_DISABLE_ARITHMETIC_WARNINGS \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wfloat-equal") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wsign-conversion") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wchar-subscripts") \ + static_assert(true) + +#define TOML_DISABLE_SUGGEST_ATTR_WARNINGS \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wsuggest-attribute=const") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wsuggest-attribute=pure") \ + static_assert(true) + +#define TOML_DISABLE_SPAM_WARNINGS \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wpadded") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wcast-align") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wcomment") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wtype-limits") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wuseless-cast") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wchar-subscripts") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wsubobject-linkage") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wmissing-field-initializers") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wmaybe-uninitialized") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wnoexcept") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wnull-dereference") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wduplicated-branches") \ + static_assert(true) + +#define TOML_POP_WARNINGS \ + TOML_PRAGMA_GCC(diagnostic pop) \ + static_assert(true) + +#define TOML_DISABLE_WARNINGS \ + TOML_PRAGMA_GCC(diagnostic push) \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wall") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wextra") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wpedantic") \ + TOML_DISABLE_SWITCH_WARNINGS; \ + TOML_DISABLE_ARITHMETIC_WARNINGS; \ + TOML_DISABLE_SUGGEST_ATTR_WARNINGS; \ + TOML_DISABLE_SPAM_WARNINGS; \ + static_assert(true) + +#define TOML_ENABLE_WARNINGS \ + TOML_PRAGMA_GCC(diagnostic pop) \ + static_assert(true) + +#endif + +#ifndef TOML_PUSH_WARNINGS +#define TOML_PUSH_WARNINGS static_assert(true) +#endif +#ifndef TOML_DISABLE_CODE_ANALYSIS_WARNINGS +#define TOML_DISABLE_CODE_ANALYSIS_WARNINGS static_assert(true) +#endif +#ifndef TOML_DISABLE_SWITCH_WARNINGS +#define TOML_DISABLE_SWITCH_WARNINGS static_assert(true) +#endif +#ifndef TOML_DISABLE_SUGGEST_ATTR_WARNINGS +#define TOML_DISABLE_SUGGEST_ATTR_WARNINGS static_assert(true) +#endif +#ifndef TOML_DISABLE_SPAM_WARNINGS +#define TOML_DISABLE_SPAM_WARNINGS static_assert(true) +#endif +#ifndef TOML_DISABLE_ARITHMETIC_WARNINGS +#define TOML_DISABLE_ARITHMETIC_WARNINGS static_assert(true) +#endif +#ifndef TOML_POP_WARNINGS +#define TOML_POP_WARNINGS static_assert(true) +#endif +#ifndef TOML_DISABLE_WARNINGS +#define TOML_DISABLE_WARNINGS static_assert(true) +#endif +#ifndef TOML_ENABLE_WARNINGS +#define TOML_ENABLE_WARNINGS static_assert(true) +#endif +#ifndef TOML_SIMPLE_STATIC_ASSERT_MESSAGES +#define TOML_SIMPLE_STATIC_ASSERT_MESSAGES 0 +#endif + +#ifdef TOML_CONFIG_HEADER +#include TOML_CONFIG_HEADER +#endif + +// is the library being built as a shared lib/dll using meson and friends? +#ifndef TOML_SHARED_LIB +#define TOML_SHARED_LIB 0 +#endif + +// header-only mode +#if !defined(TOML_HEADER_ONLY) && defined(TOML_ALL_INLINE) // was TOML_ALL_INLINE pre-2.0 +#define TOML_HEADER_ONLY TOML_ALL_INLINE +#endif +#if !defined(TOML_HEADER_ONLY) || (defined(TOML_HEADER_ONLY) && TOML_HEADER_ONLY) || TOML_INTELLISENSE +#undef TOML_HEADER_ONLY +#define TOML_HEADER_ONLY 1 +#endif +#if TOML_DOXYGEN || TOML_SHARED_LIB +#undef TOML_HEADER_ONLY +#define TOML_HEADER_ONLY 0 +#endif + +// internal implementation switch +#if defined(TOML_IMPLEMENTATION) || TOML_HEADER_ONLY +#undef TOML_IMPLEMENTATION +#define TOML_IMPLEMENTATION 1 +#else +#define TOML_IMPLEMENTATION 0 +#endif + +// dll/shared lib function exports (legacy - TOML_API was the old name for this setting) +#if !defined(TOML_EXPORTED_MEMBER_FUNCTION) && !defined(TOML_EXPORTED_STATIC_FUNCTION) \ + && !defined(TOML_EXPORTED_FREE_FUNCTION) && !defined(TOML_EXPORTED_CLASS) && defined(TOML_API) +#define TOML_EXPORTED_MEMBER_FUNCTION TOML_API +#define TOML_EXPORTED_STATIC_FUNCTION TOML_API +#define TOML_EXPORTED_FREE_FUNCTION TOML_API +#endif + +// dll/shared lib exports +#if TOML_SHARED_LIB +#undef TOML_API +#undef TOML_EXPORTED_CLASS +#undef TOML_EXPORTED_MEMBER_FUNCTION +#undef TOML_EXPORTED_STATIC_FUNCTION +#undef TOML_EXPORTED_FREE_FUNCTION +#if TOML_WINDOWS +#if TOML_IMPLEMENTATION +#define TOML_EXPORTED_CLASS __declspec(dllexport) +#define TOML_EXPORTED_FREE_FUNCTION __declspec(dllexport) +#else +#define TOML_EXPORTED_CLASS __declspec(dllimport) +#define TOML_EXPORTED_FREE_FUNCTION __declspec(dllimport) +#endif +#ifndef TOML_CALLCONV +#define TOML_CALLCONV __cdecl +#endif +#elif defined(__GNUC__) && __GNUC__ >= 4 +#define TOML_EXPORTED_CLASS __attribute__((visibility("default"))) +#define TOML_EXPORTED_MEMBER_FUNCTION __attribute__((visibility("default"))) +#define TOML_EXPORTED_STATIC_FUNCTION __attribute__((visibility("default"))) +#define TOML_EXPORTED_FREE_FUNCTION __attribute__((visibility("default"))) +#endif +#endif +#ifndef TOML_EXPORTED_CLASS +#define TOML_EXPORTED_CLASS +#endif +#ifndef TOML_EXPORTED_MEMBER_FUNCTION +#define TOML_EXPORTED_MEMBER_FUNCTION +#endif +#ifndef TOML_EXPORTED_STATIC_FUNCTION +#define TOML_EXPORTED_STATIC_FUNCTION +#endif +#ifndef TOML_EXPORTED_FREE_FUNCTION +#define TOML_EXPORTED_FREE_FUNCTION +#endif + +// experimental language features +#if !defined(TOML_ENABLE_UNRELEASED_FEATURES) && defined(TOML_UNRELEASED_FEATURES) // was TOML_UNRELEASED_FEATURES + // pre-3.0 +#define TOML_ENABLE_UNRELEASED_FEATURES TOML_UNRELEASED_FEATURES +#endif +#if (defined(TOML_ENABLE_UNRELEASED_FEATURES) && TOML_ENABLE_UNRELEASED_FEATURES) || TOML_INTELLISENSE +#undef TOML_ENABLE_UNRELEASED_FEATURES +#define TOML_ENABLE_UNRELEASED_FEATURES 1 +#endif +#ifndef TOML_ENABLE_UNRELEASED_FEATURES +#define TOML_ENABLE_UNRELEASED_FEATURES 0 +#endif + +// parser +#if !defined(TOML_ENABLE_PARSER) && defined(TOML_PARSER) // was TOML_PARSER pre-3.0 +#define TOML_ENABLE_PARSER TOML_PARSER +#endif +#if !defined(TOML_ENABLE_PARSER) || (defined(TOML_ENABLE_PARSER) && TOML_ENABLE_PARSER) || TOML_INTELLISENSE +#undef TOML_ENABLE_PARSER +#define TOML_ENABLE_PARSER 1 +#endif + +// formatters +#if !defined(TOML_ENABLE_FORMATTERS) || (defined(TOML_ENABLE_FORMATTERS) && TOML_ENABLE_FORMATTERS) || TOML_INTELLISENSE +#undef TOML_ENABLE_FORMATTERS +#define TOML_ENABLE_FORMATTERS 1 +#endif + +// SIMD +#if !defined(TOML_ENABLE_SIMD) || (defined(TOML_ENABLE_SIMD) && TOML_ENABLE_SIMD) || TOML_INTELLISENSE +#undef TOML_ENABLE_SIMD +#define TOML_ENABLE_SIMD 1 +#endif + +// windows compat +#if !defined(TOML_ENABLE_WINDOWS_COMPAT) && defined(TOML_WINDOWS_COMPAT) // was TOML_WINDOWS_COMPAT pre-3.0 +#define TOML_ENABLE_WINDOWS_COMPAT TOML_WINDOWS_COMPAT +#endif +#if !defined(TOML_ENABLE_WINDOWS_COMPAT) || (defined(TOML_ENABLE_WINDOWS_COMPAT) && TOML_ENABLE_WINDOWS_COMPAT) \ + || TOML_INTELLISENSE +#undef TOML_ENABLE_WINDOWS_COMPAT +#define TOML_ENABLE_WINDOWS_COMPAT 1 +#endif + +#if !TOML_WINDOWS +#undef TOML_ENABLE_WINDOWS_COMPAT +#define TOML_ENABLE_WINDOWS_COMPAT 0 +#endif + +#ifndef TOML_INCLUDE_WINDOWS_H +#define TOML_INCLUDE_WINDOWS_H 0 +#endif + +// custom optional +#ifdef TOML_OPTIONAL_TYPE +#define TOML_HAS_CUSTOM_OPTIONAL_TYPE 1 +#else +#define TOML_HAS_CUSTOM_OPTIONAL_TYPE 0 +#endif + +// exceptions (library use) +#if TOML_COMPILER_HAS_EXCEPTIONS +#if !defined(TOML_EXCEPTIONS) || (defined(TOML_EXCEPTIONS) && TOML_EXCEPTIONS) +#undef TOML_EXCEPTIONS +#define TOML_EXCEPTIONS 1 +#endif +#else +#if defined(TOML_EXCEPTIONS) && TOML_EXCEPTIONS +#error TOML_EXCEPTIONS was explicitly enabled but exceptions are disabled/unsupported by the compiler. +#endif +#undef TOML_EXCEPTIONS +#define TOML_EXCEPTIONS 0 +#endif + +// calling convention for static/free/friend functions +#ifndef TOML_CALLCONV +#define TOML_CALLCONV +#endif + +#ifndef TOML_UNDEF_MACROS +#define TOML_UNDEF_MACROS 1 +#endif + +#ifndef TOML_MAX_NESTED_VALUES +#define TOML_MAX_NESTED_VALUES 128 +// this refers to the depth of nested values, e.g. inline tables and arrays. +// 128 is very generous; real TOML files rarely exceed single-digit nesting. +// keep this value low enough to avoid stack overflows in sanitizer-instrumented builds +// where each recursion cycle may consume ~3KB of stack. +#endif + +#ifndef TOML_MAX_DOTTED_KEYS_DEPTH +#define TOML_MAX_DOTTED_KEYS_DEPTH 1024 +#endif + +#ifdef TOML_CHAR_8_STRINGS +#if TOML_CHAR_8_STRINGS +#error TOML_CHAR_8_STRINGS was removed in toml++ 2.0.0; all value setters and getters now work with char8_t strings implicitly. +#endif +#endif + +#ifdef TOML_LARGE_FILES +#if !TOML_LARGE_FILES +#error Support for !TOML_LARGE_FILES (i.e. 'small files') was removed in toml++ 3.0.0. +#endif +#endif + +#ifndef TOML_LIFETIME_HOOKS +#define TOML_LIFETIME_HOOKS 0 +#endif + +#ifdef NDEBUG +#undef TOML_ASSERT +#define TOML_ASSERT(expr) static_assert(true) +#endif +#ifndef TOML_ASSERT +#ifndef assert +TOML_DISABLE_WARNINGS; +#include +TOML_ENABLE_WARNINGS; +#endif +#define TOML_ASSERT(expr) assert(expr) +#endif +#ifdef NDEBUG +#define TOML_ASSERT_ASSUME(expr) TOML_ASSUME(expr) +#else +#define TOML_ASSERT_ASSUME(expr) TOML_ASSERT(expr) +#endif + +#ifndef TOML_ENABLE_FLOAT16 +#define TOML_ENABLE_FLOAT16 0 +#endif + +#ifndef TOML_DISABLE_CONDITIONAL_NOEXCEPT_LAMBDA +#define TOML_DISABLE_CONDITIONAL_NOEXCEPT_LAMBDA 0 +#endif + +#ifndef TOML_DISABLE_NOEXCEPT_NOEXCEPT +#define TOML_DISABLE_NOEXCEPT_NOEXCEPT 0 + #ifdef _MSC_VER + #if _MSC_VER <= 1943 // Up to Visual Studio 2022 Version 17.13.6 + #undef TOML_DISABLE_NOEXCEPT_NOEXCEPT + #define TOML_DISABLE_NOEXCEPT_NOEXCEPT 1 + #endif + #endif +#endif + +#if !defined(TOML_FLOAT_CHARCONV) && (TOML_GCC || TOML_CLANG || (TOML_ICC && !TOML_ICC_CL)) +// not supported by any version of GCC or Clang as of 26/11/2020 +// not supported by any version of ICC on Linux as of 11/01/2021 +#define TOML_FLOAT_CHARCONV 0 +#endif +#if !defined(TOML_INT_CHARCONV) && (defined(__EMSCRIPTEN__) || defined(__APPLE__)) +// causes link errors on emscripten +// causes Mac OS SDK version errors on some versions of Apple Clang +#define TOML_INT_CHARCONV 0 +#endif +#ifndef TOML_INT_CHARCONV +#define TOML_INT_CHARCONV 1 +#endif +#ifndef TOML_FLOAT_CHARCONV +#define TOML_FLOAT_CHARCONV 1 +#endif +#if (TOML_INT_CHARCONV || TOML_FLOAT_CHARCONV) && !TOML_HAS_INCLUDE() +#undef TOML_INT_CHARCONV +#undef TOML_FLOAT_CHARCONV +#define TOML_INT_CHARCONV 0 +#define TOML_FLOAT_CHARCONV 0 +#endif + +#if defined(__cpp_concepts) && __cpp_concepts >= 201907 +#define TOML_REQUIRES(...) requires(__VA_ARGS__) +#else +#define TOML_REQUIRES(...) +#endif +#define TOML_ENABLE_IF(...) , typename std::enable_if<(__VA_ARGS__), int>::type = 0 +#define TOML_CONSTRAINED_TEMPLATE(condition, ...) \ + template <__VA_ARGS__ TOML_ENABLE_IF(condition)> \ + TOML_REQUIRES(condition) +#define TOML_HIDDEN_CONSTRAINT(condition, ...) TOML_CONSTRAINED_TEMPLATE(condition, __VA_ARGS__) + +#if defined(__SIZEOF_FLOAT128__) && defined(__FLT128_MANT_DIG__) && defined(__LDBL_MANT_DIG__) \ + && __FLT128_MANT_DIG__ > __LDBL_MANT_DIG__ +#define TOML_FLOAT128 __float128 +#endif + +#ifdef __SIZEOF_INT128__ +#define TOML_INT128 __int128_t +#define TOML_UINT128 __uint128_t +#endif + +// clang-format off + +//******** impl/version.hpp ****************************************************************************************** + +#define TOML_LIB_MAJOR 3 +#define TOML_LIB_MINOR 4 +#define TOML_LIB_PATCH 0 + +#define TOML_LANG_MAJOR 1 +#define TOML_LANG_MINOR 0 +#define TOML_LANG_PATCH 0 + +//******** impl/preprocessor.hpp ************************************************************************************* + +#define TOML_LIB_SINGLE_HEADER 1 + +#if TOML_ENABLE_UNRELEASED_FEATURES + #define TOML_LANG_EFFECTIVE_VERSION \ + TOML_MAKE_VERSION(TOML_LANG_MAJOR, TOML_LANG_MINOR, TOML_LANG_PATCH+1) +#else + #define TOML_LANG_EFFECTIVE_VERSION \ + TOML_MAKE_VERSION(TOML_LANG_MAJOR, TOML_LANG_MINOR, TOML_LANG_PATCH) +#endif + +#define TOML_LANG_HIGHER_THAN(major, minor, patch) \ + (TOML_LANG_EFFECTIVE_VERSION > TOML_MAKE_VERSION(major, minor, patch)) + +#define TOML_LANG_AT_LEAST(major, minor, patch) \ + (TOML_LANG_EFFECTIVE_VERSION >= TOML_MAKE_VERSION(major, minor, patch)) + +#define TOML_LANG_UNRELEASED \ + TOML_LANG_HIGHER_THAN(TOML_LANG_MAJOR, TOML_LANG_MINOR, TOML_LANG_PATCH) + +#ifndef TOML_ABI_NAMESPACES + #if TOML_DOXYGEN + #define TOML_ABI_NAMESPACES 0 + #else + #define TOML_ABI_NAMESPACES 1 + #endif +#endif +#if TOML_ABI_NAMESPACES + #define TOML_NAMESPACE_START namespace toml { inline namespace TOML_CONCAT(v, TOML_LIB_MAJOR) + #define TOML_NAMESPACE_END } static_assert(true) + #define TOML_NAMESPACE ::toml::TOML_CONCAT(v, TOML_LIB_MAJOR) + #define TOML_ABI_NAMESPACE_START(name) inline namespace name { static_assert(true) + #define TOML_ABI_NAMESPACE_BOOL(cond, T, F) TOML_ABI_NAMESPACE_START(TOML_CONCAT(TOML_EVAL_BOOL_, cond)(T, F)) + #define TOML_ABI_NAMESPACE_END } static_assert(true) +#else + #define TOML_NAMESPACE_START namespace toml + #define TOML_NAMESPACE_END static_assert(true) + #define TOML_NAMESPACE toml + #define TOML_ABI_NAMESPACE_START(...) static_assert(true) + #define TOML_ABI_NAMESPACE_BOOL(...) static_assert(true) + #define TOML_ABI_NAMESPACE_END static_assert(true) +#endif +#define TOML_IMPL_NAMESPACE_START TOML_NAMESPACE_START { namespace impl +#define TOML_IMPL_NAMESPACE_END } TOML_NAMESPACE_END +#if TOML_HEADER_ONLY + #define TOML_ANON_NAMESPACE_START static_assert(TOML_IMPLEMENTATION); TOML_IMPL_NAMESPACE_START + #define TOML_ANON_NAMESPACE_END TOML_IMPL_NAMESPACE_END + #define TOML_ANON_NAMESPACE TOML_NAMESPACE::impl + #define TOML_EXTERNAL_LINKAGE inline + #define TOML_INTERNAL_LINKAGE inline +#else + #define TOML_ANON_NAMESPACE_START static_assert(TOML_IMPLEMENTATION); \ + using namespace toml; \ + namespace + #define TOML_ANON_NAMESPACE_END static_assert(true) + #define TOML_ANON_NAMESPACE + #define TOML_EXTERNAL_LINKAGE + #define TOML_INTERNAL_LINKAGE static +#endif + +// clang-format on + +// clang-format off + +#if TOML_SIMPLE_STATIC_ASSERT_MESSAGES + + #define TOML_SA_NEWLINE " " + #define TOML_SA_LIST_SEP ", " + #define TOML_SA_LIST_BEG " (" + #define TOML_SA_LIST_END ")" + #define TOML_SA_LIST_NEW " " + #define TOML_SA_LIST_NXT ", " + +#else + + #define TOML_SA_NEWLINE "\n| " + #define TOML_SA_LIST_SEP TOML_SA_NEWLINE " - " + #define TOML_SA_LIST_BEG TOML_SA_LIST_SEP + #define TOML_SA_LIST_END + #define TOML_SA_LIST_NEW TOML_SA_NEWLINE TOML_SA_NEWLINE + #define TOML_SA_LIST_NXT TOML_SA_LIST_NEW + +#endif + +#define TOML_SA_NATIVE_VALUE_TYPE_LIST \ + TOML_SA_LIST_BEG "std::string" \ + TOML_SA_LIST_SEP "int64_t" \ + TOML_SA_LIST_SEP "double" \ + TOML_SA_LIST_SEP "bool" \ + TOML_SA_LIST_SEP "toml::date" \ + TOML_SA_LIST_SEP "toml::time" \ + TOML_SA_LIST_SEP "toml::date_time" \ + TOML_SA_LIST_END + +#define TOML_SA_NODE_TYPE_LIST \ + TOML_SA_LIST_BEG "toml::table" \ + TOML_SA_LIST_SEP "toml::array" \ + TOML_SA_LIST_SEP "toml::value" \ + TOML_SA_LIST_SEP "toml::value" \ + TOML_SA_LIST_SEP "toml::value" \ + TOML_SA_LIST_SEP "toml::value" \ + TOML_SA_LIST_SEP "toml::value" \ + TOML_SA_LIST_SEP "toml::value" \ + TOML_SA_LIST_SEP "toml::value" \ + TOML_SA_LIST_END + +#define TOML_SA_UNWRAPPED_NODE_TYPE_LIST \ + TOML_SA_LIST_NEW "A native TOML value type" \ + TOML_SA_NATIVE_VALUE_TYPE_LIST \ + \ + TOML_SA_LIST_NXT "A TOML node type" \ + TOML_SA_NODE_TYPE_LIST + +// clang-format on + +TOML_PUSH_WARNINGS; +TOML_DISABLE_SPAM_WARNINGS; +TOML_DISABLE_SWITCH_WARNINGS; +TOML_DISABLE_SUGGEST_ATTR_WARNINGS; + +// misc warning false-positives +#if TOML_MSVC +#pragma warning(disable : 5031) // #pragma warning(pop): likely mismatch +#if TOML_SHARED_LIB +#pragma warning(disable : 4251) // dll exports for std lib types +#endif +#elif TOML_CLANG +TOML_PRAGMA_CLANG(diagnostic ignored "-Wheader-hygiene") +#if TOML_CLANG >= 12 +TOML_PRAGMA_CLANG(diagnostic ignored "-Wc++20-extensions") +#endif +#if TOML_CLANG == 13 +TOML_PRAGMA_CLANG(diagnostic ignored "-Wreserved-identifier") +#endif +#endif + +//******** impl/std_new.hpp ****************************************************************************************** + +TOML_DISABLE_WARNINGS; +#include +TOML_ENABLE_WARNINGS; + +#if (!defined(__apple_build_version__) && TOML_CLANG >= 8) || TOML_GCC >= 7 || TOML_ICC >= 1910 || TOML_MSVC >= 1914 +#define TOML_LAUNDER(x) __builtin_launder(x) +#elif defined(__cpp_lib_launder) && __cpp_lib_launder >= 201606 +#define TOML_LAUNDER(x) std::launder(x) +#else +#define TOML_LAUNDER(x) x +#endif + +//******** impl/std_string.hpp *************************************************************************************** + +TOML_DISABLE_WARNINGS; +#include +#include +TOML_ENABLE_WARNINGS; + +#if TOML_DOXYGEN \ + || (defined(__cpp_char8_t) && __cpp_char8_t >= 201811 && defined(__cpp_lib_char8_t) \ + && __cpp_lib_char8_t >= 201907) +#define TOML_HAS_CHAR8 1 +#else +#define TOML_HAS_CHAR8 0 +#endif + +namespace toml // non-abi namespace; this is not an error +{ + using namespace std::string_literals; + using namespace std::string_view_literals; +} + +#if TOML_ENABLE_WINDOWS_COMPAT + +TOML_IMPL_NAMESPACE_START +{ + TOML_NODISCARD + TOML_EXPORTED_FREE_FUNCTION + std::string narrow(std::wstring_view); + + TOML_NODISCARD + TOML_EXPORTED_FREE_FUNCTION + std::wstring widen(std::string_view); + +#if TOML_HAS_CHAR8 + + TOML_NODISCARD + TOML_EXPORTED_FREE_FUNCTION + std::wstring widen(std::u8string_view); + +#endif +} +TOML_IMPL_NAMESPACE_END; + +#endif // TOML_ENABLE_WINDOWS_COMPAT + +//******** impl/std_optional.hpp ************************************************************************************* + +TOML_DISABLE_WARNINGS; +#if !TOML_HAS_CUSTOM_OPTIONAL_TYPE +#include +#endif +TOML_ENABLE_WARNINGS; + +TOML_NAMESPACE_START +{ +#if TOML_HAS_CUSTOM_OPTIONAL_TYPE + + template + using optional = TOML_OPTIONAL_TYPE; + +#else + + template + using optional = std::optional; + +#endif +} +TOML_NAMESPACE_END; + +//******** impl/forward_declarations.hpp ***************************************************************************** + +TOML_DISABLE_WARNINGS; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +TOML_ENABLE_WARNINGS; +TOML_PUSH_WARNINGS; +#ifdef _MSC_VER +#ifndef __clang__ +#pragma inline_recursion(on) +#endif +#pragma push_macro("min") +#pragma push_macro("max") +#undef min +#undef max +#endif + +#ifndef TOML_DISABLE_ENVIRONMENT_CHECKS +#define TOML_ENV_MESSAGE \ + "If you're seeing this error it's because you're building toml++ for an environment that doesn't conform to " \ + "one of the 'ground truths' assumed by the library. Essentially this just means that I don't have the " \ + "resources to test on more platforms, but I wish I did! You can try disabling the checks by defining " \ + "TOML_DISABLE_ENVIRONMENT_CHECKS, but your mileage may vary. Please consider filing an issue at " \ + "https://github.com/marzer/tomlplusplus/issues to help me improve support for your target environment. " \ + "Thanks!" + +static_assert(CHAR_BIT == 8, TOML_ENV_MESSAGE); +#ifdef FLT_RADIX +static_assert(FLT_RADIX == 2, TOML_ENV_MESSAGE); +#endif +static_assert('A' == 65, TOML_ENV_MESSAGE); +static_assert(sizeof(double) == 8, TOML_ENV_MESSAGE); +static_assert(std::numeric_limits::is_iec559, TOML_ENV_MESSAGE); +static_assert(std::numeric_limits::digits == 53, TOML_ENV_MESSAGE); +static_assert(std::numeric_limits::digits10 == 15, TOML_ENV_MESSAGE); +static_assert(std::numeric_limits::radix == 2, TOML_ENV_MESSAGE); + +#undef TOML_ENV_MESSAGE +#endif // !TOML_DISABLE_ENVIRONMENT_CHECKS + +// undocumented forward declarations are hidden from doxygen because they fuck it up =/ + +namespace toml // non-abi namespace; this is not an error +{ + using ::std::size_t; + using ::std::intptr_t; + using ::std::uintptr_t; + using ::std::ptrdiff_t; + using ::std::nullptr_t; + using ::std::int8_t; + using ::std::int16_t; + using ::std::int32_t; + using ::std::int64_t; + using ::std::uint8_t; + using ::std::uint16_t; + using ::std::uint32_t; + using ::std::uint64_t; + using ::std::uint_least32_t; + using ::std::uint_least64_t; +} + +TOML_NAMESPACE_START +{ + struct date; + struct time; + struct time_offset; + + TOML_ABI_NAMESPACE_BOOL(TOML_HAS_CUSTOM_OPTIONAL_TYPE, custopt, stdopt); + struct date_time; + TOML_ABI_NAMESPACE_END; + + struct source_position; + struct source_region; + + class node; + template + class node_view; + + class key; + class array; + class table; + template + class value; + + class path; + + class toml_formatter; + class json_formatter; + class yaml_formatter; + + TOML_ABI_NAMESPACE_BOOL(TOML_EXCEPTIONS, ex, noex); +#if TOML_EXCEPTIONS + using parse_result = table; +#else + class parse_result; +#endif + TOML_ABI_NAMESPACE_END; // TOML_EXCEPTIONS +} +TOML_NAMESPACE_END; + +TOML_IMPL_NAMESPACE_START +{ + using node_ptr = std::unique_ptr; + + TOML_ABI_NAMESPACE_BOOL(TOML_EXCEPTIONS, impl_ex, impl_noex); + class parser; + TOML_ABI_NAMESPACE_END; // TOML_EXCEPTIONS + + // clang-format off + + inline constexpr std::string_view control_char_escapes[] = + { + "\\u0000"sv, + "\\u0001"sv, + "\\u0002"sv, + "\\u0003"sv, + "\\u0004"sv, + "\\u0005"sv, + "\\u0006"sv, + "\\u0007"sv, + "\\b"sv, + "\\t"sv, + "\\n"sv, + "\\u000B"sv, + "\\f"sv, + "\\r"sv, + "\\u000E"sv, + "\\u000F"sv, + "\\u0010"sv, + "\\u0011"sv, + "\\u0012"sv, + "\\u0013"sv, + "\\u0014"sv, + "\\u0015"sv, + "\\u0016"sv, + "\\u0017"sv, + "\\u0018"sv, + "\\u0019"sv, + "\\u001A"sv, + "\\u001B"sv, + "\\u001C"sv, + "\\u001D"sv, + "\\u001E"sv, + "\\u001F"sv, + }; + + inline constexpr std::string_view node_type_friendly_names[] = + { + "none"sv, + "table"sv, + "array"sv, + "string"sv, + "integer"sv, + "floating-point"sv, + "boolean"sv, + "date"sv, + "time"sv, + "date-time"sv + }; + + // clang-format on +} +TOML_IMPL_NAMESPACE_END; + +#if TOML_ABI_NAMESPACES +#if TOML_EXCEPTIONS +#define TOML_PARSER_TYPENAME TOML_NAMESPACE::impl::impl_ex::parser +#else +#define TOML_PARSER_TYPENAME TOML_NAMESPACE::impl::impl_noex::parser +#endif +#else +#define TOML_PARSER_TYPENAME TOML_NAMESPACE::impl::parser +#endif + +namespace toml +{ +} + +TOML_NAMESPACE_START // abi namespace +{ + inline namespace literals + { + } + + enum class TOML_CLOSED_ENUM node_type : uint8_t + { + none, + table, + array, + string, + integer, + floating_point, + boolean, + date, + time, + date_time + }; + + template + inline std::basic_ostream& operator<<(std::basic_ostream& lhs, node_type rhs) + { + const auto str = impl::node_type_friendly_names[static_cast>(rhs)]; + using str_char_t = decltype(str)::value_type; + if constexpr (std::is_same_v) + return lhs << str; + else + { + if constexpr (sizeof(Char) == sizeof(str_char_t)) + return lhs << std::basic_string_view{ reinterpret_cast(str.data()), str.length() }; + else + return lhs << str.data(); + } + } + + enum class TOML_OPEN_FLAGS_ENUM value_flags : uint16_t // being an "OPEN" flags enum is not an error + { + none, + format_as_binary = 1, + format_as_octal = 2, + format_as_hexadecimal = 3, + }; + TOML_MAKE_FLAGS(value_flags); + + inline constexpr value_flags preserve_source_value_flags = + POXY_IMPLEMENTATION_DETAIL(value_flags{ static_cast>(-1) }); + + enum class TOML_CLOSED_FLAGS_ENUM format_flags : uint64_t + { + none, + quote_dates_and_times = (1ull << 0), + quote_infinities_and_nans = (1ull << 1), + allow_literal_strings = (1ull << 2), + allow_multi_line_strings = (1ull << 3), + allow_real_tabs_in_strings = (1ull << 4), + allow_unicode_strings = (1ull << 5), + allow_binary_integers = (1ull << 6), + allow_octal_integers = (1ull << 7), + allow_hexadecimal_integers = (1ull << 8), + indent_sub_tables = (1ull << 9), + indent_array_elements = (1ull << 10), + indentation = indent_sub_tables | indent_array_elements, + relaxed_float_precision = (1ull << 11), + terse_key_value_pairs = (1ull << 12), + force_multiline_arrays = (1ull << 13), + }; + TOML_MAKE_FLAGS(format_flags); + + template + struct TOML_TRIVIAL_ABI inserter + { + static_assert(std::is_reference_v); + + T value; + }; + template + inserter(T&&) -> inserter; + template + inserter(T&) -> inserter; + + using default_formatter = toml_formatter; +} +TOML_NAMESPACE_END; + +TOML_IMPL_NAMESPACE_START +{ + template + using remove_cvref = std::remove_cv_t>; + + template + using common_signed_type = std::common_type_t...>; + + template + inline constexpr bool is_one_of = (false || ... || std::is_same_v); + + template + inline constexpr bool all_integral = (std::is_integral_v && ...); + + template + inline constexpr bool is_cvref = std::is_reference_v || std::is_const_v || std::is_volatile_v; + + template + inline constexpr bool is_wide_string = + is_one_of, const wchar_t*, wchar_t*, std::wstring_view, std::wstring>; + + template + inline constexpr bool value_retrieval_is_nothrow = !std::is_same_v, std::string> +#if TOML_HAS_CHAR8 + && !std::is_same_v, std::u8string> +#endif + + && !is_wide_string; + + template + struct copy_ref_; + template + using copy_ref = typename copy_ref_::type; + + template + struct copy_ref_ + { + using type = Dest; + }; + + template + struct copy_ref_ + { + using type = std::add_lvalue_reference_t; + }; + + template + struct copy_ref_ + { + using type = std::add_rvalue_reference_t; + }; + + template + struct copy_cv_; + template + using copy_cv = typename copy_cv_::type; + + template + struct copy_cv_ + { + using type = Dest; + }; + + template + struct copy_cv_ + { + using type = std::add_const_t; + }; + + template + struct copy_cv_ + { + using type = std::add_volatile_t; + }; + + template + struct copy_cv_ + { + using type = std::add_cv_t; + }; + + template + using copy_cvref = + copy_ref, std::remove_reference_t>, Dest>, Src>; + + template + inline constexpr bool always_false = false; + + template + inline constexpr bool first_is_same = false; + template + inline constexpr bool first_is_same = true; + + template > + struct underlying_type_ + { + using type = std::underlying_type_t; + }; + template + struct underlying_type_ + { + using type = T; + }; + template + using underlying_type = typename underlying_type_::type; + + // general value traits + // (as they relate to their equivalent native TOML type) + struct default_value_traits + { + using native_type = void; + static constexpr bool is_native = false; + static constexpr bool is_losslessly_convertible_to_native = false; + static constexpr bool can_represent_native = false; + static constexpr bool can_partially_represent_native = false; + static constexpr auto type = node_type::none; + }; + + template + struct value_traits; + + template > + struct value_traits_base_selector + { + static_assert(!is_cvref); + + using type = default_value_traits; + }; + template + struct value_traits_base_selector + { + static_assert(!is_cvref); + + using type = value_traits>; + }; + + template + struct value_traits : value_traits_base_selector::type + {}; + template + struct value_traits : value_traits + {}; + template + struct value_traits : value_traits + {}; + template + struct value_traits : value_traits + {}; + template + struct value_traits : value_traits + {}; + template + struct value_traits : value_traits + {}; + + // integer value_traits specializations - standard types + template + struct integer_limits + { + static constexpr T min = T{ (std::numeric_limits>::min)() }; + static constexpr T max = T{ (std::numeric_limits>::max)() }; + }; + template + struct integer_traits_base : integer_limits + { + using native_type = int64_t; + static constexpr bool is_native = std::is_same_v, native_type>; + static constexpr bool is_signed = static_cast>(-1) < underlying_type{}; + static constexpr auto type = node_type::integer; + static constexpr bool can_partially_represent_native = true; + }; + template + struct unsigned_integer_traits : integer_traits_base + { + static constexpr bool is_losslessly_convertible_to_native = + integer_limits>::max <= 9223372036854775807ULL; + static constexpr bool can_represent_native = false; + }; + template + struct signed_integer_traits : integer_traits_base + { + using native_type = int64_t; + static constexpr bool is_losslessly_convertible_to_native = + integer_limits>::min >= (-9223372036854775807LL - 1LL) + && integer_limits>::max <= 9223372036854775807LL; + static constexpr bool can_represent_native = + integer_limits>::min <= (-9223372036854775807LL - 1LL) + && integer_limits>::max >= 9223372036854775807LL; + }; + template ::is_signed> + struct integer_traits : signed_integer_traits + {}; + template + struct integer_traits : unsigned_integer_traits + {}; + template <> + struct value_traits : integer_traits + {}; + template <> + struct value_traits : integer_traits + {}; + template <> + struct value_traits : integer_traits + {}; + template <> + struct value_traits : integer_traits + {}; + template <> + struct value_traits : integer_traits + {}; + template <> + struct value_traits : integer_traits + {}; + template <> + struct value_traits : integer_traits + {}; + template <> + struct value_traits : integer_traits + {}; + template <> + struct value_traits : integer_traits + {}; + template <> + struct value_traits : integer_traits + {}; + static_assert(value_traits::is_native); + static_assert(value_traits::is_signed); + static_assert(value_traits::is_losslessly_convertible_to_native); + static_assert(value_traits::can_represent_native); + static_assert(value_traits::can_partially_represent_native); + + // integer value_traits specializations - non-standard types +#ifdef TOML_INT128 + template <> + struct integer_limits + { + static constexpr TOML_INT128 max = + static_cast((TOML_UINT128{ 1u } << ((__SIZEOF_INT128__ * CHAR_BIT) - 1)) - 1); + static constexpr TOML_INT128 min = -max - TOML_INT128{ 1 }; + }; + template <> + struct integer_limits + { + static constexpr TOML_UINT128 min = TOML_UINT128{}; + static constexpr TOML_UINT128 max = (2u * static_cast(integer_limits::max)) + 1u; + }; + template <> + struct value_traits : integer_traits + {}; + template <> + struct value_traits : integer_traits + {}; +#endif +#ifdef TOML_SMALL_INT_TYPE + template <> + struct value_traits : signed_integer_traits + {}; +#endif + + // floating-point traits base + template + struct float_traits_base + { + static constexpr auto type = node_type::floating_point; + using native_type = double; + static constexpr bool is_native = std::is_same_v; + static constexpr bool is_signed = true; + + static constexpr int bits = static_cast(sizeof(T) * CHAR_BIT); + static constexpr int digits = MantissaDigits; + static constexpr int digits10 = DecimalDigits; + + static constexpr bool is_losslessly_convertible_to_native = bits <= 64 // + && digits <= 53 // DBL_MANT_DIG + && digits10 <= 15; // DBL_DIG + + static constexpr bool can_represent_native = digits >= 53 // DBL_MANT_DIG + && digits10 >= 15; // DBL_DIG + + static constexpr bool can_partially_represent_native = digits > 0 && digits10 > 0; + }; + template + struct float_traits : float_traits_base::digits, std::numeric_limits::digits10> + {}; +#if TOML_ENABLE_FLOAT16 + template <> + struct float_traits<_Float16> : float_traits_base<_Float16, __FLT16_MANT_DIG__, __FLT16_DIG__> + {}; +#endif +#ifdef TOML_FLOAT128 + template <> + struct float_traits : float_traits_base + {}; +#endif + + // floating-point traits + template <> + struct value_traits : float_traits + {}; + template <> + struct value_traits : float_traits + {}; + template <> + struct value_traits : float_traits + {}; +#if TOML_ENABLE_FLOAT16 + template <> + struct value_traits<_Float16> : float_traits<_Float16> + {}; +#endif +#ifdef TOML_FLOAT128 + template <> + struct value_traits : float_traits + {}; +#endif +#ifdef TOML_SMALL_FLOAT_TYPE + template <> + struct value_traits : float_traits + {}; +#endif + static_assert(value_traits::is_native); + static_assert(value_traits::is_losslessly_convertible_to_native); + static_assert(value_traits::can_represent_native); + static_assert(value_traits::can_partially_represent_native); + + // string value_traits specializations - char-based strings + template + struct string_traits + { + using native_type = std::string; + static constexpr bool is_native = std::is_same_v; + static constexpr bool is_losslessly_convertible_to_native = true; + static constexpr bool can_represent_native = + !std::is_array_v && (!std::is_pointer_v || std::is_const_v>); + static constexpr bool can_partially_represent_native = can_represent_native; + static constexpr auto type = node_type::string; + }; + template <> + struct value_traits : string_traits + {}; + template <> + struct value_traits : string_traits + {}; + template <> + struct value_traits : string_traits + {}; + template + struct value_traits : string_traits + {}; + template <> + struct value_traits : string_traits + {}; + template + struct value_traits : string_traits + {}; + + // string value_traits specializations - char8_t-based strings +#if TOML_HAS_CHAR8 + template <> + struct value_traits : string_traits + {}; + template <> + struct value_traits : string_traits + {}; + template <> + struct value_traits : string_traits + {}; + template + struct value_traits : string_traits + {}; + template <> + struct value_traits : string_traits + {}; + template + struct value_traits : string_traits + {}; +#endif + + // string value_traits specializations - wchar_t-based strings on Windows +#if TOML_ENABLE_WINDOWS_COMPAT + template + struct wstring_traits + { + using native_type = std::string; + static constexpr bool is_native = false; + static constexpr bool is_losslessly_convertible_to_native = true; // narrow + static constexpr bool can_represent_native = std::is_same_v; // widen + static constexpr bool can_partially_represent_native = can_represent_native; + static constexpr auto type = node_type::string; + }; + template <> + struct value_traits : wstring_traits + {}; + template <> + struct value_traits : wstring_traits + {}; + template <> + struct value_traits : wstring_traits + {}; + template + struct value_traits : wstring_traits + {}; + template <> + struct value_traits : wstring_traits + {}; + template + struct value_traits : wstring_traits + {}; +#endif + + // other 'native' value_traits specializations + template + struct native_value_traits + { + using native_type = T; + static constexpr bool is_native = true; + static constexpr bool is_losslessly_convertible_to_native = true; + static constexpr bool can_represent_native = true; + static constexpr bool can_partially_represent_native = true; + static constexpr auto type = NodeType; + }; + template <> + struct value_traits : native_value_traits + {}; + template <> + struct value_traits : native_value_traits + {}; + template <> + struct value_traits