Repository navigation
Pending breakages: changes agreed or arguable that cannot ship before a major version #1019
Description
Activity
- addedOpinions wantedWe are interested in your opinion about the topicWe are interested in your opinion about the topic
on Aug 23, 2026 If VarsAndConsts better means used variables instead, change it to that definition too! Identify the correct design here.
Measured on current
master, and the answer is yes to your suggestion — with a correction to what I
wrote in item 2 above.VarsAndConstsalready means "used", and its own documentation says soThe 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, piIt lists
x— the bound name — under variables and constants. SoVarsAndConstsis 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
VarsAndConstsneeds to move, and nothing aboutVars, which isVarsAndConstsminus 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 binderssum(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 } rightIt handles
LambdaandConditionalSetand nothing else. Σ and Π genuinely bind their index —
sum(k, k, 1, n)depends onnalone — 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 dtisb·t²/2 + C, a function oft, andderivative(f(t), t)denotes a function of
tas 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 VarsAndConstsevery variable or constant occurring, bound ones included correct; document the word occurring Varsthe same, minus constants correct, derived FreeVariablesnot bound by an enclosing binder incomplete: sum,product, andintegralwith limitsIs it still a pending breakage?
Narrower than item 2 claims, and I will correct that entry.
VarsAndConstsandVarsdo not
change at all.FreeVariableschanges for three node shapes — anyone relying on a Σ index appearing
free gets a different answer, so it owes aBREAKING-CHANGES.mdentry, 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
FreeVariablesnow, in its own PR with the three cases pinned, and leave this
issue for the changes that genuinely cannot ship before 3.0.Section 2's
FreeVariablesdefect 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— master1f3b02aeplus 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 }limitis the one this section does not list; it is recorded inBREAKING-CHANGES.mdunder
"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 tWorth 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, onlyFreeVariableshas
moved — which matches what the later comment in this thread concludes should happen:
VarsandVarsAndConstsreport 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 FreeVariablesyyMeasured now it is
y, pi— and that is whatFreeVariables'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 readsywhere the documented example readsy, 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:VarsAndConstsreturning 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.
Re-measured every item against the tree at
5d802babrather than reading the docket back. Three of
the seven no longer hold.item now verdict 1 impliesright-assocStill true. AngouriMath.g:221-224loops$value.Implies(...).false implies true implies false→False;false implies (true implies false)→True. LaTeX already folds right, andprovidedalready groups right — so onlyimpliesmoves.Keep for the next major 2 FreeVariables/VarsAndConstsNo 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 saysFreeVariables=y; it is and waspi, 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 staticmembers, no kernel consumer, no verdictsRe-scope, see below 4 Obsoletions Still true. grep '\[Obsolete'over the kernel returns nothing, and reflection over the built assembly withDeclaredOnlyagreesKeep the rule 5 · #204 No longer true. sqrt(x)andx ^ (1/2)are the same entity —==isTrue,Complexityis 3 for both, 7 for both ofsqrt(x) + sqrt(y). The gap came from1/2parsing as aDivfbefore 2.3.0. Only printing differs, andsqrt(x^2)is not collapsedRe-scope. A print form does not move a value, so by this issue's own criterion it is a BREAKING-CHANGES.mdentry, not a major5 · #326 Still true, and piecewise(x provided x > 0, ...)round-trips. #813's near-miss now throwsUnhandledParseExceptionrather than an NRERe-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 andCodomainis 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)→+ooKeep, but it needs a design first Two things this turns up that are not items
AGENTS.mdis 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:
impliesassociativity, removing whichever
experimental members are not promoted, removing the incumbentpiecewiseform 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.- added a commit that references this issue
on Sep 9, 2026 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 asdivides, and the entry has moved to the top ofBREAKING-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:
oris the primary spelling and is what the library prints, so a round-tripped expression never contained a|to begin with. Replacing|withorrestores 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 < -1stays valid input and quietly answers something else. That is still true — it now reads asx > 0 divides x and 0 divides x < -1, since divisibility binds tighter than a comparison — and the decision was that aBREAKING-CHANGESrow 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 != breading asa! = b, is untouched and remains open as #1225.#1242 may change parsing before v3, so it needs adequate design and consideration before v3.
Remember to do an architectural review every major version.
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.
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.
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.
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.
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.
61 remaining items
The shapes look right to me, and the
keepingquestion 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.Codomainand the evaluation settings already decide what counts as a number elsewhere, andpiis a number in exactly the same sense that2is.So
4.5 pirounds to14.1at three places, and4.5 pikept symbolic is the request that namespias an atom. Three ways to say that, in the order I would rank them:- 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. - 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. - An optional parameter on each method, which is where the four overloads become eight.
I would take (1), and I would not add
keepingto 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 stepfor an integern— so the result is exactly representable in what we already have, and the moment a node carries "approximately" the whole algebra has to decide whatx - xis when both sides are approximate-to-different-precisions. TheDivf-versus-Rationalquestion is the same one the parser already answers: aRationalprints asa/band re-parses as aDivfthatInnerSimplifiedfolds back, so the round trip holds through the pipeline even though it does not hold node-for-node.ToString(PrintingContext). Agreed that it should beToStringrather thanStringizeif the context is a settings record — that is whatIFormatProvideris for in C#, and a record makes the overload unambiguous. I would keepLatexizeasToLatex(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 bywith). AFastString/StringBuilderreturn 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 meansradix^kgrids 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.- A setting, scoped like the others —
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
overalongside 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)#3syntax above (this one's equivalent to3^1+2+3*3^-1+4*3^-2) so a decimal or even transcendental radix might be in consideration too.(1,3;6)#7.5or(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.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
Evaledmeans 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.piis a number toEvaled, so4.5 piis14.1at three places, andsqrt(2) + 1is2.41.ToPlaces(3)/RoundToPlaces(3)— round the literals where they stand, evaluating nothing.4.5 piis4.5 pi,sqrt(2) a b/3is0.333 sqrt(2) a b, and4.5 pi + 1/3is4.5 pi + 0.333.
That answers "in terms of
sqrt(2)" without namingsqrt(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 ofa" trivially. What it cannot express is "fold pi but keep sqrt(2)" — a partial evaluation — and I would leave that toSubstituteor to a laterEvaled(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,EvalToSigFigsand 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, andTo*can follow.On radixes: you are right that
(1,2;3,4)#3makes a general radix printable, and I withdraw "cannot render positionally". The constraint is weaker than integer-ness: any real|r| > 1has positional expansions with digits0..ceil(r) - 1. What changes for a non-integerris that the expansion is no longer unique — in basephi,1is1and also0.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| <= 1there is no expansion at all, and for a complex radix the digit set has to be given explicitly (basei - 1needs{0, 1}, base1 + idoes not work with{0, 1}), which is where your cardinality-operator disambiguation earns its keep.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?)
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.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.DefaultCodomainisinternal abstract, so no type outside AngouriMath.dll and its two friend assemblies (the unit tests and the benchmarks) can be a concreteEntitytoday. 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 unevaluatedderivative(...), which is incomplete but not wrong.Substitute's isthis == 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
Substituteabstract, or derive it once fromReplace, 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. MakingEntityone would change every signature and everyisin 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.- seven hold abstract members (
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 functionx -> derivative(f(x), x), sof'(3)is the derivative at 3 andf''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. Ifx'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 tox_1andx_p.Several parameters. For
f : RR^n -> RR^m, the derivative a prime means in analysis is the total derivative, the JacobianDf, and for a scalarfthe second derivative is the Hessian. With tensors in the library, that is a consistent reading off'andf''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 whetherais declared, the same kind of silent misreading the parser's refused names exist to prevent.Two more things come with the annotation:
f : CC -> CCasks for the complex derivative, which exists only wherefis holomorphic.abs,conj,re,imandsgnhave none, so the declared type is what decides whether(z -> |z|)'issgn(z)or undefined.Absfalready 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".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
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 keepingNaNdistinct from null.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.
Recorded in item 13: Undefined and Indeterminate replace
NaN, a matrix operation on mismatched shapes is Undefined,0/0Indeterminate and1/0complex infinity (#217).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.
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.
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.
impliesbecomes right-associativeRaised on #1009. The grammar folds
impliesto 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:
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
providedat the same time. It folds right today, which #1009 documents and tests. Ifimpliesmoves to the right, the two agree andSyntax.md's rule becomes "^,impliesandprovidedgroup to the right"; that is worth settling in one go rather than twice.2.
FreeVariables,VarsAndConsts, and a property that does not existRaised on #989.
Measured on
masterat6b93b401, against that definition. The documented example isLambda(x, x * 2 + sin(y * pi)):Varsx, yVarsAndConstsx, y, piy, pi—xis boundFreeVariablesyySo
VarsAndConstsreturns bound names, and its XML example pins that. It is wrong forlambda, 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)is6. Nothing about that value depends onk, 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 thatCalculusOperatorandConditionalSetboth declare aVar(Let a binder bindi, in every binder there is (#976) #986).VarsAndConsts— free variables and constants, per the definition above.Vars/VarsAndConstsdo today, so the behaviour is not lost, it is renamed to something that describes it.Part 1 of #989 — the
%1placeholder 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·SymbolicFormOfCosineEach wants one of three verdicts, and the third is why this is on a breakage list:
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
Experimentalis 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.mdnames three under "Decisions only a major version may take", all still open:SimplificationContract.mdcalls it "open and deliberately a major-version question".piecewise. Simplification pattern for Piecewise #327's simplification half shipped; the syntax was never touched and no design is recorded.Codomainwith aprovided … in RRcondition. Named on Goal: Math OS — a ten-year vision for AngouriMath as an open mathematical reasoning platform #746 as a consumer of the retired expression-metadata item, and still open.And one more that is breaking by construction:
1/0fromNaN, and every consumer branching onNaNis blast radius.6.
|stops meaningorRaised on #1212, where the invitation was explicit:
|is an alias forortoday. 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 on281e0d0c:2 | 62 or 6— a disjunction of two numbers{ x | x > 0 }{ x or x > 0 }— aFiniteSetof one element, that element a disjunctionThe 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
oris 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:
Nothing fails to compile; the answer moves silently. So two steps rather than one:
|becomes a parse error, with a message namingor. Loud rather than silent, and it costs a release of waiting to remove the entire class of quietly-wrong results.|means divides, and "given" inside an expectation or probability — the scoping @Happypig375 proposed on Math Club orientation week questions #1212, whereE/Pintroduce 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 itRaised 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:Solve("x")x <> 3not (x = 3)not x = 3{ x : not x = 3 }a != ba! = b, anEqualsfa! = ba ≠ b≠The relation exists only as a negated equality, which the solver does not read, and
!=is silently a factorial equation.What moves:
NotEqualsf(a, b). It prints as one relation, is solved (x ≠ 3isCC \ { 3 }, orRR \ { 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.≠,<>and!=, with!=one token, as Mathematica reads it beside the same postfix!:a != banda!=bare the relation, and the factorial equation is writtena! = b, with the space.<>in ASCII,≠in Unicode output,\neqin LaTeX.Why a minor cannot carry it:
a!=banda != bchange meaning, andnot (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,ERationalandEDecimal, and 48 published members expose them —Integer.EInteger,Rational.ERational,Real.EDecimal,Real.Create(EDecimal),Rational.Create(ERational),Complex.Create(EDecimal, EDecimal), theDeconstructs, the implicit conversions onEntity,Number,Real,Rational,IntegerandComplex,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
EIntegerand 109 ns inSystem.Numerics.BigInteger, and anEDecimalmultiply or divide is 8–11× a decimal on aBigIntegermantissa; the fixed-point series already run onBigInteger(#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:BigIntegerbehind an unchangedEDecimalsurface;AngouriMath.Numericstower —BigInteger, a(BigInteger, BigInteger)rational and a(BigInteger mantissa, int exponent)decimal with a precision context — used byNumberbehind the current properties, which become conversions; measurable on the whole gate before any signature changes;PeterO.Numbersleaves the package's dependencies, andBREAKING-CHANGES.mdcarries the migration: everyEDecimala caller passes today becomes a decimal of ours (a parse fromEDecimal.ToString()is lossless), everyEIntegeraBigInteger(EInteger.ToBytes/new BigInteger(bytes)both ways), andSetting<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
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; anERationalbeside aRational; the several spellings of "a polynomial overx" thatTryGetPolynomial,MultivariatePolynomialand 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.mdfrom 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) = 2isTrue, becauseEqualsfsubtracts the sides, evaluates, and asksNumber.IsZero, which is|d| < PrecisionErrorZeroRange = 1e-16;Real == Realuses a third constant,PrecisionErrorCommon = 1e-6; andEvaledruns everything toDecimalPrecisionContext'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):
pi,e. A comparison of exact quantities is decided exactly, symbolically where the numbers are algebraic, and enumerates no digits:1 < 1 + 1^(-1000) < 2is settled by arithmetic on rationals.3 < sqrt(2) + sqrt(3) < 4needs one digit after the point and0 < sqrt(2) + sqrt(3) - pi < 1needs more, and each asks for what it needs ("Evaled as necessary"). The settingDecimalPrecisionContext : 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.double, or through an explicitRealconstructor, 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.EDecimaland 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 threePrecisionError*settings andEvaleditself all change what they answer, and everyTruethat came from a tolerance may becomeFalse— recorded inBREAKING-CHANGES.mdper 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,
Solvefalls back to Newton's method (MathS.Settings.AllowNewton, on by default) and answers the points it finds as aFiniteSet, which reads exactly like a complete, exact solution set.abs(x - 2) = abs(x - 3)was three points of the lineRe x = 5/2. #1578 keeps the fallback away from equations real for every complexx, which is a guard and not the separation.What moves:
Solveanswers 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
Solveanswers 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:
Simplifyreturns whichever candidate it generated has the lowestSimplifiedRate, 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:
Each is checkable on the answer, so
Simplifyis tested against the list the waycanonchecktests 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, asFactorizeandExpandalready 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(...)whereS = ...,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
Simplifygives 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.
EvalNumericalandEvalBooleanthrowCannotEvalExceptionwhere an expression has no value (Delete CannotEvalException in v3: EvalNumerical and EvalBoolean return null where there is no value #1570).NotSufficientlySupportedException, from 12 sites (Delete NotSufficientlySupportedException in v3: an unsupported operation returns null #1569).CompileandSolveNtthrowUncompilableNodeExceptionwhere the compiler has no form for a node, from 17 sites. Solve throws UncompilableNodeException when its Newton fallback meets floor, max or another node the compiler has no form for #1603 keepsSolvefrom reaching it; The floors, the rounding and the extremes compile, and Solve's numerical search declines what it cannot represent #1605 leavesSolveNt's exception in place for 2.x, because it is a public contract.LimitOperationNotSupportedException.#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:
SolveRequiresStatementExceptionandWrongNumberOfArgumentsException;AngouriBugException, 57 sites;TryParse-style entry that returns null, or a union with the parse error, can sit besideParse.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)."
InvalidMatrixOperationExceptionandInvalidNumberException, 24 sites between them).NaNin v3.0/0is Indeterminate, as one of the indeterminate forms, and1/0is 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 / bgive a value where the expression has none.1/(1/x)simplifies tox, which is0atx = 0, where1/(1/x)isNaN. O4 ofSimplificationContract.mdforbids 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/0is complex infinity (Complex Infinity #217) and0/0is Indeterminate (item 13), so1/(1/0)is1/∞ = 0, the same asx. Measured with sympy'szooandnan, 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:a/(b/c) → a*c/b(a/b)/(c/d) → (a*d)/(b*c)(a/b)/c → a/(b*c)(a^n)^m → a^(n*m),mwholea/csc(b) → a*sin(b), and likewisesec/cosandcot/tanAll 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 → 1at 0, ∞ and Indeterminate;0·a → 0anda − a → 0at ∞ and Indeterminate;(a·b)/(a·c) → b/c, which is what pairwise grouping does when it cancels;e^ln(a) → aat 0 and ∞. Whether this one belongs here depends on whether Complex Infinity #217's infinity is directed: in Mathematica,Log[0]is-Infinityande^-∞is 0.Each such rule declares its condition beside its pattern, where
when:sits today, andsoundcheckchecks 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: aprovidedinside 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 RRandover CCdon'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:ooand-ooare reals. Each part of a complex number can be infinite on its own, as inoo + ior-oo i. There is no complex infinity:1/0isNaN(Complex Infinity #217). So the default is the complex plane with an infinity in each axis direction.RRtreats the two infinities differently:-oo in RRsimplifies toTrue, whileoo in RRis left undecided.oo - ooand0 * oosimplify to0. These are item 14'sa − a → 0and0·a → 0at ∞.What moves:
RRandCCwithout infinities; the extended reals,RRbar, which areRRwith-∞and+∞; and the two projective lines,RRhatandCChat, which areRRandCCeach 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,ProjectiveRealsandProjectiveComplexes. The default has no notation, so it has only its full name.∞̃and\tilde{\infty}in LaTeX as Mathematica (ComplexInfinity) and Calcium (UnsignedInfinity) print it, with SymPy'szooproposed for its ASCII spelling: the point at infinity ofRRhatis the same point asCChat's, sinceP¹(R) ⊂ P¹(C), and it is the default's undirected infinity, Complex Infinity #217's1/0.CC, the directed infinities,∞̃, and item 13's two exceptional values, Undefined and Indeterminate, where Calcium has one. Calcium's sets match these:ExtendedRealNumbersisRRbar,ProjectiveRealNumbersandProjectiveComplexNumbersareRRhatandCChat. Writtenover ComplexSingularityClosurewhere it has to be named.ooby algebra:+∞in the default and inRRbar; underRRhatorCChat,ooand-ooboth name the one point at infinity; underRRorCC, neither is a member.RRas over the default. What an algebra decides is membership —Undefined in RRis false,Undefined in ComplexSingularityClosuretrue — and the value at its own special points:1/0is Undefined overRR, outside it, so division is partial there; and∞̃over the projective lines and the default, inside them, so division is total there.overs: every named set sits inside the default, so nodes under differentovers combine in the default and the result is the default's:(oo over RRbar) + (oo over RRhat)is+∞ + ∞̃, Indeterminate. Noovermeans the default, and a prelude can set another for every node.x/0 = ∞forx ≠ 0,x ± ∞ = ∞andx/∞ = 0for finitex,x·∞ = ∞forx ≠ 0;∞ + ∞,∞ - ∞,0·∞,∞/∞and0/0are Indeterminate; and there is no order, so∞ > 0has no value. Where they differ fromRRbaris limits:1/xat0tends to∞on the projective line and has no limit over the extended reals.∞·e^(iθ), with+∞and-∞as the real directions, plus one undirected complex infinity, which is Complex Infinity #217's1/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, andovertakes the named sets only.over RRandover CCexclude every infinity. A limit of+∞has no value overRR, andx in RRis false at±∞.oo - ooand0·ooare Indeterminate.a/a = 1division,a^2 = 0 ⇒ a = 0no zero divisors,a b = b acommutativity -- 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 177SoundUnderAssumptionsrules declare and do not name.ZZ/nZZ, the integers modulo the idealnZZ,na number or a symbol: a field for a primen, with zero divisors otherwise (2 * 3 = 0inZZ/6ZZ). Today'sa = b (mod n)is membership in one class of it.ZZ_3is not used, being the 3-adic integers to many readers.ε^2 = 0, for automatic differentiation) would be a separate algebra, outside the default.Why a minor cannot carry it:
oo - oo,0·ooand the membership of every infinity change their answers, and #217 changes1/0.16.
amcli's commands say what they doRaised 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:
evalevaluateamcli eval "x + 1"printsx + 1, and the usage line's "evaluates to a number" claims too muchsimpsimplifyfsimpnormalizeInnerSimplified, andfsimpnames neitherdiffdifferentiatediffreads as a differencesubsubstitutesubreads as subtractinfodescribedescribe, and the name says what it printssolve,latexWhy 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.
Solvetakes several unknowns, andEquationSystemgoesRaised 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
Solvealready readsandfor one unknown.MathS.Equations(...).Solve(x, y)is a second entry point that takes each equation in= 0form 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.EquationsandEquationSystemare 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.Equationsbreaks every caller, and a minor can only addSolve(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.