Skip to content

Commit 7b086e0

Browse files
committed
Round-9 review fixes: nested adds from negative/sub fallthroughs, unconverted tensor insert, zero coeff
Round 9 (adversarial probe of the round-8 delta) confirmed three value-preserving structural defects: - Nested adds: add + (-t) for a non-cancelling t, and add - expr in both the core and tensor sub get_default, pushed the WHOLE lhs add as a single child. (C-A)-B built tensor_add{-B, tensor_add{C,-A}}, and buried children then defeated merge cancellation: (A+B)+((C-A)-B) kept an unreachable interior -A ('A+C-A'). The negative dispatch and both get_defaults now copy the add and signed-insert the other operand, finishing through the usual collapse. A tensor print expectation that had pinned the nested shape's output ordering was updated to the flat form. - The tensor n-ary template dispatch still inserted its combined term via plain merge_or_insert: (5A-2A)+(-3A) held an exact {2A,-(2A)} pair instead of collapsing to zero. Now add_insert_signed, matching the scalar/t2s merge_and_finish. - merge_add stored a cancelled coefficient as a literal zero holder: (2+x)+(y-2) printed '0+x+y' and compared unequal to x+y. Cancelled coefficients are now dropped. Also converted every remaining dispatcher-side merge_or_insert in simplifier_sub.h/simplifier_add.h to the shared insert_signed helper (exact-negation combining + zero filter) so no insertion path can leave a {t,-t} pair or a zero child. Regression tests in CoreBugFixTest (RoundNineReview suite). Refs #340. Signed-off-by: petlenz <peterlenz89.pl@gmail.com>
1 parent d764b37 commit 7b086e0

8 files changed

Lines changed: 133 additions & 20 deletions

File tree

include/numsim_cas/core/simplifier/simplifier_add.h

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,12 @@ class n_ary_add_dispatch
423423
return base::m_lhs - rhs.expr();
424424
}
425425

426-
return base::get_default();
426+
// non-cancelling -t: insert into a copy — get_default would nest the
427+
// whole lhs add as a single child (round-9 review)
428+
auto add_expr{make_expression<typename Traits::add_type>(lhs)};
429+
auto &add{add_expr.template get<typename Traits::add_type>()};
430+
insert_signed<Traits>(add, base::m_rhs);
431+
return detail::finalize_add<Traits>(std::move(add_expr));
427432
}
428433

429434
protected:
@@ -560,7 +565,7 @@ class negative_add_dispatch
560565
auto &add{add_expr.template get<typename Traits::add_type>()};
561566
// (-x) + (y - x): the map may already hold -x, so merge instead of a
562567
// raw push_back (which asserts on duplicates)
563-
add.merge_or_insert(base::m_lhs);
568+
insert_signed<Traits>(add, base::m_lhs);
564569
return detail::finalize_add<Traits>(std::move(add_expr));
565570
}
566571

include/numsim_cas/core/simplifier/simplifier_common.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <numsim_cas/core/domain_traits.h>
55
#include <numsim_cas/core/expression_holder.h>
66
#include <numsim_cas/core/scalar_number.h>
7+
#include <numsim_cas/functions.h>
78

