From 547298ba7e5f9570d361822e1891d5e3d1ff71d3 Mon Sep 17 00:00:00 2001 From: rturrado Date: Fri, 5 Jun 2026 14:05:20 +0200 Subject: [PATCH 01/33] Add qir/jit library and change qir/runner to use it Add qir::jit::Session to encapsulate the LLJIT initialization, QIR runtime symbols registration, and program execution. The idea is to avoid future duplication of JIT setup both in the Runner and in the Device. Update Runner to use qir::jit::Session. --- include/mqt-core/qir/jit/Session.hpp | 51 ++++ src/qir/CMakeLists.txt | 2 + src/qir/jit/CMakeLists.txt | 39 +++ src/qir/jit/Session.cpp | 389 +++++++++++++++++++++++++++ src/qir/runner/CMakeLists.txt | 6 +- src/qir/runner/Runner.cpp | 281 +------------------ 6 files changed, 486 insertions(+), 282 deletions(-) create mode 100644 include/mqt-core/qir/jit/Session.hpp create mode 100644 src/qir/jit/CMakeLists.txt create mode 100644 src/qir/jit/Session.cpp diff --git a/include/mqt-core/qir/jit/Session.hpp b/include/mqt-core/qir/jit/Session.hpp new file mode 100644 index 0000000000..6eea8acead --- /dev/null +++ b/include/mqt-core/qir/jit/Session.hpp @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace qir::jit { + +class Session { +public: + using MainFn = int(int, char**); + + explicit Session(llvm::StringRef inputFile); + Session(llvm::StringRef irBytes, llvm::StringRef bufferName); + ~Session(); + int run(); + int run(llvm::ArrayRef args, llvm::StringRef progName = ""); + +private: + llvm::orc::ThreadSafeContext tsCtx_{std::make_unique()}; + llvm::orc::ThreadSafeModule module_; + std::unique_ptr jit_; + MainFn* mainFn_ = nullptr; + + static void registerRuntimeSymbols(); + static void initNativeTargets(); + llvm::Expected + loadModuleFromFile(llvm::StringRef irPath); + llvm::Expected + loadModuleFromMemory(llvm::StringRef irBytes, llvm::StringRef bufferName); + void initialize(); + void deinitialize(); +}; + +} // namespace qir::jit diff --git a/src/qir/CMakeLists.txt b/src/qir/CMakeLists.txt index 4a8b8ba413..8bb144add1 100644 --- a/src/qir/CMakeLists.txt +++ b/src/qir/CMakeLists.txt @@ -6,6 +6,8 @@ # # Licensed under the MIT License +add_subdirectory(jit) + add_subdirectory(runtime) if(BUILD_MQT_CORE_QIR_RUNNER) diff --git a/src/qir/jit/CMakeLists.txt b/src/qir/jit/CMakeLists.txt new file mode 100644 index 0000000000..2365f3cd2d --- /dev/null +++ b/src/qir/jit/CMakeLists.txt @@ -0,0 +1,39 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-qir-jit) + +if(NOT TARGET ${TARGET_NAME}) + # Add QIRJIT library + add_mqt_core_library(${TARGET_NAME} ALIAS_NAME QIRJIT) + + # Add sources to target + target_sources(${TARGET_NAME} PRIVATE Session.cpp + ${MQT_CORE_INCLUDE_BUILD_DIR}/qir/jit/Session.hpp) + + # Add headers using file sets + target_sources(${TARGET_NAME} PUBLIC FILE_SET HEADERS BASE_DIRS ${MQT_CORE_INCLUDE_BUILD_DIR} + FILES ${MQT_CORE_INCLUDE_BUILD_DIR}/qir/jit/Session.hpp) + + # Get the native target libraries + llvm_map_components_to_libnames( + llvm_native_libs + codegen + core + executionengine + irreader + native + orcdebugging + orcjit + orctargetprocess + support + targetparser) + + # Add link libraries + target_link_libraries(${TARGET_NAME} PUBLIC MQT::CoreQIRRuntime ${llvm_native_libs}) +endif() diff --git a/src/qir/jit/Session.cpp b/src/qir/jit/Session.cpp new file mode 100644 index 0000000000..de9efe45a0 --- /dev/null +++ b/src/qir/jit/Session.cpp @@ -0,0 +1,389 @@ +/* + * 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 "qir/jit/Session.hpp" + +#include "qir/runtime/QIR.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEBUG_TYPE "mqt-core-qir-jit" + +namespace qir::jit { + +static void exitOnLazyCallThroughFailure() { exit(1); } + +static int mingwNoopMain() { + // Cygwin and MinGW insert calls from the main function to the runtime + // function __main. The __main function is responsible for setting up main's + // environment (e.g. running static constructors), however this is not needed + // when running under lli: the executor process will have run non-JIT ctors, + // and ORC will take care of running JIT'd ctors. To avoid a missing symbol + // error we just implement __main as a no-op. + return 0; +} + +// Try to enable debugger support for the given instance. +// This always returns success, but prints a warning if it's not able to enable +// debugger support. +static llvm::Error tryEnableDebugSupport(llvm::orc::LLJIT& jit) { + if (auto err = enableDebuggerSupport(jit)) { + [[maybe_unused]] const std::string errMsg = toString(std::move(err)); + // NOLINTNEXTLINE(cppcoreguidelines-avoid-do-while) + LLVM_DEBUG(llvm::dbgs() << DEBUG_TYPE ": " << errMsg << "\n"); + } + return llvm::Error::success(); +} + +static llvm::Expected +getThreadSafeModuleOrError(std::unique_ptr module, + const llvm::SMDiagnostic& err, + llvm::orc::ThreadSafeContext tsCtx) { + if (!module) { + std::string errMsg; + { + llvm::raw_string_ostream errMsgStream(errMsg); + err.print(DEBUG_TYPE, errMsgStream); + } + return llvm::make_error(std::move(errMsg), + llvm::inconvertibleErrorCode()); + } + return llvm::orc::ThreadSafeModule(std::move(module), std::move(tsCtx)); +} + +llvm::Expected +Session::loadModuleFromFile(const llvm::StringRef irPath) { + llvm::SMDiagnostic err; + auto m = tsCtx_.withContextDo( + [&](llvm::LLVMContext* ctx) { return parseIRFile(irPath, err, *ctx); }); + return getThreadSafeModuleOrError(std::move(m), err, tsCtx_); +} + +llvm::Expected +Session::loadModuleFromMemory(const llvm::StringRef irBytes, + const llvm::StringRef bufferName) { + llvm::SMDiagnostic err; + auto buffer = llvm::MemoryBuffer::getMemBuffer( + irBytes, bufferName, + /*RequiresNullTerminator=*/false); // bitcode isn't null-terminated + auto m = tsCtx_.withContextDo([&](llvm::LLVMContext* ctx) { + return parseIR(buffer->getMemBufferRef(), err, *ctx); + }); + return getThreadSafeModuleOrError(std::move(m), err, tsCtx_); +} + +Session::Session(const llvm::StringRef inputFile) { + auto ret = loadModuleFromFile(inputFile); + if (!ret) { + throw std::runtime_error(llvm::toString(ret.takeError())); + } + module_ = std::move(*ret); + initialize(); +} + +Session::Session(const llvm::StringRef irBytes, + const llvm::StringRef bufferName) { + auto ret = loadModuleFromMemory(irBytes, bufferName); + if (!ret) { + throw std::runtime_error(llvm::toString(ret.takeError())); + } + module_ = std::move(*ret); + initialize(); +} + +Session::~Session() { deinitialize(); } + +int Session::run() { return mainFn_(0, nullptr); } + +int Session::run(llvm::ArrayRef args, llvm::StringRef progName) { + return llvm::orc::runAsMain(mainFn_, args, progName); +} + +namespace { +std::vector> manualSymbols; +} // namespace + +#define REGISTER_SYMBOL(name) \ + llvm::sys::DynamicLibrary::AddSymbol(#name, \ + reinterpret_cast(&(name))); \ + manualSymbols.emplace_back(#name, reinterpret_cast(&(name))); + +void Session::registerRuntimeSymbols() { + static std::once_flag flag; + std::call_once(flag, []() { + REGISTER_SYMBOL(__quantum__rt__result_get_zero); + REGISTER_SYMBOL(__quantum__rt__result_get_one); + REGISTER_SYMBOL(__quantum__rt__result_equal); + REGISTER_SYMBOL(__quantum__rt__result_update_reference_count); + REGISTER_SYMBOL(__quantum__rt__array_create_1d); + REGISTER_SYMBOL(__quantum__rt__array_get_size_1d); + REGISTER_SYMBOL(__quantum__rt__array_get_element_ptr_1d); + REGISTER_SYMBOL(__quantum__rt__array_update_reference_count); + REGISTER_SYMBOL(__quantum__rt__qubit_allocate); + REGISTER_SYMBOL(__quantum__rt__qubit_allocate_array); + REGISTER_SYMBOL(__quantum__rt__qubit_release); + REGISTER_SYMBOL(__quantum__rt__qubit_release_array); + REGISTER_SYMBOL(__quantum__qis__x__body); + REGISTER_SYMBOL(__quantum__qis__y__body); + REGISTER_SYMBOL(__quantum__qis__z__body); + REGISTER_SYMBOL(__quantum__qis__h__body); + REGISTER_SYMBOL(__quantum__qis__s__body); + REGISTER_SYMBOL(__quantum__qis__sdg__body); + REGISTER_SYMBOL(__quantum__qis__sx__body); + REGISTER_SYMBOL(__quantum__qis__sxdg__body); + REGISTER_SYMBOL(__quantum__qis__sqrtx__body); + REGISTER_SYMBOL(__quantum__qis__sqrtxdg__body); + REGISTER_SYMBOL(__quantum__qis__t__body); + REGISTER_SYMBOL(__quantum__qis__tdg__body); + REGISTER_SYMBOL(__quantum__qis__r__body); + REGISTER_SYMBOL(__quantum__qis__prx__body); + REGISTER_SYMBOL(__quantum__qis__rx__body); + REGISTER_SYMBOL(__quantum__qis__ry__body); + REGISTER_SYMBOL(__quantum__qis__rz__body); + REGISTER_SYMBOL(__quantum__qis__p__body); + REGISTER_SYMBOL(__quantum__qis__rxx__body); + REGISTER_SYMBOL(__quantum__qis__ryy__body); + REGISTER_SYMBOL(__quantum__qis__rzz__body); + REGISTER_SYMBOL(__quantum__qis__rzx__body); + REGISTER_SYMBOL(__quantum__qis__u__body); + REGISTER_SYMBOL(__quantum__qis__u3__body); + REGISTER_SYMBOL(__quantum__qis__u2__body); + REGISTER_SYMBOL(__quantum__qis__u1__body); + REGISTER_SYMBOL(__quantum__qis__cu1__body); + REGISTER_SYMBOL(__quantum__qis__cu3__body); + REGISTER_SYMBOL(__quantum__qis__cnot__body); + REGISTER_SYMBOL(__quantum__qis__cx__body); + REGISTER_SYMBOL(__quantum__qis__cy__body); + REGISTER_SYMBOL(__quantum__qis__cz__body); + REGISTER_SYMBOL(__quantum__qis__ch__body); + REGISTER_SYMBOL(__quantum__qis__swap__body); + REGISTER_SYMBOL(__quantum__qis__cswap__body); + REGISTER_SYMBOL(__quantum__qis__crz__body); + REGISTER_SYMBOL(__quantum__qis__cry__body); + REGISTER_SYMBOL(__quantum__qis__crx__body); + REGISTER_SYMBOL(__quantum__qis__cp__body); + REGISTER_SYMBOL(__quantum__qis__ccx__body); + REGISTER_SYMBOL(__quantum__qis__ccy__body); + REGISTER_SYMBOL(__quantum__qis__ccz__body); + REGISTER_SYMBOL(__quantum__qis__m__body); + REGISTER_SYMBOL(__quantum__qis__measure__body); + REGISTER_SYMBOL(__quantum__qis__mz__body); + REGISTER_SYMBOL(__quantum__qis__reset__body); + REGISTER_SYMBOL(__quantum__rt__initialize); + REGISTER_SYMBOL(__quantum__rt__read_result); + REGISTER_SYMBOL(__quantum__rt__result_record_output); + }); +} + +#undef REGISTER_SYMBOL + +void Session::initNativeTargets() { + static std::once_flag flag; + std::call_once(flag, []() { + // If we have a native target, initialize it to ensure it is linked in and + // usable by the JIT. + llvm::InitializeNativeTarget(); + llvm::InitializeNativeTargetAsmPrinter(); + llvm::InitializeNativeTargetAsmParser(); + }); +} + +void Session::initialize() { + registerRuntimeSymbols(); + initNativeTargets(); + + // Get TargetTriple and DataLayout from the main module if they're explicitly + // set. + std::optional tt; + std::optional dl; + module_.withModuleDo([&](llvm::Module& m) { + if (!m.getTargetTriple().empty()) { + tt = m.getTargetTriple(); + } + if (!m.getDataLayout().isDefault()) { + dl = m.getDataLayout(); + } + }); + + // Configure the lazy JIT builder. + llvm::orc::LLLazyJITBuilder builder; + + // Use the module's target triple if set, otherwise detect the host's. + auto host = llvm::orc::JITTargetMachineBuilder::detectHost(); + if (!host) { + throw std::runtime_error(llvm::toString(host.takeError())); + } + builder.setJITTargetMachineBuilder( + tt ? llvm::orc::JITTargetMachineBuilder(*tt) : *host); + + // Cache the resolved triple; apply the module's explicit data layout if any. + tt = builder.getJITTargetMachineBuilder()->getTargetTriple(); + if (dl) { + builder.setDataLayout(dl); + } + + // Optional architecture override from the -march codegen flag. + if (!llvm::codegen::getMArch().empty()) { + builder.getJITTargetMachineBuilder()->getTargetTriple().setArchName( + llvm::codegen::getMArch()); + } + + // Apply CPU, features, relocation model, and code model from codegen flags. + builder.getJITTargetMachineBuilder() + ->setCPU(llvm::codegen::getCPUStr()) + .addFeatures(llvm::codegen::getFeatureList()) + .setRelocationModel(llvm::codegen::getExplicitRelocModel()) + .setCodeModel(llvm::codegen::getExplicitCodeModel()); + + // Link process symbols. + builder.setLinkProcessSymbolsByDefault(true); + + // Set up the in-process execution session and lazy call-through manager. + auto pc = llvm::orc::SelfExecutorProcessControl::Create(); + if (!pc) { + throw std::runtime_error(llvm::toString(pc.takeError())); + } + auto es = std::make_unique(std::move(*pc)); + builder.setLazyCallthroughManager( + std::make_unique( + *es, llvm::orc::ExecutorAddr(), nullptr)); + builder.setExecutionSession(std::move(es)); + + // Abort on lazy compilation failure. + builder.setLazyCompileFailureAddr( + llvm::orc::ExecutorAddr::fromPtr(exitOnLazyCallThroughFailure)); + + // Enable debugging of JIT'd code (only works on JITLink for ELF and MachO). + builder.setPrePlatformSetup(tryEnableDebugSupport); + + // Build the JIT. + auto expectedJit = builder.create(); + if (!expectedJit) { + throw std::runtime_error(llvm::toString(expectedJit.takeError())); + } + jit_ = std::move(*expectedJit); + + // Register QIR runtime symbols. + auto& jd = jit_->getMainJITDylib(); + llvm::orc::SymbolMap hostSymbols; + for (const auto& [name, ptr] : manualSymbols) { + hostSymbols[jit_->mangleAndIntern(name)] = { + llvm::orc::ExecutorAddr::fromPtr(ptr), llvm::JITSymbolFlags::Exported}; + } + if (auto err = jd.define(llvm::orc::absoluteSymbols(hostSymbols))) { + throw std::runtime_error(llvm::toString(std::move(err))); + } + + // DynamicLibrarySearchGenerator + auto gen = llvm::orc::DynamicLibrarySearchGenerator::GetForCurrentProcess( + jit_->getDataLayout().getGlobalPrefix()); + if (!gen) { + throw std::runtime_error(llvm::toString(gen.takeError())); + } + jit_->getMainJITDylib().addGenerator(std::move(*gen)); + + // GDB listener (no error path) + auto* objLayer = &jit_->getObjLinkingLayer(); + if (auto* rtDyldObjLayer = + dyn_cast(objLayer)) { + rtDyldObjLayer->registerJITEventListener( + *llvm::JITEventListener::createGDBRegistrationListener()); + } + + // If this is a Mingw or Cygwin executor then we need to alias __main to + // orc_rt_int_void_return_0. + if (jit_->getTargetTriple().isOSCygMing()) { + auto& workaroundJD = jit_->getProcessSymbolsJITDylib() + ? *jit_->getProcessSymbolsJITDylib() + : jit_->getMainJITDylib(); + if (auto err = workaroundJD.define(llvm::orc::absoluteSymbols( + {{jit_->mangleAndIntern("__main"), + {llvm::orc::ExecutorAddr::fromPtr(mingwNoopMain), + llvm::JITSymbolFlags::Exported}}}))) { + throw std::runtime_error(llvm::toString(std::move(err))); + } + } + + // Regular modules are greedy: They materialize as a whole and trigger + // materialization for all required symbols recursively. Lazy modules go + // through partitioning, and they replace outgoing calls with reexport stubs + // that resolve on call-through. + auto addModule = [&](llvm::orc::JITDylib& jdlib, + llvm::orc::ThreadSafeModule m) { + return jit_->addIRModule(jdlib, std::move(m)); + }; + + // Add the main module. + if (auto err = addModule(jit_->getMainJITDylib(), std::move(module_))) { + throw std::runtime_error(llvm::toString(std::move(err))); + } + + // Run any static constructors. + if (auto err = jit_->initialize(jit_->getMainJITDylib())) { + throw std::runtime_error(llvm::toString(std::move(err))); + } + + // Resolve the main function. + auto mainAddr = jit_->lookup("main"); + if (!mainAddr) { + throw std::runtime_error(llvm::toString(mainAddr.takeError())); + } + mainFn_ = mainAddr->toPtr(); +} + +void Session::deinitialize() { + if (!jit_) { + return; + } + if (auto err = jit_->deinitialize(jit_->getMainJITDylib())) { + llvm::errs() << "Session deinitialize failed: " + << llvm::toString(std::move(err)) << "\n"; + } +} + +} // namespace qir::jit diff --git a/src/qir/runner/CMakeLists.txt b/src/qir/runner/CMakeLists.txt index ea9564b7fa..4577bbfa0a 100644 --- a/src/qir/runner/CMakeLists.txt +++ b/src/qir/runner/CMakeLists.txt @@ -11,12 +11,8 @@ set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-qir-runner) if(NOT TARGET ${TARGET_NAME}) add_llvm_tool(${TARGET_NAME} Runner.cpp DEPENDS intrinsics_gen EXPORT_SYMBOLS) - # Get the native target libraries - llvm_map_components_to_libnames(llvm_native_libs native) - # Add link libraries - target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQIRRuntime LLVMOrcDebugging - ${llvm_native_libs}) + target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQIRJIT) # Set versioning information set_target_properties(${TARGET_NAME} PROPERTIES VERSION ${PROJECT_VERSION} EXPORT_NAME diff --git a/src/qir/runner/Runner.cpp b/src/qir/runner/Runner.cpp index 9335c0f590..b270200d91 100644 --- a/src/qir/runner/Runner.cpp +++ b/src/qir/runner/Runner.cpp @@ -8,48 +8,15 @@ * Licensed under the MIT License */ -#include "qir/runtime/QIR.h" +#include "qir/jit/Session.hpp" -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include #include #include -#include -#include -#include -#include -#include -#include -#include #include #include -#include -#include #define DEBUG_TYPE "mqt-core-qir-runner" @@ -64,255 +31,15 @@ static llvm::cl::list static llvm::ExitOnError ExitOnError; -static void exitOnLazyCallThroughFailure() { exit(1); } - -static llvm::Expected -loadModule(const llvm::StringRef path, llvm::orc::ThreadSafeContext tsCtx) { - llvm::SMDiagnostic err; - auto m = tsCtx.withContextDo( - [&](llvm::LLVMContext* ctx) { return parseIRFile(path, err, *ctx); }); - if (!m) { - std::string errMsg; - { - llvm::raw_string_ostream errMsgStream(errMsg); - err.print(DEBUG_TYPE, errMsgStream); - } - return llvm::make_error(std::move(errMsg), - llvm::inconvertibleErrorCode()); - } - - return llvm::orc::ThreadSafeModule(std::move(m), std::move(tsCtx)); -} - -static int mingwNoopMain() { - // Cygwin and MinGW insert calls from the main function to the runtime - // function __main. The __main function is responsible for setting up main's - // environment (e.g. running static constructors), however this is not needed - // when running under lli: the executor process will have run non-JIT ctors, - // and ORC will take care of running JIT'd ctors. To avoid a missing symbol - // error we just implement __main as a no-op. - return 0; -} - -// Try to enable debugger support for the given instance. -// This always returns success, but prints a warning if it's not able to enable -// debugger support. -static llvm::Error tryEnableDebugSupport(llvm::orc::LLJIT& jit) { - if (auto err = enableDebuggerSupport(jit)) { - [[maybe_unused]] const std::string errMsg = toString(std::move(err)); - // NOLINTNEXTLINE(cppcoreguidelines-avoid-do-while) - LLVM_DEBUG(llvm::dbgs() << DEBUG_TYPE ": " << errMsg << "\n"); - } - return llvm::Error::success(); -} - -static std::vector> manualSymbols; - -static int runOrcJIT() { - // Start setting up the JIT environment. - - // Parse the main module. - const llvm::orc::ThreadSafeContext tsCtx( - std::make_unique()); - auto mainModule = ExitOnError(loadModule(InputFile, tsCtx)); - - // Get TargetTriple and DataLayout from the main module if they're explicitly - // set. - std::optional tt; - std::optional dl; - mainModule.withModuleDo([&](llvm::Module& m) { - if (!m.getTargetTriple().empty()) { - tt = m.getTargetTriple(); - } - if (!m.getDataLayout().isDefault()) { - dl = m.getDataLayout(); - } - }); - - llvm::orc::LLLazyJITBuilder builder; - - builder.setJITTargetMachineBuilder( - tt ? llvm::orc::JITTargetMachineBuilder(*tt) - : ExitOnError(llvm::orc::JITTargetMachineBuilder::detectHost())); - - tt = builder.getJITTargetMachineBuilder()->getTargetTriple(); - if (dl) { - builder.setDataLayout(dl); - } - - if (!llvm::codegen::getMArch().empty()) { - builder.getJITTargetMachineBuilder()->getTargetTriple().setArchName( - llvm::codegen::getMArch()); - } - - builder.getJITTargetMachineBuilder() - ->setCPU(llvm::codegen::getCPUStr()) - .addFeatures(llvm::codegen::getFeatureList()) - .setRelocationModel(llvm::codegen::getExplicitRelocModel()) - .setCodeModel(llvm::codegen::getExplicitCodeModel()); - - // Link process symbols. - builder.setLinkProcessSymbolsByDefault(true); - - auto es = std::make_unique( - ExitOnError(llvm::orc::SelfExecutorProcessControl::Create())); - builder.setLazyCallthroughManager( - std::make_unique( - *es, llvm::orc::ExecutorAddr(), nullptr)); - builder.setExecutionSession(std::move(es)); - - builder.setLazyCompileFailureAddr( - llvm::orc::ExecutorAddr::fromPtr(exitOnLazyCallThroughFailure)); - - // Enable debugging of JIT'd code (only works on JITLink for ELF and MachO). - builder.setPrePlatformSetup(tryEnableDebugSupport); - - const auto jit = ExitOnError(builder.create()); - - auto& jd = jit->getMainJITDylib(); - llvm::orc::SymbolMap hostSymbols; - for (const auto& [name, ptr] : manualSymbols) { - hostSymbols[jit->mangleAndIntern(name)] = { - llvm::orc::ExecutorAddr::fromPtr(ptr), llvm::JITSymbolFlags::Exported}; - } - ExitOnError(jd.define(llvm::orc::absoluteSymbols(hostSymbols))); - - jit->getMainJITDylib().addGenerator(ExitOnError( - llvm::orc::DynamicLibrarySearchGenerator::GetForCurrentProcess( - jit->getDataLayout().getGlobalPrefix()))); - - auto* objLayer = &jit->getObjLinkingLayer(); - if (auto* rtDyldObjLayer = - dyn_cast(objLayer)) { - rtDyldObjLayer->registerJITEventListener( - *llvm::JITEventListener::createGDBRegistrationListener()); - } - - // If this is a Mingw or Cygwin executor then we need to alias __main to - // orc_rt_int_void_return_0. - if (jit->getTargetTriple().isOSCygMing()) { - auto& workaroundJD = jit->getProcessSymbolsJITDylib() - ? *jit->getProcessSymbolsJITDylib() - : jit->getMainJITDylib(); - ExitOnError(workaroundJD.define(llvm::orc::absoluteSymbols( - {{jit->mangleAndIntern("__main"), - {llvm::orc::ExecutorAddr::fromPtr(mingwNoopMain), - llvm::JITSymbolFlags::Exported}}}))); - } - - // Regular modules are greedy: They materialize as a whole and trigger - // materialization for all required symbols recursively. Lazy modules go - // through partitioning and they replace outgoing calls with reexport stubs - // that resolve on call-through. - auto addModule = [&](llvm::orc::JITDylib& jd, llvm::orc::ThreadSafeModule m) { - return jit->addIRModule(jd, std::move(m)); - }; - - // Add the main module. - ExitOnError(addModule(jit->getMainJITDylib(), std::move(mainModule))); - - // Run any static constructors. - ExitOnError(jit->initialize(jit->getMainJITDylib())); - - // Resolve and run the main function. - const auto mainAddr = ExitOnError(jit->lookup("main")); - - // Manual in-process execution with RuntimeDyld. - using mainFnTy = int(int, char**); - auto mainFn = mainAddr.toPtr(); - const int result = - llvm::orc::runAsMain(mainFn, InputArgv, llvm::StringRef(InputFile)); - - // Run destructors. - ExitOnError(jit->deinitialize(jit->getMainJITDylib())); - - return result; -} - -#define REGISTER_SYMBOL(name) \ - llvm::sys::DynamicLibrary::AddSymbol(#name, \ - reinterpret_cast(&(name))); \ - manualSymbols.emplace_back(#name, reinterpret_cast(&(name))); - auto main(int argc, char* argv[]) -> int { const llvm::InitLLVM session(argc, argv); - if (const std::span args(argv, argc); args.size() > 1) { ExitOnError.setBanner(std::string(args[0]) + ": "); } - - // If we have a native target, initialize it to ensure it is linked in and - // usable by the JIT. - llvm::InitializeNativeTarget(); - llvm::InitializeNativeTargetAsmPrinter(); - llvm::InitializeNativeTargetAsmParser(); - llvm::cl::ParseCommandLineOptions(argc, argv, "qir interpreter & dynamic compiler\n"); - REGISTER_SYMBOL(__quantum__rt__result_get_zero); - REGISTER_SYMBOL(__quantum__rt__result_get_one); - REGISTER_SYMBOL(__quantum__rt__result_equal); - REGISTER_SYMBOL(__quantum__rt__result_update_reference_count); - REGISTER_SYMBOL(__quantum__rt__array_create_1d); - REGISTER_SYMBOL(__quantum__rt__array_get_size_1d); - REGISTER_SYMBOL(__quantum__rt__array_get_element_ptr_1d); - REGISTER_SYMBOL(__quantum__rt__array_update_reference_count); - REGISTER_SYMBOL(__quantum__rt__qubit_allocate); - REGISTER_SYMBOL(__quantum__rt__qubit_allocate_array); - REGISTER_SYMBOL(__quantum__rt__qubit_release); - REGISTER_SYMBOL(__quantum__rt__qubit_release_array); - REGISTER_SYMBOL(__quantum__qis__x__body); - REGISTER_SYMBOL(__quantum__qis__y__body); - REGISTER_SYMBOL(__quantum__qis__z__body); - REGISTER_SYMBOL(__quantum__qis__h__body); - REGISTER_SYMBOL(__quantum__qis__s__body); - REGISTER_SYMBOL(__quantum__qis__sdg__body); - REGISTER_SYMBOL(__quantum__qis__sx__body); - REGISTER_SYMBOL(__quantum__qis__sxdg__body); - REGISTER_SYMBOL(__quantum__qis__sqrtx__body); - REGISTER_SYMBOL(__quantum__qis__sqrtxdg__body); - REGISTER_SYMBOL(__quantum__qis__t__body); - REGISTER_SYMBOL(__quantum__qis__tdg__body); - REGISTER_SYMBOL(__quantum__qis__r__body); - REGISTER_SYMBOL(__quantum__qis__prx__body); - REGISTER_SYMBOL(__quantum__qis__rx__body); - REGISTER_SYMBOL(__quantum__qis__ry__body); - REGISTER_SYMBOL(__quantum__qis__rz__body); - REGISTER_SYMBOL(__quantum__qis__p__body); - REGISTER_SYMBOL(__quantum__qis__rxx__body); - REGISTER_SYMBOL(__quantum__qis__ryy__body); - REGISTER_SYMBOL(__quantum__qis__rzz__body); - REGISTER_SYMBOL(__quantum__qis__rzx__body); - REGISTER_SYMBOL(__quantum__qis__u__body); - REGISTER_SYMBOL(__quantum__qis__u3__body); - REGISTER_SYMBOL(__quantum__qis__u2__body); - REGISTER_SYMBOL(__quantum__qis__u1__body); - REGISTER_SYMBOL(__quantum__qis__cu1__body); - REGISTER_SYMBOL(__quantum__qis__cu3__body); - REGISTER_SYMBOL(__quantum__qis__cnot__body); - REGISTER_SYMBOL(__quantum__qis__cx__body); - REGISTER_SYMBOL(__quantum__qis__cy__body); - REGISTER_SYMBOL(__quantum__qis__cz__body); - REGISTER_SYMBOL(__quantum__qis__ch__body); - REGISTER_SYMBOL(__quantum__qis__swap__body); - REGISTER_SYMBOL(__quantum__qis__cswap__body); - REGISTER_SYMBOL(__quantum__qis__crz__body); - REGISTER_SYMBOL(__quantum__qis__cry__body); - REGISTER_SYMBOL(__quantum__qis__crx__body); - REGISTER_SYMBOL(__quantum__qis__cp__body); - REGISTER_SYMBOL(__quantum__qis__ccx__body); - REGISTER_SYMBOL(__quantum__qis__ccy__body); - REGISTER_SYMBOL(__quantum__qis__ccz__body); - REGISTER_SYMBOL(__quantum__qis__m__body); - REGISTER_SYMBOL(__quantum__qis__measure__body); - REGISTER_SYMBOL(__quantum__qis__mz__body); - REGISTER_SYMBOL(__quantum__qis__reset__body); - REGISTER_SYMBOL(__quantum__rt__initialize); - REGISTER_SYMBOL(__quantum__rt__read_result); - REGISTER_SYMBOL(__quantum__rt__result_record_output); - - return runOrcJIT(); + // Manual in-process execution with RuntimeDyld. + auto jitSession = qir::jit::Session(llvm::StringRef(InputFile)); + return jitSession.run(InputArgv, InputFile); } - -#undef REGISTER_SYMBOL From 40349947226a81686b2e0893292c1f7308754bbb Mon Sep 17 00:00:00 2001 From: rturrado Date: Fri, 5 Jun 2026 14:05:45 +0200 Subject: [PATCH 02/33] Fix typo in comment in src/qir/runtime/CMakeLists.txt --- src/qir/runtime/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qir/runtime/CMakeLists.txt b/src/qir/runtime/CMakeLists.txt index aef86c8b01..84f65ae266 100644 --- a/src/qir/runtime/CMakeLists.txt +++ b/src/qir/runtime/CMakeLists.txt @@ -9,7 +9,7 @@ set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-qir-runtime) if(NOT TARGET ${TARGET_NAME}) - # Add QIRBackend library + # Add QIRRuntime library add_mqt_core_library(${TARGET_NAME} ALIAS_NAME QIRRuntime) # Add sources to target From d1428e57fdfdb54a30f79e79b342a854323c74b8 Mon Sep 17 00:00:00 2001 From: rturrado Date: Fri, 5 Jun 2026 20:02:32 +0200 Subject: [PATCH 03/33] Add QIR program format support to the QDMI DDSim device circuits.hpp, error_handling_test.cpp: add QIR_BELL_PAIR_STATIC with an LLVM assembly string of a Bell pair program, and rename MALFORMED_PROGRAM to QASM3_MALFORMED (to be consistent with the other QASM3_ variables). src/qdmi/devices/dd/CMakeLists.txt: add MQT::CoreQIRJIT library dependency. src/qir/jit/CMakeLists.txt: link libraries privately. test/qdmi/devices/dd/CMakeLists.txt: add LLVM native libs dependencies. Device.hpp: add submitQASMProgram and submitQIRProgram APIs. Device.cpp: handle job submissions of a QASM program and of a QIR base module/string separately. Implement submitQIRProgram: for numShots_, reset the runtime, run the program, process results into a bit string, update measurement counts. Device.cpp, test_utils.cpp: the device stores the program into a std::string of a given size, and submitQIRProgram reads exactly that size. No passing +1 bytes or sometimes retrieving -1 bytes is needed in any case. job_parameters_test.cpp: set QDMI_PROGRAM_FORMAT_QIRBASEMODULE and QDMI_PROGRAM_FORMAT_QIRBASESTRING as supported formats. results_sampling_test.cpp: change HistogramKeysAndValuesSumToShots into a test class, and implement two new test fixtures for QIR base modules and strings. These two new tests exercise the submission of a QIR program for a DD device job. Runtime: add getResults API. Session: make run methods const. --- include/mqt-core/qdmi/devices/dd/Device.hpp | 6 ++ include/mqt-core/qir/jit/Session.hpp | 5 +- include/mqt-core/qir/runtime/Runtime.hpp | 2 + src/qdmi/devices/dd/CMakeLists.txt | 11 ++- src/qdmi/devices/dd/Device.cpp | 94 +++++++++++++++++-- src/qir/jit/CMakeLists.txt | 4 +- src/qir/jit/Session.cpp | 7 +- src/qir/runtime/Runtime.cpp | 4 + test/qdmi/devices/dd/CMakeLists.txt | 6 ++ test/qdmi/devices/dd/error_handling_test.cpp | 4 +- test/qdmi/devices/dd/helpers/circuits.hpp | 45 ++++++++- test/qdmi/devices/dd/helpers/test_utils.cpp | 3 +- test/qdmi/devices/dd/job_parameters_test.cpp | 10 +- .../qdmi/devices/dd/results_sampling_test.cpp | 66 ++++++++++--- 14 files changed, 226 insertions(+), 41 deletions(-) diff --git a/include/mqt-core/qdmi/devices/dd/Device.hpp b/include/mqt-core/qdmi/devices/dd/Device.hpp index d6e4350f13..c17efc89af 100644 --- a/include/mqt-core/qdmi/devices/dd/Device.hpp +++ b/include/mqt-core/qdmi/devices/dd/Device.hpp @@ -234,6 +234,12 @@ struct MQT_DDSIM_QDMI_Device_Job_impl_d { auto getProbabilities(size_t size, void* data, size_t* sizeRet) -> QDMI_STATUS; + /// Helper function to submit a QASM 2 or QASM 3 program + auto submitQASMProgram() -> QDMI_STATUS; + + /// Helper function to submit a QIR base module or string program + auto submitQIRProgram() -> QDMI_STATUS; + public: /// Constructor for the MQT_DDSIM_QDMI_Device_Job_impl_d. explicit MQT_DDSIM_QDMI_Device_Job_impl_d( diff --git a/include/mqt-core/qir/jit/Session.hpp b/include/mqt-core/qir/jit/Session.hpp index 6eea8acead..6725e6feb6 100644 --- a/include/mqt-core/qir/jit/Session.hpp +++ b/include/mqt-core/qir/jit/Session.hpp @@ -29,8 +29,9 @@ class Session { explicit Session(llvm::StringRef inputFile); Session(llvm::StringRef irBytes, llvm::StringRef bufferName); ~Session(); - int run(); - int run(llvm::ArrayRef args, llvm::StringRef progName = ""); + int run() const; + int run(llvm::ArrayRef args, + llvm::StringRef progName = "") const; private: llvm::orc::ThreadSafeContext tsCtx_{std::make_unique()}; diff --git a/include/mqt-core/qir/runtime/Runtime.hpp b/include/mqt-core/qir/runtime/Runtime.hpp index 009b3d66dd..bb51937db8 100644 --- a/include/mqt-core/qir/runtime/Runtime.hpp +++ b/include/mqt-core/qir/runtime/Runtime.hpp @@ -356,5 +356,7 @@ class Runtime { auto deref(Result* result) -> ResultStruct&; auto rFree(Result* result) -> void; auto equal(Result* result1, Result* result2) -> bool; + + auto getResults() const -> std::unordered_map; }; } // namespace qir diff --git a/src/qdmi/devices/dd/CMakeLists.txt b/src/qdmi/devices/dd/CMakeLists.txt index bfebf8a92a..f6a9d3ca04 100644 --- a/src/qdmi/devices/dd/CMakeLists.txt +++ b/src/qdmi/devices/dd/CMakeLists.txt @@ -38,8 +38,15 @@ if(NOT TARGET ${TARGET_NAME}) ${QDMI_HDRS}) # Add link libraries - target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreDD MQT::CoreQASM MQT::CoreCircuitOptimizer - MQT::CoreQDMICommon spdlog::spdlog) + target_link_libraries( + ${TARGET_NAME} + PRIVATE MQT::CoreDD + MQT::CoreQASM + MQT::CoreCircuitOptimizer + MQT::CoreQDMICommon + MQT::CoreQIRJIT + MQT::CoreQIRRuntime + spdlog::spdlog) # Make QDMI version available and ensure symbols are exported when building the library target_compile_definitions(${TARGET_NAME} PRIVATE QDMI_VERSION="${QDMI_VERSION}" diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index 4fc828b3e4..daf2f7144a 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -23,6 +23,11 @@ #include "mqt_ddsim_qdmi/device.h" #include "qasm3/Importer.hpp" #include "qdmi/common/Common.hpp" +#include "qir/jit/Session.hpp" +#include "qir/runtime/QIR.h" +#include "qir/runtime/Runtime.hpp" + +#include #include #include @@ -36,11 +41,14 @@ #include #include #include +#include #include +#include #include #include #include #include +#include #include #include @@ -346,7 +354,9 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::setParameter( return QDMI_ERROR_INVALIDARGUMENT; } if (format != QDMI_PROGRAM_FORMAT_QASM2 && - format != QDMI_PROGRAM_FORMAT_QASM3) { + format != QDMI_PROGRAM_FORMAT_QASM3 && + format != QDMI_PROGRAM_FORMAT_QIRBASEMODULE && + format != QDMI_PROGRAM_FORMAT_QIRBASESTRING) { return QDMI_ERROR_NOTSUPPORTED; } format_ = format; @@ -354,7 +364,7 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::setParameter( return QDMI_SUCCESS; case QDMI_DEVICE_JOB_PARAMETER_PROGRAM: if (value != nullptr) { - program_ = std::string(static_cast(value), size - 1); + program_ = std::string(static_cast(value), size); } return QDMI_SUCCESS; case QDMI_DEVICE_JOB_PARAMETER_SHOTSNUM: @@ -386,11 +396,7 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::queryProperty( numShots_, prop, size, value, sizeRet) return QDMI_ERROR_NOTSUPPORTED; } -auto MQT_DDSIM_QDMI_Device_Job_impl_d::submit() -> QDMI_STATUS { - if (status_.load() != QDMI_JOB_STATUS_CREATED) { - return QDMI_ERROR_BADSTATE; - } - status_.store(QDMI_JOB_STATUS_SUBMITTED); +auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQASMProgram() -> QDMI_STATUS { if (numShots_ > 0) { jobHandle_ = std::async(std::launch::async, [this]() { qdmi::dd::Device::get().increaseRunningJobs(); @@ -407,9 +413,9 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::submit() -> QDMI_STATUS { }); } else { jobHandle_ = std::async(std::launch::async, [this]() { + qdmi::dd::Device::get().increaseRunningJobs(); + status_.store(QDMI_JOB_STATUS_RUNNING); try { - qdmi::dd::Device::get().increaseRunningJobs(); - status_.store(QDMI_JOB_STATUS_RUNNING); auto qc = qasm3::Importer::imports(program_); qc::CircuitOptimizer::removeFinalMeasurements(qc); const auto nqubits = qc.getNqubits(); @@ -425,6 +431,76 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::submit() -> QDMI_STATUS { } return QDMI_SUCCESS; } +auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQIRProgram() -> QDMI_STATUS { + if (numShots_ == 0) { + return QDMI_ERROR_INVALIDARGUMENT; + } + jobHandle_ = std::async(std::launch::async, [this]() { + qdmi::dd::Device::get().increaseRunningJobs(); + status_.store(QDMI_JOB_STATUS_RUNNING); + try { + auto& runtime = qir::Runtime::getInstance(); + auto irBytes = llvm::StringRef(program_.data(), program_.size()); + auto jitSession = qir::jit::Session(irBytes, "QDMI job"); + for (size_t i = 0; i < numShots_; ++i) { + runtime.reset(); + if (const auto rc = jitSession.run(); rc != 0) { + throw std::runtime_error( + llvm::formatv("QIR program failed with error: {}", rc)); + } + + auto addressIsNotZeroOrOne = [](Result* resultPtr) { + const auto addr = reinterpret_cast(resultPtr); + return addr != qir::Runtime::RESULT_ZERO_ADDRESS && + addr != qir::Runtime::RESULT_ONE_ADDRESS; + }; + const auto results = runtime.getResults(); + // Filter results with addresses 0 and 1 out. + // And keep the boolean value from ResultStrut only, not the ref count. + auto&& resultsView = + results | + std::views::filter([addressIsNotZeroOrOne](const auto& result) { + return addressIsNotZeroOrOne(result.first); + }) | + std::views::transform([](const auto& result) { + return std::pair{result.first, result.second.r}; + }); + // Order the results by address. + const std::map orderedResults(resultsView.begin(), + resultsView.end()); + // Build a bit string from the ordered results. + std::string bitString; + bitString.reserve(orderedResults.size()); + std::ranges::transform( + orderedResults, std::back_inserter(bitString), + [](const auto& kv) { return kv.second ? '1' : '0'; }); + // Update the measurement counts. + ++counts_[bitString]; + } + status_.store(QDMI_JOB_STATUS_DONE); + } catch (const std::exception& e) { + status_.store(QDMI_JOB_STATUS_FAILED); + std::cerr << "Error: " << e.what() << '\n'; + } + qdmi::dd::Device::get().decreaseRunningJobs(); + }); + return QDMI_SUCCESS; +} +auto MQT_DDSIM_QDMI_Device_Job_impl_d::submit() -> QDMI_STATUS { + if (status_.load() != QDMI_JOB_STATUS_CREATED) { + return QDMI_ERROR_BADSTATE; + } + status_.store(QDMI_JOB_STATUS_SUBMITTED); + if (format_ == QDMI_PROGRAM_FORMAT_QASM2 || + format_ == QDMI_PROGRAM_FORMAT_QASM3) { + return submitQASMProgram(); + } + if (format_ == QDMI_PROGRAM_FORMAT_QIRBASEMODULE || + format_ == QDMI_PROGRAM_FORMAT_QIRBASESTRING) { + return submitQIRProgram(); + } + return QDMI_SUCCESS; +} auto MQT_DDSIM_QDMI_Device_Job_impl_d::cancel() -> QDMI_STATUS { const auto s = status_.load(); if (s == QDMI_JOB_STATUS_DONE || s == QDMI_JOB_STATUS_FAILED) { diff --git a/src/qir/jit/CMakeLists.txt b/src/qir/jit/CMakeLists.txt index 2365f3cd2d..8f9f92d7d8 100644 --- a/src/qir/jit/CMakeLists.txt +++ b/src/qir/jit/CMakeLists.txt @@ -20,7 +20,7 @@ if(NOT TARGET ${TARGET_NAME}) target_sources(${TARGET_NAME} PUBLIC FILE_SET HEADERS BASE_DIRS ${MQT_CORE_INCLUDE_BUILD_DIR} FILES ${MQT_CORE_INCLUDE_BUILD_DIR}/qir/jit/Session.hpp) - # Get the native target libraries + # Get the LLVM native target libraries llvm_map_components_to_libnames( llvm_native_libs codegen @@ -35,5 +35,5 @@ if(NOT TARGET ${TARGET_NAME}) targetparser) # Add link libraries - target_link_libraries(${TARGET_NAME} PUBLIC MQT::CoreQIRRuntime ${llvm_native_libs}) + target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQIRRuntime ${llvm_native_libs}) endif() diff --git a/src/qir/jit/Session.cpp b/src/qir/jit/Session.cpp index de9efe45a0..e3ef8e18b6 100644 --- a/src/qir/jit/Session.cpp +++ b/src/qir/jit/Session.cpp @@ -138,9 +138,10 @@ Session::Session(const llvm::StringRef irBytes, Session::~Session() { deinitialize(); } -int Session::run() { return mainFn_(0, nullptr); } +int Session::run() const { return mainFn_(0, nullptr); } -int Session::run(llvm::ArrayRef args, llvm::StringRef progName) { +int Session::run(llvm::ArrayRef args, + llvm::StringRef progName) const { return llvm::orc::runAsMain(mainFn_, args, progName); } @@ -225,6 +226,8 @@ void Session::registerRuntimeSymbols() { void Session::initNativeTargets() { static std::once_flag flag; std::call_once(flag, []() { + static const llvm::codegen::RegisterCodeGenFlags CGF; + // If we have a native target, initialize it to ensure it is linked in and // usable by the JIT. llvm::InitializeNativeTarget(); diff --git a/src/qir/runtime/Runtime.cpp b/src/qir/runtime/Runtime.cpp index 0ca6a70c6b..82e919d1f0 100644 --- a/src/qir/runtime/Runtime.cpp +++ b/src/qir/runtime/Runtime.cpp @@ -164,4 +164,8 @@ auto Runtime::equal(Result* result1, Result* result2) -> bool { return deref(result1).r == deref(result2).r; } +auto Runtime::getResults() const -> std::unordered_map { + return rRegister; +} + } // namespace qir diff --git a/test/qdmi/devices/dd/CMakeLists.txt b/test/qdmi/devices/dd/CMakeLists.txt index a2e19861d1..da7759aff5 100644 --- a/test/qdmi/devices/dd/CMakeLists.txt +++ b/test/qdmi/devices/dd/CMakeLists.txt @@ -33,6 +33,12 @@ if(TARGET MQT::CoreQDMI_DDSIM_Device) target_compile_definitions(${TARGET_NAME} PRIVATE MQT_CORE_VERSION="${MQT_CORE_VERSION}") target_include_directories(${TARGET_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + # Get the LLVM native target libraries + llvm_map_components_to_libnames(llvm_native_libs asmparser bitwriter core support) + + # Add link libraries + target_link_libraries(${TARGET_NAME} PRIVATE ${llvm_native_libs}) + # On Windows, we need to copy the DLL to the test executable directory if(WIN32) add_custom_command( diff --git a/test/qdmi/devices/dd/error_handling_test.cpp b/test/qdmi/devices/dd/error_handling_test.cpp index 199d42234f..b2766c051a 100644 --- a/test/qdmi/devices/dd/error_handling_test.cpp +++ b/test/qdmi/devices/dd/error_handling_test.cpp @@ -294,7 +294,7 @@ TEST_F(ErrorHandling, MalformedProgramFailsForBothModes) { { const qdmi_test::JobGuard j{s.session}; ASSERT_EQ(qdmi_test::setProgram(j.job, QDMI_PROGRAM_FORMAT_QASM3, - qdmi_test::MALFORMED_PROGRAM), + qdmi_test::QASM3_MALFORMED), QDMI_SUCCESS); ASSERT_EQ(qdmi_test::setShots(j.job, 128), QDMI_SUCCESS); ASSERT_EQ(qdmi_test::submitAndWait(j.job, 0), QDMI_SUCCESS); @@ -306,7 +306,7 @@ TEST_F(ErrorHandling, MalformedProgramFailsForBothModes) { { const qdmi_test::JobGuard j{s.session}; ASSERT_EQ(qdmi_test::setProgram(j.job, QDMI_PROGRAM_FORMAT_QASM3, - qdmi_test::MALFORMED_PROGRAM), + qdmi_test::QASM3_MALFORMED), QDMI_SUCCESS); ASSERT_EQ(qdmi_test::setShots(j.job, 0), QDMI_SUCCESS); ASSERT_EQ(qdmi_test::submitAndWait(j.job, 0), QDMI_SUCCESS); diff --git a/test/qdmi/devices/dd/helpers/circuits.hpp b/test/qdmi/devices/dd/helpers/circuits.hpp index 16a4d2af63..ec13f9f49b 100644 --- a/test/qdmi/devices/dd/helpers/circuits.hpp +++ b/test/qdmi/devices/dd/helpers/circuits.hpp @@ -30,7 +30,7 @@ h q[0]; cx q[0], q[1]; )"; -inline constexpr const char* MALFORMED_PROGRAM = "Definitely not OpenQASM"; +inline constexpr const char* QASM3_MALFORMED = "Definitely not OpenQASM"; // A slightly heavier 5-qubit sampling circuit to prolong runtime slightly while // remaining fast @@ -60,4 +60,47 @@ cx q[0], q[1]; c = measure q; )"; +inline constexpr auto QIR_BELL_PAIR_STATIC = R"( +; ModuleID = 'bell' +source_filename = "bell" + +%Qubit = type opaque +%Result = type opaque + +@0 = internal constant [3 x i8] c"r0\00" +@1 = internal constant [3 x i8] c"r1\00" + +define i32 @main() #0 { +entry: + call void @__quantum__rt__initialize(i8* null) + call void @__quantum__qis__h__body(%Qubit* null) + call void @__quantum__qis__cnot__body(%Qubit* null, %Qubit* inttoptr (i64 1 to %Qubit*)) + call void @__quantum__qis__mz__body(%Qubit* null, %Result* null) + call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 1 to %Result*)) + call void @__quantum__rt__result_record_output(%Result* null, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @0, i32 0, i32 0)) + call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 1 to %Result*), i8* getelementptr inbounds ([3 x i8], [3 x i8]* @1, i32 0, i32 0)) + ret i32 0 +} + +declare void @__quantum__qis__h__body(%Qubit*) + +declare void @__quantum__qis__cnot__body(%Qubit*, %Qubit*) + +declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly) #1 + +declare void @__quantum__rt__initialize(i8*) + +declare void @__quantum__rt__result_record_output(%Result*, i8*) + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="2" "required_num_results"="2" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} +)"; + } // namespace qdmi_test diff --git a/test/qdmi/devices/dd/helpers/test_utils.cpp b/test/qdmi/devices/dd/helpers/test_utils.cpp index f17e6b63df..527ad306f6 100644 --- a/test/qdmi/devices/dd/helpers/test_utils.cpp +++ b/test/qdmi/devices/dd/helpers/test_utils.cpp @@ -98,8 +98,7 @@ int setProgram(MQT_DDSIM_QDMI_Device_Job job, const QDMI_Program_Format fmt, return rc; } rc = MQT_DDSIM_QDMI_device_job_set_parameter( - job, QDMI_DEVICE_JOB_PARAMETER_PROGRAM, program.size() + 1, - program.data()); + job, QDMI_DEVICE_JOB_PARAMETER_PROGRAM, program.size(), program.data()); return rc; } diff --git a/test/qdmi/devices/dd/job_parameters_test.cpp b/test/qdmi/devices/dd/job_parameters_test.cpp index 6cfe4b1214..672a36ac6f 100644 --- a/test/qdmi/devices/dd/job_parameters_test.cpp +++ b/test/qdmi/devices/dd/job_parameters_test.cpp @@ -90,8 +90,12 @@ TEST(JobParameters, ProgramFormatSupport) { const qdmi_test::JobGuard j{s.session}; // Supported - for (QDMI_Program_Format fmt : - {QDMI_PROGRAM_FORMAT_QASM2, QDMI_PROGRAM_FORMAT_QASM3}) { + for (QDMI_Program_Format fmt : { + QDMI_PROGRAM_FORMAT_QASM2, + QDMI_PROGRAM_FORMAT_QASM3, + QDMI_PROGRAM_FORMAT_QIRBASEMODULE, + QDMI_PROGRAM_FORMAT_QIRBASESTRING, + }) { EXPECT_EQ(MQT_DDSIM_QDMI_device_job_set_parameter( j.job, QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT, sizeof(QDMI_Program_Format), &fmt), @@ -100,8 +104,6 @@ TEST(JobParameters, ProgramFormatSupport) { // Unsupported → NOTSUPPORTED for (QDMI_Program_Format fmt : { - QDMI_PROGRAM_FORMAT_QIRBASESTRING, - QDMI_PROGRAM_FORMAT_QIRBASEMODULE, QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING, QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE, QDMI_PROGRAM_FORMAT_CALIBRATION, diff --git a/test/qdmi/devices/dd/results_sampling_test.cpp b/test/qdmi/devices/dd/results_sampling_test.cpp index 8d329f6c16..4b1c134619 100644 --- a/test/qdmi/devices/dd/results_sampling_test.cpp +++ b/test/qdmi/devices/dd/results_sampling_test.cpp @@ -13,31 +13,67 @@ */ #include "helpers/circuits.hpp" #include "helpers/test_utils.hpp" +#include "llvm/AsmParser/Parser.h" +#include "llvm/Bitcode/BitcodeWriter.h" #include "mqt_ddsim_qdmi/constants.h" #include "mqt_ddsim_qdmi/device.h" #include +#include +#include +#include #include +#include +#include +#include #include -TEST(ResultsSampling, HistogramKeysAndValuesSumToShots) { - const qdmi_test::SessionGuard s{}; - const qdmi_test::JobGuard j{s.session}; - ASSERT_EQ(qdmi_test::setProgram(j.job, QDMI_PROGRAM_FORMAT_QASM3, - qdmi_test::QASM3_BELL_SAMPLING), - QDMI_SUCCESS); - constexpr size_t shots = 1024; - ASSERT_EQ(qdmi_test::setShots(j.job, shots), QDMI_SUCCESS); - ASSERT_EQ(qdmi_test::submitAndWait(j.job, 0), QDMI_SUCCESS); +class HistogramKeysAndValuesSumToShots : public ::testing::Test { +protected: + static void Run(QDMI_Program_Format format, std::string_view program) { + const qdmi_test::SessionGuard s{}; + const qdmi_test::JobGuard j{s.session}; + ASSERT_EQ(qdmi_test::setProgram(j.job, format, program), QDMI_SUCCESS); + constexpr size_t shots = 1024; + ASSERT_EQ(qdmi_test::setShots(j.job, shots), QDMI_SUCCESS); + ASSERT_EQ(qdmi_test::submitAndWait(j.job, 0), QDMI_SUCCESS); - auto [keys, vals] = qdmi_test::getHistogram(j.job); - ASSERT_EQ(keys.size(), vals.size()); - size_t sum = 0U; - for (const auto& v : vals) { - sum += v; + auto [keys, vals] = qdmi_test::getHistogram(j.job); + ASSERT_EQ(keys.size(), vals.size()); + size_t sum = 0U; + for (const auto& v : vals) { + sum += v; + } + EXPECT_EQ(sum, shots); } - EXPECT_EQ(sum, shots); +}; + +TEST_F(HistogramKeysAndValuesSumToShots, QASM3Program) { + const QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QASM3; + const std::string_view program = qdmi_test::QASM3_BELL_SAMPLING; + Run(format, program); +} + +TEST_F(HistogramKeysAndValuesSumToShots, QIRBaseModule) { + const QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QIRBASEMODULE; + const std::string_view program = qdmi_test::QIR_BELL_PAIR_STATIC; + llvm::LLVMContext context; + llvm::SMDiagnostic err; + auto module = llvm::parseAssemblyString(program, err, context); + ASSERT_NE(module, nullptr) + << "parseAssemblyString failed: " << err.getMessage().str(); + std::string bitcodeBuffer; + llvm::raw_string_ostream os(bitcodeBuffer); + llvm::WriteBitcodeToFile(*module, os); + os.flush(); + Run(format, bitcodeBuffer); +} + +TEST_F(HistogramKeysAndValuesSumToShots, QIRBaseString) { + const QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QIRBASESTRING; + const std::string_view program = qdmi_test::QIR_BELL_PAIR_STATIC; + Run(format, program); } TEST(ResultsSampling, BufferTooSmallErrors) { From a5280fdda123be8bb177eeca5182a729ae033d74 Mon Sep 17 00:00:00 2001 From: rturrado Date: Fri, 5 Jun 2026 21:16:27 +0200 Subject: [PATCH 04/33] Add BUILD_MQT_CORE_QDMI_WITH_QIR option This option enables the QIR format support for QDMI. It is defined in the top level CMakeLists.txt, and it is disabled by default. The QDMI DDSim Device library only links to QIR JIT and QIR Runtime libraries if the option is ON. The same applies to the corresponding test library. --- CMakeLists.txt | 2 ++ include/mqt-core/qdmi/devices/dd/Device.hpp | 2 ++ src/qdmi/devices/dd/CMakeLists.txt | 15 ++++---- src/qdmi/devices/dd/Device.cpp | 35 ++++++++++++------- test/qdmi/devices/dd/CMakeLists.txt | 10 +++--- test/qdmi/devices/dd/job_parameters_test.cpp | 6 ++++ .../qdmi/devices/dd/results_sampling_test.cpp | 14 +++++--- 7 files changed, 54 insertions(+), 30 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4c1a0eee53..a930f32b0f 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,6 +121,8 @@ endif() cmake_dependent_option(BUILD_MQT_CORE_QIR_RUNNER "Build the QIR runner of the MQT Core project" ON "BUILD_MQT_CORE_MLIR" OFF) +option(BUILD_MQT_CORE_QDMI_WITH_QIR "Enable QIR format support for QDMI" OFF) + # add main library code add_subdirectory(src) diff --git a/include/mqt-core/qdmi/devices/dd/Device.hpp b/include/mqt-core/qdmi/devices/dd/Device.hpp index c17efc89af..f4155f3ea1 100644 --- a/include/mqt-core/qdmi/devices/dd/Device.hpp +++ b/include/mqt-core/qdmi/devices/dd/Device.hpp @@ -237,8 +237,10 @@ struct MQT_DDSIM_QDMI_Device_Job_impl_d { /// Helper function to submit a QASM 2 or QASM 3 program auto submitQASMProgram() -> QDMI_STATUS; +#ifdef BUILD_MQT_CORE_QDMI_WITH_QIR /// Helper function to submit a QIR base module or string program auto submitQIRProgram() -> QDMI_STATUS; +#endif public: /// Constructor for the MQT_DDSIM_QDMI_Device_Job_impl_d. diff --git a/src/qdmi/devices/dd/CMakeLists.txt b/src/qdmi/devices/dd/CMakeLists.txt index f6a9d3ca04..58735b0696 100644 --- a/src/qdmi/devices/dd/CMakeLists.txt +++ b/src/qdmi/devices/dd/CMakeLists.txt @@ -38,15 +38,12 @@ if(NOT TARGET ${TARGET_NAME}) ${QDMI_HDRS}) # Add link libraries - target_link_libraries( - ${TARGET_NAME} - PRIVATE MQT::CoreDD - MQT::CoreQASM - MQT::CoreCircuitOptimizer - MQT::CoreQDMICommon - MQT::CoreQIRJIT - MQT::CoreQIRRuntime - spdlog::spdlog) + target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreDD MQT::CoreQASM MQT::CoreCircuitOptimizer + MQT::CoreQDMICommon spdlog::spdlog) + if(BUILD_MQT_CORE_QDMI_WITH_QIR) + target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQIRJIT MQT::CoreQIRRuntime) + target_compile_definitions(${TARGET_NAME} PRIVATE BUILD_MQT_CORE_QDMI_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}" diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index daf2f7144a..7b2375c827 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -23,11 +23,6 @@ #include "mqt_ddsim_qdmi/device.h" #include "qasm3/Importer.hpp" #include "qdmi/common/Common.hpp" -#include "qir/jit/Session.hpp" -#include "qir/runtime/QIR.h" -#include "qir/runtime/Runtime.hpp" - -#include #include #include @@ -41,17 +36,26 @@ #include #include #include -#include #include -#include #include #include #include #include -#include #include #include +#ifdef BUILD_MQT_CORE_QDMI_WITH_QIR +#include "qir/jit/Session.hpp" +#include "qir/runtime/QIR.h" +#include "qir/runtime/Runtime.hpp" + +#include + +#include +#include +#include +#endif + namespace { constexpr uintptr_t OFFSET = 0x10000U; template constexpr std::array iotaArray() { @@ -354,9 +358,12 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::setParameter( return QDMI_ERROR_INVALIDARGUMENT; } if (format != QDMI_PROGRAM_FORMAT_QASM2 && - format != QDMI_PROGRAM_FORMAT_QASM3 && - format != QDMI_PROGRAM_FORMAT_QIRBASEMODULE && - format != QDMI_PROGRAM_FORMAT_QIRBASESTRING) { + format != QDMI_PROGRAM_FORMAT_QASM3 +#ifdef BUILD_MQT_CORE_QDMI_WITH_QIR + && format != QDMI_PROGRAM_FORMAT_QIRBASEMODULE && + format != QDMI_PROGRAM_FORMAT_QIRBASESTRING +#endif + ) { return QDMI_ERROR_NOTSUPPORTED; } format_ = format; @@ -431,6 +438,7 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQASMProgram() -> QDMI_STATUS { } return QDMI_SUCCESS; } +#ifdef BUILD_MQT_CORE_QDMI_WITH_QIR auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQIRProgram() -> QDMI_STATUS { if (numShots_ == 0) { return QDMI_ERROR_INVALIDARGUMENT; @@ -486,6 +494,7 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQIRProgram() -> QDMI_STATUS { }); return QDMI_SUCCESS; } +#endif auto MQT_DDSIM_QDMI_Device_Job_impl_d::submit() -> QDMI_STATUS { if (status_.load() != QDMI_JOB_STATUS_CREATED) { return QDMI_ERROR_BADSTATE; @@ -495,11 +504,13 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::submit() -> QDMI_STATUS { format_ == QDMI_PROGRAM_FORMAT_QASM3) { return submitQASMProgram(); } +#ifdef BUILD_MQT_CORE_QDMI_WITH_QIR if (format_ == QDMI_PROGRAM_FORMAT_QIRBASEMODULE || format_ == QDMI_PROGRAM_FORMAT_QIRBASESTRING) { return submitQIRProgram(); } - return QDMI_SUCCESS; +#endif + return QDMI_ERROR_NOTSUPPORTED; } auto MQT_DDSIM_QDMI_Device_Job_impl_d::cancel() -> QDMI_STATUS { const auto s = status_.load(); diff --git a/test/qdmi/devices/dd/CMakeLists.txt b/test/qdmi/devices/dd/CMakeLists.txt index da7759aff5..ea59d65be8 100644 --- a/test/qdmi/devices/dd/CMakeLists.txt +++ b/test/qdmi/devices/dd/CMakeLists.txt @@ -33,11 +33,11 @@ if(TARGET MQT::CoreQDMI_DDSIM_Device) target_compile_definitions(${TARGET_NAME} PRIVATE MQT_CORE_VERSION="${MQT_CORE_VERSION}") target_include_directories(${TARGET_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - # Get the LLVM native target libraries - llvm_map_components_to_libnames(llvm_native_libs asmparser bitwriter core support) - - # Add link libraries - target_link_libraries(${TARGET_NAME} PRIVATE ${llvm_native_libs}) + if(BUILD_MQT_CORE_QDMI_WITH_QIR) + llvm_map_components_to_libnames(llvm_native_libs asmparser bitwriter core support) + target_link_libraries(${TARGET_NAME} PRIVATE ${llvm_native_libs}) + target_compile_definitions(${TARGET_NAME} PRIVATE BUILD_MQT_CORE_QDMI_WITH_QIR) + endif() # On Windows, we need to copy the DLL to the test executable directory if(WIN32) diff --git a/test/qdmi/devices/dd/job_parameters_test.cpp b/test/qdmi/devices/dd/job_parameters_test.cpp index 672a36ac6f..67452ff0d1 100644 --- a/test/qdmi/devices/dd/job_parameters_test.cpp +++ b/test/qdmi/devices/dd/job_parameters_test.cpp @@ -93,8 +93,10 @@ TEST(JobParameters, ProgramFormatSupport) { for (QDMI_Program_Format fmt : { QDMI_PROGRAM_FORMAT_QASM2, QDMI_PROGRAM_FORMAT_QASM3, +#ifdef BUILD_MQT_CORE_QDMI_WITH_QIR QDMI_PROGRAM_FORMAT_QIRBASEMODULE, QDMI_PROGRAM_FORMAT_QIRBASESTRING, +#endif }) { EXPECT_EQ(MQT_DDSIM_QDMI_device_job_set_parameter( j.job, QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT, @@ -106,6 +108,10 @@ TEST(JobParameters, ProgramFormatSupport) { for (QDMI_Program_Format fmt : { QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING, QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE, +#ifndef BUILD_MQT_CORE_QDMI_WITH_QIR + QDMI_PROGRAM_FORMAT_QIRBASEMODULE, + QDMI_PROGRAM_FORMAT_QIRBASESTRING, +#endif QDMI_PROGRAM_FORMAT_CALIBRATION, QDMI_PROGRAM_FORMAT_QPY, QDMI_PROGRAM_FORMAT_IQMJSON, diff --git a/test/qdmi/devices/dd/results_sampling_test.cpp b/test/qdmi/devices/dd/results_sampling_test.cpp index 4b1c134619..02cc9109ff 100644 --- a/test/qdmi/devices/dd/results_sampling_test.cpp +++ b/test/qdmi/devices/dd/results_sampling_test.cpp @@ -13,21 +13,25 @@ */ #include "helpers/circuits.hpp" #include "helpers/test_utils.hpp" -#include "llvm/AsmParser/Parser.h" -#include "llvm/Bitcode/BitcodeWriter.h" #include "mqt_ddsim_qdmi/constants.h" #include "mqt_ddsim_qdmi/device.h" #include + +#include +#include + +#ifdef BUILD_MQT_CORE_QDMI_WITH_QIR +#include +#include #include #include #include -#include #include #include #include -#include +#endif class HistogramKeysAndValuesSumToShots : public ::testing::Test { protected: @@ -55,6 +59,7 @@ TEST_F(HistogramKeysAndValuesSumToShots, QASM3Program) { Run(format, program); } +#ifdef BUILD_MQT_CORE_QDMI_WITH_QIR TEST_F(HistogramKeysAndValuesSumToShots, QIRBaseModule) { const QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QIRBASEMODULE; const std::string_view program = qdmi_test::QIR_BELL_PAIR_STATIC; @@ -75,6 +80,7 @@ TEST_F(HistogramKeysAndValuesSumToShots, QIRBaseString) { const std::string_view program = qdmi_test::QIR_BELL_PAIR_STATIC; Run(format, program); } +#endif TEST(ResultsSampling, BufferTooSmallErrors) { const qdmi_test::SessionGuard s{}; From 9422a389869eea488d0d84aff6d7e813ae9fdc0b Mon Sep 17 00:00:00 2001 From: rturrado Date: Fri, 5 Jun 2026 21:28:22 +0200 Subject: [PATCH 05/33] Update docs/qir/index.md Add a 'QIR Support in the QDMI Device' entry. Assisted-by: Claude Opus 4.7 via Claude Code --- docs/qir/index.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/qir/index.md b/docs/qir/index.md index 4e09d4acdb..f86863d848 100644 --- a/docs/qir/index.md +++ b/docs/qir/index.md @@ -36,3 +36,9 @@ The `mqt-core-qir-runner` can be used to execute a QIR file (typically with a `. This will simulate the circuit and print the measurement results to the console. The runner supports the QIR Base Profile. + +### QIR Support in the QDMI Device + +The QDMI Device accepts jobs in the following program formats: QASM2, QASM3, QIR base module (LLVM bitcode), and QIR base string (LLVM assembly). +The QIR base formats are only supported when the `BUILD_MQT_CORE_QDMI_WITH_QIR` CMake option is enabled. +It is disabled by default to avoid the cost of linking against the MQT Core QIR JIT (built on LLVM OrcJIT) and Runtime libraries. From ac958b796ac5a3dc1e0cc489137a9424ba40d17e Mon Sep 17 00:00:00 2001 From: rturrado Date: Fri, 5 Jun 2026 23:11:56 +0200 Subject: [PATCH 06/33] Clean up Session::run Leave just one function where both arguments have default values. --- include/mqt-core/qir/jit/Session.hpp | 3 +-- src/qir/jit/Session.cpp | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/include/mqt-core/qir/jit/Session.hpp b/include/mqt-core/qir/jit/Session.hpp index 6725e6feb6..dc68de0e1b 100644 --- a/include/mqt-core/qir/jit/Session.hpp +++ b/include/mqt-core/qir/jit/Session.hpp @@ -29,8 +29,7 @@ class Session { explicit Session(llvm::StringRef inputFile); Session(llvm::StringRef irBytes, llvm::StringRef bufferName); ~Session(); - int run() const; - int run(llvm::ArrayRef args, + int run(llvm::ArrayRef args = {}, llvm::StringRef progName = "") const; private: diff --git a/src/qir/jit/Session.cpp b/src/qir/jit/Session.cpp index e3ef8e18b6..e67fc34aea 100644 --- a/src/qir/jit/Session.cpp +++ b/src/qir/jit/Session.cpp @@ -138,8 +138,6 @@ Session::Session(const llvm::StringRef irBytes, Session::~Session() { deinitialize(); } -int Session::run() const { return mainFn_(0, nullptr); } - int Session::run(llvm::ArrayRef args, llvm::StringRef progName) const { return llvm::orc::runAsMain(mainFn_, args, progName); From a20336b00f8a2e8d5be2590e20f354c49dd6a9d2 Mon Sep 17 00:00:00 2001 From: rturrado Date: Fri, 5 Jun 2026 23:43:09 +0200 Subject: [PATCH 07/33] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dad0d3abf3..913f962ae1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ This project adheres to [Semantic Versioning], with the exception that minor rel - ✨ Add initial infrastructure for new QC and QCO MLIR dialects ([#1264], [#1330], [#1402], [#1428], [#1430], [#1436], [#1443], [#1446], [#1464], [#1465], [#1470], [#1471], [#1472], [#1474], [#1475], [#1506], [#1510], [#1513], [#1521], [#1542], [#1548], [#1550], [#1554], [#1567], [#1569], [#1570], [#1572], [#1573], [#1580], [#1602], [#1620], [#1623], [#1624], [#1626], [#1627], [#1635], [#1638], [#1673], [#1675], [#1700], [#1717], [#1728], [#1730], [#1749]) ([**@burgholzer**], [**@denialhaag**], [**@taminob**], [**@DRovara**], [**@li-mingbao**], [**@Ectras**], [**@MatthiasReumann**], [**@simon1hofmann**]) +- ✨ Add QIR program format support to the QDMI DDSim device ([#1766]) ([**@rturrado**]) ### Changed @@ -402,6 +403,7 @@ _📚 Refer to the [GitHub Release Notes](https://github.com/munich-quantum-tool +[#1766]: https://github.com/munich-quantum-toolkit/core/pull/1766 [#1749]: https://github.com/munich-quantum-toolkit/core/pull/1749 [#1748]: https://github.com/munich-quantum-toolkit/core/pull/1748 [#1737]: https://github.com/munich-quantum-toolkit/core/pull/1737 @@ -649,6 +651,7 @@ _📚 Refer to the [GitHub Release Notes](https://github.com/munich-quantum-tool [**@simon1hofmann**]: https://github.com/simon1hofmann [**@keefehuang**]: https://github.com/keefehuang [**@J4MMlE**]: https://github.com/J4MMlE +[**@rturrado**]: https://github.com/rturrado From 095cca1e96052680ad468db14ea7416e1a1f4817 Mon Sep 17 00:00:00 2001 From: rturrado Date: Fri, 5 Jun 2026 23:50:38 +0200 Subject: [PATCH 08/33] Fix CMake configure error when MLIR is disabled When BUILD_MQT_CORE_MLIR=OFF , find_package(MLIR) is not called, so LLVM CMake macros like llvm_map_components_to_libnames are undefined. The new qir/jit subdirectory used that macro unconditionally and failed to configure. Make BUILD_MQT_CORE_QDMI_WITH_QIR a cmake_dependent_option on BUILD_MQT_CORE_MLIR (forced OFF when MLIR is off). Add the qir/jit subdirectory only when at least one consumer (Runner CLI or QDMI-with-QIR) is enabled. Assisted-by: Claude Opus 4.7 via Claude Code --- CMakeLists.txt | 3 ++- src/qir/CMakeLists.txt | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a930f32b0f..1661d57247 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,7 +121,8 @@ endif() cmake_dependent_option(BUILD_MQT_CORE_QIR_RUNNER "Build the QIR runner of the MQT Core project" ON "BUILD_MQT_CORE_MLIR" OFF) -option(BUILD_MQT_CORE_QDMI_WITH_QIR "Enable QIR format support for QDMI" OFF) +cmake_dependent_option(BUILD_MQT_CORE_QDMI_WITH_QIR "Enable QIR format support for QDMI" OFF + "BUILD_MQT_CORE_MLIR" OFF) # add main library code add_subdirectory(src) diff --git a/src/qir/CMakeLists.txt b/src/qir/CMakeLists.txt index 8bb144add1..7b948218ec 100644 --- a/src/qir/CMakeLists.txt +++ b/src/qir/CMakeLists.txt @@ -6,7 +6,9 @@ # # Licensed under the MIT License -add_subdirectory(jit) +if(BUILD_MQT_CORE_QIR_RUNNER OR BUILD_MQT_CORE_QDMI_WITH_QIR) + add_subdirectory(jit) +endif() add_subdirectory(runtime) From 1108209adbfc324417448acc6bd01b1ea3640dcc Mon Sep 17 00:00:00 2001 From: rturrado Date: Sat, 6 Jun 2026 00:00:36 +0200 Subject: [PATCH 09/33] Wrap test fixture in anonymous namespace The HistogramKeysAndValuesSumToShots fixture class in results_sampling_test.cpp was at file scope, triggering clang-tidy's misc-use-anonymous-namespace check in CI (clang-tidy 22). Wrap it in an anonymous namespace to enforce internal linkage, matching the convention used by the ErrorHandling fixture in error_handling_test.cpp. Assisted-by: Claude Opus 4.7 via Claude Code --- test/qdmi/devices/dd/results_sampling_test.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/qdmi/devices/dd/results_sampling_test.cpp b/test/qdmi/devices/dd/results_sampling_test.cpp index 02cc9109ff..711e5a9c20 100644 --- a/test/qdmi/devices/dd/results_sampling_test.cpp +++ b/test/qdmi/devices/dd/results_sampling_test.cpp @@ -33,6 +33,8 @@ #include #endif +namespace { + class HistogramKeysAndValuesSumToShots : public ::testing::Test { protected: static void Run(QDMI_Program_Format format, std::string_view program) { @@ -53,6 +55,8 @@ class HistogramKeysAndValuesSumToShots : public ::testing::Test { } }; +} // namespace + TEST_F(HistogramKeysAndValuesSumToShots, QASM3Program) { const QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QASM3; const std::string_view program = qdmi_test::QASM3_BELL_SAMPLING; From 596b6b30bba656b09536ef2161aea4a0726b9c13 Mon Sep 17 00:00:00 2001 From: rturrado Date: Sat, 6 Jun 2026 00:17:21 +0200 Subject: [PATCH 10/33] Fix CHANGELOG.md Remove merge conflict strings. --- CHANGELOG.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e954bcdba..2d7c7a3d19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -403,12 +403,9 @@ _📚 Refer to the [GitHub Release Notes](https://github.com/munich-quantum-tool -<<<<<<< 1695 [#1766]: https://github.com/munich-quantum-toolkit/core/pull/1766 -======= [#1765]: https://github.com/munich-quantum-toolkit/core/pull/1765 [#1762]: https://github.com/munich-quantum-toolkit/core/pull/1762 ->>>>>>> main [#1749]: https://github.com/munich-quantum-toolkit/core/pull/1749 [#1748]: https://github.com/munich-quantum-toolkit/core/pull/1748 [#1737]: https://github.com/munich-quantum-toolkit/core/pull/1737 From 71ecf0dfd2fa8a6e11b0878473877e94016c05e0 Mon Sep 17 00:00:00 2001 From: rturrado Date: Sat, 6 Jun 2026 22:16:29 +0200 Subject: [PATCH 11/33] Try/catch JIT session code in the Runner Also move comment from Runner's main into Session::run. --- src/qir/jit/Session.cpp | 1 + src/qir/runner/Runner.cpp | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/qir/jit/Session.cpp b/src/qir/jit/Session.cpp index e67fc34aea..ef15e61867 100644 --- a/src/qir/jit/Session.cpp +++ b/src/qir/jit/Session.cpp @@ -140,6 +140,7 @@ Session::~Session() { deinitialize(); } int Session::run(llvm::ArrayRef args, llvm::StringRef progName) const { + // Manual in-process execution with RuntimeDyld. return llvm::orc::runAsMain(mainFn_, args, progName); } diff --git a/src/qir/runner/Runner.cpp b/src/qir/runner/Runner.cpp index b270200d91..aaeb91bbf8 100644 --- a/src/qir/runner/Runner.cpp +++ b/src/qir/runner/Runner.cpp @@ -39,7 +39,10 @@ auto main(int argc, char* argv[]) -> int { llvm::cl::ParseCommandLineOptions(argc, argv, "qir interpreter & dynamic compiler\n"); - // Manual in-process execution with RuntimeDyld. - auto jitSession = qir::jit::Session(llvm::StringRef(InputFile)); - return jitSession.run(InputArgv, InputFile); + try { + auto jitSession = qir::jit::Session(llvm::StringRef(InputFile)); + return jitSession.run(InputArgv, InputFile); + } catch (const std::exception& e) { + ExitOnError(llvm::createStringError(e.what())); + } } From 05e5ee57a347a64d6c91e0ddb296fb65bac9036d Mon Sep 17 00:00:00 2001 From: rturrado Date: Sat, 6 Jun 2026 22:54:23 +0200 Subject: [PATCH 12/33] Refactor submitQIRProgram and Runtime::getResults Move filtering and sorting of Result pointers to getResults. Add qir::toBitString to convert a map to a bit string. Assisted-by: Claude Opus 4.7 via Claude Code --- include/mqt-core/qir/runtime/Runtime.hpp | 10 ++++++- src/qdmi/devices/dd/Device.cpp | 31 +--------------------- src/qir/runtime/Runtime.cpp | 33 ++++++++++++++++++++++-- 3 files changed, 41 insertions(+), 33 deletions(-) diff --git a/include/mqt-core/qir/runtime/Runtime.hpp b/include/mqt-core/qir/runtime/Runtime.hpp index bb51937db8..bea7612d47 100644 --- a/include/mqt-core/qir/runtime/Runtime.hpp +++ b/include/mqt-core/qir/runtime/Runtime.hpp @@ -24,11 +24,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -49,6 +51,7 @@ struct ArrayImpl { }; namespace qir { + // Primary template template static constexpr bool IS_STD_ARRAY_V = false; // Specialization for std::array @@ -183,6 +186,7 @@ class Utils { return array; } }; + /** * @note This class is implemented following the design pattern Singleton in * order to access an instance of this class from the C function without having @@ -357,6 +361,10 @@ class Runtime { auto rFree(Result* result) -> void; auto equal(Result* result1, Result* result2) -> bool; - auto getResults() const -> std::unordered_map; + auto getResults() const -> std::map; }; + +/// Build a bit string from a list of measurement results. +std::string toBitString(const std::map& results); + } // namespace qir diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index 7b2375c827..98ea6518ce 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -46,13 +46,10 @@ #ifdef BUILD_MQT_CORE_QDMI_WITH_QIR #include "qir/jit/Session.hpp" -#include "qir/runtime/QIR.h" #include "qir/runtime/Runtime.hpp" #include -#include -#include #include #endif @@ -456,34 +453,8 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQIRProgram() -> QDMI_STATUS { throw std::runtime_error( llvm::formatv("QIR program failed with error: {}", rc)); } - - auto addressIsNotZeroOrOne = [](Result* resultPtr) { - const auto addr = reinterpret_cast(resultPtr); - return addr != qir::Runtime::RESULT_ZERO_ADDRESS && - addr != qir::Runtime::RESULT_ONE_ADDRESS; - }; - const auto results = runtime.getResults(); - // Filter results with addresses 0 and 1 out. - // And keep the boolean value from ResultStrut only, not the ref count. - auto&& resultsView = - results | - std::views::filter([addressIsNotZeroOrOne](const auto& result) { - return addressIsNotZeroOrOne(result.first); - }) | - std::views::transform([](const auto& result) { - return std::pair{result.first, result.second.r}; - }); - // Order the results by address. - const std::map orderedResults(resultsView.begin(), - resultsView.end()); - // Build a bit string from the ordered results. - std::string bitString; - bitString.reserve(orderedResults.size()); - std::ranges::transform( - orderedResults, std::back_inserter(bitString), - [](const auto& kv) { return kv.second ? '1' : '0'; }); // Update the measurement counts. - ++counts_[bitString]; + ++counts_[qir::toBitString(runtime.getResults())]; } status_.store(QDMI_JOB_STATUS_DONE); } catch (const std::exception& e) { diff --git a/src/qir/runtime/Runtime.cpp b/src/qir/runtime/Runtime.cpp index 82e919d1f0..d7a31b329a 100644 --- a/src/qir/runtime/Runtime.cpp +++ b/src/qir/runtime/Runtime.cpp @@ -21,11 +21,15 @@ #include #include #include +#include +#include #include #include #include +#include #include #include +#include #include #include #include @@ -164,8 +168,33 @@ auto Runtime::equal(Result* result1, Result* result2) -> bool { return deref(result1).r == deref(result2).r; } -auto Runtime::getResults() const -> std::unordered_map { - return rRegister; +auto Runtime::getResults() const -> std::map { + auto addressIsNotZeroOrOne = [](Result* resultPtr) { + const auto addr = reinterpret_cast(resultPtr); + return addr != RESULT_ZERO_ADDRESS && addr != RESULT_ONE_ADDRESS; + }; + // Filter results with addresses 0 and 1 out. + // And keep the boolean value from ResultStruct only, not the ref count. + auto&& resultsView = + rRegister | + std::views::filter([addressIsNotZeroOrOne](const auto& result) { + return addressIsNotZeroOrOne(result.first); + }) | + std::views::transform([](const auto& result) { + return std::pair{result.first, result.second.r}; + }); + // Order the results by address. + const std::map orderedResults(resultsView.begin(), + resultsView.end()); + return orderedResults; +} + +std::string toBitString(const std::map& results) { + std::string ret; + ret.reserve(results.size()); + std::ranges::transform(results, std::back_inserter(ret), + [](const auto& kv) { return kv.second ? '1' : '0'; }); + return ret; } } // namespace qir From ded5a3a8b84e23d9d517603b551bc22f79cf975d Mon Sep 17 00:00:00 2001 From: rturrado Date: Sat, 6 Jun 2026 23:54:11 +0200 Subject: [PATCH 13/33] Make Runtime's output stream injectable Add an injectable std::ostream pointer member to Runtime, defaulting to std::cout, with getOstream/setOstream/resetOstream accessors. Update __quantum__rt__result_record_output (QIR.cpp) to write through runtime.getOstream() rather than directly to std::cout. Migrate the test fixtures in test_qir_runtime.cpp (QIRRuntimeTest) and results_sampling_test.cpp (HistogramKeysAndValuesSumToShots). Inject a std::ostringstream sink via setOstream in SetUp and restore the default via resetOstream in TearDown. QIRRuntimeTest's previous std::cout.rdbuf swap is gone. The QDMI sampling tests no longer spew thousands of result-record lines to stdout per run. Link MQT::CoreQIRRuntime to the dd-device test target when BUILD_MQT_CORE_QDMI_WITH_QIR is ON, since results_sampling_test.cpp now includes Runtime.hpp directly. Assisted-by: Claude Opus 4.7 via Claude Code --- include/mqt-core/qir/runtime/Runtime.hpp | 5 +++++ src/qir/runtime/QIR.cpp | 5 +++-- src/qir/runtime/Runtime.cpp | 8 ++++++++ test/qdmi/devices/dd/CMakeLists.txt | 2 +- .../qdmi/devices/dd/results_sampling_test.cpp | 9 +++++++++ test/qir/runtime/test_qir_runtime.cpp | 20 +++++++++---------- 6 files changed, 35 insertions(+), 14 deletions(-) diff --git a/include/mqt-core/qir/runtime/Runtime.hpp b/include/mqt-core/qir/runtime/Runtime.hpp index bea7612d47..ca4c5c07e1 100644 --- a/include/mqt-core/qir/runtime/Runtime.hpp +++ b/include/mqt-core/qir/runtime/Runtime.hpp @@ -214,6 +214,7 @@ class Runtime { std::unique_ptr dd; dd::vEdge qState; std::mt19937_64 mt; + std::ostream* os = &std::cout; Runtime(); explicit Runtime(uint64_t randomSeed); @@ -362,6 +363,10 @@ class Runtime { auto equal(Result* result1, Result* result2) -> bool; auto getResults() const -> std::map; + + auto getOstream() -> std::ostream&; + auto setOstream(std::ostream& other) -> void; + auto resetOstream() -> void; }; /// Build a bit string from a list of measurement results. diff --git a/src/qir/runtime/QIR.cpp b/src/qir/runtime/QIR.cpp index dc7eef132e..c07f9b7eb8 100644 --- a/src/qir/runtime/QIR.cpp +++ b/src/qir/runtime/QIR.cpp @@ -381,8 +381,9 @@ bool __quantum__rt__read_result(Result* result) { } void __quantum__rt__result_record_output(Result* result, const char* label) { - std::cout << label << ": " << (__quantum__rt__read_result(result) ? 1 : 0) - << "\n"; + auto& runtime = qir::Runtime::getInstance(); + runtime.getOstream() << label << ": " + << (__quantum__rt__read_result(result) ? 1 : 0) << "\n"; } } // extern "C" diff --git a/src/qir/runtime/Runtime.cpp b/src/qir/runtime/Runtime.cpp index d7a31b329a..20ae979a62 100644 --- a/src/qir/runtime/Runtime.cpp +++ b/src/qir/runtime/Runtime.cpp @@ -21,10 +21,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -189,6 +191,12 @@ auto Runtime::getResults() const -> std::map { return orderedResults; } +auto Runtime::getOstream() -> std::ostream& { return *os; } + +auto Runtime::setOstream(std::ostream& other) -> void { os = &other; } + +auto Runtime::resetOstream() -> void { os = &std::cout; } + std::string toBitString(const std::map& results) { std::string ret; ret.reserve(results.size()); diff --git a/test/qdmi/devices/dd/CMakeLists.txt b/test/qdmi/devices/dd/CMakeLists.txt index ea59d65be8..876e7a5e8c 100644 --- a/test/qdmi/devices/dd/CMakeLists.txt +++ b/test/qdmi/devices/dd/CMakeLists.txt @@ -35,7 +35,7 @@ if(TARGET MQT::CoreQDMI_DDSIM_Device) if(BUILD_MQT_CORE_QDMI_WITH_QIR) llvm_map_components_to_libnames(llvm_native_libs asmparser bitwriter core support) - target_link_libraries(${TARGET_NAME} PRIVATE ${llvm_native_libs}) + target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQIRRuntime ${llvm_native_libs}) target_compile_definitions(${TARGET_NAME} PRIVATE BUILD_MQT_CORE_QDMI_WITH_QIR) endif() diff --git a/test/qdmi/devices/dd/results_sampling_test.cpp b/test/qdmi/devices/dd/results_sampling_test.cpp index 711e5a9c20..2ecdf51ad9 100644 --- a/test/qdmi/devices/dd/results_sampling_test.cpp +++ b/test/qdmi/devices/dd/results_sampling_test.cpp @@ -22,6 +22,8 @@ #include #ifdef BUILD_MQT_CORE_QDMI_WITH_QIR +#include "qir/runtime/Runtime.hpp" + #include #include #include @@ -29,6 +31,7 @@ #include #include +#include #include #include #endif @@ -37,6 +40,12 @@ namespace { class HistogramKeysAndValuesSumToShots : public ::testing::Test { protected: +#ifdef BUILD_MQT_CORE_QDMI_WITH_QIR + std::ostringstream sink; + void SetUp() override { qir::Runtime::getInstance().setOstream(sink); } + void TearDown() override { qir::Runtime::getInstance().resetOstream(); } +#endif + static void Run(QDMI_Program_Format format, std::string_view program) { const qdmi_test::SessionGuard s{}; const qdmi_test::JobGuard j{s.session}; diff --git a/test/qir/runtime/test_qir_runtime.cpp b/test/qir/runtime/test_qir_runtime.cpp index cc4e733f7f..ed9e34979e 100644 --- a/test/qir/runtime/test_qir_runtime.cpp +++ b/test/qir/runtime/test_qir_runtime.cpp @@ -10,6 +10,7 @@ #include "ir/Definitions.hpp" #include "qir/runtime/QIR.h" +#include "qir/runtime/Runtime.hpp" #include #include @@ -18,9 +19,7 @@ #include #include #include -#include #include -#include #ifdef _WIN32 #define SYSTEM _wsystem @@ -34,10 +33,9 @@ namespace { class QIRRuntimeTest : public testing::Test { protected: - std::stringstream buffer; - std::streambuf* old = nullptr; - void SetUp() override { old = std::cout.rdbuf(buffer.rdbuf()); } - void TearDown() override { std::cout.rdbuf(old); } + std::ostringstream sink; + void SetUp() override { Runtime::getInstance().setOstream(sink); } + void TearDown() override { Runtime::getInstance().resetOstream(); } }; } // namespace @@ -263,7 +261,7 @@ TEST_F(QIRRuntimeTest, SwapGate) { __quantum__qis__mz__body(q1, r1); __quantum__rt__result_record_output(r0, "r0"); __quantum__rt__result_record_output(r1, "r1"); - EXPECT_EQ(buffer.str(), "r0: 0\nr1: 1\n"); + EXPECT_EQ(sink.str(), "r0: 0\nr1: 1\n"); } TEST_F(QIRRuntimeTest, CSwapGate) { @@ -366,7 +364,7 @@ TEST_F(QIRRuntimeTest, BellPairStatic) { EXPECT_EQ(m1, m2); __quantum__rt__result_record_output(r0, "r0"); __quantum__rt__result_record_output(r1, "r1"); - EXPECT_THAT(buffer.str(), testing::AnyOf("r0: 0\nr1: 0\n", "r0: 1\nr1: 1\n")); + EXPECT_THAT(sink.str(), testing::AnyOf("r0: 0\nr1: 0\n", "r0: 1\nr1: 1\n")); } TEST_F(QIRRuntimeTest, BellPairDynamic) { @@ -384,7 +382,7 @@ TEST_F(QIRRuntimeTest, BellPairDynamic) { EXPECT_EQ(m1, m2); __quantum__rt__result_record_output(r0, "r0"); __quantum__rt__result_record_output(r1, "r1"); - EXPECT_THAT(buffer.str(), testing::AnyOf("r0: 0\nr1: 0\n", "r0: 1\nr1: 1\n")); + EXPECT_THAT(sink.str(), testing::AnyOf("r0: 0\nr1: 0\n", "r0: 1\nr1: 1\n")); __quantum__rt__result_update_reference_count(r0, -1); __quantum__rt__result_update_reference_count(r1, -1); } @@ -404,7 +402,7 @@ TEST_F(QIRRuntimeTest, BellPairStaticReverse) { EXPECT_EQ(m1, m2); __quantum__rt__result_record_output(r0, "r0"); __quantum__rt__result_record_output(r1, "r1"); - EXPECT_THAT(buffer.str(), testing::AnyOf("r0: 0\nr1: 0\n", "r0: 1\nr1: 1\n")); + EXPECT_THAT(sink.str(), testing::AnyOf("r0: 0\nr1: 0\n", "r0: 1\nr1: 1\n")); } TEST_F(QIRRuntimeTest, BellPairDynamicReverse) { @@ -422,7 +420,7 @@ TEST_F(QIRRuntimeTest, BellPairDynamicReverse) { EXPECT_EQ(m1, m2); __quantum__rt__result_record_output(r0, "r0"); __quantum__rt__result_record_output(r1, "r1"); - EXPECT_THAT(buffer.str(), testing::AnyOf("r0: 0\nr1: 0\n", "r0: 1\nr1: 1\n")); + EXPECT_THAT(sink.str(), testing::AnyOf("r0: 0\nr1: 0\n", "r0: 1\nr1: 1\n")); __quantum__rt__result_update_reference_count(r0, -1); __quantum__rt__result_update_reference_count(r1, -1); } From f920621f5028d9f6b7989269116e958afb7ac7cb Mon Sep 17 00:00:00 2001 From: rturrado Date: Sun, 7 Jun 2026 00:31:47 +0200 Subject: [PATCH 14/33] Fix missing header Assisted-by: Claude Opus 4.7 via Claude Code --- src/qir/runner/Runner.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/qir/runner/Runner.cpp b/src/qir/runner/Runner.cpp index aaeb91bbf8..5f9d505b64 100644 --- a/src/qir/runner/Runner.cpp +++ b/src/qir/runner/Runner.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include From 8febb1233efa65c92380f017bba59a186e6d5792 Mon Sep 17 00:00:00 2001 From: rturrado Date: Sun, 7 Jun 2026 19:27:30 +0200 Subject: [PATCH 15/33] Record outputs into Runtime to support dynamic QIR Add Runtime::recordOutput(Result*) and Runtime::getRecordedOutputs(), backed by a std::string field. __quantum__rt__result_record_output now records the value into recordedOutputs before emitting the QIR-spec "label: 0|1" line, so dynamic QIR programs that release their Results via reference-count decrement no longer lose data before the QDMI device reads it. submitQIRProgram in the dd device consumes runtime.getRecordedOutputs() directly as the histogram key. Remove the now-unused Runtime::getResults() and the qir::toBitString helper. Add QIR_BELL_PAIR_DYNAMIC to circuits.hpp and the matching QIRBaseModuleDynamic and QIRBaseStringDynamic sampling tests. Flip dynamic_qubit_management and dynamic_result_management to i1 true in BellPairDynamic.ll and the inline dynamic string, for spec-compliant module metadata. Assisted-by: Claude Opus 4.7 via Claude Code --- include/mqt-core/qir/runtime/Runtime.hpp | 13 ++-- src/qdmi/devices/dd/Device.cpp | 2 +- src/qir/runtime/QIR.cpp | 1 + src/qir/runtime/Runtime.cpp | 37 +++--------- test/qdmi/devices/dd/helpers/circuits.hpp | 59 ++++++++++++++++++- .../qdmi/devices/dd/results_sampling_test.cpp | 21 +++++++ test/qir/BellPairDynamic.ll | 12 ++-- test/qir/BellPairStatic.ll | 4 +- 8 files changed, 103 insertions(+), 46 deletions(-) diff --git a/include/mqt-core/qir/runtime/Runtime.hpp b/include/mqt-core/qir/runtime/Runtime.hpp index ca4c5c07e1..8849561b5f 100644 --- a/include/mqt-core/qir/runtime/Runtime.hpp +++ b/include/mqt-core/qir/runtime/Runtime.hpp @@ -24,7 +24,6 @@ #include #include #include -#include #include #include #include @@ -207,6 +206,7 @@ class Runtime { std::vector qubitPermutation; static constexpr uintptr_t MIN_DYN_RESULT_ADDRESS = 0x10000; std::unordered_map rRegister; + std::string recordedOutputs; uintptr_t currentMaxQubitAddress; qc::Qubit currentMaxQubitId; uintptr_t currentMaxResultAddress; @@ -362,14 +362,17 @@ class Runtime { auto rFree(Result* result) -> void; auto equal(Result* result1, Result* result2) -> bool; - auto getResults() const -> std::map; + /// Append the value referenced by `result` to the recorded outputs bit + /// string in record order. + auto recordOutput(Result* result) -> void; + + /// Return the outputs declared by the program as a bit string in record + /// order. + auto getRecordedOutputs() const -> const std::string&; auto getOstream() -> std::ostream&; auto setOstream(std::ostream& other) -> void; auto resetOstream() -> void; }; -/// Build a bit string from a list of measurement results. -std::string toBitString(const std::map& results); - } // namespace qir diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index 98ea6518ce..e8def1fa87 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -454,7 +454,7 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQIRProgram() -> QDMI_STATUS { llvm::formatv("QIR program failed with error: {}", rc)); } // Update the measurement counts. - ++counts_[qir::toBitString(runtime.getResults())]; + ++counts_[runtime.getRecordedOutputs()]; } status_.store(QDMI_JOB_STATUS_DONE); } catch (const std::exception& e) { diff --git a/src/qir/runtime/QIR.cpp b/src/qir/runtime/QIR.cpp index c07f9b7eb8..a52c5c1cb5 100644 --- a/src/qir/runtime/QIR.cpp +++ b/src/qir/runtime/QIR.cpp @@ -382,6 +382,7 @@ bool __quantum__rt__read_result(Result* result) { void __quantum__rt__result_record_output(Result* result, const char* label) { auto& runtime = qir::Runtime::getInstance(); + runtime.recordOutput(result); runtime.getOstream() << label << ": " << (__quantum__rt__read_result(result) ? 1 : 0) << "\n"; } diff --git a/src/qir/runtime/Runtime.cpp b/src/qir/runtime/Runtime.cpp index 20ae979a62..8b81f742a4 100644 --- a/src/qir/runtime/Runtime.cpp +++ b/src/qir/runtime/Runtime.cpp @@ -22,13 +22,10 @@ #include #include #include -#include -#include #include #include #include #include -#include #include #include #include @@ -63,6 +60,7 @@ auto Runtime::reset() -> void { mt.seed(generateRandomSeed()); qRegister.clear(); rRegister.clear(); + recordedOutputs.clear(); // NOLINTBEGIN(performance-no-int-to-ptr) rRegister.emplace(reinterpret_cast(RESULT_ZERO_ADDRESS), ResultStruct{.refcount = 0, .r = false}); @@ -170,25 +168,12 @@ auto Runtime::equal(Result* result1, Result* result2) -> bool { return deref(result1).r == deref(result2).r; } -auto Runtime::getResults() const -> std::map { - auto addressIsNotZeroOrOne = [](Result* resultPtr) { - const auto addr = reinterpret_cast(resultPtr); - return addr != RESULT_ZERO_ADDRESS && addr != RESULT_ONE_ADDRESS; - }; - // Filter results with addresses 0 and 1 out. - // And keep the boolean value from ResultStruct only, not the ref count. - auto&& resultsView = - rRegister | - std::views::filter([addressIsNotZeroOrOne](const auto& result) { - return addressIsNotZeroOrOne(result.first); - }) | - std::views::transform([](const auto& result) { - return std::pair{result.first, result.second.r}; - }); - // Order the results by address. - const std::map orderedResults(resultsView.begin(), - resultsView.end()); - return orderedResults; +auto Runtime::recordOutput(Result* result) -> void { + recordedOutputs.push_back(deref(result).r ? '1' : '0'); +} + +auto Runtime::getRecordedOutputs() const -> const std::string& { + return recordedOutputs; } auto Runtime::getOstream() -> std::ostream& { return *os; } @@ -197,12 +182,4 @@ auto Runtime::setOstream(std::ostream& other) -> void { os = &other; } auto Runtime::resetOstream() -> void { os = &std::cout; } -std::string toBitString(const std::map& results) { - std::string ret; - ret.reserve(results.size()); - std::ranges::transform(results, std::back_inserter(ret), - [](const auto& kv) { return kv.second ? '1' : '0'; }); - return ret; -} - } // namespace qir diff --git a/test/qdmi/devices/dd/helpers/circuits.hpp b/test/qdmi/devices/dd/helpers/circuits.hpp index ec13f9f49b..7e05f019b8 100644 --- a/test/qdmi/devices/dd/helpers/circuits.hpp +++ b/test/qdmi/devices/dd/helpers/circuits.hpp @@ -61,8 +61,8 @@ c = measure q; )"; inline constexpr auto QIR_BELL_PAIR_STATIC = R"( -; ModuleID = 'bell' -source_filename = "bell" +; ModuleID = 'Static module implementing Bell pair' +source_filename = "BellPairStatic.ll" %Qubit = type opaque %Result = type opaque @@ -103,4 +103,59 @@ attributes #1 = { "irreversible" } !3 = !{i32 1, !"dynamic_result_management", i1 false} )"; +inline constexpr auto QIR_BELL_PAIR_DYNAMIC = R"( +; ModuleID = 'Dynamic module implementing Bell pair' +source_filename = "BellPairDynamic.ll" + +%Qubit = type opaque +%Result = type opaque + +@0 = internal constant [3 x i8] c"r0\00" +@1 = internal constant [3 x i8] c"r1\00" + +define i32 @main() #0 { +entry: + call void @__quantum__rt__initialize(i8* null) + %q0 = call %Qubit* @__quantum__rt__qubit_allocate() + %q1 = call %Qubit* @__quantum__rt__qubit_allocate() + call void @__quantum__qis__h__body(%Qubit* %q0) + call void @__quantum__qis__cnot__body(%Qubit* %q0, %Qubit* %q1) + %r0 = call %Result* @__quantum__qis__m__body(%Qubit* %q0) + %r1 = call %Result* @__quantum__qis__m__body(%Qubit* %q1) + call void @__quantum__rt__qubit_release(%Qubit* %q0) + call void @__quantum__rt__qubit_release(%Qubit* %q1) + call void @__quantum__rt__result_record_output(%Result* %r0, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @0, i32 0, i32 0)) + call void @__quantum__rt__result_record_output(%Result* %r1, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @1, i32 0, i32 0)) + call void @__quantum__rt__result_update_reference_count(%Result* %r0, i32 -1) + call void @__quantum__rt__result_update_reference_count(%Result* %r1, i32 -1) + ret i32 0 +} + +declare void @__quantum__qis__h__body(%Qubit*) + +declare void @__quantum__qis__cnot__body(%Qubit*, %Qubit*) + +declare %Result* @__quantum__qis__m__body(%Qubit*) #1 + +declare void @__quantum__rt__initialize(i8*) + +declare %Qubit* @__quantum__rt__qubit_allocate() + +declare void @__quantum__rt__qubit_release(%Qubit*) + +declare void @__quantum__rt__result_record_output(%Result*, i8*) + +declare void @__quantum__rt__result_update_reference_count(%Result*, i32) + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="2" "required_num_results"="2" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 true} +)"; + } // namespace qdmi_test diff --git a/test/qdmi/devices/dd/results_sampling_test.cpp b/test/qdmi/devices/dd/results_sampling_test.cpp index 2ecdf51ad9..d8842184a0 100644 --- a/test/qdmi/devices/dd/results_sampling_test.cpp +++ b/test/qdmi/devices/dd/results_sampling_test.cpp @@ -93,6 +93,27 @@ TEST_F(HistogramKeysAndValuesSumToShots, QIRBaseString) { const std::string_view program = qdmi_test::QIR_BELL_PAIR_STATIC; Run(format, program); } + +TEST_F(HistogramKeysAndValuesSumToShots, QIRBaseModuleDynamic) { + const QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QIRBASEMODULE; + const std::string_view program = qdmi_test::QIR_BELL_PAIR_DYNAMIC; + llvm::LLVMContext context; + llvm::SMDiagnostic err; + auto module = llvm::parseAssemblyString(program, err, context); + ASSERT_NE(module, nullptr) + << "parseAssemblyString failed: " << err.getMessage().str(); + std::string bitcodeBuffer; + llvm::raw_string_ostream os(bitcodeBuffer); + llvm::WriteBitcodeToFile(*module, os); + os.flush(); + Run(format, bitcodeBuffer); +} + +TEST_F(HistogramKeysAndValuesSumToShots, QIRBaseStringDynamic) { + const QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QIRBASESTRING; + const std::string_view program = qdmi_test::QIR_BELL_PAIR_DYNAMIC; + Run(format, program); +} #endif TEST(ResultsSampling, BufferTooSmallErrors) { diff --git a/test/qir/BellPairDynamic.ll b/test/qir/BellPairDynamic.ll index 15696c4816..1d80f9e034 100644 --- a/test/qir/BellPairDynamic.ll +++ b/test/qir/BellPairDynamic.ll @@ -1,5 +1,5 @@ -; ModuleID = 'bell' -source_filename = "bell" +; ModuleID = 'Dynamic module implementing Bell pair' +source_filename = "BellPairDynamic.ll" %Qubit = type opaque %Result = type opaque @@ -10,8 +10,8 @@ source_filename = "bell" define i32 @main() #0 { entry: call void @__quantum__rt__initialize(i8* null) - %q0 = call %Qubit* @__quantum__rt__qubit_allocate(); - %q1 = call %Qubit* @__quantum__rt__qubit_allocate(); + %q0 = call %Qubit* @__quantum__rt__qubit_allocate() + %q1 = call %Qubit* @__quantum__rt__qubit_allocate() call void @__quantum__qis__h__body(%Qubit* %q0) call void @__quantum__qis__cnot__body(%Qubit* %q0, %Qubit* %q1) %r0 = call %Result* @__quantum__qis__m__body(%Qubit* %q0) @@ -48,5 +48,5 @@ attributes #1 = { "irreversible" } !0 = !{i32 1, !"qir_major_version", i32 1} !1 = !{i32 7, !"qir_minor_version", i32 0} -!2 = !{i32 1, !"dynamic_qubit_management", i1 false} -!3 = !{i32 1, !"dynamic_result_management", i1 false} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 true} diff --git a/test/qir/BellPairStatic.ll b/test/qir/BellPairStatic.ll index eb98b571ce..972ab7261a 100644 --- a/test/qir/BellPairStatic.ll +++ b/test/qir/BellPairStatic.ll @@ -1,5 +1,5 @@ -; ModuleID = 'bell' -source_filename = "bell" +; ModuleID = 'Static module implementing Bell pair' +source_filename = "BellPairStatic.ll" %Qubit = type opaque %Result = type opaque From a1c36d2396b30c2b2d0eba8ffe2a3a1df1bc1d57 Mon Sep 17 00:00:00 2001 From: rturrado Date: Sun, 7 Jun 2026 20:04:53 +0200 Subject: [PATCH 16/33] Move QIR circuits to test/circuits and load them in dd-device tests Move BellPairStatic.ll, BellPairDynamic.ll, and GHZ4Dynamic.ll from test/qir to test/circuits, so the same files can serve both the qir-{runner,runtime} tests and the QDMI device tests. Update QIR_FILES_DIR in test/qir/{runner,runtime}/CMakeLists.txt to point at the new location. Add a QIR_FILES_DIR compile definition to the QDMI device test target. Switch the QIR tests in results_sampling_test.cpp to load BellPairStatic.ll and BellPairDynamic.ll from that path. Drop the QIR_BELL_PAIR_STATIC and QIR_BELL_PAIR_DYNAMIC inline strings from circuits.hpp. Assisted-by: Claude Opus 4.7 via Claude Code --- test/{qir => circuits}/BellPairDynamic.ll | 0 test/{qir => circuits}/BellPairStatic.ll | 0 test/{qir => circuits}/GHZ4Dynamic.ll | 0 test/qdmi/devices/dd/CMakeLists.txt | 4 +- test/qdmi/devices/dd/helpers/circuits.hpp | 98 ------------------- .../qdmi/devices/dd/results_sampling_test.cpp | 42 +++++--- test/qir/runner/CMakeLists.txt | 2 +- test/qir/runtime/CMakeLists.txt | 2 +- 8 files changed, 36 insertions(+), 112 deletions(-) rename test/{qir => circuits}/BellPairDynamic.ll (100%) rename test/{qir => circuits}/BellPairStatic.ll (100%) rename test/{qir => circuits}/GHZ4Dynamic.ll (100%) diff --git a/test/qir/BellPairDynamic.ll b/test/circuits/BellPairDynamic.ll similarity index 100% rename from test/qir/BellPairDynamic.ll rename to test/circuits/BellPairDynamic.ll diff --git a/test/qir/BellPairStatic.ll b/test/circuits/BellPairStatic.ll similarity index 100% rename from test/qir/BellPairStatic.ll rename to test/circuits/BellPairStatic.ll diff --git a/test/qir/GHZ4Dynamic.ll b/test/circuits/GHZ4Dynamic.ll similarity index 100% rename from test/qir/GHZ4Dynamic.ll rename to test/circuits/GHZ4Dynamic.ll diff --git a/test/qdmi/devices/dd/CMakeLists.txt b/test/qdmi/devices/dd/CMakeLists.txt index 876e7a5e8c..3bd86f99e8 100644 --- a/test/qdmi/devices/dd/CMakeLists.txt +++ b/test/qdmi/devices/dd/CMakeLists.txt @@ -36,7 +36,9 @@ if(TARGET MQT::CoreQDMI_DDSIM_Device) if(BUILD_MQT_CORE_QDMI_WITH_QIR) llvm_map_components_to_libnames(llvm_native_libs asmparser bitwriter core support) target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQIRRuntime ${llvm_native_libs}) - target_compile_definitions(${TARGET_NAME} PRIVATE BUILD_MQT_CORE_QDMI_WITH_QIR) + target_compile_definitions( + ${TARGET_NAME} PRIVATE BUILD_MQT_CORE_QDMI_WITH_QIR + QIR_FILES_DIR="${PROJECT_SOURCE_DIR}/test/circuits") endif() # On Windows, we need to copy the DLL to the test executable directory diff --git a/test/qdmi/devices/dd/helpers/circuits.hpp b/test/qdmi/devices/dd/helpers/circuits.hpp index 7e05f019b8..156dd1de94 100644 --- a/test/qdmi/devices/dd/helpers/circuits.hpp +++ b/test/qdmi/devices/dd/helpers/circuits.hpp @@ -60,102 +60,4 @@ cx q[0], q[1]; c = measure q; )"; -inline constexpr auto QIR_BELL_PAIR_STATIC = R"( -; ModuleID = 'Static module implementing Bell pair' -source_filename = "BellPairStatic.ll" - -%Qubit = type opaque -%Result = type opaque - -@0 = internal constant [3 x i8] c"r0\00" -@1 = internal constant [3 x i8] c"r1\00" - -define i32 @main() #0 { -entry: - call void @__quantum__rt__initialize(i8* null) - call void @__quantum__qis__h__body(%Qubit* null) - call void @__quantum__qis__cnot__body(%Qubit* null, %Qubit* inttoptr (i64 1 to %Qubit*)) - call void @__quantum__qis__mz__body(%Qubit* null, %Result* null) - call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 1 to %Result*)) - call void @__quantum__rt__result_record_output(%Result* null, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @0, i32 0, i32 0)) - call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 1 to %Result*), i8* getelementptr inbounds ([3 x i8], [3 x i8]* @1, i32 0, i32 0)) - ret i32 0 -} - -declare void @__quantum__qis__h__body(%Qubit*) - -declare void @__quantum__qis__cnot__body(%Qubit*, %Qubit*) - -declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly) #1 - -declare void @__quantum__rt__initialize(i8*) - -declare void @__quantum__rt__result_record_output(%Result*, i8*) - -attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="2" "required_num_results"="2" } -attributes #1 = { "irreversible" } - -!llvm.module.flags = !{!0, !1, !2, !3} - -!0 = !{i32 1, !"qir_major_version", i32 1} -!1 = !{i32 7, !"qir_minor_version", i32 0} -!2 = !{i32 1, !"dynamic_qubit_management", i1 false} -!3 = !{i32 1, !"dynamic_result_management", i1 false} -)"; - -inline constexpr auto QIR_BELL_PAIR_DYNAMIC = R"( -; ModuleID = 'Dynamic module implementing Bell pair' -source_filename = "BellPairDynamic.ll" - -%Qubit = type opaque -%Result = type opaque - -@0 = internal constant [3 x i8] c"r0\00" -@1 = internal constant [3 x i8] c"r1\00" - -define i32 @main() #0 { -entry: - call void @__quantum__rt__initialize(i8* null) - %q0 = call %Qubit* @__quantum__rt__qubit_allocate() - %q1 = call %Qubit* @__quantum__rt__qubit_allocate() - call void @__quantum__qis__h__body(%Qubit* %q0) - call void @__quantum__qis__cnot__body(%Qubit* %q0, %Qubit* %q1) - %r0 = call %Result* @__quantum__qis__m__body(%Qubit* %q0) - %r1 = call %Result* @__quantum__qis__m__body(%Qubit* %q1) - call void @__quantum__rt__qubit_release(%Qubit* %q0) - call void @__quantum__rt__qubit_release(%Qubit* %q1) - call void @__quantum__rt__result_record_output(%Result* %r0, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @0, i32 0, i32 0)) - call void @__quantum__rt__result_record_output(%Result* %r1, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @1, i32 0, i32 0)) - call void @__quantum__rt__result_update_reference_count(%Result* %r0, i32 -1) - call void @__quantum__rt__result_update_reference_count(%Result* %r1, i32 -1) - ret i32 0 -} - -declare void @__quantum__qis__h__body(%Qubit*) - -declare void @__quantum__qis__cnot__body(%Qubit*, %Qubit*) - -declare %Result* @__quantum__qis__m__body(%Qubit*) #1 - -declare void @__quantum__rt__initialize(i8*) - -declare %Qubit* @__quantum__rt__qubit_allocate() - -declare void @__quantum__rt__qubit_release(%Qubit*) - -declare void @__quantum__rt__result_record_output(%Result*, i8*) - -declare void @__quantum__rt__result_update_reference_count(%Result*, i32) - -attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="2" "required_num_results"="2" } -attributes #1 = { "irreversible" } - -!llvm.module.flags = !{!0, !1, !2, !3} - -!0 = !{i32 1, !"qir_major_version", i32 1} -!1 = !{i32 7, !"qir_minor_version", i32 0} -!2 = !{i32 1, !"dynamic_qubit_management", i1 true} -!3 = !{i32 1, !"dynamic_result_management", i1 true} -)"; - } // namespace qdmi_test diff --git a/test/qdmi/devices/dd/results_sampling_test.cpp b/test/qdmi/devices/dd/results_sampling_test.cpp index d8842184a0..af1379b71e 100644 --- a/test/qdmi/devices/dd/results_sampling_test.cpp +++ b/test/qdmi/devices/dd/results_sampling_test.cpp @@ -30,6 +30,9 @@ #include #include +#include +#include +#include #include #include #include @@ -46,7 +49,8 @@ class HistogramKeysAndValuesSumToShots : public ::testing::Test { void TearDown() override { qir::Runtime::getInstance().resetOstream(); } #endif - static void Run(QDMI_Program_Format format, std::string_view program) { + static void Run(const QDMI_Program_Format format, + const std::string_view program) { const qdmi_test::SessionGuard s{}; const qdmi_test::JobGuard j{s.session}; ASSERT_EQ(qdmi_test::setProgram(j.job, format, program), QDMI_SUCCESS); @@ -67,15 +71,19 @@ class HistogramKeysAndValuesSumToShots : public ::testing::Test { } // namespace TEST_F(HistogramKeysAndValuesSumToShots, QASM3Program) { - const QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QASM3; - const std::string_view program = qdmi_test::QASM3_BELL_SAMPLING; + constexpr QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QASM3; + constexpr std::string_view program = qdmi_test::QASM3_BELL_SAMPLING; Run(format, program); } #ifdef BUILD_MQT_CORE_QDMI_WITH_QIR TEST_F(HistogramKeysAndValuesSumToShots, QIRBaseModule) { - const QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QIRBASEMODULE; - const std::string_view program = qdmi_test::QIR_BELL_PAIR_STATIC; + constexpr QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QIRBASEMODULE; + const std::filesystem::path path = + std::filesystem::path(QIR_FILES_DIR) / "BellPairStatic.ll"; + std::ifstream ifs(path); + ASSERT_TRUE(ifs.is_open()) << "Failed to open " << path.string(); + const std::string program(std::istreambuf_iterator{ifs}, {}); llvm::LLVMContext context; llvm::SMDiagnostic err; auto module = llvm::parseAssemblyString(program, err, context); @@ -89,14 +97,22 @@ TEST_F(HistogramKeysAndValuesSumToShots, QIRBaseModule) { } TEST_F(HistogramKeysAndValuesSumToShots, QIRBaseString) { - const QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QIRBASESTRING; - const std::string_view program = qdmi_test::QIR_BELL_PAIR_STATIC; + constexpr QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QIRBASESTRING; + const std::filesystem::path path = + std::filesystem::path(QIR_FILES_DIR) / "BellPairStatic.ll"; + std::ifstream ifs(path); + ASSERT_TRUE(ifs.is_open()) << "Failed to open " << path.string(); + const std::string program(std::istreambuf_iterator{ifs}, {}); Run(format, program); } TEST_F(HistogramKeysAndValuesSumToShots, QIRBaseModuleDynamic) { - const QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QIRBASEMODULE; - const std::string_view program = qdmi_test::QIR_BELL_PAIR_DYNAMIC; + constexpr QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QIRBASEMODULE; + const std::filesystem::path path = + std::filesystem::path(QIR_FILES_DIR) / "BellPairDynamic.ll"; + std::ifstream ifs(path); + ASSERT_TRUE(ifs.is_open()) << "Failed to open " << path.string(); + const std::string program(std::istreambuf_iterator{ifs}, {}); llvm::LLVMContext context; llvm::SMDiagnostic err; auto module = llvm::parseAssemblyString(program, err, context); @@ -110,8 +126,12 @@ TEST_F(HistogramKeysAndValuesSumToShots, QIRBaseModuleDynamic) { } TEST_F(HistogramKeysAndValuesSumToShots, QIRBaseStringDynamic) { - const QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QIRBASESTRING; - const std::string_view program = qdmi_test::QIR_BELL_PAIR_DYNAMIC; + constexpr QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QIRBASESTRING; + const std::filesystem::path path = + std::filesystem::path(QIR_FILES_DIR) / "BellPairDynamic.ll"; + std::ifstream ifs(path); + ASSERT_TRUE(ifs.is_open()) << "Failed to open " << path.string(); + const std::string program(std::istreambuf_iterator{ifs}, {}); Run(format, program); } #endif diff --git a/test/qir/runner/CMakeLists.txt b/test/qir/runner/CMakeLists.txt index f79e3cf62d..6fd179f5be 100644 --- a/test/qir/runner/CMakeLists.txt +++ b/test/qir/runner/CMakeLists.txt @@ -13,7 +13,7 @@ if(TARGET MQT::CoreQIRRunner) package_add_test(${TARGET_NAME} "" test_qir_runner.cpp) # Collect QIR files set(QIR_FILES "") - set(QIR_FILES_DIR ${CMAKE_CURRENT_SOURCE_DIR}/..) + set(QIR_FILES_DIR ${PROJECT_SOURCE_DIR}/test/circuits) file(REAL_PATH "${QIR_FILES_DIR}" QIR_FILES_DIR) file(GLOB QIR_FILES "${QIR_FILES_DIR}/*.ll") # transform QIR_FILES to comma separated list of string ("...") diff --git a/test/qir/runtime/CMakeLists.txt b/test/qir/runtime/CMakeLists.txt index 782b801182..ba21107f7d 100644 --- a/test/qir/runtime/CMakeLists.txt +++ b/test/qir/runtime/CMakeLists.txt @@ -53,7 +53,7 @@ if(TARGET MQT::CoreQIRRuntime) # add tests for QIR files set(QIR_EXECUTABLES "") - set(QIR_FILES_DIR ${CMAKE_CURRENT_SOURCE_DIR}/..) + set(QIR_FILES_DIR ${PROJECT_SOURCE_DIR}/test/circuits) file(GLOB QIR_FILES "${QIR_FILES_DIR}/*.ll") foreach(QIR_EXAMPLE ${QIR_FILES}) From 48a45286026c60f173f94b235ae77554cb55a80e Mon Sep 17 00:00:00 2001 From: rturrado Date: Mon, 8 Jun 2026 00:53:04 +0200 Subject: [PATCH 17/33] Add Adaptive QIR profile support to the DDSIM QDMI device Route `QDMI_PROGRAM_FORMAT_QIRADAPTIVE{MODULE,STRING}` to `submitQIRProgram` and accept them in the program-format gate. The existing JIT runtime already handles `__quantum__rt__read_result + br i1`, so a `H; measure; conditional-X; measure` Bell pair via classical correction runs end-to-end with no further runtime change. `bindings/fomac/fomac.cpp` already exposed `QIR_ADAPTIVE_{MODULE,STRING}`, so the Python surface is unchanged. New `test/circuits/BellPairAdaptive.ll`: produces the same `{"00","11"}` correlation as the entangled Bell pair, but via classical control flow on a measured result, so the same assertion validates all three encodings (Static, Dynamic, Adaptive). Refactor `results_sampling_test.cpp` into a shared `HistogramTest` fixture plus two QIR subclasses. `checkHistogram` now asserts both `sum == SHOTS` and the histogram keys are exactly `{"00","11"}`. The new key-shape check is what proves the adaptive branch actually fires for every shot rather than collapsing to a single value. Move the adaptive format constants from the unsupported loop to the supported loop in `job_parameters_test.cpp`. Assisted-by: Claude Opus 4.7 via Claude Code --- src/qdmi/devices/dd/Device.cpp | 8 +- test/circuits/BellPairAdaptive.ll | 61 +++++++ test/qdmi/devices/dd/job_parameters_test.cpp | 6 +- .../qdmi/devices/dd/results_sampling_test.cpp | 155 ++++++++++-------- 4 files changed, 160 insertions(+), 70 deletions(-) create mode 100644 test/circuits/BellPairAdaptive.ll diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index e8def1fa87..102e1a5b41 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -358,7 +358,9 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::setParameter( format != QDMI_PROGRAM_FORMAT_QASM3 #ifdef BUILD_MQT_CORE_QDMI_WITH_QIR && format != QDMI_PROGRAM_FORMAT_QIRBASEMODULE && - format != QDMI_PROGRAM_FORMAT_QIRBASESTRING + format != QDMI_PROGRAM_FORMAT_QIRBASESTRING && + format != QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE && + format != QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING #endif ) { return QDMI_ERROR_NOTSUPPORTED; @@ -477,7 +479,9 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::submit() -> QDMI_STATUS { } #ifdef BUILD_MQT_CORE_QDMI_WITH_QIR if (format_ == QDMI_PROGRAM_FORMAT_QIRBASEMODULE || - format_ == QDMI_PROGRAM_FORMAT_QIRBASESTRING) { + format_ == QDMI_PROGRAM_FORMAT_QIRBASESTRING || + format_ == QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE || + format_ == QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING) { return submitQIRProgram(); } #endif diff --git a/test/circuits/BellPairAdaptive.ll b/test/circuits/BellPairAdaptive.ll new file mode 100644 index 0000000000..441546ebce --- /dev/null +++ b/test/circuits/BellPairAdaptive.ll @@ -0,0 +1,61 @@ +; ModuleID = 'Adaptive module implementing Bell-pair correlation via classical correction' +source_filename = "BellPairAdaptive.ll" + +%Qubit = type opaque +%Result = type opaque + +@0 = internal constant [3 x i8] c"r0\00" +@1 = internal constant [3 x i8] c"r1\00" + +define i32 @main() #0 { +entry: + call void @__quantum__rt__initialize(i8* null) + %q0 = call %Qubit* @__quantum__rt__qubit_allocate() + %q1 = call %Qubit* @__quantum__rt__qubit_allocate() + call void @__quantum__qis__h__body(%Qubit* %q0) + %r0 = call %Result* @__quantum__qis__m__body(%Qubit* %q0) + %b = call i1 @__quantum__rt__read_result(%Result* %r0) + br i1 %b, label %correct, label %record + +correct: + call void @__quantum__qis__x__body(%Qubit* %q1) + br label %record + +record: + %r1 = call %Result* @__quantum__qis__m__body(%Qubit* %q1) + call void @__quantum__rt__qubit_release(%Qubit* %q0) + call void @__quantum__rt__qubit_release(%Qubit* %q1) + call void @__quantum__rt__result_record_output(%Result* %r0, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @0, i32 0, i32 0)) + call void @__quantum__rt__result_record_output(%Result* %r1, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @1, i32 0, i32 0)) + call void @__quantum__rt__result_update_reference_count(%Result* %r0, i32 -1) + call void @__quantum__rt__result_update_reference_count(%Result* %r1, i32 -1) + ret i32 0 +} + +declare void @__quantum__qis__h__body(%Qubit*) + +declare void @__quantum__qis__x__body(%Qubit*) + +declare %Result* @__quantum__qis__m__body(%Qubit*) #1 + +declare i1 @__quantum__rt__read_result(%Result*) + +declare void @__quantum__rt__initialize(i8*) + +declare %Qubit* @__quantum__rt__qubit_allocate() + +declare void @__quantum__rt__qubit_release(%Qubit*) + +declare void @__quantum__rt__result_record_output(%Result*, i8*) + +declare void @__quantum__rt__result_update_reference_count(%Result*, i32) + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="2" "required_num_results"="2" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 true} diff --git a/test/qdmi/devices/dd/job_parameters_test.cpp b/test/qdmi/devices/dd/job_parameters_test.cpp index 67452ff0d1..a27581964b 100644 --- a/test/qdmi/devices/dd/job_parameters_test.cpp +++ b/test/qdmi/devices/dd/job_parameters_test.cpp @@ -96,6 +96,8 @@ TEST(JobParameters, ProgramFormatSupport) { #ifdef BUILD_MQT_CORE_QDMI_WITH_QIR QDMI_PROGRAM_FORMAT_QIRBASEMODULE, QDMI_PROGRAM_FORMAT_QIRBASESTRING, + QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE, + QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING, #endif }) { EXPECT_EQ(MQT_DDSIM_QDMI_device_job_set_parameter( @@ -106,11 +108,11 @@ TEST(JobParameters, ProgramFormatSupport) { // Unsupported → NOTSUPPORTED for (QDMI_Program_Format fmt : { - QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING, - QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE, #ifndef BUILD_MQT_CORE_QDMI_WITH_QIR QDMI_PROGRAM_FORMAT_QIRBASEMODULE, QDMI_PROGRAM_FORMAT_QIRBASESTRING, + QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE, + QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING, #endif QDMI_PROGRAM_FORMAT_CALIBRATION, QDMI_PROGRAM_FORMAT_QPY, diff --git a/test/qdmi/devices/dd/results_sampling_test.cpp b/test/qdmi/devices/dd/results_sampling_test.cpp index af1379b71e..eb18f9c4e2 100644 --- a/test/qdmi/devices/dd/results_sampling_test.cpp +++ b/test/qdmi/devices/dd/results_sampling_test.cpp @@ -18,7 +18,13 @@ #include +#include #include +#include +#include +#include +#include +#include #include #ifdef BUILD_MQT_CORE_QDMI_WITH_QIR @@ -35,13 +41,11 @@ #include #include #include -#include -#include #endif namespace { -class HistogramKeysAndValuesSumToShots : public ::testing::Test { +class HistogramTest : public ::testing::Test { protected: #ifdef BUILD_MQT_CORE_QDMI_WITH_QIR std::ostringstream sink; @@ -49,90 +53,109 @@ class HistogramKeysAndValuesSumToShots : public ::testing::Test { void TearDown() override { qir::Runtime::getInstance().resetOstream(); } #endif - static void Run(const QDMI_Program_Format format, - const std::string_view program) { + using Histogram = std::pair, std::vector>; + static constexpr size_t SHOTS = 1024; + + static Histogram runProgram(const QDMI_Program_Format format, + const std::string_view program) { const qdmi_test::SessionGuard s{}; const qdmi_test::JobGuard j{s.session}; - ASSERT_EQ(qdmi_test::setProgram(j.job, format, program), QDMI_SUCCESS); - constexpr size_t shots = 1024; - ASSERT_EQ(qdmi_test::setShots(j.job, shots), QDMI_SUCCESS); - ASSERT_EQ(qdmi_test::submitAndWait(j.job, 0), QDMI_SUCCESS); + EXPECT_EQ(qdmi_test::setProgram(j.job, format, program), QDMI_SUCCESS); + EXPECT_EQ(qdmi_test::setShots(j.job, SHOTS), QDMI_SUCCESS); + EXPECT_EQ(qdmi_test::submitAndWait(j.job, 0), QDMI_SUCCESS); + return qdmi_test::getHistogram(j.job); + } - auto [keys, vals] = qdmi_test::getHistogram(j.job); + static void checkHistogram(const Histogram& hist) { + const auto& [keys, vals] = hist; + // Keys and values come from two independent device queries. + // Check both vectors have the same size. ASSERT_EQ(keys.size(), vals.size()); - size_t sum = 0U; - for (const auto& v : vals) { - sum += v; + // Values should sum up to the number of SHOTS. + const auto sum = std::accumulate(vals.cbegin(), vals.cend(), size_t{0}); + EXPECT_EQ(sum, SHOTS); + // Both keys '00' and '11' should be expected. + ASSERT_EQ(keys.size(), 2U); + // And no other keys should be expected. + EXPECT_TRUE(std::ranges::all_of( + keys, [](const auto& k) { return k == "00" || k == "11"; })); + } +}; + +#ifdef BUILD_MQT_CORE_QDMI_WITH_QIR +class QIRHistogramTestModule : public HistogramTest { +protected: + static std::string getProgram(const std::string_view file) { + const std::filesystem::path path = + std::filesystem::path(QIR_FILES_DIR) / file; + std::ifstream ifs(path); + EXPECT_TRUE(ifs.is_open()) << "Failed to open " << path.string(); + const std::string text(std::istreambuf_iterator{ifs}, {}); + llvm::LLVMContext context; + llvm::SMDiagnostic err; + auto llvmModule = llvm::parseAssemblyString(text, err, context); + EXPECT_NE(llvmModule, nullptr) + << "parseAssemblyString failed: " << err.getMessage().str(); + if (llvmModule == nullptr) { + return {}; } - EXPECT_EQ(sum, shots); + std::string bitcodeBuffer; + llvm::raw_string_ostream os(bitcodeBuffer); + llvm::WriteBitcodeToFile(*llvmModule, os); + os.flush(); + return bitcodeBuffer; + } +}; + +class QIRHistogramTestString : public HistogramTest { +protected: + static std::string getProgram(const std::string_view file) { + const std::filesystem::path path = + std::filesystem::path(QIR_FILES_DIR) / file; + std::ifstream ifs(path); + EXPECT_TRUE(ifs.is_open()) << "Failed to open " << path.string(); + return {std::istreambuf_iterator{ifs}, {}}; } }; +#endif } // namespace -TEST_F(HistogramKeysAndValuesSumToShots, QASM3Program) { +TEST_F(HistogramTest, QASM3Program) { constexpr QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QASM3; constexpr std::string_view program = qdmi_test::QASM3_BELL_SAMPLING; - Run(format, program); + checkHistogram(runProgram(format, program)); } #ifdef BUILD_MQT_CORE_QDMI_WITH_QIR -TEST_F(HistogramKeysAndValuesSumToShots, QIRBaseModule) { - constexpr QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QIRBASEMODULE; - const std::filesystem::path path = - std::filesystem::path(QIR_FILES_DIR) / "BellPairStatic.ll"; - std::ifstream ifs(path); - ASSERT_TRUE(ifs.is_open()) << "Failed to open " << path.string(); - const std::string program(std::istreambuf_iterator{ifs}, {}); - llvm::LLVMContext context; - llvm::SMDiagnostic err; - auto module = llvm::parseAssemblyString(program, err, context); - ASSERT_NE(module, nullptr) - << "parseAssemblyString failed: " << err.getMessage().str(); - std::string bitcodeBuffer; - llvm::raw_string_ostream os(bitcodeBuffer); - llvm::WriteBitcodeToFile(*module, os); - os.flush(); - Run(format, bitcodeBuffer); +TEST_F(QIRHistogramTestModule, BaseStatic) { + constexpr auto format = QDMI_PROGRAM_FORMAT_QIRBASEMODULE; + checkHistogram(runProgram(format, getProgram("BellPairStatic.ll"))); +} + +TEST_F(QIRHistogramTestString, BaseStatic) { + constexpr auto format = QDMI_PROGRAM_FORMAT_QIRBASESTRING; + checkHistogram(runProgram(format, getProgram("BellPairStatic.ll"))); +} + +TEST_F(QIRHistogramTestModule, BaseDynamic) { + constexpr auto format = QDMI_PROGRAM_FORMAT_QIRBASEMODULE; + checkHistogram(runProgram(format, getProgram("BellPairDynamic.ll"))); } -TEST_F(HistogramKeysAndValuesSumToShots, QIRBaseString) { - constexpr QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QIRBASESTRING; - const std::filesystem::path path = - std::filesystem::path(QIR_FILES_DIR) / "BellPairStatic.ll"; - std::ifstream ifs(path); - ASSERT_TRUE(ifs.is_open()) << "Failed to open " << path.string(); - const std::string program(std::istreambuf_iterator{ifs}, {}); - Run(format, program); +TEST_F(QIRHistogramTestString, BaseDynamic) { + constexpr auto format = QDMI_PROGRAM_FORMAT_QIRBASESTRING; + checkHistogram(runProgram(format, getProgram("BellPairDynamic.ll"))); } -TEST_F(HistogramKeysAndValuesSumToShots, QIRBaseModuleDynamic) { - constexpr QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QIRBASEMODULE; - const std::filesystem::path path = - std::filesystem::path(QIR_FILES_DIR) / "BellPairDynamic.ll"; - std::ifstream ifs(path); - ASSERT_TRUE(ifs.is_open()) << "Failed to open " << path.string(); - const std::string program(std::istreambuf_iterator{ifs}, {}); - llvm::LLVMContext context; - llvm::SMDiagnostic err; - auto module = llvm::parseAssemblyString(program, err, context); - ASSERT_NE(module, nullptr) - << "parseAssemblyString failed: " << err.getMessage().str(); - std::string bitcodeBuffer; - llvm::raw_string_ostream os(bitcodeBuffer); - llvm::WriteBitcodeToFile(*module, os); - os.flush(); - Run(format, bitcodeBuffer); +TEST_F(QIRHistogramTestModule, Adaptive) { + constexpr auto format = QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE; + checkHistogram(runProgram(format, getProgram("BellPairAdaptive.ll"))); } -TEST_F(HistogramKeysAndValuesSumToShots, QIRBaseStringDynamic) { - constexpr QDMI_Program_Format format = QDMI_PROGRAM_FORMAT_QIRBASESTRING; - const std::filesystem::path path = - std::filesystem::path(QIR_FILES_DIR) / "BellPairDynamic.ll"; - std::ifstream ifs(path); - ASSERT_TRUE(ifs.is_open()) << "Failed to open " << path.string(); - const std::string program(std::istreambuf_iterator{ifs}, {}); - Run(format, program); +TEST_F(QIRHistogramTestString, Adaptive) { + constexpr auto format = QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING; + checkHistogram(runProgram(format, getProgram("BellPairAdaptive.ll"))); } #endif From f69c771343c9a38ec59fedee503d3d97203c9723 Mon Sep 17 00:00:00 2001 From: rturrado Date: Tue, 9 Jun 2026 22:53:59 +0200 Subject: [PATCH 18/33] Address review feedback Small follow-ups from the PR review, grouped together because each is a one-line fix in a different file. - CHANGELOG.md: move the new entry to the top of the Added list. - docs/qir/index.md: refer to the QIR formats as "QIR Base Profile Module" and "QIR Base Profile String". - src/qdmi/devices/dd/Device.cpp: replace the trailing `return QDMI_ERROR_NOTSUPPORTED;` in `submit()` with `qdmi::unreachable()`. - src/qir/jit/CMakeLists.txt: drop the redundant `Session.hpp` entry from the PRIVATE `target_sources` call. - test/qdmi/devices/dd/job_parameters_test.cpp: restore the original ordering of QIR format defines in the supported and unsupported lists. Assisted-by: Claude Opus 4.7 via Claude Code --- CHANGELOG.md | 2 +- docs/qir/index.md | 2 +- src/qdmi/devices/dd/Device.cpp | 3 ++- src/qir/jit/CMakeLists.txt | 3 +-- test/qdmi/devices/dd/job_parameters_test.cpp | 8 ++++---- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d7c7a3d19..0b48d5328f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning], with the exception that minor rel ### Added +- ✨ Add QIR program format support to the QDMI DDSim device ([#1766]) ([**@rturrado**]) - 🚸 Add [CMake presets] to provide a standardized and reproducible way to configure builds ([#1660]) ([**@denialhaag**]) - ✨ Add a `quantum-loop-unroll` pass for unrolling for-loop operations containing quantum operations ([#1718]) ([**@MatthiasReumann**]) - ✨ Add a `hadamard-lifting` pass for lifting Hadamard gates above Pauli gates ([#1605]) ([**@lirem101**], [**@burgholzer**]) @@ -20,7 +21,6 @@ This project adheres to [Semantic Versioning], with the exception that minor rel - ✨ Add initial infrastructure for new QC and QCO MLIR dialects ([#1264], [#1330], [#1402], [#1428], [#1430], [#1436], [#1443], [#1446], [#1464], [#1465], [#1470], [#1471], [#1472], [#1474], [#1475], [#1506], [#1510], [#1513], [#1521], [#1542], [#1548], [#1550], [#1554], [#1567], [#1569], [#1570], [#1572], [#1573], [#1580], [#1602], [#1620], [#1623], [#1624], [#1626], [#1627], [#1635], [#1638], [#1673], [#1675], [#1700], [#1717], [#1728], [#1730], [#1749], [#1762], [#1765]) ([**@burgholzer**], [**@denialhaag**], [**@taminob**], [**@DRovara**], [**@li-mingbao**], [**@Ectras**], [**@MatthiasReumann**], [**@simon1hofmann**]) -- ✨ Add QIR program format support to the QDMI DDSim device ([#1766]) ([**@rturrado**]) ### Changed diff --git a/docs/qir/index.md b/docs/qir/index.md index f86863d848..85f9742664 100644 --- a/docs/qir/index.md +++ b/docs/qir/index.md @@ -39,6 +39,6 @@ The runner supports the QIR Base Profile. ### QIR Support in the QDMI Device -The QDMI Device accepts jobs in the following program formats: QASM2, QASM3, QIR base module (LLVM bitcode), and QIR base string (LLVM assembly). +The QDMI Device accepts jobs in the following program formats: QASM2, QASM3, QIR Base Profile Module (LLVM bitcode), and QIR Base Profile String (LLVM assembly). The QIR base formats are only supported when the `BUILD_MQT_CORE_QDMI_WITH_QIR` CMake option is enabled. It is disabled by default to avoid the cost of linking against the MQT Core QIR JIT (built on LLVM OrcJIT) and Runtime libraries. diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index 102e1a5b41..77af927566 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -485,7 +485,8 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::submit() -> QDMI_STATUS { return submitQIRProgram(); } #endif - return QDMI_ERROR_NOTSUPPORTED; + // Format is validated against the allowed set at setParameter time. + qdmi::unreachable(); } auto MQT_DDSIM_QDMI_Device_Job_impl_d::cancel() -> QDMI_STATUS { const auto s = status_.load(); diff --git a/src/qir/jit/CMakeLists.txt b/src/qir/jit/CMakeLists.txt index 8f9f92d7d8..4cfd646176 100644 --- a/src/qir/jit/CMakeLists.txt +++ b/src/qir/jit/CMakeLists.txt @@ -13,8 +13,7 @@ if(NOT TARGET ${TARGET_NAME}) add_mqt_core_library(${TARGET_NAME} ALIAS_NAME QIRJIT) # Add sources to target - target_sources(${TARGET_NAME} PRIVATE Session.cpp - ${MQT_CORE_INCLUDE_BUILD_DIR}/qir/jit/Session.hpp) + target_sources(${TARGET_NAME} PRIVATE Session.cpp) # Add headers using file sets target_sources(${TARGET_NAME} PUBLIC FILE_SET HEADERS BASE_DIRS ${MQT_CORE_INCLUDE_BUILD_DIR} diff --git a/test/qdmi/devices/dd/job_parameters_test.cpp b/test/qdmi/devices/dd/job_parameters_test.cpp index a27581964b..bb5db9f395 100644 --- a/test/qdmi/devices/dd/job_parameters_test.cpp +++ b/test/qdmi/devices/dd/job_parameters_test.cpp @@ -94,10 +94,10 @@ TEST(JobParameters, ProgramFormatSupport) { QDMI_PROGRAM_FORMAT_QASM2, QDMI_PROGRAM_FORMAT_QASM3, #ifdef BUILD_MQT_CORE_QDMI_WITH_QIR - QDMI_PROGRAM_FORMAT_QIRBASEMODULE, QDMI_PROGRAM_FORMAT_QIRBASESTRING, - QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE, + QDMI_PROGRAM_FORMAT_QIRBASEMODULE, QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING, + QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE, #endif }) { EXPECT_EQ(MQT_DDSIM_QDMI_device_job_set_parameter( @@ -109,10 +109,10 @@ TEST(JobParameters, ProgramFormatSupport) { // Unsupported → NOTSUPPORTED for (QDMI_Program_Format fmt : { #ifndef BUILD_MQT_CORE_QDMI_WITH_QIR - QDMI_PROGRAM_FORMAT_QIRBASEMODULE, QDMI_PROGRAM_FORMAT_QIRBASESTRING, - QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE, + QDMI_PROGRAM_FORMAT_QIRBASEMODULE, QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING, + QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE, #endif QDMI_PROGRAM_FORMAT_CALIBRATION, QDMI_PROGRAM_FORMAT_QPY, From d1f03bb4d8dcbd4ea1444fe2477c1a0f8269c8b7 Mon Sep 17 00:00:00 2001 From: rturrado Date: Tue, 9 Jun 2026 23:48:30 +0200 Subject: [PATCH 19/33] Address review feedback regarding jit::Session - Flatten qir::jit to qir. - Rename Session to JitSession. - Add docstrings to JitSession. Assisted-by: Claude Opus 4.7 via Claude Code --- include/mqt-core/qir/jit/Session.hpp | 49 ++++++++++++++++++++++++---- src/qdmi/devices/dd/Device.cpp | 2 +- src/qir/jit/Session.cpp | 32 +++++++++--------- src/qir/runner/Runner.cpp | 2 +- 4 files changed, 61 insertions(+), 24 deletions(-) diff --git a/include/mqt-core/qir/jit/Session.hpp b/include/mqt-core/qir/jit/Session.hpp index dc68de0e1b..ce7712f0b9 100644 --- a/include/mqt-core/qir/jit/Session.hpp +++ b/include/mqt-core/qir/jit/Session.hpp @@ -20,15 +20,52 @@ #include #include -namespace qir::jit { +namespace qir { -class Session { +/** + * @brief In-process JIT executor for QIR programs. + * @details The session does the following, in order: + * - Loads an LLVM module from either an IR file (text or bitcode) or + * an in-memory buffer, + * - JIT-compiles it via LLVM's OrcJIT with lazy compilation. + * - wires up the QIR runtime symbols, and + * - runs the module's @c main function. + * A session owns a single LLJIT instance and is not meant to be reused across + * modules; create a new @ref JitSession for each program. + */ +class JitSession { public: + /// Signature of the @c main function produced by QIR-compiled modules. using MainFn = int(int, char**); - explicit Session(llvm::StringRef inputFile); - Session(llvm::StringRef irBytes, llvm::StringRef bufferName); - ~Session(); + /** + * @brief Build a session by loading IR from a file on disk. + * @param inputFile Path to a textual IR or bitcode file. + * @throws std::runtime_error if the file cannot be parsed or the JIT fails + * to initialize. + */ + explicit JitSession(llvm::StringRef inputFile); + + /** + * @brief Build a session by loading IR from a memory buffer. + * @details Accepts either textual IR or bitcode. The buffer does not have + * to be null-terminated. + * @param irBytes Byte view of the IR. + * @param bufferName Identifier used in diagnostics. + * @throws std::runtime_error if the IR cannot be parsed or the JIT fails + * to initialize. + */ + JitSession(llvm::StringRef irBytes, llvm::StringRef bufferName); + + /// Tears down the LLJIT and any JIT'd resources owned by the session. + ~JitSession(); + + /** + * @brief Executes the JIT'd @c main function. + * @param args Argument strings passed as @c argv (excluding @c argv[0]). + * @param progName Value used as @c argv[0]. + * @return The integer returned by the JIT'd @c main. + */ int run(llvm::ArrayRef args = {}, llvm::StringRef progName = "") const; @@ -48,4 +85,4 @@ class Session { void deinitialize(); }; -} // namespace qir::jit +} // namespace qir diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index 77af927566..ca6d6b519e 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -448,7 +448,7 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQIRProgram() -> QDMI_STATUS { try { auto& runtime = qir::Runtime::getInstance(); auto irBytes = llvm::StringRef(program_.data(), program_.size()); - auto jitSession = qir::jit::Session(irBytes, "QDMI job"); + auto jitSession = qir::JitSession(irBytes, "QDMI job"); for (size_t i = 0; i < numShots_; ++i) { runtime.reset(); if (const auto rc = jitSession.run(); rc != 0) { diff --git a/src/qir/jit/Session.cpp b/src/qir/jit/Session.cpp index ef15e61867..7d0c9b5c92 100644 --- a/src/qir/jit/Session.cpp +++ b/src/qir/jit/Session.cpp @@ -54,7 +54,7 @@ #define DEBUG_TYPE "mqt-core-qir-jit" -namespace qir::jit { +namespace qir { static void exitOnLazyCallThroughFailure() { exit(1); } @@ -97,7 +97,7 @@ getThreadSafeModuleOrError(std::unique_ptr module, } llvm::Expected -Session::loadModuleFromFile(const llvm::StringRef irPath) { +JitSession::loadModuleFromFile(const llvm::StringRef irPath) { llvm::SMDiagnostic err; auto m = tsCtx_.withContextDo( [&](llvm::LLVMContext* ctx) { return parseIRFile(irPath, err, *ctx); }); @@ -105,8 +105,8 @@ Session::loadModuleFromFile(const llvm::StringRef irPath) { } llvm::Expected -Session::loadModuleFromMemory(const llvm::StringRef irBytes, - const llvm::StringRef bufferName) { +JitSession::loadModuleFromMemory(const llvm::StringRef irBytes, + const llvm::StringRef bufferName) { llvm::SMDiagnostic err; auto buffer = llvm::MemoryBuffer::getMemBuffer( irBytes, bufferName, @@ -117,7 +117,7 @@ Session::loadModuleFromMemory(const llvm::StringRef irBytes, return getThreadSafeModuleOrError(std::move(m), err, tsCtx_); } -Session::Session(const llvm::StringRef inputFile) { +JitSession::JitSession(const llvm::StringRef inputFile) { auto ret = loadModuleFromFile(inputFile); if (!ret) { throw std::runtime_error(llvm::toString(ret.takeError())); @@ -126,8 +126,8 @@ Session::Session(const llvm::StringRef inputFile) { initialize(); } -Session::Session(const llvm::StringRef irBytes, - const llvm::StringRef bufferName) { +JitSession::JitSession(const llvm::StringRef irBytes, + const llvm::StringRef bufferName) { auto ret = loadModuleFromMemory(irBytes, bufferName); if (!ret) { throw std::runtime_error(llvm::toString(ret.takeError())); @@ -136,10 +136,10 @@ Session::Session(const llvm::StringRef irBytes, initialize(); } -Session::~Session() { deinitialize(); } +JitSession::~JitSession() { deinitialize(); } -int Session::run(llvm::ArrayRef args, - llvm::StringRef progName) const { +int JitSession::run(llvm::ArrayRef args, + llvm::StringRef progName) const { // Manual in-process execution with RuntimeDyld. return llvm::orc::runAsMain(mainFn_, args, progName); } @@ -153,7 +153,7 @@ std::vector> manualSymbols; reinterpret_cast(&(name))); \ manualSymbols.emplace_back(#name, reinterpret_cast(&(name))); -void Session::registerRuntimeSymbols() { +void JitSession::registerRuntimeSymbols() { static std::once_flag flag; std::call_once(flag, []() { REGISTER_SYMBOL(__quantum__rt__result_get_zero); @@ -222,7 +222,7 @@ void Session::registerRuntimeSymbols() { #undef REGISTER_SYMBOL -void Session::initNativeTargets() { +void JitSession::initNativeTargets() { static std::once_flag flag; std::call_once(flag, []() { static const llvm::codegen::RegisterCodeGenFlags CGF; @@ -235,7 +235,7 @@ void Session::initNativeTargets() { }); } -void Session::initialize() { +void JitSession::initialize() { registerRuntimeSymbols(); initNativeTargets(); @@ -378,14 +378,14 @@ void Session::initialize() { mainFn_ = mainAddr->toPtr(); } -void Session::deinitialize() { +void JitSession::deinitialize() { if (!jit_) { return; } if (auto err = jit_->deinitialize(jit_->getMainJITDylib())) { - llvm::errs() << "Session deinitialize failed: " + llvm::errs() << "JitSession deinitialize failed: " << llvm::toString(std::move(err)) << "\n"; } } -} // namespace qir::jit +} // namespace qir diff --git a/src/qir/runner/Runner.cpp b/src/qir/runner/Runner.cpp index 5f9d505b64..4ef8fc200e 100644 --- a/src/qir/runner/Runner.cpp +++ b/src/qir/runner/Runner.cpp @@ -41,7 +41,7 @@ auto main(int argc, char* argv[]) -> int { "qir interpreter & dynamic compiler\n"); try { - auto jitSession = qir::jit::Session(llvm::StringRef(InputFile)); + auto jitSession = qir::JitSession(llvm::StringRef(InputFile)); return jitSession.run(InputArgv, InputFile); } catch (const std::exception& e) { ExitOnError(llvm::createStringError(e.what())); From 1b8e2dcce749b7fd596aa447d54eadd0dc59e6a3 Mon Sep 17 00:00:00 2001 From: rturrado Date: Thu, 11 Jun 2026 01:43:36 +0200 Subject: [PATCH 20/33] Honor QDMI text/binary wire convention in PROGRAM payloads Restore the spec-faithful behavior the device had before this PR: text payloads (QASM2, QASM3, QIR Base/Adaptive String) ship over the wire with a trailing `'\0'` counted in `size`; binary payloads (QIR Base/Adaptive Module bitcode) ship the exact byte count, since `'\0'` may appear inside the payload. The earlier symmetric "always send / store `size`" approach worked end-to-end inside the device but diverged from the QDMI wire contract and broke C-style consumers that depend on the terminator. Implementation: - include/mqt-core/qdmi/devices/dd/ProgramFormat.hpp: new small header exposing `qdmi::dd::isTextProgramFormat(QDMI_Program_Format)`. - include/mqt-core/qdmi/devices/dd/Device.hpp: document on `setParameter` that callers must set `QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT` before `QDMI_DEVICE_JOB_PARAMETER_PROGRAM`. - src/qdmi/devices/dd/Device.cpp: in `setParameter`'s PROGRAM case, strip the trailing `'\0'` for text formats. - src/qdmi/devices/dd/CMakeLists.txt: expose `ProgramFormat.hpp` via the public `FILE_SET HEADERS`. - test/qdmi/devices/dd/helpers/test_utils.cpp: `setProgram` now ships `program.size() + 1` for text formats and `program.size()` for binary formats, matching the QDMI wire convention. The `+1` on the test side is safe because every existing text call site backs `program` with a string literal or `std::string`, both of which guarantee `'\0'` at `data()[size()]`. Assisted-by: Claude Opus 4.7 via Claude Code --- include/mqt-core/qdmi/devices/dd/Device.hpp | 9 ++++++ .../qdmi/devices/dd/ProgramFormat.hpp | 31 +++++++++++++++++++ src/qdmi/devices/dd/CMakeLists.txt | 1 + src/qdmi/devices/dd/Device.cpp | 10 +++++- test/qdmi/devices/dd/helpers/test_utils.cpp | 10 +++++- 5 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 include/mqt-core/qdmi/devices/dd/ProgramFormat.hpp diff --git a/include/mqt-core/qdmi/devices/dd/Device.hpp b/include/mqt-core/qdmi/devices/dd/Device.hpp index f4155f3ea1..00fed1ea2b 100644 --- a/include/mqt-core/qdmi/devices/dd/Device.hpp +++ b/include/mqt-core/qdmi/devices/dd/Device.hpp @@ -259,6 +259,15 @@ struct MQT_DDSIM_QDMI_Device_Job_impl_d { /** * @brief Sets a parameter for the job. + * @note When setting @c QDMI_DEVICE_JOB_PARAMETER_PROGRAM, the device uses + * the current @c QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT to decide whether + * the payload's wire @p size: + * - includes a trailing @c '\0' (text formats: QASM2, QASM3, + * QIR Base/Adaptive String) or + * - is the exact byte count (binary formats: QIR Base/Adaptive Module). + * Callers should therefore set @c PROGRAMFORMAT before @c PROGRAM. + * The default of @c QDMI_PROGRAM_FORMAT_QASM3 is assumed if @c PROGRAMFORMAT + * is not set. * @see MQT_DDSIM_QDMI_device_job_set_parameter */ auto setParameter(QDMI_Device_Job_Parameter param, size_t size, diff --git a/include/mqt-core/qdmi/devices/dd/ProgramFormat.hpp b/include/mqt-core/qdmi/devices/dd/ProgramFormat.hpp new file mode 100644 index 0000000000..d32bd09e62 --- /dev/null +++ b/include/mqt-core/qdmi/devices/dd/ProgramFormat.hpp @@ -0,0 +1,31 @@ +/* + * 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 "mqt_ddsim_qdmi/constants.h" + +namespace qdmi::dd { + +/** + * @brief Whether @p fmt is a text-based QDMI program format. + * @details QDMI program formats fall into two byte-shape categories: + * - text formats (QASM, QIR Base/Adaptive String) are shipped with a trailing + * '\0' counted in the buffer size. + * - binary formats (QIR Base/Adaptive Module bitcode) are shipped as exact byte + * counts since '\0' may appear inside the payload. + */ +inline bool isTextProgramFormat(QDMI_Program_Format fmt) { + return fmt == QDMI_PROGRAM_FORMAT_QASM2 || fmt == QDMI_PROGRAM_FORMAT_QASM3 || + fmt == QDMI_PROGRAM_FORMAT_QIRBASESTRING || + fmt == QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING; +} + +} // namespace qdmi::dd diff --git a/src/qdmi/devices/dd/CMakeLists.txt b/src/qdmi/devices/dd/CMakeLists.txt index 58735b0696..7c72f4649c 100644 --- a/src/qdmi/devices/dd/CMakeLists.txt +++ b/src/qdmi/devices/dd/CMakeLists.txt @@ -35,6 +35,7 @@ if(NOT TARGET ${TARGET_NAME}) ${CMAKE_CURRENT_BINARY_DIR}/include FILES ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/devices/dd/Device.hpp + ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/devices/dd/ProgramFormat.hpp ${QDMI_HDRS}) # Add link libraries diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index ca6d6b519e..4af3891d57 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -23,6 +23,7 @@ #include "mqt_ddsim_qdmi/device.h" #include "qasm3/Importer.hpp" #include "qdmi/common/Common.hpp" +#include "qdmi/devices/dd/ProgramFormat.hpp" #include #include @@ -370,7 +371,14 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::setParameter( return QDMI_SUCCESS; case QDMI_DEVICE_JOB_PARAMETER_PROGRAM: if (value != nullptr) { - program_ = std::string(static_cast(value), size); + // Text payloads include the trailing '\0' in `size`. + // Strip it so it is not counted in `program_.size()`. + // `std::string` re-synthesizes its own '\0' at `data()[size()]` for + // c_str() consumers. + // Binary payloads are stored exactly as received. + const auto bytes = + qdmi::dd::isTextProgramFormat(format_) ? size - 1 : size; + program_ = std::string(static_cast(value), bytes); } return QDMI_SUCCESS; case QDMI_DEVICE_JOB_PARAMETER_SHOTSNUM: diff --git a/test/qdmi/devices/dd/helpers/test_utils.cpp b/test/qdmi/devices/dd/helpers/test_utils.cpp index 527ad306f6..6435aae69d 100644 --- a/test/qdmi/devices/dd/helpers/test_utils.cpp +++ b/test/qdmi/devices/dd/helpers/test_utils.cpp @@ -12,6 +12,7 @@ #include "mqt_ddsim_qdmi/constants.h" #include "mqt_ddsim_qdmi/device.h" +#include "qdmi/devices/dd/ProgramFormat.hpp" #include @@ -97,8 +98,15 @@ int setProgram(MQT_DDSIM_QDMI_Device_Job job, const QDMI_Program_Format fmt, if (rc != QDMI_SUCCESS && rc != QDMI_ERROR_NOTSUPPORTED) { return rc; } + // Text payloads include the trailing '\0' per the QDMI wire convention. + // Binary payloads ship the exact byte count. + // The `+1` is safe here because every existing call to `setProgram` with a + // text format passes a `program` with a string literal or `std::string`, both + // of which guarantee `'\0'` at `data()[size()]`. + const auto bytesToSend = + qdmi::dd::isTextProgramFormat(fmt) ? program.size() + 1 : program.size(); rc = MQT_DDSIM_QDMI_device_job_set_parameter( - job, QDMI_DEVICE_JOB_PARAMETER_PROGRAM, program.size(), program.data()); + job, QDMI_DEVICE_JOB_PARAMETER_PROGRAM, bytesToSend, program.data()); return rc; } From e2a1fd743504d3490cb7aadb2e4715b4a7c6020a Mon Sep 17 00:00:00 2001 From: rturrado Date: Thu, 11 Jun 2026 02:09:08 +0200 Subject: [PATCH 21/33] Rename BUILD_MQT_CORE_QDMI_WITH_QIR to BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR. Update the option description to "Enable QIR program format support for the DDSim QDMI device" so it is clear the flag only affects the DDSim device, not all QDMI devices. Assisted-by: Claude Opus 4.7 via Claude Code --- CMakeLists.txt | 5 +++-- docs/qir/index.md | 2 +- include/mqt-core/qdmi/devices/dd/Device.hpp | 2 +- src/qdmi/devices/dd/CMakeLists.txt | 4 ++-- src/qdmi/devices/dd/Device.cpp | 8 ++++---- src/qir/CMakeLists.txt | 2 +- test/qdmi/devices/dd/CMakeLists.txt | 4 ++-- test/qdmi/devices/dd/job_parameters_test.cpp | 4 ++-- test/qdmi/devices/dd/results_sampling_test.cpp | 8 ++++---- 9 files changed, 20 insertions(+), 19 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1661d57247..0a5914f326 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,8 +121,9 @@ endif() cmake_dependent_option(BUILD_MQT_CORE_QIR_RUNNER "Build the QIR runner of the MQT Core project" ON "BUILD_MQT_CORE_MLIR" OFF) -cmake_dependent_option(BUILD_MQT_CORE_QDMI_WITH_QIR "Enable QIR format support for QDMI" OFF - "BUILD_MQT_CORE_MLIR" OFF) +cmake_dependent_option( + BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR "Enable QIR program format support for the DDSim QDMI device" + OFF "BUILD_MQT_CORE_MLIR" OFF) # add main library code add_subdirectory(src) diff --git a/docs/qir/index.md b/docs/qir/index.md index 85f9742664..bb8b8be4d5 100644 --- a/docs/qir/index.md +++ b/docs/qir/index.md @@ -40,5 +40,5 @@ The runner supports the QIR Base Profile. ### QIR Support in the QDMI Device The QDMI Device accepts jobs in the following program formats: QASM2, QASM3, QIR Base Profile Module (LLVM bitcode), and QIR Base Profile String (LLVM assembly). -The QIR base formats are only supported when the `BUILD_MQT_CORE_QDMI_WITH_QIR` CMake option is enabled. +The QIR base formats are only supported when the `BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR` CMake option is enabled. It is disabled by default to avoid the cost of linking against the MQT Core QIR JIT (built on LLVM OrcJIT) and Runtime libraries. diff --git a/include/mqt-core/qdmi/devices/dd/Device.hpp b/include/mqt-core/qdmi/devices/dd/Device.hpp index 00fed1ea2b..e0b19e1c3d 100644 --- a/include/mqt-core/qdmi/devices/dd/Device.hpp +++ b/include/mqt-core/qdmi/devices/dd/Device.hpp @@ -237,7 +237,7 @@ struct MQT_DDSIM_QDMI_Device_Job_impl_d { /// Helper function to submit a QASM 2 or QASM 3 program auto submitQASMProgram() -> QDMI_STATUS; -#ifdef BUILD_MQT_CORE_QDMI_WITH_QIR +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR /// Helper function to submit a QIR base module or string program auto submitQIRProgram() -> QDMI_STATUS; #endif diff --git a/src/qdmi/devices/dd/CMakeLists.txt b/src/qdmi/devices/dd/CMakeLists.txt index 7c72f4649c..e203c1ea68 100644 --- a/src/qdmi/devices/dd/CMakeLists.txt +++ b/src/qdmi/devices/dd/CMakeLists.txt @@ -41,9 +41,9 @@ if(NOT TARGET ${TARGET_NAME}) # Add link libraries target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreDD MQT::CoreQASM MQT::CoreCircuitOptimizer MQT::CoreQDMICommon spdlog::spdlog) - if(BUILD_MQT_CORE_QDMI_WITH_QIR) + if(BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR) target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQIRJIT MQT::CoreQIRRuntime) - target_compile_definitions(${TARGET_NAME} PRIVATE BUILD_MQT_CORE_QDMI_WITH_QIR) + 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 diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index 4af3891d57..54e4b01fc4 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -45,7 +45,7 @@ #include #include -#ifdef BUILD_MQT_CORE_QDMI_WITH_QIR +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR #include "qir/jit/Session.hpp" #include "qir/runtime/Runtime.hpp" @@ -357,7 +357,7 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::setParameter( } if (format != QDMI_PROGRAM_FORMAT_QASM2 && format != QDMI_PROGRAM_FORMAT_QASM3 -#ifdef BUILD_MQT_CORE_QDMI_WITH_QIR +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR && format != QDMI_PROGRAM_FORMAT_QIRBASEMODULE && format != QDMI_PROGRAM_FORMAT_QIRBASESTRING && format != QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE && @@ -445,7 +445,7 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQASMProgram() -> QDMI_STATUS { } return QDMI_SUCCESS; } -#ifdef BUILD_MQT_CORE_QDMI_WITH_QIR +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQIRProgram() -> QDMI_STATUS { if (numShots_ == 0) { return QDMI_ERROR_INVALIDARGUMENT; @@ -485,7 +485,7 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::submit() -> QDMI_STATUS { format_ == QDMI_PROGRAM_FORMAT_QASM3) { return submitQASMProgram(); } -#ifdef BUILD_MQT_CORE_QDMI_WITH_QIR +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR if (format_ == QDMI_PROGRAM_FORMAT_QIRBASEMODULE || format_ == QDMI_PROGRAM_FORMAT_QIRBASESTRING || format_ == QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE || diff --git a/src/qir/CMakeLists.txt b/src/qir/CMakeLists.txt index 7b948218ec..2c60f4191d 100644 --- a/src/qir/CMakeLists.txt +++ b/src/qir/CMakeLists.txt @@ -6,7 +6,7 @@ # # Licensed under the MIT License -if(BUILD_MQT_CORE_QIR_RUNNER OR BUILD_MQT_CORE_QDMI_WITH_QIR) +if(BUILD_MQT_CORE_QIR_RUNNER OR BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR) add_subdirectory(jit) endif() diff --git a/test/qdmi/devices/dd/CMakeLists.txt b/test/qdmi/devices/dd/CMakeLists.txt index 3bd86f99e8..50c44a3e40 100644 --- a/test/qdmi/devices/dd/CMakeLists.txt +++ b/test/qdmi/devices/dd/CMakeLists.txt @@ -33,11 +33,11 @@ if(TARGET MQT::CoreQDMI_DDSIM_Device) target_compile_definitions(${TARGET_NAME} PRIVATE MQT_CORE_VERSION="${MQT_CORE_VERSION}") target_include_directories(${TARGET_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - if(BUILD_MQT_CORE_QDMI_WITH_QIR) + if(BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR) llvm_map_components_to_libnames(llvm_native_libs asmparser bitwriter core support) target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQIRRuntime ${llvm_native_libs}) target_compile_definitions( - ${TARGET_NAME} PRIVATE BUILD_MQT_CORE_QDMI_WITH_QIR + ${TARGET_NAME} PRIVATE BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR QIR_FILES_DIR="${PROJECT_SOURCE_DIR}/test/circuits") endif() diff --git a/test/qdmi/devices/dd/job_parameters_test.cpp b/test/qdmi/devices/dd/job_parameters_test.cpp index bb5db9f395..044b13ec1e 100644 --- a/test/qdmi/devices/dd/job_parameters_test.cpp +++ b/test/qdmi/devices/dd/job_parameters_test.cpp @@ -93,7 +93,7 @@ TEST(JobParameters, ProgramFormatSupport) { for (QDMI_Program_Format fmt : { QDMI_PROGRAM_FORMAT_QASM2, QDMI_PROGRAM_FORMAT_QASM3, -#ifdef BUILD_MQT_CORE_QDMI_WITH_QIR +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR QDMI_PROGRAM_FORMAT_QIRBASESTRING, QDMI_PROGRAM_FORMAT_QIRBASEMODULE, QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING, @@ -108,7 +108,7 @@ TEST(JobParameters, ProgramFormatSupport) { // Unsupported → NOTSUPPORTED for (QDMI_Program_Format fmt : { -#ifndef BUILD_MQT_CORE_QDMI_WITH_QIR +#ifndef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR QDMI_PROGRAM_FORMAT_QIRBASESTRING, QDMI_PROGRAM_FORMAT_QIRBASEMODULE, QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING, diff --git a/test/qdmi/devices/dd/results_sampling_test.cpp b/test/qdmi/devices/dd/results_sampling_test.cpp index eb18f9c4e2..fe9ce96b5c 100644 --- a/test/qdmi/devices/dd/results_sampling_test.cpp +++ b/test/qdmi/devices/dd/results_sampling_test.cpp @@ -27,7 +27,7 @@ #include #include -#ifdef BUILD_MQT_CORE_QDMI_WITH_QIR +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR #include "qir/runtime/Runtime.hpp" #include @@ -47,7 +47,7 @@ namespace { class HistogramTest : public ::testing::Test { protected: -#ifdef BUILD_MQT_CORE_QDMI_WITH_QIR +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR std::ostringstream sink; void SetUp() override { qir::Runtime::getInstance().setOstream(sink); } void TearDown() override { qir::Runtime::getInstance().resetOstream(); } @@ -82,7 +82,7 @@ class HistogramTest : public ::testing::Test { } }; -#ifdef BUILD_MQT_CORE_QDMI_WITH_QIR +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR class QIRHistogramTestModule : public HistogramTest { protected: static std::string getProgram(const std::string_view file) { @@ -127,7 +127,7 @@ TEST_F(HistogramTest, QASM3Program) { checkHistogram(runProgram(format, program)); } -#ifdef BUILD_MQT_CORE_QDMI_WITH_QIR +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR TEST_F(QIRHistogramTestModule, BaseStatic) { constexpr auto format = QDMI_PROGRAM_FORMAT_QIRBASEMODULE; checkHistogram(runProgram(format, getProgram("BellPairStatic.ll"))); From b6144fee5e9add0c4a8c21eda4f58298df22d315 Mon Sep 17 00:00:00 2001 From: rturrado Date: Wed, 10 Jun 2026 20:44:02 +0200 Subject: [PATCH 22/33] Add QIR IR rewriter for stripping measurements Introduce `qir::stripMeasurementsAndRecording`, an in-place LLVM IR transform that erases QIR measurement and result-management calls from a module: the measurement intrinsics (`__quantum__qis__mz__body`, `__quantum__qis__m__body`, `__quantum__qis__measure__body`), the result-recording call (`__quantum__rt__result_record_output`), and the result reference-count update call (`__quantum__rt__result_update_reference_count`). The transform is intended for QIR Base Profile programs and is the first step toward supporting `numShots_ == 0` for QIR submissions in the DDSim QDMI device: stripping the measurements lets the JIT'd `main` run once and leave the resulting state in the `qir::Runtime` DD instead of collapsing it. The rewriter is added but not yet wired into `qir::JitSession` or the device's `submitQIRProgram` path; that follows. Files: - include/mqt-core/qir/jit/IRRewriter.hpp: public declaration with docstring noting the Base Profile restriction. - src/qir/jit/IRRewriter.cpp: single-pass implementation using `llvm::make_early_inc_range`; `replaceAllUsesWith(null)` guards against `m`/`measure` consumers we did not strip. - src/qir/jit/CMakeLists.txt: add the new source to PRIVATE `target_sources` and the new header to the PUBLIC `FILE_SET HEADERS`. Assisted-by: Claude Opus 4.7 via Claude Code --- include/mqt-core/qir/jit/IRRewriter.hpp | 40 ++++++++++++++ src/qir/jit/CMakeLists.txt | 13 +++-- src/qir/jit/IRRewriter.cpp | 69 +++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 include/mqt-core/qir/jit/IRRewriter.hpp create mode 100644 src/qir/jit/IRRewriter.cpp diff --git a/include/mqt-core/qir/jit/IRRewriter.hpp b/include/mqt-core/qir/jit/IRRewriter.hpp new file mode 100644 index 0000000000..e4f76abdf9 --- /dev/null +++ b/include/mqt-core/qir/jit/IRRewriter.hpp @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include + +#include + +namespace qir { + +/** + * @brief Strips QIR measurement and result-management calls from @p m + * in place. + * @details Erases calls to the QIR measurement intrinsics, to the + * result-recording intrinsic, and to the result reference-count update + * intrinsic (whose Result operands would otherwise reference the null + * pointers left by the stripped measurements). + * Intended for QIR Base Profile programs only: in Adaptive Profile programs, + * measurement results feed classical control flow, so removing them silently + * changes observable behavior. + * + * The typical use is to prepare a Base Profile module for state-vector + * extraction: after this transform the JIT'd @c main can be run once and + * the resulting state remains in the @ref qir::Runtime DD instead of being + * collapsed by measurement. + * + * @param m Module to rewrite in place. + * @return Number of instructions erased. + */ +std::size_t stripMeasurementsAndRecording(llvm::Module& m); + +} // namespace qir diff --git a/src/qir/jit/CMakeLists.txt b/src/qir/jit/CMakeLists.txt index 4cfd646176..6795fc58c1 100644 --- a/src/qir/jit/CMakeLists.txt +++ b/src/qir/jit/CMakeLists.txt @@ -13,11 +13,18 @@ if(NOT TARGET ${TARGET_NAME}) add_mqt_core_library(${TARGET_NAME} ALIAS_NAME QIRJIT) # Add sources to target - target_sources(${TARGET_NAME} PRIVATE Session.cpp) + target_sources(${TARGET_NAME} PRIVATE Session.cpp IRRewriter.cpp) # Add headers using file sets - target_sources(${TARGET_NAME} PUBLIC FILE_SET HEADERS BASE_DIRS ${MQT_CORE_INCLUDE_BUILD_DIR} - FILES ${MQT_CORE_INCLUDE_BUILD_DIR}/qir/jit/Session.hpp) + target_sources( + ${TARGET_NAME} + PUBLIC FILE_SET + HEADERS + BASE_DIRS + ${MQT_CORE_INCLUDE_BUILD_DIR} + FILES + ${MQT_CORE_INCLUDE_BUILD_DIR}/qir/jit/Session.hpp + ${MQT_CORE_INCLUDE_BUILD_DIR}/qir/jit/IRRewriter.hpp) # Get the LLVM native target libraries llvm_map_components_to_libnames( diff --git a/src/qir/jit/IRRewriter.cpp b/src/qir/jit/IRRewriter.cpp new file mode 100644 index 0000000000..3856ee72db --- /dev/null +++ b/src/qir/jit/IRRewriter.cpp @@ -0,0 +1,69 @@ +/* + * 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 "qir/jit/IRRewriter.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace qir { + +static constexpr std::array STRIP_TARGETS = { + "__quantum__qis__mz__body", + "__quantum__qis__m__body", + "__quantum__qis__measure__body", + "__quantum__rt__result_record_output", + "__quantum__rt__result_update_reference_count", +}; + +static bool isStripTarget(const llvm::CallInst& call) { + const auto* callee = call.getCalledFunction(); + if (callee == nullptr) { + return false; + } + const auto name = callee->getName(); + return std::ranges::any_of( + STRIP_TARGETS, [&](llvm::StringRef target) { return name == target; }); +} + +std::size_t stripMeasurementsAndRecording(llvm::Module& m) { + std::size_t erased = 0; + for (auto& fn : m) { + for (auto& bb : fn) { + for (auto& inst : llvm::make_early_inc_range(bb)) { + auto* call = llvm::dyn_cast(&inst); + if (call == nullptr || !isStripTarget(*call)) { + continue; + } + // `m` and `measure` return a `Result*`. + // Replace uses of `m` and `measure` return values with a null before + // erasure so they do not dangle. + if (!call->getType()->isVoidTy() && !call->use_empty()) { + call->replaceAllUsesWith( + llvm::Constant::getNullValue(call->getType())); + } + call->eraseFromParent(); + ++erased; + } + } + } + return erased; +} + +} // namespace qir From 2b851ef675fce66dc8f1f8b539146b196cdb1607 Mon Sep 17 00:00:00 2001 From: rturrado Date: Wed, 10 Jun 2026 21:34:57 +0200 Subject: [PATCH 23/33] Add unit tests for the QIR IR rewriter Parametrized GoogleTest fixture that loads a `.ll` file from `test/circuits`, counts calls to each strip target, runs `qir::stripMeasurementRelatedCalls`, and asserts that the returned count equals the pre-strip count and that no strip-target calls remain. Cases: - `BellPairStatic.ll`: covers the canonical Base Profile shape (`mz` + `record_output`, no refcount). - `BellPairDynamic.ll`: covers the dynamic-result shape (`m` + `record_output` + `result_update_reference_count`) and exercises the `replaceAllUsesWith(null)` branch for the `m` return values. While adding the test, consolidate `STRIP_TARGETS` (previously a `static constexpr` in `IRRewriter.cpp`) into the public header `IRRewriter.hpp` as `inline constexpr`. Both the implementation and the test now read the same list; the rewriter's contract is documented in machine-readable form. Without this, the test could silently miss a new strip target since `before` and `erased` would both be off by the same amount. Files: - include/mqt-core/qir/jit/IRRewriter.hpp: add `STRIP_TARGETS` as `inline constexpr` and the supporting includes. - src/qir/jit/IRRewriter.cpp: drop the duplicate constant and the now-unused `` include. - test/qir/jit/test_ir_rewriter.cpp: the test fixture; uses `qir::STRIP_TARGETS` from the header. - test/qir/jit/CMakeLists.txt: gated by `if(TARGET MQT::CoreQIRJIT)`; links the LLVM IR-parsing libs and sets `QIR_FILES_DIR` to point at the shared fixture directory. - test/qir/CMakeLists.txt: add the new `jit` subdirectory next to `runtime` and `runner`, and gate `jit`/`runner` under the same options used in `src/qir/CMakeLists.txt` (`BUILD_MQT_CORE_QIR_RUNNER`, `BUILD_MQT_CORE_QDMI_WITH_QIR`) so the test layout mirrors the source layout. Assisted-by: Claude Opus 4.7 via Claude Code --- include/mqt-core/qir/jit/IRRewriter.hpp | 16 +++- src/qir/jit/IRRewriter.cpp | 11 +-- test/qir/CMakeLists.txt | 9 ++- test/qir/jit/CMakeLists.txt | 20 +++++ test/qir/jit/test_ir_rewriter.cpp | 98 +++++++++++++++++++++++++ 5 files changed, 140 insertions(+), 14 deletions(-) create mode 100644 test/qir/jit/CMakeLists.txt create mode 100644 test/qir/jit/test_ir_rewriter.cpp diff --git a/include/mqt-core/qir/jit/IRRewriter.hpp b/include/mqt-core/qir/jit/IRRewriter.hpp index e4f76abdf9..d144f0fe45 100644 --- a/include/mqt-core/qir/jit/IRRewriter.hpp +++ b/include/mqt-core/qir/jit/IRRewriter.hpp @@ -10,15 +10,25 @@ #pragma once +#include #include +#include #include namespace qir { +/// The set of call targets that @ref stripMeasurementRelatedCalls erases. +inline constexpr std::array STRIP_TARGETS = { + "__quantum__qis__mz__body", + "__quantum__qis__m__body", + "__quantum__qis__measure__body", + "__quantum__rt__result_record_output", + "__quantum__rt__result_update_reference_count", +}; + /** - * @brief Strips QIR measurement and result-management calls from @p m - * in place. + * @brief Strips QIR measurement-related calls from @p m in place. * @details Erases calls to the QIR measurement intrinsics, to the * result-recording intrinsic, and to the result reference-count update * intrinsic (whose Result operands would otherwise reference the null @@ -35,6 +45,6 @@ namespace qir { * @param m Module to rewrite in place. * @return Number of instructions erased. */ -std::size_t stripMeasurementsAndRecording(llvm::Module& m); +std::size_t stripMeasurementRelatedCalls(llvm::Module& m); } // namespace qir diff --git a/src/qir/jit/IRRewriter.cpp b/src/qir/jit/IRRewriter.cpp index 3856ee72db..e7a98cb1e3 100644 --- a/src/qir/jit/IRRewriter.cpp +++ b/src/qir/jit/IRRewriter.cpp @@ -19,19 +19,10 @@ #include #include -#include #include namespace qir { -static constexpr std::array STRIP_TARGETS = { - "__quantum__qis__mz__body", - "__quantum__qis__m__body", - "__quantum__qis__measure__body", - "__quantum__rt__result_record_output", - "__quantum__rt__result_update_reference_count", -}; - static bool isStripTarget(const llvm::CallInst& call) { const auto* callee = call.getCalledFunction(); if (callee == nullptr) { @@ -42,7 +33,7 @@ static bool isStripTarget(const llvm::CallInst& call) { STRIP_TARGETS, [&](llvm::StringRef target) { return name == target; }); } -std::size_t stripMeasurementsAndRecording(llvm::Module& m) { +std::size_t stripMeasurementRelatedCalls(llvm::Module& m) { std::size_t erased = 0; for (auto& fn : m) { for (auto& bb : fn) { diff --git a/test/qir/CMakeLists.txt b/test/qir/CMakeLists.txt index d5fce427ab..2c60f4191d 100644 --- a/test/qir/CMakeLists.txt +++ b/test/qir/CMakeLists.txt @@ -6,5 +6,12 @@ # # Licensed under the MIT License +if(BUILD_MQT_CORE_QIR_RUNNER OR BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR) + add_subdirectory(jit) +endif() + add_subdirectory(runtime) -add_subdirectory(runner) + +if(BUILD_MQT_CORE_QIR_RUNNER) + add_subdirectory(runner) +endif() diff --git a/test/qir/jit/CMakeLists.txt b/test/qir/jit/CMakeLists.txt new file mode 100644 index 0000000000..c9ef7f5307 --- /dev/null +++ b/test/qir/jit/CMakeLists.txt @@ -0,0 +1,20 @@ +# 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 + +if(TARGET MQT::CoreQIRJIT) + set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-qir-jit-test) + + # Get the LLVM native target libraries + llvm_map_components_to_libnames(llvm_native_libs_for_test asmparser core irreader support) + + package_add_test(${TARGET_NAME} MQT::CoreQIRJIT test_ir_rewriter.cpp) + target_link_libraries(${TARGET_NAME} PRIVATE ${llvm_native_libs_for_test}) + + target_compile_definitions(${TARGET_NAME} + PRIVATE QIR_FILES_DIR="${PROJECT_SOURCE_DIR}/test/circuits") +endif() diff --git a/test/qir/jit/test_ir_rewriter.cpp b/test/qir/jit/test_ir_rewriter.cpp new file mode 100644 index 0000000000..7c1c7ba698 --- /dev/null +++ b/test/qir/jit/test_ir_rewriter.cpp @@ -0,0 +1,98 @@ +/* + * 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 "qir/jit/IRRewriter.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +std::size_t countCallsTo(const llvm::Module& m, llvm::StringRef name) { + std::size_t count = 0; + for (const auto& fn : m) { + for (const auto& bb : fn) { + for (const auto& inst : bb) { + const auto* call = llvm::dyn_cast(&inst); + if (call == nullptr) { + continue; + } + const auto* callee = call->getCalledFunction(); + if (callee != nullptr && callee->getName() == name) { + ++count; + } + } + } + } + return count; +} + +std::size_t countCallsToStripTarget(const llvm::Module& m) { + return std::accumulate(qir::STRIP_TARGETS.begin(), qir::STRIP_TARGETS.end(), + std::size_t{}, + [&m](std::size_t total, const auto& target) { + return total + countCallsTo(m, target); + }); +} + +std::unique_ptr loadIRFile(const std::filesystem::path& path, + llvm::LLVMContext& ctx) { + llvm::SMDiagnostic err; + auto llvmModule = llvm::parseIRFile(path.string(), err, ctx); + if (!llvmModule) { + std::string errStr; + llvm::raw_string_ostream s(errStr); + err.print("test_ir_rewriter", s); + throw std::runtime_error("Failed to parse IR file " + path.string() + ": " + + errStr); + } + return llvmModule; +} + +class IRRewriterTest : public testing::TestWithParam { +protected: + llvm::LLVMContext ctx_; +}; + +TEST_P(IRRewriterTest, StripMeasurementRelatedCalls) { + const std::filesystem::path path = + std::filesystem::path(QIR_FILES_DIR) / GetParam(); + auto llvmModule = loadIRFile(path, ctx_); + + const auto numStripCalls = countCallsToStripTarget(*llvmModule); + ASSERT_GT(numStripCalls, 0U) << "Module has no calls to strip targets"; + + const auto numErased = qir::stripMeasurementRelatedCalls(*llvmModule); + + EXPECT_EQ(numErased, numStripCalls); + EXPECT_EQ(countCallsToStripTarget(*llvmModule), 0U); +} + +INSTANTIATE_TEST_SUITE_P(BellPair, IRRewriterTest, + testing::Values("BellPairStatic.ll", + "BellPairDynamic.ll")); + +} // namespace From cb4abba9ec38a755662d6edc2b170a9db900f041 Mon Sep 17 00:00:00 2001 From: rturrado Date: Thu, 11 Jun 2026 14:03:12 +0200 Subject: [PATCH 24/33] Add Execution mode to JitSession Introduce a `qir::Execution` enum with `Sampling` and `StateExtraction` values, accepted by both `JitSession` constructors and defaulting to `Sampling`. In `StateExtraction` mode the session calls `stripMeasurementRelatedCalls` on the loaded module before JIT-compiling, so the QIR runtime's quantum state remains intact after `main` returns. The mode is the first half of supporting `numShots_ == 0` for QIR Base Profile in the DDSim QDMI device; the device wiring comes in a follow-up commit. Implementation: - include/mqt-core/qir/jit/Session.hpp: add the `Execution` enum, and route `mode` through both constructors and through `initialize`. - src/qir/jit/Session.cpp: include `IRRewriter.hpp`; fold the shared constructor body into the existing `initialize`, which now takes the `Expected` plus the mode, validates the module, conditionally strips measurements, then builds the JIT. - test/qir/jit/test_jit_session.cpp: new fixture with two cases that JIT-execute `BellPairStatic.ll`: `Sampling` populates the runtime's recorded outputs, `StateExtraction` leaves them empty. - test/qir/jit/CMakeLists.txt: register the new source and link `MQT::CoreQIRRuntime` so the test can read `qir::Runtime` state. Assisted-by: Claude Opus 4.7 via Claude Code --- include/mqt-core/qir/jit/Session.hpp | 46 +++++++++++++++++++++++--- src/qir/jit/Session.cpp | 37 +++++++++++---------- test/qir/jit/CMakeLists.txt | 4 +-- test/qir/jit/test_jit_session.cpp | 48 ++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 22 deletions(-) create mode 100644 test/qir/jit/test_jit_session.cpp diff --git a/include/mqt-core/qir/jit/Session.hpp b/include/mqt-core/qir/jit/Session.hpp index ce7712f0b9..0e8bb4c2da 100644 --- a/include/mqt-core/qir/jit/Session.hpp +++ b/include/mqt-core/qir/jit/Session.hpp @@ -22,6 +22,17 @@ namespace qir { +/** + * @brief Whether the JIT'd program runs to produce measurement samples or + * to leave the final quantum state in @ref qir::Runtime for external + * extraction. + * @details In @c StateExtraction mode the session strips QIR measurement + * and result-management calls from the IR before JIT-compiling, so the + * runtime's quantum state remains intact after @c main returns. Intended + * for QIR Base Profile programs only. + */ +enum class Execution { Sampling, StateExtraction }; + /** * @brief In-process JIT executor for QIR programs. * @details The session does the following, in order: @@ -41,10 +52,12 @@ class JitSession { /** * @brief Build a session by loading IR from a file on disk. * @param inputFile Path to a textual IR or bitcode file. + * @param mode Execution mode; see @ref Execution. * @throws std::runtime_error if the file cannot be parsed or the JIT fails * to initialize. */ - explicit JitSession(llvm::StringRef inputFile); + explicit JitSession(llvm::StringRef inputFile, + Execution mode = Execution::Sampling); /** * @brief Build a session by loading IR from a memory buffer. @@ -52,10 +65,12 @@ class JitSession { * to be null-terminated. * @param irBytes Byte view of the IR. * @param bufferName Identifier used in diagnostics. + * @param mode Execution mode; see @ref Execution. * @throws std::runtime_error if the IR cannot be parsed or the JIT fails * to initialize. */ - JitSession(llvm::StringRef irBytes, llvm::StringRef bufferName); + JitSession(llvm::StringRef irBytes, llvm::StringRef bufferName, + Execution mode = Execution::Sampling); /// Tears down the LLJIT and any JIT'd resources owned by the session. ~JitSession(); @@ -75,14 +90,37 @@ class JitSession { std::unique_ptr jit_; MainFn* mainFn_ = nullptr; + /// Registers the QIR runtime symbols with @c llvm::sys::DynamicLibrary so the + /// JIT can resolve them at link time. + /// Safe to call multiple times; the work runs only on the first call. static void registerRuntimeSymbols(); + + /// Initializes the native target, asm printer and asm parser. + /// Safe to call multiple times; the work runs only on the first call. static void initNativeTargets(); + + /// Parses LLVM IR from @p irPath using the session's thread-safe context. llvm::Expected loadModuleFromFile(llvm::StringRef irPath); + + /// Parses LLVM IR (textual or bitcode) from @p irBytes using the session's + /// thread-safe context. @p bufferName is used in diagnostics. llvm::Expected loadModuleFromMemory(llvm::StringRef irBytes, llvm::StringRef bufferName); - void initialize(); - void deinitialize(); + + /// Prepares the session to run the program: + /// - Validates the loaded module. + /// - Optionally strips measurement and result management calls + /// (for @c Execution::StateExtraction). + /// - Builds the @c LLJIT instance + /// - Registers QIR runtime symbols + /// - Resolves @c main. + /// @throws std::runtime_error if loading failed or the JIT cannot start. + void initialize(llvm::Expected llvmModule, + Execution mode); + + /// Tears down the @c LLJIT. + void deinitialize() const; }; } // namespace qir diff --git a/src/qir/jit/Session.cpp b/src/qir/jit/Session.cpp index 7d0c9b5c92..322a79ec68 100644 --- a/src/qir/jit/Session.cpp +++ b/src/qir/jit/Session.cpp @@ -10,6 +10,7 @@ #include "qir/jit/Session.hpp" +#include "qir/jit/IRRewriter.hpp" #include "qir/runtime/QIR.h" #include @@ -117,23 +118,13 @@ JitSession::loadModuleFromMemory(const llvm::StringRef irBytes, return getThreadSafeModuleOrError(std::move(m), err, tsCtx_); } -JitSession::JitSession(const llvm::StringRef inputFile) { - auto ret = loadModuleFromFile(inputFile); - if (!ret) { - throw std::runtime_error(llvm::toString(ret.takeError())); - } - module_ = std::move(*ret); - initialize(); +JitSession::JitSession(const llvm::StringRef inputFile, const Execution mode) { + initialize(loadModuleFromFile(inputFile), mode); } JitSession::JitSession(const llvm::StringRef irBytes, - const llvm::StringRef bufferName) { - auto ret = loadModuleFromMemory(irBytes, bufferName); - if (!ret) { - throw std::runtime_error(llvm::toString(ret.takeError())); - } - module_ = std::move(*ret); - initialize(); + const llvm::StringRef bufferName, const Execution mode) { + initialize(loadModuleFromMemory(irBytes, bufferName), mode); } JitSession::~JitSession() { deinitialize(); } @@ -235,7 +226,21 @@ void JitSession::initNativeTargets() { }); } -void JitSession::initialize() { +void JitSession::initialize( + llvm::Expected llvmModule, + const Execution mode) { + if (!llvmModule) { + throw std::runtime_error(llvm::toString(llvmModule.takeError())); + } + module_ = std::move(*llvmModule); + + // In StateExtraction mode, strip QIR measurement and result-management calls + // so the runtime's quantum state remains intact after main returns. + if (mode == Execution::StateExtraction) { + module_.withModuleDo( + [](llvm::Module& m) { stripMeasurementRelatedCalls(m); }); + } + registerRuntimeSymbols(); initNativeTargets(); @@ -378,7 +383,7 @@ void JitSession::initialize() { mainFn_ = mainAddr->toPtr(); } -void JitSession::deinitialize() { +void JitSession::deinitialize() const { if (!jit_) { return; } diff --git a/test/qir/jit/CMakeLists.txt b/test/qir/jit/CMakeLists.txt index c9ef7f5307..8a1eeaabe9 100644 --- a/test/qir/jit/CMakeLists.txt +++ b/test/qir/jit/CMakeLists.txt @@ -12,8 +12,8 @@ if(TARGET MQT::CoreQIRJIT) # Get the LLVM native target libraries llvm_map_components_to_libnames(llvm_native_libs_for_test asmparser core irreader support) - package_add_test(${TARGET_NAME} MQT::CoreQIRJIT test_ir_rewriter.cpp) - target_link_libraries(${TARGET_NAME} PRIVATE ${llvm_native_libs_for_test}) + package_add_test(${TARGET_NAME} MQT::CoreQIRJIT test_ir_rewriter.cpp test_jit_session.cpp) + target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQIRRuntime ${llvm_native_libs_for_test}) target_compile_definitions(${TARGET_NAME} PRIVATE QIR_FILES_DIR="${PROJECT_SOURCE_DIR}/test/circuits") diff --git a/test/qir/jit/test_jit_session.cpp b/test/qir/jit/test_jit_session.cpp new file mode 100644 index 0000000000..737e86a00a --- /dev/null +++ b/test/qir/jit/test_jit_session.cpp @@ -0,0 +1,48 @@ +/* + * 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 "qir/jit/Session.hpp" +#include "qir/runtime/Runtime.hpp" + +#include + +#include +#include + +namespace { + +class JitSessionExecutionMode : public testing::Test { +protected: + std::ostringstream sink; + + void SetUp() override { + auto& runtime = qir::Runtime::getInstance(); + runtime.reset(); + runtime.setOstream(sink); + } + void TearDown() override { qir::Runtime::getInstance().resetOstream(); } +}; + +TEST_F(JitSessionExecutionMode, SamplingRecordsOutputs) { + const auto path = std::filesystem::path(QIR_FILES_DIR) / "BellPairStatic.ll"; + // qir::Execution::Sampling is the default Execution mode + const qir::JitSession session(path.string()); + ASSERT_EQ(session.run(), 0); + EXPECT_FALSE(qir::Runtime::getInstance().getRecordedOutputs().empty()); +} + +TEST_F(JitSessionExecutionMode, StateExtractionLeavesNoRecordedOutputs) { + const auto path = std::filesystem::path(QIR_FILES_DIR) / "BellPairStatic.ll"; + const qir::JitSession session(path.string(), qir::Execution::StateExtraction); + ASSERT_EQ(session.run(), 0); + EXPECT_TRUE(qir::Runtime::getInstance().getRecordedOutputs().empty()); +} + +} // namespace From 5e42342dfdb805075bc576aa4ff8123a44c5c41d Mon Sep 17 00:00:00 2001 From: rturrado Date: Thu, 11 Jun 2026 15:11:43 +0200 Subject: [PATCH 25/33] Add Runtime::takeState for state extraction Introduce a `qir::Runtime::QState` nested struct bundling the DD package, the root edge and the qubit count, and a `takeState` method that moves the runtime's quantum state out and resets the runtime to a fresh slate. This is the second half of supporting `numShots_ == 0` for QIR Base Profile in the DDSim QDMI device: after a `JitSession` constructed with `Execution::StateExtraction` finishes running, the caller can pull the surviving quantum state out of the runtime with `takeState` and use it directly. While here, bundle the three pre-existing separate fields (`dd`, `qState` as a `dd::vEdge`, `numQubitsInQState`) into the new `QState` struct so they are owned, moved and reset as a unit. A `QState::reset` method encapsulates the "if dd is null, allocate fresh; otherwise decRef + garbageCollect" logic so `Runtime::reset` does not have to duplicate it. Implementation: - include/mqt-core/qir/runtime/Runtime.hpp: add the `QState` struct with `dd`, `edge`, `numQubits` and a `reset` method; replace the three field declarations with a single `QState qState` member; declare `takeState`. - src/qir/runtime/Runtime.cpp: `Runtime::reset` now defers the DD bit to `qState.reset()`; `takeState` moves `qState` out, then calls `reset` so the runtime is ready for the next job; the constructor no longer initializes `qState` explicitly because `QState` has its own default constructor now; `enlargeState` accesses through the new struct. - test/qir/runtime/test_qir_runtime.cpp: new `TakeStateReturnsStateAndResetsRuntime` test that drives a small program through the runtime, calls `takeState`, asserts the returned `QState` is populated, then verifies the runtime is in a clean state by running another tiny program. Assisted-by: Claude Opus 4.7 via Claude Code --- include/mqt-core/qir/runtime/Runtime.hpp | 49 +++++++++++++++--- src/qir/runtime/Runtime.cpp | 66 ++++++++++++------------ test/qir/runtime/test_qir_runtime.cpp | 16 ++++++ 3 files changed, 91 insertions(+), 40 deletions(-) diff --git a/include/mqt-core/qir/runtime/Runtime.hpp b/include/mqt-core/qir/runtime/Runtime.hpp index 8849561b5f..0f96416807 100644 --- a/include/mqt-core/qir/runtime/Runtime.hpp +++ b/include/mqt-core/qir/runtime/Runtime.hpp @@ -196,6 +196,37 @@ class Runtime { static constexpr uintptr_t RESULT_ZERO_ADDRESS = 0x10000; static constexpr uintptr_t RESULT_ONE_ADDRESS = 0x10001; + /// The quantum state held by the runtime: + /// - a DD package, + /// - the root edge into that package, and + /// - the number of qubits the state spans. + struct QState { + std::unique_ptr dd; + dd::vEdge edge; + dd::Qubit numQubits; + + QState() + : dd(std::make_unique()), edge(dd::vEdge::one()), + numQubits(0) {} + + /// Reset to a fresh empty state. + /// If @c dd is currently populated, the existing package's `decRef` plus + /// `garbageCollect` path is used so the package (and its internal caches) + /// is kept warm. + /// If @c dd was moved out (e.g. by @ref Runtime::takeState), a new package + /// is allocated. + auto reset() -> void { + if (dd) { + dd->decRef(edge); + dd->garbageCollect(); + } else { + dd = std::make_unique(); + } + edge = dd::vEdge::one(); + numQubits = 0; + } + }; + private: static constexpr uintptr_t MIN_DYN_QUBIT_ADDRESS = 0x10000; enum class AddressMode : uint8_t { UNKNOWN, DYNAMIC, STATIC }; @@ -210,9 +241,7 @@ class Runtime { uintptr_t currentMaxQubitAddress; qc::Qubit currentMaxQubitId; uintptr_t currentMaxResultAddress; - dd::Qubit numQubitsInQState; - std::unique_ptr dd; - dd::vEdge qState; + QState qState; std::mt19937_64 mt; std::ostream* os = &std::cout; @@ -274,7 +303,7 @@ class Runtime { auto apply(Args&&... args) -> void { const qc::StandardOperation& operation = createOperation(std::forward(args)...); - qState = applyUnitaryOperation(operation, qState, *dd); + qState.edge = applyUnitaryOperation(operation, qState.edge, *qState.dd); } template auto measure(Args... args) -> void { const auto& qubits = Utils::packOfType(args...); @@ -298,8 +327,8 @@ class Runtime { // measure qubits Utils::apply2( [&](const auto q, auto& r) { - const auto& result = - dd->measureOneCollapsing(qState, static_cast(q), mt); + const auto& result = qState.dd->measureOneCollapsing( + qState.edge, static_cast(q), mt); deref(r).r = result == '1'; }, targets, results); @@ -311,7 +340,7 @@ class Runtime { } const qc::NonUnitaryOperation resetOp( {targets.data(), targets.data() + SIZE}, qc::Reset); - qState = applyReset(resetOp, qState, *dd, mt); + qState.edge = applyReset(resetOp, qState.edge, *qState.dd, mt); } auto swap(Qubit* qubit1, Qubit* qubit2) -> void; auto qAlloc() -> Qubit*; @@ -370,6 +399,12 @@ class Runtime { /// order. auto getRecordedOutputs() const -> const std::string&; + /// Move the quantum state out of the runtime. + /// Then reset the runtime to a clean state ready for the next job. + /// Intended for use after a @c JitSession constructed with + /// @c Execution::StateExtraction has finished running. + auto takeState() -> QState; + auto getOstream() -> std::ostream&; auto setOstream(std::ostream& other) -> void; auto resetOstream() -> void; diff --git a/src/qir/runtime/Runtime.cpp b/src/qir/runtime/Runtime.cpp index 8b81f742a4..0e3caf1d5a 100644 --- a/src/qir/runtime/Runtime.cpp +++ b/src/qir/runtime/Runtime.cpp @@ -50,23 +50,20 @@ Runtime& Runtime::getInstance() { } auto Runtime::reset() -> void { addressMode = AddressMode::UNKNOWN; - currentMaxQubitAddress = MIN_DYN_QUBIT_ADDRESS; - currentMaxQubitId = 0; - currentMaxResultAddress = MIN_DYN_RESULT_ADDRESS; - numQubitsInQState = 0; - dd->decRef(qState); - dd->garbageCollect(); - qState = dd::vEdge::one(); - mt.seed(generateRandomSeed()); qRegister.clear(); rRegister.clear(); - recordedOutputs.clear(); // NOLINTBEGIN(performance-no-int-to-ptr) rRegister.emplace(reinterpret_cast(RESULT_ZERO_ADDRESS), ResultStruct{.refcount = 0, .r = false}); rRegister.emplace(reinterpret_cast(RESULT_ONE_ADDRESS), ResultStruct{.refcount = 0, .r = true}); // NOLINTEND(performance-no-int-to-ptr) + recordedOutputs.clear(); + currentMaxQubitAddress = MIN_DYN_QUBIT_ADDRESS; + currentMaxQubitId = 0; + currentMaxResultAddress = MIN_DYN_RESULT_ADDRESS; + qState.reset(); + mt.seed(generateRandomSeed()); } Runtime::Runtime() : Runtime(generateRandomSeed()) {} @@ -74,9 +71,7 @@ Runtime::Runtime() : Runtime(generateRandomSeed()) {} Runtime::Runtime(const uint64_t randomSeed) : addressMode(AddressMode::UNKNOWN), currentMaxQubitAddress(MIN_DYN_QUBIT_ADDRESS), currentMaxQubitId(0), - currentMaxResultAddress(MIN_DYN_RESULT_ADDRESS), numQubitsInQState(0), - dd(std::make_unique()), qState(dd::vEdge::one()), - mt(randomSeed) { + currentMaxResultAddress(MIN_DYN_RESULT_ADDRESS), mt(randomSeed) { qRegister = std::unordered_map(); rRegister = std::unordered_map(); // NOLINTBEGIN(performance-no-int-to-ptr) @@ -88,32 +83,31 @@ Runtime::Runtime(const uint64_t randomSeed) } auto Runtime::enlargeState(const std::uint64_t maxQubit) -> void { - if (maxQubit >= numQubitsInQState) { - const auto d = maxQubit - numQubitsInQState + 1; - qubitPermutation.resize(numQubitsInQState + d); - std::iota(qubitPermutation.begin() + - static_cast::difference_type>( - numQubitsInQState), - qubitPermutation.end(), numQubitsInQState); - numQubitsInQState += static_cast(d); - - // resize the DD package only if necessary - if (dd->qubits() < numQubitsInQState) { - dd->resize(numQubitsInQState); + if (maxQubit >= qState.numQubits) { + const auto d = maxQubit - qState.numQubits + 1; + qubitPermutation.resize(qState.numQubits + d); + std::iota(qubitPermutation.begin() + qState.numQubits, + qubitPermutation.end(), qState.numQubits); + qState.numQubits += static_cast(d); + + // Resize the DD package only if necessary. + if (qState.dd->qubits() < qState.numQubits) { + qState.dd->resize(qState.numQubits); } - // if the state is terminal, we need to create a new node - if (qState.isTerminal()) { - qState = makeZeroState(d, *dd); + // If the state is terminal, we need to create a new node. + if (qState.edge.isTerminal()) { + qState.edge = makeZeroState(d, *qState.dd); return; } - // enlarge state - for (auto q = qState.p->v; q < numQubitsInQState; ++q) { - auto old = qState; - qState = dd->makeDDNode(q + 1U, std::array{qState, dd::vEdge::zero()}); - dd->incRef(qState); - dd->decRef(old); + // Enlarge state. + for (auto q = qState.edge.p->v; q < qState.numQubits; ++q) { + auto old = qState.edge; + qState.edge = qState.dd->makeDDNode( + q + 1U, std::array{qState.edge, dd::vEdge::zero()}); + qState.dd->incRef(qState.edge); + qState.dd->decRef(old); } } } @@ -176,6 +170,12 @@ auto Runtime::getRecordedOutputs() const -> const std::string& { return recordedOutputs; } +auto Runtime::takeState() -> QState { + QState ret = std::move(qState); + reset(); + return ret; +} + auto Runtime::getOstream() -> std::ostream& { return *os; } auto Runtime::setOstream(std::ostream& other) -> void { os = &other; } diff --git a/test/qir/runtime/test_qir_runtime.cpp b/test/qir/runtime/test_qir_runtime.cpp index ed9e34979e..3076cd747b 100644 --- a/test/qir/runtime/test_qir_runtime.cpp +++ b/test/qir/runtime/test_qir_runtime.cpp @@ -503,6 +503,22 @@ TEST_F(QIRRuntimeTest, GHZ4Dynamic) { __quantum__rt__array_update_reference_count(rArr, -1); } +TEST_F(QIRRuntimeTest, TakeStateReturnsStateAndResetsRuntime) { + // Drive a small program through the runtime: H on q0. + auto* q0 = reinterpret_cast(0UL); + __quantum__rt__initialize(nullptr); + __quantum__qis__h__body(q0); + + auto state = Runtime::getInstance().takeState(); + EXPECT_NE(state.dd, nullptr); + EXPECT_FALSE(state.edge.isTerminal()); + EXPECT_EQ(state.numQubits, 1); + + // After takeState the runtime is reset and usable again. + EXPECT_NO_THROW(__quantum__rt__initialize(nullptr)); + EXPECT_NO_THROW(__quantum__qis__h__body(q0)); +} + namespace { class QIRFilesTest : public ::testing::TestWithParam {}; From 35e33867cd62822073ca2a984a5f77e3519eee05 Mon Sep 17 00:00:00 2001 From: rturrado Date: Thu, 11 Jun 2026 19:25:38 +0200 Subject: [PATCH 26/33] Wire numShots == 0 for QIR Base Profile Complete the support for `numShots_ == 0` in the DDSim QDMI device for QIR Base Profile submissions: - `submitQIRProgram` now dispatches by shots count, mirroring the existing QASM path. - The state-extraction branch builds a `JitSession` with `Execution::StateExtraction`, runs the program once, takes the surviving quantum state out of `qir::Runtime` via `takeState`, and stores it as the job's `dd_` + `stateVecDD_`, exactly like the QASM zero-shots branch does after `removeFinalMeasurements`. - Adaptive Profile + zero shots returns `QDMI_ERROR_NOTSUPPORTED`: stripping measurement calls would silently change Adaptive programs' behavior because they have measurement-dependent control flow. While here: - Fix an off-by-one in `Runtime::enlargeState`: the loop that adds DD nodes above the current root was running one iteration too many, raising `root.v` to `numQubits` instead of `numQubits - 1`. The bug was hidden in the sampling path because the spurious qubit is always 0 (the state lives in the `|0xx>` subspace), so the measured-qubit statistics are unchanged. State extraction is the first thing to look at the state directly, so it surfaced now. Files: - src/qdmi/devices/dd/Device.cpp: implement the QIR state-extraction path. - src/qir/runtime/Runtime.cpp: fix the `enlargeState` loop condition (`q + 1 < qState.numQubits`). - test/qdmi/devices/dd/results_statevector_test.cpp: new `QIRStateExtractionTest` fixture with `BellPairStaticBaseStringYieldsBellState`, asserting the runtime hands back a 2-qubit state vector with the expected Bell amplitudes. Assisted-by: Claude Opus 4.7 via Claude Code --- include/mqt-core/qdmi/devices/dd/Device.hpp | 32 ++++- src/qdmi/devices/dd/Device.cpp | 126 ++++++++++-------- src/qir/runtime/Runtime.cpp | 4 +- .../devices/dd/results_statevector_test.cpp | 55 ++++++++ 4 files changed, 161 insertions(+), 56 deletions(-) diff --git a/include/mqt-core/qdmi/devices/dd/Device.hpp b/include/mqt-core/qdmi/devices/dd/Device.hpp index e0b19e1c3d..dfefd3213b 100644 --- a/include/mqt-core/qdmi/devices/dd/Device.hpp +++ b/include/mqt-core/qdmi/devices/dd/Device.hpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -234,12 +235,39 @@ struct MQT_DDSIM_QDMI_Device_Job_impl_d { auto getProbabilities(size_t size, void* data, size_t* sizeRet) -> QDMI_STATUS; - /// Helper function to submit a QASM 2 or QASM 3 program + /// Run @p body on a worker thread with the standard job lifecycle: + /// - increase the running-job count, + /// - set status to RUNNING, + /// - run @p body, + /// - set status to DONE or FAILED, and + /// - decrease the running-job count. + /// @p body is the format-specific work: + /// - parse the program, + /// - run or simulate it, and + /// - store the results in the job's output fields. + /// Returns @c QDMI_SUCCESS once the worker has been spawned. + /// Failures inside @p body are reported through the job status (FAILED), + /// not through the return value. + auto submitProgramAsync(std::function body) -> QDMI_STATUS; + + /// Submit a QASM 2 or QASM 3 program. + /// Dispatches to the sampling or the state-extraction helper depending on + /// @c numShots_. auto submitQASMProgram() -> QDMI_STATUS; + /// Sampling path for a QASM program (@c numShots_ > 0). + auto submitQASMProgramSampling() -> QDMI_STATUS; + /// State-extraction path for a QASM program (@c numShots_ == 0). + auto submitQASMProgramStateExtraction() -> QDMI_STATUS; #ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR - /// Helper function to submit a QIR base module or string program + /// Submit a QIR Base/Adaptive Module or String program. + /// Dispatches to the sampling or the state-extraction helper depending on + /// @c numShots_. auto submitQIRProgram() -> QDMI_STATUS; + /// Sampling path for a QIR program (@c numShots_ > 0). + auto submitQIRProgramSampling() -> QDMI_STATUS; + /// State-extraction path for a QIR Base Profile program (@c numShots_ == 0). + auto submitQIRProgramStateExtraction() -> QDMI_STATUS; #endif public: diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index 54e4b01fc4..d16bce5995 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -410,62 +411,13 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::queryProperty( numShots_, prop, size, value, sizeRet) return QDMI_ERROR_NOTSUPPORTED; } -auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQASMProgram() -> QDMI_STATUS { - if (numShots_ > 0) { - jobHandle_ = std::async(std::launch::async, [this]() { - qdmi::dd::Device::get().increaseRunningJobs(); - status_.store(QDMI_JOB_STATUS_RUNNING); - try { - const auto qc = qasm3::Importer::imports(program_); - counts_ = dd::sample(qc, numShots_); - status_.store(QDMI_JOB_STATUS_DONE); - } catch (const std::exception& e) { - status_.store(QDMI_JOB_STATUS_FAILED); - std::cerr << "Error: " << e.what() << '\n'; - } - qdmi::dd::Device::get().decreaseRunningJobs(); - }); - } else { - jobHandle_ = std::async(std::launch::async, [this]() { - qdmi::dd::Device::get().increaseRunningJobs(); - status_.store(QDMI_JOB_STATUS_RUNNING); - try { - auto qc = qasm3::Importer::imports(program_); - qc::CircuitOptimizer::removeFinalMeasurements(qc); - const auto nqubits = qc.getNqubits(); - dd_ = std::make_unique(nqubits); - stateVecDD_ = dd::simulate(qc, dd::makeZeroState(nqubits, *dd_), *dd_); - status_.store(QDMI_JOB_STATUS_DONE); - } catch (const std::exception& e) { - status_.store(QDMI_JOB_STATUS_FAILED); - std::cerr << "Error: " << e.what() << '\n'; - } - qdmi::dd::Device::get().decreaseRunningJobs(); - }); - } - return QDMI_SUCCESS; -} -#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR -auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQIRProgram() -> QDMI_STATUS { - if (numShots_ == 0) { - return QDMI_ERROR_INVALIDARGUMENT; - } - jobHandle_ = std::async(std::launch::async, [this]() { +auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitProgramAsync( + std::function body) -> QDMI_STATUS { + jobHandle_ = std::async(std::launch::async, [this, body = std::move(body)]() { qdmi::dd::Device::get().increaseRunningJobs(); status_.store(QDMI_JOB_STATUS_RUNNING); try { - auto& runtime = qir::Runtime::getInstance(); - auto irBytes = llvm::StringRef(program_.data(), program_.size()); - auto jitSession = qir::JitSession(irBytes, "QDMI job"); - for (size_t i = 0; i < numShots_; ++i) { - runtime.reset(); - if (const auto rc = jitSession.run(); rc != 0) { - throw std::runtime_error( - llvm::formatv("QIR program failed with error: {}", rc)); - } - // Update the measurement counts. - ++counts_[runtime.getRecordedOutputs()]; - } + body(); status_.store(QDMI_JOB_STATUS_DONE); } catch (const std::exception& e) { status_.store(QDMI_JOB_STATUS_FAILED); @@ -475,6 +427,74 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQIRProgram() -> QDMI_STATUS { }); return QDMI_SUCCESS; } +auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQASMProgram() -> QDMI_STATUS { + return numShots_ > 0 ? submitQASMProgramSampling() + : submitQASMProgramStateExtraction(); +} +auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQASMProgramSampling() + -> QDMI_STATUS { + return submitProgramAsync([this]() { + const auto qc = qasm3::Importer::imports(program_); + counts_ = dd::sample(qc, numShots_); + }); +} +auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQASMProgramStateExtraction() + -> QDMI_STATUS { + return submitProgramAsync([this]() { + auto qc = qasm3::Importer::imports(program_); + qc::CircuitOptimizer::removeFinalMeasurements(qc); + const auto nQubits = qc.getNqubits(); + dd_ = std::make_unique(nQubits); + stateVecDD_ = dd::simulate(qc, dd::makeZeroState(nQubits, *dd_), *dd_); + }); +} +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR +auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQIRProgram() -> QDMI_STATUS { + return numShots_ > 0 ? submitQIRProgramSampling() + : submitQIRProgramStateExtraction(); +} +auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQIRProgramSampling() + -> QDMI_STATUS { + return submitProgramAsync([this]() { + auto& runtime = qir::Runtime::getInstance(); + auto irBytes = llvm::StringRef(program_.data(), program_.size()); + auto jitSession = qir::JitSession(irBytes, "QDMI job"); + for (size_t i = 0; i < numShots_; ++i) { + runtime.reset(); + if (const auto rc = jitSession.run(); rc != 0) { + throw std::runtime_error( + llvm::formatv("QIR program failed with error: {}", rc)); + } + // Update the measurement counts. + ++counts_[runtime.getRecordedOutputs()]; + } + }); +} +auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQIRProgramStateExtraction() + -> QDMI_STATUS { + // State extraction strips measurement calls from the IR, which only + // preserves semantics for QIR Base Profile (measurements are terminal there). + // Adaptive Profile has measurement-dependent control flow, so stripping would + // silently change the program's meaning. + if (format_ != QDMI_PROGRAM_FORMAT_QIRBASEMODULE && + format_ != QDMI_PROGRAM_FORMAT_QIRBASESTRING) { + return QDMI_ERROR_NOTSUPPORTED; + } + return submitProgramAsync([this]() { + auto& runtime = qir::Runtime::getInstance(); + runtime.reset(); + auto irBytes = llvm::StringRef(program_.data(), program_.size()); + auto jitSession = + qir::JitSession(irBytes, "QDMI job", qir::Execution::StateExtraction); + if (const auto rc = jitSession.run(); rc != 0) { + throw std::runtime_error( + llvm::formatv("QIR program failed with error: {}", rc)); + } + auto state = runtime.takeState(); + dd_ = std::move(state.dd); + stateVecDD_ = state.edge; + }); +} #endif auto MQT_DDSIM_QDMI_Device_Job_impl_d::submit() -> QDMI_STATUS { if (status_.load() != QDMI_JOB_STATUS_CREATED) { diff --git a/src/qir/runtime/Runtime.cpp b/src/qir/runtime/Runtime.cpp index 0e3caf1d5a..16f53c1d88 100644 --- a/src/qir/runtime/Runtime.cpp +++ b/src/qir/runtime/Runtime.cpp @@ -102,7 +102,9 @@ auto Runtime::enlargeState(const std::uint64_t maxQubit) -> void { } // Enlarge state. - for (auto q = qState.edge.p->v; q < qState.numQubits; ++q) { + // Each iteration adds one level above the current root, raising root.v by + // one. After the loop, root.v == numQubits - 1. + for (auto q = qState.edge.p->v; q + 1 < qState.numQubits; ++q) { auto old = qState.edge; qState.edge = qState.dd->makeDDNode( q + 1U, std::array{qState.edge, dd::vEdge::zero()}); diff --git a/test/qdmi/devices/dd/results_statevector_test.cpp b/test/qdmi/devices/dd/results_statevector_test.cpp index 5f9b78a033..509124882d 100644 --- a/test/qdmi/devices/dd/results_statevector_test.cpp +++ b/test/qdmi/devices/dd/results_statevector_test.cpp @@ -22,6 +22,19 @@ #include #include +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR +#include "qir/runtime/Runtime.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#endif + TEST(ResultsStatevector, DenseNormalizedAndBufferTooSmall) { const qdmi_test::SessionGuard s{}; const qdmi_test::JobGuard j{s.session}; @@ -103,3 +116,45 @@ TEST(ResultsStatevector, HistogramRequestsInvalidWithShotsZero) { j.job, QDMI_JOB_RESULT_HIST_VALUES, 0, nullptr, nullptr), QDMI_ERROR_INVALIDARGUMENT); } + +#ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR +namespace { + +class QIRStateExtractionTest : public testing::Test { +protected: + std::ostringstream sink; + void SetUp() override { qir::Runtime::getInstance().setOstream(sink); } + void TearDown() override { qir::Runtime::getInstance().resetOstream(); } + + static std::string getProgram(const std::string_view file) { + const std::filesystem::path path = + std::filesystem::path(QIR_FILES_DIR) / file; + std::ifstream ifs(path); + EXPECT_TRUE(ifs.is_open()) << "Failed to open " << path.string(); + return {std::istreambuf_iterator{ifs}, {}}; + } +}; + +TEST_F(QIRStateExtractionTest, BellPairStaticBaseStringYieldsBellState) { + const qdmi_test::SessionGuard s{}; + const qdmi_test::JobGuard j{s.session}; + const auto program = getProgram("BellPairStatic.ll"); + ASSERT_EQ( + qdmi_test::setProgram(j.job, QDMI_PROGRAM_FORMAT_QIRBASESTRING, program), + QDMI_SUCCESS); + ASSERT_EQ(qdmi_test::setShots(j.job, 0), QDMI_SUCCESS); + ASSERT_EQ(qdmi_test::submitAndWait(j.job, 0), QDMI_SUCCESS); + + const auto vec = qdmi_test::getDenseState(j.job); + ASSERT_EQ(vec.size(), 4U); + + // Bell pair: amplitudes at |00> and |11> are 1/sqrt(2), |01> and |10> are 0. + constexpr double invSqrt2 = 1.0 / std::numbers::sqrt2; + EXPECT_NEAR(std::abs(vec[0]), invSqrt2, 1e-6); + EXPECT_NEAR(std::abs(vec[1]), 0.0, 1e-6); + EXPECT_NEAR(std::abs(vec[2]), 0.0, 1e-6); + EXPECT_NEAR(std::abs(vec[3]), invSqrt2, 1e-6); +} + +} // namespace +#endif From a4829b0593565e8e0bc67a0ff5020d8df159f9e5 Mon Sep 17 00:00:00 2001 From: rturrado Date: Fri, 12 Jun 2026 13:13:02 +0200 Subject: [PATCH 27/33] Add JitSession memory and error path tests Lift `src/qir/jit/Session.cpp` diff coverage from 76.8% toward the Codecov 90% target by exercising paths the existing tests skipped: - `LoadModuleFromMemory`: build a `JitSession` from an in-memory IR buffer (the contents of `BellPairStatic.ll`) and run it, covering `loadModuleFromMemory` and the memory-based constructor. - `MalformedIRThrows`: pass unparseable IR and expect `std::runtime_error`, covering `getThreadSafeModuleOrError`'s null-module branch and the throw in `initialize` when the loader hands back an error. - `MissingMainThrows`: pass valid IR with no `main` and expect `std::runtime_error`, covering the `lookup("main")` failure branch. While here, rename the fixture from `JitSessionExecutionMode` to `JitSessionTest` and factor out a `getProgram` helper that mirrors the one in `results_sampling_test.cpp`. The remaining uncovered lines in `Session.cpp` are Cygwin/MinGW paths, the GDB JIT listener, and throw arms behind LLVM `Expected` failures that cannot be portably forced from a test. Files: - test/qir/jit/test_jit_session.cpp: new tests, renamed fixture, and `getProgram` helper. Assisted-by: Claude Opus 4.7 via Claude Code --- test/qir/jit/test_jit_session.cpp | 36 ++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/test/qir/jit/test_jit_session.cpp b/test/qir/jit/test_jit_session.cpp index 737e86a00a..732a8e0ade 100644 --- a/test/qir/jit/test_jit_session.cpp +++ b/test/qir/jit/test_jit_session.cpp @@ -14,11 +14,16 @@ #include #include +#include +#include #include +#include +#include +#include namespace { -class JitSessionExecutionMode : public testing::Test { +class JitSessionTest : public testing::Test { protected: std::ostringstream sink; @@ -28,9 +33,24 @@ class JitSessionExecutionMode : public testing::Test { runtime.setOstream(sink); } void TearDown() override { qir::Runtime::getInstance().resetOstream(); } + + static std::string getProgram(const std::string_view file) { + const std::filesystem::path path = + std::filesystem::path(QIR_FILES_DIR) / file; + std::ifstream ifs(path); + EXPECT_TRUE(ifs.is_open()) << "Failed to open " << path.string(); + return {std::istreambuf_iterator{ifs}, {}}; + } }; -TEST_F(JitSessionExecutionMode, SamplingRecordsOutputs) { +TEST_F(JitSessionTest, LoadModuleFromMemory) { + const auto program = getProgram("BellPairStatic.ll"); + const qir::JitSession session(program, "BellPairStatic.ll"); + ASSERT_EQ(session.run(), 0); + EXPECT_FALSE(qir::Runtime::getInstance().getRecordedOutputs().empty()); +} + +TEST_F(JitSessionTest, SamplingRecordsOutputs) { const auto path = std::filesystem::path(QIR_FILES_DIR) / "BellPairStatic.ll"; // qir::Execution::Sampling is the default Execution mode const qir::JitSession session(path.string()); @@ -38,11 +58,21 @@ TEST_F(JitSessionExecutionMode, SamplingRecordsOutputs) { EXPECT_FALSE(qir::Runtime::getInstance().getRecordedOutputs().empty()); } -TEST_F(JitSessionExecutionMode, StateExtractionLeavesNoRecordedOutputs) { +TEST_F(JitSessionTest, StateExtractionLeavesNoRecordedOutputs) { const auto path = std::filesystem::path(QIR_FILES_DIR) / "BellPairStatic.ll"; const qir::JitSession session(path.string(), qir::Execution::StateExtraction); ASSERT_EQ(session.run(), 0); EXPECT_TRUE(qir::Runtime::getInstance().getRecordedOutputs().empty()); } +TEST(JitSessionErrors, MalformedIRThrows) { + constexpr std::string_view ir = R"(define i32 @main() {})"; + EXPECT_THROW(qir::JitSession(ir, "MalformedIR.ll"), std::runtime_error); +} + +TEST(JitSessionErrors, MissingMainThrows) { + constexpr std::string_view ir = R"(define i32 @notMain() { ret i32 0 })"; + EXPECT_THROW(qir::JitSession(ir, "NoMain.ll"), std::runtime_error); +} + } // namespace From e2c05103798eb923a1ea18225441a007b8629de5 Mon Sep 17 00:00:00 2001 From: rturrado Date: Fri, 12 Jun 2026 13:30:33 +0200 Subject: [PATCH 28/33] Drop platform-specific MissingMainThrows test The `JitSessionErrors.MissingMainThrows` test added in the previous commit failed on macOS CI. The host process's `main` symbol is exported through `DynamicLibrarySearchGenerator::GetForCurrentProcess` on macOS but not on Linux. So the JIT's `lookup("main")` finds the gtest runner's `main` on macOS and the constructor returns without throwing. On Linux the lookup fails and the throw fires, which is what the test expected. We do not control which symbols the process generator exposes from the test side, so the test cannot be made portable. The lost line (the `lookup("main")` failure branch at line 381 of `Session.cpp`) is not worth a platform-specific carve-out. Files: - test/qir/jit/test_jit_session.cpp: remove the `MissingMainThrows` test. Assisted-by: Claude Opus 4.7 via Claude Code --- test/qir/jit/test_jit_session.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/qir/jit/test_jit_session.cpp b/test/qir/jit/test_jit_session.cpp index 732a8e0ade..a1393eb375 100644 --- a/test/qir/jit/test_jit_session.cpp +++ b/test/qir/jit/test_jit_session.cpp @@ -70,9 +70,4 @@ TEST(JitSessionErrors, MalformedIRThrows) { EXPECT_THROW(qir::JitSession(ir, "MalformedIR.ll"), std::runtime_error); } -TEST(JitSessionErrors, MissingMainThrows) { - constexpr std::string_view ir = R"(define i32 @notMain() { ret i32 0 })"; - EXPECT_THROW(qir::JitSession(ir, "NoMain.ll"), std::runtime_error); -} - } // namespace From 678e19dccc4157e99715c872ba9119e30f77f7fe Mon Sep 17 00:00:00 2001 From: rturrado Date: Fri, 12 Jun 2026 13:56:50 +0200 Subject: [PATCH 29/33] Cover Runtime::enlargeState resize branch Add `QIRRuntimeTest.PackageResizeWhenEnlargingState`: apply H to qubit address 32, which makes `translateAddresses` call `enlargeState(32)` and pushes `qState.numQubits` past the `dd::Package` default capacity of 32, triggering `qState.dd->resize`. Files: - test/qir/runtime/test_qir_runtime.cpp: new test driving H on qubit index 32. Assisted-by: Claude Opus 4.7 via Claude Code --- test/qir/runtime/test_qir_runtime.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/qir/runtime/test_qir_runtime.cpp b/test/qir/runtime/test_qir_runtime.cpp index 3076cd747b..7486517916 100644 --- a/test/qir/runtime/test_qir_runtime.cpp +++ b/test/qir/runtime/test_qir_runtime.cpp @@ -503,6 +503,14 @@ TEST_F(QIRRuntimeTest, GHZ4Dynamic) { __quantum__rt__array_update_reference_count(rArr, -1); } +TEST_F(QIRRuntimeTest, PackageResizeWhenEnlargingState) { + // dd::Package starts at 32 qubits. + // Acting on qubit 32 forces qState.dd->resize. + auto* q32 = reinterpret_cast(32UL); + __quantum__rt__initialize(nullptr); + __quantum__qis__h__body(q32); +} + TEST_F(QIRRuntimeTest, TakeStateReturnsStateAndResetsRuntime) { // Drive a small program through the runtime: H on q0. auto* q0 = reinterpret_cast(0UL); From e6d26520d0bd60d398aa67a3ecbd547ae223b26e Mon Sep 17 00:00:00 2001 From: rturrado Date: Mon, 15 Jun 2026 20:20:17 +0200 Subject: [PATCH 30/33] Address comments from Yannick's code review Apply review suggestions across CMake, docs, and Doxygen comments, plus one parameter rename. - CMakeLists.txt: default `BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR` to `ON` instead of `OFF`, so QIR support is enabled whenever MLIR is. - test/qir/CMakeLists.txt: drop the outer `if(BUILD_MQT_CORE_QIR_RUNNER)` and `if(BUILD_MQT_CORE_QIR_RUNNER OR BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR)` guards, since the inner `if(TARGET ...)` checks in each child already subsume them. Assisted-by: Claude Opus 4.7 via Claude Code --- CMakeLists.txt | 2 +- docs/qir/index.md | 5 +++-- include/mqt-core/qdmi/devices/dd/Device.hpp | 4 ++-- include/mqt-core/qir/jit/Session.hpp | 2 +- include/mqt-core/qir/runtime/Runtime.hpp | 3 ++- src/qir/jit/Session.cpp | 6 +++--- test/qir/CMakeLists.txt | 10 ++-------- 7 files changed, 14 insertions(+), 18 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a5914f326..f616e74631 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -123,7 +123,7 @@ cmake_dependent_option(BUILD_MQT_CORE_QIR_RUNNER "Build the QIR runner of the MQ cmake_dependent_option( BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR "Enable QIR program format support for the DDSim QDMI device" - OFF "BUILD_MQT_CORE_MLIR" OFF) + ON "BUILD_MQT_CORE_MLIR" OFF) # add main library code add_subdirectory(src) diff --git a/docs/qir/index.md b/docs/qir/index.md index bb8b8be4d5..68867b5870 100644 --- a/docs/qir/index.md +++ b/docs/qir/index.md @@ -16,7 +16,8 @@ See {cite:p}`stadeTowardsSupportingQIR2025` for more details. ### Building the Runner -To build this tool, the CMake option `BUILD_MQT_CORE_QIR_RUNNER` has to be enabled (which depends on `BUILD_MQT_CORE_MLIR` being set). +To build this tool, the CMake option `BUILD_MQT_CORE_QIR_RUNNER` has to be enabled. +It is enabled by default, but depends on `BUILD_MQT_CORE_MLIR` being set. From the root of the repository, you can build the runner as follows: ```bash @@ -41,4 +42,4 @@ The runner supports the QIR Base Profile. The QDMI Device accepts jobs in the following program formats: QASM2, QASM3, QIR Base Profile Module (LLVM bitcode), and QIR Base Profile String (LLVM assembly). The QIR base formats are only supported when the `BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR` CMake option is enabled. -It is disabled by default to avoid the cost of linking against the MQT Core QIR JIT (built on LLVM OrcJIT) and Runtime libraries. +It is enabled by default, but depends on `BUILD_MQT_CORE_MLIR` being set. diff --git a/include/mqt-core/qdmi/devices/dd/Device.hpp b/include/mqt-core/qdmi/devices/dd/Device.hpp index dfefd3213b..fc89098fb0 100644 --- a/include/mqt-core/qdmi/devices/dd/Device.hpp +++ b/include/mqt-core/qdmi/devices/dd/Device.hpp @@ -241,11 +241,11 @@ struct MQT_DDSIM_QDMI_Device_Job_impl_d { /// - run @p body, /// - set status to DONE or FAILED, and /// - decrease the running-job count. - /// @p body is the format-specific work: + /// Typically, @p body will: /// - parse the program, /// - run or simulate it, and /// - store the results in the job's output fields. - /// Returns @c QDMI_SUCCESS once the worker has been spawned. + /// @returns @c QDMI_SUCCESS once the worker has been spawned. /// Failures inside @p body are reported through the job status (FAILED), /// not through the return value. auto submitProgramAsync(std::function body) -> QDMI_STATUS; diff --git a/include/mqt-core/qir/jit/Session.hpp b/include/mqt-core/qir/jit/Session.hpp index 0e8bb4c2da..7a19524b81 100644 --- a/include/mqt-core/qir/jit/Session.hpp +++ b/include/mqt-core/qir/jit/Session.hpp @@ -111,7 +111,7 @@ class JitSession { /// Prepares the session to run the program: /// - Validates the loaded module. /// - Optionally strips measurement and result management calls - /// (for @c Execution::StateExtraction). + /// (for @ref Execution::StateExtraction). /// - Builds the @c LLJIT instance /// - Registers QIR runtime symbols /// - Resolves @c main. diff --git a/include/mqt-core/qir/runtime/Runtime.hpp b/include/mqt-core/qir/runtime/Runtime.hpp index 0f96416807..8459225d90 100644 --- a/include/mqt-core/qir/runtime/Runtime.hpp +++ b/include/mqt-core/qir/runtime/Runtime.hpp @@ -395,7 +395,7 @@ class Runtime { /// string in record order. auto recordOutput(Result* result) -> void; - /// Return the outputs declared by the program as a bit string in record + /// @returns the outputs declared by the program as a bit string in record /// order. auto getRecordedOutputs() const -> const std::string&; @@ -403,6 +403,7 @@ class Runtime { /// Then reset the runtime to a clean state ready for the next job. /// Intended for use after a @c JitSession constructed with /// @c Execution::StateExtraction has finished running. + /// @returns the moved @c QState from the runtime. auto takeState() -> QState; auto getOstream() -> std::ostream&; diff --git a/src/qir/jit/Session.cpp b/src/qir/jit/Session.cpp index 322a79ec68..11c86ee446 100644 --- a/src/qir/jit/Session.cpp +++ b/src/qir/jit/Session.cpp @@ -82,10 +82,10 @@ static llvm::Error tryEnableDebugSupport(llvm::orc::LLJIT& jit) { } static llvm::Expected -getThreadSafeModuleOrError(std::unique_ptr module, +getThreadSafeModuleOrError(std::unique_ptr llvmModule, const llvm::SMDiagnostic& err, llvm::orc::ThreadSafeContext tsCtx) { - if (!module) { + if (!llvmModule) { std::string errMsg; { llvm::raw_string_ostream errMsgStream(errMsg); @@ -94,7 +94,7 @@ getThreadSafeModuleOrError(std::unique_ptr module, return llvm::make_error(std::move(errMsg), llvm::inconvertibleErrorCode()); } - return llvm::orc::ThreadSafeModule(std::move(module), std::move(tsCtx)); + return llvm::orc::ThreadSafeModule(std::move(llvmModule), std::move(tsCtx)); } llvm::Expected diff --git a/test/qir/CMakeLists.txt b/test/qir/CMakeLists.txt index 2c60f4191d..935e50f876 100644 --- a/test/qir/CMakeLists.txt +++ b/test/qir/CMakeLists.txt @@ -6,12 +6,6 @@ # # Licensed under the MIT License -if(BUILD_MQT_CORE_QIR_RUNNER OR BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR) - add_subdirectory(jit) -endif() - +add_subdirectory(jit) add_subdirectory(runtime) - -if(BUILD_MQT_CORE_QIR_RUNNER) - add_subdirectory(runner) -endif() +add_subdirectory(runner) From 3769ae34c2dd72944e78b61c9a5b9bef63497dfe Mon Sep 17 00:00:00 2001 From: rturrado Date: Mon, 15 Jun 2026 21:47:13 +0200 Subject: [PATCH 31/33] Extract `getProgram` into a shared QIR test helper Replace three identical inline `getProgram` definitions across the QIR JIT and DDSIM QDMI test files with a single shared utility. - test/qir/helpers/test_utils.{h,c}pp: new `qir_test::getProgram(std::string_view)` that reads a QIR source file from `QIR_FILES_DIR` and returns its contents as a string. - test/qir/jit/test_jit_session.cpp, test/qdmi/devices/dd/{results_sampling_test.cpp, results_statevector_test.cpp}: drop the local `getProgram` and call `qir_test::getProgram` directly. - test/CMakeLists.txt: reorder so `qir` is added before `qdmi`, so the qdmi tests can link against `mqt-core-qir-test-utils`. - test/qir/CMakeLists.txt: add the new `helpers` subdirectory first. - test/qir/helpers/CMakeLists.txt: new `mqt-core-qir-test-utils` STATIC library, with `test/` on its public include path so consumers include via `#include "qir/helpers/test_utils.hpp"`. - test/qir/jit/CMakeLists.txt, test/qdmi/devices/dd/CMakeLists.txt: link the helper. Assisted-by: Claude Opus 4.7 via Claude Code --- test/CMakeLists.txt | 2 +- test/qdmi/devices/dd/CMakeLists.txt | 3 +- .../qdmi/devices/dd/results_sampling_test.cpp | 29 +++++------------ .../devices/dd/results_statevector_test.cpp | 16 ++-------- test/qir/CMakeLists.txt | 1 + test/qir/helpers/CMakeLists.txt | 13 ++++++++ test/qir/helpers/test_utils.cpp | 31 +++++++++++++++++++ test/qir/helpers/test_utils.hpp | 25 +++++++++++++++ test/qir/jit/CMakeLists.txt | 3 +- test/qir/jit/test_jit_session.cpp | 14 ++------- 10 files changed, 87 insertions(+), 50 deletions(-) create mode 100644 test/qir/helpers/CMakeLists.txt create mode 100644 test/qir/helpers/test_utils.cpp create mode 100644 test/qir/helpers/test_utils.hpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e58442a617..6203a14e01 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -13,9 +13,9 @@ add_subdirectory(dd) add_subdirectory(ir) add_subdirectory(na) add_subdirectory(zx) +add_subdirectory(qir) add_subdirectory(qdmi) add_subdirectory(fomac) -add_subdirectory(qir) # copy test circuits to build directory file(COPY ${PROJECT_SOURCE_DIR}/test/circuits DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/test/qdmi/devices/dd/CMakeLists.txt b/test/qdmi/devices/dd/CMakeLists.txt index 50c44a3e40..321b105718 100644 --- a/test/qdmi/devices/dd/CMakeLists.txt +++ b/test/qdmi/devices/dd/CMakeLists.txt @@ -35,7 +35,8 @@ if(TARGET MQT::CoreQDMI_DDSIM_Device) if(BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR) llvm_map_components_to_libnames(llvm_native_libs asmparser bitwriter core support) - target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQIRRuntime ${llvm_native_libs}) + target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQIRRuntime mqt-core-qir-test-utils + ${llvm_native_libs}) target_compile_definitions( ${TARGET_NAME} PRIVATE BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR QIR_FILES_DIR="${PROJECT_SOURCE_DIR}/test/circuits") diff --git a/test/qdmi/devices/dd/results_sampling_test.cpp b/test/qdmi/devices/dd/results_sampling_test.cpp index fe9ce96b5c..622e14b83b 100644 --- a/test/qdmi/devices/dd/results_sampling_test.cpp +++ b/test/qdmi/devices/dd/results_sampling_test.cpp @@ -28,6 +28,7 @@ #include #ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR +#include "qir/helpers/test_utils.hpp" #include "qir/runtime/Runtime.hpp" #include @@ -36,9 +37,6 @@ #include #include -#include -#include -#include #include #include #endif @@ -86,11 +84,7 @@ class HistogramTest : public ::testing::Test { class QIRHistogramTestModule : public HistogramTest { protected: static std::string getProgram(const std::string_view file) { - const std::filesystem::path path = - std::filesystem::path(QIR_FILES_DIR) / file; - std::ifstream ifs(path); - EXPECT_TRUE(ifs.is_open()) << "Failed to open " << path.string(); - const std::string text(std::istreambuf_iterator{ifs}, {}); + const std::string text = qir_test::getProgram(file); llvm::LLVMContext context; llvm::SMDiagnostic err; auto llvmModule = llvm::parseAssemblyString(text, err, context); @@ -107,16 +101,7 @@ class QIRHistogramTestModule : public HistogramTest { } }; -class QIRHistogramTestString : public HistogramTest { -protected: - static std::string getProgram(const std::string_view file) { - const std::filesystem::path path = - std::filesystem::path(QIR_FILES_DIR) / file; - std::ifstream ifs(path); - EXPECT_TRUE(ifs.is_open()) << "Failed to open " << path.string(); - return {std::istreambuf_iterator{ifs}, {}}; - } -}; +class QIRHistogramTestString : public HistogramTest {}; #endif } // namespace @@ -135,7 +120,7 @@ TEST_F(QIRHistogramTestModule, BaseStatic) { TEST_F(QIRHistogramTestString, BaseStatic) { constexpr auto format = QDMI_PROGRAM_FORMAT_QIRBASESTRING; - checkHistogram(runProgram(format, getProgram("BellPairStatic.ll"))); + checkHistogram(runProgram(format, qir_test::getProgram("BellPairStatic.ll"))); } TEST_F(QIRHistogramTestModule, BaseDynamic) { @@ -145,7 +130,8 @@ TEST_F(QIRHistogramTestModule, BaseDynamic) { TEST_F(QIRHistogramTestString, BaseDynamic) { constexpr auto format = QDMI_PROGRAM_FORMAT_QIRBASESTRING; - checkHistogram(runProgram(format, getProgram("BellPairDynamic.ll"))); + checkHistogram( + runProgram(format, qir_test::getProgram("BellPairDynamic.ll"))); } TEST_F(QIRHistogramTestModule, Adaptive) { @@ -155,7 +141,8 @@ TEST_F(QIRHistogramTestModule, Adaptive) { TEST_F(QIRHistogramTestString, Adaptive) { constexpr auto format = QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING; - checkHistogram(runProgram(format, getProgram("BellPairAdaptive.ll"))); + checkHistogram( + runProgram(format, qir_test::getProgram("BellPairAdaptive.ll"))); } #endif diff --git a/test/qdmi/devices/dd/results_statevector_test.cpp b/test/qdmi/devices/dd/results_statevector_test.cpp index 509124882d..eb05ee429a 100644 --- a/test/qdmi/devices/dd/results_statevector_test.cpp +++ b/test/qdmi/devices/dd/results_statevector_test.cpp @@ -23,16 +23,12 @@ #include #ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR +#include "qir/helpers/test_utils.hpp" #include "qir/runtime/Runtime.hpp" #include -#include -#include -#include #include #include -#include -#include #endif TEST(ResultsStatevector, DenseNormalizedAndBufferTooSmall) { @@ -125,20 +121,12 @@ class QIRStateExtractionTest : public testing::Test { std::ostringstream sink; void SetUp() override { qir::Runtime::getInstance().setOstream(sink); } void TearDown() override { qir::Runtime::getInstance().resetOstream(); } - - static std::string getProgram(const std::string_view file) { - const std::filesystem::path path = - std::filesystem::path(QIR_FILES_DIR) / file; - std::ifstream ifs(path); - EXPECT_TRUE(ifs.is_open()) << "Failed to open " << path.string(); - return {std::istreambuf_iterator{ifs}, {}}; - } }; TEST_F(QIRStateExtractionTest, BellPairStaticBaseStringYieldsBellState) { const qdmi_test::SessionGuard s{}; const qdmi_test::JobGuard j{s.session}; - const auto program = getProgram("BellPairStatic.ll"); + const auto program = qir_test::getProgram("BellPairStatic.ll"); ASSERT_EQ( qdmi_test::setProgram(j.job, QDMI_PROGRAM_FORMAT_QIRBASESTRING, program), QDMI_SUCCESS); diff --git a/test/qir/CMakeLists.txt b/test/qir/CMakeLists.txt index 935e50f876..434588601e 100644 --- a/test/qir/CMakeLists.txt +++ b/test/qir/CMakeLists.txt @@ -6,6 +6,7 @@ # # Licensed under the MIT License +add_subdirectory(helpers) add_subdirectory(jit) add_subdirectory(runtime) add_subdirectory(runner) diff --git a/test/qir/helpers/CMakeLists.txt b/test/qir/helpers/CMakeLists.txt new file mode 100644 index 0000000000..bd57bd8850 --- /dev/null +++ b/test/qir/helpers/CMakeLists.txt @@ -0,0 +1,13 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +add_library(mqt-core-qir-test-utils STATIC test_utils.cpp) +target_link_libraries(mqt-core-qir-test-utils PUBLIC gtest) +target_include_directories(mqt-core-qir-test-utils PUBLIC ${PROJECT_SOURCE_DIR}/test) +target_compile_definitions(mqt-core-qir-test-utils + PRIVATE QIR_FILES_DIR="${PROJECT_SOURCE_DIR}/test/circuits") diff --git a/test/qir/helpers/test_utils.cpp b/test/qir/helpers/test_utils.cpp new file mode 100644 index 0000000000..f6c6fa7da8 --- /dev/null +++ b/test/qir/helpers/test_utils.cpp @@ -0,0 +1,31 @@ +/* + * 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 "qir/helpers/test_utils.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace qir_test { + +std::string getProgram(const std::string_view file) { + const std::filesystem::path path = + std::filesystem::path(QIR_FILES_DIR) / file; + std::ifstream ifs(path); + EXPECT_TRUE(ifs.is_open()) << "Failed to open " << path.string(); + return {std::istreambuf_iterator{ifs}, {}}; +} + +} // namespace qir_test diff --git a/test/qir/helpers/test_utils.hpp b/test/qir/helpers/test_utils.hpp new file mode 100644 index 0000000000..e6d5c0777a --- /dev/null +++ b/test/qir/helpers/test_utils.hpp @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +/* + * Test utilities for QIR-based tests. + */ +#pragma once + +#include +#include + +namespace qir_test { + +/// Read a QIR source file from the test circuits directory and +/// return its contents as a string. +std::string getProgram(std::string_view file); + +} // namespace qir_test diff --git a/test/qir/jit/CMakeLists.txt b/test/qir/jit/CMakeLists.txt index 8a1eeaabe9..8552a167d0 100644 --- a/test/qir/jit/CMakeLists.txt +++ b/test/qir/jit/CMakeLists.txt @@ -13,7 +13,8 @@ if(TARGET MQT::CoreQIRJIT) llvm_map_components_to_libnames(llvm_native_libs_for_test asmparser core irreader support) package_add_test(${TARGET_NAME} MQT::CoreQIRJIT test_ir_rewriter.cpp test_jit_session.cpp) - target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQIRRuntime ${llvm_native_libs_for_test}) + target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQIRRuntime mqt-core-qir-test-utils + ${llvm_native_libs_for_test}) target_compile_definitions(${TARGET_NAME} PRIVATE QIR_FILES_DIR="${PROJECT_SOURCE_DIR}/test/circuits") diff --git a/test/qir/jit/test_jit_session.cpp b/test/qir/jit/test_jit_session.cpp index a1393eb375..5f1ff5feaf 100644 --- a/test/qir/jit/test_jit_session.cpp +++ b/test/qir/jit/test_jit_session.cpp @@ -8,17 +8,15 @@ * Licensed under the MIT License */ +#include "qir/helpers/test_utils.hpp" #include "qir/jit/Session.hpp" #include "qir/runtime/Runtime.hpp" #include #include -#include -#include #include #include -#include #include namespace { @@ -33,18 +31,10 @@ class JitSessionTest : public testing::Test { runtime.setOstream(sink); } void TearDown() override { qir::Runtime::getInstance().resetOstream(); } - - static std::string getProgram(const std::string_view file) { - const std::filesystem::path path = - std::filesystem::path(QIR_FILES_DIR) / file; - std::ifstream ifs(path); - EXPECT_TRUE(ifs.is_open()) << "Failed to open " << path.string(); - return {std::istreambuf_iterator{ifs}, {}}; - } }; TEST_F(JitSessionTest, LoadModuleFromMemory) { - const auto program = getProgram("BellPairStatic.ll"); + const auto program = qir_test::getProgram("BellPairStatic.ll"); const qir::JitSession session(program, "BellPairStatic.ll"); ASSERT_EQ(session.run(), 0); EXPECT_FALSE(qir::Runtime::getInstance().getRecordedOutputs().empty()); From 818ef78f312148fc3b30896fa205556aa18f0136 Mon Sep 17 00:00:00 2001 From: rturrado Date: Mon, 15 Jun 2026 23:06:12 +0200 Subject: [PATCH 32/33] Store `program_` as a variant of text and binary - include/mqt-core/qdmi/devices/dd/Device.hpp: change `program_` to `std::variant>`. Text formats (QASM2/3, QIR Base/Adaptive String) use the string alternative, binary formats (QIR Base/Adaptive Module) use the byte-vector alternative. - src/qdmi/devices/dd/Device.cpp: write site dispatches on `isTextProgramFormat(format_)`. QIR JIT paths use `std::visit` with a generic lambda so the same lambda extracts `.data()` and `.size()` from either alternative and builds an `llvm::StringRef`. Assisted-by: Claude Opus 4.7 via Claude Code --- include/mqt-core/qdmi/devices/dd/Device.hpp | 9 +++- include/mqt-core/qir/jit/Session.hpp | 2 +- src/qdmi/devices/dd/Device.cpp | 48 +++++++++++++++------ 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/include/mqt-core/qdmi/devices/dd/Device.hpp b/include/mqt-core/qdmi/devices/dd/Device.hpp index fc89098fb0..38b7c5e203 100644 --- a/include/mqt-core/qdmi/devices/dd/Device.hpp +++ b/include/mqt-core/qdmi/devices/dd/Device.hpp @@ -31,6 +31,8 @@ #include #include #include +#include +#include namespace qdmi::dd { class Device final : public Singleton { @@ -190,8 +192,11 @@ struct MQT_DDSIM_QDMI_Device_Job_impl_d { /// The program format QDMI_Program_Format format_ = QDMI_PROGRAM_FORMAT_QASM3; - /// The quantum program associated with the job - std::string program_; + /// The quantum program associated with the job. + /// Text formats (QASM2/3, QIR Base/Adaptive String) are stored as + /// @c std::string; binary formats (QIR Base/Adaptive Module) are stored as + /// @c std::vector. + std::variant> program_; /// The number of shots for the job size_t numShots_ = 1024U; diff --git a/include/mqt-core/qir/jit/Session.hpp b/include/mqt-core/qir/jit/Session.hpp index 7a19524b81..abeb416fc0 100644 --- a/include/mqt-core/qir/jit/Session.hpp +++ b/include/mqt-core/qir/jit/Session.hpp @@ -37,7 +37,7 @@ enum class Execution { Sampling, StateExtraction }; * @brief In-process JIT executor for QIR programs. * @details The session does the following, in order: * - Loads an LLVM module from either an IR file (text or bitcode) or - * an in-memory buffer, + * an in-memory buffer, * - JIT-compiles it via LLVM's OrcJIT with lazy compilation. * - wires up the QIR runtime symbols, and * - runs the module's @c main function. diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index d16bce5995..658a2084c4 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -43,13 +43,17 @@ #include #include #include +#include #include #include +#include +#include #ifdef BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR #include "qir/jit/Session.hpp" #include "qir/runtime/Runtime.hpp" +#include #include #include @@ -372,14 +376,16 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::setParameter( return QDMI_SUCCESS; case QDMI_DEVICE_JOB_PARAMETER_PROGRAM: if (value != nullptr) { - // Text payloads include the trailing '\0' in `size`. - // Strip it so it is not counted in `program_.size()`. - // `std::string` re-synthesizes its own '\0' at `data()[size()]` for - // c_str() consumers. - // Binary payloads are stored exactly as received. - const auto bytes = - qdmi::dd::isTextProgramFormat(format_) ? size - 1 : size; - program_ = std::string(static_cast(value), bytes); + if (qdmi::dd::isTextProgramFormat(format_)) { + // Text payloads include the trailing '\0' in `size`. + // Strip it so it is not counted in the stored string's size. + const auto* text = static_cast(value); + program_ = std::string(text, size - 1); + } else { + // Binary payloads are stored exactly as received. + const std::span bytes(static_cast(value), size); + program_ = std::vector(bytes.begin(), bytes.end()); + } } return QDMI_SUCCESS; case QDMI_DEVICE_JOB_PARAMETER_SHOTSNUM: @@ -405,8 +411,10 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::queryProperty( ADD_SINGLE_VALUE_PROPERTY(QDMI_DEVICE_JOB_PROPERTY_PROGRAMFORMAT, QDMI_Program_Format, format_, prop, size, value, sizeRet) - ADD_STRING_PROPERTY(QDMI_DEVICE_JOB_PROPERTY_PROGRAM, program_.c_str(), prop, - size, value, sizeRet) + if (const auto* text = std::get_if(&program_)) { + ADD_STRING_PROPERTY(QDMI_DEVICE_JOB_PROPERTY_PROGRAM, text->c_str(), prop, + size, value, sizeRet) + } ADD_SINGLE_VALUE_PROPERTY(QDMI_DEVICE_JOB_PROPERTY_SHOTSNUM, size_t, numShots_, prop, size, value, sizeRet) return QDMI_ERROR_NOTSUPPORTED; @@ -434,14 +442,16 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQASMProgram() -> QDMI_STATUS { auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQASMProgramSampling() -> QDMI_STATUS { return submitProgramAsync([this]() { - const auto qc = qasm3::Importer::imports(program_); + const auto& text = std::get(program_); + const auto qc = qasm3::Importer::imports(text); counts_ = dd::sample(qc, numShots_); }); } auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQASMProgramStateExtraction() -> QDMI_STATUS { return submitProgramAsync([this]() { - auto qc = qasm3::Importer::imports(program_); + const auto& text = std::get(program_); + auto qc = qasm3::Importer::imports(text); qc::CircuitOptimizer::removeFinalMeasurements(qc); const auto nQubits = qc.getNqubits(); dd_ = std::make_unique(nQubits); @@ -457,7 +467,12 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQIRProgramSampling() -> QDMI_STATUS { return submitProgramAsync([this]() { auto& runtime = qir::Runtime::getInstance(); - auto irBytes = llvm::StringRef(program_.data(), program_.size()); + auto irBytes = std::visit( + [](const auto& p) { + return llvm::StringRef(reinterpret_cast(p.data()), + p.size()); + }, + program_); auto jitSession = qir::JitSession(irBytes, "QDMI job"); for (size_t i = 0; i < numShots_; ++i) { runtime.reset(); @@ -483,7 +498,12 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQIRProgramStateExtraction() return submitProgramAsync([this]() { auto& runtime = qir::Runtime::getInstance(); runtime.reset(); - auto irBytes = llvm::StringRef(program_.data(), program_.size()); + auto irBytes = std::visit( + [](const auto& p) { + return llvm::StringRef(reinterpret_cast(p.data()), + p.size()); + }, + program_); auto jitSession = qir::JitSession(irBytes, "QDMI job", qir::Execution::StateExtraction); if (const auto rc = jitSession.run(); rc != 0) { From d6e726db8257b95a0918e24b8940114ee7469e79 Mon Sep 17 00:00:00 2001 From: rturrado Date: Tue, 16 Jun 2026 14:18:04 +0200 Subject: [PATCH 33/33] Address comments from Lucas' code review - Device.cpp: Fix bug at `queryProperty`. The previous `std::get_if` guard silently dropped binary payloads. Use ADD_STRING_PROPERTY for the `std::string` alternative and ADD_LIST_PROPERTY for the `std::vector` alternative. - ProgramFormat.hpp: deleted; the predicate is inlined at its two call sites. - index.md: cover Adaptive Profile. - CHANGELOG.md, CMakeLists.txt, index.md: normalize "DDSIM QDMI Device" term. Assisted-by: Claude Opus 4.7 via Claude Code --- CHANGELOG.md | 2 +- CMakeLists.txt | 2 +- docs/qir/index.md | 6 ++-- .../qdmi/devices/dd/ProgramFormat.hpp | 31 ------------------- src/qdmi/devices/dd/CMakeLists.txt | 1 - src/qdmi/devices/dd/Device.cpp | 17 +++++++--- test/qdmi/devices/dd/helpers/test_utils.cpp | 7 +++-- 7 files changed, 23 insertions(+), 43 deletions(-) delete mode 100644 include/mqt-core/qdmi/devices/dd/ProgramFormat.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dc35df27e..0214758b81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ This project adheres to [Semantic Versioning], with the exception that minor rel ### Added -- ✨ Add QIR program format support to the QDMI DDSim device ([#1766]) ([**@rturrado**]) +- ✨ Add QIR program format support to the DDSIM QDMI Device ([#1766]) ([**@rturrado**]) - 🚸 Add [CMake presets] to provide a standardized and reproducible way to configure builds ([#1660]) ([**@denialhaag**]) - ✨ Add a `quantum-loop-unroll` pass for unrolling for-loop operations containing quantum operations ([#1718]) ([**@MatthiasReumann**]) - ✨ Add a `hadamard-lifting` pass for lifting Hadamard gates above Pauli gates ([#1605]) ([**@lirem101**], [**@burgholzer**]) diff --git a/CMakeLists.txt b/CMakeLists.txt index f616e74631..017d91b5f0 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -122,7 +122,7 @@ cmake_dependent_option(BUILD_MQT_CORE_QIR_RUNNER "Build the QIR runner of the MQ "BUILD_MQT_CORE_MLIR" OFF) cmake_dependent_option( - BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR "Enable QIR program format support for the DDSim QDMI device" + BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR "Enable QIR program format support for the DDSIM QDMI Device" ON "BUILD_MQT_CORE_MLIR" OFF) # add main library code diff --git a/docs/qir/index.md b/docs/qir/index.md index 68867b5870..c794af124c 100644 --- a/docs/qir/index.md +++ b/docs/qir/index.md @@ -38,8 +38,8 @@ The `mqt-core-qir-runner` can be used to execute a QIR file (typically with a `. This will simulate the circuit and print the measurement results to the console. The runner supports the QIR Base Profile. -### QIR Support in the QDMI Device +### QIR Support in the DDSIM QDMI Device -The QDMI Device accepts jobs in the following program formats: QASM2, QASM3, QIR Base Profile Module (LLVM bitcode), and QIR Base Profile String (LLVM assembly). -The QIR base formats are only supported when the `BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR` CMake option is enabled. +The QDMI Device accepts jobs in the following program formats: QASM2, QASM3, QIR Base/Adaptive Profile Module (LLVM bitcode), and QIR Base/Adaptive Profile String (LLVM assembly). +These QIR formats are only supported when the `BUILD_MQT_CORE_QDMI_DDSIM_WITH_QIR` CMake option is enabled. It is enabled by default, but depends on `BUILD_MQT_CORE_MLIR` being set. diff --git a/include/mqt-core/qdmi/devices/dd/ProgramFormat.hpp b/include/mqt-core/qdmi/devices/dd/ProgramFormat.hpp deleted file mode 100644 index d32bd09e62..0000000000 --- a/include/mqt-core/qdmi/devices/dd/ProgramFormat.hpp +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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 "mqt_ddsim_qdmi/constants.h" - -namespace qdmi::dd { - -/** - * @brief Whether @p fmt is a text-based QDMI program format. - * @details QDMI program formats fall into two byte-shape categories: - * - text formats (QASM, QIR Base/Adaptive String) are shipped with a trailing - * '\0' counted in the buffer size. - * - binary formats (QIR Base/Adaptive Module bitcode) are shipped as exact byte - * counts since '\0' may appear inside the payload. - */ -inline bool isTextProgramFormat(QDMI_Program_Format fmt) { - return fmt == QDMI_PROGRAM_FORMAT_QASM2 || fmt == QDMI_PROGRAM_FORMAT_QASM3 || - fmt == QDMI_PROGRAM_FORMAT_QIRBASESTRING || - fmt == QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING; -} - -} // namespace qdmi::dd diff --git a/src/qdmi/devices/dd/CMakeLists.txt b/src/qdmi/devices/dd/CMakeLists.txt index e203c1ea68..ee4a2a3458 100644 --- a/src/qdmi/devices/dd/CMakeLists.txt +++ b/src/qdmi/devices/dd/CMakeLists.txt @@ -35,7 +35,6 @@ if(NOT TARGET ${TARGET_NAME}) ${CMAKE_CURRENT_BINARY_DIR}/include FILES ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/devices/dd/Device.hpp - ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/devices/dd/ProgramFormat.hpp ${QDMI_HDRS}) # Add link libraries diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index 658a2084c4..d20e0202d4 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -23,7 +23,6 @@ #include "mqt_ddsim_qdmi/device.h" #include "qasm3/Importer.hpp" #include "qdmi/common/Common.hpp" -#include "qdmi/devices/dd/ProgramFormat.hpp" #include #include @@ -376,7 +375,12 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::setParameter( return QDMI_SUCCESS; case QDMI_DEVICE_JOB_PARAMETER_PROGRAM: if (value != nullptr) { - if (qdmi::dd::isTextProgramFormat(format_)) { + const bool isTextProgramFormat = + format_ == QDMI_PROGRAM_FORMAT_QASM2 || + format_ == QDMI_PROGRAM_FORMAT_QASM3 || + format_ == QDMI_PROGRAM_FORMAT_QIRBASESTRING || + format_ == QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING; + if (isTextProgramFormat) { // Text payloads include the trailing '\0' in `size`. // Strip it so it is not counted in the stored string's size. const auto* text = static_cast(value); @@ -411,9 +415,14 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::queryProperty( ADD_SINGLE_VALUE_PROPERTY(QDMI_DEVICE_JOB_PROPERTY_PROGRAMFORMAT, QDMI_Program_Format, format_, prop, size, value, sizeRet) - if (const auto* text = std::get_if(&program_)) { - ADD_STRING_PROPERTY(QDMI_DEVICE_JOB_PROPERTY_PROGRAM, text->c_str(), prop, + if (std::holds_alternative(program_)) { + const auto& text = std::get(program_); + ADD_STRING_PROPERTY(QDMI_DEVICE_JOB_PROPERTY_PROGRAM, text.c_str(), prop, size, value, sizeRet) + } else { + const auto& bytes = std::get>(program_); + ADD_LIST_PROPERTY(QDMI_DEVICE_JOB_PROPERTY_PROGRAM, std::byte, bytes, prop, + size, value, sizeRet) } ADD_SINGLE_VALUE_PROPERTY(QDMI_DEVICE_JOB_PROPERTY_SHOTSNUM, size_t, numShots_, prop, size, value, sizeRet) diff --git a/test/qdmi/devices/dd/helpers/test_utils.cpp b/test/qdmi/devices/dd/helpers/test_utils.cpp index 6435aae69d..9290821378 100644 --- a/test/qdmi/devices/dd/helpers/test_utils.cpp +++ b/test/qdmi/devices/dd/helpers/test_utils.cpp @@ -12,7 +12,6 @@ #include "mqt_ddsim_qdmi/constants.h" #include "mqt_ddsim_qdmi/device.h" -#include "qdmi/devices/dd/ProgramFormat.hpp" #include @@ -103,8 +102,12 @@ int setProgram(MQT_DDSIM_QDMI_Device_Job job, const QDMI_Program_Format fmt, // The `+1` is safe here because every existing call to `setProgram` with a // text format passes a `program` with a string literal or `std::string`, both // of which guarantee `'\0'` at `data()[size()]`. + const bool isTextProgramFormat = fmt == QDMI_PROGRAM_FORMAT_QASM2 || + fmt == QDMI_PROGRAM_FORMAT_QASM3 || + fmt == QDMI_PROGRAM_FORMAT_QIRBASESTRING || + fmt == QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING; const auto bytesToSend = - qdmi::dd::isTextProgramFormat(fmt) ? program.size() + 1 : program.size(); + isTextProgramFormat ? program.size() + 1 : program.size(); rc = MQT_DDSIM_QDMI_device_job_set_parameter( job, QDMI_DEVICE_JOB_PARAMETER_PROGRAM, bytesToSend, program.data()); return rc;