Skip to content

Commit 0e7e58b

Browse files
committed
♻️ Return native errors without exceptions
🤖 *AI text below* 🤖 Make QDMI, benchmark, and DD failures explicit without adding an LLVM requirement or changing C++20. Preserve provider statuses and diagnostics through LLVM and Python, and replace fallible constructors with factories. Stop QIR execution before using failed runtime results and leave sessions reusable. Disable exceptions in the migrated algorithms while retaining Python translation and deliberate C ABI barriers. Assisted-by: Codex
1 parent f7553f5 commit 0e7e58b

169 files changed

Lines changed: 9900 additions & 6017 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Explicit errors across Core and compiler APIs
2+
3+
Status: complete.
4+
5+
## Goal and scope
6+
7+
Replace exception-based recoverable failures in QDMI, benchmark, DD, and QIR
8+
execution APIs with explicit results. Keep C++20 and standalone builds without
9+
LLVM. Python keeps its normal exception interface. The baseline is PR #2545 at
10+
`f7553f5a5`.
11+
12+
## Decisions
13+
14+
- Standalone APIs use C++20 value/error variants and optional errors for
15+
operations without a result. Errors retain their category and message.
16+
- LLVM services use `llvm::Expected` and `llvm::Error`. MLIR operations use
17+
`LogicalResult` or `FailureOr` with diagnostics.
18+
- Each operation has one canonical fallible interface. Remove throwing/try
19+
pairs; translate errors only at LLVM, Python, CLI, and QDMI C boundaries.
20+
- Fallible construction uses factories; RAII continues to own partial resources.
21+
- Python bindings translate explicit errors with native nanobind primitives.
22+
Python call signatures remain stable where practical.
23+
- Preserve unsupported optional properties, warning behavior, strict JSON
24+
validation and canonical output, numerical checks, job states, and cleanup.
25+
- Runtime ABI callbacks require safe propagation through generated code. An
26+
error flag alone is insufficient: execution must stop before dependent work.
27+
- Recoverable input and operational failures must remain recoverable. Allocation
28+
exhaustion and unexpected dependency exceptions are nonrecoverable in the
29+
native algorithms. QIR ABI functions are noexcept, so exceptions cannot unwind
30+
through generated frames. Input checks continue to return errors.
31+
32+
## Progress
33+
34+
- [x] Canonical QDMI result APIs, driver/configuration propagation, and callers.
35+
- [x] Benchmark factories, parsing/evaluation results, and diagnostic transport.
36+
- [x] LLVM-native JIT setup and DD results through MLIR consumers.
37+
- [x] Safe QIR runtime failure propagation and exception build policy.
38+
- [x] Binding adaptation, stubs, migration documentation, and final validation.
39+
40+
## Validation
41+
42+
Use the release and release-no-mlir presets. Run subsystem tests while
43+
migrating, then native and Python regression suites, stub regeneration, C++ and
44+
repository lint. Verify actual compile commands for migrated targets. Retain
45+
provider failure/recovery, malformed input, partial initialization, runtime
46+
failure, Python exception category, and CLI file-safety checks.
47+
48+
Final checks:
49+
50+
- Release build and CTest: 3557 pass; the existing unsupported SC job-ID test
51+
skips. The suite includes runtime error propagation, session reuse, provider
52+
status preservation, strict JSON validation, and CLI regression checks.
53+
- Standalone build without LLVM and CTest: 579 pass; the same SC check skips.
54+
- Python 3.14 suite with both packaged providers: 1420 pass.
55+
- Stub generation passes and leaves Python signatures unchanged.
56+
- Complete executable documentation and local documentation links pass.
57+
- Repository lint and the standard C++ lint session pass. A supplementary
58+
whole-file clang-tidy scan covers every changed C++ source; affected files
59+
were checked again after fixes.
60+
- Actual compile commands confirm C++20 and disabled exceptions in the migrated
61+
algorithms. Python and C ABI exception barriers retain exception handling.
62+
63+
The error categories intentionally correct four Python test expectations:
64+
invalid DD arguments raise `ValueError`, and unsupported QDMI submissions raise
65+
`RuntimeError` through both the direct and compiled-program interfaces.
66+
67+
Local release builds disable IPO to avoid duplicate LLVM TypeID definitions from
68+
GCC LTO and the installed LLVM static libraries. This is a local configure
69+
option, not a repository policy change. The standard C++ linter omitted most
70+
uncommitted modified files, so the supplementary scan verifies its selection
71+
gap.
72+
73+
The follow-up complexity review removes 32 implementation lines: optional SC
74+
fields reuse required-field validation, QIR instrumentation shares attribute
75+
cleanup and keeps one current failure block, DD deserialization drops an
76+
obsolete array reset, and registry discovery uses one ordered loop.
77+
78+
The native and standalone suites and the Python tests pass after this cleanup.
79+
The update belongs to PR #2545.

‎bindings/bench/Result.hpp‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright (c) 2023 - 2026 Chair for Design Automation, TUM
3+
* Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH
4+
* All rights reserved.
5+
*
6+
* SPDX-License-Identifier: MIT
7+
*
8+
* Licensed under the MIT License
9+
*/
10+
11+
#pragma once
12+
13+
#include "bench/Error.hpp"
14+
15+
#include "nanobind/nanobind.h"
16+
17+
#include <pyerrors.h>
18+
#include <utility>
19+
#include <variant>
20+
21+
namespace mqt::bindings {
22+
23+
template <typename T> T takeBenchResult(bench::Result<T> result) {
24+
if (const auto* error = std::get_if<bench::Error>(&result)) {
25+
if (error->kind == bench::Error::Kind::Overflow) {
26+
const nanobind::gil_scoped_acquire acquire;
27+
PyErr_SetString(PyExc_OverflowError, error->message.c_str());
28+
throw nanobind::python_error();
29+
}
30+
throw nanobind::value_error(error->message.c_str());
31+
}
32+
return std::get<T>(std::move(result));
33+
}
34+
35+
template <typename C, typename R, typename... Args>
36+
auto bindBenchResult(R (C::*method)(Args...) const) {
37+
return [method](const C& self, Args... args) {
38+
return takeBenchResult((self.*method)(std::forward<Args>(args)...));
39+
};
40+
}
41+
42+
template <typename R, typename... Args>
43+
auto bindBenchResult(R (*function)(Args...)) {
44+
return [function](Args... args) {
45+
return takeBenchResult(function(std::forward<Args>(args)...));
46+
};
47+
}
48+
49+
} // namespace mqt::bindings

‎bindings/bench/register_bv.cpp‎

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,23 @@
1111
#include "bench/BV.hpp"
1212
#include "bench/JSON.hpp"
1313

14+
#include "Result.hpp"
15+
1416
#include "nanobind/nanobind.h"
1517
#include "nanobind/stl/map.h" // NOLINT(misc-include-cleaner)
1618
#include "nanobind/stl/string.h" // NOLINT(misc-include-cleaner)
1719
#include "nanobind/stl/string_view.h" // NOLINT(misc-include-cleaner)
1820

21+
#include <new>
1922
#include <string>
23+
#include <utility>
2024