89
namespace numsim::cas::detail {
910

@@ -44,6 +45,18 @@ finalize_add(expression_holder<typename Traits::expression_type> expr) {
4445
return expr;
4546
}
4647

48+
// merge_or_insert with exact-negation combining and zero filtering:
49+
// dispatcher-side child insertion must never leave a {t, -t} pair or a
50+
// literal zero child (round-8/9 reviews).
51+
template <typename Traits>
52+
inline void insert_signed(typename Traits::add_type &add,
53+
typename Traits::expr_holder_t entry) {
54+
add_insert_signed(add, std::move(entry),
55+
[](typename Traits::expr_holder_t const &e) {
56+
return is_same<typename Traits::zero_type>(e);
57+
});
58+
}
59+
4760
// Fold a numeric delta into the coefficient of `expr` (an add node).
4861
// A valid-but-non-numeric coefficient (symbolic, t2s) is combined as an
4962
// expression — get_coefficient reports 0 for it, and the old free()/

include/numsim_cas/core/simplifier/simplifier_sub.h

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ class sub_dispatch {
4141
return Traits::make_constant(result);
4242
}
4343

44+
if (is_same<add_type>(m_lhs)) {
45+
// copy children — pushing the whole add would nest it (round-9)
46+
auto add_expr{make_expression<add_type>(m_lhs.template get<add_type>())};
47+
if (rhs_val) {
48+
return fold_constant_into_add_coeff<Traits>(std::move(add_expr),
49+
-(*rhs_val));
50+
}
51+
auto &add{add_expr.template get<add_type>()};
52+
insert_signed<Traits>(add, -m_rhs);
53+
return finalize_add<Traits>(std::move(add_expr));
54+
}
55+
4456
auto add_new{make_expression<add_type>()};
4557
auto &add{add_new.template get<add_type>()};
4658
if (lhs_val) {
@@ -54,7 +66,7 @@ class sub_dispatch {
5466
} else {
5567
// -m_rhs may collapse onto m_lhs (z - (-z)); a raw push_back
5668
// would hit the no-duplicates assert
57-
add.merge_or_insert(-m_rhs);
69+
insert_signed<Traits>(add, -m_rhs);
5870
}
5971
return finalize_add<Traits>(std::move(add_new));
6072
}
@@ -99,7 +111,7 @@ class negative_sub_dispatch
99111
expr_holder_t dispatch(typename Traits::add_type const &rhs) {
100112
auto add_expr{make_expression<typename Traits::add_type>(rhs)};
101113
auto &add{add_expr.template get<typename Traits::add_type>()};
102-
add.merge_or_insert(lhs.expr());
114+
insert_signed<Traits>(add, lhs.expr());
103115
return make_expression<typename Traits::negative_type>(std::move(add_expr));
104116
}
105117

@@ -145,7 +157,7 @@ class constant_sub_dispatch
145157
// call operator- on an invalid holder and throw. Mirrors the guard
146158
// pattern in n_ary_sub_dispatch::dispatch(add_type) from PR #98.
147159
//
148-
// Children are negated and pushed via merge_or_insert (not push_back)
160+
// Children are negated and inserted via insert_signed (not push_back)
149161
// so a `-child` that collides with another entry in the result is
150162
// combined rather than throwing duplicate-child internal_error.
151163
//
@@ -163,7 +175,7 @@ class constant_sub_dispatch
163175
add.set_coeff(base::m_lhs);
164176
}
165177
for (auto &child : rhs.symbol_map() | std::views::values) {
166-
add.merge_or_insert(-child);
178+
insert_signed<Traits>(add, -child);
167179
}
168180
return finalize_add<Traits>(std::move(add_expr));
169181
}
@@ -258,15 +270,15 @@ class n_ary_sub_dispatch
258270
used_expr.insert(pos->second);
259271
auto combined = child - pos->second;
260272
if (!is_same<typename Traits::zero_type>(combined))
261-
add.merge_or_insert(std::move(combined));
273+
insert_signed<Traits>(add, std::move(combined));
262274
} else {
263-
add.merge_or_insert(child);
275+
insert_signed<Traits>(add, child);
264276
}
265277
}
266278
if (used_expr.size() != rhs.size()) {
267279
for (auto &child : rhs.symbol_map() | std::views::values) {
268280
if (!used_expr.count(child)) {
269-
add.merge_or_insert(-child);
281+
insert_signed<Traits>(add, -child);
270282
}
271283
}
272284
}
@@ -289,10 +301,10 @@ class n_ary_sub_dispatch
289301
auto combined{pos->second - base::m_rhs};
290302
add.symbol_map().erase(pos);
291303
if (!is_same<typename Traits::zero_type>(combined))
292-
add.merge_or_insert(std::move(combined));
304+
insert_signed<Traits>(add, std::move(combined));
293305
return finalize_add<Traits>(std::move(expr_add));
294306
}
295-
add.merge_or_insert(-base::m_rhs);
307+
insert_signed<Traits>(add, -base::m_rhs);
296308
return finalize_add<Traits>(std::move(expr_add));
297309
}
298310

include/numsim_cas/functions.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,12 @@ constexpr inline void merge_add(n_ary_tree<Derived> const &lhs,
4848
using expr_t = typename Derived::expr_t;
4949

5050
if (lhs.coeff().is_valid() && rhs.coeff().is_valid()) {
51-
result.set_coeff(lhs.coeff() + rhs.coeff());
51+
// cancelled coefficients must be dropped, not stored as a literal
52+
// zero ((2+x)+(y-2) printed "0+x+y", round-9 review)
53+
auto coeff{lhs.coeff() + rhs.coeff()};
54+
if (!is_zero(coeff)) {
55+
result.set_coeff(std::move(coeff));
56+
}
5257
} else {
5358
if (lhs.coeff().is_valid()) {
5459
result.set_coeff(lhs.coeff());

include/numsim_cas/tensor/simplifier/tensor_simplifier_sub.h

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include <numsim_cas/basic_functions.h>
55
#include <numsim_cas/core/operators.h>
6+
#include <numsim_cas/functions.h>
67
#include <numsim_cas/tensor/tensor_definitions.h>
78
#include <numsim_cas/tensor/tensor_expression.h>
89
#include <numsim_cas/tensor/tensor_std.h>
@@ -32,12 +33,30 @@ class sub_default : public tensor_visitor_return_expr_t {
3233
#undef NUMSIM_ADD_OVR
3334

3435
// rhs is negative
35-
auto get_default() {
36+
expr_holder_t get_default() {
37+
// copy an add lhs (pushing it wholesale would nest, round-9 review);
38+
// signed insert so X-(-X) combines instead of hitting the dup assert
3639
auto add_new{
37-
make_expression<tensor_add>(m_lhs.get().dim(), m_lhs.get().rank())};
40+
is_same<tensor_add>(m_lhs)
41+
? make_expression<tensor_add>(m_lhs.template get<tensor_add>())
42+
: make_expression<tensor_add>(m_lhs.get().dim(),
43+
m_lhs.get().rank())};
3844
auto &add{add_new.template get<tensor_add>()};
39-
add.push_back(m_lhs);
40-
add.push_back(-m_rhs);
45+
if (!is_same<tensor_add>(m_lhs)) {
46+
add.push_back(m_lhs);
47+
}
48+
add_insert_signed(add, -m_rhs, [](expr_holder_t const &e) {
49+
return is_same<tensor_zero>(e);
50+
});
51+
add.invalidate_hash();
52+
add.recompute_space();
53+
if (add.size() == 0) {
54+
return make_expression<tensor_zero>(m_lhs.get().dim(),
55+
m_lhs.get().rank());
56+
}
57+
if (add.size() == 1 && !add.coeff().is_valid()) {
58+
return add.symbol_map().begin()->second;
59+
}
4160
return add_new;
4261
}
4362

src/numsim_cas/tensor/simplifier/tensor_simplifier_add.cpp

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ n_ary_add::dispatch([[maybe_unused]] Expr const &rhs) {
5454
auto combined{pos->second + m_rhs};
5555
add.symbol_map().erase(pos);
5656
if (!is_same<tensor_zero>(combined)) {
57-
add.merge_or_insert(std::move(combined));
57+
// signed insert: the combined term may exactly negate another child
58+
// ((5A-2A)+(-3A) left a {2A,-(2A)} pair, round-9 review)
59+
add_insert_signed(add, std::move(combined), [](expr_holder_t const &e) {
60+
return is_same<tensor_zero>(e);
61+
});
5862
}
5963
add.invalidate_hash();
6064
add.recompute_space();
@@ -104,7 +108,22 @@ n_ary_add::dispatch(tensor_negative const &rhs) {
104108
}
105109
return expr;
106110
}
107-
return get_default();
111+
// non-cancelling -t: insert into a copy — get_default would nest the
112+
// whole lhs add as a single child (round-9 review)
113+
auto expr{make_expression<tensor_add>(m_lhs_node)};
114+
auto &add{expr.template get<tensor_add>()};
115+
add_insert_signed(add, m_rhs, [](expr_holder_t const &e) {
116+
return is_same<tensor_zero>(e);
117+
});
118+
add.invalidate_hash();
119+
add.recompute_space();
120+
if (add.size() == 0) {
121+
return tensor_traits::zero(m_lhs);
122+
}
123+
if (add.size() == 1 && !add.coeff().is_valid()) {
124+
return add.symbol_map().begin()->second;
125+
}
126+
return expr;
108127
}
109128

110129
// ------------------------------------------------------------

tests/CoreBugFixTest.h

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1485,6 +1485,45 @@ TEST(RoundEightReview, TensorMergeAddZeroFiltered) {
14851485
EXPECT_TRUE(is_same<tensor_zero>(e3)) << to_string(e3);
14861486
}
14871487

1488+
// Round-9 review: nested adds from the negative fall-through, one
1489+
// unconverted tensor insert, and a literal zero coefficient.
1490+
1491+
// R9-1: add + (-t) with a non-cancelling t fell to get_default, which
1492+
// nested the whole lhs add as a single child; buried terms then defeated
1493+
// merge cancellation.
1494+
TEST(RoundNineReview, AddNegativeStaysFlat) {
1495+
auto [x, y] = make_scalar_variable("x", "y");
1496+
auto e = (x + 5.0 * y) + (-y);
1497+
// flat children: the exact 5*y child stays reachable for cancellation
1498+
auto e2 = e + (-(5.0 * y));
1499+
EXPECT_TRUE(*e2 == *(x - y)) << to_string(e2);
1500+
auto [A, B, C] = make_tensor_variable(std::tuple{"A", std::size_t{3}, 2},
1501+
std::tuple{"B", std::size_t{3}, 2},
1502+
std::tuple{"C", std::size_t{3}, 2});
1503+
auto t = (A + B) + ((C - A) - B);
1504+
EXPECT_TRUE(*t == *C) << to_string(t);
1505+
EXPECT_TRUE(is_same<tensor_zero>(t - C)) << to_string(t - C);
1506+
}
1507+
1508+
// R9-2: the tensor n-ary fallback still plain-inserted the combined term;
1509+
// (5A-2A)+(-3A) held an exact {2A, -(2A)} pair instead of collapsing.
1510+
TEST(RoundNineReview, TensorCombinedInsertIsSigned) {
1511+
auto [A] = make_tensor_variable(std::tuple{"A", std::size_t{3}, 2});
1512+
auto e = (5.0 * A + (-(2.0 * A))) + (-3.0) * A;
1513+
EXPECT_TRUE(is_same<tensor_zero>(e)) << to_string(e);
1514+
}
1515+
1516+
// R9-3: merge_add stored a cancelled coefficient as a literal zero —
1517+
// (2+x)+(y-2) printed "0+x+y" and compared unequal to x+y.
1518+
TEST(RoundNineReview, MergeAddDropsCancelledCoeff) {
1519+
auto [x, y] = make_scalar_variable("x", "y");
1520+
auto e = (2.0 + x) + (y - 2.0);
1521+
EXPECT_TRUE(*e == *(x + y)) << to_string(e);
1522+
auto [A] = make_tensor_variable(std::tuple{"A", std::size_t{3}, 2});
1523+
auto t = (2.0 + trace(A)) + (det(A) - 2.0);
1524+
EXPECT_TRUE(*t == *(trace(A) + det(A))) << to_string(t);
1525+
}
1526+
14881527
} // namespace numsim::cas
14891528

14901529
#endif // COREBUGFIXTEST_H

tests/TensorExpressionTest.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -438,8 +438,9 @@ TYPED_TEST(TensorExpressionTest, AddNaryPlusNegativeCancellation) {
438438
EXPECT_PRINT((X + Y) + (-X), "Y");
439439
EXPECT_PRINT((X + Y) + (-Y), "X");
440440
EXPECT_PRINT((X + Y + Z) + (-Y), "X+Z");
441-
// negative not in sum → default add (hash-ordered output)
442-
EXPECT_PRINT((X + Y) + (-Z), "-Z+X+Y");
441+
// negative not in sum → flat insert into the copy (round-9: the old
442+
// fallback nested the whole lhs add, which printed as "-Z+X+Y")
443+
EXPECT_PRINT((X + Y) + (-Z), "X+Y-Z");
443444
}
444445

445446
// n_ary_add: merge two adds: (X+Y) + (Y+Z) → X+2*Y+Z

0 commit comments

Comments
 (0)