Gather logarithms only where that is exact, and tell the limit machinery where it is going - #922
Merged
Rafael-SOWNet merged 1 commit intoAug 13, 2026
Conversation
…ery where it is going ln(a) + ln(b) = ln(a*b) is false off the positive reals -- at x = -3 the two sides differ by 2*pi*i, the turn of the argument the principal branch discards -- and both it and its Minusf sibling were applied unconditionally. It was the last disagreement boundcheck reported, which now reports none. It cannot simply be guarded. The limit machinery expands logarithms and relies on the simplifier to gather them back, so withdrawing the rule does not cost coverage, it costs termination: the suite stops finishing. That is what makes this #802's case rather than a missing condition -- the identity has to move to where it is checkable, not be deleted. Where it is checkable is a stated approach. A limit knows where the expression is going, which is the one thing a simplification rule cannot work out for itself, and on the way to a destination the sign of each operand is decidable. So the limit machinery states the approach and the rule consults it: both operands positive for a sum, and merely agreeing in sign for a difference, since ln of a negative is ln|.| + pi*i and that cancels in a difference while it would double in a sum. The negative case is not an edge -- a destination at -oo is normalised by substituting -x, and lim x->-oo (x-5)^x / x^x arrives as ln(-x) - ln(-(x+5)). Four things that had to be measured rather than reasoned out, each having been wrong first. The identity is gathered inside the simplifier's own candidate search, so a limit solver never sees the node and a rewrite in the limit pipeline is reached zero times. The approach has to be stated for the whole computation and not around one Simplify: stated narrowly, the rule was reached 192 times for that limit and found no approach every time. Stating it only at the two +oo-normalised entries left it correct but six times slower, because a rule that declines leaves the search to explore what it would have collapsed -- 37.8s against master's 6.4s on #596's limit, now 6.9s. And the memo that looked like the fix for that made no difference at all, which is how the real cause was found. The sign memo stays, keyed by destination as well as expression and shared by every approach inside the outermost one, because the same operands are asked about repeatedly. Re-entry is bounded, and the approach is withdrawn while its own check runs so the limits it asks cannot come back through the same door. A plain Simplify after a limit still refuses to gather -- there is a test for it. Measured on this build: boundcheck 2 disagreements -> 0. simpsweep 10463 expressions and 62778 point comparisons, 0 disagreements, unchanged. 6508 C# tests and 130 F# tests pass, no hang, the suite back to its usual five and a half minutes. Numbers still gather: ln(2) + ln(3) is ln(6). Issue: #721 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rafael-SOWNet
force-pushed
the
fix/721-gather-logarithms-on-a-stated-approach
branch
from
August 13, 2026 19:15
bd7b556 to
22350b9
Compare
Rafael-SOWNet
added a commit
that referenced
this pull request
Aug 13, 2026
…was not (#924) Section 11 was written while the rule was still unsound and gave acceptance criteria for whoever took it. #922 met them, so the section now records what closed it rather than what was owed -- and section 8 no longer says boundcheck reports one disagreement, because it reports none. Three things kept rather than dropped, since they generalise past this rule. Withdrawing a rewrite can break termination and not merely coverage: the limit machinery expands logarithms and relied on the simplifier to gather them back, so the guarded rule hung the suite where it was expected to cost answers. The identity moves to where it is checkable rather than being deleted, which is #802's rule and section 3's third row. And withdrawing a rewrite has a cost in search that is invisible until something is timed -- stating the approach too narrowly left the answer correct and six times slower. Also the four placements it took to find where the identity had to be stated, each ruled out by measurement: a limit solver never fires, a rewrite in the limit pipeline is reached zero times, stating the approach around one Simplify leaves the rule reached 192 times with nothing to check against, and stating it only at the +oo-normalised entries is correct but slow. Issue: #721 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rafael-SOWNet
added a commit
that referenced
this pull request
Aug 13, 2026
… its sign (#902) (#925) 2.1.0 withdrew log_b(a^c) = c * log_b(a) from an undecided argument, because the identity needs Im(c * ln a) inside ln's principal strip and it was asking for nothing -- ln(e^x) came back as x, which at x = 3*pi*i is 9.4247i where the expression is pi*i. Two limits were the recorded price: lim x->+oo (x^2)^x / e^(2*x*ln(x)) 1 -> unevaluated lim x->+oo x^x / e^(x*ln(x) - ln(x)) +oo -> unevaluated Both are answered again, by the route #922 built for the logarithm gathering: a stated approach. A base that holds a positive sign on the way to the destination makes ln(a) real, and an exponent that is real along the approach leaves the product real, so there is nothing for the principal branch to discard. Neither half is answerable to a simplifier reading an expression on its own account, so outside a limit the rule still declines and ln(e^x) is still left as written -- which now has a test of its own, because widening the guard restores #902's wrong answer while every limit that motivated the change keeps passing. Realness is decided structurally rather than read off a limit: a positive limit does not make a base real on the way to it, since x + i*sin(x) tends to +oo off the real line. Closed real subexpressions, the approach variable, and sums, products, quotients and absolute values of those qualify; a power needs a whole exponent or a decidably positive base, since (-2)^(1/2) is imaginary; a second variable carries no approach and is refused, as is anything unlisted. The prediction this retires is worth more than the two answers. Both limits were recorded as needing an assumption travelling with the expression -- #746's tier 1, and #721's second half -- on the strength of three insertion points that were each implemented, instrumented and measured to fail. The measurement was sound and the conclusion was not: all three were pre-passes, which hand the expression on and cannot reach the candidate search that rebuilds the logarithm behind them. An ambient scope is not a pass. The rule itself asks whether an approach is being read, so it is answered wherever the rule is asked, candidate search included. No assumption mechanism was needed. Written up as section 12 of SimplificationContract.md, with the distinction to carry to the next unsound rule of this shape. Measured: suite 6950 passed / 0 failed in 4m38s -- the failure mode for these rules is a hang rather than a wrong answer, so the duration is the reading that matters; F# wrapper 130/130; boundcheck 0 disagreements; propcheck 1340 checks / 0 failures; rootcheck 596/596 clean; simpsweep 10463/10463 agree; casbench 116/119 with 0 wrong / 0 error / 0 timeout, equal to a stock-master build of the same harness. #902 #721 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.
Closes
boundcheck's last disagreement. 1 → 0.ln(a) + ln(b) = ln(a*b)is false off the positive reals, and both it and itsMinusfsibling were applied unconditionally:The two differ by
2*pi*i, the turn of the argument the principal branch discards.Why it could not simply be guarded
Guarding it is not a coverage trade. The limit machinery expands logarithms and relies on the simplifier to gather them back, so withdrawing the rule makes the suite stop finishing — a hang, not a lost answer. Measured before this PR: three failures and a run that passed three and a half hours against a normal five minutes.
That is what makes this #802's case. The identity has to move to where it is checkable, not be deleted — the same repair
a^n / b^n -> (a/b)^nalready had.Where it is checkable is a stated approach
A limit knows where the expression is going, which is the one thing a simplification rule cannot work out for itself. On the way to a destination the sign of each operand is decidable, so the limit machinery states the approach and the rule consults it:
lnof a negative isln|.| + pi*i, and that cancels in a difference while it would double in a sum.The negative case is not an edge. A destination at
-oois normalised by substituting-x, solim x->-oo (x-5)^x / x^xarrives asln(-x) - ln(-(x+5))with both operands negative.Four things that had to be measured, each wrong first
This is the part worth reading, because none of it was reachable by argument:
Simplify, the rule was reached 192 times for that limit, with the right operands, and found no approach to check against every time. It has to be stated for the whole computation.+oo-normalised entries left it correct but 6× slower — 37.8s against 6.4s on Unexpected behavior of limits #596's limit — because a rule that declines leaves the search to explore what it would have collapsed. The memo that looked like the fix for that made no difference at all, which is how the real cause was found.Both of the placements that failed were deleted rather than left in, once removing them and re-measuring showed nothing depended on them.
Guards
The sign memo is keyed by destination as well as expression and shared by every approach inside the outermost one; it is dropped when that one exits. Re-entry is bounded. The approach is withdrawn while its own check runs, so the limits it asks cannot come back through the same door. A plain
Simplifyafter a limit still refuses to gather — there is a test for it.Measured
Rebased onto
a3c7554f, the polynomial layer (#918), and re-measured against it — every figure below is on that base, not the one this branch was cut from.a3c7554fboundcheckdisagreementssimpsweep(62778 point comparisons)Timings for the two limits this touches, taken on the previous base where the comparison was made directly:
lim x->0- 1/ln(x+sqrt(x^2+1)) - 1/ln(x+1)6.4s → 6.9s, andlim x->-oo (x-5)^x / x^x1.2s → 1.3s.Numbers still gather:
ln(2) + ln(3)isln(6),ln(6) - ln(2)isln(3). What no longer happens is the same rewrite on a symbol, which may be anything.The changed answers are in
BREAKING-CHANGES.mdunderUnreleased — since 2.1.0, alongside #918's.Not in scope
This is not per-symbol assumptions (#721's second half). It is the narrower thing that consumer actually needed, and the measurement above is the argument for why: no assumption on
aandbdischarges a termination dependency, because the limit machinery's own expansion is what creates the operands.