Summary
constant_mul::dispatch(tensor_to_scalar_mul) (src/numsim_cas/tensor_to_scalar/simplifier/tensor_to_scalar_simplifier_mul.cpp:31-44) silently drops a non-numeric scalar_wrapper LHS when multiplying into an existing product (reproduced at 0f6c0b9):
// x scalar symbol, A tensor
wrapper(x) * (trace(A) * det(A)) // → "1*det(A)*tr(A)" — x has vanished, coeff reset to 1
When lhs_val (numeric extraction) is nullopt, the code computes coeff{get_coefficient(mul,1)}, skips the coeff * *lhs_val merge, and never inserts m_lhs into the product. The promoted path x * (f*g) happens to route through mul_base::dispatch and is correct — which is why tests miss it; the bug fires whenever the wrapper is the visitor's LHS (e.g. (x*f)*g built in an order that puts the wrapper first, or internal simplifier calls).
Severity: high (silent factor loss). Confirmed. Related: #104 (mul×mul merge consolidation — this is a concrete drift instance of that risk).
Fix
In the !lhs_val branch, insert m_lhs as a child (push_or_combine(mul, m_lhs), merging with an existing scalar_wrapper child the way n_ary_add::dispatch(scalar_wrapper) does) instead of touching only the numeric coefficient.
Tests (lock-in)
TEST(T2sConstantMul, SymbolicWrapperFactorSurvives) {
auto [x] = make_scalar_variable("x");
auto [A] = make_tensor_variable(std::tuple{"A", 3, 2});
auto f = trace(A) * det(A);
auto e = /* wrapper-first product, e.g. via promote/explicit wrapper */ wrap(x) * f;
// evaluate with x=2, A=diag(1,2,3): trace=6, det=6 → expect 72, not 36
EXPECT_NE(to_string(e).find("x"), std::string::npos);
}
(Construct the wrapper-first path the same way the probe did — building tensor_to_scalar_scalar_wrapper(x) explicitly and multiplying it by an existing tensor_to_scalar_mul — so the regression can't dodge through the promoted route.)
Summary
constant_mul::dispatch(tensor_to_scalar_mul)(src/numsim_cas/tensor_to_scalar/simplifier/tensor_to_scalar_simplifier_mul.cpp:31-44) silently drops a non-numericscalar_wrapperLHS when multiplying into an existing product (reproduced at 0f6c0b9):When
lhs_val(numeric extraction) isnullopt, the code computescoeff{get_coefficient(mul,1)}, skips thecoeff * *lhs_valmerge, and never insertsm_lhsinto the product. The promoted pathx * (f*g)happens to route throughmul_base::dispatchand is correct — which is why tests miss it; the bug fires whenever the wrapper is the visitor's LHS (e.g.(x*f)*gbuilt in an order that puts the wrapper first, or internal simplifier calls).Severity: high (silent factor loss). Confirmed. Related: #104 (mul×mul merge consolidation — this is a concrete drift instance of that risk).
Fix
In the
!lhs_valbranch, insertm_lhsas a child (push_or_combine(mul, m_lhs), merging with an existing scalar_wrapper child the wayn_ary_add::dispatch(scalar_wrapper)does) instead of touching only the numeric coefficient.Tests (lock-in)
(Construct the wrapper-first path the same way the probe did — building
tensor_to_scalar_scalar_wrapper(x)explicitly and multiplying it by an existingtensor_to_scalar_mul— so the regression can't dodge through the promoted route.)