Repository navigation
Add sum and product, the first operators that bind a variable over a range (#248) - #966
Merged
Merged
Conversation
…range (#248) sum(k, k, 1, 10) 55 product(k, k, 1, 5) 120 sum(k^2, k, 1, 4) 30 sum(k, k, 1, n) sum(k, k, 1, n) -- carried, not refused sum(x, k, 1, 3) 3 * x sum(k, k, 5, 1) 0 -- empty sum product(k, k, 5, 1) 1 -- empty product Nodes rather than a parser trick, because the bounds may be symbolic and sum(k, k, 1, n) is a well-formed expression with no finite expansion. Where the bounds are concrete integers and there are few enough terms it expands; otherwise it is carried. The cap is 100 terms: a thousand-term expansion is correct and useless, and everything downstream then walks it. The empty range returns the operator's identity explicitly -- 0 for a sum, 1 for a product -- rather than letting the accumulator's initial value fall out of the loop, where it would read as a coincidence rather than a decision. **The index is bound**, which is the whole point of the exercise and the part that is easy to get wrong: Substitute alpha-renames before substituting, exactly as Integralf does, so sum(k, k, 1, n).Substitute("k", 5) is unchanged while .Substitute("n", 3) gives 6. #878 was this bug for set-builders. Consistent with the existing binders rather than inventing conventions: Vars exposes the index as integral and lambda do; an un-invertible binder answers the empty set as apply(f, x) does; differentiating or taking a limit of one returns the unevaluated operator rather than NaN, which is #958's lesson. i is the imaginary unit, so it cannot be an index -- sum(i, i, 1, 10) is left unevaluated rather than guessed at. Documented on the parameter and pinned by a test, since every textbook writes this sum with i. Suite 7297 passed, 0 failed, 17 new tests. PublicApi.txt regenerated.
Member
|
It might be desirable to allow redefining how Also, refer to WolframAlpha and Sympy if the expression should be at front or end; we write the iteration index and limit in the front in math. |
This was referenced Aug 17, 2026
Rafael-SOWNet
added a commit
that referenced
this pull request
Aug 17, 2026
`sum(i, i, 1, 10)` summed nothing. `i` is the imaginary unit, and that is decided in the lexer -- `NUMBER: ... | 'i'` -- so it never reaches the rule that makes variables and cannot be one anywhere in the language. The index arrived as a number, the operator had no variable to bind, and the whole thing stayed unevaluated. #966 documented that as a trap; Happypig375 asked for it not to be one. sum(i, i, 1, 10) unevaluated -> 55 product(i, i, 1, 5) unevaluated -> 120 sum(i ^ 2, i, 1, 4) unevaluated -> 30 Naming i as the index is a statement about the whole operator, so it is honoured through the summand and the bounds and not only in the index position. Doing it there alone would be worse than leaving it alone: the index would become a variable while every i in the summand stayed the imaginary unit, nothing would substitute, and sum(i, i, 1, 10) would answer 10i -- a wrong answer in place of an unevaluated one. And only inside the operator that declares it. These are unchanged, and they are what makes the above a fix rather than a new defect: sum(i * k, k, 1, 3) 6i sum(k + i, k, 1, 2) 3 + 2i sum(i, i, 1, 3) + i 6 + i i * i -1 sqrt(-1) i In MathS.Sum and MathS.Product rather than in the grammar. The first attempt was a grammar action, which fixed `"sum(i, i, 1, 10)".ToEntity()` and left `MathS.Sum("i", "i", 1, 10)` broken -- one library with two answers, since the string conversion is the only thing the parser sees. Putting it at the factory covers both, and leaves the parser untouched, so there is no regeneration and nothing for the grammar-drift check from #965 to catch. The class remark on SummationProductTest taught the trap and now records that it is gone. The test that pinned the old answer is replaced by cases for both halves -- the index being read, and i still being the imaginary unit everywhere else. Suite 7318 passed, 0 failed, 14 skipped. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Rafael-SOWNet
added a commit
that referenced
this pull request
Aug 17, 2026
`i` lexes as the imaginary unit -- the NUMBER rule in AngouriMath.g ends `| 'i'` -- so `sum(i, i, 1, 10)`, which is how a textbook writes that sum, arrived with a number in the index position, bound nothing, and was carried unevaluated. #966 documented and pinned that, which was honest but is not what a reader means by it. An index is a name and the imaginary unit is not one, so no reading of the old behaviour has the caller meaning the constant: the declaration is now taken seriously and shadows it inside the body. sum(i, i, 1, 10) carried -> 55 product(i, i, 1, 4) carried -> 24 sum(2i, i, 1, 3) carried -> 12 sum(2 + 3i, i, 1, 2) carried -> 13 sum(i, k, 1, 3) 3i -> 3i -- i was not declared here sum(sqrt(-1), i, 1, 3) carried -> 3i -- denotes it without naming it sum(i, i, 1, i) carried -> carried, i still the constant in the bound It is the name that is shadowed rather than the value, which decides the three cases that are not the obvious one. A pure-imaginary literal is rewritten because `2i` is a single number token: reading `sum(2i, i, 1, 3)` as `6i` while `sum(2 * i, i, 1, 3)` is `12` would answer one expression two ways. `sqrt(-1)` names nothing and keeps its value. The bounds sit outside the binder, in the scope the declaration is made in, so the constant survives there and a range ending at it is carried for want of an integer bound. In MathS.Sum and MathS.Product rather than in the grammar action, so that the parser and `MathS.Sum("i", "i", 1, 10)` agree -- `"i"` becomes an Entity by being parsed, so the C# call arrives exactly as the text does -- and so the parser needs no regeneration. The test that pinned the old answer is replaced rather than loosened: the answer got better. The round-trip theory gains a shadowed index, which only closes because parsing the printed form shadows it again. Suite 7323 passed, 0 failed. No BREAKING-CHANGES entry: sum and product are in no released version, since v2.2.0 predates #966, so no observable answer changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #248 for Σ and Π. The n-ary set operators (⋃, ⋂) are not included — see below.
Nodes, not a parser trick
The bounds may be symbolic, and
sum(k, k, 1, n)is a well-formed expression with no finite expansion. A parser-level version that only worked for concrete bounds would have to change shape later, so this isSummationfandProductfasCalculusOperators alongsideIntegralfandLimitf.Where the bounds are concrete integers and there are at most 100 terms it expands; otherwise it is carried. A thousand-term expansion is correct and useless, and everything downstream then walks it.
The empty range returns the operator's identity explicitly —
0for a sum,1for a product — rather than letting the accumulator's initial value fall out of the loop, where it would read as a coincidence rather than a decision.The index is bound
This is the part that is easy to get wrong, and #878 is it going wrong for set-builders.
Substitutealpha-renames before substituting, exactly asIntegralfdoes:Both are pinned by tests.
Consistent with the binders already here, rather than inventing conventions
I checked each against
integral,lambda,applyandConditionalSetbefore choosing:sum/productVarsexposes the indexintegral(y, x)→y,x;lambda(x, x^2)→x{ }apply(f, x)→{ }NaNDomainConditionTrueIntegralfOne of those is worth flagging rather than hiding.
sum(k, k, 1, n) = 0solved fornanswers{ }, andn = 0is a solution (the empty sum is 0). That is the existing convention for a binder the solver cannot invert —apply(f, x)does the same — so I matched it rather than deviating in one new node. If that convention is wrong it is wrong in three places and should be changed in all of them; #964 is the same family.iis the imaginary unitsum(i, i, 1, 10)is left unevaluated, becauseiis not aVariableand so binds nothing. Every textbook writes this sum withi, so it is documented on the parameter and pinned by a test. It declines rather than guessing.Evidence
PublicApi.txtregeneratedAngouriMath.g— Fail CI when the committed parser no longer matches its grammar (#898) #965 adds a CI check for exactly that\sum_{k=1}^{n},\prod_{k=1}^{n}),Stringizeround-trip, andsympy.Sum/sympy.Productexport are all coveredNot included
⋃ and ⋂, the other two operators the issue names. They bind over a family of sets rather than a numeric range, and the set layer has its own union/intersection already; that is a separate design question and I would rather it were decided than assumed.
This also does not add
=>or juxtaposition syntax (#495) or quantifiers (#225) — but it establishes the pattern both would follow, which is what makes them cheaper now.🤖 Generated with Claude Code