2125
namespace mqt {
2226

2327
namespace nb = nanobind;
2428
using namespace nb::literals;
29+
using bindings::bindBenchResult;
30+
using bindings::takeBenchResult;
2531

2632
// NOLINTNEXTLINE(misc-use-internal-linkage)
2733
void registerBV(const nb::module_& m) {
@@ -40,16 +46,22 @@ void registerBV(const nb::module_& m) {
4046

4147
auto bv = nb::class_<bench::BV>(m, "BV",
4248
"A validated Bernstein--Vazirani benchmark.");
43-
bv.def(nb::init<bench::BVOptions>(), "options"_a)
49+
bv.def(
50+
"__init__",
51+
[](bench::BV* self, bench::BVOptions options) {
52+
new (self)
53+
bench::BV(takeBenchResult(bench::BV::create(std::move(options))));
54+
},
55+
"options"_a)
4456
.def_prop_ro("options", &bench::BV::options,
4557
nb::rv_policy::reference_internal,
4658
"The resolved benchmark parameters.")
4759
.def_prop_ro("output", &bench::BV::output,
4860
nb::rv_policy::reference_internal,
4961
"The logical output register.")
50-
.def("probability", &bench::BV::probability, "outcome"_a,
62+
.def("probability", bindBenchResult(&bench::BV::probability), "outcome"_a,
5163
"Return the ideal probability of an outcome.")
52-
.def("evaluate", &bench::BV::evaluate, "counts"_a,
64+
.def("evaluate", bindBenchResult(&bench::BV::evaluate), "counts"_a,
5365
"Compare sampled counts with the ideal distribution.")
5466
.def(
5567
"generate",
@@ -68,17 +80,23 @@ void registerBV(const nb::module_& m) {
6880
"The canonical instance specification JSON.")
6981
.def_prop_ro(
7082
"manifest_json",
71-
[](const bench::BV& value) { return bench::toManifestJSON(value); },
83+
[](const bench::BV& value) {
84+
return takeBenchResult(bench::toManifestJSON(value));
85+
},
7286
"The canonical manifest JSON.")
7387
.def_prop_ro(
7488
"case_id",
75-
[](const bench::BV& value) { return bench::caseId(value); },
89+
[](const bench::BV& value) {
90+
return takeBenchResult(bench::caseId(value));
91+
},
7692
"The stable semantic case ID.")
7793
.def_static("from_instance_specification_json",
78-
&bench::bvFromInstanceSpecificationJSON, "json"_a,
79-
nb::kw_only(), "source"_a = "<instance-specification>",
94+
bindBenchResult(&bench::bvFromInstanceSpecificationJSON),
95+
"json"_a, nb::kw_only(),
96+
"source"_a = "<instance-specification>",
8097
"Parse a strict benchmark instance specification.")
81-
.def_static("from_manifest_json", &bench::bvFromManifestJSON, "json"_a,
98+
.def_static("from_manifest_json",
99+
bindBenchResult(&bench::bvFromManifestJSON), "json"_a,
82100
nb::kw_only(), "source"_a = "<manifest>",
83101
"Parse a strict benchmark manifest.");
84102
}

‎bindings/bench/register_ghz.cpp‎

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,22 @@
1111
#include "bench/GHZ.hpp"
1212
#include "bench/JSON.hpp"
1313

14+
#include "Result.hpp"
15+
1416
#include "nanobind/nanobind.h"
1517
#include "nanobind/stl/map.h" // NOLINT(misc-include-cleaner)
1618
#include "nanobind/stl/string.h" // NOLINT(misc-include-cleaner)
1719
#include "nanobind/stl/string_view.h" // NOLINT(misc-include-cleaner)
1820

1921
#include <cstddef>
22+
#include <new>
2023

2124
namespace mqt {
2225

2326
namespace nb = nanobind;
2427
using namespace nb::literals;
28+
using bindings::bindBenchResult;
29+
using bindings::takeBenchResult;
2530

2631
// NOLINTNEXTLINE(misc-use-internal-linkage)
2732
void registerGHZ(const nb::module_& m) {
@@ -44,16 +49,21 @@ void registerGHZ(const nb::module_& m) {
4449
.def_ro("basis", &bench::GHZOptions::basis, "The measurement basis.");
4550

4651
auto ghz = nb::class_<bench::GHZ>(m, "GHZ", "A validated GHZ benchmark.");
47-
ghz.def(nb::init<bench::GHZOptions>(), "options"_a)
52+
ghz.def(
53+
"__init__",
54+
[](bench::GHZ* self, bench::GHZOptions options) {
55+
new (self) bench::GHZ(takeBenchResult(bench::GHZ::create(options)));
56+
},
57+
"options"_a)
4858
.def_prop_ro("options", &bench::GHZ::options,
4959
nb::rv_policy::reference_internal,
5060
"The resolved benchmark parameters.")
5161
.def_prop_ro("output", &bench::GHZ::output,
5262
nb::rv_policy::reference_internal,
5363
"The logical output register.")
54-
.def("probability", &bench::GHZ::probability, "outcome"_a,
55-
"Return the ideal probability of an outcome.")
56-
.def("evaluate", &bench::GHZ::evaluate, "counts"_a,
64+
.def("probability", bindBenchResult(&bench::GHZ::probability),
65+
"outcome"_a, "Return the ideal probability of an outcome.")
66+
.def("evaluate", bindBenchResult(&bench::GHZ::evaluate), "counts"_a,
5767
"Compare sampled counts with the ideal distribution.")
5868
.def(
5969
"generate",
@@ -72,17 +82,23 @@ void registerGHZ(const nb::module_& m) {
7282
"The canonical instance specification JSON.")
7383
.def_prop_ro(
7484
"manifest_json",
75-
[](const bench::GHZ& value) { return bench::toManifestJSON(value); },
85+
[](const bench::GHZ& value) {
86+
return takeBenchResult(bench::toManifestJSON(value));
87+
},
7688
"The canonical manifest JSON.")
7789
.def_prop_ro(
7890
"case_id",
79-
[](const bench::GHZ& value) { return bench::caseId(value); },
91+
[](const bench::GHZ& value) {
92+
return takeBenchResult(bench::caseId(value));
93+
},
8094
"The stable semantic case ID.")
8195
.def_static("from_instance_specification_json",
82-
&bench::ghzFromInstanceSpecificationJSON, "json"_a,
83-
nb::kw_only(), "source"_a = "<instance-specification>",
96+
bindBenchResult(&bench::ghzFromInstanceSpecificationJSON),
97+
"json"_a, nb::kw_only(),
98+
"source"_a = "<instance-specification>",
8499
"Parse a strict benchmark instance specification.")
85-
.def_static("from_manifest_json", &bench::ghzFromManifestJSON, "json"_a,
100+
.def_static("from_manifest_json",
101+
bindBenchResult(&bench::ghzFromManifestJSON), "json"_a,
86102
nb::kw_only(), "source"_a = "<manifest>",
87103
"Parse a strict benchmark manifest.");
88104
}

‎bindings/bench/register_grover.cpp‎

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,26 @@
1111
#include "bench/Grover.hpp"
1212
#include "bench/JSON.hpp"
1313

14+
#include "Result.hpp"
15+
1416
#include "nanobind/nanobind.h"
1517
#include "nanobind/stl/map.h" // NOLINT(misc-include-cleaner)
1618
#include "nanobind/stl/optional.h" // NOLINT(misc-include-cleaner)
1719
#include "nanobind/stl/string.h" // NOLINT(misc-include-cleaner)
1820
#include "nanobind/stl/string_view.h" // NOLINT(misc-include-cleaner)
1921

2022
#include <cstddef>
23+
#include <new>
2124
#include <optional>
2225
#include <string>
26+
#include <utility>
2327

2428
namespace mqt {
2529

2630
namespace nb = nanobind;
2731
using namespace nb::literals;
32+
using bindings::bindBenchResult;
33+
using bindings::takeBenchResult;
2834

2935
// NOLINTNEXTLINE(misc-use-internal-linkage)
3036
void registerGrover(const nb::module_& m) {
@@ -39,7 +45,14 @@ void registerGrover(const nb::module_& m) {
3945

4046
auto grover = nb::class_<bench::Grover>(
4147
m, "Grover", "A validated single-solution Grover benchmark.");
42-
grover.def(nb::init<bench::GroverOptions>(), "options"_a)
48+
grover
49+
.def(
50+
"__init__",
51+
[](bench::Grover* self, bench::GroverOptions options) {
52+
new (self) bench::Grover(
53+
takeBenchResult(bench::Grover::create(std::move(options))));
54+
},
55+
"options"_a)
4356
.def_prop_ro("options", &bench::Grover::options,
4457
nb::rv_policy::reference_internal,
4558
"The resolved benchmark parameters.")
@@ -48,9 +61,9 @@ void registerGrover(const nb::module_& m) {
4861
"The logical output register.")
4962
.def_prop_ro("qubits", &bench::Grover::qubits,
5063
"The number of search qubits.")
51-
.def("probability", &bench::Grover::probability, "outcome"_a,
52-
"Return the ideal probability of an outcome.")
53-
.def("evaluate", &bench::Grover::evaluate, "counts"_a,
64+
.def("probability", bindBenchResult(&bench::Grover::probability),
65+
"outcome"_a, "Return the ideal probability of an outcome.")
66+
.def("evaluate", bindBenchResult(&bench::Grover::evaluate), "counts"_a,
5467
"Compare sampled counts with the ideal distribution.")
5568
.def(
5669
"generate",
@@ -70,19 +83,23 @@ void registerGrover(const nb::module_& m) {
7083
.def_prop_ro(
7184
"manifest_json",
7285
[](const bench::Grover& value) {
73-
return bench::toManifestJSON(value);
86+
return takeBenchResult(bench::toManifestJSON(value));
7487
},
7588
"The canonical manifest JSON.")
7689
.def_prop_ro(
7790
"case_id",
78-
[](const bench::Grover& value) { return bench::caseId(value); },
91+
[](const bench::Grover& value) {
92+
return takeBenchResult(bench::caseId(value));
93+
},
7994
"The stable semantic case ID.")
8095
.def_static("from_instance_specification_json",
81-
&bench::groverFromInstanceSpecificationJSON, "json"_a,
82-
nb::kw_only(), "source"_a = "<instance-specification>",
96+
bindBenchResult(&bench::groverFromInstanceSpecificationJSON),
97+
"json"_a, nb::kw_only(),
98+
"source"_a = "<instance-specification>",
8399
"Parse a strict benchmark instance specification.")
84-
.def_static("from_manifest_json", &bench::groverFromManifestJSON,
85-
"json"_a, nb::kw_only(), "source"_a = "<manifest>",
100+
.def_static("from_manifest_json",
101+
bindBenchResult(&bench::groverFromManifestJSON), "json"_a,
102+
nb::kw_only(), "source"_a = "<manifest>",
86103
"Parse a strict benchmark manifest.");
87104
}
88105

0 commit comments

Comments
 (0)