From b2d6c5eb64da3082b09fe37264cb43b16dc32897 Mon Sep 17 00:00:00 2001 From: rturrado Date: Sat, 5 Sep 2026 00:47:05 +0200 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9C=85=20Add=20regression=20test=20for?= =?UTF-8?q?=20multi-line=20if=20body?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `test_custom_code` already exercises `if(c==1) { x q[0]; x q[1]; }` on a single physical line. Add the same case with a real multi-line block, since issue #168 hints that this form is not accepted. It already is; the new test locks that in. Assisted-by: Claude Opus 4.7 via Claude Code --- test/test_custom_code.cpp | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/test_custom_code.cpp b/test/test_custom_code.cpp index 8528bbb6..e2370956 100644 --- a/test/test_custom_code.cpp +++ b/test/test_custom_code.cpp @@ -95,6 +95,28 @@ TEST_F(CustomCodeTest, IfElseOperationMulti) { ASSERT_TRUE(complexEquality(amplitudes[2], 1, 0.0)); } +/** + * @test Same behaviour as `IfElseOperationMulti`, but with the `if` body + * written as a real multi-line block. + * Ensures the parser handles line breaks inside a classic-controlled body, + * not only the single-line form. + */ +TEST_F(CustomCodeTest, IfElseOperationMultiMultilineBlock) { + loadCode(2, 1, + "x q[0];\n" + "measure q[0] -> c[0];\n" + "if(c==1) {\n" + " x q[0];\n" + " x q[1];\n" + "}\n"); + ASSERT_EQ(state->runSimulation(state), OK); + + std::array amplitudes{}; + Statevector sv{2, 4, amplitudes.data()}; + state->getStateVectorFull(state, &sv); + ASSERT_TRUE(complexEquality(amplitudes[2], 1, 0.0)); +} + /** * @test Test the `reset` instruction. */ From 3e49a113a14d33ed38b822f15958dfba81ef8ef4 Mon Sep 17 00:00:00 2001 From: rturrado Date: Sat, 5 Sep 2026 00:50:06 +0200 Subject: [PATCH 2/6] =?UTF-8?q?=E2=9C=A8=20Support=20non-equality=20compar?= =?UTF-8?q?ators=20in=20if=20conditions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The debugger only accepted `==` in classic-controlled `if` conditions and threw at runtime when mqt-core's `IfElseOperation` reported any other comparison kind, even though mqt-core supports the full set. Extend `ClassicCondition` with a `qc::ComparisonKind` field so the parser carries the operator through and evaluate the condition with the matching comparison in the DD backend. Add one test per new operator; each mixes a case that triggers with one that does not, so the operator is actually applied and not read as always-true by accident. Part of #168. Assisted-by: Claude Opus 4.7 via Claude Code --- include/common/parsing/CodePreprocessing.hpp | 5 ++ src/backend/dd/DDSimDebug.cpp | 41 ++++++--- src/common/parsing/CodePreprocessing.cpp | 41 +++++++-- test/test_custom_code.cpp | 95 ++++++++++++++++++++ 4 files changed, 163 insertions(+), 19 deletions(-) diff --git a/include/common/parsing/CodePreprocessing.hpp b/include/common/parsing/CodePreprocessing.hpp index f777d234..d2896120 100644 --- a/include/common/parsing/CodePreprocessing.hpp +++ b/include/common/parsing/CodePreprocessing.hpp @@ -17,6 +17,7 @@ #pragma once #include "AssertionParsing.hpp" +#include "ir/operations/IfElseOperation.hpp" #include #include @@ -228,6 +229,10 @@ struct ClassicCondition { * @brief The expected value in the condition comparison. */ size_t expectedValue; + /** + * @brief The comparison operator used in the condition. + */ + qc::ComparisonKind kind = qc::Eq; }; /** diff --git a/src/backend/dd/DDSimDebug.cpp b/src/backend/dd/DDSimDebug.cpp index 1bed2617..3409efca 100644 --- a/src/backend/dd/DDSimDebug.cpp +++ b/src/backend/dd/DDSimDebug.cpp @@ -95,6 +95,31 @@ struct DDSimulationStateGuard { DDSimulationState* state; }; +/** + * @brief Apply a classical comparison operator between two unsigned values. + * @param lhs The left-hand side of the comparison. + * @param rhs The right-hand side of the comparison. + * @param kind The comparison operator to apply. + * @return The boolean result of `lhs rhs`. + */ +bool applyComparison(size_t lhs, size_t rhs, qc::ComparisonKind kind) { + switch (kind) { + case qc::Eq: + return lhs == rhs; + case qc::Neq: + return lhs != rhs; + case qc::Lt: + return lhs < rhs; + case qc::Leq: + return lhs <= rhs; + case qc::Gt: + return lhs > rhs; + case qc::Geq: + return lhs >= rhs; + } + return false; +} + /** * @brief Evaluate a classic-controlled condition from the original code. * @param ddsim The simulation state. @@ -133,7 +158,7 @@ std::optional evaluateClassicConditionFromCode(DDSimulationState* ddsim, } } - return registerValue == parsed->expectedValue; + return applyComparison(registerValue, parsed->expectedValue, parsed->kind); } /** @@ -1104,10 +1129,6 @@ Result ddsimStepForward(SimulationState* self) { // register first. const auto* op = dynamic_cast((*ddsim->iterator).get()); - if (op->getComparisonKind() != qc::Eq) { - throw std::runtime_error("If-else operations with non-equality " - "comparisons are currently not supported"); - } const auto condition = evaluateClassicConditionFromCode(ddsim, currentInstruction); bool conditionMet = false; @@ -1130,7 +1151,8 @@ Result ddsimStepForward(SimulationState* self) { registerValue |= (value ? 1ULL : 0ULL) << i; } } - conditionMet = (registerValue == exp); + conditionMet = + applyComparison(registerValue, exp, op->getComparisonKind()); } if (conditionMet) { auto* thenOp = op->getThenOp(); @@ -1202,10 +1224,6 @@ Result ddsimStepBackward(SimulationState* self) { if ((*ddsim->iterator)->isIfElseOperation()) { const auto* op = dynamic_cast((*ddsim->iterator).get()); - if (op->getComparisonKind() != qc::Eq) { - throw std::runtime_error("If-else operations with non-equality " - "comparisons are currently not supported"); - } const auto condition = evaluateClassicConditionFromCode(ddsim, ddsim->currentInstruction); bool conditionMet = false; @@ -1228,7 +1246,8 @@ Result ddsimStepBackward(SimulationState* self) { registerValue |= (value ? 1ULL : 0ULL) << i; } } - conditionMet = (registerValue == exp); + conditionMet = + applyComparison(registerValue, exp, op->getComparisonKind()); } if (conditionMet) { auto* thenOp = op->getThenOp(); diff --git a/src/common/parsing/CodePreprocessing.cpp b/src/common/parsing/CodePreprocessing.cpp index 4c0b3112..3fe3801b 100644 --- a/src/common/parsing/CodePreprocessing.cpp +++ b/src/common/parsing/CodePreprocessing.cpp @@ -20,6 +20,7 @@ #include "common/parsing/Utils.hpp" #include +#include #include #include #include @@ -30,6 +31,7 @@ #include #include #include +#include #include #include @@ -410,12 +412,31 @@ parseClassicConditionExpression(const std::string& condition) { if (!normalized.empty() && normalized.front() == '(') { normalized.erase(0, 1); } - const auto eqPos = normalized.find("=="); - if (eqPos == std::string::npos) { + + // Operators must be scanned longest-first so that "<=" is not misread as "<". + struct OperatorMatch { + std::string_view text; + qc::ComparisonKind kind; + }; + static constexpr std::array operators{{ + {"<=", qc::Leq}, + {">=", qc::Geq}, + {"==", qc::Eq}, + {"!=", qc::Neq}, + {"<", qc::Lt}, + {">", qc::Gt}, + }}; + + const auto found = + std::ranges::find_if(operators, [&normalized](const auto& op) { + return normalized.find(op.text) != std::string::npos; + }); + if (found == operators.end()) { return std::nullopt; } - const auto lhs = normalized.substr(0, eqPos); - const auto rhs = normalized.substr(eqPos + 2); + const auto opPos = normalized.find(found->text); + const auto lhs = normalized.substr(0, opPos); + const auto rhs = normalized.substr(opPos + found->text.size()); if (lhs.empty() || rhs.empty()) { return std::nullopt; } @@ -453,12 +474,16 @@ parseClassicConditionExpression(const std::string& condition) { } catch (const std::out_of_range&) { return std::nullopt; } - return ClassicCondition{ - .registerName = base, .bitIndex = bitIndex, .expectedValue = expected}; + return ClassicCondition{.registerName = base, + .bitIndex = bitIndex, + .expectedValue = expected, + .kind = found->kind}; } - return ClassicCondition{ - .registerName = lhs, .bitIndex = std::nullopt, .expectedValue = expected}; + return ClassicCondition{.registerName = lhs, + .bitIndex = std::nullopt, + .expectedValue = expected, + .kind = found->kind}; } std::optional diff --git a/test/test_custom_code.cpp b/test/test_custom_code.cpp index e2370956..8ff2d53b 100644 --- a/test/test_custom_code.cpp +++ b/test/test_custom_code.cpp @@ -117,6 +117,101 @@ TEST_F(CustomCodeTest, IfElseOperationMultiMultilineBlock) { ASSERT_TRUE(complexEquality(amplitudes[2], 1, 0.0)); } +/** + * @test Test classic-controlled operations that use the `!=` comparator. + * The measured value of `c` is 1, so `c != 0` triggers and `c != 1` does not. + */ +TEST_F(CustomCodeTest, IfElseOperationNeq) { + loadCode(2, 1, + "x q[0];" + "cx q[0], q[1];" + "measure q[0] -> c[0];" + "if(c!=0) x q[1];" + "if(c!=1) z q[1];"); + ASSERT_EQ(state->runSimulation(state), OK); + + std::array amplitudes{}; + Statevector sv{2, 4, amplitudes.data()}; + state->getStateVectorFull(state, &sv); + ASSERT_TRUE(complexEquality(amplitudes[1], 1, 0.0)); +} + +/** + * @test Test classic-controlled operations that use the `<` comparator. + * The measured value of `c` is 1, so `c < 2` triggers and `c < 1` does not. + */ +TEST_F(CustomCodeTest, IfElseOperationLt) { + loadCode(2, 1, + "x q[0];" + "cx q[0], q[1];" + "measure q[0] -> c[0];" + "if(c<2) x q[1];" + "if(c<1) z q[1];"); + ASSERT_EQ(state->runSimulation(state), OK); + + std::array amplitudes{}; + Statevector sv{2, 4, amplitudes.data()}; + state->getStateVectorFull(state, &sv); + ASSERT_TRUE(complexEquality(amplitudes[1], 1, 0.0)); +} + +/** + * @test Test classic-controlled operations that use the `<=` comparator. + * The measured value of `c` is 1, so `c <= 1` triggers and `c <= 0` does not. + */ +TEST_F(CustomCodeTest, IfElseOperationLeq) { + loadCode(2, 1, + "x q[0];" + "cx q[0], q[1];" + "measure q[0] -> c[0];" + "if(c<=1) x q[1];" + "if(c<=0) z q[1];"); + ASSERT_EQ(state->runSimulation(state), OK); + + std::array amplitudes{}; + Statevector sv{2, 4, amplitudes.data()}; + state->getStateVectorFull(state, &sv); + ASSERT_TRUE(complexEquality(amplitudes[1], 1, 0.0)); +} + +/** + * @test Test classic-controlled operations that use the `>` comparator. + * The measured value of `c` is 1, so `c > 0` triggers and `c > 1` does not. + */ +TEST_F(CustomCodeTest, IfElseOperationGt) { + loadCode(2, 1, + "x q[0];" + "cx q[0], q[1];" + "measure q[0] -> c[0];" + "if(c>0) x q[1];" + "if(c>1) z q[1];"); + ASSERT_EQ(state->runSimulation(state), OK); + + std::array amplitudes{}; + Statevector sv{2, 4, amplitudes.data()}; + state->getStateVectorFull(state, &sv); + ASSERT_TRUE(complexEquality(amplitudes[1], 1, 0.0)); +} + +/** + * @test Test classic-controlled operations that use the `>=` comparator. + * The measured value of `c` is 1, so `c >= 1` triggers and `c >= 2` does not. + */ +TEST_F(CustomCodeTest, IfElseOperationGeq) { + loadCode(2, 1, + "x q[0];" + "cx q[0], q[1];" + "measure q[0] -> c[0];" + "if(c>=1) x q[1];" + "if(c>=2) z q[1];"); + ASSERT_EQ(state->runSimulation(state), OK); + + std::array amplitudes{}; + Statevector sv{2, 4, amplitudes.data()}; + state->getStateVectorFull(state, &sv); + ASSERT_TRUE(complexEquality(amplitudes[1], 1, 0.0)); +} + /** * @test Test the `reset` instruction. */ From 225ea81751910581f09e1385b81ebad264937ebc Mon Sep 17 00:00:00 2001 From: rturrado Date: Sat, 5 Sep 2026 19:41:05 +0200 Subject: [PATCH 3/6] =?UTF-8?q?=E2=9C=85=20Cover=20single-bit=20conditions?= =?UTF-8?q?=20with=20non-equality=20operators?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comparator change already handles conditions on a single bit like `if(c[0] > 0) ...` through the existing bracket path in the parser and mqt-core's `getControlBit()`. Add a test to lock that in and to cover the single-bit branch of the DD backend with a non-`Eq` comparator. Assisted-by: Claude Opus 4.7 via Claude Code --- test/test_custom_code.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/test_custom_code.cpp b/test/test_custom_code.cpp index 8ff2d53b..dcba9144 100644 --- a/test/test_custom_code.cpp +++ b/test/test_custom_code.cpp @@ -212,6 +212,27 @@ TEST_F(CustomCodeTest, IfElseOperationGeq) { ASSERT_TRUE(complexEquality(amplitudes[1], 1, 0.0)); } +/** + * @test Test classic-controlled operations that condition on a single bit + * of a classical register, using a non-equality comparator. + * The measured value of `c[0]` is 1, so `c[0] > 0` triggers and + * `c[0] > 1` does not. + */ +TEST_F(CustomCodeTest, IfElseOperationSingleBit) { + loadCode(2, 1, + "x q[0];" + "cx q[0], q[1];" + "measure q[0] -> c[0];" + "if(c[0]>0) x q[1];" + "if(c[0]>1) z q[1];"); + ASSERT_EQ(state->runSimulation(state), OK); + + std::array amplitudes{}; + Statevector sv{2, 4, amplitudes.data()}; + state->getStateVectorFull(state, &sv); + ASSERT_TRUE(complexEquality(amplitudes[1], 1, 0.0)); +} + /** * @test Test the `reset` instruction. */ From 54d1f4c982c90ef28649f372e4e01d507da7a8ab Mon Sep 17 00:00:00 2001 From: rturrado Date: Sat, 5 Sep 2026 20:21:59 +0200 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=8E=A8=20Address=20clang-tidy=20findi?= =?UTF-8?q?ngs=20from=20cpp-linter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four small changes that satisfy the checks reported by the `cpp-linter` job on the PR, all in `parseClassicConditionExpression`: - Directly include `ir/operations/IfElseOperation.hpp` in the `.cpp`. The header was only pulled in transitively via `CodePreprocessing.hpp`, which `misc-include-cleaner` rejects. - Rename the operator table `operators` to `OPERATORS`. The project's `readability-identifier-naming` rule requires `StaticConstantCase = UPPER_CASE`. - Use designated initializers in the `OPERATORS` array (`.text = ..., .kind = ...`). - Qualify the `std::ranges::find_if` result as `const auto* const found`. The iterator over a plain `std::array` is a raw pointer, so `readability-qualified-auto` wants the qualification. Assisted-by: Claude Opus 4.7 via Claude Code --- src/common/parsing/CodePreprocessing.cpp | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/common/parsing/CodePreprocessing.cpp b/src/common/parsing/CodePreprocessing.cpp index 3fe3801b..0cc55411 100644 --- a/src/common/parsing/CodePreprocessing.cpp +++ b/src/common/parsing/CodePreprocessing.cpp @@ -18,6 +18,7 @@ #include "common/parsing/AssertionParsing.hpp" #include "common/parsing/ParsingError.hpp" #include "common/parsing/Utils.hpp" +#include "ir/operations/IfElseOperation.hpp" #include #include @@ -418,20 +419,20 @@ parseClassicConditionExpression(const std::string& condition) { std::string_view text; qc::ComparisonKind kind; }; - static constexpr std::array operators{{ - {"<=", qc::Leq}, - {">=", qc::Geq}, - {"==", qc::Eq}, - {"!=", qc::Neq}, - {"<", qc::Lt}, - {">", qc::Gt}, + static constexpr std::array OPERATORS{{ + {.text = "<=", .kind = qc::Leq}, + {.text = ">=", .kind = qc::Geq}, + {.text = "==", .kind = qc::Eq}, + {.text = "!=", .kind = qc::Neq}, + {.text = "<", .kind = qc::Lt}, + {.text = ">", .kind = qc::Gt}, }}; - const auto found = - std::ranges::find_if(operators, [&normalized](const auto& op) { + const auto* const found = + std::ranges::find_if(OPERATORS, [&normalized](const auto& op) { return normalized.find(op.text) != std::string::npos; }); - if (found == operators.end()) { + if (found == OPERATORS.end()) { return std::nullopt; } const auto opPos = normalized.find(found->text); From 77445d38e235e59327e21b95ac4af73e126df1c9 Mon Sep 17 00:00:00 2001 From: rturrado Date: Sat, 5 Sep 2026 20:33:25 +0200 Subject: [PATCH 5/6] =?UTF-8?q?=E2=9C=85=20Cover=20backward=20stepping=20f?= =?UTF-8?q?or=20non-equality=20conditions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-operator tests added earlier only call `runSimulation`, so the backward-step branch of the DD backend's `if` handling (also updated in the comparator commit) had no coverage. Add one test that runs forward, then calls `stepBackward` and verifies that the `x q[1]` applied by `if(c > 0)` is correctly undone. Addresses a CodeRabbit review comment on the PR. Assisted-by: Claude Opus 4.7 via Claude Code --- test/test_custom_code.cpp | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/test_custom_code.cpp b/test/test_custom_code.cpp index dcba9144..e55337ad 100644 --- a/test/test_custom_code.cpp +++ b/test/test_custom_code.cpp @@ -233,6 +233,32 @@ TEST_F(CustomCodeTest, IfElseOperationSingleBit) { ASSERT_TRUE(complexEquality(amplitudes[1], 1, 0.0)); } +/** + * @test Exercise the backward-execution branch of the DD backend for a + * classic-controlled operation with a non-equality comparator. + * The measured value of `c` is 1, so `c > 0` triggers and `x q[1]` is + * applied on the forward pass; stepping backward undoes that `x q[1]`. + */ +TEST_F(CustomCodeTest, IfElseOperationBackwardStep) { + loadCode(2, 1, + "x q[0];" + "cx q[0], q[1];" + "measure q[0] -> c[0];" + "if(c>0) x q[1];"); + ASSERT_EQ(state->runSimulation(state), OK); + + std::array amplitudes{}; + Statevector sv{2, 4, amplitudes.data()}; + state->getStateVectorFull(state, &sv); + // q[1] = 0, q[0] = 1 after the `if` fires, so amplitude index 1. + ASSERT_TRUE(complexEquality(amplitudes[1], 1, 0.0)); + + ASSERT_EQ(state->stepBackward(state), OK); + state->getStateVectorFull(state, &sv); + // Undoing the `if`'s `x q[1]` restores q[1] = 1, q[0] = 1, i.e. index 3. + ASSERT_TRUE(complexEquality(amplitudes[3], 1, 0.0)); +} + /** * @test Test the `reset` instruction. */ From d86f5a0d7e6bca0b6d4dec3b9f01564eca6bdb1b Mon Sep 17 00:00:00 2001 From: rturrado Date: Sat, 5 Sep 2026 21:03:01 +0200 Subject: [PATCH 6/6] =?UTF-8?q?=F0=9F=90=9B=20Fix=20Windows=20build=20afte?= =?UTF-8?q?r=20qualified-auto=20change?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The iterator returned by `std::ranges::find_if` over `std::array` is a raw pointer on libstdc++ and libc++, but a class type (`_Array_const_iterator`) on MSVC STL. The earlier `const auto* const found` compiled on Linux; on MSVC it failed to deduce and cascaded into "cannot be used before it is initialized" errors on every variable that read from `found`. Replace the `find_if` + iterator pattern with a range-based `for` that stores the match in a `std::optional`. No iterator escapes the loop, so the code compiles cleanly under all three standard libraries, and no clang-tidy check fires. Assisted-by: Claude Opus 4.7 via Claude Code --- src/common/parsing/CodePreprocessing.cpp | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/common/parsing/CodePreprocessing.cpp b/src/common/parsing/CodePreprocessing.cpp index 0cc55411..19409a6f 100644 --- a/src/common/parsing/CodePreprocessing.cpp +++ b/src/common/parsing/CodePreprocessing.cpp @@ -419,7 +419,8 @@ parseClassicConditionExpression(const std::string& condition) { std::string_view text; qc::ComparisonKind kind; }; - static constexpr std::array OPERATORS{{ + using OperatorsT = std::array; + static constexpr OperatorsT OPERATORS{{ {.text = "<=", .kind = qc::Leq}, {.text = ">=", .kind = qc::Geq}, {.text = "==", .kind = qc::Eq}, @@ -428,16 +429,19 @@ parseClassicConditionExpression(const std::string& condition) { {.text = ">", .kind = qc::Gt}, }}; - const auto* const found = - std::ranges::find_if(OPERATORS, [&normalized](const auto& op) { - return normalized.find(op.text) != std::string::npos; - }); - if (found == OPERATORS.end()) { + std::optional match; + for (const auto& op : OPERATORS) { + if (normalized.find(op.text) != std::string::npos) { + match = op; + break; + } + } + if (!match.has_value()) { return std::nullopt; } - const auto opPos = normalized.find(found->text); + const auto opPos = normalized.find(match->text); const auto lhs = normalized.substr(0, opPos); - const auto rhs = normalized.substr(opPos + found->text.size()); + const auto rhs = normalized.substr(opPos + match->text.size()); if (lhs.empty() || rhs.empty()) { return std::nullopt; } @@ -478,13 +482,13 @@ parseClassicConditionExpression(const std::string& condition) { return ClassicCondition{.registerName = base, .bitIndex = bitIndex, .expectedValue = expected, - .kind = found->kind}; + .kind = match->kind}; } return ClassicCondition{.registerName = lhs, .bitIndex = std::nullopt, .expectedValue = expected, - .kind = found->kind}; + .kind = match->kind}; } std::optional