Skip to content

✨ Add Constant Propagation#1845

Draft
LiRem101 wants to merge 198 commits into
munich-quantum-toolkit:mainfrom
LiRem101:mlir/constant-propagation
Draft

✨ Add Constant Propagation#1845
LiRem101 wants to merge 198 commits into
munich-quantum-toolkit:mainfrom
LiRem101:mlir/constant-propagation

Conversation

@LiRem101

@LiRem101 LiRem101 commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR introduces the optimization pass constant propagation. It propagates the state of a quantum machine through a circuit and optimizes the applied gates with respect to the current state. Namely, it removes controlling qubits, exchanges controlling qubits with controlling classical values and removes phase gates which only introduce a global phase. The optimization routine is based on this paper, accepted at QSW 2026.

I suggest having constant propagation switched to off on default, because it assumes that all states are initial zero, can take quite a while and can create dynamic circuits, which might not be everyone's goal.

GPT-4 and GPT-5 have been used for refactoring and for first drafts of code. No Gen AI was integrated in the used IDE.

Fixes #1387

Checklist

  • The pull request only contains commits that are focused and relevant to this change.
  • I have added appropriate tests that cover the new/changed functionality.
  • I have updated the documentation to reflect these changes.
  • I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals.
  • I have added migration instructions to the upgrade guide (if needed).
  • The changes follow the project's style guidelines and introduce no new warnings.
  • The changes are fully tested and pass the CI checks.
  • I have reviewed my own code changes.

If PR contains AI-assisted content:

  • I have disclosed the use of AI tools in the PR description as per our AI Usage Guidelines.
  • 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.

LiRem101 and others added 30 commits March 26, 2026 16:52
@mergify mergify Bot added the conflict label Jul 15, 2026
@mergify mergify Bot removed the conflict label Jul 15, 2026
@LiRem101

Copy link
Copy Markdown
Collaborator Author

@coderabbitai Full review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added a constant-propagation optimization pass for quantum programs.
    • Simplifies gates and controls, folds classical computations, reduces global-phase-only operations, and repositions measurements.
    • Supports quantum/classical state tracking with configurable amplitude and state limits.
  • Documentation

    • Updated the changelog, upgrade references, PR template, and documentation formatting.
  • Tests

    • Added comprehensive coverage for quantum states, hybrid states, union tracking, and constant-propagation behavior.

Walkthrough

Changes

Introduces QCO quantum/classical state tracking and a ConstantPropagation MLIR pass that simplifies controls, branches, phase gates, and measurement placement. Adds extensive unit coverage and updates project documentation, templates, changelog, and formatting.

Constant propagation foundation

Layer / File(s) Summary
Quantum state representation and gate evaluation
mlir/include/mlir/.../ConstantPropagation/*, mlir/lib/mlir/.../ConstantPropagation/QuantumState.cpp, mlir/unittests/.../ConstantPropagation/test_quantumState.cpp
Adds amplitude-based quantum state propagation, gate mappings, measurement/reset branching, unification, and predicates.
Hybrid state and union-table propagation
mlir/include/mlir/.../ConstantPropagation/HybridState.hpp, UnionTable.hpp, ClassicalArithOperation.h, mlir/lib/.../HybridState.cpp, UnionTable.cpp, test_hybridState.cpp, test_unionTable.cpp
Tracks classical values alongside quantum states, propagates operations, manages state partitions, and analyzes controls, equivalences, phases, and satisfiability.
ConstantPropagation pass and control rewriting
mlir/include/mlir/.../Passes.td, mlir/lib/.../ConstantPropagation.cpp, mlir/unittests/.../test_qco_constant_propagation.cpp
Adds the configurable pass worklist and rewrites controlled gates, conditionals, global phases, and measurement order.
Supporting project and documentation updates
.github/*, CHANGELOG.md, UPGRADING.md, docs/*, pyproject.toml, mlir/unittests/.../CMakeLists.txt
Adds the changelog entry and test wiring while applying template, documentation, and configuration formatting updates.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested labels: enhancement, c++

Suggested reviewers: burgholzer

Poem

I’m a rabbit with qubits tucked under my ear,
Propagating constants till branches disappear.
Gates hop aside, amplitudes align,
Classical carrots make controls divine.
Tests thump their paws: the new pass is here!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR also changes unrelated files like .agent/plans, .github/pull_request_template.md, and pyproject.toml formatting. Move unrelated formatting and template updates to a separate PR or justify them as required for constant propagation.
Docstring Coverage ⚠️ Warning Docstring coverage is 20.24% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly names the main change: adding constant propagation.
Description check ✅ Passed The description includes the summary, issue reference, motivation, AI disclosure, and checklist items required by the template.
Linked Issues check ✅ Passed The PR implements the quantum and classical constant propagation requested by #1387 and the expected gate simplifications.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 22

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 15-16: Correct the contributor attribution markup in the changelog
entry by adding the missing closing bold delimiter after the username, while
preserving the existing link and entry text.

In
`@mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/ClassicalArithOperation.h`:
- Around line 31-107: Replace the native arithmetic in getArithIntegerOpResult
and getArithDoubleOpResult with MLIR arith folding or APInt/APFloat operations
so results match arith semantics, including overflow, shifts, division,
remainder, NaN, and signed-zero behavior. Keep maximumf and maxnumf handled
distinctly, and propagate a not-foldable result whenever the computed value
cannot be represented safely rather than forcing an int64_t or double result.

In
`@mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/GateToMap.h`:
- Around line 148-152: Update the iSWAPOp case in the gate-to-map optimization
so the |01⟩ and |10⟩ coefficients use +i (Complex(0, 1)) rather than -i,
matching MQT’s iSWAP matrix. Add an isolated iSWAP test that directly verifies
these mappings, independent of the multi-gate aggregate test.
- Around line 126-132: Update the U2Op case in the gate-to-map conversion so
every matrix entry uses the common 1/√2 normalization factor, not only the first
coefficient; preserve the existing parameterized phase expressions. Add a direct
regression test covering U2 basis-state propagation and verifying the normalized
output.

In
`@mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.hpp`:
- Around line 11-12: Add `#pragma` once at the beginning of the HybridState
header, while retaining the existing MQT_CORE_HYBRIDSTATE_H include guard.
- Around line 51-81: Update isOperationExecutable so both positive and negative
controls use one consistent zero predicate based on the documented exact
zero/nonzero semantics, rather than the current 1e-4 norm thresholds and
opposing comparisons. Ensure zero-valued controls satisfy only the appropriate
polarity, nonzero values satisfy the opposite polarity, and preserve the
existing missing-value domain_error behavior.

In
`@mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.hpp`:
- Around line 54-57: Cap supported state width before any unsigned-int mask
shifts in the quantum-state constant propagation logic, including the
union-table handling and the code around amplitudeMap updates. When a union
entry exceeds the bit width of unsigned int, transition it to TOP or otherwise
reject it before shifting; preserve existing behavior for representable widths
and prevent overflow or basis-state aliasing.
- Around line 11-12: Replace the manual include guard with `#pragma` once in
QuantumState.hpp at
mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.hpp:11-12,
and remove its matching closing `#endif`. Apply the same change to UnionTable.hpp
at
mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.hpp:11-12,
removing its closing `#endif` as well.

In
`@mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.hpp`:
- Around line 168-175: Update the entry-count calculation around
numberOfNewEntries to use std::size_t and guard each multiplication against
overflow by checking whether the current count exceeds maximumHybridEntries
divided by e.states.size() before multiplying. Throw the existing domain_error
when the cap would be exceeded, preserving the limit enforcement for large
Cartesian products.

In `@mlir/include/mlir/Dialect/QCO/Transforms/Passes.td`:
- Around line 214-231: Correct the public pass documentation in the pass
description and the additional referenced section: change “routines assumes” to
“routine assumes,” “vales” to “values,” “alaways” to “always,” and “tracted” to
“tracked,” without modifying the documented behavior.

In `@mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation.cpp`:
- Around line 1074-1090: Update the worklist processing around isEntryPoint and
iterateThroughWorklist so each entry-point function creates and uses its own
UnionTable state. Build the worklist and invoke processing per entry function,
ensuring values and equivalence lookups cannot persist across functions;
preserve the existing limits and rewriter behavior.
- Around line 612-638: Update putOperationIntoBranch so every value in
ControlsToModify::classicalPosCtrlsToAdd and classicalNegCtrlsToAdd contributes
to the branch condition, rather than selecting only the first DenseSet element.
Build nested conditions or an equivalent conjunction that preserves the original
positive and negative control requirements, and apply the same correction to the
corresponding logic at the later referenced branch.
- Around line 560-604: Update the both-empty branch handling around the
implicit-swap mapping so `order` is derived from each branch’s YieldOp operands,
not only from non-empty `thenArgs` or `elseArgs`. Ensure identity yields produce
a complete identity mapping and permuted yields are detected before
`order.at(...)` is used during result replacement, preserving the existing swap
error behavior.
- Around line 868-893: Update the target-mapping logic around targetQubits and
resultQubits to resize both vectors to numTargets and assign each mapped value
by index instead of inserting into an empty vector. In the target loop, compare
arguments[argIndex] with body.getInputTarget(i) and store
op->getInputTarget(argIndex) at the matched index; apply the equivalent indexed
assignment for yieldOP output mappings using op->getOutputTarget(uOpOutIndex).

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.cpp`:
- Around line 91-98: Update HybridState::operator== to handle top on either
operand before comparing probability or dereferencing qState: return true only
when both states are TOP, and false when exactly one is TOP. Preserve the
existing non-TOP probability and quantum-state comparison for states where both
qState values are available.

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.cpp`:
- Around line 39-48: Update QuantumState::QuantumState to copy globalQubitNumber
into a local container before sorting, then build globalToLocalQubitNumber from
the sorted copy. Do not mutate the caller-owned span.
- Around line 154-167: Guard every unsigned-int basis-state shift throughout
QuantumState, including construction, unify, gate propagation,
measurement/reset, and qubit-query paths. Validate that each local qubit index
is below std::numeric_limits<unsigned int>::digits before evaluating 1U <<
index, rejecting states or operations that exceed the supported width; preserve
existing behavior for valid indices.

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.cpp`:
- Around line 283-299: Apply the configured maximumHybridEntries cap while
accumulating states in the reset propagation loop around
HybridState::propagateReset. Before or during vecOfNewStates insertion, detect
when the generated total exceeds the limit and handle it using the same
top-state fallback behavior as the measurement branching path, rather than
assigning all branches to ute->states. Preserve the existing domain_error
handling and normal replacement flow when the cap is not exceeded.
- Around line 319-328: Update UnionTable::propagateIntAlloc and the
corresponding double-allocation path to create one shared UnionTableEntry
pointer, then insert that same pointer into both entries and valuesToEntries
instead of constructing separate objects. Preserve the existing entry contents
and indexing behavior.
- Around line 342-405: Update UnionTable::isQubitAlwaysOne, isQubitAlwaysZero,
isClassicalValueAlwaysTrue, isClassicalValueAlwaysFalse, and
hasAlwaysZeroProbability to detect TOP entries before evaluating their state
collections. Treat TOP as unknown: return false for “always” proofs and do not
report zero probability, rather than allowing empty-state all_of or iteration
results to imply certainty. Apply the same TOP handling to the related
proof/query paths around the additional referenced methods.

In
`@mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_hybridState.cpp`:
- Around line 369-375: Populate the right-hand state’s classical maps in all
three unification tests: at
mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_hybridState.cpp
lines 369-375, add v2 and v3 to hState2; at lines 393-397 and 412-416, make
hState2 mutable and add the intended classical values before calling unify.
- Around line 238-251: The negative-control tests pass ctrl as the
positive-control argument. In test_hybridState.cpp lines 238-251, update the
propagateMeasurement call to pass an empty positive-control vector and ctrl as
the negative-control vector, then assert measurement executes when the control
value is zero. In lines 302-315, make the same argument correction for
propagateReset and assert reset executes under the zero negative control.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5af346a4-b749-424b-b2dd-cf13bc2052c5

📥 Commits

Reviewing files that changed from the base of the PR and between 559fde4 and 22f705e.

📒 Files selected for processing (23)
  • .agent/plans/first-mlir-python-bindings.md
  • .github/pull_request_template.md
  • CHANGELOG.md
  • UPGRADING.md
  • docs/_templates/doxygen-header.html
  • docs/conf.py
  • docs/mlir/python_compiler_collection.md
  • mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/ClassicalArithOperation.h
  • mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/GateToMap.h
  • mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.hpp
  • mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.hpp
  • mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.hpp
  • mlir/include/mlir/Dialect/QCO/Transforms/Passes.td
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation.cpp
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.cpp
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.cpp
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.cpp
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_hybridState.cpp
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_quantumState.cpp
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_unionTable.cpp
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_constant_propagation.cpp
  • pyproject.toml
💤 Files with no reviewable changes (1)
  • docs/conf.py

Comment thread CHANGELOG.md
Comment on lines +15 to +16
- ✨ Add a `constant propagation` pass for reducing superfluous quantum
resources by propagating the quantum machine state ([#1845]) ([**@lirem101])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the author attribution markup.

Line 16 is missing the closing **, so the contributor attribution renders incorrectly.

Proposed fix
-  resources by propagating the quantum machine state ([`#1845`]) ([**`@lirem101`])
+  resources by propagating the quantum machine state ([`#1845`]) ([**`@lirem101`**])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- ✨ Add a `constant propagation` pass for reducing superfluous quantum
resources by propagating the quantum machine state ([#1845]) ([**@lirem101])
- ✨ Add a `constant propagation` pass for reducing superfluous quantum
resources by propagating the quantum machine state ([`#1845`]) ([**`@lirem101`**])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 15 - 16, Correct the contributor attribution
markup in the changelog entry by adding the missing closing bold delimiter after
the username, while preserving the existing link and entry text.

Comment on lines +31 to +107
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<mlir::VectorType>(operand.getType())) {
throw std::runtime_error(
"Constant propagation does not support vectors as classical types.");
}
}

return mlir::TypeSwitch<mlir::Operation*, int64_t>(operation)
.Case<mlir::arith::AddIOp>([&](auto) { return value1 + value2; })
.Case<mlir::arith::AndIOp>([&](auto) { return value1 & value2; })
.Case<mlir::arith::CeilDivSIOp>([&](auto) {
// Division that rounds to positive infinity
return ceil(1.0 * value1 / value2);
})
.Case<mlir::arith::DivSIOp>([&](auto) {
// Division that rounds towards zero
return value1 / value2;
})
.Case<mlir::arith::FloorDivSIOp>([&](auto) {
// Division that rounds to negative infinity
return floor(1.0 * value1 / value2);
})
.Case<mlir::arith::MaxSIOp>(
[&](auto) { return value1 > value2 ? value1 : value2; })
.Case<mlir::arith::MinSIOp>(
[&](auto) { return value1 < value2 ? value1 : value2; })
.Case<mlir::arith::MulIOp>([&](auto) { return value1 * value2; })
.Case<mlir::arith::OrIOp>([&](auto) { return value1 | value2; })
.Case<mlir::arith::RemSIOp>(
[&](auto) { return remainder(value1, value2); })
.Case<mlir::arith::ShLIOp>([&](auto) { return value1 << value2; })
.Case<mlir::arith::ShRSIOp>([&](auto) { return value1 >> value2; })
.Case<mlir::arith::SubIOp>([&](auto) { return value1 - value2; })
.Case<mlir::arith::XOrIOp>([&](auto) { return value1 ^ value2; })
.Case<mlir::arith::SelectOp>(
[&](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<mlir::VectorType>(operand.getType())) {
throw std::runtime_error(
"Constant propagation does not support vectors as classical types.");
}
}

return mlir::TypeSwitch<mlir::Operation*, double>(operation)
.Case<mlir::arith::AddFOp>([&](auto) { return value1 + value2; })
.Case<mlir::arith::DivFOp>([&](auto) { return value1 / value2; })
.Case<mlir::arith::MaximumFOp>(
[&](auto) { return value1 > value2 ? value1 : value2; })
.Case<mlir::arith::MaxNumFOp>(
[&](auto) { return value1 > value2 ? value1 : value2; })
.Case<mlir::arith::MinimumFOp>(
[&](auto) { return value1 < value2 ? value1 : value2; })
.Case<mlir::arith::MinNumFOp>(
[&](auto) { return value1 < value2 ? value1 : value2; })
.Case<mlir::arith::MulFOp>([&](auto) { return value1 * value2; })
.Case<mlir::arith::NegFOp>([&](auto) { return -value1; })
.Case<mlir::arith::RemFOp>(
[&](auto) { return remainder(value1, value2); })
.Case<mlir::arith::SubFOp>([&](auto) { return value1 - value2; })
.Default([&](auto) -> double {
throw std::runtime_error("Unsupported floating-point operation in "
"mlir::qco::classicalarithoperation");
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

What are the exact scalar folding semantics of MLIR arith.remsi, arith.remf, ceildivsi, floordivsi, maximumf, and maxnumf, including integer bit widths and poison/undefined cases?

💡 Result:

The MLIR 'arith' dialect defines basic arithmetic operations that operate on scalars (integers or floating-point), as well as vectors and tensors of these types [1]. Unless otherwise stated, these operations use two's complement representation for integers and propagate poison values [1]. If any input is poison, the result is poison [1]. arith.remsi (Signed Integer Division Remainder) This operation performs a signed integer remainder [2][3]. It treats the leading bit as a sign bit (e.g., 6 % -2 = 0) [2][3]. Division by zero is undefined behavior [2][3]. For vector and tensor types, if any element is divided by zero, the behavior is undefined [2][3]. arith.remf (Floating-Point Remainder) This operation performs a floating-point remainder, with semantics matching the llvm.frem instruction [4]. The sign of the result always matches the sign of the dividend (the left-hand side operand) [4]. arith.ceildivsi (Signed Integer Ceil Division) This performs signed integer division rounded towards positive infinity (e.g., 7 / -2 = -3) [5]. arith.floordivsi (Signed Integer Floor Division) This performs signed integer division rounded towards negative infinity (e.g., 5 / -2 = -3) [5][6]. arith.maximumf (Floating-Point Maximum) Returns the maximum of two arguments, treating -0.0 as less than +0.0 [2][5]. If either argument is NaN, the result is NaN [2][5]. arith.maxnumf (Floating-Point Maximum) Returns the maximum of two arguments [2][5]. If the arguments are -0.0 and +0.0, the result is either value [2][5]. If one argument is NaN, the result is the other argument [2][5]. Note on Integer Bit Widths: These operations are polymorphic and accept integers of any bit width (e.g., i8, i16, i32, i64) as defined in the MLIR type system [1][2]. The specific behavior, such as two's complement arithmetic and overflow behavior, applies generically across these widths [1]. Note on Poison: Unless otherwise specified, all operations in the arith dialect propagate poison values, meaning that if any of the inputs are poison, the output is also poison [1]. For structured types (vector/tensor), this propagation occurs element-wise [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the target header and nearby symbols.
ast-grep outline mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/ClassicalArithOperation.h --view expanded || true

echo
echo "=== File excerpt ==="
sed -n '1,220p' mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/ClassicalArithOperation.h

echo
echo "=== Call sites for helpers ==="
rg -n "getArith(Integer|Double)OpResult|maximumf|maxnumf|remsi|remf|ceildivsi|floordivsi" mlir -g '!**/build/**'

Repository: munich-quantum-toolkit/core

Length of output: 5551


Use MLIR folding/APInt/APFloat here instead of native C++ arithmetic.

  • std::remainder, double-based ceil/floor division, and raw shifts/overflow rules do not match arith semantics.
  • maximumf and maxnumf are also conflated; they differ on NaN and signed zero.
  • Delegate to the ops’ fold hooks or use APInt/APFloat, and return not-foldable when the result can’t be represented safely.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/ClassicalArithOperation.h`
around lines 31 - 107, Replace the native arithmetic in getArithIntegerOpResult
and getArithDoubleOpResult with MLIR arith folding or APInt/APFloat operations
so results match arith semantics, including overflow, shifts, division,
remainder, NaN, and signed-zero behavior. Keep maximumf and maxnumf handled
distinctly, and propagate a not-foldable result whenever the computed value
cannot be represented safely rather than forcing an int64_t or double result.

Comment on lines +126 to +132
.Case<mlir::qco::U2Op>([&](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]))}}}}};
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Apply the 1/√2 factor to every U2 matrix entry.

Only the first coefficient is normalized, making this matrix non-unitary. MQT defines the entire U2 matrix with a common 1/√2 factor. (mqt.readthedocs.io)

Proposed fix
       .Case<mlir::qco::U2Op>([&](auto) {
         return ResultMap{
-            {{0, {{0, Complex(inv_sqrt2, 0)}, {1, exp(Complex(0, params[0]))}}},
+            {{0,
+              {{0, Complex(inv_sqrt2, 0)},
+               {1, inv_sqrt2 * exp(Complex(0, params[0]))}}},
              {1,
-              {{0, -exp(Complex(0, params[1]))},
-               {1, exp(Complex(0, params[0] + params[1]))}}}}};
+              {{0, -inv_sqrt2 * exp(Complex(0, params[1]))},
+               {1, inv_sqrt2 *
+                       exp(Complex(0, params[0] + params[1]))}}}}};
       })

Add a direct U2 basis-state regression test.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.Case<mlir::qco::U2Op>([&](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<mlir::qco::U2Op>([&](auto) {
return ResultMap{
{{0,
{{0, Complex(inv_sqrt2, 0)},
{1, inv_sqrt2 * exp(Complex(0, params[0]))}}},
{1,
{{0, -inv_sqrt2 * exp(Complex(0, params[1]))},
{1, inv_sqrt2 *
exp(Complex(0, params[0] + params[1]))}}}}};
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/GateToMap.h`
around lines 126 - 132, Update the U2Op case in the gate-to-map conversion so
every matrix entry uses the common 1/√2 normalization factor, not only the first
coefficient; preserve the existing parameterized phase expressions. Add a direct
regression test covering U2 basis-state propagation and verifying the normalized
output.

Comment on lines +148 to +152
.Case<mlir::qco::iSWAPOp>([&](auto) {
return ResultMap{{{0, {{0, Complex(1, 0)}}},
{1, {{2, Complex(0, -1)}}},
{2, {{1, Complex(0, -1)}}},
{3, {{3, Complex(1, 0)}}}}};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use +i for iSWAP, not -i.

These coefficients implement iSWAP†. MQT's iSWAP matrix maps |01⟩ and |10⟩ with +i. (mqt.readthedocs.io)

Proposed fix
       .Case<mlir::qco::iSWAPOp>([&](auto) {
         return ResultMap{{{0, {{0, Complex(1, 0)}}},
-                          {1, {{2, Complex(0, -1)}}},
-                          {2, {{1, Complex(0, -1)}}},
+                          {1, {{2, Complex(0, 1)}}},
+                          {2, {{1, Complex(0, 1)}}},
                           {3, {{3, Complex(1, 0)}}}}};
       })

Add an isolated iSWAP test rather than relying on the multi-gate aggregate test.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.Case<mlir::qco::iSWAPOp>([&](auto) {
return ResultMap{{{0, {{0, Complex(1, 0)}}},
{1, {{2, Complex(0, -1)}}},
{2, {{1, Complex(0, -1)}}},
{3, {{3, Complex(1, 0)}}}}};
.Case<mlir::qco::iSWAPOp>([&](auto) {
return ResultMap{{{0, {{0, Complex(1, 0)}}},
{1, {{2, Complex(0, 1)}}},
{2, {{1, Complex(0, 1)}}},
{3, {{3, Complex(1, 0)}}}}};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/GateToMap.h`
around lines 148 - 152, Update the iSWAPOp case in the gate-to-map optimization
so the |01⟩ and |10⟩ coefficients use +i (Complex(0, 1)) rather than -i,
matching MQT’s iSWAP matrix. Add an isolated iSWAP test that directly verifies
these mappings, independent of the multi-gate aggregate test.

Comment on lines +11 to +12
#ifndef MQT_CORE_HYBRIDSTATE_H
#define MQT_CORE_HYBRIDSTATE_H

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the required #pragma once.

As per coding guidelines, C++ headers must use #pragma once.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@mlir/include/mlir/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.hpp`
around lines 11 - 12, Add `#pragma` once at the beginning of the HybridState
header, while retaining the existing MQT_CORE_HYBRIDSTATE_H include guard.

Source: Coding guidelines

Comment on lines +283 to +299
std::vector<HybridState> vecOfNewStates;

for (auto hs : ute->states) {
try {
auto newStates =
hs.propagateReset(qubitsToGlobalIndices.at(quantumTarget),
posCtrlsClassical, negCtrlsClassical);
vecOfNewStates.insert(vecOfNewStates.end(), newStates.begin(),
newStates.end());
} catch (std::domain_error&) {
putEntriesToTop({*ute});
vecOfNewStates.clear();
break;
}
}
ute->states = vecOfNewStates;
replaceValuesGlobally(quantumTargetVec, newQuantumValueVec);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Apply maximumHybridEntries to reset branching.

Unlike measurement, reset assigns every generated branch without checking the configured cap. Repeated resets can grow states exponentially and bypass the pass’s advertised complexity limit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.cpp`
around lines 283 - 299, Apply the configured maximumHybridEntries cap while
accumulating states in the reset propagation loop around
HybridState::propagateReset. Before or during vecOfNewStates insertion, detect
when the generated total exceeds the limit and handle it using the same
top-state fallback behavior as the measurement branching path, rather than
assigning all branches to ute->states. Preserve the existing domain_error
handling and normal replacement flow when the cap is not exceeded.

Comment on lines +319 to +328
void UnionTable::propagateIntAlloc(const Value intValue, const int64_t number) {
auto hs = HybridState({}, maxNonzeroAmplitudes);
hs.addIntegerValue(intValue, number);

auto ute = UnionTableEntry();
ute.states.push_back(hs);
ute.participatingClassicalValues.insert(intValue);
entries.insert(std::make_shared<UnionTableEntry>(ute));
valuesToEntries[intValue] = std::make_shared<UnionTableEntry>(ute);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Store the same UnionTableEntry pointer in both indexes.

Each classical allocation inserts one object into entries and another into valuesToEntries. Later mutations through the value map are therefore invisible when iterating entries.

Proposed pattern
-  entries.insert(std::make_shared<UnionTableEntry>(ute));
-  valuesToEntries[intValue] = std::make_shared<UnionTableEntry>(ute);
+  const auto entry = std::make_shared<UnionTableEntry>(std::move(ute));
+  entries.insert(entry);
+  valuesToEntries[intValue] = entry;

Apply the same change to double allocations.

Also applies to: 330-340

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.cpp`
around lines 319 - 328, Update UnionTable::propagateIntAlloc and the
corresponding double-allocation path to create one shared UnionTableEntry
pointer, then insert that same pointer into both entries and valuesToEntries
instead of constructing separate objects. Preserve the existing entry contents
and indexing behavior.

Comment on lines +342 to +405
bool UnionTable::isQubitAlwaysOne(const Value q) const {
const unsigned int qubitIndex = qubitsToGlobalIndices.at(q);
const auto ute = valuesToEntries.at(q);
return std::ranges::all_of(ute->states, [&](const auto& hs) {
return hs.isQubitAlwaysOne(qubitIndex);
});
}

bool UnionTable::isQubitAlwaysZero(const Value q) const {
const unsigned int qubitIndex = qubitsToGlobalIndices.at(q);
const auto ute = valuesToEntries.at(q);
return std::ranges::all_of(ute->states, [&](const auto& hs) {
return hs.isQubitAlwaysZero(qubitIndex);
});
}

bool UnionTable::isClassicalValueAlwaysTrue(const Value c) const {
const auto ute = valuesToEntries.at(c);
return std::ranges::all_of(ute->states,
[&](const auto& hs) { return hs.isValueTrue(c); });
}

bool UnionTable::isClassicalValueAlwaysFalse(const Value c) const {
const auto ute = valuesToEntries.at(c);
return std::ranges::all_of(
ute->states, [&](const auto& hs) { return !hs.isValueTrue(c); });
}

bool UnionTable::hasAlwaysZeroProbability(
const llvm::DenseMap<Value, bool>& qubitValues,
const llvm::DenseMap<Value, bool>& classicalValues) const {
std::set<UnionTableEntry> participatingEntries;
for (const auto& [qV, _] : qubitValues) {
participatingEntries.insert(*valuesToEntries.at(qV));
}
for (const auto& [cV, _] : classicalValues) {
participatingEntries.insert(*valuesToEntries.at(cV));
}
for (const auto& ute : participatingEntries) {
std::unordered_map<unsigned int, bool> qubitValuesThisEntry;
llvm::DenseMap<Value, bool> classicalValuesThisEntry;
for (const auto& [qV, qBool] : qubitValues) {
if (ute.participatingQubits.contains(qV)) {
qubitValuesThisEntry[qubitsToGlobalIndices.at(qV)] = qBool;
}
}
for (const auto& [v, vBool] : classicalValues) {
if (ute.participatingClassicalValues.contains(v)) {
classicalValuesThisEntry[v] = vBool;
}
}
bool oneEntryIsNonzero = false;
for (const auto& hs : ute.states) {
if (!hs.hasAlwaysZeroProbability(qubitValuesThisEntry,
classicalValuesThisEntry)) {
oneEntryIsNonzero = true;
break;
}
}
if (!oneEntryIsNonzero) {
return true;
}
}
return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Treat TOP as unknown, not as an empty impossible state.

For TOP entries, all_of(empty) makes qubits/classical values simultaneously “always zero” and “always one,” while hasAlwaysZeroProbability returns true. A later controlled gate can therefore be incorrectly removed. Explicitly return “unknown” for TOP in these proof/query paths.

Also applies to: 408-449, 548-626

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.cpp`
around lines 342 - 405, Update UnionTable::isQubitAlwaysOne, isQubitAlwaysZero,
isClassicalValueAlwaysTrue, isClassicalValueAlwaysFalse, and
hasAlwaysZeroProbability to detect TOP entries before evaluating their state
collections. Treat TOP as unknown: return false for “always” proofs and do not
report zero probability, rather than allowing empty-state all_of or iteration
results to imply certainty. Apply the same TOP handling to the related
proof/query paths around the additional referenced methods.

Comment on lines +238 to +251
TEST_F(HybridStateTest, doMeasurementWithNegClassicalCtrl) {
auto hState = HybridState(fourQubits, 4, 0.6);
constexpr auto v1 = mlir::Value();
hState.addIntegerValue(v1, 0);
std::vector ctrl = {v1};
hState.propagateGate(hOp.getOperation(), vectorZero);
const auto resStates = hState.propagateMeasurement(0, v1, ctrl);

EXPECT_TRUE(resStates.size() == 1);
const auto& resHybridState = resStates.at(0);
EXPECT_THAT(
resHybridState.toString(),
testing::HasSubstr(
"{|0000> -> 0.71, |0001> -> 0.71}: integerValue0 = 0; p = 0.60"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The negative-control tests bind their control vectors to the positive-control parameter.

  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_hybridState.cpp#L238-L251: call propagateMeasurement(0, v1, {}, ctrl) and assert that a zero negative control executes measurement.
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_hybridState.cpp#L302-L315: call propagateReset(0, {}, ctrl) and assert that a zero negative control executes reset.
📍 Affects 1 file
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_hybridState.cpp#L238-L251 (this comment)
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_hybridState.cpp#L302-L315
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_hybridState.cpp`
around lines 238 - 251, The negative-control tests pass ctrl as the
positive-control argument. In test_hybridState.cpp lines 238-251, update the
propagateMeasurement call to pass an empty positive-control vector and ctrl as
the negative-control vector, then assert measurement executes when the control
value is zero. In lines 302-315, make the same argument correction for
propagateReset and assert reset executes under the zero negative control.

Comment on lines +369 to +375
auto hState2 = HybridState(vectorOneThree, 10, 0.5);
hState2.propagateGate(hOp.getOperation(), vectorThree);
hState2.propagateGate(xOp.getOperation(), vectorOne, vectorThree);
hState1.addIntegerValue(v2, 7);
hState1.addDoubleValue(v3, 4.2);

const HybridState unified = hState1.unify(hState2);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The unification tests never populate the right-hand classical maps.

  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_hybridState.cpp#L369-L375: add v2 and v3 to hState2.
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_hybridState.cpp#L393-L397: make hState2 mutable and add the intended values to it.
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_hybridState.cpp#L412-L416: make hState2 mutable and populate its classical-only values.
📍 Affects 1 file
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_hybridState.cpp#L369-L375 (this comment)
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_hybridState.cpp#L393-L397
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_hybridState.cpp#L412-L416
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_hybridState.cpp`
around lines 369 - 375, Populate the right-hand state’s classical maps in all
three unification tests: at
mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_hybridState.cpp
lines 369-375, add v2 and v3 to hState2; at lines 393-397 and 412-416, make
hState2 mutable and add the intended classical values before calling unify.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request MLIR Anything related to MLIR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ MLIR - Introduce Constant Propagation as optimization method

3 participants