Add round, min, max and gcd, closing #809 - #828
Merged
Conversation
Both were refused by name since #733. They exist now, as nodes rather than as sugar, with every convention measured against SymPy 1.14 rather than chosen -- AGENTS.md asks for exactly that when a convention has to be settled. What the measurement decided, and in three places it is not what the issue assumed: - Rounding is toward the infinities, not toward zero: floor(-3/2) is -2 and ceil(-3/2) is -1. - A complex argument is taken componentwise, so floor(3/2 + 5/2i) is 1 + 2i. SymPy and Mathematica agree, and it is the only reading under which floor of a real keeps its meaning when the imaginary part is zero. - The derivative is 0 provided the argument is not an integer. The issue guessed "zero almost everywhere"; SymPy declines to answer at all. Stating the condition says more than either, and it is what Signumf and Absf already do -- this library has Providedf and SymPy does not. ceiling( is accepted because that is SymPy's spelling, so an expression copied from there parses; Stringize prints the short ceil(, which is what the round trip pins. Inverting them is many-to-one: floor(f(x)) = v is solvable only for integer v and then f(x) is anywhere in [v, v + 1). So the inverse is a parameter over that interval, the same device Signumf and Absf use, and floor(x) - 3 = 0 now answers { 3 + t_1 provided t_1 in RR and t_1 >= 0 and t_1 < 1 } where #733 recorded it answering { 3 / floor }. The grammar was regenerated with JDK 25 and the committed ANTLR 4.13.1 jar. The unmodified grammar was regenerated first and reproduced the checked-in parser byte for byte, so the diff here is the new rules and not a toolchain difference. Seven tests in MissingFunctionNamesRefusedTest asserted these names were refused. That is the answer getting better rather than a test that was pinning a fudge: the refusal cases move to names still missing, and the equation from the original report is now asserted to be answered instead of refused. Tests: 5872 passing, 0 failed, 14 skipped; F# 130. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The four that were left after floor and ceil. Every convention was measured against SymPy 1.14 rather than chosen, and two of them are not what the issue assumed. round is half to even -- round(1/2) is 0, round(5/2) is 2 -- matching Python, SymPy, Mathematica and IEEE 754, and .NET's Math.Round default. It is deliberately not floor(x + 1/2), which is the obvious translation and disagrees at every tie: that sends 1/2 to 1 and 5/2 to 3. A test pins that the two differ, so nobody simplifies the node into it. That also decided the SymPy export. SymPy has no symbolic round -- RoundFunction is an abstract base that raises, and .round() is a method on a concrete number -- so ToSymPy builds the tie correction out of floor, frac and Mod rather than emitting the wrong one-liner. Checked against Rational.round() on ties, non-ties and negatives. min and max are nodes, not sugar over (a + b -+ abs(a - b)) / 2. The closed form is what they simplify to where it helps; it is not what they should print as, and the round-trip contract would make that permanent. SymPy keeps Min as a node for the same reason. They compare only where the arguments are ordered, so an unordered pair is left alone rather than guessed at, and their derivative stays unevaluated because stating it needs a case split on which argument is smaller. gcd is not integers only, which is what the issue said and what I repeated. SymPy's gcd is the polynomial gcd with the integer case inside it, so gcd(1/2, 1/3) is 1/6 and gcd(x^2-1, x-1) is x-1. Integers and rationals are computed here; the polynomial case is left as the node rather than half-done, and this library already has PolynomialGcd for whoever wires it in. min, max and gcd take any number of arguments and fold, which needed one new helper -- AssertAtLeast -- since every existing arity check is for a fixed count. The grammar was regenerated with JDK 25 and the committed ANTLR 4.13.1 jar, after confirming on the previous change that the unmodified grammar reproduces the checked-in parser byte for byte. Twelve tests in MissingFunctionNamesRefusedTest asserted these names were refused, and they are not any more. That is the answer getting better: the refusal cases move to trunc, lcm, erf and conjugate, which are what is still missing. Tests: 5928 passing, 0 failed, 14 skipped; F# 130. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rafael-SOWNet
added a commit
that referenced
this pull request
Aug 9, 2026
* Bring Syntax.md up to date with the functions that now exist Syntax.md still listed floor, ceil, ceiling, round, min, max and gcd under "Refused by name". Six of them have existed since #827 and #828, so the document was telling a caller the opposite of what the parser does -- and AGENTS.md is explicit that a stale one of these is worse than none. I changed the grammar twice and did not update it either time. The entries say what each does rather than only that it parses, since the conventions are the part a caller cannot guess: rounding toward the infinities rather than zero, round going to the nearest even on a tie and so not being floor(x + 1/2), the componentwise reading of a complex argument, min and max leaving an unordered pair alone, and gcd covering rationals. Every claim in the new text was checked against a build rather than written from memory, including that the four still-refused names still raise and that re and im are still read as products. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Say in Syntax.md too that LaTeX output is parsed elsewhere The same false claim #823 corrected in AGENTS.md -- "nothing parses LaTeX" -- was in Syntax.md as well, and I fixed only the one I had been pointed at. CSharpMath.Evaluation reads LaTeX back into an Entity and states the contract in its own source, so the sentence was wrong in both places. The point worth keeping is the one the correction adds: Latexise is still free to use \frac and the rest, but a change to what it emits can break a downstream project and nothing here will catch it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rafael-SOWNet
added a commit
that referenced
this pull request
Aug 9, 2026
… (#836) ComputeLimitDivideEtImpera defaulted to new Limitf(this, x, dist, side). That is the cycle AGENTS.md names, with this exact expression as its example: the caller evaluates the returned node to compare it, evaluating a Limitf computes the limit, and computing arrives back at the default. It overflows the stack, which kills the process rather than raising anything a caller can catch. So a node crashed by inheriting the default -- by not being mentioned anywhere. Seven did: limit(floor(x), x, 0) stack overflow, process dies limit(ceil(x), x, 0) stack overflow limit(round(x), x, 0) stack overflow limit(min(x, 1), x, 0) stack overflow limit(max(x, 1), x, 0) stack overflow limit(gcd(x, 2), x, 0) stack overflow limit(phi(x), x, 0) stack overflow The first six arrived with #827 and #828, which added the nodes without the ComputeLimitDivideEtImpera override that AddingNode.cs lists at step 3d. phi predates both and is #833, so this is not only my regression -- the default has been a landmine for every node that ever inherited it, which is what #704 was. Returning null is what AGENTS.md prescribes and what every caller already expects: each of the eleven call sites tests the result with `is { }` or switches on it, and the signature was already Entity?. All seven now come back as an unevaluated limit node, which is the honest "I could not settle this", and nothing else moves -- 5928 tests passed before this change and after it. LimitTerminatesOnEveryNodeTest enumerates the node types by reflection rather than listing them, because the defect is in what a node inherits by not being mentioned, and a per-node test cannot catch that: 45 types, each over three destinations and three sides, asserting only that the call returns. Checked against the unfixed code, where the run aborts. This does not replace #831, which gives floor and ceil real limit answers and fixes the OverflowException of #830. That adds capability; this removes the way to crash. They compose, and the order does not matter. Tests: 5973 passing, 0 failed, 14 skipped; F# 130. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 #809. Follows #827, which added
floorandceil.Every convention measured, and two were not what the issue assumed
roundis half to evenPython, SymPy, Mathematica and IEEE 754 all mean this by rounding, and it is .NET's
Math.Rounddefault. It is deliberately notfloor(x + 1/2)— the obvious translation, which disagrees at every tie: that sends1/2to1and5/2to3. There is a test pinning that the two differ, so nobody simplifies the node into it later.That also decided the SymPy export. SymPy has no symbolic round —
RoundFunctionis an abstract base that raises, and.round()is a method on a concrete number, not a function of an expression. SoToSymPybuilds the tie correction out offloor,fracandModrather than emitting the wrong one-liner, and the construction was checked against SymPy's ownRational.round()on ties, non-ties and negatives.gcdis not integers onlyThe issue said "only defined on integers" and I repeated it. SymPy's
gcdis the polynomial gcd with the integer case inside it:Integers and rationals are computed here. The polynomial case is left as the node rather than half-done — this library already has
PolynomialGcdbehind thePolynomialGcdCancellationrule set, and wiring that in is its own change.minandmaxare nodes, not sugarExpressible as
(a + b ∓ |a − b|)/2, and SymPy still keepsMin/Maxas nodes. That is the right call: the closed form is what they simplify to where it helps, not what they should print as — and the round-trip contract would make a sugar spelling permanent, sincemin(a, b)could then never print asmin.They compare only where the arguments are ordered; an unordered pair is left alone rather than guessed at. Their derivative stays unevaluated, because stating it needs a case split on which argument is smaller — declining is the honest answer there.
Variadic
min,maxandgcdtake any number of arguments and fold left, as they do everywhere else:min(3, 5, 1)is1. That needed one new helper,AssertAtLeast, since every existing arity check is for a fixed count.Tests
5928 passing, 0 failed, 14 skipped. F# 130 passing.
RoundMinMaxGcdTestcovers the tie behaviour in both directions, thatrounddiffers fromfloor(x + 1/2), comparison and folding,gcdover integers and rationals, printing, LaTeX, round-trips, derivatives, arity errors and substitution.Twelve tests in
MissingFunctionNamesRefusedTestasserted these names were refused and no longer pass. Same category as last time and worth naming: that is the answer getting better, not a test pinning a fudge. The refusal cases move totrunc,lcm,erfandconjugate, which are what is still missing.Compatibility
BREAKING-CHANGES.mdrecords it. If you were catchingUnrecognizedFunctionParseExceptionaround these names, that is dead code now. Baremin,maxandfloorwithout a bracket are still ordinary variables, andminimum(x)is still the implicit product.🤖 Generated with Claude Code