Summary
Scalar and tensor-to-scalar both implement the (mul) * (mul) merge that promotes like factors to powers, but using different approaches:
| Domain |
Source |
Approach |
| Scalar |
src/numsim_cas/scalar/simplifier/scalar_simplifier_mul.cpp:100 (n_ary_mul::dispatch(scalar_mul)) |
Operator-chain: copy lhs into a new mul, then iterate rhs children and apply * via operator*. Each * re-enters the visitor pipeline and triggers full simplification. |
| T2s |
src/numsim_cas/tensor_to_scalar/simplifier/tensor_to_scalar_simplifier_mul.cpp:99 (mul_base::dispatch(tensor_to_scalar_mul)) |
Inlined merge via a domain-local push_or_combine helper (with try_fold_numeric_pow). |
Both produce correct results for the cases verified in tests/CoreBugFixTest.h::MulMulMergesLikeFactors{Scalar,T2s}. The architectural concern is drift: the two implementations can diverge in subtle ways (handling of zero children, pow promotion rules, etc.) over time, repeating the bug class that #91 was an instance of.
Surfaced during the #97 investigation — see PR #103.
What a consolidation would look like
Lift the merge into a generic n_ary_mul_mul_dispatch<Traits> in core/simplifier/simplifier_mul.h, paralleling n_ary_add_dispatch. The two approaches need to be reconciled:
- Scalar's operator-chain is conceptually simpler (delegates to existing simplification rules) but slower (O(N × simplification_cost) per merge).
- T2s's
push_or_combine is more efficient (O(N) with map lookups) but inlines logic that the operator-chain gets for free.
A unified n_ary_mul_mul_dispatch would need to either:
- Generalize
push_or_combine to be domain-agnostic — needs a Traits::combine(child, existing) hook.
- Stick with the operator-chain and accept the perf trade-off.
Either way, the rules that push_or_combine inlines (numeric pow folding, identity-multiplied terms) need explicit handling.
Acceptance criteria
Not blocking
The functional behavior is correct today; this is a code-organization refactor that closes a drift surface. Worth pursuing alongside #95's broader simplifier-unification work but not urgent.
References
Summary
Scalar and tensor-to-scalar both implement the
(mul) * (mul)merge that promotes like factors to powers, but using different approaches:src/numsim_cas/scalar/simplifier/scalar_simplifier_mul.cpp:100(n_ary_mul::dispatch(scalar_mul))*viaoperator*. Each*re-enters the visitor pipeline and triggers full simplification.src/numsim_cas/tensor_to_scalar/simplifier/tensor_to_scalar_simplifier_mul.cpp:99(mul_base::dispatch(tensor_to_scalar_mul))push_or_combinehelper (withtry_fold_numeric_pow).Both produce correct results for the cases verified in
tests/CoreBugFixTest.h::MulMulMergesLikeFactors{Scalar,T2s}. The architectural concern is drift: the two implementations can diverge in subtle ways (handling of zero children, pow promotion rules, etc.) over time, repeating the bug class that #91 was an instance of.Surfaced during the #97 investigation — see PR #103.
What a consolidation would look like
Lift the merge into a generic
n_ary_mul_mul_dispatch<Traits>incore/simplifier/simplifier_mul.h, parallelingn_ary_add_dispatch. The two approaches need to be reconciled:push_or_combineis more efficient (O(N) with map lookups) but inlines logic that the operator-chain gets for free.A unified
n_ary_mul_mul_dispatchwould need to either:push_or_combineto be domain-agnostic — needs aTraits::combine(child, existing)hook.Either way, the rules that
push_or_combineinlines (numeric pow folding, identity-multiplied terms) need explicit handling.Acceptance criteria
n_ary_mul_mul_dispatch<Traits>incore/simplifier/simplifier_mul.h.n_ary_mul::dispatch(scalar_mul)).push_or_combinehelper, or moves it into the generic layer).MulMulMergesLikeFactors{Scalar,T2s}continue to pass.push_or_combine).Not blocking
The functional behavior is correct today; this is a code-organization refactor that closes a drift surface. Worth pursuing alongside #95's broader simplifier-unification work but not urgent.
References
docs/simplifier-coverage.md— audit doc.