Implement a sharable const cache for FBig#83
Conversation
Add an opt-in MathCache<B> type to dashu-float that caches exact binary-splitting tree state for mathematical constants, so repeated calls at increasing precision *extend* prior work instead of recomputing from scratch (e.g. π at 100 digits then 1000 digits pays only for the extra work). Context and FBig are untouched and remain Copy + Send + Sync; the cache is purely additive (Send + !Sync, wrappable in Arc<Mutex<..>>). Implementation (FLOAT-CACHE.md): - math/cache.rs: MathCache<B>, ConstCache (one Option<CachedState> field per series — pi, iacoth_6/9/99), CachedState (exact (P,Q,T,num_terms)), extend_or_compute (reuse / extend / cold-compute), manual Debug that reports term counts and bit lengths rather than MB-sized integers. - math/consts.rs: factor out a shared universal merge() and add iacoth_bs() (ratio-form binary splitting for L(n), keeping Q at O(p) digits); give chudnovsky_bs an a>=b identity guard and make the helpers pub(crate). - math::pi/ln2/ln10/ln_base: finalize from the cached exact integers; ln2/ln10/ln_base combine sub-series at an elevated work precision so the linear combination survives the final round. - Switch Context::iacoth from its iterative loop to iacoth_bs (behavior pinned by the existing test_iacoth / test_ln2_ln10 fixtures). Works in no_std (uses core::cell::RefCell + EstimatedLog2; no std-only transcendentals). All float unit tests, doctests, cargo check --all-features --tests, clippy -D warnings, and rustfmt pass. Co-Authored-By: Claude <noreply@anthropic.com>
The design plan has been fully reflected in float/src/math/cache.rs and the supporting helpers; the document no longer adds value beyond the code, tests, and changelog. Only the explicitly-deferred Phase 4 roadmap items (benchmark/leaf-threshold tuning, optional sqrt(10005) caching, future sin/cos) were not implemented. Co-Authored-By: Claude <noreply@anthropic.com>
The cache Debug test used format!, which is not in scope when the crate is built without std (the CI runs `cargo test --no-default-features --features rand`). Import it from alloc, matching how other no_std test modules in the workspace pull in alloc items. Co-Authored-By: Claude <noreply@anthropic.com>
The first several leaves of the L(n) = acoth(n) binary splitting are cheap but redundant to recompute on every fresh evaluation. Precompute their merged (P, Q, T) state as a compile-time constant for the sub-series that back ln2 / ln10 (n = 6, 9, 99), and use it as the basecase of iacoth_bs: when the range starts at the series origin it returns the constant triple instead of recursing into those leaves. K is chosen per n so that P, Q and |T| each fit in a DoubleWord, which keeps the triples portable consts on both 32- and 64-bit Word (verified by the CI `--cfg force_bits="32"` clippy/test gate). Because the merge is associative, the constant equals the recursively computed state regardless of split order — pinned by a new test that re-derives each block independently and cross-checks iacoth_bs. pi cannot use this trick: its 2-term T (~1.5e23) already overflows a DoubleWord, so Chudnovsky keeps its existing single-term k=0 const leaf. Co-Authored-By: Claude <noreply@anthropic.com>
The previous basecase constants fit only u64, so they failed to compile on Word = u16 (where DoubleWord = u32), which the CI exercises via --cfg force_bits="16". dashu-int's Word width is not detectable from the float crate, so instead keep every precomputed P/Q/|T| within u32: a u32 literal is accepted by from_dword / from_parts_const on Word = u16/32/64 alike, giving a single portable set of constants with no width detection. This reduces the precomputed depth (4/3/2 leaves for n = 6/9/99, down from 7/6/4) in exchange for portability. Verified on force_bits = 16, 32 and 64 (clippy -D warnings + tests), no_std, and the default 64-bit build. Co-Authored-By: Claude <noreply@anthropic.com>
Introduce `CachedFBig`, an FBig carrying a shared `Rc<RefCell<ConstCache>>` handle whose transcendental operations (ln, exp, trig, pi, base conversion) thread the handle through the `Context` methods, reusing/extending the cached exact binary-splitting state instead of recomputing constants from scratch. The cache lives outside `Context`, which stays `Copy + Send + Sync + no_std` (so `static_fbig!`/`static_dbig!` keep working); only `CachedFBig` is `!Send + !Sync` (sharing state via `Rc<RefCell<..>>`). `ConstCache` replaces the earlier `MathCache` wrapper as the sole public cache type (`Send + Sync`, `&mut self` methods). Since `Context` accepts `Option<&mut ConstCache>`, users can also build `Arc<Mutex<ConstCache>>`-based variants. - The constant-source `Context` methods (ln, ln_1p, exp, exp_m1, powf, pi, sin/cos/sin_cos/tan/asin/acos/atan/atan2, and internal ln2/ln10/ln_base/ convert_base) gain a `cache: Option<&mut ConstCache>` parameter — a breaking change to the low-level `Context` API. The high-level `FBig` API is unchanged (it passes `None`). - CachedFBig mirrors FBig's surface explicitly; every value-producing op preserves the handle, so `(a + b).ln().exp()` stays cached. - Fix a pre-existing no_std bug: `f64::ceil()` in ConstCache's precision helpers is std-only on the MSRV (1.68) and broke the workspace `--all-features --tests` build (dashu-float is built without std as a dependency of dashu-ratio); replaced with an integer `ceil_usize`. Co-Authored-By: Claude <noreply@anthropic.com>
Re-encode +inf/-inf as exponent isize::MAX/isize::MIN (was 1/-1) and add the Repr::neg_zero() constructor at exponent -1, per the repr.rs:125-126 plan. Update is_infinite() to match the new sentinel exponents and add is_neg_zero(). normalize() now preserves infinity sentinels instead of clobbering them (the prior documented bug); -0 is not yet produced by any operation, so existing behavior is unchanged. NumHash short-circuits zero-significand values to avoid overflow when negating the isize::MIN exponent of -inf. No user-visible behavior change; infinity still panics in arithmetic. First milestone of the signed-zero / FpResult-reshape refactor (single PR). Co-Authored-By: Claude <noreply@anthropic.com>
Operations now produce the IEEE-mandated sign of zero, and -0 is a first-class value distinct from +0 only where the sign matters (1/-0 = -inf etc., landing in M3). Highlights: - Repr: manual PartialEq/Eq so +0 == -0; normalize() preserves the -0 encoding. - Neg/Abs/signum: correctly toggle ±0 and ±inf by flipping the sentinel exponent (negating IBig::ZERO is a no-op). Sign-mul (Sign*FBig) delegates to Neg. - Comparisons (cmp.rs): ±0 compare and order equal. - Arithmetic: mul attaches XOR sign to zero products; div/rem attach the dividend/XOR sign to zero results; add/sub cancellation yields -0 only under roundTowardNegative (Down), +0 otherwise (new Round::IS_ROUND_TOWARD_NEGATIVE). repr_round preserves input sign when rounding to zero. - Transcendentals: sqrt/cbrt/nth_root preserve ±0; sin/tan/atan/sin_cos carry the sign (cos(±0)=+1); ln_1p(±0)=±0; pow(-0,n) sign via sqr/mul. - Rounding ops: trunc/round/fract yield signed zero; ceil/floor pass -0 through. - Conversions: -0.0 round-trips through f32/f64 (TryFrom checks is_sign_negative; into_f*_internal preserves -0). num_traits is_positive/is_negative follow the sign bit (matching Rust's f64::is_sign_*). - shift.rs skips exponent modification for any zero-significand value. Infinity-as-value (1/0 -> +inf, ln(0) -> -inf, exp(huge) -> +inf) and the FpResult reshape remain for M3. Co-Authored-By: Claude <noreply@anthropic.com>
Reshape the result model so that infinite *inputs* are errors and infinite
*outputs* are legitimate values, unifying the old FpResult enum with Rounded.
- FpError { InfiniteInput, OutOfDomain, Indeterminate } (Display + std Error),
modeled on ConversionError; FpResult<T> = Result<Rounded<T>, FpError>.
- unwrap_fp() maps FpError variants to granular panics for the FBig/CachedFBig
convenience layer, which now uniformly panics on error (including trig).
- Deleted the old FpResult enum.
- Context arithmetic/transcendental/trig methods return FpResult:
* inf input -> Err(InfiniteInput), except atan(±inf)=±π/2 and the atan2
signed-∞ quadrant table (preserved, well-defined finite results).
* inf output as a value: 1/0 -> ±inf, inv(0) -> ±inf, ln(0) -> -inf,
tan(odd·π/2) -> ±inf (repr_div produces inf for finite/0).
* 0/0 -> Err(Indeterminate); sqrt/ln of negative, asin(|x|>1), pow(neg base),
even root of negative -> Err(OutOfDomain).
- FBig/CachedFBig layers panic-on-error via unwrap_fp; new
forward_to_context_unwrap! macro unifies sin/cos/tan/asin/acos/atan (which
finally fit the macro now that trig returns the uniform Result shape).
- Internal call sites updated (cache/consts/exp/log/convert/rand); removed
unused panic helpers; tests and doctests updated to the Result shape.
New tests/fpresult.rs covers the inf-as-value / inf-input-as-error / domain
contract. The old dead Overflow/Underflow variants are gone; inf flows as a
value, errors flow as Err.
Co-Authored-By: Claude <noreply@anthropic.com>
- Rewrite the IEEE-754 compliance section in fbig.rs to describe the new model: NaN → FpError/panic, signed zero, and infinities as terminal values (producible and comparable, but inf inputs error/panic; atan/atan2 inf cases preserved). - Replace the stale lib.rs TODO about inf arithmetic with a note describing the implemented terminal-value behavior. - CHANGELOG (Unreleased, → 0.5.0): document the breaking encoding change (±inf sentinel exponents, -0), the FpResult = Result<Rounded<T>, FpError> reshape and old-enum removal, and the FBig trig methods returning Self. rational::to_float and dashu-python are unchanged (they only use the still-Rounded conversions); the workspace compiles and all 66 test binaries pass. Co-Authored-By: Claude <noreply@anthropic.com>
- FBig +/- operators: produce -0 on exact cancellation under round-toward-negative (Down), matching Context::add/sub. The equal-exponent fast paths now route the summed significand through cancel_zero (previously they yielded +0). - powf(±0, y): return the *positive* result (+0 for y>0, +inf for y<0), matching the common float-pow convention (a float exponent doesn't track parity). powi remains the sign-correct variant (pow(-0, odd) = -0). Fixes powf(0, negative) which previously returned +0. - exp(huge)/exp_m1(huge)/powi: return +inf (or 0 / -1 for huge negative exp args) instead of panicking when the result exponent overflows isize. - Fix pre-existing bug: FBig's ShrAssign (>>=) subtracted the shift twice; now once. - CHANGELOG: document the powf convention, overflow-to-inf, and the shr_assign fix. New tests cover operator cancellation under Down, exp overflow, powf zero base, and shr_assign. Co-Authored-By: Claude <noreply@anthropic.com>
| let sqrt_10005 = work_context | ||
| .sqrt(&work_context.convert_int::<B>(10005.into()).value().repr) | ||
| .value(); | ||
| let sqrt_10005 = |
There was a problem hiding this comment.
- The code of calculation of pi are duplicate in consts.ts and cache.rs
- the constant 10005 here don't need rounding (unnecessary)
| } else { | ||
| Repr::zero() | ||
| }; | ||
| Ok(FBig::<R, B>::new(zero, *ctx).with_precision(ctx.precision)) |
There was a problem hiding this comment.
The last with_precision call is unnecessary. new() already ensures this
| /// Calculate the cosine of the floating point representation. | ||
| #[must_use] | ||
| pub fn cos<const B: Word>(&self, x: &Repr<B>) -> FpResult<B> { | ||
| pub fn cos<const B: Word>( |
There was a problem hiding this comment.
I think we still need #[must_use] tag for functions using the cache.
| impl<R: Round> Context<R> { | ||
| // Convert the [Repr] from base B to base NewB, with the precision under the target base from this context. | ||
| #[allow(non_upper_case_globals)] | ||
| fn convert_base<const B: Word, const NewB: Word>(&self, repr: Repr<B>) -> Rounded<Repr<NewB>> { |
There was a problem hiding this comment.
Shall we also return FpResult in convert_base, since overflow and underflow can happen
| /// Attach the dividend/divisor XOR sign to a zero quotient: the raw quotient significand is | ||
| /// `+0`, so the sign of a zero result (`0/finite`, or a finite/finite that rounds to zero) is | ||
| /// `sign(lhs) XOR sign(rhs)`. | ||
| fn div_repr<const B: Word>(sign_negative: bool, significand: IBig, exponent: isize) -> Repr<B> { |
There was a problem hiding this comment.
need a better name, make_div_repr?
| /// `ln(-x)`, `asin(|x| > 1)`, `pow(negative, non-integer)`, an even root of a negative value. | ||
| OutOfDomain, | ||
|
|
||
| /// An indeterminate form, e.g. `0 / 0`. |
There was a problem hiding this comment.
need to check whether x / 0 all goes to indeterminate. If so, document here
| forward_to_context!(exp); | ||
| forward_to_context!(exp_m1); | ||
|
|
||
| /// Square root (see [`Context::sqrt`]). |
There was a problem hiding this comment.
Why can't we wrap sqrt, and inv?
| // TODO: test if we can use log_B(p/2log_B(n)) directly | ||
| let guard_digits = (self.precision.log2_est() / B.log2_est()) as usize; | ||
| let work_context = Self::new(self.precision + guard_digits + 2); | ||
| let n: u32 = (&n).try_into().expect("iacoth argument must fit in u32"); |
There was a problem hiding this comment.
We lost the nice graphic comment
|
|
||
| /// Raw product of two finite reprs, attaching the XOR sign of the operands to a zero product | ||
| /// (the significand product alone is `+0`, losing the sign). | ||
| fn mul_finite_reprs<const B: Word>(lhs: &Repr<B>, rhs: &Repr<B>) -> Repr<B> { |
There was a problem hiding this comment.
make naming consistent: make_mul_repr?
| impl<R: Round, const B: Word> FBig<R, B> { | ||
| /// Return `±0` carrying the same sign as `self` (used by trunc/round/fract where a zero | ||
| /// result inherits the sign of the input). The result has precision 0, matching `FBig::ZERO`. | ||
| fn signed_zero(&self) -> Self { |
There was a problem hiding this comment.
These two helper functions seems very redundant, can we have something like UBig::with_sign()?
| @@ -0,0 +1,187 @@ | |||
| //! Tests for the `FpResult` contract: infinite inputs → `Err`, infinite outputs → `Ok(±inf)`, | |||
There was a problem hiding this comment.
Most of the testing cases should be moved to the test cases per operations.
| - Fix rounding issues in `to_f32()` and `to_f64()` ([#53](https://github.com/cmpute/dashu/issues/53), [#56](https://github.com/cmpute/dashu/issues/56)). | ||
| - Fix several rounding bugs in `FBig`/`Context` addition and subtraction: severe-cancellation collapse, spurious-ULP errors from negligible operands, the window-edge boundary, and `Context::sub` with a zero left operand under directed rounding modes. | ||
| - Fix `FBig::fract()` inflating context precision and `split_at_point_internal` using an incorrect fractional scale for values smaller than one. | ||
| - Fix rounding issues in `to_32()` and `to_f64()` (fixes [#53](https://github.com/cmpute/dashu/issues/53) and [#56](https://github.com/cmpute/dashu/issues/56)). |
There was a problem hiding this comment.
We shouldn't touch the existing Changelog items
No description provided.