Sums do not canonicalize: 282 of 1728 triples over a 12-term corpus produce structurally unequal expressions depending only on how the sum is associated. All results are value-correct — this is canonical form, not arithmetic. Measured during #411 (2026-09-17).
Two root causes, neither of which is merge_add's reverse-probe hygiene (the part #411 names):
1. n_ary_tree::like_term_of's cross-type branch is dead code. Its final line implements exactly "c*T is a like term of T", but an early hash guard rejects the pair before reaching it, and the two hashes differ by construction (hash(x) = 2654435889, hash(2*x) = 173676443751). So find_like can never pair a bare term with its scaled form. x + 2x → 3x works only through the type dispatchers, and inside merge_add it does not: (x+y) + (2x+y) → x + 2*x + 2*y.
2. A negated term never merges with a scaled one, with no adds involved at all:
-x + 2x -> 2*x-x (expected x)
-x + -2x -> -2*x-x (expected -3x)
Both fixes are blocked on a design decision:
merge_add is shared (functions.h) and used by the tensor add simplifier too, so cause 1 applies in every domain.
Related: #414 (negative-of-add children are opaque) is the same canonicalization family; #379 (canonical form + hash-consing) is where this ultimately belongs, and #453 records why the lossy-hash/identity conflation must be resolved before interning.
Signed-off-by: petlenz peterlenz89.pl@gmail.com
Sums do not canonicalize: 282 of 1728 triples over a 12-term corpus produce structurally unequal expressions depending only on how the sum is associated. All results are value-correct — this is canonical form, not arithmetic. Measured during #411 (2026-09-17).
Two root causes, neither of which is
merge_add's reverse-probe hygiene (the part #411 names):1.
n_ary_tree::like_term_of's cross-type branch is dead code. Its final line implements exactly "c*Tis a like term ofT", but an early hash guard rejects the pair before reaching it, and the two hashes differ by construction (hash(x) = 2654435889,hash(2*x) = 173676443751). Sofind_likecan never pair a bare term with its scaled form.x + 2x → 3xworks only through the type dispatchers, and insidemerge_addit does not:(x+y) + (2x+y) → x + 2*x + 2*y.2. A negated term never merges with a scaled one, with no adds involved at all:
Both fixes are blocked on a design decision:
hash(c*T) == hash(T)matches the existing tensor rule, but changes hash-driven print order — the same blast radius that caused the hash attempt in Scalar and tensor symbols with the same name collide (name-only hash, per-domain ids): assert abort in debug, UB with NDEBUG #348/PR Fix #348: discriminate symbols across domains in expression identity #442 to be reverted (measured there: 7–43 print-pinned tests flip depending on the discriminator).find_likeavoids the hash change but makes sum-building O(n²), which contains_expression re-traverses shared DAG nodes — exponential for nested trace(t)*t #441 already flags as a live cost.mul_add_dispatchcarries the comment "a map find would alias child x+2 against x". Any relaxation must be restricted to mul-type trees, not applied globally.merge_addis shared (functions.h) and used by the tensor add simplifier too, so cause 1 applies in every domain.Related: #414 (negative-of-add children are opaque) is the same canonicalization family; #379 (canonical form + hash-consing) is where this ultimately belongs, and #453 records why the lossy-hash/identity conflation must be resolved before interning.
Signed-off-by: petlenz peterlenz89.pl@gmail.com