Skip to content

Pending breakages: changes agreed or arguable that cannot ship before a major version #1019

Description

@Rafael-SOWNet

"Maybe an issue on pending breakages would be appropriate. Remember to note to yourself to follow through on the next major version. Also, maybe noting down to promote experimentals and removing obsoletions would be appropriate."
— @Happypig375, #1009

This is that issue: the list of changes that are agreed or arguable but cannot ship in a minor, so that 3.0 has a docket rather than a memory. Nothing here is scheduled and nothing here is a defect report.

A change belongs on this list when it moves the value of existing user input, or removes a published member. A changed answer has gone in a minor here before, on the stated principle that correctness outranks compatibility — but every one of those was a wrong answer becoming right. Everything below is a deliberate convention change, which is a different thing and should not borrow that licence.


1. implies becomes right-associative

Raised on #1009. The grammar folds implies to the left; mathematics, Lean, Coq, Agda, Haskell and CSharpMath.Evaluation all read → to the right. Ours is the outlier, and CSharpMath reading it differently from us is a live inconsistency across a boundary we do not control.

It cannot go in a minor because it re-values existing text:

"false implies true implies false"     today  False        after  True

Nothing fails to compile; answers move silently. #1009 makes the printer state the grouping the grammar actually has, so the change is a one-line flip in ToString.Discrete.Classes.cs (from <=-on-the-right to <=-on-the-left) plus the grammar, and the round-trip tests written there keep it honest.

Also decide provided at the same time. It folds right today, which #1009 documents and tests. If implies moves to the right, the two agree and Syntax.md's rule becomes "^, implies and provided group to the right"; that is worth settling in one go rather than twice.

2. FreeVariables, VarsAndConsts, and a property that does not exist

Raised on #989.

"VarAndConsts is what it says: free variables and constants (pi and e). "Used variables" looks like another property." — @Happypig375

Measured on master at 6b93b401, against that definition. The documented example is Lambda(x, x * 2 + sin(y * pi)):

today by the definition above
Vars x, y —
VarsAndConsts x, y, pi y, pi — x is bound
FreeVariables y y

So VarsAndConsts returns bound names, and its XML example pins that. It is wrong for lambda, which is the one binder the library already honours elsewhere — and wrong for the other five (sum, product, integral, limit, derivative, set-builder) in the same way:

sum(k, k, 1, 3)      Vars = k    VarsAndConsts = k    FreeVariables = k

sum(k, k, 1, 3) is 6. Nothing about that value depends on k, and all three properties say it does.

