Skip to content

Re-read the second remarkable limit after simplification (#738) - #739

Merged
Rafael-SOWNet merged 2 commits into
masterfrom
fix/remarkable-limit-after-simplification
Aug 5, 2026
Merged

Rafael-SOWNet merged 2 commits into
masterfrom
fix/remarkable-limit-after-simplification

Conversation

@Rafael-SOWNet

@Rafael-SOWNet Rafael-SOWNet commented Aug 5, 2026 •

Copy link
Copy Markdown
Member

Closes the first half of #738.

lim x->+oo (x - 5)^x / x^x came back 1 where it is e^(-5), and so did every quotient of two powers whose base ratio tends to 1.

Cause

The rule that reads a 1^oo — the second remarkable limit — runs once, at the top of ComputeLimit, against the expression as it was written. A quotient of powers is not a 1^oo at that point, so the rule has nothing to match on. The descent then reaches SimplifyAndComputeLimitToInfinity, whose first act is to simplify, and (x - 5)^x / x^x becomes ((x - 5)/x)^x — a 1^oo that nothing will now re-read, because the rule that reads one has already run. Straight on into the solvers, where 1^(+oo) evaluates to 1.

Written as the single power the same limits were always right (((x - 5)/x)^x gives 1/e^5), which is what locates the cause in the ordering rather than in the rule.

Fix

Neither the simplification nor the rule is wrong; what was wrong is that a form created by simplification was judged by rules that ran before it existed. The same rule is asked again at the point where the expression is in the form the solvers read it in — which is also where every destination has already been normalised to +oo. Guarded by a depth counter like the other rules in that file, since each level asks for limits of its own. 52 lines added to the library, nothing removed.

This closes both of the routes #738 separates, because both run through that one point after the simplification: the SolveBySubstitution route, where (1 + 1/x)^x substitutes to an exact 1^(+oo), and the Powf descent route, where ((x - 5)/x)^x builds 1^x and that simplifies to 1. It also turns out not to need #736 underneath it, which earlier tracing had expected — the rewrite answers these outright rather than dropping them through to Gruntz.

Measured

lim x->+oo before after
(x - 5)^x / x^x 1 1/e^5 (309 ms)
(x + 1)^x / x^x 1 e (16 ms)
(x + 3)^x / x^x 1 e^3 (21 ms)
x^x / (x + 1)^x 1 1/e (57 ms)
x^x / (x - 5)^x 1 e^5 (50 ms)
(2x + 1)^x / (2x)^x 1 sqrt(e) (59 ms)
(x - 1)^x / x^x unevaluated, 5.2 s 1/e, 163 ms

The neighbouring quotients, where the base ratio does not tend to 1, are untouched: (2x)^x / x^x and (x^2)^x / x^x are still +oo, x^x / (2x)^x still 0.

What this does not do

Validation

Full suite 4909 passed / 0 failed (4889 on master + 20 new cases), F# 130/130, corpus 112/117 with 0 wrong, 0 error, 0 timeout — identical to master's baseline. The suite still runs in 3 m 30 s, so the extra re-read costs nothing measurable.

🤖 Generated with Claude Code

`lim x->+oo (x - 5)^x / x^x` came back 1 where it is `e^(-5)`, and so did
every quotient of two powers whose base ratio tends to 1.

The rule that reads a `1^oo` runs once, at the top of `ComputeLimit`,
against the expression as it was written. A quotient of powers is not a
`1^oo` at that point -- it is a quotient -- so the rule had nothing to
match on. The descent then reaches `SimplifyAndComputeLimitToInfinity`,
whose first act is to simplify, and `(x - 5)^x / x^x` becomes
`((x - 5)/x)^x`: a `1^oo` which nothing now re-reads, because the rule
that reads one has already run. Straight on into the solvers, where
`1^(+oo)` evaluates to 1.

Neither the simplification nor the rule is wrong. What was wrong is that
a form *created* by simplification was judged by rules that ran before it
existed. So the same rule is asked again where the expression is in the
form the solvers read it in, which is also where every destination has
already been normalised to `+oo`. Guarded by a depth counter like the
other rules here, since each level of it asks for limits of its own.

This closes both of the routes the issue separates, because both of them
run through that one point after the simplification has happened: the
`SolveBySubstitution` route, where `(1 + 1/x)^x` substitutes to an exact
`1^(+oo)`, and the `Powf` descent route, where `((x - 5)/x)^x` builds
`1^x` and that simplifies to 1. It also turns out not to need PR #736
underneath it, which the earlier tracing had expected: the rewrite
answers these outright rather than dropping them through to Gruntz.

Measured, at x -> +oo:

  (x - 5)^x / x^x        1  ->  1/e^5     (309 ms)
  (x + 1)^x / x^x        1  ->  e         (16 ms)
  (x + 3)^x / x^x        1  ->  e^3       (21 ms)
  x^x / (x + 1)^x        1  ->  1/e       (57 ms)
  x^x / (x - 5)^x        1  ->  e^5       (50 ms)
  (2x + 1)^x / (2x)^x    1  ->  sqrt(e)   (59 ms)
  (x - 1)^x / x^x        unevaluated in 5.2 s  ->  1/e in 163 ms

and the neighbouring quotients, where the base ratio does not tend to 1,
are untouched: `(2x)^x / x^x` and `(x^2)^x / x^x` are still `+oo`,
`x^x / (2x)^x` still 0.

What this does not reach: `(x^2 + 1)^x / (x^2)^x`, which `Simplify`
leaves written as a quotient, so no `1^oo` is created for anything to
re-read. That one is left unevaluated rather than answered wrongly, and
its cause is in the simplification rather than in the limits -- the same
quotient written `((x^2 + 1) / x^2)^x` is answered 1 without any of
this. Filed separately.

The other half of #738 -- that `1^(+oo)` evaluates to 1 at all -- reaches
everything that evaluates a power rather than only limits, and wants its
own measurement rather than being bundled in here. Left open.

Full suite 4909 passed / 0 failed (4889 + 20 new), F# 130/130, corpus
112/117 with 0 wrong, 0 error, 0 timeout, and the suite still runs in
3 m 30 s.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The cases already here all approach +oo, which is the one destination
that arrives at SimplifyAndComputeLimitToInfinity unsubstituted. The
others get there by a substitution first, and that is a different road
to the same rule, so it is worth pinning that they arrive at all.

x -> -oo was wrong in the same way and is fixed by the same change:
lim x->-oo (x - 5)^x / x^x answered 1 on master and now answers e^(-5),
which is right -- (1 - 5/x)^x for x -> -oo is 1/(1 + 5/t)^t at t -> +oo.
Measured against stock master at 318ac9f.

The finite destinations were never wrong, because (1 + x)^(1/x) is
already a 1^oo as written and the rule catches it at the top of
ComputeLimit without anything needing to be re-read. They are pinned
because the re-read runs on that path too and must not disturb it, and
measuring confirmed it does not.

Full suite 4913 passed / 0 failed, 3 m 22 s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Rafael-SOWNet

Copy link
Copy Markdown
Member Author

Update: destinations reached by substitution

The cases originally here all approach +oo, which is the one destination that arrives at SimplifyAndComputeLimitToInfinity unsubstituted. The others get there via a substitution first — a different road to the same rule — so a second commit pins that they arrive at all.

x -> -oo turned out to be wrong in the same way and fixed by the same change, measured against stock master (318ac9f3):

is was now
lim x->-oo (x - 5)^x / x^x e^(-5) 1 1/e^5

The finite destinations were never wrong — (1 + x)^(1/x) is already a 1^oo as written, so the rule catches it at the top of ComputeLimit without needing anything re-read. They are pinned because the re-read runs on that path too and must not disturb it; measuring confirms it does not.

Full suite now 4913 passed / 0 failed, 3 m 22 s.

@Rafael-SOWNet
Rafael-SOWNet merged commit 40ce109 into master Aug 5, 2026
24 checks passed
@Rafael-SOWNet
Rafael-SOWNet deleted the fix/remarkable-limit-after-simplification branch August 5, 2026 17:46
Rafael-SOWNet added a commit that referenced this pull request Aug 7, 2026
`sqrt(x) * sqrt(y)` was simplified to `sqrt(x * y)`, and at x = y = -1 the first
is i * i = -1 while the second is sqrt(1) = 1. A wrong answer, reached from an
expression anyone might write.

`{1}^n * {2}^n = ({1} * {2})^n` was applied unconditionally in two places -- once
in PowerRules and once, separately, in FactorizeRules. Both now carry the same
condition the ({}^{})^{} rule directly above the first already had, and were
missing for the same reason: #752 fixed that rule and did not look at its
neighbours. True for a whole n whatever the signs, since a^3 b^3 is (ab)(ab)(ab),
and for positive real bases whatever the exponent. Outside those two the sides
can differ by a full turn of the argument.

    sqrt(x) * sqrt(y)         sqrt(x * y)  ->  unchanged
    x ^ (3/2) * y ^ (3/2)     gathered     ->  unchanged
    sqrt(2) * sqrt(3)         sqrt(6)          unchanged -- both bases positive
    x ^ 2 * y ^ 2             (x * y) ^ 2      unchanged -- whole exponent

How it was found: not by a harness. The perfect-square collapse for #176 asked
`Simplify` whether a cross term was twice the product of two roots, and got back
an answer that leaned on this rewrite -- so the collapse fired on
`x + 2*sqrt(x*y) + y`, which is 0 at x = y = -1 where its "square" is -4. That
fix gates its symbolic match on a numeric check, so the wrong collapse never
shipped; this is the cause underneath it. `simpsweep` cannot reach it, because
it generates single-variable expressions and this needs two, both negative.

**The quotient form is deliberately left, and is #802.** It has the same hole --
sqrt(2)/sqrt(-3) is -0.8165i where (2/-3)^(1/2) is +0.8165i -- but guarding it
costs answers rather than shapes: it is what lets the limit machinery read a
1^oo out of a quotient, and with it guarded `(x^2 + 1)^x / (x^2)^x` has no limit
at all, which is the whole of #739 and #740. Measured: twelve tests fail, two of
them limits. Which to prefer is a maintainer's call, so it is filed with the
options rather than taken here, and pinned by a test that asserts the
disagreement so the day it is fixed that test fails and says where to look.

Unit 5544 pass 0 fail; F# 130/130; casbench 113/117 0 wrong; rootcheck 596/596;
simpsweep 10463/10463; propcheck 0 failures.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rafael-SOWNet added a commit that referenced this pull request Aug 8, 2026
…ier (#802) (#805)

`a^n / b^n` was gathered into `(a/b)^n` unconditionally, and that is false
across the branch cuts: sqrt(2)/sqrt(-3) is -0.8165i where (2/-3)^(1/2) is
+0.8165i. It is the quotient twin of #801 and was left out of that fix, because
guarding it cost *answers*: `(x^2 + 1)^x / (x^2)^x` stopped having a limit at
all, which is the whole of #739 and #740.

That framing was wrong, and this is the correction. The gathering was never the
point -- #740 wanted it so the limit machinery could read a 1^oo out of a
quotient, and its own test comment says so. The right place for the rewrite is
therefore the limit reader, not the simplifier, because that is the only place
where it can be justified: a limit needs the identity to hold in a neighbourhood
of the destination, so requiring both bases to be eventually positive there is
enough, and there is a destination to check that against. In the simplifier
there is no destination and nothing to check, which is why it was unconditional.

So ApplySecondRemarkable now recognises `a^n / b^n` itself, and the simplifier's
rule takes the same guard its whole family carries. Every limit survives:

    (x^2 + 1)^x / (x^2)^x      1        unchanged
    (x^3 + 1)^x / (x^3)^x      1        unchanged
    (x - 5)^x / x^x            e^(-5)   unchanged
    (sqrt(x) + 1)^x / sqrt(x)^x         unchanged
    sqrt(x) / sqrt(y)          sqrt(x/y) -> unchanged, and correct at x=2, y=-3

Eleven tests in PowerQuotientGatheringTest moved from asserting the *mechanism*
-- a single power in Simplify's output -- to asserting the *outcome* it existed
for, which is that the limit is answered. That distinction is the whole content
of this change, and the tests that pinned the mechanism were pinning something
unsound. The four with symbolic bases no longer gather at all and now assert
that simplifying does not change their value, since nothing can say a symbolic
quotient stays on the principal branch.

The test recording #802 as open is flipped to assert it stays fixed, which is
what it was written to do.

Unit 5543 pass 0 fail; F# 130/130; casbench 113/117 0 wrong; rootcheck 596/596;
simpsweep 10463/10463; propcheck 0 failures.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant