diff --git a/.agent/plans/first-mlir-python-bindings.md b/.agent/plans/first-mlir-python-bindings.md
index 305053adcf..bb695d05d9 100644
--- a/.agent/plans/first-mlir-python-bindings.md
+++ b/.agent/plans/first-mlir-python-bindings.md
@@ -21,8 +21,8 @@ Jeff serialization, QIR output, and Qiskit subclass support.
## Progress
- [x] (2026-06-26 to 2026-07-15) Implemented typed compiler programs, the Python
- binding module, package integration, documentation, and tests; merged
- through pull request #1815.
+ binding module, package integration, documentation, and tests; merged
+ through pull request #1815.
- [x] (2026-07-15) Reviewed CodeRabbit feedback and incorporated valid input,
type-validation, CLI, pass-registration, typing, and documentation fixes.
- [x] (2026-07-15) Added binding-owned Python docstrings and regenerated stubs.
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index f38e122258..23036ef938 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -27,7 +27,9 @@ This checklist serves as a reminder of a couple of things that ensure your pull
**If PR contains AI-assisted content:**
-- [ ] Any agent that created, edited, or submitted GitHub content was explicitly authorized for that scope, as required by our [AI Usage Guidelines](https://github.com/munich-quantum-toolkit/core/blob/main/docs/ai_usage.md).
-- [ ] Every agent-authored or agent-edited public text body begins with the visible disclosure `🤖 *AI text below* 🤖` (titles are exempt).
+- [ ] Any agent that created, edited, or submitted GitHub content was explicitly authorized for that scope, as required
+ by our [AI Usage Guidelines](https://github.com/munich-quantum-toolkit/core/blob/main/docs/ai_usage.md).
+- [ ] Every agent-authored or agent-edited public text body begins with the visible disclosure `🤖 *AI text below* 🤖` (
+ titles are exempt).
- [ ] AI-assisted commits include an `Assisted-by: [Model Name] via [Tool Name]` footer.
- [ ] I confirm that I have personally reviewed and understood all AI-generated content, and accept full responsibility for it.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b1ecfbd17a..6838dcdc93 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,8 @@ releases may include breaking changes.
### Added
+- ✨ Add a `constant propagation` pass for reducing superfluous quantum
+ resources by propagating the quantum machine state ([#1845]) ([**@lirem101])
- ✨ Add Python bindings for the MQT Compiler Collection ([#1815])
([**@burgholzer**], [**@denialhaag**])
- ✨ Add support for QDMI child devices to the driver and FoMaC libraries
@@ -649,6 +651,7 @@ changelogs._
[#1850]: https://github.com/munich-quantum-toolkit/core/pull/1850
[#1849]: https://github.com/munich-quantum-toolkit/core/pull/1849
[#1848]: https://github.com/munich-quantum-toolkit/core/pull/1848
+[#1845]: https://github.com/munich-quantum-toolkit/core/pull/1845
[#1844]: https://github.com/munich-quantum-toolkit/core/pull/1844
[#1842]: https://github.com/munich-quantum-toolkit/core/pull/1842
[#1836]: https://github.com/munich-quantum-toolkit/core/pull/1836
@@ -659,6 +662,7 @@ changelogs._
[#1824]: https://github.com/munich-quantum-toolkit/core/pull/1824
[#1823]: https://github.com/munich-quantum-toolkit/core/pull/1823
[#1817]: https://github.com/munich-quantum-toolkit/core/pull/1817
+
[#1815]: https://github.com/munich-quantum-toolkit/core/pull/1815
[#1814]: https://github.com/munich-quantum-toolkit/core/pull/1814
[#1809]: https://github.com/munich-quantum-toolkit/core/pull/1809
diff --git a/UPGRADING.md b/UPGRADING.md
index 043fd920e2..9cb8c065ff 100644
--- a/UPGRADING.md
+++ b/UPGRADING.md
@@ -407,6 +407,7 @@ It also requires the `uv` library version 0.5.20 or higher.
[unreleased]: https://github.com/munich-quantum-toolkit/core/compare/v3.7.0...HEAD
+
[3.7.0]: https://github.com/munich-quantum-toolkit/core/compare/v3.6.0...v3.7.0
[3.6.0]: https://github.com/munich-quantum-toolkit/core/compare/v3.5.1...v3.6.0
[3.5.1]: https://github.com/munich-quantum-toolkit/core/compare/v3.5.0...v3.5.1
diff --git a/docs/_templates/doxygen-header.html b/docs/_templates/doxygen-header.html
index ee3ed16948..2052185fed 100644
--- a/docs/_templates/doxygen-header.html
+++ b/docs/_templates/doxygen-header.html
@@ -84,7 +84,9 @@
-
$projectbrief |
+
+ $projectbrief
+ |
$searchbox |
diff --git a/docs/conf.py b/docs/conf.py
index e4cb231de1..a063535212 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -173,7 +173,6 @@ def format_url(self, _e: Entry) -> HRef: # noqa: PLR6301
("py:class", r"qiskit\.primitives\.containers\.(Estimator|Sampler)PubLike"),
]
-
cpp_api_tagfile = ("_build/doxygen/mqt-core.tag", "cpp/", "_build/doxygen/xml")
qdmi_api_tagfile = (
"_build/qdmi.tag",
diff --git a/docs/mlir/python_compiler_collection.md b/docs/mlir/python_compiler_collection.md
index 85b72e060b..415348fce9 100644
--- a/docs/mlir/python_compiler_collection.md
+++ b/docs/mlir/python_compiler_collection.md
@@ -62,14 +62,14 @@ The compiler accepts source strings, `.qasm`, `.mlir`, and `.jeff` files, MQT
and Qiskit circuit objects, and typed compiler programs. Select an output format
to stop the pipeline at a particular representation:
-| Purpose | Output format | Result type |
-| --- | --- | --- |
-| Inspect frontend translation | `OutputFormat.QC_IMPORT` | `QCProgram` |
-| Inspect QCO immediately after conversion | `OutputFormat.QCO` | `QCOProgram` |
-| Inspect QCO after optimization | `OutputFormat.QCO_OPTIMIZED` | `QCOProgram` |
-| Obtain the optimized circuit | `OutputFormat.QC` (default) | `QCProgram` |
-| Serialize a compiler program | `OutputFormat.JEFF` | `JeffProgram` |
-| Generate QIR | `OutputFormat.QIR_BASE` or `OutputFormat.QIR_ADAPTIVE` | `QIRProgram` |
+| Purpose | Output format | Result type |
+| ---------------------------------------- | ------------------------------------------------------ | ------------- |
+| Inspect frontend translation | `OutputFormat.QC_IMPORT` | `QCProgram` |
+| Inspect QCO immediately after conversion | `OutputFormat.QCO` | `QCOProgram` |
+| Inspect QCO after optimization | `OutputFormat.QCO_OPTIMIZED` | `QCOProgram` |
+| Obtain the optimized circuit | `OutputFormat.QC` (default) | `QCProgram` |
+| Serialize a compiler program | `OutputFormat.JEFF` | `JeffProgram` |
+| Generate QIR | `OutputFormat.QIR_BASE` or `OutputFormat.QIR_ADAPTIVE` | `QIRProgram` |
For example, select optimized QCO to inspect the representation after the
default QCO pass pipeline:
diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/ClassicalArithOperation.h b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/ClassicalArithOperation.h
new file mode 100644
index 0000000000..15d8f351f4
--- /dev/null
+++ b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/ClassicalArithOperation.h
@@ -0,0 +1,109 @@
+/*
+ * 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
+
+#ifndef MQT_CORE_CLASSICALARITHOPERATION_H
+#define MQT_CORE_CLASSICALARITHOPERATION_H
+
+#include "mlir/Dialect/QCO/IR/QCOOps.h"
+#include "mlir/Dialect/QCO/Utils/Drivers.h"
+
+#include
+#include
+#include
+
+#include
+#include
+
+/**
+ * This file provides information of available arith operations. It calculates
+ * the result of valid arith operations. Operations are only valid with one to
+ * two operands, not if they are applied to sequences.
+ */
+inline int64_t getArithIntegerOpResult(mlir::Operation* operation,
+ int64_t value1, int64_t value2 = 0,
+ int64_t value3 = 0) {
+
+ for (mlir::Value operand : operation->getOperands()) {
+ if (isa(operand.getType())) {
+ throw std::runtime_error(
+ "Constant propagation does not support vectors as classical types.");
+ }
+ }
+
+ return mlir::TypeSwitch(operation)
+ .Case([&](auto) { return value1 + value2; })
+ .Case([&](auto) { return value1 & value2; })
+ .Case([&](auto) {
+ // Division that rounds to positive infinity
+ return ceil(1.0 * value1 / value2);
+ })
+ .Case([&](auto) {
+ // Division that rounds towards zero
+ return value1 / value2;
+ })
+ .Case([&](auto) {
+ // Division that rounds to negative infinity
+ return floor(1.0 * value1 / value2);
+ })
+ .Case(
+ [&](auto) { return value1 > value2 ? value1 : value2; })
+ .Case(
+ [&](auto) { return value1 < value2 ? value1 : value2; })
+ .Case([&](auto) { return value1 * value2; })
+ .Case([&](auto) { return value1 | value2; })
+ .Case(
+ [&](auto) { return remainder(value1, value2); })
+ .Case([&](auto) { return value1 << value2; })
+ .Case([&](auto) { return value1 >> value2; })
+ .Case([&](auto) { return value1 - value2; })
+ .Case([&](auto) { return value1 ^ value2; })
+ .Case(
+ [&](auto) { return value1 == 0 ? value3 : value2; })
+ .Default([&](auto) -> int64_t {
+ throw std::runtime_error("Unsupported integer operation in "
+ "mlir::qco::classicalarithoperation");
+ });
+}
+
+inline double getArithDoubleOpResult(mlir::Operation* operation, double value1,
+ double value2 = 0.0) {
+
+ for (mlir::Value operand : operation->getOperands()) {
+ if (isa(operand.getType())) {
+ throw std::runtime_error(
+ "Constant propagation does not support vectors as classical types.");
+ }
+ }
+
+ return mlir::TypeSwitch(operation)
+ .Case([&](auto) { return value1 + value2; })
+ .Case([&](auto) { return value1 / value2; })
+ .Case(
+ [&](auto) { return value1 > value2 ? value1 : value2; })
+ .Case(
+ [&](auto) { return value1 > value2 ? value1 : value2; })
+ .Case(
+ [&](auto) { return value1 < value2 ? value1 : value2; })
+ .Case(
+ [&](auto) { return value1 < value2 ? value1 : value2; })
+ .Case([&](auto) { return value1 * value2; })
+ .Case([&](auto) { return -value1; })
+ .Case(
+ [&](auto) { return remainder(value1, value2); })
+ .Case([&](auto) { return value1 - value2; })
+ .Default([&](auto) -> double {
+ throw std::runtime_error("Unsupported floating-point operation in "
+ "mlir::qco::classicalarithoperation");
+ });
+}
+
+#endif // MQT_CORE_MQT_CORE_CLASSICALARITHOPERATION_H
diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/GateToMap.h b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/GateToMap.h
new file mode 100644
index 0000000000..663ebdcb76
--- /dev/null
+++ b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/GateToMap.h
@@ -0,0 +1,229 @@
+/*
+ * 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
+#ifndef MQT_CORE_GATETOMAP_H
+#define MQT_CORE_GATETOMAP_H
+
+#include "mlir/Dialect/QCO/IR/QCOOps.h"
+#include "mlir/Dialect/QCO/Utils/Drivers.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+/**
+ * This file provides information of available quantum gates as mappings. It is
+ * used in constant propagation to get the factors each amplitude has to be
+ * multiplied with to get the new amplitudes after a gate application.
+ */
+
+using Complex = std::complex;
+
+using ResultMap =
+ std::unordered_map>;
+
+constexpr double inv_sqrt2 = 1.0 / std::numbers::sqrt2;
+
+inline std::unordered_map>
+getQubitMappingOfGates(mlir::Operation* gate, const std::span& params) {
+
+ return mlir::TypeSwitch<
+ mlir::Operation*,
+ std::unordered_map>>(
+ gate)
+ .Case([&](auto) {
+ return ResultMap{{0, {{0, Complex(1, 0)}}}, {1, {{1, Complex(1, 0)}}}};
+ })
+ .Case([&](auto) {
+ return ResultMap{
+ {{0, {{0, Complex(inv_sqrt2, 0)}, {1, Complex(inv_sqrt2, 0)}}},
+ {1, {{0, Complex(inv_sqrt2, 0)}, {1, Complex(-inv_sqrt2, 0)}}}}};
+ })
+ .Case([&](auto) {
+ return ResultMap{
+ {{0, {{1, Complex(1, 0)}}}, {1, {{0, Complex(1, 0)}}}}};
+ })
+ .Case([&](auto) {
+ return ResultMap{
+ {{0, {{1, Complex(0, 1)}}}, {1, {{0, Complex(0, -1)}}}}};
+ })
+ .Case([&](auto) {
+ return ResultMap{
+ {{0, {{0, Complex(1, 0)}}}, {1, {{1, Complex(-1, 0)}}}}};
+ })
+ .Case([&](auto) {
+ return ResultMap{
+ {{0, {{0, Complex(1, 0)}}}, {1, {{1, Complex(0, 1)}}}}};
+ })
+ .Case([&](auto) {
+ return ResultMap{
+ {{0, {{0, Complex(1, 0)}}}, {1, {{1, Complex(0, -1)}}}}};
+ })
+ .Case([&](auto) {
+ return ResultMap{{{0, {{0, Complex(1, 0)}}},
+ {1, {{1, Complex(inv_sqrt2, inv_sqrt2)}}}}};
+ })
+ .Case([&](auto) {
+ return ResultMap{{{0, {{0, Complex(1, 0)}}},
+ {1, {{1, Complex(inv_sqrt2, -inv_sqrt2)}}}}};
+ })
+ .Case([&](auto) {
+ return ResultMap{
+ {{0, {{0, Complex(0.5, 0.5)}, {1, Complex(0.5, -0.5)}}},
+ {1, {{0, Complex(0.5, -0.5)}, {1, Complex(0.5, 0.5)}}}}};
+ })
+ .Case([&](auto) {
+ return ResultMap{
+ {{0, {{0, Complex(0.5, -0.5)}, {1, Complex(0.5, 0.5)}}},
+ {1, {{0, Complex(0.5, 0.5)}, {1, Complex(0.5, -0.5)}}}}};
+ })
+ .Case([&](auto) {
+ const double c = cos(0.5 * params[0]);
+ const double s = sin(0.5 * params[0]);
+ return ResultMap{{{0, {{0, Complex(c, 0)}, {1, Complex(0, -s)}}},
+ {1, {{0, Complex(0, -s)}, {1, Complex(c, 0)}}}}};
+ })
+ .Case([&](auto) {
+ const double c = cos(0.5 * params[0]);
+ const double s = sin(0.5 * params[0]);
+ return ResultMap{{{0, {{0, Complex(c, 0)}, {1, Complex(s, 0)}}},
+ {1, {{0, Complex(-s, 0)}, {1, Complex(c, 0)}}}}};
+ })
+ .Case([&](auto) {
+ const double halfParameter = 0.5 * params[0];
+ return ResultMap{{{0, {{0, exp(Complex(0, -halfParameter))}}},
+ {1, {{1, exp(Complex(0, halfParameter))}}}}};
+ })
+ .Case([&](auto) {
+ return ResultMap{{{0, {{0, Complex(1, 0)}}},
+ {1, {{1, exp(Complex(0, params[0]))}}}}};
+ })
+ .Case([&](auto) {
+ const double c = cos(0.5 * params[0]);
+ const double s = sin(0.5 * params[0]);
+ return ResultMap{{{0,
+ {{0, Complex(c, 0)},
+ {1, exp(Complex(0, params[1])) * Complex(0, -s)}}},
+ {1,
+ {{0, exp(Complex(0, -params[1])) * Complex(0, -s)},
+ {1, Complex(c, 0)}}}}};
+ })
+ .Case([&](auto) {
+ return ResultMap{
+ {{0, {{0, Complex(inv_sqrt2, 0)}, {1, exp(Complex(0, params[0]))}}},
+ {1,
+ {{0, -exp(Complex(0, params[1]))},
+ {1, exp(Complex(0, params[0] + params[1]))}}}}};
+ })
+ .Case([&](auto) {
+ const double c = cos(0.5 * params[0]);
+ const double s = sin(0.5 * params[0]);
+ return ResultMap{
+ {{0, {{0, Complex(c, 0)}, {1, exp(Complex(0, params[1])) * s}}},
+ {1,
+ {{0, -exp(Complex(0, params[2])) * s},
+ {1, exp(Complex(0, params[1] + params[2])) * c}}}}};
+ })
+ .Case([&](auto) {
+ return ResultMap{{{0, {{0, Complex(1, 0)}}},
+ {1, {{2, Complex(1, 0)}}},
+ {2, {{1, Complex(1, 0)}}},
+ {3, {{3, Complex(1, 0)}}}}};
+ })
+ .Case([&](auto) {
+ return ResultMap{{{0, {{0, Complex(1, 0)}}},
+ {1, {{2, Complex(0, -1)}}},
+ {2, {{1, Complex(0, -1)}}},
+ {3, {{3, Complex(1, 0)}}}}};
+ })
+ .Case([&](auto) {
+ return ResultMap{{{0, {{0, Complex(1, 0)}}},
+ {1, {{2, Complex(1, 0)}}},
+ {2, {{3, Complex(1, 0)}}},
+ {3, {{1, Complex(1, 0)}}}}};
+ })
+ .Case([&](auto) {
+ return ResultMap{
+ {{0, {{2, Complex(inv_sqrt2, 0)}, {3, Complex(0, -inv_sqrt2)}}},
+ {1, {{2, Complex(0, -inv_sqrt2)}, {3, Complex(inv_sqrt2, 0)}}},
+ {2, {{0, Complex(inv_sqrt2, 0)}, {1, Complex(0, inv_sqrt2)}}},
+ {3, {{0, Complex(0, inv_sqrt2)}, {1, Complex(inv_sqrt2, 0)}}}}};
+ })
+ .Case([&](auto) {
+ const double c = cos(0.5 * params[0]);
+ const double s = sin(0.5 * params[0]);
+ return ResultMap{{{0, {{0, Complex(c, 0)}, {3, Complex(0, -s)}}},
+ {1, {{1, Complex(c, 0)}, {2, Complex(0, -s)}}},
+ {2, {{1, Complex(0, -s)}, {2, Complex(c, 0)}}},
+ {3, {{0, Complex(0, -s)}, {3, Complex(c, 0)}}}}};
+ })
+ .Case([&](auto) {
+ const double c = cos(0.5 * params[0]);
+ const double s = sin(0.5 * params[0]);
+ return ResultMap{{{0, {{0, Complex(c, 0)}, {3, Complex(0, s)}}},
+ {1, {{1, Complex(c, 0)}, {2, Complex(0, -s)}}},
+ {2, {{1, Complex(0, -s)}, {2, Complex(c, 0)}}},
+ {3, {{0, Complex(0, s)}, {3, Complex(c, 0)}}}}};
+ })
+ .Case([&](auto) {
+ const double c = cos(0.5 * params[0]);
+ const double s = sin(0.5 * params[0]);
+ return ResultMap{{{0, {{0, Complex(c, 0)}, {2, Complex(0, -s)}}},
+ {1, {{1, Complex(c, 0)}, {3, Complex(0, s)}}},
+ {2, {{0, Complex(0, -s)}, {2, Complex(c, 0)}}},
+ {3, {{1, Complex(0, s)}, {3, Complex(c, 0)}}}}};
+ })
+ .Case([&](auto) {
+ const double halfParam = 0.5 * params[0];
+ Complex ePos = exp(Complex(0, halfParam));
+ Complex eNeg = exp(Complex(0, -halfParam));
+ return ResultMap{{{0, {{0, eNeg}}},
+ {1, {{1, ePos}}},
+ {2, {{2, ePos}}},
+ {3, {{3, eNeg}}}}};
+ })
+ .Case([&](auto) {
+ const double c = cos(0.5 * params[0]);
+ const double s = sin(0.5 * params[0]);
+ return ResultMap{{{0, {{0, Complex(1, 0)}}},
+ {1,
+ {{1, Complex(c, 0)},
+ {2, Complex(0, -s) * exp(Complex(0, params[1]))}}},
+ {2,
+ {{1, Complex(0, -s) * exp(Complex(0, -params[1]))},
+ {2, Complex(c, 0)}}},
+ {3, {{3, Complex(1, 0)}}}}};
+ })
+ .Case([&](auto) {
+ const double c = cos(0.5 * params[0]);
+ const double s = sin(0.5 * params[0]);
+ return ResultMap{{{0,
+ {{0, Complex(c, 0)},
+ {3, Complex(0, -s) * exp(Complex(0, params[1]))}}},
+ {1, {{1, Complex(1, 0)}}},
+ {2, {{2, Complex(1, 0)}}},
+ {3,
+ {{0, Complex(0, -s) * exp(Complex(0, -params[1]))},
+ {3, Complex(c, 0)}}}}};
+ })
+ .Default([&](auto) -> ResultMap {
+ throw std::runtime_error("Unsupported gate in mlir::qco::gatetomap");
+ });
+}
+
+#endif // MQT_CORE_GATETOMAP_H
diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.hpp b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.hpp
new file mode 100644
index 0000000000..853b65bae7
--- /dev/null
+++ b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.hpp
@@ -0,0 +1,322 @@
+/*
+ * 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
+ */
+
+#ifndef MQT_CORE_HYBRIDSTATE_H
+#define MQT_CORE_HYBRIDSTATE_H
+
+#include "QuantumState.hpp"
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+namespace mlir::qco {
+/**
+ * @brief This class represents a hybrid state.
+ *
+ * This class holds a QuantumState and a mapping form values to integers or
+ * doubles, as well as a probability.
+ */
+class HybridState {
+ bool top = false;
+ std::shared_ptr qState;
+ double probability;
+ llvm::DenseMap integerValues;
+ llvm::DenseMap doubleValues;
+
+ HybridState() : probability(0.0) {}
+
+ /**
+ * @brief Checks if all positive classical controls hold and all negative
+ * classical controls do not hold.
+ *
+ * @param posCtrlsClassical An array of the classical positive control values.
+ * @param negCtrlsClassical An array of the classical negative control values.
+ * @return True if the controls together evaluate to true.
+ * @throws domain_error If a classical control value cannot be found.
+ */
+ bool isOperationExecutable(const std::span posCtrlsClassical,
+ const std::span negCtrlsClassical) {
+ for (const Value posCtrl : posCtrlsClassical) {
+ if (integerValues.contains(posCtrl) && integerValues.at(posCtrl) == 0) {
+ return false;
+ }
+ if (doubleValues.contains(posCtrl) &&
+ std::norm(doubleValues.at(posCtrl)) < 1e-4) {
+ return false;
+ }
+ if (!doubleValues.contains(posCtrl) && !integerValues.contains(posCtrl)) {
+ throw std::domain_error(
+ "HybridState needs a classical value for operation control that is "
+ "not existent in current HybridState.");
+ }
+ }
+ for (const Value negCtrl : negCtrlsClassical) {
+ if (integerValues.contains(negCtrl) && integerValues.at(negCtrl) != 0) {
+ return false;
+ }
+ if (doubleValues.contains(negCtrl) &&
+ std::norm(doubleValues.at(negCtrl)) > 1e-4) {
+ return false;
+ }
+ if (!doubleValues.contains(negCtrl) && !integerValues.contains(negCtrl)) {
+ throw std::domain_error(
+ "HybridState needs a classical value for operation control that is "
+ "not existent in current HybridState.");
+ }
+ }
+ return true;
+ }
+
+ /**
+ * @brief This method applies a measurement or reset.
+ *
+ * This method applies a measurement or reset, changing the qubits and the
+ * classical values (if a measurement is applied) corresponding to the
+ * measurement.
+ *
+ * @param quantumTarget The index of the qubit to be measured.
+ * @param reset True if a reset is applied.
+ * @param classicalTarget The value to save the measurement result to.
+ * @param posCtrlsClassical An array of the classical positive control values.
+ * @param negCtrlsClassical An array of the classical negative control values.
+ * @throws domain_error If a classical control value cannot be found.
+ * @return One or two hybrid states corresponding to the measurement or reset
+ * outcomes.
+ */
+ std::vector
+ propagateMeasurementOrReset(const unsigned int quantumTarget,
+ const bool reset,
+ const Value classicalTarget = nullptr,
+ const std::span posCtrlsClassical = {},
+ const std::span negCtrlsClassical = {}) {
+ if (top || !isOperationExecutable(posCtrlsClassical, negCtrlsClassical)) {
+ return {*this};
+ }
+
+ std::vector results;
+
+ const auto [newQuantumStates, availableStates] =
+ reset ? qState->resetQubit(quantumTarget)
+ : qState->measureQubit(quantumTarget);
+
+ for (const int64_t i : {0, 1}) {
+ if (!availableStates.contains(i) || !availableStates.at(i)) {
+ continue;
+ }
+
+ const auto& [measProbability, measQS] = newQuantumStates.at(i);
+ auto newHybrid = HybridState();
+ newHybrid.probability = measProbability * probability;
+ newHybrid.integerValues = integerValues;
+ newHybrid.doubleValues = doubleValues;
+ if (!reset) {
+ newHybrid.integerValues[classicalTarget] = i;
+ }
+ newHybrid.qState = measQS;
+
+ results.push_back(newHybrid);
+ }
+
+ return results;
+ }
+
+public:
+ explicit HybridState(std::span globalQubitNumber,
+ std::size_t maxNonzeroAmplitudes,
+ double probability = 1.0);
+
+ ~HybridState();
+
+ void print(std::ostream& os) const;
+
+ [[nodiscard("HybridState::toString called but ignored")]]
+ std::string toString() const;
+
+ [[nodiscard("HybridState::isHybridStateTop called but ignored")]] bool
+ isHybridStateTop() const {
+ return top;
+ }
+
+ /**
+ * @brief This method adds a classical integer value to the hybrid state.
+ *
+ * @param value The value object of the new value.
+ * @param number The number of the new value.
+ */
+ void addIntegerValue(Value value, int64_t number);
+
+ /**
+ * @brief This method adds a classical double value to the hybrid state.
+ *
+ * @param value The value object of the new value.
+ * @param number The number of the new value.
+ */
+ void addDoubleValue(Value value, double number);
+
+ /**
+ * @brief This method changes the global index of a qubit in the quantum
+ * state.
+ *
+ * @param target The old global index of a qubit.
+ * @param newIndex The new global index for the qubit.
+ */
+ void changeGlobalIndex(unsigned int target, unsigned int newIndex) const;
+
+ /**
+ * @brief This method applies a gate to the state.
+ *
+ * This method changes the hybrid state according to a gate.
+ *
+ * @param gate The name of the gate to be applied.
+ * @param targets An array of the indices of the target qubits.
+ * @param ctrlsQuantum An array of the global indices of the ctrl qubits.
+ * @param posCtrlsClassical An array of the classical positive control values.
+ * @param negCtrlsClassical An array of the classical negative control values.
+ * @param params The values of parameters applied to the gate.
+ * @throws domain_error If a classical control value cannot be found.
+ */
+ void propagateGate(Operation* gate, std::span targets,
+ std::span ctrlsQuantum = {},
+ std::span posCtrlsClassical = {},
+ std::span negCtrlsClassical = {},
+ std::span params = {});
+
+ /**
+ * @brief This method applies a measurement.
+ *
+ * This method applies a measurement, changing the qubits and the classical
+ * values corresponding to the measurement.
+ *
+ * @param quantumTarget The index of the qubit to be measured.
+ * @param classicalTarget The value to save the measurement result to.
+ * @param posCtrlsClassical An array of the classical positive control values.
+ * @param negCtrlsClassical An array of the classical negative control values.
+ * @throws domain_error If a classical control value cannot be found.
+ * @return One or two hybrid states corresponding to the measurement
+ * outcomes.
+ */
+ std::vector
+ propagateMeasurement(unsigned int quantumTarget, Value classicalTarget,
+ std::span posCtrlsClassical = {},
+ std::span negCtrlsClassical = {});
+
+ /**
+ * @brief This method applies a reset.
+ *
+ * This method applies a reset, changing the qubits and creates one or two new
+ * states. The procedure is done as if the qubit was measured, put to zero if
+ * the measurement was one, and the result discarded.
+ *
+ * @param target The index of the qubit to be measured.
+ * @param posCtrlsClassical An array of the classical positive control values.
+ * @param negCtrlsClassical An array of the classical negative control values.
+ * @throws domain_error If a classical control value cannot be found.
+ * @return One or two hybrid states corresponding to the measurement outcomes
+ * during the reset, but with the qubit always in the zero state.
+ */
+ std::vector
+ propagateReset(unsigned int target, std::span posCtrlsClassical = {},
+ std::span negCtrlsClassical = {});
+
+ /**
+ * @brief This method applies a classical operation.
+ *
+ * This method changes the hybrid state according to a classical operation.
+ * The operation might be controlled by classical values.
+ *
+ * @param op The operation to be applied.
+ * @param dest The value the result of the operation is written to.
+ * @param operand1 The first value used by the operation.
+ * @param operand2 The second value used by the operation, might be null.
+ * @param operand3 The third value used by the operation, might be null.
+ * @param posCtrlsClassical An array of the classical positive control values.
+ * @param negCtrlsClassical An array of the classical negative control values.
+ * @throws domain_error If a classical value cannot be found.
+ * @throws runtime_error If classical operation is not supported.
+ */
+ void propagateClassicalOperation(Operation* op, Value dest, Value operand1,
+ Value operand2 = nullptr,
+ Value operand3 = nullptr,
+ std::span posCtrlsClassical = {},
+ std::span negCtrlsClassical = {});
+
+ /**
+ * @brief This method unifies two HybridStates.
+ *
+ * This method unifies the current HybridState with the given one and returns
+ * a new HybridState, if the new state has no more than maxNonzeroAmplitudes.
+ * Otherwise, throws a domain_error.
+ *
+ * @param that The HybridState to unify this with.
+ * @throw std::domain_error If the unified QuantumState would exceed
+ * maxNonzeroAmplitudes of this.
+ */
+ HybridState unify(const HybridState& that);
+
+ bool operator==(const HybridState& that) const;
+
+ [[nodiscard("HybridState::isQubitAlwaysOne called but ignored")]] bool
+ isQubitAlwaysOne(unsigned int q) const;
+
+ [[nodiscard("HybridState::isQubitAlwaysZero called but ignored")]] bool
+ isQubitAlwaysZero(unsigned int q) const;
+
+ [[nodiscard("HybridState::isValueTrue called but ignored")]] bool
+ isValueTrue(Value v) const;
+
+ /**
+ * @brief Checks if a given combination of values-qubit values has a nonzero
+ * probability.
+ *
+ * This method receives a number of qubit and values and checks whether
+ * they have for a given value always a zero amplitude. If the hybridState is
+ * top, it is not guaranteed that the amplitude is always zero and false is
+ * returned.
+ * The values for the classical values are not the numeric ones, but whether
+ * they are zero (false) or non-zero (true).
+ *
+ * @param qubitValues Pairs of the qubits that are being checked and the
+ * values that they are being checked for.
+ * @param classicalValues The classical values to check.
+ * @throws domain_error If a classical value cannot be found.
+ * @returns True if the amplitude is always zero, false otherwise.
+ */
+ [[nodiscard("HybridState::hasAlwaysZeroAmplitude called but ignored")]] bool
+ hasAlwaysZeroProbability(
+ const std::unordered_map& qubitValues,
+ const llvm::DenseMap& classicalValues) const;
+
+ /**
+ * @brief Returns a classical value that is equivalent to qubit.
+ *
+ * Returns a classical value that is always true (=/= 0) when the given qubit
+ * is 1, and the boolean value true. Alternatively, it can return a value that
+ * is always false (== 0) if the qubit is 1. In that case, the returned bool
+ * is false.
+ *
+ * @param qubit Index of qubit.
+ * @returns A map of classical values that are equivalent or inverse to qubit.
+ * Th emaps value is true, if the qubit is equivalent to the value. False, if
+ * the qubit is the inverse of the value.
+ */
+ [[nodiscard("HybridState::getValueThatIsEquivalentToQubit called but "
+ "ignored")]] llvm::DenseMap
+ getValueThatIsEquivalentToQubit(unsigned int qubit) const;
+};
+} // namespace mlir::qco
+
+#endif // MQT_CORE_HYBRIDSTATE_H
diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.hpp b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.hpp
new file mode 100644
index 0000000000..db59ec1251
--- /dev/null
+++ b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.hpp
@@ -0,0 +1,349 @@
+/*
+ * 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
+ */
+
+#ifndef MQT_CORE_QUANTUMSTATE_H
+#define MQT_CORE_QUANTUMSTATE_H
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace mlir::qco {
+
+/**
+ * @brief Result of a measurement or reset.
+ *
+ * The result contains 0, 1 or 2 QuantumStates, together with their respective
+ * results.
+ */
+struct MeasurementResult {
+ // The pair is (probability, resulting state).
+ std::unordered_map>>
+ states;
+
+ // Which entries are available
+ std::unordered_map availableStates;
+};
+
+/**
+ * @brief This class represents a quantum state.
+ *
+ * This class holds n qubits in different basis states with their corresponding
+ * complex amplitude. It holds the information about which global qubit number
+ * corresponds to which local qubit number.
+ */
+class QuantumState {
+ std::size_t nQubits;
+ std::size_t maxNonzeroAmplitudes;
+ std::unordered_map globalToLocalQubitNumber;
+ std::unordered_map> amplitudeMap;
+
+ std::string qubitStringToBinary(const unsigned int q) const {
+ std::string result;
+ result.reserve(nQubits);
+
+ for (std::size_t i = nQubits; i > 0; --i) {
+ result.push_back((q >> (i - 1) & 1U) != 0 ? '1' : '0');
+ }
+ return result;
+ }
+
+ /** @brief Puts a zero or one at bit position n in a bitstring.
+ *
+ * @param bitstring Bitstring that gets altered
+ * @param position Position of bit that needs to be changed
+ * @param one True if bit should be set to one
+ * @return The altered bitstring
+ */
+ static unsigned int setBitN(const unsigned int bitstring,
+ const unsigned int position, const bool one) {
+ const unsigned int mask = 1U << position;
+ if (one) {
+ return bitstring | mask;
+ }
+ return bitstring & ~mask;
+ }
+
+ /**
+ * @brief This method receives a gate mapping and a bitmask ctrls and the
+ * positions of the one (or two) qubit targets.
+ *
+ * This method receives a two qubit gate mapping and a bitmask for ctrl qubits
+ * and a span with the first target qubit position at first position and the
+ * potential second target qubit at second position. The gate is applied to
+ * the valid qubit states. It returns the map which would be the qubit state
+ * after gate application.
+ *
+ * @param gateMapping The mapping representing the gate
+ * @param positionOfTargetQubits The position of the qubit targets.
+ * @param bitmaskForCtrls The bitmask of the positively controlling qubits.
+ * @return The qubit state after the gate has been applied.
+ */
+ std::unordered_map>
+ getNewMappingFromQubitGate(
+ std::unordered_map>>
+ gateMapping,
+ const std::span positionOfTargetQubits,
+ const unsigned int bitmaskForCtrls) {
+ std::unordered_map> newValues;
+ const auto numberOfTargetValues = 1U << positionOfTargetQubits.size();
+
+ for (const auto& [key, value] : amplitudeMap) {
+ if ((bitmaskForCtrls & key) != bitmaskForCtrls) {
+ newValues[key] += value;
+ continue;
+ }
+
+ unsigned int mapFrom = 0;
+
+ std::vector keysForNewValue(numberOfTargetValues);
+
+ // Find the target keys from the current keys
+ for (unsigned int i = 0; i < numberOfTargetValues; ++i) {
+ if (i % 2 == 1) {
+ keysForNewValue[i] = setBitN(key, positionOfTargetQubits[0], true);
+ } else {
+ keysForNewValue[i] = setBitN(key, positionOfTargetQubits[0], false);
+ }
+ if (numberOfTargetValues > 2) {
+ if (i > 1) {
+ keysForNewValue[i] =
+ setBitN(keysForNewValue[i], positionOfTargetQubits[1], true);
+ } else {
+ keysForNewValue[i] =
+ setBitN(keysForNewValue[i], positionOfTargetQubits[1], false);
+ }
+ }
+ if (keysForNewValue[i] == key) {
+ mapFrom = i;
+ }
+ }
+
+ auto mapForThisQubit = gateMapping[mapFrom];
+ for (unsigned int i = 0; i < numberOfTargetValues; i++) {
+ if (auto valueToI = mapForThisQubit[i]; abs(valueToI) > 1e-4) {
+ newValues[keysForNewValue.at(i)] += valueToI * value;
+ }
+ }
+ }
+
+ return newValues;
+ }
+
+ /**
+ * @brief This method applies a measurement or reset to the qubits.
+ *
+ * This method applies a measurement or reset to the qubits. It returns the
+ * QuantumState in case the measurement was 0 and in case it was 1, alongside
+ * the respective probabilities. If a reset was applied, the qubit that was
+ * measured is set to 0.
+ *
+ * @param target The global index of the qubit to be measured.
+ * @param reset True if target should be reset in addition to measured.
+ * @return MeasurementResult, containing the probability for the result and
+ * the QuantumStates after measurement.
+ */
+ MeasurementResult measureOrResetQubit(const unsigned int target,
+ const bool reset) {
+ const auto qubitMask = 1U << globalToLocalQubitNumber.at(target);
+
+ double probabilityZero = 0.0;
+ double probabilityOne = 0.0;
+ std::unordered_map> newValuesZeroRes;
+ std::unordered_map> newValuesOneRes;
+
+ for (const auto& [key, value] : amplitudeMap) {
+ unsigned int newKey = key;
+ const bool isZero = (qubitMask & key) == 0;
+ const double probability = norm(value);
+ if (isZero) {
+ probabilityZero += probability;
+ newValuesZeroRes[newKey] = value;
+ } else {
+ if (reset) {
+ newKey = key ^ qubitMask;
+ }
+ probabilityOne += probability;
+ newValuesOneRes[newKey] = value;
+ }
+ }
+
+ if (std::abs(1.0 - probabilityZero - probabilityOne) > 1e-4) {
+ throw std::domain_error(
+ "Probabilities of 0 and 1 do not add up to one after measurement.");
+ }
+ auto globalKeysView = std::views::keys(globalToLocalQubitNumber);
+ std::vector globalKeys{globalKeysView.begin(),
+ globalKeysView.end()};
+ MeasurementResult res = {};
+ if (probabilityZero > 1e-4) {
+ auto stateZero =
+ std::make_shared(globalKeys, maxNonzeroAmplitudes);
+ stateZero->amplitudeMap = std::move(newValuesZeroRes);
+ stateZero->normalize();
+ res.states[0] = {probabilityZero, stateZero};
+ res.availableStates[0] = true;
+ }
+ if (probabilityOne > 1e-4) {
+ auto stateOne =
+ std::make_shared(globalKeys, maxNonzeroAmplitudes);
+ stateOne->amplitudeMap = std::move(newValuesOneRes);
+ stateOne->normalize();
+ res.states[1] = {probabilityOne, stateOne};
+ res.availableStates[1] = true;
+ }
+
+ return res;
+ }
+
+public:
+ QuantumState(std::span globalQubitNumber,
+ std::size_t maxNonzeroAmplitudes);
+
+ ~QuantumState() = default;
+
+ void print(std::ostream& os) const;
+
+ [[nodiscard("QuantumState::toString called but ignored")]] std::string
+ toString() const;
+
+ [[nodiscard("QuantumState::== called but ignored")]] bool
+ operator==(const QuantumState& that) const;
+
+ /**
+ * @brief This method normalizes the amplitudes of a state.
+ */
+ void normalize();
+
+ /**
+ * @brief This method unifies two QuantumState.
+ *
+ * This method unifies the current QuantumState with the given one and returns
+ * a new QuantumState, if the new state has no more than maxNonzeroAmplitude
+ * nonzero amplitudes. Otherwise, throws a domain_error.
+ *
+ * @param that The QuantumState to unify this with.
+ * @throw std::domain_error If the number of nonzero amplitudes would exceed
+ * maxNonzeroAmplitudes of this.
+ */
+ [[nodiscard("QuantumState::unify called but ignored")]] QuantumState
+ unify(const QuantumState& that);
+
+ /**
+ * @brief This method changes the global index of a qubit.
+ *
+ * @param target The old global index of a qubit.
+ * @param newIndex The new global index for the qubit.
+ */
+ void changeGlobalIndex(unsigned int target, unsigned int newIndex);
+
+ /**
+ * @brief This method applies a gate to the qubits.
+ *
+ * This method changes the amplitudes of a QuantumState according to the
+ * applied gate. Returns the current QuantumState if it has no more than
+ * maxNonZeroAmplitude nonzero amplitudes. Otherwise, throws a domain_error.
+ *
+ * @param gate The gate to be applied.
+ * @param targets A span of the global indices of the target qubits.
+ * @param ctrls A span of the global indices of the ctrl qubits.
+ * @param params The parameter applied to the gate.
+ * @throw std::domain_error If the number of nonzero amplitudes would exceed
+ * maxNonzeroAmplitudes.
+ */
+ void propagateGate(Operation* gate, std::span targets,
+ std::span ctrls = {},
+ std::span params = {});
+
+ /**
+ * @brief This method applies a measurement to the qubits.
+ *
+ * This method applies a measurement to the qubits. It returns the
+ * QuantumState in case the measurement was 0 and in case it was 1, alongside
+ * the respective probabilities.
+ *
+ * @param target The global index of the qubit to be measured.
+ * @return MeasurementResult, containing the probability for the result and
+ * the QuantumStates after measurement.
+ */
+ [[nodiscard(
+ "QuantumState::measureQubit called but ignored")]] MeasurementResult
+ measureQubit(unsigned int target);
+
+ /**
+ * @brief This method resets a qubit.
+ *
+ * This method resets a qubit. This is done by assuming that a measurement is
+ * applied and measurements in the one-state are set to the zero state. In
+ * order to not get a mixed state, the method returns one to two
+ * QuantumStates, one in case the measurement was 0 and one in case it was 1,
+ * alongside the respective probabilities. In both cases, the target qubit
+ * will be zero (as a reset is performed).
+ *
+ * @param target The global index of the qubit to be measured.
+ * @return MeasurementResult, containing the probability for the result and
+ * the QuantumStates after measurement.
+ */
+ [[nodiscard("QuantumState::resetQubit called but ignored")]] MeasurementResult
+ resetQubit(unsigned int target);
+
+ /**
+ * @brief This method checks if only amplitudes with a given qubit = 1 are
+ * nonzero.
+ *
+ * @param q The global index of the qubit.
+ * @return A set of one to two QuantumStates and their corresponding
+ * probabilities.
+ */
+ [[nodiscard("QuantumState::isQubitAlwaysOne called but ignored")]] bool
+ isQubitAlwaysOne(unsigned int q) const;
+
+ /**
+ * @brief This method checks if only amplitudes with a given qubit = 0 are
+ * nonzero.
+ *
+ * @param q The global index of the qubit.
+ * @return A set of one to two QuantumStates and their corresponding
+ * probabilities.
+ */
+ [[nodiscard("QuantumState::isQubitAlwaysZero called but ignored")]] bool
+ isQubitAlwaysZero(unsigned int q) const;
+
+ /**
+ * @brief Returns whether the given qubits have for a given value always a
+ * zero amplitude.
+ *
+ * This method receives a number of global qubit indices and checks whether
+ * they have for a given value always a zero amplitude.
+ *
+ * @param qubitValues Pairs of the qubits that are being checked and the
+ * values that they are being checked for.
+ * @returns True if the amplitude is always zero, false otherwise.
+ */
+ [[nodiscard("QuantumState::hasAlwaysZeroAmplitude called but ignored")]] bool
+ hasAlwaysZeroAmplitude(
+ const std::unordered_map& qubitValues) const;
+};
+
+} // namespace mlir::qco
+
+#endif // MQT_CORE_QUANTUMSTATE_H
diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.hpp b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.hpp
new file mode 100644
index 0000000000..65066dafdd
--- /dev/null
+++ b/mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.hpp
@@ -0,0 +1,547 @@
+/*
+ * 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
+ */
+
+#ifndef MQT_CORE_UNIONTABLE_H
+#define MQT_CORE_UNIONTABLE_H
+#include "HybridState.hpp"
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace mlir::qco {
+
+/**
+ * @brief Result of the check for superfluous values.
+ */
+struct SuperfluousResult {
+ bool completelySuperfluous = false;
+ llvm::DenseSet superfluousQubits;
+ llvm::DenseSet superfluousClassicalValues;
+};
+
+/**
+ * @brief Managing entries of the union table
+ */
+struct UnionTableEntry {
+ const unsigned int index;
+ bool top = false;
+ std::vector states;
+ // Values and global indices of the participating qubits
+ llvm::DenseSet participatingQubits = {};
+ llvm::DenseSet participatingClassicalValues = {};
+
+ bool operator<(const UnionTableEntry& ute) const noexcept {
+ return index < ute.index;
+ }
+
+ bool operator==(const UnionTableEntry& ute) const noexcept {
+ return index == ute.index;
+ }
+
+ UnionTableEntry() : index(nextId()) {}
+
+private:
+ static std::uint64_t nextId() {
+ static unsigned int counter = 0;
+ return ++counter;
+ }
+};
+
+/**
+ * @brief This class represents a union table.
+ *
+ * This class holds multiple hybrid states and can propagate operations on the
+ * values in the states.
+ */
+class UnionTable {
+ bool allTop = false;
+ std::size_t maxNonzeroAmplitudes;
+ std::size_t maximumHybridEntries;
+ llvm::DenseMap> valuesToEntries =
+ llvm::DenseMap>();
+ std::set> entries;
+ llvm::DenseMap qubitsToGlobalIndices;
+
+ /** @brief: Collects a set of all participating entries.
+ *
+ * @param targets An array of the Values of the target qubits.
+ * @param ctrlsQuantum An array of the values of the ctrl qubits.
+ * @param posCtrlsClassical An array of the values of the ctrl bits.
+ * @param negCtrlsClassical An array of the values of the negative ctrl bits.
+ * @param params The parameter applied to the gate.
+ */
+ std::set
+ collectParticipatingEntries(const std::span targets,
+ const std::span ctrlsQuantum,
+ const std::span posCtrlsClassical,
+ const std::span negCtrlsClassical,
+ const std::span params = {}) {
+ std::set participatingEntries;
+ for (auto const q : targets) {
+ participatingEntries.insert(*valuesToEntries.at(q));
+ }
+ for (auto const q : ctrlsQuantum) {
+ participatingEntries.insert(*valuesToEntries.at(q));
+ }
+ for (auto const i : posCtrlsClassical) {
+ participatingEntries.insert(*valuesToEntries.at(i));
+ }
+ for (auto const i : negCtrlsClassical) {
+ participatingEntries.insert(*valuesToEntries.at(i));
+ }
+ for (auto const i : params) {
+ participatingEntries.insert(*valuesToEntries.at(i));
+ }
+ return participatingEntries;
+ }
+
+ /** @brief Puts the given UnionTableEntries to top
+ *
+ * @param entriesToTop The UnionTableEntries to become top.
+ */
+ void putEntriesToTop(const std::set& entriesToTop) {
+ auto topUnionTableEntry = UnionTableEntry();
+ topUnionTableEntry.top = true;
+ for (const auto& e : entriesToTop) {
+ topUnionTableEntry.participatingQubits.insert(
+ e.participatingQubits.begin(), e.participatingQubits.end());
+ topUnionTableEntry.participatingClassicalValues.insert(
+ e.participatingClassicalValues.begin(),
+ e.participatingClassicalValues.end());
+ auto it = std::ranges::find_if(
+ entries, [&](auto const& entry) { return *entry == e; });
+ if (it != entries.end()) {
+ entries.erase(it);
+ }
+ }
+ const auto ptrUte = std::make_shared(topUnionTableEntry);
+ entries.insert(ptrUte);
+ for (auto q : ptrUte->participatingQubits) {
+ valuesToEntries.erase(q);
+ valuesToEntries[q] = ptrUte;
+ }
+ for (auto c : ptrUte->participatingClassicalValues) {
+ valuesToEntries.erase(c);
+ valuesToEntries[c] = ptrUte;
+ }
+ }
+
+ /**
+ * @brief This method unifies the given UnionTableEntries.
+ *
+ * This method unifies the given UnionTableEntries. If the new states have
+ * more than maxNonzeroAmplitudes, it throws a domain_error. The same holds
+ * if the resulting hybridStates are more than maximumHybridEntries.
+ *
+ * @param entriesToUnify The UnionTableEntries to be unified.
+ * @throws domain_error If more than maNonzeroAmplitudes are created in a
+ * quantumstate or more than maximumHybridEntries are created.
+ */
+ void unifyEntries(const std::set& entriesToUnify) {
+ if (entriesToUnify.size() == 1) {
+ return;
+ }
+ bool entriesBecomeTop = false;
+ for (const auto& e : entriesToUnify) {
+ entriesBecomeTop |= e.top;
+ }
+
+ // Check if the number of entries would be too large
+ unsigned int numberOfNewEntries = 1;
+ for (const auto& e : entriesToUnify) {
+ numberOfNewEntries *= e.states.size();
+ }
+ if (numberOfNewEntries > maximumHybridEntries) {
+ throw std::domain_error("Maximum of allowed hybrid entries exceeded.");
+ }
+
+ // Create new entry
+ auto newEntry = UnionTableEntry();
+ newEntry.top = entriesBecomeTop;
+ for (const auto& e : entriesToUnify) {
+ auto classicalValues = e.participatingClassicalValues;
+ auto qubits = e.participatingQubits;
+ newEntry.participatingClassicalValues.insert(classicalValues.begin(),
+ classicalValues.end());
+ newEntry.participatingQubits.insert(qubits.begin(), qubits.end());
+ if (!newEntry.top & newEntry.states.empty()) {
+ newEntry.states = e.states;
+ continue;
+ }
+ if (newEntry.top || e.states.empty()) {
+ continue;
+ }
+ std::vector unifiedHS = {};
+ for (auto hs1 : newEntry.states) {
+ for (auto hs2 : e.states) {
+ unifiedHS.push_back(hs1.unify(hs2));
+ }
+ }
+ newEntry.states = unifiedHS;
+ }
+
+ // Adapt global data structures to new entry
+ auto valuesToReplace = llvm::DenseSet();
+ for (const auto& [v, _] : valuesToEntries) {
+ if (newEntry.participatingQubits.contains(v) ||
+ newEntry.participatingClassicalValues.contains(v)) {
+ valuesToReplace.insert(v);
+ }
+ }
+ const auto ptrUTE = std::make_shared(newEntry);
+ for (const auto& v : valuesToReplace) {
+ valuesToEntries[v] = ptrUTE;
+ }
+ for (const auto& e : entriesToUnify) {
+ auto it = std::ranges::find_if(
+ entries, [&](auto const& entry) { return *entry == e; });
+ if (it != entries.end()) {
+ entries.erase(it);
+ }
+ }
+ entries.insert(ptrUTE);
+ }
+
+ /**
+ * @brief This method applies a swap gate by switching two qubits if the
+ * qubits are in different entries.
+ *
+ * @param targets The values that partake in the swap.
+ * @param newQuantumTargets The values after the swap.
+ */
+ void applySwapGate(const std::span targets,
+ const std::span newQuantumTargets) {
+ for (const auto& hs : valuesToEntries.at(targets[0])->states) {
+ hs.changeGlobalIndex(qubitsToGlobalIndices.at(targets[0]),
+ qubitsToGlobalIndices.at(targets[1]));
+ }
+ for (const auto& hs : valuesToEntries.at(targets[1])->states) {
+ hs.changeGlobalIndex(qubitsToGlobalIndices.at(targets[1]),
+ qubitsToGlobalIndices.at(targets[0]));
+ }
+ const auto targetOneIndex = qubitsToGlobalIndices.at(targets[0]);
+ qubitsToGlobalIndices[targets[0]] = qubitsToGlobalIndices.at(targets[1]);
+ qubitsToGlobalIndices[targets[1]] = targetOneIndex;
+ std::ranges::reverse(newQuantumTargets);
+ replaceValuesGlobally(targets, newQuantumTargets);
+ }
+
+ /**
+ * @brief This method returns classical values which are always either true or
+ * false.
+ *
+ * @return A map of values as keys. The values of th emap say whether the
+ * classical values are always true or always false.
+ */
+ llvm::DenseMap
+ getClassicalValuesThatAreAlwaysTrueOrFalse() const {
+ llvm::DenseMap result;
+ for (auto const& e : entries) {
+ for (auto const& v : e->participatingClassicalValues) {
+ for (auto const& hs : e->states) {
+ const auto isTrue = hs.isValueTrue(v);
+ if (result.contains(v) && result.at(v) != isTrue) {
+ result.erase(v);
+ break;
+ }
+ result[v] = isTrue;
+ }
+ }
+ }
+ return result;
+ }
+
+public:
+ explicit UnionTable(std::size_t maxNonzeroAmplitudes,
+ std::size_t maximumHybridEntries);
+
+ ~UnionTable();
+
+ void print(std::ostream& os) const;
+
+ [[nodiscard("UnionTable::toString called but ignored")]]
+ std::string toString() const;
+
+ /** @brief: Replaces values globally by new values
+ *
+ * @param replacedValues Values to be replaced
+ * @param newValues Values the first values are replaced with.
+ * @throws runtime_error if the size of the two parameters is not equal.
+ */
+ void replaceValuesGlobally(std::span replacedValues,
+ std::span newValues);
+
+ [[nodiscard("UnionTable::allTop called but ignored")]]
+ bool areStatesAllTop();
+
+ /**
+ * @brief This method applies a gate to the qubits.
+ *
+ * This method changes the amplitudes of a QuantumState according to the
+ * applied gate.
+ *
+ * @param gate The gate to be applied.
+ * @param targets An array of the Values of the target qubits.
+ * @param newQuantumTargets The value of the qubits after the gate.
+ * @param ctrlsQuantum An array of the values of the ctrl qubits.
+ * @param newCtrlsQuantum An values of the ctrl qubits after the gate.
+ * @param posCtrlsClassical An array of the values of the ctrl bits.
+ * @param negCtrlsClassical An array of the values of the negative ctrl bits.
+ * @param params The parameter applied to the gate.
+ * @throws invalid_argument if a value is given, but is not found in the
+ * existing ones.
+ */
+ void propagateGate(Operation* gate, std::span targets,
+ std::span newQuantumTargets,
+ std::span ctrlsQuantum = {},
+ std::span newCtrlsQuantum = {},
+ std::span posCtrlsClassical = {},
+ std::span negCtrlsClassical = {},
+ std::span params = {});
+
+ /**
+ * @brief This method propagates a classical operation.
+ *
+ *
+ * @param op The operation to be applied.
+ * @param targets An array of the Values of the target classical values.
+ * @param results The value of the result.
+ * @param posCtrlsClassical An array of the values of the ctrl bits.
+ * @param negCtrlsClassical An array of the values of the negative ctrl bits.
+ * @throws invalid_argument if a value is given, but is not found in the
+ * existing ones.
+ */
+ void propagateClassicalOperation(Operation* op, std::span targets,
+ std::span results,
+ std::span posCtrlsClassical = {},
+ std::span negCtrlsClassical = {});
+
+ /**
+ * @brief This method applies a measurement.
+ *
+ * This method applies a measurement, changing the qubits and the classical
+ * bit corresponding to the measurement.
+ *
+ * @param quantumTarget The value of the qubit to be measured.
+ * @param newQuantumValue The value of the qubit after the measurement.
+ * @param classicalTarget The value of the bit to save the measurement result
+ * in.
+ * @param posCtrlsClassical An array of the values of the ctrl bits.
+ * @param negCtrlsClassical An array of the values of the negative ctrl bits.
+ * @throws invalid_argument if a value is given, but is not found in the
+ * existing ones. This does not hold for the classical target, which can be
+ * newly created.
+ */
+ void propagateMeasurement(Value quantumTarget, Value newQuantumValue,
+ Value classicalTarget,
+ std::span posCtrlsClassical = {},
+ std::span negCtrlsClassical = {});
+
+ /**
+ * @brief This method propagates a qubit reset.
+ *
+ * This method propagates a qubit reset. This means that the qubit is put into
+ * zero state. It is also put in its own QubitState again if it does not
+ * correspond to already assigned bit values.
+ *
+ * @param quantumTarget The value of the qubit to be reset.
+ * @param newQuantumValue The value of the qubit after the reset.
+ * @param posCtrlsClassical An array of the values of the ctrl bits.
+ * @param negCtrlsClassical An array of the values of the negative ctrl bits.
+ * @throws invalid_argument if a value is given, but is not found in the
+ * existing ones.
+ */
+ void propagateReset(Value quantumTarget, Value newQuantumValue,
+ std::span posCtrlsClassical = {},
+ std::span negCtrlsClassical = {});
+
+ /**
+ * @brief This method propagates a qubit alloc.
+ *
+ * This method propagates a qubit alloc. This means that the qubit is added to
+ * the UnionTable in zero state.
+ *
+ * @param qubit The value of the qubit to be allocated.
+ */
+ void propagateQubitAlloc(Value qubit);
+
+ /**
+ * @brief This method propagates an int alloc.
+ *
+ * This method propagates an int alloc. This means that the int is added to
+ * the UnionTable as new HybridState.
+ *
+ * @param intValue The value of the int to be allocated.
+ * @param number The number that the int is initialized with.
+ */
+ void propagateIntAlloc(Value intValue, int64_t number);
+
+ /**
+ * @brief This method propagates a double alloc.
+ *
+ * This method propagates a double alloc. This means that the double is added
+ * to the UnionTable as new HybridState.
+ *
+ * @param doubleValue The value of the double to be allocated.
+ * @param number The number that the double is initialized with.
+ */
+ void propagateDoubleAlloc(Value doubleValue, double number);
+
+ [[nodiscard("UnionTable::isQubitAlwaysOne called but ignored")]] bool
+ isQubitAlwaysOne(Value q) const;
+
+ [[nodiscard("UnionTable::isQubitAlwaysZero called but ignored")]] bool
+ isQubitAlwaysZero(Value q) const;
+
+ [[nodiscard(
+ "UnionTable::isClassicalValueAlwaysTrue called but ignored")]] bool
+ isClassicalValueAlwaysTrue(Value c) const;
+
+ [[nodiscard(
+ "UnionTable::isClassicalValueAlwaysFalse called but ignored")]] bool
+ isClassicalValueAlwaysFalse(Value c) const;
+
+ /**
+ * @brief Checks if a given combination of values-qubit values has a nonzero
+ * probability.
+ *
+ * This method receives a number of qubit and values and checks whether
+ * they have for a given value always a zero amplitude.
+ * The values for the classical values are not the numeric ones, but whether
+ * they are zero (false) or non-zero (true).
+ *
+ * @param qubitValues Pairs of the qubits that are being checked and the
+ * values that they are being checked for.
+ * @param classicalValues The classical values to check.
+ * @throws invalid_argument if a value is given, but is not found in the
+ * existing ones.
+ * @returns True if the amplitude is always zero, false otherwise.
+ */
+ [[nodiscard("HybridState::hasAlwaysZeroAmplitude called but ignored")]] bool
+ hasAlwaysZeroProbability(
+ const llvm::DenseMap& qubitValues,
+ const llvm::DenseMap& classicalValues) const;
+
+ /**
+ * @brief Returns a classical value that is equivalent to qubit.
+ *
+ * Returns a classical value that is always true (=/= 0) when the given qubit
+ * is 1, and the boolean value true. Alternatively, it can return a value that
+ * is always false (== 0) if the qubit is 1. In that case, the returned bool
+ * is false.
+ *
+ * @param qubit Index of qubit.
+ * @returns A map of classical values that are equivalent or inverse to qubit.
+ * The value of the map is true, if the qubit is equivalent to the value.
+ * False, if the qubit is the inverse of the value.
+ */
+ [[nodiscard(
+ "UnionTable::getValueThatIsEquivalentToQubit called but ignored")]] llvm::
+ DenseMap
+ getValueThatIsEquivalentToQubit(Value qubit) const;
+
+ /**
+ * @brief This method checks whether a diagonal gate only adds a global phase
+ * and returns it.
+ *
+ * This method receives a diagonal gate and checks, if only a global phase is
+ * added to the circuit by it under the current configuration. If that is the
+ * case, the returned optional contains the global phase. Only works with
+ * 1-qubit gates without parameters.
+ *
+ * @param op The gate to be checked.
+ * @param target The Values of the target qubits.
+ * @param ctrlsQuantum An array of the values of the ctrl qubits.
+ * @param posCtrlsClassical An array of the values of the ctrl bits.
+ * @param negCtrlsClassical An array of the values of the negative ctrl bits.
+ * @throws invalid_argument if a value is given, but is not found in the
+ * existing ones.
+ * @returns An optional containing the globally added value, if applicable.
+ */
+ [[nodiscard("UnionTable::globalPhaseThatIsAdded called but ignored")]]
+ std::optional
+ globalPhaseThatIsAdded(Operation* op, Value target,
+ std::span ctrlsQuantum = {},
+ std::span posCtrlsClassical = {},
+ std::span negCtrlsClassical = {});
+
+ /**
+ * @brief This method checks which qubits and classical values are superfluous
+ * given a controlled gate.
+ *
+ * This method checks which qubits and classical values are superfluous given
+ * a controlled gate. If the gate can never be executed, the SuperfluousResult
+ * will indicate that the gate is completely superfluous. Apart from that, all
+ * posCtrl (negCtrl) qubits/values that are always true (false) are
+ * superfluous.
+ *
+ * @param qubitCtrls The values of the positively controlling qubits.
+ * @param posCtrlsClassical The values of the positively controlling classical
+ * values.
+ * @param negCtrlsClassical The values of the negatively controlling classical
+ * values.
+ * @returns The superfluous result, i.e. the qubits and classical values that
+ * are superfluous and whether the whole operation is superfluous.
+ */
+ SuperfluousResult
+ getSuperfluousControls(std::span qubitCtrls,
+ std::span posCtrlsClassical = {},
+ std::span negCtrlsClassical = {});
+
+ /**
+ * @brief This method checks whether there are satisfiable combinations of
+ * controls.
+ *
+ * @param qubitCtrls The values of the controlling qubit values.
+ * @param posCtrlsClassical The values of the positively controlling classical
+ * values.
+ * @param negCtrlsClassical The values of the negatively controlling classical
+ * values.
+ * @returns Whether there are satisfiable combinations or not.
+ */
+ bool areThereSatisfiableCombinations(
+ std::span qubitCtrls, std::span posCtrlsClassical = {},
+ std::span negCtrlsClassical = {}) const;
+
+ /**
+ * @brief Returns whether the given qubits and classical values that imply the
+ * given qubit.
+ *
+ * This method checks whether in the given list are qubits or classical values
+ * that imply (are antecedents of) the given qubit. I.e. if there are
+ * qubits/values a for which holds: a -> q.
+ *
+ * @param q The qubit for which is checked whether it is implied.
+ * @param qubits The qubits for which are checked if they imply q.
+ * @param classicalPositive The values for which are checked if they imply q.
+ * @param classicalNegative The values for which their negations are checked
+ * if they imply q.
+ * @returns A pair of 1. qubits and 2. classical values that are antecedents
+ * of q.
+ */
+ bool isQubitImplied(Value q, std::span qubits,
+ std::span classicalPositive,
+ std::span classicalNegative) const;
+};
+} // namespace mlir::qco
+
+#endif // MQT_CORE_UNIONTABLE_H
diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td
index 971dc18f71..0a9d993e1d 100644
--- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td
+++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td
@@ -201,4 +201,61 @@ def HadamardLifting : Pass<"hadamard-lifting", "mlir::ModuleOp"> {
}];
}
+def ConstantPropagation : Pass<"constant-propagation", "mlir::ModuleOp"> {
+ let dependentDialects = ["mlir::qco::QCODialect"];
+ let summary =
+ "This pass applies constant propagation to a circuit. It "
+ "assumes that all input qubits are |0>. It propagates the "
+ "state of the qubits up to a given complexity threshold and "
+ "removes gates which are superfluous considering the current "
+ "state. It also replaces quantum control with classical control "
+ "if possible and moves measurements as far to the front as possible.";
+ let description = [{
+ This pass applies quantum constant propagation. This optimization routines assumes that the input qubits of the
+ circuits are |0>. It propagates the qubit states and the state of additional classical values through the circuit.
+ All quantum instructions are removed which are superfluous considering the current state. Additionally, quantum
+ controls can be replaced by equivalent classical control. To do this most efficiently, the measurements are moved
+ as far to the front of a circuit as possible.
+
+ The qubit states and classical values are stored in hybrid states. Hybrid states are stored in a union table to
+ reduce the amount of complex amplitudes and classical values to track. There is a maximum number of non zero
+ amplitudes that is saved per union table entry. Additionally, there is also a maximum of hybrid states that can be
+ propagated. If the maximum number of amplitudes or the maximum number of hybrid states is exceeded, the propagated
+ state reaches top and no optimization routines are further applied.
+
+ The applied optimization routines are:
+
+ **General Control Reduction**
+ If a controlling qubit or classical value is always true, the control is removed. Classical vales are considered
+ true if they are not zero.
+ If a controlling qubit or classical value is alaways false, the complete gate is removed.
+
+ **Unsatisfiable Controls**
+ If a combination of controls (both quantum and classical) cannot be satisfied, the complete controlled gate is
+ removed.
+
+ **Quantum Control Reduction**
+ If a controlling qubit is true if and only if a classical value is true (false), the quantum control is replaced by a
+ classical one. If the classical value is false if and only if the qubit is true, the classical value becomes a
+ negative control.
+
+ **Implied Qubits**
+ If a controlling qubit is implied by other controlling qubits/classical values, the implied qubit is removed from
+ the controls.
+
+ **Phase Gate Reduction**
+ If a phase gate would only apply a global phase (e.g. if the quantum state is |11> and a Z-gate is applied), the
+ phase gate is removed and replaced by a global phase gate instead.
+
+ }];
+ let options = [Option<"maximumNonzeroAmplitudes", "maximumNonzeroAmplitudes",
+ "std::size_t", "4",
+ "The maximum number of non-zero amplitudes in the "
+ "tracted quantum states before reaching top.">,
+ Option<"maximumHybridStates", "maximumHybridStates",
+ "std::size_t", "4",
+ "The maximum number of hybrid states which have a "
+ "non-zero probability.">];
+}
+
#endif // MLIR_DIALECT_QCO_TRANSFORMS_PASSES_TD
diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation.cpp
new file mode 100644
index 0000000000..d6748924fa
--- /dev/null
+++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation.cpp
@@ -0,0 +1,1115 @@
+/*
+ * 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
+ */
+
+/*
+ * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM
+ * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH All rights
+ * reserved.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ * Licensed under the MIT License
+ */
+
+#include "mlir/Dialect/QCO/IR/QCOInterfaces.h"
+#include "mlir/Dialect/QCO/IR/QCOOps.h"
+#include "mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.hpp"
+#include "mlir/Dialect/QCO/Transforms/Passes.h"
+#include "mlir/Dialect/QTensor/IR/QTensorOps.h"
+#include "mlir/Dialect/Utils/Utils.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
+
+namespace {
+/**
+ * @brief Result of checking how do modify a controlled gate.
+ */
+struct ControlsToModify {
+ llvm::DenseSet quantumCtrlsToRemove;
+ llvm::DenseSet classicalPosCtrlsToAdd;
+ llvm::DenseSet classicalNegCtrlsToAdd;
+};
+} // namespace
+
+namespace mlir::qco {
+
+#define GEN_PASS_DEF_CONSTANTPROPAGATION
+#include "mlir/Dialect/QCO/Transforms/Passes.h.inc"
+
+#define CREATE_OP_CASE_NO_PARAMS(opType) \
+ .Case([&](opType gate) { \
+ return opType::create(rewriter, gate.getLoc(), qubitsIn[0]); \
+ })
+
+#define CREATE_OP_CASE_NO_PARAMS_TWO_QUBITS(opType) \
+ .Case([&](opType gate) { \
+ return opType::create(rewriter, gate.getLoc(), qubitsIn[0], qubitsIn[1]); \
+ })
+
+#define CREATE_OP_CASE_ONE_PARAM(opType) \
+ .Case([&](opType gate) { \
+ return opType::create(rewriter, gate.getLoc(), qubitsIn[0], \
+ gate.getTheta()); \
+ })
+
+#define CREATE_OP_CASE_ONE_PARAM_TWO_QUBITS(opType) \
+ .Case([&](opType gate) { \
+ return opType::create(rewriter, gate.getLoc(), qubitsIn[0], qubitsIn[1], \
+ gate.getTheta()); \
+ })
+
+#define CREATE_OP_CASE_TWO_PARAMS(opType) \
+ .Case([&](opType gate) { \
+ return opType::create(rewriter, gate.getLoc(), qubitsIn[0], \
+ gate.getTheta(), gate.getPhi()); \
+ })
+
+#define CREATE_OP_CASE_TWO_PARAMS_TWO_QUBITS(opType) \
+ .Case([&](opType gate) { \
+ return opType::create(rewriter, gate.getLoc(), qubitsIn[0], qubitsIn[1], \
+ gate.getTheta(), gate.getPhi()); \
+ })
+
+#define CREATE_OP_CASE_PLUS_MINUS_OPS(opType) \
+ .Case([&](opType gate) { \
+ return opType::create(rewriter, gate.getLoc(), qubitsIn[0], qubitsIn[1], \
+ gate.getTheta(), gate.getBeta()); \
+ })
+
+#define CREATE_OP_CASE_THREE_PARAMS(opType) \
+ .Case([&](opType gate) { \
+ return opType::create(rewriter, gate.getLoc(), qubitsIn[0], \
+ gate.getTheta(), gate.getPhi(), gate.getLambda()); \
+ })
+
+static LogicalResult iterateThroughWorklist(PatternRewriter& rewriter,
+ UnionTable* ut,
+ std::span& worklist,
+ std::span posClassicalCtrls,
+ std::span negClassicalCtrls);
+
+/**
+ * This method checks whether the func::FuncOp is an entry point to the program.
+ *
+ * @param op The func::FuncOp to be checked.
+ * @return Whether the operation is an entry point to the program.
+ */
+static bool isEntryPoint(const func::FuncOp op) {
+ const auto passthroughAttr = op->getAttrOfType("passthrough");
+ if (!passthroughAttr) {
+ return false;
+ }
+
+ return llvm::any_of(passthroughAttr, [](const Attribute attr) {
+ return mlir::isa