Skip to content

N-ary operators with variables #248

Description

@WhiteBlackGoose

Such operators as Σ, Π, ⋃, ⋂

Activity

  1. added this to the 1.3 milestone on Oct 16, 2020
  2. added
    AcceptedFor proposals, which were approved and will be implemented
    on Mar 24, 2021
  3. Rafael-SOWNet commented on Aug 8, 2026

    @Rafael-SOWNet
    Member

    Raised while working out what has to be decided before 2.0: adding Σ, Π, ⋃ and ⋂ means adding Entity subtypes, 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

    Entity is a public abstract partial record with protected abstract Entity[] InitDirectChildren(), and dozens of sealed subtypes under Core/Entity/. Because that member is protected rather than private protected, the hierarchy is open — a consumer can derive from Entity themselves.

    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:

    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.md where consumers will read it. The version I would argue for:

    New Entity node 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.

  4. Rafael-SOWNet commented on Aug 14, 2026

    @Rafael-SOWNet
    Member

    I set out to build the n-ary half of this — matching across a flattened chain, a + b + c against x + 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 is Sumf(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()              ->  c
    

    The mechanism is CanonicalOrder, and it is visible directly:

    CanonicalOrderExact("sin(x)^2 + y + cos(x)^2")   ->   cos(x)^2 + sin(x)^2 + y
    

    It 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 rulecheck reached from the other direction: eight cases across NumericNeat, Power and Common cycle when iterated alone and settle only once InnerSimplified runs 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

    1. 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.
    2. 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 switch arms in the live pipeline: where two factors are shared, a*b + b*a, the four arms give b*(a+a) and one commutative rule gives a*(b+b). Both are 2ab. 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.

  5. Rafael-SOWNet commented on Aug 16, 2026

    @Rafael-SOWNet
    Member

    Measured on master at a45a7256: genuinely unimplemented, recording it so the issue is dated rather than assumed.

    sum(i, i, 1, 10)      -> UnhandledParseException
    product(i, i, 1, 5)   -> UnhandledParseException
    

    There 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    AcceptedFor proposals, which were approved and will be implemented

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions