Skip to content

Gather logarithms only where that is exact, and tell the limit machinery where it is going - #922

Merged
Rafael-SOWNet merged 1 commit into
masterfrom
fix/721-gather-logarithms-on-a-stated-approach
Aug 13, 2026
Merged

Gather logarithms only where that is exact, and tell the limit machinery where it is going#922
Rafael-SOWNet merged 1 commit into
masterfrom
fix/721-gather-logarithms-on-a-stated-approach

Conversation

@Rafael-SOWNet

@Rafael-SOWNet Rafael-SOWNet commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Closes boundcheck's last disagreement. 1 → 0.

ln(a) + ln(b) = ln(a*b) is false off the positive reals, and both it and its Minusf sibling were applied unconditionally:

"ln(x) + ln(x+1)" at x = -3     1.7918 + 6.2832i
gathered to ln(x*(1+x))         1.7918              — what was returned

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)^n already 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:

  • a sum needs both operands positive;
  • a difference needs only that their signs agreeln 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, so lim x->-oo (x-5)^x / x^x arrives as ln(-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:

  1. A limit solver never fires — the machinery never asks a sub-limit for that node.
  2. A rewrite in the limit pipeline is reached zero times.
  3. The gathering happens inside the simplifier's own candidate search. With the approach stated around only the first 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.
  4. Stating it only at the two +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 Simplify after 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.

master a3c7554f here
boundcheck disagreements 1 0
simpsweep (62778 point comparisons) 0 0
C# tests 6948 6948, 0 failed
F# tests 130 130
suite wall clock ~4m57s 4m57s, no hang

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, and lim x->-oo (x-5)^x / x^x 1.2s → 1.3s.

Numbers still gather: ln(2) + ln(3) is ln(6), ln(6) - ln(2) is ln(3). What no longer happens is the same rewrite on a symbol, which may be anything.

The changed answers are in BREAKING-CHANGES.md under Unreleased — 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 a and b discharges a termination dependency, because the limit machinery's own expansion is what creates the operands.

…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
Rafael-SOWNet force-pushed the fix/721-gather-logarithms-on-a-stated-approach branch from bd7b556 to 22350b9 Compare August 13, 2026 19:15
@Rafael-SOWNet
Rafael-SOWNet merged commit 9d37492 into master Aug 13, 2026
25 checks passed
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>
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