Skip to content

Add a modulus node (#402, #618) - #703

Merged
Rafael-SOWNet merged 2 commits into
ASC-Community:masterfrom
Rafael-SOWNet:feat/modulus
Aug 4, 2026
Merged

Rafael-SOWNet merged 2 commits into
ASC-Community:masterfrom
Rafael-SOWNet:feat/modulus

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Member

There was no remainder in the library at all — % was a parse error and there was nothing to call. #402 asks for MathS.Modulus and for % in the F# wrapper; #618 asks how to get a remainder and is told there is no operator for it.

Adds Modf and takes it through everything in the project's own AddingNode.cs checklist: parser, MathS.Mod, the % operator on Entity, evaluation, simplification, differentiation, limits, both compilers, stringize, LaTeX, SymPy export, sort hash, codomain, substitution and inversion.

Conventions, both pinned by tests

The remainder takes the sign of the dividend — the truncated convention, which is what C# does and what the arbitrary-precision arithmetic underneath already implements. (-7) % 3 is -1, not 2. The Euclidean convention is the other common choice and the two disagree on every negative dividend, so the tests state it rather than leave it to be discovered. MathS.Mod documents how to get the non-negative form.

% binds as tightly as * and / and associates left, again as in C#. There is a test per case: 2 * 7 % 3, 7 % 3 * 2, 12 / 4 % 2, 1 + 7 % 3, 2 ^ 3 % 3, -7 % 3, each asserted against the value C# itself computes for the same source.

Three places where it deliberately claims less than it could

  • The derivative is the dividend's own where the divisor does not contain the variable, and is left alone where it does. a % b is a - b*floor(a/b); there is no floor node here, so the general case could only be written as something that is wrong at the jumps.
  • A limit is answered where the remainder is continuous, and left unevaluated at the jumps — where the dividend reaches a non-zero multiple of the divisor. The value at a jump is one of the two one-sided limits and neither the other nor the two-sided one, so answering with it would be a wrong answer rather than a missing one. Zero is not a jump: the remainder takes the dividend's sign, so x % 3 is x on either side of 0 and passes through continuously.
  • Inverting is not implemented, so solving answers with no roots rather than wrong ones. x % a = v has one solution per period and wants an integer parameter, as the trigonometric inversions have. There is a test pinning the empty answer so that whoever writes it sees the change.

Complex arguments

There is no one remainder of a complex number by another — which multiple of the divisor to subtract is a choice, and rounding the quotient to the nearest Gaussian integer and truncating it disagree. The interpreter declines and leaves a % b unevaluated. The stack machine carries every value as a System.Numerics.Complex and has no way to return an unevaluated node, so it answers NaN, which says the same thing. Tested both ways.

The Linq compiler uses Expression.Modulo and is not widened to double the way division is: the remainder of two integers is an integer and widening would only lose that. Both compilers are tested against the interpreter on the same inputs.

Simplification

a % a → 0, 0 % a → 0 (both carrying the node's own "divisor is not zero" condition), (a % b) % b → a % b, and numeric evaluation. x % 1 is deliberately not reduced — that is 0 only for whole x, and 2.5 % 1 is 0.5, so it stays unless the dividend is known to be an integer.

Measurements

  • 42 new C# tests, 3 new F# tests.
  • Suite: Failed: 0, Passed: 4503, Skipped: 14, Total: 4517 and Failed: 0, Passed: 130 for F#.
  • A 117-problem corpus of simplifications, solves, integrals and limits: unchanged at 111/117, 0 wrong / 0 error / 0 timeout.
  • AngouriMath.FSharp and AngouriMath.Terminal.Lib both build.

A note on the diff size

Sources/AngouriMath/Core/Antlr/* is regenerated by the project's own Sources/Utils/antlr_rerun.bat (ANTLR 4.13.1, then the post-processor that makes the generated classes internal). One new token shifts every token index after it, which is where the ~1500 changed lines come from. The hand-written change is the single '%' alternative in mult_expression in AngouriMath.g.

There was no remainder in the library at all: `%` was a parse error and there
was nothing to call. Adds `Modf`, with the parser, `MathS.Mod`, the `%` operator
on `Entity`, evaluation, simplification, differentiation, limits, both
compilers, LaTeX, SymPy export and the F# `%`, which needs no wrapper code of
its own since F# resolves it through `op_Modulus`.

The remainder takes the sign of the dividend, which is the truncated convention:
the one C# has, and the one the arbitrary-precision arithmetic underneath
already implements. (-7) % 3 is -1, not 2. `%` binds as tightly as `*` and `/`
and associates to the left, again as in C#.

Three places where less is claimed than could be:

  The derivative is the dividend's own where the divisor does not depend on the
  variable, and is left alone where it does. a % b is a - b*floor(a/b), and
  there is no floor node here, so writing the general case would mean writing
  something wrong at the jumps.

  A limit is answered where the remainder is continuous and left unevaluated at
  the jumps, where the dividend reaches a non-zero multiple of the divisor. The
  value at a jump is one of the two one-sided limits and neither the other nor
  the two-sided one. Zero is not a jump: the remainder takes the dividend's
  sign, so x % 3 is x on either side of it.

  Inverting is not implemented, so solving answers with no roots rather than
  with wrong ones -- x % a = v has one solution per period and wants an integer
  parameter, as the trigonometric inversions have.

The stack machine carries every value as a complex number and answers NaN where
either part is not real, which is the same refusal the interpreter makes by
leaving a complex remainder unevaluated. There is no one remainder of a complex
number by another.

The parser files under Core/Antlr are regenerated by the project's own
antlr_rerun script; the churn is one new token shifting every index after it.

42 new C# tests, 3 new F# tests; suite 4503 + 130 passed, 0 failed; corpus
unchanged at 111/117 with 0 wrong.
@Happypig375

Copy link
Copy Markdown
Member

Is preserving negative dividend mathematically correct as opposed to the Euclidean remainer? Also - the parser should accept % as percentage and mod is to be written explicitly.

@codecov

codecov Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.65739% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.62%. Comparing base (90c00a8) to head (5d66b3b).
⚠️ Report is 79 commits behind head on master.

Files with missing lines Patch % Lines
...ources/AngouriMath/Core/Antlr/AngouriMathParser.cs 97.75% 20 Missing ⚠️
...nuous/Evaluation.Continuous.Arithmetics.Classes.cs 82.14% 1 Missing and 4 partials ⚠️
...nctions/Continuous/Limits/Solvers/Limit.Classes.cs 72.72% 1 Missing and 2 partials ⚠️
Sources/AngouriMath/Core/Domains.Classes.cs 50.00% 1 Missing ⚠️
.../Entity/Continuous/Entity.Continuous.Definition.cs 50.00% 1 Missing ⚠️
...ns/Output/ToString/ToString.Arithmetics.Classes.cs 50.00% 1 Missing ⚠️
...ions/Output/ToSympy/ToSympy.Arithmetics.Classes.cs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #703      +/-   ##
==========================================
+ Coverage   80.99%   81.62%   +0.62%     
==========================================
  Files         155      160       +5     
  Lines       13687    13882     +195     
  Branches     1957     2344     +387     
==========================================
+ Hits        11086    11331     +245     
+ Misses       1990     1892      -98     
- Partials      611      659      +48     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Two corrections from review, both of which the AGENTS.md rule decides against
what was here: mathematical convention outranks the programming one.

The remainder now takes the sign of the divisor -- the floored a - b*floor(a/b)
-- so (-7) mod 3 is 2 and 7 mod (-3) is -2. This is the convention under which
the residues modulo n are the numbers from 0 to n - 1, which is the point of the
operation in number theory, and it is what the other systems answer: SymPy,
Mathematica and Maxima all give 2. Checked against SymPy 1.14 on all four sign
pairs rather than asserted. The truncated convention that was here is C's, and
C's % is an operation on machine integers.

Both compilers follow. The stack machine converts by adding one divisor where
the signs disagree; the Linq compiler emits ((a % b) + b) % b, which is the
shortest expression that turns the runtime's truncation into the floored answer
for every pair of signs.

The parser now spells it `mod`, and `%` is left alone so that it stays free to
mean percent, which is what it means in mathematical writing. The stringizer
follows, so the round trip holds. The C# and F# `%` operator on Entity stays --
it is the language's operator over a different type, and #402 asks for it -- and
says in its documentation that it is not int's.

Suite 4504 passed, 0 failed.
@Rafael-SOWNet

Copy link
Copy Markdown
Member Author

Both points taken, and the first one you are right about — I had reached for the programmer's convention, not the mathematician's. Pushed as 5d66b3b6.

The convention

I checked rather than argued. SymPy 1.14, all four sign pairs:

7,3 -7,3 7,-3 -7,-3
SymPy Mod 1 2 -2 -1
Mathematica Mod, Maxima mod 1 2 -2 -1
C / C# % 1 -1 1 -1

So it is now the floored remainder, a - b*floor(a/b), taking the sign of the divisor. That is the convention under which the residues modulo n are the numbers 0 to n-1, which is the whole point of the operation in number theory. C's % is an operation on machine integers and the library is not obliged to inherit it — and by the rule in the AGENTS.md I just wrote, mathematical convention outranks the programming one, so it would have been inconsistent of me to argue otherwise.

Both compilers follow: the stack machine adds one divisor where the signs disagree, and the Linq compiler emits ((a % b) + b) % b, the shortest expression that converts the runtime's truncation for every sign pair. All three paths are tested against each other.

The spelling

mod is now the parser's, written out and at the same precedence as * and /. % is no longer accepted by the parser at all — there is a test asserting it throws — so it stays free to mean percent. The stringizer prints mod too, so the round trip holds.

I kept the % operator on Entity, since #402 asks for it in the F# wrapper specifically and % is C#'s and F#'s own operator name for this. Its documentation now says outright that it is not int's %. If you would rather that go too and leave only MathS.Mod, say so and I will drop it — it is three lines.

Two things I found while doing this, neither of them mine to fix here

The numeric % operators already disagree with each other, and one of them is wrong:

7%3 -7%3 7%-3 -7%-3
Integer % Integer 1 2 throws throws
Real % Real 1 -1 1 -1
Rational % Rational (7/2 etc) 1/2 5/2 1/2 -7/2

Three types, three behaviours, one ArithmeticException: Divisor is negative, and Rational carries a // TODO: consider the case for the divisor to be negative and gets -7/2 mod -3 wrong under every convention (it is -1/2). Modf does not go through them — it computes the floored remainder itself — so this PR neither fixes nor depends on that. I will open it as its own issue since it is public API and changing it changes existing behaviour.

There is no syntax reference in the repository. The grammar is the only statement of what the language accepts, and it is ANTLR source. That is part of why % was an open question at all. I am writing one.

Suite Failed: 0, Passed: 4504, Skipped: 14, Total: 4518, plus the F# wrapper.

@Rafael-SOWNet

Copy link
Copy Markdown
Member Author

@Happypig375 both of your points are implemented and verified, so I am merging this — but the second half of my reply was an offer rather than a rhetorical question, and it still stands.

Done: the remainder is floored and takes the sign of the divisor, checked against SymPy 1.14 on all four sign pairs; the parser spells it mod; % is rejected outright, with a test pinning that, so it stays free to mean percent.

Still open, and yours to decide: I kept the % operator on Entity (not the parser), because #402 asks for it in the F# wrapper and % is C#'s and F#'s own operator name for this. Its documentation says outright that it is not int's %. If you would rather it went and left only MathS.Mod, say the word — it is three lines and I will send it as a follow-up rather than leaving it to be argued about later.

One consequence now that #709 has landed: the three numeric % operators are floored too, so Modf's own FlooredRemainder helper is redundant and can just call %. I will send that simplification separately.

@Rafael-SOWNet
Rafael-SOWNet merged commit 70fcbea into ASC-Community:master Aug 4, 2026
26 checks passed
@Rafael-SOWNet
Rafael-SOWNet deleted the feat/modulus branch August 4, 2026 23:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants