You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
n_ary_sub_dispatch::dispatch(typename Traits::add_type const &rhs) — the "merge two add expressions" branch in include/numsim_cas/core/simplifier/simplifier_sub.h — has two pre-existing defects that surface when both operands are add expressions with the same coefficient state.
Discovered during the PR #90 review while routing the other sub-side dispatchers through n_ary_tree::merge_or_insert. Out of scope for that PR; filing here for follow-up.
Repro
auto [x, y, z] = make_scalar_variable("x", "y", "z");
auto lhs = x + y + z; // scalar_add, no constant coefficientauto rhs = x + y; // scalar_add, no constant coefficientauto diff = lhs - rhs; // throws invalid_expression_error
(x+y+z) - (x+y) should simplify to z. Instead it throws "expression_holder: access to invalid (null) expression".
Root cause
The dispatch body, abbreviated:
expr_holder_tdispatch(typename Traits::add_type const &rhs) {
auto expr{make_expression<typename Traits::add_type>()};
auto &add{expr.templateget<typename Traits::add_type>()};
add.set_coeff(lhs.coeff() + rhs.coeff()); // <-- defect 1 and 2
...
for (auto &child : lhs.symbol_map() | std::views::values) {
auto pos{rhs.symbol_map().find(child)};
if (pos != rhs.symbol_map().end()) {
add.merge_or_insert(child + pos->second); // <-- defect 2 (also here)
} else {
add.merge_or_insert(child);
}
}
...
}
Defect 1 — unguarded coefficient addition
lhs.coeff() + rhs.coeff() is only safe when both holders are valid. When both adds have no constant term (their default state after x + y + z style construction), both coeffs are default-constructed (invalid). tag_invoke(add_fn, invalid, invalid) calls lhs.get<scalar_visitable_t>() which throws invalid_expression_error.
The symmetric routine in functions.h already handles this correctly:
The sub dispatch should use the same shape (with - semantics — see defect 2).
Defect 2 — wrong sign for subtraction
The function lives in simplifier_sub.h and constructs the result of lhs_add - rhs_add. Mathematically:
(c_l + a + b) - (c_r + a + d) = (c_l - c_r) + 0 + b - d
= (c_l - c_r) + b - d
Coefficient should be lhs.coeff() - rhs.coeff(), not +.
Child-child combination (when both add children share a key) should be child - pos->second, not child + pos->second.
Children present only in rhs should be added as -child, not child.
The current implementation looks like a copy-paste from the corresponding add routine that was never converted to subtraction semantics.
Why existing tests don't catch this
Two factors:
The combination of (a) both LHS and RHS being add types and (b) both having invalid coefficients isn't exercised by any current named test. The fuzz suite may have hit it intermittently but cas_error is caught and the seed silently skipped.
The +-instead-of-- defect produces a wrong but non-throwing result on most inputs (e.g. (2+x) - (1+x) would compute 3 + 2x instead of 1), but no test asserts the value of a 2-add subtraction whose result depends on the merge path.
Fix sketch
expr_holder_tdispatch(typename Traits::add_type const &rhs) {
auto expr{make_expression<typename Traits::add_type>()};
auto &add{expr.templateget<typename Traits::add_type>()};
// Coefficient: c_l - c_r, guarded against invalid sides.if (lhs.coeff().is_valid() && rhs.coeff().is_valid()) {
add.set_coeff(lhs.coeff() - rhs.coeff());
} elseif (lhs.coeff().is_valid()) {
add.set_coeff(lhs.coeff());
} elseif (rhs.coeff().is_valid()) {
add.set_coeff(-rhs.coeff());
}
std::set<expr_holder_t> used_expr;
for (auto &child : lhs.symbol_map() | std::views::values) {
auto pos{rhs.symbol_map().find(child)};
if (pos != rhs.symbol_map().end()) {
used_expr.insert(pos->second);
add.merge_or_insert(child - pos->second); // was `+`
} else {
add.merge_or_insert(child);
}
}
if (used_expr.size() != rhs.size()) {
for (auto &child : rhs.symbol_map() | std::views::values) {
if (!used_expr.count(child)) {
add.merge_or_insert(-child); // was `child`
}
}
}
return expr;
}
Acceptance criteria
(x+y+z) - (x+y) simplifies to z without throwing.
(2+x) - (1+x) simplifies to 1.
(a + b) - (a + b) simplifies to 0.
Deterministic regression test added to CoreBugFixTest covering at least the three cases above (replaces the marker comment near SubSymbolDispatchSmoke in tests/CoreBugFixTest.h).
Same fix applied symmetrically to the t2s and tensor specializations if they have parallel implementations.
Summary
n_ary_sub_dispatch::dispatch(typename Traits::add_type const &rhs)— the "merge two add expressions" branch ininclude/numsim_cas/core/simplifier/simplifier_sub.h— has two pre-existing defects that surface when both operands areaddexpressions with the same coefficient state.Discovered during the PR #90 review while routing the other sub-side dispatchers through
n_ary_tree::merge_or_insert. Out of scope for that PR; filing here for follow-up.Repro
(x+y+z) - (x+y)should simplify toz. Instead it throws"expression_holder: access to invalid (null) expression".Root cause
The dispatch body, abbreviated:
Defect 1 — unguarded coefficient addition
lhs.coeff() + rhs.coeff()is only safe when both holders are valid. When both adds have no constant term (their default state afterx + y + zstyle construction), both coeffs are default-constructed (invalid).tag_invoke(add_fn, invalid, invalid)callslhs.get<scalar_visitable_t>()which throwsinvalid_expression_error.The symmetric routine in
functions.halready handles this correctly:The sub dispatch should use the same shape (with
-semantics — see defect 2).Defect 2 — wrong sign for subtraction
The function lives in
simplifier_sub.hand constructs the result oflhs_add - rhs_add. Mathematically:lhs.coeff() - rhs.coeff(), not+.child - pos->second, notchild + pos->second.rhsshould be added as-child, notchild.The current implementation looks like a copy-paste from the corresponding add routine that was never converted to subtraction semantics.
Why existing tests don't catch this
Two factors:
addtypes and (b) both having invalid coefficients isn't exercised by any current named test. The fuzz suite may have hit it intermittently butcas_erroris caught and the seed silently skipped.+-instead-of--defect produces a wrong but non-throwing result on most inputs (e.g.(2+x) - (1+x)would compute3 + 2xinstead of1), but no test asserts the value of a 2-add subtraction whose result depends on the merge path.Fix sketch
Acceptance criteria
(x+y+z) - (x+y)simplifies tozwithout throwing.(2+x) - (1+x)simplifies to1.(a + b) - (a + b)simplifies to0.CoreBugFixTestcovering at least the three cases above (replaces the marker comment nearSubSymbolDispatchSmokeintests/CoreBugFixTest.h).References
bf75393.simplifier_sub.hat the affected dispatch.tests/CoreBugFixTest.hnearSubSymbolDispatchSmoke.merge_addininclude/numsim_cas/functions.h.