Skip to content

Refuse a sine whose reduction needs more of π than exists [patch] - #97

Merged
matt-edmondson merged 1 commit into
mainfrom
claude/precisenumber-96-constant-precision-ceiling
Sep 23, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
claude/precisenumber-96-constant-precision-ceiling

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #96

The fix is not where the issue points, and the surface is narrower

The issue proposes throwing from ConstantTo when more digits are asked for than the constants hold. I did that first, and it broke two existing tests — both correctly. The capping is not the defect:

  • ConstantTo's behaviour is documented on every accessor ("or Pi itself when that is no fewer digits than it carries") and pinned by TestConstantAccessorsReturnTheWholeConstantWhenAskedForMore, which asserts PiTo(int.MaxValue) == Pi. PiTo(n) means at most n digits, by design.
  • More decisively, it broke TestSinReducesALargeArgumentAgainstAWidePi. That test calls Sin(1000000, 130), which asks π for 157 digits and is served 150 — and its answer is correct to every digit it reports, against a committed independent reference. A blanket throw refuses a call that works.

So the defect is the issue's second suggestion: the reduction call site asked for more and assumed it got it. And the guard has to be on what the reduction genuinely needs, not on the padded width it requests.

Measured, not assumed

I compared against references computed independently at several hundred digits, by two methods that share nothing with this library (mpmath, and a Taylor series in Python's decimal over a hand-verified 200-digit π — the two agree to every digit compared).

The correct digits of a sine come out as min(significantDigits, ConstantPrecision − integerDigits):

integer digits asked correct
1 150 150
1 151 150
7 130 130
7 145 144
30 120 120
30 125 120
50 100 100
50 110 100
80 100 71
80 140 71

An argument of 80 integer digits yields 71 correct digits whether 100 or 140 are requested. Sin(1e200) has no correct digits at any precision. The boundary is exactly where significantDigits + integerDigits crosses ConstantPrecision.

RequireReducibleArgument enforces that sum. Deliberately not reductionDigits, which is working + argumentDigits + TrigonometricGuardDigits — that carries the guard width twice over as margin, and margin is allowed to be unavailable. Bounding it would refuse Sin(1000000, 130). The bound is conservative by at most one digit at the very edge, which is the right direction for a guard.

The rest of the surface the issue lists

The same measurement clears most of it, which is why this PR is smaller than the issue implies:

  • Log is unaffected. It carries magnitude in the decimal exponent rather than cancelling it against a constant. Correct to 130 digits at 180 total working digits — no degradation at all.
  • Exp tops out around ConstantPrecision − 1, and everywhere short of that it is inside the one-digit noise of the measurement. Documented rather than guarded: I do not have evidence sharp enough to place a threshold, and a guard on the wrong side of it would refuse working calls, which is the failure this PR exists to avoid.
  • The half-turn family (SinPi and the rest) has no ceiling by construction — it reduces modulo two before multiplying by π, as the class remarks already say.

Tests

Four cases. The two Refuses ones are the issue's acceptance criterion.

test asserts
TestSinRefusesMoreDigitsThanPiCanReduceAgainst one digit past the ceiling throws, and the message names both the shortfall and what is available
TestSinDeliversEveryDigitUpToTheReductionCeiling ConstantPrecision - 7 digits of Sin(1000000) succeeds and matches the wide reference — the bound is not merely safe, it is tight
TestSinRefusesAnArgumentTooLargeToReduceAtAll Sin/Cos/Tan of 1e200 at only 10 digits, where magnitude alone exhausts π
TestOrdinaryAnglesAreUnaffectedByTheReductionCeiling the whole existing angle sweep at 60 digits still works

The message is asserted, not just the type, because "ask for fewer digits" is useless advice without a number.

Confirmed the tests depend on the change by neutering the magnitude term and re-running: 2 failed, 22 passed — exactly the two refusal cases. The other two pass either way by design; they are the guard against the guard reaching calls that were always valid, not evidence for it.

Full suite green: 354 total, 0 failed (350 before, 4 added), all four TFMs. Release build clean, zero warnings. TestSinReducesALargeArgumentAgainstAWidePi is among the passing ones, which is the check that matters here.

Not covered

ConstantTo is unchanged, so PiTo(200) still returns 150 digits silently. That is its documented contract and a test pins it; changing it is a breaking API change that this bug does not require, now that the caller which actually depended on the missing digits checks for itself. If you would rather it throw, that is a separate decision and a [major].

Atan2, Asin, Acos and DegreesToRadians read PiTo(working) without a magnitude term, so they degrade only past ~150 requested digits, the same mild case as Exp. Not guarded here for the same reason.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XH3MikiXVCii3abMojvibs


Generated by Claude Code

PiTo serves at most ConstantPrecision digits and returns the capped
constant rather than failing. That is deliberate, documented on the
accessor and pinned by a test, so it is not the defect. The defect is
that SinCos asked for more and assumed it got it.

Reducing modulo pi/2 cancels the argument's integer digits out of pi, so
the constant has to carry those on top of the answer. Past 150 it could
not, and the result still reported the full significantDigits, which on
this type is a promise that those digits are correct.

Measured against references computed independently at several hundred
digits, the correct digits come out as min(significantDigits,
ConstantPrecision - integerDigits): an argument of 80 integer digits
yields 71 correct digits whether 100 or 140 are asked for, one of 50
yields 100, and Sin(1e200) has no correct digits at any precision.

RequireReducibleArgument enforces that sum. Deliberately not the wider
reductionDigits the call site passes to PiTo: that pads the requirement
with TrigonometricGuardDigits twice over as margin, and margin is
allowed to be unavailable. Bounding it would refuse Sin(1000000, 130),
which asks pi for 157 digits, is served 150, and is correct to every
digit it reports -- as TestSinReducesALargeArgumentAgainstAWidePi has
always pinned against an independent reference.

The same measurement clears the rest of the surface the issue lists. Log
is unaffected, carrying magnitude in the decimal exponent rather than
cancelling it, and was correct to 130 digits at 180 total working
digits. Exp tops out around ConstantPrecision - 1, inside the one-digit
noise of the measurement everywhere short of that, so it is documented
rather than guarded on this evidence.

Fixes #96

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XH3MikiXVCii3abMojvibs
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit dbbe8b3 into main Sep 23, 2026
12 checks passed
@matt-edmondson
matt-edmondson deleted the claude/precisenumber-96-constant-precision-ceiling branch September 23, 2026 00:04
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.

Trig/exp/log functions silently return under-precision (or wrong) digits once required working precision exceeds the 150-digit π/e/ln constants

2 participants