Repository navigation
Let limits be told whether they are being read over the reals (#719, step 1) - #755
Merged
Merged
Conversation
…step 1)
AngouriMath's one-sided limits do not stay inside the reals:
lim x->0- ln(x) -oo
lim x->0- x * ln(x) 0
lim x->0- x ^ x 1
None of those is a real limit. The logarithm of a negative real is
`ln|x| + i*pi`, and what comes back is the magnitude of something that is not a
real number at any point of the approach.
That is the missing information behind a family of questions, and #719 names it:
a two-sided limit is the two one-sided ones agreeing, and that promotion cannot
be made while the sides answer from the complex continuation, because it would
give `lim x->0 x^x` the value 1 -- which the suite pins as non-existent, since
`x^x` is not real for negative x. The guard has to refuse the whole promotion
because it cannot tell that case from the honest one.
So: `MathS.Settings.Codomain`. It is a statement about the reading, where an
Entity's own `Codomain` is a statement about a node. `Domain.Complex` by
default, so nothing changes for anyone who does not ask -- the default is
pinned by tests against the values, not against the setting.
Under `Domain.Real` a limit approached through values the function does not take
is withdrawn:
lim x->0- ln(x) unevaluated (0+ is still -oo)
lim x->0- x * ln(x) unevaluated (0+ is still 0)
lim x->0- x ^ x unevaluated (0+ is still 1)
lim x->0- sqrt(x) unevaluated
lim x->-oo ln(x) unevaluated
lim x->0 sin(x)/x 1, either side, unchanged
Whether a function takes non-real values near a point is not a property of the
tree, so it is decided by sampling the approach. The direction of error is
chosen: a sample that will not evaluate, or comes back non-finite, is passed
over rather than counted, and an expression carrying a second variable is not
judged at all. The answer is only ever "yes, demonstrably" or "not shown", and a
limit is withdrawn on evidence rather than on the absence of it.
Two things this cost, both worth knowing:
- **It has to be asked before the rewrites, not after.** Each of them asks for
limits of its own; under this reading those come back unevaluated, and
evaluating an unevaluated limit asks for it again through the same rewrite,
without end. Placed after `ApplyFirstRemarkable` it overflows the stack.
- **The samples stop at a thousandth.** The sample point goes into the
expression, and an expression may put it in an exponent: `(1 + x)^(1/x)` at
1e-9 is 1.000000001 raised to a billion at a hundred digits, six times over on
every limit the machinery takes, and a 20 ms limit became a timeout. Sampling
less far in only ever misses, and a miss answers exactly as before.
Under a real codomain limits are slower -- `(1 + x)^(1/x)` at 0+ is 273 ms
against 20 -- because the sampling is on the recursive path. That is opt-in and
measured rather than hidden.
**Step 2, promoting agreeing one-sided limits, is deliberately not here.** #719
asks for step 1 to land with its own measurements first, since it changes what
every one-sided limit answers under a real codomain. A test pins the
unpromoted state so the next step has somewhere to land.
Suite 5107 -> 5135 passed / 0 failed, F# 130/130, corpus 112/117 with every
verdict and answer byte-identical, rootcheck 596/596, propcheck 0 failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rafael-SOWNet
added a commit
that referenced
this pull request
Aug 6, 2026
) (#756) A two-sided limit is the two one-sided ones agreeing, and AngouriMath could not say so. The branch that compares them compares two `ComputeLimitDivideEtImpera` results -- the bare descent -- where a caller who names a side gets that descent *and* everything behind it: SolveAsIndeterminatePower, l'Hopital's rule, and the substitution that moves a finite destination out to infinity. The promotion could not be made before because the sides did not stay inside the reals. Over the complex plane `lim x->0- x^x` answers 1 from the continuation, so promoting agreement would give `lim x->0 x^x` the value 1 -- and `x^x` is not real to the left of 0 at all. #755 gave limits a codomain to read; this uses it, and **only under `Domain.Real`**, which is exactly what makes it sound: there that side has no value to agree with, so the case this could get wrong is the case that no longer arises. The expression #596 reported, at 0, over the reals: 1/ln(x + sqrt(x^2+1)) - 1/ln(x+1) left -1/2 already right -1/2 already both unevaluated -> -1/2 What must not be promoted, and is not: x * ln(x), x^x, sqrt(x), ln(x) real on one side only, so unevaluated 1/x, abs(x)/x, e^(1/x), arctan(1/x) sides disagree, so still NaN A second change came with it, and it belongs here rather than in #755 because this is what exposed it. A **two-sided** limit is now withdrawn if *either* approach leaves the reals, where the check asked about one direction only. `lim x->0 sqrt(x)` answered 0 while its own left-hand limit had been withdrawn, which is the two halves disagreeing about the same reading. Asked only where the answer would otherwise be NaN or nothing, so no limit that already answers pays for it, and bounded by a depth of two: each side may reach a two-sided limit of a subexpression, and without a bound the two questions branch into four and so on down the tree. Nothing changes by default. The complex reading answers as it always has, which is what keeps `LimitTest.TestNoLimit` meaning what it meant, and that is pinned. Suite 5135 -> 5147 passed / 0 failed, F# 130/130, corpus 112/117 with every verdict and answer byte-identical, rootcheck 596/596, propcheck 0 failures. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 6, 2026
Rafael-SOWNet
added a commit
that referenced
this pull request
Aug 6, 2026
sqrt(x^2) came back as x, which at -0.63 is -0.63 where it is 0.63
(x^2)^(3/2) came back as x^3, which at -2 is -8 where it is 8
sqrt(-x) came back as i*sqrt(x), which at -0.63 is -0.7937, not 0.7937
Two rules, each applied without the condition it needs. `(a^b)^c = a^(b*c)` holds
for a positive `a`, and for any `a` when `c` is whole -- `(a^b)^3` is `a^b`
multiplied by itself three times however `a` is signed. `(a*x)^c = a^c * x^c`
holds for a positive `a`, and again for a whole `c`. Both are restricted to
exactly that.
Where they hold they are unchanged: `(x^(1/2))^2` is still `x`, `(-x)^2` is
still `x^2`, `(2^2)^(1/2)` is still `2`, `(x^2)^2` is still `x^4`.
`sqrt(x^2)` is `abs(x)` and the library does not write that for you. It leaves
the expression alone rather than answering something false; writing `abs`
requires knowing the expression is real, which the codomain added in #755 now
makes sayable and the simplifier does not yet read.
Two test expectations move, and they are different cases:
- `PowerQuotientGatheringTest` pinned `(sqrt(x) + 1)^x / sqrt(x)^x` as
deliberately *not* gathered, because gathering it would have squared the
numerator. **It gathers now, and better**: `sqrt(x)^x` no longer flattens to
`x^(x/2)`, so both exponents stay `x` and the ordinary pairing rule reaches
it. The gap that file is about was a rewrite running ahead of the pairing, and
one less rewrite is one less way to run ahead of it.
- `SolveInequality.Test` pinned `(x - a)(x + a) <= 0` as `[a; -a]`. **That got
worse and it is a fudge coming out**, not a regression to hide: `[a; -a]` is
right for `a < 0` and empty for `a > 0`, and it read that way only because
`sqrt(4a^2)` was simplifying to `2a`. It now answers
`{ a, -a } \/ (sqrt(a^2); -sqrt(a^2))`, whose interval is empty for either
sign. Neither is right for both, because the solver has no case split on the
sign of a symbolic coefficient. Filed as #757 and pinned as it now stands with
a comment pointing there, rather than deleted -- the expectation is still
evidence, of something other than correctness.
Found by `work/simpsweep`, which generates the expressions it checks rather than
listing them. **Its count of disagreements over 10463 expressions goes 30 to 0**,
and those 30 were the whole of what it had left.
Suite 5147 -> 5169 passed / 0 failed, F# 130/130, corpus 112/117 with every
verdict and answer byte-identical, rootcheck 596/596, propcheck 0 failures.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Step 1 of #719. Step 2 — promoting agreeing one-sided limits — is deliberately not here; see the bottom.
What was wrong
AngouriMath's one-sided limits do not stay inside the reals:
None of those is a real limit. The logarithm of a negative real is
ln|x| + i*pi, and what comes back is the magnitude of something that is not a real number at any point of the approach.#719 names why this matters beyond the three lines: a two-sided limit is the two one-sided ones agreeing, and that promotion cannot be made while the sides answer from the complex continuation — it would give
lim x->0 x^xthe value 1, which the suite pins as non-existent becausex^xis not real for negative x. The guard has to refuse the whole promotion because it cannot tell that case from the honest one. The codomain is exactly the missing information.What this adds
MathS.Settings.Codomain— a statement about the reading, where anEntity's ownCodomainis a statement about a node.Domain.Complexby default, so nothing changes for anyone who does not ask. The default is pinned by tests against the values themselves, not against the setting's value.Under
Domain.Real:Domain.Reallim x->0- ln(x)-oolim x->0+ ln(x)-oo-oolim x->0- x * ln(x)0lim x->0+ x * ln(x)00lim x->0- x ^ x1lim x->0- sqrt(x)0lim x->-oo ln(x)lim x->0 sin(x)/x11lim x->0 (1 + x)^(1/x)eeIt is the approach that is judged, not the expression — which is why the same expression is withdrawn on one side and untouched on the other.
How it decides, and which way it errs
Whether a function takes non-real values near a point is not a property of the tree, so the approach is sampled. The direction of error is chosen deliberately: a sample that will not evaluate, or comes back non-finite, is passed over rather than counted, and an expression carrying a second variable is not judged at all. The answer is only ever "yes, demonstrably" or "not shown" — a limit is withdrawn on evidence, never on the absence of it.
Two things this cost, both in the code as comments
ApplyFirstRemarkableit overflows the stack — which is how it was found.(1 + x)^(1/x)at1e-9is1.000000001raised to a billion, at a hundred digits, six times over on every limit the machinery takes. A 20 ms limit became a timeout. Sampling less far in only ever misses, and a miss answers exactly as before.Under a real codomain limits are slower —
(1 + x)^(1/x)at 0+ is 273 ms against 20 — because the sampling sits on the recursive path. Opt-in and measured rather than hidden.Measured
work/rootcheck596/596 clean,work/propcheck0 failures.What is not here
Step 2, the promotion of agreeing one-sided limits to a two-sided one, which is the point of the setting. #719 asks for step 1 to land with its own measurements first, because it changes what every one-sided limit answers under a real codomain, and this is the subsystem where a plausible-looking change has broken things more than once. A test pins the unpromoted state so the next step has somewhere to land.
One correction to #719 for whoever picks up step 2: it says "
ComputeLimitreadsMathS.Settings.Codomain" as though the setting existed. It did not — adding it was part of the work, and it is what this PR is.🤖 Generated with Claude Code