The shape of the fix, which is three published properties moving at once:

  • FreeVariables — extend "bound" from lambda to every binder. Its own doc comment currently defines bound as "a parameter of some outer lambda", which was accurate when written and is not now that CalculusOperator and ConditionalSet both declare a Var (Let a binder bind i, in every binder there is (#976) #986).
  • VarsAndConsts — free variables and constants, per the definition above.
  • A new property for "used variables" — every name that occurs, bound ones included. That is what Vars/VarsAndConsts do today, so the behaviour is not lost, it is renamed to something that describes it.

Part 1 of #989 — the %1 placeholder escaping — was a defect on any definition and is already fixed (#1000). This is part 2, which was always a design call.

3. Promote or retire the experimental features

MathS.ExperimentalFeatures (Sources/AngouriMath/Convenience/Experimental/) is 11 public members, documented as "features that might become stable in the future, but are not guaranteed to do anything useful or correctly at the current moment":

SolveDiophantineEquation · DecomposeRational ×2 · GetSineOfHalvedAngle · GetCosineOfHalvedAngle · ExpandSineArgumentMultiplied · ExpandCosineArgumentMultiplied · ExpandSineOfSum · ExpandCosineOfSum · SymbolicFormOfSine · SymbolicFormOfCosine

Each wants one of three verdicts, and the third is why this is on a breakage list:

  • promote — move to the stable surface under its proper name, and commit to the answer;
  • keep — it is still not guaranteed, and say what would settle it;
  • remove — it has not earned a place, and deleting a published member is a major-version act.

Worth doing as one pass rather than one member at a time, because "experimental" is a promise about the whole namespace and it decays if it is never spent.

Note also that Experimental is a folder inside the kernel package, not a separate package as #746 describes it — see #1008.

4. Removing obsoletions — a standing practice, not a backlog

There are currently zero [Obsolete] members in the tree: 2.0.0 removed the lot. So there is nothing to schedule, and the note to keep is the rule:

Anything marked [Obsolete] during 2.x is removed at 3.0. An obsoletion that survives a major stops meaning anything, and 1.x accumulated a set precisely by never spending them.

5. Already-known decisions that belong on the same docket

AGENTS.md names three under "Decisions only a major version may take", all still open:

And one more that is breaking by construction:

  • Complex Infinity #217 — complex infinity. It changes 1/0 from NaN, and every consumer branching on NaN is blast radius.

6. | stops meaning or

Raised on #1212, where the invitation was explicit:

"You may change the meaning of | on the next major version if that's more mathematically appropriate. Consider this for all the other parsed inputs as well." — @Happypig375

| is an alias for or today. It is the only spelling in the grammar that already means something else in mathematics — divides (a | b), "such that" ({x | P(x)}), "given" (P(A | B)), and the delimiter in |x| — so input written by a mathematician is read as a disjunction and answered as one. Measured on 281e0d0c:

written reads today as
2 | 6 2 or 6 — a disjunction of two numbers
{ x | x > 0 } { x or x > 0 } — a FiniteSet of one element, that element a disjunction

The second is what decided it: {x | x > 0} is ordinary set-builder notation, and we read it as a one-element set. Well-formed, silent, and nothing like what was written.

Nothing is lost by retiring the alias, since or is the primary spelling and is what everything prints as. & is deliberately not on this list: it is equally a programming convention, but no mathematical meaning competes for it, and the test applied throughout was "does this symbol already mean something else to a mathematician", not "would a mathematician have chosen it".

It cannot go in a minor because it re-values existing text, and the proposal is that it should not go in one major either:

"x > 0 | x < -1"     today  a disjunction        after  a divisibility statement

Nothing fails to compile; the answer moves silently. So two steps rather than one:

  1. First major: | becomes a parse error, with a message naming or. Loud rather than silent, and it costs a release of waiting to remove the entire class of quietly-wrong results.
  2. The major after: | means divides, and "given" inside an expectation or probability — the scoping @Happypig375 proposed on Math Club orientation week questions #1212, where E/P introduce the meaning and their closing bracket ends it. divides (a divides b: divisibility as a statement node #1220) then becomes the alias and | the canonical spelling.

If one step is preferred, the refusal step is the half I would keep regardless of how long it lasts.

7. Not-equal is a relation of its own, and != spells it

Raised on #1212's survey, filed as #1225; the node proposed there by @Happypig375, and the syntax below approved on #1225 (2026-10-02).

Measured on 94ba5ff6:

written parses as prints Solve("x")
x <> 3 not (x = 3) not x = 3 { x : not x = 3 }
a != b a! = b, an Equalsf a! = b
a ≠ b a parse error at ≠

The relation exists only as a negated equality, which the solver does not read, and != is silently a factorial equation.

What moves:

  • A node, NotEqualsf(a, b). It prints as one relation, is solved (x ≠ 3 is CC \ { 3 }, or RR \ { 3 } over the reals), and is matched by rules as a relation of its own. not (a = b) simplifies to it, so there is one canonical form.
  • Input ≠, <> and !=, with != one token, as Mathematica reads it beside the same postfix !: a != b and a!=b are the relation, and the factorial equation is written a! = b, with the space.
  • Printed <> in ASCII, ≠ in Unicode output, \neq in LaTeX.

Why a minor cannot carry it: a!=b and a != b change meaning, and not (a = b) prints and simplifies differently.

8. PeterO's types leave the public API

Raised on #1338, queued for v3 by @Happypig375 there (2026-09-16).

The library's numbers are PeterO's EInteger, ERational and EDecimal, and 48 published members expose them — Integer.EInteger, Rational.ERational, Real.EDecimal, Real.Create(EDecimal), Rational.Create(ERational), Complex.Create(EDecimal, EDecimal), the Deconstructs, the implicit conversions on Entity, Number, Real, Rational, Integer and Complex, MathS.Numbers.Create(...), ToNumber(...), DecimalConst.pi/e, Settings.DecimalPrecisionContext : Setting<EContext>, MaxAbsNumeratorOrDenominatorValue : Setting<EInteger>, PrecisionErrorCommon/PrecisionErrorZeroRange : Setting<EDecimal>, NewtonSetting.From/To, MathS.Utils.TryGetPolynomial(..., Dictionary<EInteger, Entity>), Number.GetAllRoots(Complex, EInteger), GetAllRootsOf1(EInteger), Number.IsZero(EDecimal), Entity.DefiniteIntegral(Variable, EDecimal, EDecimal, int), Rational.FindRational(EDecimal, int).

What moves and why a minor cannot carry it: measured on #1338, one multiply of two hundred-digit integers is 1,271 ns in EInteger and 109 ns in System.Numerics.BigInteger, and an EDecimal multiply or divide is 8–11× a decimal on a BigInteger mantissa; the fixed-point series already run on BigInteger (#1364, #1366) with the conversion at the boundary, and the remaining factor of two to mpmath is that boundary and the decimal layer above it. The plan posted there is in three steps, and only the last is breaking:

  1. done — the series on BigInteger behind an unchanged EDecimal surface;
  2. an internal AngouriMath.Numerics tower — BigInteger, a (BigInteger, BigInteger) rational and a (BigInteger mantissa, int exponent) decimal with a precision context — used by Number behind the current properties, which become conversions; measurable on the whole gate before any signature changes;
  3. v3: the 48 members above take and return the new types, PeterO.Numbers leaves the package's dependencies, and BREAKING-CHANGES.md carries the migration: every EDecimal a caller passes today becomes a decimal of ours (a parse from EDecimal.ToString() is lossless), every EInteger a BigInteger (EInteger.ToBytes/new BigInteger(bytes) both ways), and Setting<EContext> becomes a setting of a precision-and-rounding record of ours.

No answer moves: this changes the types a caller holds, not what any of them is worth.

9. An architectural review, every major version — and for v3, a global restructure

"Remember to do an architectural review every major version." — "refactor the API to reflect the best architecture as needed; major versions are for breaking changes." — and for this one in particular: "for v2->3 we still want to do a global architecture review and restructure pass because v1->2 didn't consider how a Math OS (according to everything in #746) should be structured - it followed mostly v1. Everything across file structure and API structure as well as projects and packages need to be restructured according to that vision. And because this is a global architecture pass, ensure that it's Claude Fable doing it instead of Opus." — @Happypig375, 1, 2, 3

Two things, then. As a standing rule, every major version gets an architectural review: the API read against what was added since the last one, and the parts that now share a better abstraction with the new work refactored to it there, since the major is where the breakage is allowed. For v3 specifically, a global pass: 2.0 followed 1.x's structure and never asked how a Math OS in #746's sense should be laid out, so the file structure, the API structure, the projects and the packages are to be restructured against that vision, not only patched where an abstraction has drifted — item 8's tower and the 48 members it retires are one piece of it, #746's package boundaries (its item 78) another. It is to be done by Claude Fable rather than Opus, per the maintainer, being a global pass; what it decides lands here or in the release, and it runs before v3 is named, not after.

The review also looks for duplicates and synthesises them — "whether hidden in implementation or types representing similar concepts" (4). Two kinds: the same computation written twice in different places (a rule and a solver each doing their own long division, a check re-implemented beside the helper that already answers it), and two types standing for one concept (a Setting<EContext> beside the precision the numeric tower will carry; an ERational beside a Rational; the several spellings of "a polynomial over x" that TryGetPolynomial, MultivariatePolynomial and the Hermite ansatz each keep). Each pair either becomes one thing or gets a written reason for being two, and a major is where the surviving one may take the other's name.

And every release, major or not, updates the website (AngouriMathSite): its What's new and its quickstart name the version they were last brought to, and as of 2026-09-16 that is 2.1.0 against a library at 2.5.0. It is a line on the release checklist in AGENTS.md from here on, not a thing to remember.

10. Numbers, evaluation and comparison: exact where exact, to a requested accuracy where not

Raised on #1376 and queued for v3 by @Happypig375 there (2026-09-16): "It just doesn't look right to have a logical boolean change its value just because we ask it at a different precision. An approximate comparison should be written properly as a chained inequality instead." — "we might want to separate the idea of a complex number that consists of two EDecimals and a real number that consists of an EDecimal from the node hierarchy as well. Any approximate operations with a floating point number would instead be interval arithmetic (or statistical distribution arithmetic if modelled that way)."

What is wrong today: 2 + 2^(-100) = 2 is True, because Equalsf subtracts the sides, evaluates, and asks Number.IsZero, which is |d| < PrecisionErrorZeroRange = 1e-16; Real == Real uses a third constant, PrecisionErrorCommon = 1e-6; and Evaled runs everything to DecimalPrecisionContext's hundred digits whether the question needs one or none. A boolean's value depends on the precision, and an exact rational is thrown away on the way to it.

What moves (the decisions are the review's, item 9):

  • Exact stays exact. Every parsable construction is exact — integers, rationals, radicals, pi, e. A comparison of exact quantities is decided exactly, symbolically where the numbers are algebraic, and enumerates no digits: 1 < 1 + 1^(-1000) < 2 is settled by arithmetic on rationals.
  • Evaluation to a requested accuracy, not to a global context: 3 < sqrt(2) + sqrt(3) < 4 needs one digit after the point and 0 < sqrt(2) + sqrt(3) - pi < 1 needs more, and each asks for what it needs ("Evaled as necessary"). The setting DecimalPrecisionContext : Setting<EContext> (item 8's PeterO type) becomes a request carried by the call, in significant digits or digits after the point, with a repeating expansion for a rational. Speed and memory of getting the correct digits with minimal computation is its own area, and worth it.
  • Approximate values are approximate on their face. A number that arrived as a double, or through an explicit Real constructor, is not exact, and arithmetic on it is interval arithmetic (or a distribution, if modelled so) rather than a decimal that pretends to a hundred digits. An approximate comparison is a chained inequality with the accuracy in it, never an equality with a hidden tolerance.
  • The numeric representation leaves the node hierarchy: a real as one EDecimal and a complex as two are representation, and the tower of item 8 is where that lives, with the nodes holding numbers rather than being them.

Why a minor cannot carry it: Equalsf, Number.IsZero, Real ==, the three PrecisionError* settings and Evaled itself all change what they answer, and every True that came from a tolerance may become False — recorded in BREAKING-CHANGES.md per input as the review finds them.

11. Newton's method is an entry point of its own

Raised on #1573 and queued for v3 by @Happypig375 there (2026-09-29): "Newton's solver is to be a separate entry point compared to the analytical solver since the guarantees of the answers are too different - Newton's solver guarantees "some" solutions on "more" equations while the analytical solver guarantees "all" solutions on "less" equations. Moreover, numerical approximation is to be their own types separate from the normal exact node hierarchy."

What is wrong today: where every analytical route declines, Solve falls back to Newton's method (MathS.Settings.AllowNewton, on by default) and answers the points it finds as a FiniteSet, which reads exactly like a complete, exact solution set. abs(x - 2) = abs(x - 3) was three points of the line Re x = 5/2. #1578 keeps the fallback away from equations real for every complex x, which is a guard and not the separation.

What moves: Solve answers what the analytical solvers establish, all the solutions or the equation left unsolved, and Newton's method becomes its own entry point, returning approximations typed as approximations (item 10) with no claim to be all of them.

Why a minor cannot carry it: every equation Solve answers today only through the fallback becomes unsolved there, a changed answer for each, and the types the new entry point returns are item 10's.


12. "Simplified" is a stated property, not a rating

Raised on #1409 by @Happypig375 (2026-09-29): "We also need a better definition of "simplified" for v3 (#1019). Counting the arbitrarily defined simplified rate isn't how mathematicians do it..."

What is wrong today: Simplify returns whichever candidate it generated has the lowest SimplifiedRate, a weighted count of nodes, and a tie goes to whichever was generated first. CanonicalForm.md §1 makes that the definition: "simplest is the best-rated member of an equivalence class under a cost metric." So an answer is simplified only relative to a number nobody states, and moving a weight moves answers nobody asked about.

What moves: simplified becomes a list of properties a result has, written per node class beside the canonical form, the way a textbook defines simplest radical form:

  • like terms combined;
  • a quotient in lowest terms, with no fraction nested in it;
  • no radical in a denominator, and no perfect power under a radical;
  • no negative exponents;
  • a function of an exact number evaluated where the value is exact.

Each is checkable on the answer, so Simplify is tested against the list the way canoncheck tests canonical form. A rate at most breaks ties among forms that already satisfy it. A shape that a question wants for its own purpose (factored, expanded, in partial fractions, over a common denominator) is asked for by name, as Factorize and Expand already are.

Auxiliary quantities belong in the definition too, raised by @Happypig375 (2026-10-06): the quartic formula is given in terms of quantities computed first from the coefficients, because written out in full it is unwieldy. A result may therefore be a body with named sub-expressions defined beside it (x = -b/(4a) ± S ± sqrt(...) where S = ..., Q = ...), and the fully substituted formula becomes one output form among several rather than the only one. The integrator shows the cost of not having this: answers with symbolic roots in them run to 20,000-70,000 characters (#1831), and in one measured case naming the compound coefficients before solving took an answer from 478,000 characters to 960. That needs two decisions: which sub-expressions are worth a name (repeated, and above some size), and whether the definitions travel with the entity or are only a way of printing it.

Why a minor cannot carry it: most answers Simplify gives today would be re-derived against the list, and every one that changes is a changed answer.


13. Exceptions are for unrecoverable errors; the lack of a value is null

Raised on this issue by @Happypig375 (2026-09-30): "Design principle for v3: exceptions are for unrecoverable errors; the lack of value is null."

What is wrong today: several public methods throw when they have no value to give, and a caller finds out only by catching.

#1540 took exceptions out of the library's own control flow for 2.6.0. These are what remain, and all of them are on the public surface.

What moves: each of these returns null, typed as nullable, so that the compiler holds every caller to the check. Every other exception type is to be reviewed the same way, and presumably replaced (@Happypig375 on #1569, 2026-09-30: "all exception types require a review to see if null-returns or returning a union type (think how you'd do it in functional languages like F#) is cleaner for the API shape instead. Presumably all exceptions are to be replaced with proper return types."). Where there is more than one reason for the lack of a value, the reason is itself the return value, as a union. The candidates for staying an exception, which the review decides:

  • misuse, such as SolveRequiresStatementException and WrongNumberOfArgumentsException;
  • the library contradicting itself: AngouriBugException, 57 sites;
  • input that is not an expression at all: the parse exceptions. A TryParse-style entry that returns null, or a union with the parse error, can sit beside Parse.

One is kept already (@Happypig375 on #1569): the implicit conversion from a string, which declares an entity in one line and has no return value to put a failure in. No other case for throwing is known.

Two decisions went with it, and @Happypig375 settled both (2026-09-30): "treat Undefined and Indeterminate as proper values on their own like Mathematica does (We won't have NaN in v3 because it's inherently a value defined by an approximate floating point type)."

  • An operation on matrices of shapes it isn't defined for is Undefined, a value: neither misuse nor null (InvalidMatrixOperationException and InvalidNumberException, 24 sites between them).
  • There is no NaN in v3. 0/0 is Indeterminate, as one of the indeterminate forms, and 1/0 is complex infinity (Complex Infinity #217). Null still says only that no value was produced.

Why a minor cannot carry it: every method named above changes its return type or its contract, and a caller that catches today either stops compiling or stops being reached.


14. Definedness comes from the arithmetic, and from a rule's stated condition

Raised on #1648 by @Happypig375 (2026-10-01): "Note the correct design for v3 (#1019) if you identify one".

What is wrong today: rewrites such as a / (b / c) → a * c / b give a value where the expression has none. 1/(1/x) simplifies to x, which is 0 at x = 0, where 1/(1/x) is NaN. O4 of SimplificationContract.md forbids that, and of the three 2.x fixes measured on #1648, none is clean.

What moves:

  • On v3's arithmetic, most of these rewrites are identities. There 1/0 is complex infinity (Complex Infinity #217) and 0/0 is Indeterminate (item 13), so 1/(1/0) is 1/∞ = 0, the same as x. Measured with sympy's zoo and nan, letting each hole take 0, ∞, Indeterminate and five finite values. Holes need the infinite values because a hole matches a subexpression, and a subexpression can be infinite where the variables are not:

    rewrite assignments that agree
    a/(b/c) → a*c/b 512 of 512
    (a/b)/(c/d) → (a*d)/(b*c) 4096 of 4096
    (a/b)/c → a/(b*c) 512 of 512
    (a^n)^m → a^(n*m), m whole 200 of 200
    a/csc(b) → a*sin(b), and likewise sec/cos and cot/tan 88 of 88 each

    All eight examples of Dividing by a reciprocal gives a value where the quotient has none: 1/(1/x) is x #1648 agree at their singular points. These rules carry no condition in v3, and any condition 2.x attaches to them in the meantime comes off.

  • The rest state their condition in the rule, as data. Some rewrites change definedness even on the sphere, and the same measurement finds them:

    • a/a → 1 at 0, ∞ and Indeterminate;
    • 0·a → 0 and a − a → 0 at ∞ and Indeterminate;
    • (a·b)/(a·c) → b/c, which is what pairwise grouping does when it cancels;
    • e^ln(a) → a at 0 and ∞. Whether this one belongs here depends on whether Complex Infinity #217's infinity is directed: in Mathematica, Log[0] is -Infinity and e^-∞ is 0.

    Each such rule declares its condition beside its pattern, where when: sits today, and soundcheck checks each declaration at the points where the two sides differ.

  • The engine carries conditions beside the candidate, not inside it. It collects what the applied rules declare and drops whatever the input's own domain (DomainConditionIn) or the preamble already implies. What remains is stated once, on the answer. The alternative was measured on Dividing by a reciprocal gives a value where the quotient has none: 1/(1/x) is x #1648: a provided inside the search hid the rewrite's shape from the rules that continue from it, and gave 34 failures, about 20 of them lost capability.

Why a minor cannot carry it: the arithmetic is #217's, and every answer that gains or loses a condition is a changed answer.


15. The number system says which infinities it has

Raised on #1648 by @Happypig375 (2026-10-02). over RR and over CC don't include infinities, and today's default includes them in a way that is neither the extended reals nor the Riemann sphere.

What is true today, measured on master bc6c190c:

  • oo and -oo are reals. Each part of a complex number can be infinite on its own, as in oo + i or -oo i. There is no complex infinity: 1/0 is NaN (Complex Infinity #217). So the default is the complex plane with an infinity in each axis direction.
  • RR treats the two infinities differently: -oo in RR simplifies to True, while oo in RR is left undecided.
  • oo - oo and 0 * oo simplify to 0. These are item 14's a − a → 0 and 0·a → 0 at ∞.

What moves:

  • Five sets, each with a name, agreed on Dividing by a reciprocal gives a value where the quotient has none: 1/(1/x) is x #1648: RR and CC without infinities; the extended reals, RRbar, which are RR with -∞ and +∞; and the two projective lines, RRhat and CChat, which are RR and CC each with one unsigned ∞ — the projectively extended reals and the Riemann sphere. The bar is a line with two ends, the hat two lines meeting at one point. Each algebra with an unambiguous notation has a short name, and its full name is a synonym (@Happypig375, 2026-10-02): Reals, Complexes, ExtendedReals, ProjectiveReals and ProjectiveComplexes. The default has no notation, so it has only its full name.
  • One projective infinity, printed ∞̃ and \tilde{\infty} in LaTeX as Mathematica (ComplexInfinity) and Calcium (UnsignedInfinity) print it, with SymPy's zoo proposed for its ASCII spelling: the point at infinity of RRhat is the same point as CChat's, since P¹(R) ⊂ P¹(C), and it is the default's undirected infinity, Complex Infinity #217's 1/0.
  • The default is the complex singularity closure, Calcium's term (Johansson, arXiv 2011.01728, after Mathematica): CC, the directed infinities, ∞̃, and item 13's two exceptional values, Undefined and Indeterminate, where Calcium has one. Calcium's sets match these: ExtendedRealNumbers is RRbar, ProjectiveRealNumbers and ProjectiveComplexNumbers are RRhat and CChat. Written over ComplexSingularityClosure where it has to be named.
  • oo by algebra: +∞ in the default and in RRbar; under RRhat or CChat, oo and -oo both name the one point at infinity; under RR or CC, neither is a member.
  • Undefined and Indeterminate propagate in every algebra (@Happypig375 on Dividing by a reciprocal gives a value where the quotient has none: 1/(1/x) is x #1648): an operation on either gives it, over RR as over the default. What an algebra decides is membership — Undefined in RR is false, Undefined in ComplexSingularityClosure true — and the value at its own special points: 1/0 is Undefined over RR, outside it, so division is partial there; and ∞̃ over the projective lines and the default, inside them, so division is total there.
  • Mixing overs: every named set sits inside the default, so nodes under different overs combine in the default and the result is the default's: (oo over RRbar) + (oo over RRhat) is +∞ + ∞̃, Indeterminate. No over means the default, and a prelude can set another for every node.
  • The projective lines share one arithmetic: x/0 = ∞ for x ≠ 0, x ± ∞ = ∞ and x/∞ = 0 for finite x, x·∞ = ∞ for x ≠ 0; ∞ + ∞, ∞ - ∞, 0·∞, ∞/∞ and 0/0 are Indeterminate; and there is no order, so ∞ > 0 has no value. Where they differ from RRbar is limits: 1/x at 0 tends to ∞ on the projective line and has no limit over the extended reals.
  • The default is stated. One model already has the default's shape, and it is Mathematica's: directed infinities ∞·e^(iθ), with +∞ and -∞ as the real directions, plus one undirected complex infinity, which is Complex Infinity #217's 1/0. Together with Indeterminate (item 13), that set is closed under the arithmetic. Over the extended reals only the two real directions remain, and over the sphere every infinity is the one ∞. Its name is Calcium's, the complex singularity closure (below). It is documented as what evaluation works in, and over takes the named sets only.
  • over RR and over CC exclude every infinity. A limit of +∞ has no value over RR, and x in RR is false at ±∞. oo - oo and 0·oo are Indeterminate.
  • Rules ask for properties, not for algebras (@Happypig375 on Dividing by a reciprocal gives a value where the quotient has none: 1/(1/x) is x #1648, 2026-10-02). Every algebra declares: division by every nonzero element (a field); no zero divisors; commutative multiplication; an order compatible with the arithmetic; its characteristic. A rule names what it needs -- a/a = 1 division, a^2 = 0 ⇒ a = 0 no zero divisors, a b = b a commutativity -- and applies only where the algebra has it. These are the assumptions SoundUnderAssumptions names no assumption: 177 rules declare it and the condition exists only in comments #1252 says the 177 SoundUnderAssumptions rules declare and do not name.
  • ZZ/nZZ, the integers modulo the ideal nZZ, n a number or a symbol: a field for a prime n, with zero divisors otherwise (2 * 3 = 0 in ZZ/6ZZ). Today's a = b (mod n) is membership in one class of it. ZZ_3 is not used, being the 3-adic integers to many readers.
  • No further algebras in the default: hyperreals, infinitesimals, quaternions. @Happypig375 on Dividing by a reciprocal gives a value where the quotient has none: 1/(1/x) is x #1648: the simplification rules would be damaged the further the default goes beyond the complex numbers with infinities, because more functions become multi-valued under inversion and more identities stop holding. Orders of vanishing and asymptotics, what infinitesimals would be used for here, limits and series already answer. Dual numbers (ε^2 = 0, for automatic differentiation) would be a separate algebra, outside the default.

Why a minor cannot carry it: oo - oo, 0·oo and the membership of every infinity change their answers, and #217 changes 1/0.


16. amcli's commands say what they do

Raised on #1654 by @Happypig375 (2026-10-02): "remember to revise the command names for clarity and correctness for v3".

#1654 kept the standalone CLI's names so that its scripts would keep working. A proposal:

today v3 why
eval evaluate the library's verb. It evaluates what it can, so amcli eval "x + 1" prints x + 1, and the usage line's "evaluates to a number" claims too much
simp simplify the library's verb
fsimp normalize it is the normalisation without the search, InnerSimplified, and fsimp names neither
diff differentiate diff reads as a difference
sub substitute sub reads as subtract
info describe the code already calls it describe, and the name says what it prints
solve, latex unchanged

Why a minor cannot carry it: removing a name breaks the scripts that call it. A minor can add the new names beside the old ones first, and v3 then removes the abbreviations.


17. Solve takes several unknowns, and EquationSystem goes

Raised on #1673 by @Happypig375 (2026-10-02): "this method is to be removed for v3 - let solve return for multiple variables."

A system is a conjunction, and Solve already reads and for one unknown. MathS.Equations(...).Solve(x, y) is a second entry point that takes each equation in = 0 form and returns a matrix of solution rows. Proposed:

  • "x + y = 3 and x - y = 1".Solve("x", "y") solves for both unknowns and returns the solutions jointly, { (2, 1) }, which wants a tuple that is not a vector.
  • MathS.Equations and EquationSystem are removed.

Before it, #1680 has to hold: a conjunction with a parameter keeps its second equation where its members leave it undecided, rather than dropping it.

Why a minor cannot carry it: removing MathS.Equations breaks every caller, and a minor can only add Solve(x, y) beside it.


What this issue is for

To be read before the next major version is named, in the same way #746 is read before any version is named. It is not a plan and it does not commit anyone.

Anything added here should say what moves, what the old and new values are, and why a minor cannot carry it.

Activity

  1. Happypig375 commented on Aug 23, 2026

    @Happypig375
    Member

    If VarsAndConsts better means used variables instead, change it to that definition too! Identify the correct design here.

  2. Rafael-SOWNet commented on Aug 23, 2026

    @Rafael-SOWNet
    MemberAuthor

    Measured on current master, and the answer is yes to your suggestion — with a correction to what I
    wrote in item 2 above.

    VarsAndConsts already means "used", and its own documentation says so

    The XML example on all three properties uses lambda(x, x * 2 + sin(y * pi)) and lists:

    Variables:              x, y
    Variables and constants: x, y, pi
    Only free variables:     y, pi
    

    It lists x — the bound name — under variables and constants. So VarsAndConsts is documented
    as every name occurring, and the implementation matches it exactly. My item 2 was wrong to call
    that a defect
    , and wrong that it makes this a three-property breaking change. Nothing about
    VarsAndConsts needs to move, and nothing about Vars, which is VarsAndConsts minus the constants.

    What is owed there is wording, not behaviour: the summary says "set of unique variables", which does
    not distinguish occurring from free. It should say occurring, and say that a bound name is
    included on purpose, so that the next person does not read the lambda case as a bug the way I did.

    The property that is actually wrong is FreeVariables, and only for the range binders

    sum(k, k, 1, n)              free = { k, n }     wrong -- k is bound
    product(k, k, 1, n)          free = { k, n }     wrong -- k is bound
    integral(t * b, t, 0, 1)     free = { b, t }     wrong -- a definite integral binds t
    integral(t * b, t)           free = { b, t }     right -- the indefinite integral is a function of t
    derivative(t * b, t)         free = { b, t }     right -- d/dt denotes a function of t
    lambda(x, x + y)             free = { y }        right
    { k : k > a }                free = { a }        right
    

    It handles Lambda and ConditionalSet and nothing else. Σ and Π genuinely bind their index —
    sum(k, k, 1, n) depends on n alone — and a definite integral genuinely binds its variable between
    its limits.

    The two that look like the same shape and are not: an indefinite integral and a derivative do not
    bind.
    ∫ t·b dt is b·t²/2 + C, a function of t, and derivative(f(t), t) denotes a function of
    t as well. Their current answers are right, and worth writing down precisely so that a later sweep
    does not "fix" them into being wrong.

    So the design, stated

    property means status
    VarsAndConsts every variable or constant occurring, bound ones included correct; document the word occurring
    Vars the same, minus constants correct, derived
    FreeVariables not bound by an enclosing binder incomplete: sum, product, and integral with limits

    Is it still a pending breakage?

    Narrower than item 2 claims, and I will correct that entry. VarsAndConsts and Vars do not
    change at all. FreeVariables changes for three node shapes — anyone relying on a Σ index appearing
    free gets a different answer, so it owes a BREAKING-CHANGES.md entry, but it is a wrong answer
    becoming right rather than a convention being chosen, which by this issue's own criterion at the top
    means it does not have to wait for a major version.

    I would rather fix FreeVariables now, in its own PR with the three cases pinned, and leave this
    issue for the changes that genuinely cannot ship before 3.0.

  3. Rafael-SOWNet commented on Sep 2, 2026

    @Rafael-SOWNet
    MemberAuthor

    Section 2's FreeVariables defect is fixed, and the two cases the section is careful to mark
    as right have stayed right — which is the half worth checking, since the fix is the kind that
    over-corrects.

    Measured at c639d489 — master 1f3b02ae plus the pull request in flight for #233, which
    touches only the integrator and cannot reach any of this.

    The four that were wrong

                                  filed here          now
    sum(k, k, 1, n)               free = { k, n }     free = { n }
    product(k, k, 1, n)           free = { k, n }     free = { n }
    integral(t * b, t, 0, 1)      free = { b, t }     free = { b }
    limit(t * b, t, 0)            free = { b, t }     free = { b }
    

    limit is the one this section does not list; it is recorded in BREAKING-CHANGES.md under
    "every limit", and it moved with the others.

    The two that were right, and still are

    integral(t * b, t)            free = { t, b }     unchanged -- the indefinite integral is a function of t
    derivative(t * b, t)          free = { t, b }     unchanged -- d/dt denotes a function of t
    

    Worth stating explicitly because "extend bound from lambda to every binder" is exactly the
    change that would have swept these up too, and the distinction the section draws — a definite
    integral binds its variable, an indefinite one does not — is not one a mechanical fix would
    make on its own.

    sum(k, k, 1, 3)

                     filed here    now
    Vars             k             k
    VarsAndConsts    k             k
    FreeVariables    k             {} (empty)
    

    So of the three properties the section says all wrongly report k, only FreeVariables has
    moved — which matches what the later comment in this thread concludes should happen:
    Vars and VarsAndConsts report what occurs, bound names included, and that is the
    documented meaning rather than a defect.

    One discrepancy in the record itself

    The table in section 2 gives, for lambda(x, x * 2 + sin(y * pi)):

    today by the definition above
    FreeVariables y y

    Measured now it is y, pi — and that is what FreeVariables's own XML example has always
    printed (Only free variables: y, pi). So this is not a change and not a regression; the row
    here reads y where the documented example reads y, pi. Flagging it only so that nobody
    later reads the difference as the fix having leaked.

    What this leaves

    Sections 1 and 3 are untouched by any of the above, and section 2's remaining items are the
    two that were always design calls rather than defects:

    • VarsAndConsts returning bound names — which the thread has since argued is correct, since
      it reports occurrence.
    • A new property for "used variables" — still not added.

    So the defect in section 2 is closed; what is left of it is the naming question.

  4. Rafael-SOWNet commented on Sep 5, 2026

    @Rafael-SOWNet
    MemberAuthor

    Re-measured every item against the tree at 5d802bab rather than reading the docket back. Three of
    the seven no longer hold.

    item now verdict
    1 implies right-assoc Still true. AngouriMath.g:221-224 loops $value.Implies(...). false implies true implies false → False; false implies (true implies false) → True. LaTeX already folds right, and provided already groups right — so only implies moves. Keep for the next major
    2 FreeVariables / VarsAndConsts No longer true. sum(k,k,1,n) → { n }, product → { n }, integral(t*b,t,0,1) → { b }, limit(t*b,t,0) → { b }. (The table in the body also says FreeVariables = y; it is and was pi, y.) Drop. What is left is a word in a summary and an optional additive property — minor-version work
    3 Experimental Still true. Exactly 11 public static members, no kernel consumer, no verdicts Re-scope, see below
    4 Obsoletions Still true. grep '\[Obsolete' over the kernel returns nothing, and reflection over the built assembly with DeclaredOnly agrees Keep the rule
    5 · #204 No longer true. sqrt(x) and x ^ (1/2) are the same entity — == is True, Complexity is 3 for both, 7 for both of sqrt(x) + sqrt(y). The gap came from 1/2 parsing as a Divf before 2.3.0. Only printing differs, and sqrt(x^2) is not collapsed Re-scope. A print form does not move a value, so by this issue's own criterion it is a BREAKING-CHANGES.md entry, not a major
    5 · #326 Still true, and piecewise(x provided x > 0, ...) round-trips. #813's near-miss now throws UnhandledParseException rather than an NRE Re-scope. New syntax can be added in a minor while the incumbent keeps parsing; only removing the incumbent is a major act
    5 · #721 No longer true as a breakage. #1090 merged: DomainConditionIn(Domain) is additive and Codomain is untouched. The issue's own last comment says "Done" Drop
    5 · #217 Still true. 1/0 → NaN, limit(1/x,x,0) → NaN, limit(1/x^2,x,0) → +oo Keep, but it needs a design first

    Two things this turns up that are not items

    AGENTS.md is stale. Lines 597-598 list #204, #326 and #721 under "Decisions only a major version
    may take". By the measurements above, #721 is done and #204 is not a value change — so two of its
    three entries are wrong. That file is read as authoritative; I would correct it whatever is decided
    here.

    Item 3 does not have to wait for a major. Promoting an experimental member and keeping one are
    both additive: a stable name plus an [Obsolete] forwarder on the experimental one can ship in a
    minor. Only removal waits, and item 4's rule then governs it. So the eleven verdicts are worth
    giving now rather than at the major — which also means item 3 stops blocking on the release.

    Net

    Of the seven, four survive as genuine breakages: implies associativity, removing whichever
    experimental members are not promoted, removing the incumbent piecewise form if a new one is
    adopted, and complex infinity.

    Not re-measured

    The [Obsolete] count on the F# and Interactive packages — I checked the kernel only — and whether
    any external consumer switches exhaustively over node types.

  5. Rafael-SOWNet commented on Sep 9, 2026

    @Rafael-SOWNet
    MemberAuthor

    Item 6 is done, and in one step rather than two. @Happypig375 decided on #1212:

    Just make | divides; semantic versioning allows such a breakage and the break should be put earlier in the breakages list.

    So the two-release migration this register proposed — a major where | becomes a parse error, then a major where it takes its meaning — is not what happened. | is divisibility now, the same node and the same precedence as divides, and the entry has moved to the top of BREAKING-CHANGES.md's unreleased section as asked.

    The register's job was to say what it would cost, so here is what it actually cost, measured rather than estimated: 7 of 8,525 tests referenced the old reading — five boolean-solver inputs written A | B, and two that existed to record this spelling as at risk. All five suites are green.

    The reason the number is small is the one this register gave for expecting it to be: or is the primary spelling and is what the library prints, so a round-tripped expression never contained a | to begin with. Replacing | with or restores the old reading exactly, everywhere.

    What the register got wrong is worth keeping. It said the change was unsafe to make in one step because x > 0 | x < -1 stays valid input and quietly answers something else. That is still true — it now reads as x > 0 divides x and 0 divides x < -1, since divisibility binds tighter than a comparison — and the decision was that a BREAKING-CHANGES row is the right protection for it after all. My argument was about the shape of the risk and not about its size, and I had not measured the size.

    Item 7, a != b reading as a! = b, is untouched and remains open as #1225.

  6. Happypig375 commented on Sep 10, 2026

    @Happypig375
    Member

    #1242 may change parsing before v3, so it needs adequate design and consideration before v3.

  7. Happypig375 commented on Sep 16, 2026

    @Happypig375
    Member

    Remember to do an architectural review every major version.

  8. Rafael-SOWNet commented on Sep 16, 2026

    @Rafael-SOWNet
    MemberAuthor

    Added as item 9, as a standing rule beside the obsoletions one: before a major version is named, an architectural review against #746's tiers and conditions, the layers, the public surface and the dependencies, written down, with what it decides landing here or in the release.

  9. Happypig375 commented on Sep 16, 2026

    @Happypig375
    Member

    refactor the API to reflect the best architecture as needed; major versions are for breaking changes. I doubt the best global architecture would change unnecessarily, it's usually some parts of the API that now shares a better abstraction with what we added.

  10. Rafael-SOWNet commented on Sep 16, 2026

    @Rafael-SOWNet
    MemberAuthor

    Reworded item 9 to say that: an API pass at each major against what was added since — the parts that now share a better abstraction with the new work get refactored to it there, since the major is where the breakage is allowed — rather than a redesign of the whole.

  11. Happypig375 commented on Sep 16, 2026

    @Happypig375
    Member

    I think for v2->3 we still want to do a global architecture review and restructure pass because v1->2 didn't consider how a Math OS (according to everything in #746) should be structured - it followed mostly v1. Everything across file structure and API structure as well as projects and packages need to be restructured according to that vision.

    And because this is a global architecture pass, ensure that it's Claude Fable doing it instead of Opus.

  12. Rafael-SOWNet commented on Sep 16, 2026

    @Rafael-SOWNet
    MemberAuthor

    Understood — item 9 now says both: the standing per-major review, and for v3 a global review and restructure pass against #746 over the file structure, the API, the projects and the packages, to be done by Claude Fable rather than Opus, before v3 is named.

  13. 61 remaining items

  14. Rafael-SOWNet commented on Sep 22, 2026

    @Rafael-SOWNet
    MemberAuthor

    The shapes look right to me, and the keeping question has an answer that removes the parameter: do not pass a list — round what a codomain says is a number, and let the caller widen the codomain. Rounding acts on maximal subtrees with no free variables, and the only decision is whether a constant is one of those. That is not a new axis: MathS.Settings.Codomain and the evaluation settings already decide what counts as a number elsewhere, and pi is a number in exactly the same sense that 2 is.

    So 4.5 pi rounds to 14.1 at three places, and 4.5 pi kept symbolic is the request that names pi as an atom. Three ways to say that, in the order I would rank them:

    1. A setting, scoped like the others — using var _ = MathS.Settings.SymbolicConstants.Set(new[] { MathS.pi }); around the call. It composes with every method in your list without adding an overload to any of them, it is the pattern the library already uses for precision and downcasting, and it says the thing once for a whole computation rather than per call.
    2. Substitute first. v.Substitute("pi", someSymbol).EvalToPlaces(3) is already possible today and needs nothing new; it is ugly but it proves the feature is not load-bearing.
    3. An optional parameter on each method, which is where the four overloads become eight.

    I would take (1), and I would not add keeping to the signatures.

    Two smaller things in the sketch:

    What the rounded node is. I would keep rationals, and not introduce an approximation-carrying node for this. EvalToNearest(step) has an exact answer — n step for an integer n — so the result is exactly representable in what we already have, and the moment a node carries "approximately" the whole algebra has to decide what x - x is when both sides are approximate-to-different-precisions. The Divf-versus-Rational question is the same one the parser already answers: a Rational prints as a/b and re-parses as a Divf that InnerSimplified folds back, so the round trip holds through the pipeline even though it does not hold node-for-node.

    ToString(PrintingContext). Agreed that it should be ToString rather than Stringize if the context is a settings record — that is what IFormatProvider is for in C#, and a record makes the overload unambiguous. I would keep Latexize as ToLatex(PrintingContext) for the same reason. Mutating the context to accumulate output, though, I would avoid: the moment printing writes into the thing that configures it, two threads printing with one context is a data race, and the library has had that shape of bug before (an instance cache copied by with). A FastString/StringBuilder return is fine; passing the builder in as a parameter is also fine, since the builder is the output and the context stays read-only.

    Fractional radices — worth saying out loud that EvalToSigFigs(figures, radix) with a non-integer radix has no digit string, so "significant figures" there means radix^k grids rather than digits, and a printer cannot render it positionally. I would either restrict the printing radix to integers ≥ 2 and let the value methods take any radix, or say in the doc that a fractional radix rounds but does not print.

  15. Happypig375 commented on Sep 22, 2026

    @Happypig375
    Member

    it is the pattern the library already uses for precision and downcasting

    those two specifically are not to be used as references for v3 because this eval we are designing replaces those two settings whole. Codomain will also become over alongside the preamble definition, so I suppose the preamble would be the setting rather than codomain(?) though a design that keeps default variable types and codomain separate from a combined preamble also makes sense... Meanwhile I agree that we don't have to fit everything into the methods if it feels more like a global configuration, but "in terms of pi" feels like a per-operation specifier rather than a global configuration about the whole system.

    Substitute first may work for this. It feels like a specific solution rather than in general though. It wouldn't work for something like "in terms of sqrt(2)" (though it's also questionable how much this question is asked. it feels like something a school-level math exercise might specify so it's not something too exotic to support)

    Another design point on radixes - "EvalToSigFigs(figures, radix) with a non-integer radix has no digit string" might be fixable. we already discussed a (1,2;3,4)#3 syntax above (this one's equivalent to 3^1+2+3*3^-1+4*3^-2) so a decimal or even transcendental radix might be in consideration too. (1,3;6)#7.5 or (4,3;2)#(23/4)? it might be a bit overboard but we don't support symbolic radixes in this syntax. Maybe (2,3;6)#sqrt(2) is still unambiguous but once (7;5,4)#(3+i) starts appearing we need to disambiguate with the cardinality operator again.

  16. Rafael-SOWNet commented on Sep 22, 2026

    @Rafael-SOWNet
    MemberAuthor

    Taking the correction: if this eval replaces the precision and downcasting settings, then pointing at them as the pattern is pointing at the thing being removed, and "in terms of pi" being per-operation is right — a global "pi is a symbol" would change what Evaled means everywhere, which is not what someone formatting one matrix is asking for.

    So here is a better answer, which needs no list, no setting and no extra parameter: make the two operations two methods, and let the name say whether the constants fold.

    • EvalToPlaces(3) — evaluate, then round. pi is a number to Evaled, so 4.5 pi is 14.1 at three places, and sqrt(2) + 1 is 2.41.
    • ToPlaces(3) / RoundToPlaces(3) — round the literals where they stand, evaluating nothing. 4.5 pi is 4.5 pi, sqrt(2) a b/3 is 0.333 sqrt(2) a b, and 4.5 pi + 1/3 is 4.5 pi + 0.333.

    That answers "in terms of sqrt(2)" without naming sqrt(2): it is not a literal, so the rounding that does not evaluate keeps it, and the one that evaluates folds it. It answers "in terms of pi" the same way, and "in terms of a" trivially. What it cannot express is "fold pi but keep sqrt(2)" — a partial evaluation — and I would leave that to Substitute or to a later Evaled(keeping:) on the evaluation side, where it belongs, rather than bending the rounding API around it. The per-operation choice is then the verb the caller picked, which is as per-operation as it gets.

    EvalToNearest, EvalToSigFigs and their radix overloads pair the same way. Whether both halves need to exist in v3 from the start is a separate question — EvalTo* alone is the common case, and To* can follow.

    On radixes: you are right that (1,2;3,4)#3 makes a general radix printable, and I withdraw "cannot render positionally". The constraint is weaker than integer-ness: any real |r| > 1 has positional expansions with digits 0..ceil(r) - 1. What changes for a non-integer r is that the expansion is no longer unique — in base phi, 1 is 1 and also 0.11, and every number with a terminating expansion has a second infinite one — so a printer has to fix a convention (the greedy expansion is the usual one) and a parser has to accept the others. That is worth writing into the syntax's definition rather than discovering it from a round-trip that does not close. For |r| <= 1 there is no expansion at all, and for a complex radix the digit set has to be given explicitly (base i - 1 needs {0, 1}, base 1 + i does not work with {0, 1}), which is where your cardinality-operator disambiguation earns its keep.

  17. Happypig375 commented on Sep 27, 2026

    @Happypig375
    Member

    Also for v3: consider using C# 15 discriminated unions (in fact, you can use it for new types right now) for the nodes, unless we really want to maintain an external contract for extending the node hierarchy (how would one even approach this and is this even necessary compared to fork and PR?)

  18. Happypig375 commented on Sep 28, 2026

    @Happypig375
    Member

    For v3:

    let f : CC -> CC
    f' // implicit differentiation of its only variable.
    f" // implicit second differentiation of its only variable?
    

    need to also consider if this is the only mathematical interpretation of ' and ", and how would this work for non-single-parameter functions and non-functions.

  19. Rafael-SOWNet commented on Sep 28, 2026

    @Rafael-SOWNet
    MemberAuthor

    consider using C# 15 discriminated unions [...] for the nodes, unless we really want to maintain an external contract for extending the node hierarchy

    There is no external contract to keep: the hierarchy is already closed to other assemblies, because Entity.DefaultCodomain is internal abstract, so no type outside AngouriMath.dll and its two friend assemblies (the unit tests and the benchmarks) can be a concrete Entity today. Fork and PR is already the only way to add a node, and closing the hierarchy officially loses nothing.

    What closing it would buy is exhaustiveness, and that helps less than it looks, because most of what a node owes is already a member the compiler asks for. Of the fourteen files the four nodes of #1529 had to touch:

    • seven hold abstract members (InnerSimplify, IntrinsicCondition, InvertNode, LatexizeNode, StringizeNode, ToSymPy, SortHashName, DefaultCodomain), which do not compile when missing;
    • two hold virtual members with a default. InnerDifferentiate's default is an unevaluated derivative(...), which is incomplete but not wrong. Substitute's is this == x ? value : this, which does not recurse, so a node that forgets it silently never substitutes into its argument;
    • three are tables: the JSON converter attribute, the pattern language's map of buildable types, and RealValued's list. A missing entry fails at run time, or quietly weakens an analysis;
    • two are the grammar and MathS's entry points.

    Neither the virtual defaults nor the tables are switches, so exhaustiveness would not see them. For v3 I would make Substitute abstract, or derive it once from Replace, which is already abstract, and write the three tables as switch expressions with no default arm. Then a closed hierarchy's exhaustiveness check covers them.

    On representation: if what ships is the nominal union U(A, B), that is a value wrapping one of its case types. Making Entity one would change every signature and every is in the library to get exhaustiveness that a closed class hierarchy gives without a wrapper, and the closed form fits nodes that are already records. I have not tried the preview here (this machine's SDK is .NET 10), so which of the two shapes actually shipped is the first thing I would check.

  20. Rafael-SOWNet commented on Sep 28, 2026

    @Rafael-SOWNet
    MemberAuthor

    f' // implicit differentiation of its only variable [...] need to also consider if this is the only mathematical interpretation of ' and ", and how would this work for non-single-parameter functions and non-functions.

    I would make ' a postfix operator on function values, typed (A -> B) -> (A -> B). f' is the function x -> derivative(f(x), x), so f'(3) is the derivative at 3 and f'' is (f')'. That answers all three questions in one place.

    Other meanings of the prime. Besides the derivative, a prime decorates a name (x' a new coordinate, A' a second point). It also means complement (sets, Boolean algebra), transpose (statistics; MATLAB and Julia spell the adjoint '), the dual space or the derived subgroup, and arcminutes. An operator on functions keeps the derivative and rejects the rest with a message rather than guessing: on a set it is a type error, not a complement, and transpose already has a spelling here, [...]T. The one to decide rather than reject is the decorated name. If x' should ever be an identifier, the lexer has to know before any type is known, so the choice is whether a prime can be part of a name. I would say no, and leave that job to x_1 and x_p.

    Several parameters. For f : RR^n -> RR^m, the derivative a prime means in analysis is the total derivative, the Jacobian Df, and for a scalar f the second derivative is the Hessian. With tensors in the library, that is a consistent reading of f' and f'' rather than an error. Partial derivatives keep an explicit spelling, derivative(f(x, y), x), because a prime cannot say which variable.

    Non-functions. (x^2 + y)' has no parameter list to say what to differentiate by, so I would reject a prime on an expression, even one with a single free variable. Otherwise (a x)' would mean different things depending on whether a is declared, the same kind of silent misreading the parser's refused names exist to prevent.

    Two more things come with the annotation:

    • f : CC -> CC asks for the complex derivative, which exists only where f is holomorphic. abs, conj, re, im and sgn have none, so the declared type is what decides whether (z -> |z|)' is sgn(z) or undefined. Absf already leaves its derivative unevaluated where the argument is not shown real (limit answers 1 for z/abs(z) whatever the phase of z #1186), and the type is what would show it.
    • " for the second derivative: I would spell it '' and keep " free. It is the character a C# caller has to escape inside "...", it is the likeliest delimiter for any text a v3 file ever holds, and after a number it reads as arcseconds.

    A naming note: "implicit differentiation" already names differentiating F(x, y) = 0, so the docs might say "the derivative in its only variable".

  21. Happypig375 commented on Sep 30, 2026

    @Happypig375
    Member

    Design principle for v3: exceptions are for unrecoverable errors (do we even have any??); the lack of value is null. If we model different reasons for a lack of value, return the reason itself as a union.

    Related: #1569

  22. Rafael-SOWNet commented on Sep 30, 2026

    @Rafael-SOWNet
    MemberAuthor

    Recorded as item 13 above. It lists the four exceptions thrown for a missing value today (CannotEvalException, NotSufficientlySupportedException, UncompilableNodeException, LimitOperationNotSupportedException) and what stays an exception. Two decisions come with it: whether a matrix operation on shapes it isn't defined for is misuse or a missing value, and keeping NaN distinct from null.

  23. Happypig375 commented on Sep 30, 2026

    @Happypig375
    Member

    The way to go would probably be to treat Undefined and Indeterminate as proper values on their own like Mathematica does (We won't have NaN in v3 because it's inherently a value defined by an approximate floating point type). Matrix multiplication on wrong sizes seems to be Undefined, and 0/0 would be Indeterminate (because it's one of the indeterminate forms). 1/0 would be Complex Infinity instead (see the issue on Complex Infinity). These two are about the mathematics itself; null is outside the mathematical language to indicate things like failing to integrate according to a specific strategy.

  24. Rafael-SOWNet commented on Sep 30, 2026

    @Rafael-SOWNet
    MemberAuthor

    Recorded in item 13: Undefined and Indeterminate replace NaN, a matrix operation on mismatched shapes is Undefined, 0/0 Indeterminate and 1/0 complex infinity (#217).

  25. Happypig375 commented on Oct 6, 2026

    @Happypig375
    Member

    For v3, how a formula is "simplified" needs adequate design. For example the general form to the solution of a quartic equation is usually denoted with helper variables:

    Wikipedia:

    The quartic formula fully written out in terms of the coefficients of the quartic, for the monic case. Because of its unwieldy nature, it is generally given in terms of auxiliary variables first computed from the coefficients.

    Using direct formulae may still be one of the output forms, but auxiliary variables might want to be included in our definition of simplification too.

  26. Rafael-SOWNet commented on Oct 9, 2026

    @Rafael-SOWNet
    MemberAuthor

    Recorded in item 12: a simplified result may be a body with named auxiliary quantities beside it, as the quartic formula is usually given, with the fully substituted formula one output form among several. It leaves two decisions open: which sub-expressions get a name, and whether the definitions travel with the entity or only shape how it prints.

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

    Opinions wantedWe are interested in your opinion about the topic

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions