From 3eab1262173f71a061f73de20a44d32bd7a34a0e Mon Sep 17 00:00:00 2001 From: petlenz Date: Sun, 2 Aug 2026 22:35:06 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20#422:=20tensor=20sub=20simplifier=20round?= =?UTF-8?q?-7=20parity=20=E2=80=94=20route=20dispatch(zero)/dispatch(negat?= =?UTF-8?q?ive)=20through=20operator-?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: petlenz --- .../simplifier/tensor_simplifier_sub.cpp | 15 +++++++---- tests/CoreBugFixTest.h | 25 +++++++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/numsim_cas/tensor/simplifier/tensor_simplifier_sub.cpp b/src/numsim_cas/tensor/simplifier/tensor_simplifier_sub.cpp index 84e311d5..368eaf05 100644 --- a/src/numsim_cas/tensor/simplifier/tensor_simplifier_sub.cpp +++ b/src/numsim_cas/tensor/simplifier/tensor_simplifier_sub.cpp @@ -56,18 +56,23 @@ sub_base::expr_holder_t sub_base::dispatch(tensor const &) { return _rhs.accept(visitor); } -// 0 - expr +// 0 - expr (operator- so -0/-(-x) normalize — round-7 parity, issue #422: +// the round-7 fix covered scalar + t2s and missed this domain, so +// 0 - (-A) built negative(negative(A))) sub_base::expr_holder_t sub_base::dispatch(tensor_zero const &) { - if (is_same(m_rhs)) - return make_expression(m_rhs.get().dim(), m_rhs.get().rank()); - return make_expression(std::move(m_rhs)); + return -std::move(m_rhs); } // - expr_lhs - expr_rhs --> -(expr_lhs+expr_rhs) +// operator-, not a raw negative node (round-7 parity, issue #422): the sum +// may itself normalize to a negative or to zero, and wrapping either mints +// the nested-negative / negative(zero) shapes round-7 eliminated. The +// is_valid guard is tensor-specific (tensor add can yield an invalid +// holder on full cancellation) and stays. sub_base::expr_holder_t sub_base::dispatch(tensor_negative const &lhs) { auto expr{lhs.expr() + std::move(m_rhs)}; if (expr.is_valid()) { - return make_expression(expr); + return -std::move(expr); } return make_expression(lhs.dim(), lhs.rank()); } diff --git a/tests/CoreBugFixTest.h b/tests/CoreBugFixTest.h index d3bfcc6d..f45c0edd 100644 --- a/tests/CoreBugFixTest.h +++ b/tests/CoreBugFixTest.h @@ -1446,6 +1446,31 @@ TEST(RoundSevenReview, AddCancelsAgainstNegativeChild) { EXPECT_TRUE(*f == *(trace(A) + w(c4))) << to_string(f); } +// R7 tensor parity (issue #422): the round-7 fix covered scalar + t2s and +// missed the tensor domain — 0 - (-A) built negative(negative(A)) (found +// downstream: numsim-codegen's emitter turned it into invalid `--A`), and +// a negative-lhs sub whose sum normalizes could re-wrap into the nested +// shapes round-7 eliminated. Tensor sub_base now routes dispatch(zero) and +// dispatch(negative) through operator- like the other two domains. +TEST(RoundSevenReview, TensorSubNormalizesLikeScalarAndT2s) { + auto [A, B] = + make_tensor_variable(std::tuple{"A", std::size_t{3}, std::size_t{2}}, + std::tuple{"B", std::size_t{3}, std::size_t{2}}); + auto zero = make_expression(3, 2); + // 0 - (-A) --> A (was negative(negative(A))) + auto e1 = zero - (-A); + EXPECT_TRUE(*e1 == *A) << to_string(e1); + // 0 - 0 --> 0 (previous special case preserved through operator-) + auto e0 = zero - make_expression(3, 2); + EXPECT_TRUE(is_same(e0)) << to_string(e0); + // (-A) - (-A) --> 0, not negative(zero) + auto e2 = (-A) - (-A); + EXPECT_TRUE(is_same(e2)) << to_string(e2); + // (-A) - B --> -(A + B): unchanged for the ordinary case + auto e3 = (-A) - B; + EXPECT_TRUE(*e3 == *(-(A + B))) << to_string(e3); +} + // Round-8 review: regressions from the round-7 negation probe. // R8-1: merge_add consumed the same rhs child twice when the lhs held an