Repository navigation
N-ary operators with variables #248
Description
Activity
- addedAcceptedFor proposals, which were approved and will be implementedFor proposals, which were approved and will be implemented
on Mar 24, 2021 Raised while working out what has to be decided before 2.0: adding Σ, Π, ⋃ and ⋂ means adding
Entitysubtypes, and there is currently no stated policy on whether that is a breaking change. The answer decides more than this issue.What the hierarchy actually is
Entityis apublic abstract partial recordwithprotected abstract Entity[] InitDirectChildren(), and dozens of sealed subtypes underCore/Entity/. Because that member isprotectedrather thanprivate protected, the hierarchy is open — a consumer can derive fromEntitythemselves.That openness is what makes this subtle rather than obvious:
- It is not a compile break. A C# switch expression over an open hierarchy cannot be proven exhaustive, so the compiler already requires a
_arm (CS8509). Anyone matching on node types has one. - It is a runtime behaviour change for the code that matters most: a custom printer, evaluator, serializer or visitor that handles every node kind it knows about and throws or falls back on the rest. Those hit their fallback the first time a Σ node reaches them, and they hit it silently if the fallback is lenient.
So the cost lands on exactly the consumers who integrated most deeply.
Why this is a 2.0 question and not a #248 question
If "a new node type is breaking", then it may only ship in a major version — and that gates far more than this issue:
- AngouriMath has no rounding functions (floor, ceil, round), nor min, max or gcd #809 —
floor,ceil,round, and possiblymin/max/gcd - Functions as separate entities #286 — functions as separate entities
- Lambdas & functions as entities #495 — lambdas and functions as entities
All three add node types. Under a strict reading, none of them could ship in 2.1, and the roadmap would be waiting on 3.0 for years.
Suggested resolution
State the policy explicitly rather than leaving it to be inferred, and put it in
BREAKING-CHANGES.mdwhere consumers will read it. The version I would argue for:New
Entitynode types are additive and may ship in a minor version. Consumers matching on the node hierarchy must include a default arm and must not assume the set of node types is closed.That keeps the roadmap unblocked, and it is honest: the hierarchy is already open, so the guarantee never existed — it just was not written down. The alternative — declaring the node set closed and sealing the hierarchy with
private protected— is defensible too, but it should be a deliberate choice made now, because after 2.0 ships either answer is expensive to change.Not proposing a change here; this needs a maintainer decision.
- It is not a compile break. A C# switch expression over an open hierarchy cannot be proven exhaustive, so the compiler already requires a
- added 3 commits that reference this issue
on Aug 14, 2026 I set out to build the n-ary half of this — matching across a flattened chain,
a + b + cagainstx + y— and measured first. It buys implementation tidiness rather than capability, and the reason is worth writing down before someone spends a month on it.The library already AC-normalises, so binary matching is enough
The Pythagorean rule is binary:
Sumf(Powf(Sinf(a), 2), Powf(Cosf(a), 2)). So it cannot see the identity buried in a longer sum, where the tree isSumf(Sumf(1, sin²), cos²)and the two terms are not siblings. Except that it does:"1 + sin(x)^2 + cos(x)^2".Simplify() -> 2 "y + sin(x)^2 + cos(x)^2".Simplify() -> 1 + y "sin(x)^2 + y + cos(x)^2 + z".Simplify() -> 1 + y + z "x * y * (1/x)".Simplify() -> y provided not x = 0 "x * z * y / x".Simplify() -> y * z provided not x = 0 "(a+b) + c - (a+b)".Simplify() -> cThe mechanism is
CanonicalOrder, and it is visible directly:CanonicalOrderExact("sin(x)^2 + y + cos(x)^2") -> cos(x)^2 + sin(x)^2 + yIt sorts and groups the operands of a commutative chain, which makes the two trig terms adjacent — and in a left-associated tree, adjacent means siblings. The binary rule then matches at the inner node. Sorting before matching is doing the job AC-matching would do, for every case I could construct.
That is not an accident of these examples: within one commutative chain, a sort can always bring any two operands together, and across two different chains AC-matching would not apply either. A canonical order on a commutative operator is a complete substitute for AC-matching on that operator.
The caveat, which is where n-ary matching does earn its keep
The substitution only holds because the sort runs before the rules, in
Simplificator.simplifyChildren. A caller applying one rule set on its own gets no sort — and applying one rule set on its own is precisely what rules-as-data is for.That is the same finding
rulecheckreached from the other direction: eight cases acrossNumericNeat,PowerandCommoncycle when iterated alone and settle only onceInnerSimplifiedruns between passes. A rule set is half a rewrite system, and the other half is the normalisation. If individually addressable rules are going to be applied individually, they need to be self-sufficient, and n-ary matching is part of what self-sufficient means.So the sequencing I would suggest
- Commutative matching first — it is built and proven in Make a rewrite rule's left-hand side data, and prove it against the switch (#248, #746 v1.0) #938, one pattern replacing four hand-written arms, and it needs backtracking, which is the part that is easy to get wrong.
- N-ary matching when rules start being applied outside the pipeline, not before. Today it would be a reimplementation of what the sort already achieves, judged against a pipeline that would not use it.
And one thing to settle before any commutative rule replaces its
switcharms in the live pipeline: where two factors are shared,a*b + b*a, the four arms giveb*(a+a)and one commutative rule givesa*(b+b). Both are2ab. The tie-break is fixed today by the order the arms happen to be written in, and was never chosen deliberately — so it needs choosing, or printed answers move.- added 6 commits that reference this issue
on Aug 14, 2026 Measured on
masterata45a7256: genuinely unimplemented, recording it so the issue is dated rather than assumed.sum(i, i, 1, 10) -> UnhandledParseException product(i, i, 1, 5) -> UnhandledParseExceptionThere is no big-operator syntax and no node for one. Binary union and intersection exist as set operators, but that is the binary form rather than the n-ary-with-a-bound-variable this asks for.
Worth noting it shares its blocker with #225 and with #495's syntax half: all three need a binder in the grammar — a variable bound by an operator over a range or a domain — and the parser has no such construct today. Whichever is built first pays for that machinery, and the other two get much cheaper.
- added a commit that references this issue
on Aug 16, 2026 - added 6 commits that reference this issue
on Aug 24, 2026
Such operators as Σ, Π, ⋃, ⋂