Skip to main content

dashu_float/math/
trig.rs

1//! Trigonometric functions, built on top of the cached constants π/2 and the real
2//! [`exp`](crate::FBig::exp)/[`ln`](crate::FBig::ln) primitives:
3//!
4//! - Circular: `sin`, `cos`, `tan`, `sin_cos`, the ×π variants `sin_pi`, `cos_pi`, `tan_pi`,
5//!   `sin_cos_pi`, and the inverses `asin`, `acos`, `atan`.
6//!
7//! Argument reduction to the first quadrant reuses the cached π so that repeated
8//! calls at increasing precision extend the shared constant state. The ×π variants reduce
9//! exactly in integer arithmetic instead (see [`Context::sin_pi`]), so they need no
10//! magnitude-dependent guard digits and resolve quarter-integer arguments exactly.
11
12use crate::{
13    ball::{ulps, Ball},
14    cmp::repr_cmp_same_base,
15    error::{assert_limited_precision, FpError},
16    fbig::FBig,
17    mag::Mag,
18    math::{
19        cache::{compute_e, reborrow_cache, ConstCache},
20        FpResult,
21    },
22    repr::{Context, Repr, Word},
23    round::{mode, ErrorBounds, Round, Rounded},
24    utils::{digit_len, log2_u_bs_lb, shl_digits},
25};
26use core::convert::TryFrom;
27use dashu_base::{
28    Abs, AbsOrd, Approximation::Exact, DivRem, EstimatedLog2, RemEuclid, Sign, UnsignedAbs,
29};
30use dashu_int::{fast_div::ConstDivisor, IBig, UBig};
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33enum Quadrant {
34    First,
35    Second,
36    Third,
37    Fourth,
38}
39
40/// Build a `Normal` result equal to `±0`, preserving the sign of `x` (used by `sin`/`tan`/`sin_cos`
41/// at zero input, where `sin(-0) = -0` and `tan(-0) = -0`).
42fn signed_zero_normal<R: Round, const B: Word>(
43    ctx: &Context<R>,
44    x: &Repr<B>,
45) -> FpResult<FBig<R, B>> {
46    let zero = if x.is_neg_zero() {
47        Repr::neg_zero()
48    } else {
49        Repr::zero()
50    };
51    Ok(Exact(FBig::<R, B>::new(zero, *ctx)))
52}
53
54/// `B^e mod M` through the integer crate's prepared-divisor ring — the residue of a radix
55/// power without ever materializing the power (e can be astronomically large, e.g. an
56/// exponent near the `isize` range). All intermediates stay below `M`, which the callers
57/// keep small (`2k·u ≤ 24u`), so the single/double-word fast paths of [`ConstDivisor`]
58/// apply.
59fn digits_powmod<const B: Word>(e: usize, modulus: &IBig) -> IBig {
60    let ring = ConstDivisor::new(modulus.unsigned_abs());
61    let base = ring.reduce(crate::utils::base_as_ibig::<B>().unsigned_abs());
62    IBig::from(base.pow(&UBig::from(e)).residue())
63}
64
65/// Classify the ×u exact cases: `Some(j)` with `j = (k·x/u) mod 2k` (an euclidean modulus on
66/// the signed value, so negative arguments classify directly) when `k·x/u` is an integer,
67/// `None` otherwise. The two keys the ×u family uses:
68/// - `k = 12` (sine/cosine): `j ≡ 0 (mod 3)` means the angle is a multiple of π/2 — the
69///   quarter table keyed by `(4x/u) mod 8 = (j/3) mod 8`; `j` odd (`j mod 12 ∈ {1, 5}` /
70///   `{7, 11}`) means ±π/6 modulo π — sine is `±1/2`; `j` even with `j mod 12 ∈ {2, 10}` /
71///   `{4, 8}` means ±π/3 modulo π — cosine is `±1/2`.
72/// - `k = 8` (tangent): `j mod 8` is the eighth-of-turn index — `0`/`4` map to `±0`, `2`/`6`
73///   to the poles, `1`/`5` to `+1`, `3`/`7` to `−1`.
74///
75/// Powers of B are only materialized when bounded by the input's own size; astronomical
76/// exponents are reduced by modular exponentiation.
77fn unit_residue<const B: Word>(x: &Repr<B>, u: usize, k: usize) -> Option<i8> {
78    debug_assert!(u > 0, "the callers reject u = 0");
79    let u_ibig = IBig::from(u);
80    let k_ibig = IBig::from(k);
81    let modulus = IBig::from(2 * k) * &u_ibig; // 2k·u
82    let m = &x.significand;
83    if m.is_zero() {
84        // ±0 — the infinities are screened out by the callers before this.
85        return Some(0);
86    }
87    let e = x.exponent;
88    if e >= 0 {
89        // j = (k·m·B^e / u) mod 2k = (k·m·B^e mod 2ku) / u — all modular, so the
90        // astronomically large B^e is reduced by binary exponentiation instead of being
91        // materialized.
92        let km = &k_ibig * m;
93        let n = IBig::from(km.rem_euclid(modulus.clone()))
94            * digits_powmod::<B>(e.unsigned_abs(), &modulus);
95        let n = IBig::from(n.rem_euclid(modulus));
96        if &n % &u_ibig != IBig::ZERO {
97            return None;
98        }
99        let j = n / &u_ibig; // in [0, 2k)
100        Some(i8::try_from(j).expect("j < 2k ≤ 24 fits in i8"))
101    } else {
102        // x = m/B^s: k·x/u = k·m/(u·B^s) is an integer iff u·B^s divides k·m — possible only
103        // when u·B^s ≤ |k·m|, so astronomical exponents are rejected by log2 bounds before any
104        // power of B is materialized (the materialized power stays within the input's size).
105        let s = e.unsigned_abs();
106        let km = &k_ibig * m;
107        let (_, ub_km) = km.log2_bounds();
108        let (u_lb, _) = u.log2_bounds();
109        let (b_lb, _) = B.log2_bounds();
110        if ub_km < log2_u_bs_lb(u_lb, b_lb, s) {
111            // u·B^s > |k·m| (bound certified below the true value): cannot divide.
112            return None;
113        }
114        let d = &u_ibig * shl_digits::<B>(&IBig::ONE, s);
115        let (v, rem) = km.div_rem(d);
116        if !rem.is_zero() {
117            return None;
118        }
119        let j = v.rem_euclid(IBig::from(2 * k));
120        Some(i8::try_from(j).expect("j mod 2k ≤ 23 fits in i8"))
121    }
122}
123
124/// The quadrant of a half-integer index k — the `k = round(x/(π/2))` of the radian reduction
125/// and the `k = round(4|x|/u)` of the ×u reduction alike (both subtract k·π/2 from the angle).
126fn quadrant_of(k: &IBig) -> Quadrant {
127    let r = k.rem_euclid(IBig::from(4));
128    match i8::try_from(r).expect("k mod 4 fits in i8") {
129        0 => Quadrant::First,
130        1 => Quadrant::Second,
131        2 => Quadrant::Third,
132        3 => Quadrant::Fourth,
133        _ => unreachable!(),
134    }
135}
136
137/// The exact ×u argument reduction (on `|x|`): first `|x| mod u` (exact, `∈ [0, u)`), then
138/// `k = round(4·|x|/u)` with ties rounded up and `r = |x| − k·u/4 ∈ [−u/8, u/8]` as an exact
139/// rational, plus the quadrant `k mod 4`. Unlike the radian
140/// [`Context::reduce_to_quadrant`], the reduction is exact integer arithmetic on the
141/// significand, so it holds for arbitrarily large `|x|` — the working precision needs no
142/// guard scaling with the magnitude.
143enum UnitReduced<const B: Word> {
144    /// The `k = 0` case (`|x| mod u < u/8`, quadrant `First`): `r` itself, kept as a raw
145    /// [`Repr`] because the power of B behind its exponent can be astronomically large
146    /// (e.g. `3·2^−10⁹`) and must not be materialized — the argument ball is formed by a
147    /// product instead of a rational split.
148    Small(Repr<B>),
149    /// `r = num/den` as an exact rational — never a `Repr`, since an odd base cannot
150    /// represent the fractions exactly. `den` is bounded by the input's own digit count,
151    /// because the astronomically-scaled cases were taken by the `k = 0` fast path.
152    Split {
153        quadrant: Quadrant,
154        num: IBig,
155        den: IBig,
156    },
157}
158
159fn reduce_unit_argument<const B: Word>(x: &Repr<B>, u: usize) -> UnitReduced<B> {
160    debug_assert!(!x.significand.is_zero());
161    debug_assert!(u > 0);
162
163    // Step 1: r0 = |x| mod u ∈ [0, u), exact. (A zero r0 means x is a multiple of u — a
164    // quarter case, resolved by the exact-case table before the reduction.)
165    let u_ibig = IBig::from(u);
166    let m = x.significand.clone().abs();
167    let e = x.exponent;
168    // log2 lower bounds of u and of the base B, for the `u·B^s` magnitude comparisons below.
169    let (u_lb, _) = u.log2_bounds();
170    let (b_lb, _) = B.log2_bounds();
171    let (sig0, e0) = if e >= 0 {
172        // Integer argument: m·B^e mod u by modular exponentiation — B^e is never materialized.
173        let r = IBig::from(m.rem_euclid(u_ibig.clone()))
174            * digits_powmod::<B>(e.unsigned_abs(), &u_ibig);
175        (IBig::from(r.rem_euclid(u_ibig.clone())), 0isize)
176    } else {
177        // |x| = m/B^s < u ⟺ m < u·B^s (checked by bounds, no materialization): r0 = |x| itself.
178        let s = e.unsigned_abs();
179        let (_, ub_m) = m.log2_bounds();
180        if ub_m < log2_u_bs_lb(u_lb, b_lb, s) {
181            (m, e)
182        } else {
183            // |x| ≥ u, so u·B^s ≤ m: the power is bounded by the input's own size.
184            let d = &u_ibig * shl_digits::<B>(&IBig::ONE, s);
185            (IBig::from(m.rem_euclid(d)), e)
186        }
187    };
188    debug_assert!(!sig0.is_zero(), "multiples of u are table-resolved");
189    let s0 = e0.unsigned_abs(); // e0 ≤ 0
190
191    // Step 2 — k = 0 fast path: 8·sig0 < u·B^(s0) (guaranteed by the bounds), so 4r0/u < 1/2
192    // rounds to k = 0 and no power of B is materialized — crucially so, since here it can be
193    // astronomically large. The bound is the conservative [`log2_u_bs_lb`]: overshooting here
194    // would classify a `k ≥ 1` argument as `Small` (wrong quadrant — a wrong result), while
195    // undershooting only falls through to the exact general split.
196    let (_, ub8m) = (IBig::from(8) * &sig0).log2_bounds();
197    if ub8m < log2_u_bs_lb(u_lb, b_lb, s0) {
198        return UnitReduced::Small(Repr::new(sig0, e0));
199    }
200
201    // Not tiny: within the bounds' slack sig0 ≥ u·B^(s0)/8, so the power stays within the
202    // input's own digit count. k = round-half-up of 4r0/u = ⌊(8·sig0 + u·B^(s0)) /
203    // (2·u·B^(s0))⌋ (truncating division of non-negatives is a floor); the split
204    // r = (4·sig0 − k·u·B^(s0)) / (4·B^(s0)) is exact integer arithmetic — the cancellation a
205    // float subtraction would suffer is free here.
206    let b_pow = shl_digits::<B>(&IBig::ONE, s0);
207    let u_bs = &u_ibig * &b_pow; // u·B^(s0)
208    let k = (IBig::from(8) * &sig0 + &u_bs) / (IBig::from(2) * &u_bs);
209    let num = IBig::from(4) * sig0 - &k * &u_bs;
210    let den = IBig::from(4) * b_pow;
211    UnitReduced::Split {
212        quadrant: quadrant_of(&k),
213        num,
214        den,
215    }
216}
217
218/// The ×π argument as a ball on a raw (unreduced) value: `π·x`, with π's conservative radius
219/// and x's rounding to the work precision both tracked by the ball arithmetic. Used by the
220/// hyperbolic ×π functions (which have no periodic argument reduction at all).
221pub(crate) fn pi_scaled_ball<const B: Word>(
222    work: &Context<mode::HalfEven>,
223    x: &Repr<B>,
224    mut cache: Option<&mut ConstCache>,
225) -> Result<Ball<B>, FpError> {
226    let wp = work.precision;
227    let pi = work.pi::<B>(reborrow_cache(&mut cache)).value();
228    // π as a ball: the cached constant is correctly rounded to the work precision; 8 is a
229    // conservative sound radius (as for the ln(2) constant).
230    let rad = ulps::<B>(&pi.repr, wp, 8);
231    let pi_ball = Ball::with_error(pi.into_repr(), rad);
232    let x_ball = Ball::from_rounded(work.repr_round_ref(x), wp);
233    pi_ball.mul(&x_ball, wp)
234}
235
236/// The reduced ×u argument as a ball: `t = (2π/u)·r` with `|t| ≤ π/4`, plus the quadrant. The
237/// constant `2π/u` carries π's conservative radius (as in `Context::reduce_to_quadrant`) and
238/// divides by the *exact* integer u (held at its own digit width — a work precision narrower
239/// than u would round the divisor); the rational `r` is rounded to the work precision, with
240/// every rounding tracked by the ball arithmetic.
241fn unit_argument_ball<const B: Word>(
242    work: &Context<mode::HalfEven>,
243    reduced: &UnitReduced<B>,
244    u: usize,
245    mut cache: Option<&mut ConstCache>,
246) -> Result<(Ball<B>, Quadrant), FpError> {
247    let wp = work.precision;
248    let pi = work.pi::<B>(reborrow_cache(&mut cache)).value();
249    // π as a ball: the cached constant is correctly rounded to the work precision; 8 is a
250    // conservative sound radius (as for the ln(2) constant).
251    let rad = ulps::<B>(&pi.repr, wp, 8);
252    let pi_ball = Ball::with_error(pi.into_repr(), rad);
253    let two_pi = pi_ball.scale_int(&IBig::from(2), wp)?;
254    let u_digits = digit_len::<B>(&IBig::from(u));
255    let u_ball = Ball::exact_int(IBig::from(u), u_digits.max(wp));
256    let two_pi_over_u = two_pi.div(&u_ball, wp)?;
257    Ok(match reduced {
258        UnitReduced::Small(r) => {
259            let r_ball = Ball::from_rounded(work.repr_round_ref(r), wp);
260            (two_pi_over_u.mul(&r_ball, wp)?, Quadrant::First)
261        }
262        UnitReduced::Split { quadrant, num, den } => {
263            // num and den are rounded to the work precision before the ball division — for a
264            // long input significand this caps the intermediate width instead of carrying the
265            // input's full digit count through the division.
266            let num_ball = Ball::from_rounded(work.repr_round_ref(&Repr::new(num.clone(), 0)), wp);
267            let den_ball = Ball::from_rounded(work.repr_round_ref(&Repr::new(den.clone(), 0)), wp);
268            (two_pi_over_u.mul(&num_ball, wp)?.div(&den_ball, wp)?, *quadrant)
269        }
270    })
271}
272
273/// `v·u/(2π)` of a ball — the inverse ×u family's final scaling. 2π carries the usual
274/// conservative π radius; the integer scaling is exact.
275fn scale_unit<const B: Word>(
276    work: &Context<mode::HalfEven>,
277    v: &Ball<B>,
278    u: usize,
279    mut cache: Option<&mut ConstCache>,
280) -> Result<Ball<B>, FpError> {
281    let wp = work.precision;
282    let pi = work.pi::<B>(reborrow_cache(&mut cache)).value();
283    // π as a ball: the cached constant is correctly rounded to the work precision; 8 is a
284    // conservative sound radius (as for the ln(2) constant).
285    let rad = ulps::<B>(&pi.repr, wp, 8);
286    let two_pi = Ball::with_error(pi.into_repr(), rad).scale_int(&IBig::from(2), wp)?;
287    v.scale_int(&IBig::from(u), wp)?.div(&two_pi, wp)
288}
289
290/// The exact rational `num/den` rounded under the caller's mode — the exact-value rows of the
291/// ×u tables (exact whenever den's prime factors divide the base, e.g. `u/4` in base 2 or 10;
292/// correctly rounded otherwise).
293fn exact_ratio<R: Round, const B: Word>(
294    ctx: &Context<R>,
295    num: IBig,
296    den: usize,
297) -> FpResult<FBig<R, B>> {
298    ctx.div(&Repr::new(num, 0), &Repr::from(den))
299}
300
301impl<R: ErrorBounds> Context<R> {
302    /// Work context for trigonometric functions: enough guard digits to absorb the catastrophic
303    /// cancellation in `x − k·(π/2)` for large `|x|`. `guard` (the Ziv retry's growing margin)
304    /// replaces the fixed base; `x_mag/10` covers cumulative reduction error scaling with `|x|`.
305    /// Always [`mode::HalfEven`] — the Ball arithmetic the trig functions now run on.
306    fn compute_work_context_trig<const B: Word>(
307        self,
308        x: &Repr<B>,
309        guard: usize,
310    ) -> Context<mode::HalfEven> {
311        // x_mag estimates m = floor(log_BASE(|x|))
312        let x_mag = (x.exponent.saturating_add(x.digits_ub() as isize)).max(0) as usize;
313        let extra_guards = guard + x_mag / 10;
314        let work_precision = self
315            .precision
316            .saturating_add(x_mag)
317            .saturating_add(extra_guards);
318        Context::<mode::HalfEven>::new(work_precision)
319    }
320
321    /// Reduces the argument to the first quadrant: `r = x − k·(π/2)` with `r ∈ (−π/4, π/4]`.
322    /// Returns the work context, `r` as a [`Ball`] whose radius already covers the reduction
323    /// error (dominated by `|k|·ulp(π/2)` for huge `|x|` — the cancellation is tracked by the
324    /// Ball subtraction), and the quadrant `k % 4`.
325    fn reduce_to_quadrant<const B: Word>(
326        self,
327        x: &Repr<B>,
328        guard: usize,
329        mut cache: Option<&mut ConstCache>,
330    ) -> Result<(Context<mode::HalfEven>, Ball<B>, Quadrant), FpError> {
331        let work_context = self.compute_work_context_trig(x, guard);
332        let work_precision = work_context.precision;
333        let x_ball = Ball::from_rounded(work_context.repr_round(x.clone()), work_precision);
334        // `x_f` is exactly the ball's midpoint (the same rounded value), so no second rounding.
335        // `div_rem_euclid`-style quotient extraction lives on FBig, so wrap the bare mids
336        // (zero-cost).
337        let x_f = FBig::new(x_ball.mid.clone(), work_context);
338
339        let pi = work_context.pi::<B>(reborrow_cache(&mut cache)).value();
340        let half_pi = &pi / 2u8;
341        // π as a ball: the cached constant is correctly rounded to the work precision; 8 is a
342        // conservative sound radius (as for the ln(2) constant).
343        let rad = ulps::<B>(&half_pi.repr, work_precision, 8);
344        let half_pi_ball = Ball::with_error(half_pi.clone().into_repr(), rad);
345
346        let x_scaled = &x_f / &half_pi;
347        let k_f = x_scaled.round();
348        // `k_f` is the integer nearest `x_scaled`, so it's exact (or a signed zero for a tiny
349        // argument in (-1, 0), which `IBig::try_from` treats as plain 0).
350        let k = IBig::try_from(k_f).expect("k_f is an exact integer or signed zero");
351
352        // r = x − k·(π/2): the cancellation and π's error (scaled by |k|) are tracked by the Ball.
353        let scaled = half_pi_ball.scale_int(&k, work_precision)?;
354        let r_ball = x_ball.sub(&scaled, work_precision)?;
355
356        Ok((work_context, r_ball, quadrant_of(&k)))
357    }
358
359    /// Calculate the sine of the floating point representation.
360    pub fn sin<const B: Word>(
361        &self,
362        x: &Repr<B>,
363        mut cache: Option<&mut ConstCache>,
364    ) -> FpResult<FBig<R, B>> {
365        if x.is_infinite() {
366            return Err(FpError::InfiniteInput);
367        }
368        assert_limited_precision(self.precision);
369        if x.significand.is_zero() {
370            // sin(±0) = ±0
371            return signed_zero_normal(self, x);
372        }
373
374        // Ziv: reduce to the first quadrant (the guard grows per retry, enlarging the work precision
375        // that absorbs the `x − k·(π/2)` cancellation), evaluate the series. The reduction error is
376        // already inside the reduced argument's Ball radius.
377        self.ziv(50, |guard| {
378            let (work, r, quadrant) =
379                self.reduce_to_quadrant(x, guard, reborrow_cache(&mut cache))?;
380            let val = match quadrant {
381                Quadrant::First => work.sin_compute(&r)?,
382                Quadrant::Second => work.cos_compute(&r)?,
383                Quadrant::Third => -work.sin_compute(&r)?,
384                Quadrant::Fourth => -work.cos_compute(&r)?,
385            };
386            Ok(val.to_value_radius::<R>(&Context::<R>::new(work.precision)))
387        })
388    }
389
390    /// Near-correct sine series `S(x) = x − x³/3! + x⁵/5! − …` on the reduced argument, returning a
391    /// [`Ball`] whose radius is tracked mechanically (each term's rounding plus the truncated tail).
392    fn sin_compute<const B: Word>(self, x: &Ball<B>) -> Result<Ball<B>, FpError> {
393        if x.mid.significand.is_zero() {
394            return Ok(Ball::exact(x.mid.clone()));
395        }
396        let wp = self.precision;
397        let x2 = x.mul(x, wp)?;
398        let mut sum = x.clone();
399        let mut term = x.clone();
400        let mut k = 1usize;
401        loop {
402            term = term.mul(&x2, wp)?.div_int((2 * k) * (2 * k + 1), wp)?;
403            if term.mid_le_ulp_lb(&sum, wp) {
404                break;
405            }
406            if k % 2 == 1 {
407                sum = sum.sub(&term, wp)?;
408            } else {
409                sum = sum.add(&term, wp)?;
410            }
411            k += 1;
412        }
413        // Omitted tail: the alternating series tail is < the first omitted term < 1 ulp.
414        // A zero sum means the argument (and every term) is exactly zero — the tail is 0.
415        if !sum.mid.significand().is_zero() {
416            sum.add_error(ulps::<B>(&sum.mid, wp, 2));
417        }
418        Ok(sum)
419    }
420
421    /// Calculate the cosine of the floating point representation.
422    pub fn cos<const B: Word>(
423        &self,
424        x: &Repr<B>,
425        mut cache: Option<&mut ConstCache>,
426    ) -> FpResult<FBig<R, B>> {
427        if x.is_infinite() {
428            return Err(FpError::InfiniteInput);
429        }
430        assert_limited_precision(self.precision);
431
432        if x.significand.is_zero() {
433            // cos(±0) = 1
434            return Ok(FBig::<R, B>::ONE.with_precision(self.precision));
435        }
436
437        self.ziv(50, |guard| {
438            let (work, r, quadrant) =
439                self.reduce_to_quadrant(x, guard, reborrow_cache(&mut cache))?;
440            let val = match quadrant {
441                Quadrant::First => work.cos_compute(&r)?,
442                Quadrant::Second => -work.sin_compute(&r)?,
443                Quadrant::Third => -work.cos_compute(&r)?,
444                Quadrant::Fourth => work.sin_compute(&r)?,
445            };
446            Ok(val.to_value_radius::<R>(&Context::<R>::new(work.precision)))
447        })
448    }
449
450    /// Near-correct cosine series `C(x) = 1 − x²/2! + x⁴/4! − …`, returning a [`Ball`] with a
451    /// mechanically tracked radius. (See [`sin_compute`](Self::sin_compute).)
452    fn cos_compute<const B: Word>(self, x: &Ball<B>) -> Result<Ball<B>, FpError> {
453        let wp = self.precision;
454        if x.mid.significand.is_zero() {
455            return Ok(Ball::exact_int(IBig::ONE, wp));
456        }
457        let x2 = x.mul(x, wp)?;
458        let one = Ball::exact_int(IBig::ONE, wp);
459        let mut sum = one.clone();
460        let mut term = one.clone();
461        let mut k = 1usize;
462        loop {
463            term = term.mul(&x2, wp)?.div_int((2 * k) * (2 * k - 1), wp)?;
464            if term.mid_le_ulp_lb(&sum, wp) {
465                break;
466            }
467            if k % 2 == 1 {
468                sum = sum.sub(&term, wp)?;
469            } else {
470                sum = sum.add(&term, wp)?;
471            }
472            k += 1;
473        }
474        sum.add_error(ulps::<B>(&sum.mid, wp, 2));
475        Ok(sum)
476    }
477
478    /// Calculate both the sine and cosine of the floating point representation.
479    ///
480    /// This is more efficient than calling `sin` and `cos` separately.
481    pub fn sin_cos<const B: Word>(
482        &self,
483        x: &Repr<B>,
484        mut cache: Option<&mut ConstCache>,
485    ) -> (FpResult<FBig<R, B>>, FpResult<FBig<R, B>>) {
486        if x.is_infinite() {
487            return (Err(FpError::InfiniteInput), Err(FpError::InfiniteInput));
488        }
489        assert_limited_precision(self.precision);
490
491        if x.significand.is_zero() {
492            // sin(±0) = ±0, cos(±0) = 1
493            let s = signed_zero_normal(self, x);
494            let c = Ok(FBig::<R, B>::ONE.with_precision(self.precision));
495            return (s, c);
496        }
497
498        let (s, c) = self.ziv_pair(50, |guard| {
499            let (work, r, quadrant) =
500                self.reduce_to_quadrant(x, guard, reborrow_cache(&mut cache))?;
501            let (sin_ball, cos_ball) = work.sin_cos_compute(&r)?;
502            let (s, c) = match quadrant {
503                Quadrant::First => (sin_ball, cos_ball),
504                Quadrant::Second => (cos_ball, -sin_ball),
505                Quadrant::Third => (-sin_ball, -cos_ball),
506                Quadrant::Fourth => (-cos_ball, sin_ball),
507            };
508            let ctx = Context::<R>::new(work.precision);
509            Ok((s.to_value_radius::<R>(&ctx), c.to_value_radius::<R>(&ctx)))
510        });
511        (s, c)
512    }
513
514    /// Simultaneously evaluate the sine and cosine series, returning both [`Ball`]s with
515    /// mechanically tracked radii.
516    pub(crate) fn sin_cos_compute<const B: Word>(
517        self,
518        x: &Ball<B>,
519    ) -> Result<(Ball<B>, Ball<B>), FpError> {
520        let wp = self.precision;
521        if x.mid.significand.is_zero() {
522            return Ok((Ball::exact(x.mid.clone()), Ball::exact_int(IBig::ONE, wp)));
523        }
524        let x2 = x.mul(x, wp)?;
525        let one = Ball::exact_int(IBig::ONE, wp);
526        let mut sin_sum = x.clone();
527        let mut cos_sum = one.clone();
528        let mut sin_term = x.clone();
529        let mut cos_term = one.clone();
530        let mut k = 1usize;
531        loop {
532            cos_term = cos_term.mul(&x2, wp)?.div_int((2 * k) * (2 * k - 1), wp)?;
533            sin_term = sin_term.mul(&x2, wp)?.div_int((2 * k) * (2 * k + 1), wp)?;
534
535            if sin_term.mid_le_ulp_lb(&sin_sum, wp) && cos_term.mid_le_ulp_lb(&cos_sum, wp) {
536                break;
537            }
538
539            if k % 2 == 1 {
540                cos_sum = cos_sum.sub(&cos_term, wp)?;
541                sin_sum = sin_sum.sub(&sin_term, wp)?;
542            } else {
543                cos_sum = cos_sum.add(&cos_term, wp)?;
544                sin_sum = sin_sum.add(&sin_term, wp)?;
545            }
546            k += 1;
547        }
548        if !sin_sum.mid.significand().is_zero() {
549            sin_sum.add_error(ulps::<B>(&sin_sum.mid, wp, 2));
550        }
551        cos_sum.add_error(ulps::<B>(&cos_sum.mid, wp, 2));
552        Ok((sin_sum, cos_sum))
553    }
554
555    /// Calculate the tangent of the floating point representation.
556    ///
557    /// # Note
558    /// Near odd multiples of π/2 the value grows without bound; dashu's wide exponent range holds
559    /// it as a large finite number rather than saturating to ±∞.
560    pub fn tan<const B: Word>(
561        &self,
562        x: &Repr<B>,
563        mut cache: Option<&mut ConstCache>,
564    ) -> FpResult<FBig<R, B>> {
565        if x.is_infinite() {
566            return Err(FpError::InfiniteInput);
567        }
568        assert_limited_precision(self.precision);
569
570        if x.significand.is_zero() {
571            // tan(±0) = ±0
572            return signed_zero_normal(self, x);
573        }
574
575        // tan = sin/cos, correctly rounded via the Ziv loop; the sin/cos error propagation into the
576        // quotient is tracked by the Ball division. The closure's `significand.is_zero()` guard
577        // below handles the unreachable exact-pole case (cos cancelling to a zero significand) by
578        // forcing a retry.
579        self.ziv(50, |guard| {
580            let (work, r, quadrant) =
581                self.reduce_to_quadrant(x, guard, reborrow_cache(&mut cache))?;
582            let (sin_ball, cos_ball) = work.sin_cos_compute(&r)?;
583            let (s, c) = match quadrant {
584                Quadrant::First => (sin_ball, cos_ball),
585                Quadrant::Second => (cos_ball, -sin_ball),
586                Quadrant::Third => (-sin_ball, -cos_ball),
587                Quadrant::Fourth => (-cos_ball, sin_ball),
588            };
589            if c.mid.significand.is_zero() {
590                // cos rounded to a zero significand at this guard (the input sits on a work-
591                // precision pole — unreachable for finite-precision x): force a retry.
592                return Ok((FBig::<R, B>::ZERO, FBig::<R, B>::ONE));
593            }
594            Ok(s.div(&c, work.precision)?
595                .to_value_radius::<R>(&Context::<R>::new(work.precision)))
596        })
597    }
598
599    /// Calculate the sine of `2π·x/u` — the ×u family with `u = 2` (i.e. `sin(x·π)`).
600    ///
601    /// See [`sin_unit`](Self::sin_unit) for the semantics; this is a thin wrapper.
602    pub fn sin_pi<const B: Word>(
603        &self,
604        x: &Repr<B>,
605        cache: Option<&mut ConstCache>,
606    ) -> FpResult<FBig<R, B>> {
607        self.sin_unit(x, 2, cache)
608    }
609
610    /// Calculate the sine of `2π·x/u` (the angle x is measured in units of the full turn
611    /// divided by `u`; e.g. `u = 360` gives degrees).
612    ///
613    /// # Methodology
614    /// The ×u variant has rational special points: the argument reduces *exactly* mod u in
615    /// integer arithmetic, so arguments where `12x/u` is an integer resolve to exact values
616    /// (`0`, `±1`, `±1/2`) and — unlike [`sin`](Self::sin) — the accuracy does not degrade
617    /// with the magnitude of x. The exact cases are resolved before the Ziv loop, whose
618    /// containment test cannot certify exact values under directed rounding; the general path
619    /// evaluates the sine/cosine series on the exact rational remainder
620    /// `r = |x| − k·u/4 ∈ [−u/8, u/8]` scaled by `2π/u`.
621    ///
622    /// Returns `Err(OutOfDomain)` if `u = 0`.
623    pub fn sin_unit<const B: Word>(
624        &self,
625        x: &Repr<B>,
626        u: usize,
627        mut cache: Option<&mut ConstCache>,
628    ) -> FpResult<FBig<R, B>> {
629        if u == 0 {
630            return Err(FpError::OutOfDomain);
631        }
632        if x.is_infinite() {
633            return Err(FpError::InfiniteInput);
634        }
635        assert_limited_precision(self.precision);
636
637        // Exact cases (±0 lands in the quarter table's q = 0 row). Resolved outside the Ziv
638        // loop: its containment test cannot certify an exact 0/±1/±1/2 under directed rounding.
639        if let Some(j) = unit_residue(x, u, 12) {
640            if j % 3 == 0 {
641                // quarter table, keyed by q = (4x/u) mod 8 = (j/3) mod 8 — sin(q·π/2)
642                let q = j / 3;
643                return match q {
644                    0 | 2 | 4 | 6 => {
645                        Ok(Exact(FBig::<R, B>::new(Repr::zero_with_sign(x.sign()), *self)))
646                    }
647                    1 | 5 => Ok(FBig::<R, B>::ONE.with_precision(self.precision)),
648                    3 | 7 => Ok(FBig::<R, B>::NEG_ONE.with_precision(self.precision)),
649                    _ => unreachable!("q = 4x/u mod 8 covers all rows"),
650                };
651            } else if j % 2 == 1 {
652                // sine sixths: j mod 12 ∈ {1, 5} → +1/2, {7, 11} → −1/2
653                let positive = j % 12 == 1 || j % 12 == 5;
654                return exact_ratio(self, if positive { IBig::ONE } else { -IBig::ONE }, 2);
655            }
656            // j even with 3 ∤ j: the cosine sixths — sine is ±√3/2 there, not exact.
657        }
658
659        // sin is odd: reduce and evaluate on |x|, apply the sign at the end.
660        let reduced = reduce_unit_argument(x, u);
661        let negative = x.sign() == Sign::Negative;
662        self.ziv(50, |guard| {
663            let work = Context::<mode::HalfEven>::new(self.precision + guard);
664            let (t, quadrant) = unit_argument_ball(&work, &reduced, u, reborrow_cache(&mut cache))?;
665            let val = match quadrant {
666                Quadrant::First => work.sin_compute(&t)?,
667                Quadrant::Second => work.cos_compute(&t)?,
668                Quadrant::Third => -work.sin_compute(&t)?,
669                Quadrant::Fourth => -work.cos_compute(&t)?,
670            };
671            let val = if negative { -val } else { val };
672            Ok(val.to_value_radius::<R>(&Context::<R>::new(work.precision)))
673        })
674    }
675
676    /// Calculate the cosine of `2π·x/u` — the ×u family with `u = 2` (i.e. `cos(x·π)`).
677    ///
678    /// See [`cos_unit`](Self::cos_unit) for the semantics; this is a thin wrapper.
679    pub fn cos_pi<const B: Word>(
680        &self,
681        x: &Repr<B>,
682        cache: Option<&mut ConstCache>,
683    ) -> FpResult<FBig<R, B>> {
684        self.cos_unit(x, 2, cache)
685    }
686
687    /// Calculate the cosine of `2π·x/u` (see [`sin_unit`](Self::sin_unit) for the ×u
688    /// semantics).
689    ///
690    /// # Methodology
691    /// Exact whenever `12x/u` is an integer (integers map to `±1`, odd multiples of `u/4` to
692    /// `+0`, the cosine sixths to `±1/2`); the general path is [`sin_unit`](Self::sin_unit)'s
693    /// with the cos quadrant mapping. cos is even, so the result never depends on the sign
694    /// of x.
695    ///
696    /// Returns `Err(OutOfDomain)` if `u = 0`.
697    pub fn cos_unit<const B: Word>(
698        &self,
699        x: &Repr<B>,
700        u: usize,
701        mut cache: Option<&mut ConstCache>,
702    ) -> FpResult<FBig<R, B>> {
703        if u == 0 {
704            return Err(FpError::OutOfDomain);
705        }
706        if x.is_infinite() {
707            return Err(FpError::InfiniteInput);
708        }
709        assert_limited_precision(self.precision);
710
711        // Exact cases — outside the Ziv loop, whose containment test cannot certify exact
712        // values under directed rounding.
713        if let Some(j) = unit_residue(x, u, 12) {
714            if j % 3 == 0 {
715                // quarter table, keyed by q = (4x/u) mod 8 — cos(q·π/2)
716                let q = j / 3;
717                return match q {
718                    0 | 4 => Ok(FBig::<R, B>::ONE.with_precision(self.precision)),
719                    2 | 6 => Ok(FBig::<R, B>::NEG_ONE.with_precision(self.precision)),
720                    1 | 3 | 5 | 7 => Ok(Exact(FBig::<R, B>::new(Repr::zero(), *self))),
721                    _ => unreachable!("q = 4x/u mod 8 covers all rows"),
722                };
723            } else if j % 2 == 0 {
724                // cosine sixths: 6x/u mod 6 ∈ {1, 5} → +1/2, {2, 4} → −1/2; from
725                // j = 12x/u mod 24: j mod 12 ∈ {2, 10} → +1/2, {4, 8} → −1/2.
726                let m = j % 12;
727                let positive = m == 2 || m == 10;
728                return exact_ratio(self, if positive { IBig::ONE } else { -IBig::ONE }, 2);
729            }
730            // j odd (the sine sixths): cosine is ±√3/2 there, not exact.
731        }
732
733        let reduced = reduce_unit_argument(x, u);
734        self.ziv(50, |guard| {
735            let work = Context::<mode::HalfEven>::new(self.precision + guard);
736            let (t, quadrant) = unit_argument_ball(&work, &reduced, u, reborrow_cache(&mut cache))?;
737            let val = match quadrant {
738                Quadrant::First => work.cos_compute(&t)?,
739                Quadrant::Second => -work.sin_compute(&t)?,
740                Quadrant::Third => -work.cos_compute(&t)?,
741                Quadrant::Fourth => work.sin_compute(&t)?,
742            };
743            Ok(val.to_value_radius::<R>(&Context::<R>::new(work.precision)))
744        })
745    }
746
747    /// Calculate both the sine and cosine of `2π·x/u` — the ×u family with `u = 2`.
748    ///
749    /// See [`sin_cos_unit`](Self::sin_cos_unit); this is a thin wrapper.
750    pub fn sin_cos_pi<const B: Word>(
751        &self,
752        x: &Repr<B>,
753        cache: Option<&mut ConstCache>,
754    ) -> (FpResult<FBig<R, B>>, FpResult<FBig<R, B>>) {
755        self.sin_cos_unit(x, 2, cache)
756    }
757
758    /// Calculate both the sine and cosine of `2π·x/u`.
759    ///
760    /// This is more efficient than calling [`sin_unit`](Self::sin_unit) and
761    /// [`cos_unit`](Self::cos_unit) separately — except at the sixth-type exact points, where
762    /// exactly one of the two results is exact (`±1/2` vs `±√3/2`) and the other is evaluated
763    /// on its own.
764    ///
765    /// Returns `Err(OutOfDomain)` in both slots if `u = 0`.
766    pub fn sin_cos_unit<const B: Word>(
767        &self,
768        x: &Repr<B>,
769        u: usize,
770        mut cache: Option<&mut ConstCache>,
771    ) -> (FpResult<FBig<R, B>>, FpResult<FBig<R, B>>) {
772        if u == 0 {
773            return (Err(FpError::OutOfDomain), Err(FpError::OutOfDomain));
774        }
775        if x.is_infinite() {
776            return (Err(FpError::InfiniteInput), Err(FpError::InfiniteInput));
777        }
778        assert_limited_precision(self.precision);
779
780        // Exact cases — outside the Ziv loop, whose containment test cannot certify exact
781        // values under directed rounding.
782        if let Some(j) = unit_residue(x, u, 12) {
783            if j % 3 == 0 {
784                // both exact: the two quarter tables, keyed by q = (4x/u) mod 8
785                let q = j / 3;
786                let s = match q {
787                    0 | 2 | 4 | 6 => {
788                        Ok(Exact(FBig::<R, B>::new(Repr::zero_with_sign(x.sign()), *self)))
789                    }
790                    1 | 5 => Ok(FBig::<R, B>::ONE.with_precision(self.precision)),
791                    3 | 7 => Ok(FBig::<R, B>::NEG_ONE.with_precision(self.precision)),
792                    _ => unreachable!("q = 4x/u mod 8 covers all rows"),
793                };
794                let c = match q {
795                    0 | 4 => Ok(FBig::<R, B>::ONE.with_precision(self.precision)),
796                    2 | 6 => Ok(FBig::<R, B>::NEG_ONE.with_precision(self.precision)),
797                    1 | 3 | 5 | 7 => Ok(Exact(FBig::<R, B>::new(Repr::zero(), *self))),
798                    _ => unreachable!("q = 4x/u mod 8 covers all rows"),
799                };
800                return (s, c);
801            } else {
802                // sixth-type: one result is the exact ±1/2, the other (±√3/2) is evaluated on
803                // its own (its own Ziv loop — the re-classification inside the callee lands on
804                // the general path).
805                if j % 2 == 1 {
806                    let positive = j % 12 == 1 || j % 12 == 5;
807                    let s = exact_ratio(self, if positive { IBig::ONE } else { -IBig::ONE }, 2);
808                    let c = self.cos_unit(x, u, reborrow_cache(&mut cache));
809                    return (s, c);
810                }
811                let m = j % 12;
812                let positive = m == 2 || m == 10;
813                let c = exact_ratio(self, if positive { IBig::ONE } else { -IBig::ONE }, 2);
814                let s = self.sin_unit(x, u, reborrow_cache(&mut cache));
815                return (s, c);
816            }
817        }
818
819        // sin is odd (cos even): reduce and evaluate on |x|, apply the sign to sin at the end.
820        let reduced = reduce_unit_argument(x, u);
821        let negative = x.sign() == Sign::Negative;
822        let (s, c) = self.ziv_pair(50, |guard| {
823            let work = Context::<mode::HalfEven>::new(self.precision + guard);
824            let (t, quadrant) = unit_argument_ball(&work, &reduced, u, reborrow_cache(&mut cache))?;
825            let (sin_ball, cos_ball) = work.sin_cos_compute(&t)?;
826            let (s, c) = match quadrant {
827                Quadrant::First => (sin_ball, cos_ball),
828                Quadrant::Second => (cos_ball, -sin_ball),
829                Quadrant::Third => (-sin_ball, -cos_ball),
830                Quadrant::Fourth => (-cos_ball, sin_ball),
831            };
832            let s = if negative { -s } else { s };
833            let ctx = Context::<R>::new(work.precision);
834            Ok((s.to_value_radius::<R>(&ctx), c.to_value_radius::<R>(&ctx)))
835        });
836        (s, c)
837    }
838
839    /// Calculate the tangent of `2π·x/u` — the ×u family with `u = 2` (i.e. `tan(x·π)`).
840    ///
841    /// See [`tan_unit`](Self::tan_unit) for the semantics; this is a thin wrapper.
842    pub fn tan_pi<const B: Word>(
843        &self,
844        x: &Repr<B>,
845        cache: Option<&mut ConstCache>,
846    ) -> FpResult<FBig<R, B>> {
847        self.tan_unit(x, 2, cache)
848    }
849
850    /// Calculate the tangent of `2π·x/u` (see [`sin_unit`](Self::sin_unit) for the ×u
851    /// semantics).
852    ///
853    /// # Methodology
854    /// Every quarter-type argument (u a multiple of `u/4`) is exact: integers map to `±0`,
855    /// quarter-integers to `±1`. The odd multiples of `u/4` are the poles: tan approaches
856    /// `+∞` from one side and `−∞` from the other, so the sign of an infinity is unknown
857    /// there — like `0/0`, the case is reported as [`FpError::Indeterminate`] instead of
858    /// guessing a sign. Otherwise [`sin_unit`](Self::sin_unit)/[`cos_unit`](Self::cos_unit)
859    /// share one series evaluation; near a pole the wide exponent range holds a large finite
860    /// value, as for [`tan`](Self::tan).
861    ///
862    /// Returns `Err(OutOfDomain)` if `u = 0`.
863    pub fn tan_unit<const B: Word>(
864        &self,
865        x: &Repr<B>,
866        u: usize,
867        mut cache: Option<&mut ConstCache>,
868    ) -> FpResult<FBig<R, B>> {
869        if u == 0 {
870            return Err(FpError::OutOfDomain);
871        }
872        if x.is_infinite() {
873            return Err(FpError::InfiniteInput);
874        }
875        assert_limited_precision(self.precision);
876
877        // tan hits an exact value at every eighth-of-turn argument (`8x/u` an integer) — the
878        // whole table resolves before the Ziv loop (its containment test cannot certify exact
879        // 0/±1 under directed rounding). The poles (odd multiples of u/4, m ≡ 2 or 6 mod 8) are
880        // indeterminate: the two one-sided limits are +∞ and −∞, so no signed infinity can be
881        // certified — unlike 1/0, whose sign is well-defined.
882        if let Some(j) = unit_residue(x, u, 8) {
883            let m = j % 8; // the eighth-of-turn index (mod its period 8)
884            return match m {
885                0 | 4 => Ok(Exact(FBig::<R, B>::new(Repr::zero_with_sign(x.sign()), *self))),
886                1 | 5 => Ok(FBig::<R, B>::ONE.with_precision(self.precision)),
887                2 | 6 => Err(FpError::Indeterminate),
888                3 | 7 => Ok(FBig::<R, B>::NEG_ONE.with_precision(self.precision)),
889                _ => unreachable!("m = 8x/u mod 8 covers all rows"),
890            };
891        }
892
893        // tan is odd: reduce and evaluate on |x|, apply the sign at the end.
894        let reduced = reduce_unit_argument(x, u);
895        let negative = x.sign() == Sign::Negative;
896        self.ziv(50, |guard| {
897            let work = Context::<mode::HalfEven>::new(self.precision + guard);
898            let (t, quadrant) = unit_argument_ball(&work, &reduced, u, reborrow_cache(&mut cache))?;
899            let (sin_ball, cos_ball) = work.sin_cos_compute(&t)?;
900            let (s, c) = match quadrant {
901                Quadrant::First => (sin_ball, cos_ball),
902                Quadrant::Second => (cos_ball, -sin_ball),
903                Quadrant::Third => (-sin_ball, -cos_ball),
904                Quadrant::Fourth => (-cos_ball, sin_ball),
905            };
906            if c.mid.significand.is_zero() {
907                // cos rounded to a zero significand at this guard — unreachable here (the
908                // exact poles were resolved by the table, and |t| ≤ π/4 keeps cos ≥ √2/2);
909                // force a retry to stay on the safe side.
910                return Ok((FBig::<R, B>::ZERO, FBig::<R, B>::ONE));
911            }
912            let val = s.div(&c, work.precision)?;
913            let val = if negative { -val } else { val };
914            Ok(val.to_value_radius::<R>(&Context::<R>::new(work.precision)))
915        })
916    }
917
918    /// Calculate `asin(x)·u/(2π)` — the arc sine expressed in units of the full turn divided
919    /// by `u` (e.g. `u = 360` gives degrees). The inverse of [`sin_unit`](Self::sin_unit).
920    ///
921    /// # Methodology
922    /// The exact-value rows resolve outside the Ziv loop (directed rounding cannot certify
923    /// them there): `asin(±0) = ±0`, `asin(±1) = ±u/4`, and `asin(±1/2) = ±u/12`. The general
924    /// path scales the [`Ball`](crate) composition of the radian [`asin`](Self::asin) by
925    /// `u/(2π)`.
926    ///
927    /// `u = 0` is the limit `u·asin(x)/(2π) → ±0` (the signed zero, matching the function's
928    /// oddness, in-domain inputs only); returns `Err(OutOfDomain)` if `|x| > 1` — the domain
929    /// error outranks the `u = 0` limit (`asin_unit(2, 0)` is an error, not `+0`).
930    pub fn asin_unit<const B: Word>(
931        &self,
932        x: &Repr<B>,
933        u: usize,
934        mut cache: Option<&mut ConstCache>,
935    ) -> FpResult<FBig<R, B>> {
936        if x.is_infinite() {
937            return Err(FpError::InfiniteInput);
938        }
939        assert_limited_precision(self.precision);
940        if x.significand.is_zero() {
941            // asin(±0) = ±0 (asin is odd), exact.
942            return signed_zero_normal(self, x);
943        }
944
945        // Domain check |x| ≤ 1 and the `±u/4` row, compared *exactly* (no precision
946        // argument) — `±1` is a vertical tangent of `asin`, and the ×u inverse family's exact
947        // rows are exactly that, not "rounded onto".
948        if repr_cmp_same_base::<B, true>(x, &Repr::<B>::one(), None).is_gt() {
949            return Err(FpError::OutOfDomain);
950        }
951        if u == 0 {
952            // the u → 0 limit: the signed zero (asin is odd)
953            return signed_zero_normal(self, x);
954        }
955        // exact rows (±u/4, ±u/12), correctly rounded for any base by the exact ratio
956        if repr_cmp_same_base::<B, true>(x, &Repr::<B>::one(), None).is_eq() {
957            let num = if x.sign() == Sign::Negative {
958                -(IBig::from(u))
959            } else {
960                IBig::from(u)
961            };
962            return exact_ratio(self, num, 4);
963        }
964        // The `±1/2` row keeps the *rounded* comparison: the derivative is bounded there
965        // (`2/√3`), so an argument a sub-ulp below `1/2` moves the result by at most an ulp — and
966        // `1/2` is not exactly representable in an odd base, so there is no exact form to compare.
967        let x_orig = FBig::<R, B>::new(x.clone(), *self);
968        if x_orig.abs_cmp(&(FBig::<R, B>::ONE / 2u8)).is_eq() {
969            let num = if x.sign() == Sign::Negative {
970                -(IBig::from(u))
971            } else {
972                IBig::from(u)
973            };
974            return exact_ratio(self, num, 12);
975        }
976
977        self.ziv(50, |guard| {
978            let work = Context::<mode::HalfEven>::new(self.precision + guard);
979            let x_ball = Ball::from_rounded(work.repr_round_ref(x), work.precision);
980            let a = work.asin_ball::<B>(&x_ball, reborrow_cache(&mut cache))?;
981            Ok(scale_unit(&work, &a, u, reborrow_cache(&mut cache))?
982                .to_value_radius::<R>(&Context::<R>::new(work.precision)))
983        })
984    }
985
986    /// Calculate `acos(x)·u/(2π)` — the arc cosine in units of the full turn divided by `u`.
987    /// The inverse of [`cos_unit`](Self::cos_unit).
988    ///
989    /// # Methodology
990    /// Uses the identity `acos(x) = π/2 − asin(x)` scaled by `u/(2π)`, i.e. `u/4 −
991    /// asin_unit(x)`. Exact rows (outside the Ziv loop): `acos(+1) = 0`, `acos(−1) = u/2`,
992    /// `acos(±0) = u/4`, `acos(±1/2) = u/6` / `u/3`.
993    ///
994    /// `u = 0` is the limit `u·acos(x)/(2π) → +0` (acos ≥ 0, in-domain inputs only); returns
995    /// `Err(OutOfDomain)` if `|x| > 1` — the domain error outranks the `u = 0` limit.
996    pub fn acos_unit<const B: Word>(
997        &self,
998        x: &Repr<B>,
999        u: usize,
1000        mut cache: Option<&mut ConstCache>,
1001    ) -> FpResult<FBig<R, B>> {
1002        if x.is_infinite() {
1003            return Err(FpError::InfiniteInput);
1004        }
1005        assert_limited_precision(self.precision);
1006
1007        // Domain |x| ≤ 1 and the `u/2` row, compared *exactly* (no precision argument) — see
1008        // `asin_unit` for why the exact form matters here.
1009        let cmp_one = repr_cmp_same_base::<B, true>(x, &Repr::<B>::one(), None);
1010        if cmp_one.is_gt() {
1011            return Err(FpError::OutOfDomain);
1012        }
1013        if u == 0 {
1014            // the u → 0 limit: +0 (acos ≥ 0)
1015            return Ok(Exact(FBig::<R, B>::new(Repr::zero(), *self)));
1016        }
1017        // exact rows — see asin_unit for why they stay outside the Ziv loop
1018        if cmp_one.is_eq() {
1019            return if x.sign() == Sign::Positive {
1020                Ok(Exact(FBig::<R, B>::new(Repr::zero(), *self)))
1021            } else {
1022                exact_ratio(self, IBig::from(u), 2)
1023            };
1024        }
1025        if x.significand.is_zero() {
1026            return exact_ratio(self, IBig::from(u), 4);
1027        }
1028        // The `u/6` row keeps the rounded comparison, as in `asin_unit` (bounded derivative).
1029        if FBig::<R, B>::new(x.clone(), *self)
1030            .abs_cmp(&(FBig::<R, B>::ONE / 2u8))
1031            .is_eq()
1032        {
1033            return exact_ratio(
1034                self,
1035                if x.sign() == Sign::Positive {
1036                    IBig::from(u)
1037                } else {
1038                    IBig::from(2) * IBig::from(u)
1039                },
1040                6,
1041            );
1042        }
1043
1044        // acos_unit(x) = u/4 − asin_unit(x): the composition avoids the catastrophic
1045        // cancellation `π/2 − asin(x)` would suffer near x = 1 once scaled.
1046        self.ziv(50, |guard| {
1047            let work = Context::<mode::HalfEven>::new(self.precision + guard);
1048            let x_ball = Ball::from_rounded(work.repr_round_ref(x), work.precision);
1049            let a = work.asin_ball::<B>(&x_ball, reborrow_cache(&mut cache))?;
1050            let quarter = scale_unit(&work, &a, u, reborrow_cache(&mut cache))?;
1051            // u/4 as an exact ball (held at u's own digit width — see unit_argument_ball)
1052            let u_digits = digit_len::<B>(&IBig::from(u)).max(work.precision);
1053            let wider = u_digits.max(2);
1054            let u_over_4 = Ball::exact_int(IBig::from(u), u_digits)
1055                .div(&Ball::exact_int(IBig::from(4), wider), wider)?;
1056            Ok(u_over_4
1057                .sub(&quarter, wider)?
1058                .to_value_radius::<R>(&Context::<R>::new(work.precision)))
1059        })
1060    }
1061
1062    /// Calculate `atan(x)·u/(2π)` — the arc tangent in units of the full turn divided by `u`.
1063    /// The inverse of [`tan_unit`](Self::tan_unit).
1064    ///
1065    /// # Methodology
1066    /// Exact rows (outside the Ziv loop): `atan(±0) = ±0`, `atan(±1) = ±u/8`,
1067    /// `atan(±inf) = ±u/4`. The general path scales the radian [`atan`](Self::atan)
1068    /// composition by `u/(2π)`.
1069    ///
1070    /// `u = 0` is the limit `u·atan(x)/(2π) → ±0` (the signed zero, matching the oddness).
1071    pub fn atan_unit<const B: Word>(
1072        &self,
1073        x: &Repr<B>,
1074        u: usize,
1075        mut cache: Option<&mut ConstCache>,
1076    ) -> FpResult<FBig<R, B>> {
1077        if x.is_infinite() {
1078            // atan(±inf) = ±u/4 — the scaled form of ±π/2
1079            if u == 0 {
1080                return signed_zero_normal(self, x);
1081            }
1082            let num = if x.sign() == Sign::Negative {
1083                -(IBig::from(u))
1084            } else {
1085                IBig::from(u)
1086            };
1087            return exact_ratio(self, num, 4);
1088        }
1089        assert_limited_precision(self.precision);
1090        if u == 0 {
1091            return signed_zero_normal(self, x);
1092        }
1093        if x.significand.is_zero() {
1094            // atan(±0) = ±0
1095            return signed_zero_normal(self, x);
1096        }
1097        // atan(±1) = ±u/8 — outside the Ziv loop. Compared *exactly*, like the other `±1` rows;
1098        // `atan` has no pole there, so a rounded `1 ± ulp` argument would only be a fraction of an
1099        // ulp out, but the exact form makes the whole family's `±1` rows mean "the argument *is*
1100        // the endpoint".
1101        if repr_cmp_same_base::<B, true>(x, &Repr::<B>::one(), None).is_eq() {
1102            let num = if x.sign() == Sign::Negative {
1103                -(IBig::from(u))
1104            } else {
1105                IBig::from(u)
1106            };
1107            return exact_ratio(self, num, 8);
1108        }
1109
1110        self.ziv(50, |guard| {
1111            let work = Context::<mode::HalfEven>::new(self.precision + guard);
1112            let x_ball = Ball::from_rounded(work.repr_round_ref(x), work.precision);
1113            let a = work.atan_ball::<B>(&x_ball, reborrow_cache(&mut cache))?;
1114            Ok(scale_unit(&work, &a, u, reborrow_cache(&mut cache))?
1115                .to_value_radius::<R>(&Context::<R>::new(work.precision)))
1116        })
1117    }
1118
1119    /// Calculate `atan2(y, x)·u/(2π)` — the four-quadrant arc tangent in units of the full
1120    /// turn divided by `u`. The inverse of [`tan_unit`](Self::tan_unit) with quadrant
1121    /// disambiguation.
1122    ///
1123    /// # Methodology
1124    /// The axis/infinity special values follow the same C99 signed-zero model as
1125    /// [`atan2`](Self::atan2), with the π-multiples mapped to their `u`-fractions (`π/4 →
1126    /// u/8`, `π/2 → u/4`, `3π/4 → 3u/8`, `π → u/2`). The general path scales the radian
1127    /// atan2 by `u/(2π)`.
1128    ///
1129    /// `u = 0` is the limit `u·atan2(y, x)/(2π) → ±0` (the sign of the angle). Returns
1130    /// `Err(OutOfDomain)` if both arguments are zero.
1131    pub fn atan2_unit<const B: Word>(
1132        &self,
1133        y: &Repr<B>,
1134        x: &Repr<B>,
1135        u: usize,
1136        mut cache: Option<&mut ConstCache>,
1137    ) -> FpResult<FBig<R, B>> {
1138        if y.is_finite() && x.is_finite() && y.significand.is_zero() && x.significand.is_zero() {
1139            return Err(FpError::OutOfDomain);
1140        }
1141        if u == 0 {
1142            // the u → 0 limit: the signed zero carrying the angle's sign (atan2's sign is y's)
1143            return Ok(Exact(FBig::<R, B>::new(Repr::zero_with_sign(y.sign()), *self)));
1144        }
1145
1146        assert_limited_precision(self.precision);
1147
1148        // The `k·π/8` angles as exact `k·u/8` ratios (shared by the infinity table and the
1149        // finite axis/diagonal rows below).
1150        let u8th = |k: i8| -> FpResult<FBig<R, B>> {
1151            let num = IBig::from(u) * IBig::from(k.unsigned_abs());
1152            exact_ratio::<R, B>(self, if k < 0 { -num } else { num }, 8)
1153        };
1154
1155        // Infinities, per the C99 model — the π-multiples map to u-fractions.
1156        if y.is_infinite() || x.is_infinite() {
1157            let (sy, sx) = (y.sign() == Sign::Positive, x.sign() == Sign::Positive);
1158            return match (y.is_infinite(), x.is_infinite(), sy, sx) {
1159                (true, true, true, true) => u8th(1),   // atan2(+inf, +inf) = π/4
1160                (true, true, true, false) => u8th(3),  // 3π/4
1161                (true, true, false, true) => u8th(-1), // −π/4
1162                (true, true, false, false) => u8th(-3), // −3π/4
1163                (true, false, true, _) => u8th(2),     // atan2(+inf, finite) = π/2
1164                (true, false, false, _) => u8th(-2),   // −π/2
1165                (false, true, _, true) => {
1166                    // atan2(±finite, +inf) = ±0 (signed zero of y)
1167                    Ok(Exact(FBig::<R, B>::new(Repr::zero_with_sign(y.sign()), *self)))
1168                }
1169                (false, true, true, false) => u8th(4), // atan2(+y, −inf) = π
1170                (false, true, false, false) => u8th(-4), // −π
1171                _ => unreachable!(),
1172            };
1173        }
1174
1175        // The finite axis/diagonal rows, where atan2(y, x) is an exact multiple of π/4 (so the
1176        // scaled result `k·u/8` is exactly representable). Like the other exact rows they must
1177        // stay outside the Ziv loop: an exactly-representable result has a one-sided rounding
1178        // preimage under the directed modes, which no ball interval can certify.
1179        if y.significand.is_zero() {
1180            // y = ±0, x ≠ 0 (both-zero was screened): atan2(±0, +x) = ±0 (C99 signed zero),
1181            // atan2(±0, −x) = ±π → the u/2 row with the sign of y.
1182            return if x.sign() == Sign::Positive {
1183                Ok(Exact(FBig::<R, B>::new(Repr::zero_with_sign(y.sign()), *self)))
1184            } else {
1185                u8th(if y.sign() == Sign::Positive { 4 } else { -4 })
1186            };
1187        }
1188        if x.significand.is_zero() {
1189            // x = 0, y nonzero: atan2 = ±π/2 → ±u/4.
1190            return u8th(if y.sign() == Sign::Positive { 2 } else { -2 });
1191        }
1192        // |y| == |x| (finite, nonzero): the diagonals ±π/4, ±3π/4.
1193        let y_abs =
1194            FBig::<R, B>::new(Repr::new(y.significand.clone().abs(), y.exponent), Context::new(0));
1195        let x_abs =
1196            FBig::<R, B>::new(Repr::new(x.significand.clone().abs(), x.exponent), Context::new(0));
1197        if y_abs.abs_cmp(&x_abs).is_eq() {
1198            let same_half = (y.sign() == Sign::Positive) == (x.sign() == Sign::Positive);
1199            return u8th(match (y.sign() == Sign::Positive, same_half) {
1200                (true, true) => 1,   // (+y, +x): π/4
1201                (true, false) => 3,  // (+y, −x): 3π/4
1202                (false, true) => -1, // (−y, +x): −π/4
1203                (false, false) => -3,
1204            });
1205        }
1206
1207        // x ≠ 0, finite: atan2(y/x) ± (quadrant π), scaled by u/(2π). The inner atan2 is
1208        // itself correctly rounded at the working precision; the scaling's rounding is
1209        // tracked by the Ball.
1210        self.ziv(50, |guard| {
1211            let work = Context::<mode::HalfEven>::new(self.precision + guard);
1212            let a = work.atan2::<B>(y, x, reborrow_cache(&mut cache))?;
1213            let a_ball = Ball::from_rounded(a.map(FBig::into_repr), work.precision);
1214            Ok(scale_unit(&work, &a_ball, u, reborrow_cache(&mut cache))?
1215                .to_value_radius::<R>(&Context::<R>::new(work.precision)))
1216        })
1217    }
1218
1219    /// Calculate the arcsine of the floating point representation.
1220    ///
1221    /// # Methodology
1222    /// Uses the identity: `asin(x) = atan(x / sqrt(1 - x^2))`
1223    /// Returns `Err(OutOfDomain)` if `|x| > 1`.
1224    pub fn asin<const B: Word>(
1225        &self,
1226        x: &Repr<B>,
1227        mut cache: Option<&mut ConstCache>,
1228    ) -> FpResult<FBig<R, B>> {
1229        if x.is_infinite() {
1230            return Err(FpError::InfiniteInput);
1231        }
1232        assert_limited_precision(self.precision);
1233        if x.significand.is_zero() {
1234            // asin(±0) = ±0 (asin is odd), exact.
1235            return signed_zero_normal(self, x);
1236        }
1237
1238        // Domain check: |x| must be <= 1, compared *exactly* (no precision argument) — `±1` is
1239        // a vertical tangent of `asin`, so an argument that merely rounds onto it
1240        // (`1 − 10⁻¹⁵⁵` at 100 digits) is not the endpoint.
1241        if repr_cmp_same_base::<B, true>(x, &Repr::<B>::one(), None).is_gt() {
1242            return Err(FpError::OutOfDomain);
1243        }
1244
1245        self.ziv(50, |guard| {
1246            let work = Context::<mode::HalfEven>::new(self.precision + guard);
1247            let x_ball = Ball::from_rounded(work.repr_round_ref(x), work.precision);
1248            Ok(work
1249                .asin_ball::<B>(&x_ball, reborrow_cache(&mut cache))?
1250                .to_value_radius::<R>(&Context::<R>::new(work.precision)))
1251        })
1252    }
1253
1254    /// `asin` of a ball: `atan(x / √(1−x²))`, with the `|x| = 1` endpoint `±π/2` handled directly
1255    /// (the composition's `√(1−x²)` denominator would round to zero there).
1256    fn asin_ball<const B: Word>(
1257        &self,
1258        x: &Ball<B>,
1259        mut cache: Option<&mut ConstCache>,
1260    ) -> Result<Ball<B>, FpError> {
1261        let wp = self.precision;
1262        let one = Ball::exact_int(IBig::ONE, wp);
1263        let d = one.sub(&x.mul(x, wp)?, wp)?.sqrt(wp)?;
1264        if d.mid.significand.is_zero() {
1265            // |x| = 1: asin(±1) = ±π/2 (an exact-ish endpoint; the π radius is folded in).
1266            let pi = Context::<mode::HalfEven>::new(wp)
1267                .pi::<B>(reborrow_cache(&mut cache))
1268                .value();
1269            let half_pi = &pi / 2u8;
1270            // `d = √(1 − x²)` collapsed onto zero — which is the endpoint *only* when the
1271            // argument is exactly ±1. Otherwise the input merely rounded onto ±1 at this work
1272            // precision, and the true `√(2δ)` is many ulps away (the derivative at the endpoint
1273            // is infinite): reporting π/2 with a few ulps of radius would certify a value the
1274            // true result is nowhere near. An infinite radius instead forces a Ziv retry at a
1275            // higher guard, where the argument (and so the true difference) is representable and
1276            // the general path below computes it.
1277            let rad = if d.rad.is_zero() {
1278                ulps::<B>(&half_pi.repr, wp, 8)
1279            } else {
1280                Mag::INFINITY
1281            };
1282            let half_pi = Ball::with_error(half_pi.into_repr(), rad);
1283            Ok(if x.mid.sign() == Sign::Negative {
1284                -half_pi
1285            } else {
1286                half_pi
1287            })
1288        } else {
1289            let arg = x.div(&d, wp)?;
1290            self.atan_ball::<B>(&arg, reborrow_cache(&mut cache))
1291        }
1292    }
1293
1294    /// Calculate the arccosine of the floating point representation.
1295    ///
1296    /// # Methodology
1297    /// Uses the identity: `acos(x) = pi/2 - asin(x)`.
1298    /// Higher precision is used internally to avoid catastrophic cancellation near x ≈ 1.
1299    pub fn acos<const B: Word>(
1300        &self,
1301        x: &Repr<B>,
1302        mut cache: Option<&mut ConstCache>,
1303    ) -> FpResult<FBig<R, B>> {
1304        if x.is_infinite() {
1305            return Err(FpError::InfiniteInput);
1306        }
1307        assert_limited_precision(self.precision);
1308
1309        // Domain |x| ≤ 1 and the `±1` rows, compared *exactly* (no precision argument):
1310        // `acos(1 − δ)` is `√(2δ)`, which the endpoint's `0` misses entirely once `δ` is far
1311        // above the result's ulp.
1312        let cmp_one = repr_cmp_same_base::<B, true>(x, &Repr::<B>::one(), None);
1313        if cmp_one.is_gt() {
1314            return Err(FpError::OutOfDomain);
1315        }
1316        if cmp_one.is_eq() {
1317            // |x| = 1: the composition π/2 − asin(±1) cancels onto an exact value. acos(1) = 0 is
1318            // the acute case — under directed rounding 0's preimage is one-sided ([0, ulp)), so the
1319            // Ziv containment test can never certify it. acos(-1) = π is handled here too.
1320            return Ok(if x.sign() == Sign::Positive {
1321                Exact(FBig::<R, B>::new(Repr::zero(), *self))
1322            } else {
1323                self.pi::<B>(reborrow_cache(&mut cache))
1324            });
1325        }
1326
1327        self.ziv(50, |guard| {
1328            let work = Context::<mode::HalfEven>::new(self.precision + guard);
1329            let x_ball = Ball::from_rounded(work.repr_round_ref(x), work.precision);
1330            let asin_ball = work.asin_ball::<B>(&x_ball, reborrow_cache(&mut cache))?;
1331            let pi = work.pi::<B>(reborrow_cache(&mut cache)).value();
1332            let half_pi = &pi / 2u8;
1333            let rad = ulps::<B>(&half_pi.repr, work.precision, 8);
1334            let half_pi = Ball::with_error(half_pi.into_repr(), rad);
1335            Ok(half_pi
1336                .sub(&asin_ball, work.precision)?
1337                .to_value_radius::<R>(&Context::<R>::new(work.precision)))
1338        })
1339    }
1340
1341    /// Calculate the arctangent of the floating point representation.
1342    pub fn atan<const B: Word>(
1343        &self,
1344        x: &Repr<B>,
1345        mut cache: Option<&mut ConstCache>,
1346    ) -> FpResult<FBig<R, B>> {
1347        if x.is_infinite() {
1348            // atan(±inf) = ±π/2 — preserved (a well-defined finite result for an infinite input)
1349            let pi = self.pi::<B>(reborrow_cache(&mut cache)).value();
1350            let half_pi: FBig<R, B> = pi / 2;
1351            let res: FBig<R, B> = if x.sign() == Sign::Positive {
1352                half_pi
1353            } else {
1354                -half_pi
1355            };
1356            return Ok(res.with_precision(self.precision));
1357        }
1358
1359        assert_limited_precision(self.precision);
1360
1361        if x.significand.is_zero() {
1362            // atan(±0) = ±0
1363            return signed_zero_normal(self, x);
1364        }
1365
1366        self.ziv(50, |guard| {
1367            let work = Context::<mode::HalfEven>::new(self.precision + guard);
1368            let x_ball = Ball::from_rounded(work.repr_round_ref(x), work.precision);
1369            Ok(work
1370                .atan_ball::<B>(&x_ball, reborrow_cache(&mut cache))?
1371                .to_value_radius::<R>(&Context::<R>::new(work.precision)))
1372        })
1373    }
1374
1375    /// `atan` of a ball, with the `|x| ≥ 1` branch (`π/2 − atan(1/x)`). Odd: the sign of `x` is
1376    /// applied last (the `|x| ≥ 1` branch's `1/x` would otherwise lose it).
1377    fn atan_ball<const B: Word>(
1378        &self,
1379        x: &Ball<B>,
1380        mut cache: Option<&mut ConstCache>,
1381    ) -> Result<Ball<B>, FpError> {
1382        let wp = self.precision;
1383        let sign = x.mid.sign();
1384        let x_abs = if sign == Sign::Negative {
1385            -x.clone()
1386        } else {
1387            x.clone()
1388        };
1389        let one = Ball::exact_int(IBig::ONE, wp);
1390        let abs_ge_one = repr_cmp_same_base::<B, true>(&x_abs.mid, &one.mid, None).is_ge();
1391        let res = if abs_ge_one {
1392            let pi = Context::<mode::HalfEven>::new(wp)
1393                .pi::<B>(reborrow_cache(&mut cache))
1394                .value();
1395            let half_pi = &pi / 2u8;
1396            let rad = ulps::<B>(&half_pi.repr, wp, 8);
1397            let half_pi = Ball::with_error(half_pi.into_repr(), rad);
1398            let inv_x = one.div(&x_abs, wp)?;
1399            half_pi.sub(&self.atan_compute(&inv_x)?, wp)
1400        } else {
1401            self.atan_compute(&x_abs)
1402        };
1403        Ok(if sign == Sign::Negative { -res? } else { res? })
1404    }
1405
1406    /// Near-correct Euler series for `atan(x)` (`|x| ≤ 1`), returning a [`Ball`] with a
1407    /// mechanically tracked radius.
1408    fn atan_compute<const B: Word>(self, x: &Ball<B>) -> Result<Ball<B>, FpError> {
1409        let wp = self.precision;
1410        let x2 = x.mul(x, wp)?;
1411        let one = Ball::exact_int(IBig::ONE, wp);
1412        let one_plus_x2 = one.add(&x2, wp)?;
1413        let mut term = x.div(&one_plus_x2, wp)?;
1414        let mut sum = term.clone();
1415        let factor = x2.scale_int(&IBig::from(2), wp)?.div(&one_plus_x2, wp)?;
1416        let mut n = 1usize;
1417        loop {
1418            term = term
1419                .mul(&factor, wp)?
1420                .scale_int(&IBig::from(n), wp)?
1421                .div_int(2 * n + 1, wp)?;
1422            if term.mid_le_ulp_lb(&sum, wp) {
1423                break;
1424            }
1425            sum = sum.add(&term, wp)?;
1426            n += 1;
1427        }
1428        // Omitted tail: the Euler terms shrink by (2x²/(1+x²))·n/(2n+1) < 1/2, so the tail is < 2 ulps.
1429        // A zero sum means x (and every term) is exactly zero — the tail is 0 (atan2(±0, ·)).
1430        if !sum.mid.significand().is_zero() {
1431            sum.add_error(ulps::<B>(&sum.mid, wp, 2));
1432        }
1433        Ok(sum)
1434    }
1435
1436    /// Calculate the arctangent of y / x.
1437    ///
1438    /// Handles signed infinities according to IEEE 754 standards.
1439    /// Returns `Err(OutOfDomain)` if both arguments are zero.
1440    pub fn atan2<const B: Word>(
1441        &self,
1442        y: &Repr<B>,
1443        x: &Repr<B>,
1444        mut cache: Option<&mut ConstCache>,
1445    ) -> FpResult<FBig<R, B>> {
1446        if y.is_finite() && x.is_finite() && y.significand.is_zero() && x.significand.is_zero() {
1447            return Err(FpError::OutOfDomain);
1448        }
1449
1450        assert_limited_precision(self.precision);
1451
1452        // Handle Infinities according to IEEE 754 (computed at the target precision).
1453        if y.is_infinite() || x.is_infinite() {
1454            let (sy, sx) = (y.sign() == Sign::Positive, x.sign() == Sign::Positive);
1455            let pi_val = self.pi::<B>(reborrow_cache(&mut cache)).value();
1456            let res: FBig<R, B> = match (y.is_infinite(), x.is_infinite(), sy, sx) {
1457                (true, true, true, true) => pi_val.clone() / 4u8,
1458                (true, true, true, false) => pi_val.clone() * 3u8 / 4u8,
1459                (true, true, false, true) => -(pi_val.clone() / 4u8),
1460                (true, true, false, false) => -(pi_val.clone() * 3u8 / 4u8),
1461                (true, false, true, _) => pi_val.clone() / 2u8,
1462                (true, false, false, _) => -(pi_val.clone() / 2u8),
1463                (false, true, _, true) => {
1464                    // atan2(±finite, +inf) = ±0 (signed zero of y)
1465                    if sy {
1466                        FBig::<R, B>::ZERO
1467                    } else {
1468                        FBig::<R, B>::new(Repr::neg_zero(), *self)
1469                    }
1470                }
1471                (false, true, true, false) => pi_val.clone(),
1472                (false, true, false, false) => -pi_val,
1473                _ => unreachable!(),
1474            };
1475            return Ok(res.with_precision(self.precision));
1476        }
1477
1478        // x == 0, y finite nonzero: atan2 = ±π/2.
1479        if x.significand.is_zero() {
1480            let half_pi = self.pi::<B>(reborrow_cache(&mut cache)).value() / 2u8;
1481            let res = if y.sign() == Sign::Positive {
1482                half_pi
1483            } else {
1484                -half_pi
1485            };
1486            return Ok(res.with_precision(self.precision));
1487        }
1488
1489        // x ≠ 0, finite: atan2 = atan(y/x) ± (quadrant π), all as Ball composition.
1490        self.ziv(50, |guard| {
1491            let work = Context::<mode::HalfEven>::new(self.precision + guard);
1492            let y_ball = Ball::from_rounded(work.repr_round_ref(y), work.precision);
1493            let x_ball = Ball::from_rounded(work.repr_round_ref(x), work.precision);
1494            let ratio = y_ball.div(&x_ball, work.precision)?;
1495            let atan_val = work.atan_ball::<B>(&ratio, reborrow_cache(&mut cache))?;
1496            let res = if x.sign() == Sign::Positive {
1497                atan_val
1498            } else {
1499                let pi = work.pi::<B>(reborrow_cache(&mut cache)).value();
1500                let rad = ulps::<B>(&pi.repr, work.precision, 8);
1501                let pi_ball = Ball::with_error(pi.into_repr(), rad);
1502                if y.sign() == Sign::Positive {
1503                    atan_val.add(&pi_ball, work.precision)?
1504                } else {
1505                    atan_val.sub(&pi_ball, work.precision)?
1506                }
1507            };
1508            Ok(res.to_value_radius::<R>(&Context::<R>::new(work.precision)))
1509        })
1510    }
1511}
1512
1513impl<R: ErrorBounds, const B: Word> FBig<R, B> {
1514    /// Calculate the sine of the floating point number.
1515    ///
1516    /// # Panics
1517    /// Panics if the input is infinite.
1518    #[inline]
1519    pub fn sin(&self) -> Self {
1520        self.context.unwrap_fp(self.context.sin(&self.repr, None))
1521    }
1522
1523    /// Calculate the cosine of the floating point number.
1524    ///
1525    /// # Panics
1526    /// Panics if the input is infinite.
1527    #[inline]
1528    pub fn cos(&self) -> Self {
1529        self.context.unwrap_fp(self.context.cos(&self.repr, None))
1530    }
1531
1532    /// Calculate both the sine and cosine of the floating point number.
1533    ///
1534    /// This is more efficient than calling `sin` and `cos` separately.
1535    ///
1536    /// # Panics
1537    /// Panics if the input is infinite.
1538    #[inline]
1539    pub fn sin_cos(&self) -> (Self, Self) {
1540        let (s, c) = self.context.sin_cos(&self.repr, None);
1541        (self.context.unwrap_fp(s), self.context.unwrap_fp(c))
1542    }
1543
1544    /// Calculate the tangent of the floating point number.
1545    ///
1546    /// At odd multiples of π/2 the result is an infinity (returned as a value).
1547    ///
1548    /// # Panics
1549    /// Panics if the input is infinite.
1550    #[inline]
1551    pub fn tan(&self) -> Self {
1552        self.context.unwrap_fp(self.context.tan(&self.repr, None))
1553    }
1554
1555    /// Calculate the sine of the floating point number multiplied by π, i.e. `sin(self·π)`.
1556    ///
1557    /// Unlike [`sin`](Self::sin), the ×π variant resolves rational fractions of π exactly:
1558    /// integers map to `±0` (with the sign of `self`) and half-integers to `±1`. The argument
1559    /// reduces exactly in integer arithmetic, so the accuracy is independent of the magnitude
1560    /// of `self`.
1561    ///
1562    /// # Examples
1563    /// ```
1564    /// # use core::str::FromStr;
1565    /// # use dashu_float::DBig;
1566    /// assert_eq!(DBig::from_str("0.5")?.sin_pi(), DBig::from(1));
1567    /// // sin(π·10^100) = 0 *exactly*: huge integer arguments lose nothing
1568    /// assert_eq!(DBig::from_str("1e100")?.sin_pi(), DBig::ZERO);
1569    /// # Ok::<(), dashu_base::ParseError>(())
1570    /// ```
1571    ///
1572    /// # Panics
1573    /// Panics if the input is infinite.
1574    #[inline]
1575    pub fn sin_pi(&self) -> Self {
1576        self.context
1577            .unwrap_fp(self.context.sin_pi(&self.repr, None))
1578    }
1579
1580    /// Calculate the cosine of the floating point number multiplied by π, i.e. `cos(self·π)`.
1581    ///
1582    /// Unlike [`cos`](Self::cos), the ×π variant resolves rational fractions of π exactly:
1583    /// integers map to `±1` and odd half-integers to `+0`. The argument reduces exactly in
1584    /// integer arithmetic, so the accuracy is independent of the magnitude of `self`.
1585    ///
1586    /// # Examples
1587    /// ```
1588    /// # use core::str::FromStr;
1589    /// # use dashu_float::DBig;
1590    /// assert_eq!(DBig::from_str("0.5")?.cos_pi(), DBig::ZERO);
1591    /// assert_eq!(DBig::from_str("3")?.cos_pi(), DBig::from(-1));
1592    /// # Ok::<(), dashu_base::ParseError>(())
1593    /// ```
1594    ///
1595    /// # Panics
1596    /// Panics if the input is infinite.
1597    #[inline]
1598    pub fn cos_pi(&self) -> Self {
1599        self.context
1600            .unwrap_fp(self.context.cos_pi(&self.repr, None))
1601    }
1602
1603    /// Calculate both the sine and cosine of the floating point number multiplied by π.
1604    ///
1605    /// This is more efficient than calling `sin_pi` and `cos_pi` separately. At half-integer
1606    /// arguments both results are exact.
1607    ///
1608    /// # Examples
1609    /// ```
1610    /// # use core::str::FromStr;
1611    /// # use dashu_float::DBig;
1612    /// let (s, c) = DBig::from_str("0.25")?.sin_cos_pi();
1613    /// // sin(π/4) = cos(π/4) = √2/2
1614    /// assert!(s == c);
1615    /// # Ok::<(), dashu_base::ParseError>(())
1616    /// ```
1617    ///
1618    /// # Panics
1619    /// Panics if the input is infinite.
1620    #[inline]
1621    pub fn sin_cos_pi(&self) -> (Self, Self) {
1622        let (s, c) = self.context.sin_cos_pi(&self.repr, None);
1623        (self.context.unwrap_fp(s), self.context.unwrap_fp(c))
1624    }
1625
1626    /// Calculate the tangent of the floating point number multiplied by π, i.e. `tan(self·π)`.
1627    ///
1628    /// Every quarter-integer argument is exact: integers map to `±0` (with the sign of the
1629    /// input, keeping `tan_pi` odd — matching MPFR) and quarter-integers to `±1`. At odd
1630    /// half-integers — the poles — the one-sided limits are `+∞` and `−∞`, so no
1631    /// signed infinity can be certified: the case is indeterminate (like `0/0`) and reported
1632    /// as an error at the context layer. The argument reduces exactly in integer arithmetic,
1633    /// so the accuracy is independent of the magnitude of `self`.
1634    ///
1635    /// # Examples
1636    /// ```
1637    /// # use core::str::FromStr;
1638    /// # use dashu_float::DBig;
1639    /// assert_eq!(DBig::from_str("0.25")?.tan_pi(), DBig::from(1));
1640    /// assert_eq!(DBig::from_str("1.75")?.tan_pi(), DBig::from(-1));
1641    /// # Ok::<(), dashu_base::ParseError>(())
1642    /// ```
1643    ///
1644    /// # Panics
1645    /// Panics if the input is infinite or an odd multiple of `1/2` (a pole, where the sign
1646    /// of the infinity is indeterminate).
1647    #[inline]
1648    pub fn tan_pi(&self) -> Self {
1649        self.context
1650            .unwrap_fp(self.context.tan_pi(&self.repr, None))
1651    }
1652
1653    /// Calculate the sine of `self·2π/u` — the angle `self` measured in units of the full
1654    /// turn divided by `u` (e.g. `u = 360` gives degrees).
1655    ///
1656    /// Unlike [`sin`](Self::sin), the ×u variant has rational special points: the argument
1657    /// reduces *exactly* mod u in integer arithmetic, so the accuracy is independent of the
1658    /// magnitude of `self`, and arguments where `12·self/u` is an integer resolve exactly
1659    /// (e.g. `sin_unit(90, 360)` is exactly `1`).
1660    ///
1661    /// # Examples
1662    /// ```
1663    /// # use core::str::FromStr;
1664    /// # use dashu_float::DBig;
1665    /// let deg = DBig::from_str("90")?;
1666    /// assert_eq!(deg.sin_unit(360), DBig::from(1));
1667    /// let thirty = DBig::from_str("30")?;
1668    /// assert_eq!(thirty.sin_unit(360), DBig::from_str("0.5")?);
1669    /// # Ok::<(), dashu_base::ParseError>(())
1670    /// ```
1671    ///
1672    /// # Panics
1673    /// Panics if the input is infinite or `u` is zero.
1674    #[inline]
1675    pub fn sin_unit(&self, u: usize) -> Self {
1676        self.context
1677            .unwrap_fp(self.context.sin_unit(&self.repr, u, None))
1678    }
1679
1680    /// Calculate the cosine of `self·2π/u` (see [`sin_unit`](Self::sin_unit) for the ×u
1681    /// semantics).
1682    ///
1683    /// # Examples
1684    /// ```
1685    /// # use core::str::FromStr;
1686    /// # use dashu_float::DBig;
1687    /// let deg = DBig::from_str("60")?;
1688    /// assert_eq!(deg.cos_unit(360), DBig::from_str("0.5")?);
1689    /// # Ok::<(), dashu_base::ParseError>(())
1690    /// ```
1691    ///
1692    /// # Panics
1693    /// Panics if the input is infinite or `u` is zero.
1694    #[inline]
1695    pub fn cos_unit(&self, u: usize) -> Self {
1696        self.context
1697            .unwrap_fp(self.context.cos_unit(&self.repr, u, None))
1698    }
1699
1700    /// Calculate both the sine and cosine of `self·2π/u` (see [`sin_unit`](Self::sin_unit)).
1701    ///
1702    /// This is more efficient than calling [`sin_unit`](Self::sin_unit) and
1703    /// [`cos_unit`](Self::cos_unit) separately, except at the sixth-type exact points where
1704    /// exactly one of the two results is exact.
1705    ///
1706    /// # Examples
1707    /// ```
1708    /// # use core::str::FromStr;
1709    /// # use dashu_float::DBig;
1710    /// let deg = DBig::from_str("30")?;
1711    /// let (s, c) = deg.sin_cos_unit(360);
1712    /// assert_eq!(s, DBig::from_str("0.5")?);
1713    /// assert_eq!(c, DBig::from_str("0.86602540378443864676")?.with_precision(deg.precision()).value());
1714    /// # Ok::<(), dashu_base::ParseError>(())
1715    /// ```
1716    ///
1717    /// # Panics
1718    /// Panics if the input is infinite or `u` is zero.
1719    #[inline]
1720    pub fn sin_cos_unit(&self, u: usize) -> (Self, Self) {
1721        let (s, c) = self.context.sin_cos_unit(&self.repr, u, None);
1722        (self.context.unwrap_fp(s), self.context.unwrap_fp(c))
1723    }
1724
1725    /// Calculate the tangent of `self·2π/u` (see [`sin_unit`](Self::sin_unit) for the ×u
1726    /// semantics).
1727    ///
1728    /// Every eighth-of-turn argument (`8·self/u` an integer) is exact; at the odd multiples
1729    /// of `u/4` — the poles — the one-sided limits are `+∞` and `−∞`: the case is
1730    /// indeterminate and reported as an error at the context layer.
1731    ///
1732    /// # Examples
1733    /// ```
1734    /// # use core::str::FromStr;
1735    /// # use dashu_float::DBig;
1736    /// let deg = DBig::from_str("45")?;
1737    /// assert_eq!(deg.tan_unit(360), DBig::from(1));
1738    /// # Ok::<(), dashu_base::ParseError>(())
1739    /// ```
1740    ///
1741    /// # Panics
1742    /// Panics if the input is infinite, `u` is zero, or `self` is an odd multiple of `u/4`
1743    /// (a pole).
1744    #[inline]
1745    pub fn tan_unit(&self, u: usize) -> Self {
1746        self.context
1747            .unwrap_fp(self.context.tan_unit(&self.repr, u, None))
1748    }
1749
1750    /// Calculate `asin(self)·u/(2π)` — the arc sine in units of the full turn divided by `u`
1751    /// (the inverse of [`sin_unit`](Self::sin_unit)).
1752    ///
1753    /// # Examples
1754    /// ```
1755    /// # use core::str::FromStr;
1756    /// # use dashu_float::DBig;
1757    /// let half = DBig::from_str("0.5")?;
1758    /// assert_eq!(half.asin_unit(360), DBig::from(30)); // asin(1/2) = 30°
1759    /// # Ok::<(), dashu_base::ParseError>(())
1760    /// ```
1761    ///
1762    /// # Panics
1763    /// Panics if the input is infinite or `|self| > 1` (out of domain).
1764    #[inline]
1765    pub fn asin_unit(&self, u: usize) -> Self {
1766        self.context
1767            .unwrap_fp(self.context.asin_unit(&self.repr, u, None))
1768    }
1769
1770    /// Calculate `acos(self)·u/(2π)` — the arc cosine in units of the full turn divided by
1771    /// `u` (the inverse of [`cos_unit`](Self::cos_unit)).
1772    ///
1773    /// # Examples
1774    /// ```
1775    /// # use core::str::FromStr;
1776    /// # use dashu_float::DBig;
1777    /// let half = DBig::from_str("0.5")?;
1778    /// assert_eq!(half.acos_unit(360), DBig::from(60)); // acos(1/2) = 60°
1779    /// # Ok::<(), dashu_base::ParseError>(())
1780    /// ```
1781    ///
1782    /// # Panics
1783    /// Panics if the input is infinite or `|self| > 1` (out of domain).
1784    #[inline]
1785    pub fn acos_unit(&self, u: usize) -> Self {
1786        self.context
1787            .unwrap_fp(self.context.acos_unit(&self.repr, u, None))
1788    }
1789
1790    /// Calculate `atan(self)·u/(2π)` — the arc tangent in units of the full turn divided by
1791    /// `u` (the inverse of [`tan_unit`](Self::tan_unit)). `atan_unit(±inf) = ±u/4`.
1792    ///
1793    /// # Examples
1794    /// ```
1795    /// # use core::str::FromStr;
1796    /// # use dashu_float::DBig;
1797    /// let one = DBig::from_str("1.0")?; // two digits: the result 45 needs precision ≥ 2
1798    /// assert_eq!(one.atan_unit(360), DBig::from(45)); // atan(1) = 45°
1799    /// # Ok::<(), dashu_base::ParseError>(())
1800    /// ```
1801    #[inline]
1802    pub fn atan_unit(&self, u: usize) -> Self {
1803        self.context
1804            .unwrap_fp(self.context.atan_unit(&self.repr, u, None))
1805    }
1806
1807    /// Calculate `atan2(self, x)·u/(2π)` — the four-quadrant arc tangent in units of the
1808    /// full turn divided by `u`. Follows the same C99 signed-zero model as
1809    /// [`atan2`](Self::atan2). The receiver is the `y`-coordinate, the first argument the
1810    /// `x`-coordinate: `(1).atan2_unit(&x, 360)`.
1811    ///
1812    /// # Examples
1813    /// ```
1814    /// # use core::str::FromStr;
1815    /// # use dashu_float::DBig;
1816    /// // atan2(1, 1) = 45° — the receiver is y, the argument x
1817    /// let y = DBig::from_str("1.0")?; // two digits: the result 45 needs precision ≥ 2
1818    /// let x = DBig::from_str("1.0")?;
1819    /// assert_eq!(y.atan2_unit(&x, 360), DBig::from(45));
1820    /// # Ok::<(), dashu_base::ParseError>(())
1821    /// ```
1822    ///
1823    /// # Panics
1824    /// Panics if both arguments are zero.
1825    #[inline]
1826    pub fn atan2_unit(&self, x: &Self, u: usize) -> Self {
1827        self.context
1828            .unwrap_fp(self.context.atan2_unit(&self.repr, &x.repr, u, None))
1829    }
1830
1831    /// Calculate the arcsine of the floating point number.
1832    ///
1833    /// # Panics
1834    /// Panics if the input is infinite or `|self| > 1` (out of domain).
1835    #[inline]
1836    pub fn asin(&self) -> Self {
1837        self.context.unwrap_fp(self.context.asin(&self.repr, None))
1838    }
1839
1840    /// Calculate the arccosine of the floating point number.
1841    ///
1842    /// # Panics
1843    /// Panics if the input is infinite or `|self| > 1` (out of domain).
1844    #[inline]
1845    pub fn acos(&self) -> Self {
1846        self.context.unwrap_fp(self.context.acos(&self.repr, None))
1847    }
1848
1849    /// Calculate the arctangent of the floating point number. `atan(±inf) = ±π/2`.
1850    #[inline]
1851    pub fn atan(&self) -> Self {
1852        self.context.unwrap_fp(self.context.atan(&self.repr, None))
1853    }
1854
1855    /// Calculate the arctangent of `self / x`.
1856    ///
1857    /// # Panics
1858    /// Panics if both arguments are zero.
1859    #[inline]
1860    pub fn atan2(&self, x: &Self) -> Self {
1861        self.context
1862            .unwrap_fp(self.context.atan2(&self.repr, &x.repr, None))
1863    }
1864}
1865
1866impl<R: Round> Context<R> {
1867    /// Calculate π using the Chudnovsky algorithm with binary splitting.
1868    ///
1869    /// The Chudnovsky algorithm is one of the most efficient methods for
1870    /// high-precision π calculation, providing ~14.18 decimal digits per term.
1871    ///
1872    /// # Methodology
1873    /// We use Binary Splitting to evaluate the series. This technique transforms
1874    /// the linear-time summation into a recursive tree evaluation. By combining
1875    /// terms into large products, it allows the library to leverage fast
1876    /// multiplication algorithms (like Toom-3 or FFT) as the numbers grow,
1877    /// leading to significant performance gains over simple iterative summation.
1878    #[must_use]
1879    pub fn pi<const B: Word>(&self, cache: Option<&mut ConstCache>) -> Rounded<FBig<R, B>> {
1880        if let Some(c) = cache {
1881            return c.pi::<B, R>(self.precision);
1882        }
1883
1884        // No shared cache: compute via a one-shot ConstCache so the Chudnovsky series
1885        // and the 426880·√10005·Q/T finalization live in exactly one place (see
1886        // ConstCache::pi), instead of being duplicated here.
1887        let mut fresh = ConstCache::new();
1888        fresh.pi::<B, R>(self.precision)
1889    }
1890
1891    /// Calculate *e* (Euler's number) by binary splitting on `e = Σ 1/k!`.
1892    ///
1893    /// Unlike [`pi`](Self::pi), this takes no constant cache: *e* depends on no
1894    /// other cached constant and is itself reused by no operation, so there is no
1895    /// state worth sharing across calls. The factorial series is the optimal
1896    /// algorithm for *e* (`O(M(n) log n)`, faster than π) and avoids the
1897    /// argument-reduction and `√p`-fold powering that `exp(1)` would pay for.
1898    ///
1899    /// # Panics
1900    ///
1901    /// Panics if the context precision is 0.
1902    #[must_use]
1903    pub fn e<const B: Word>(&self) -> Rounded<FBig<R, B>> {
1904        compute_e::<B, R>(self.precision)
1905    }
1906}
1907
1908impl<R: Round, const B: Word> FBig<R, B> {
1909    /// Calculate π with the given precision and the default rounding mode.
1910    #[inline]
1911    #[must_use]
1912    pub fn pi(precision: usize) -> Self {
1913        Context::<R>::new(precision).pi(None).value()
1914    }
1915
1916    /// Calculate *e* (Euler's number) with the given precision and the default
1917    /// rounding mode.
1918    #[inline]
1919    #[must_use]
1920    pub fn e(precision: usize) -> Self {
1921        Context::<R>::new(precision).e::<B>().value()
1922    }
1923}
1924
1925#[cfg(test)]
1926mod tests {
1927    use super::*;
1928    use crate::round::mode;
1929    use crate::DBig;
1930    use alloc::format;
1931    use core::str::FromStr;
1932
1933    #[test]
1934    fn test_atan_infinity_is_preserved() {
1935        let ctx = Context::<mode::HalfEven>::new(53);
1936        // atan(±inf) = ±π/2 — a finite result, preserved (not an error)
1937        let r = ctx.atan::<2>(&Repr::<2>::infinity(), None).unwrap().value();
1938        assert!(r.repr().sign() == Sign::Positive);
1939        // it should be approximately π/2
1940        assert!(r > FBig::<mode::HalfEven>::ONE);
1941    }
1942
1943    /// Regression: a tiny *negative* argument used to panic in `reduce_to_quadrant`.
1944    /// `round()` of a value in (-1, 0) yields signed zero (exponent sentinel -1),
1945    /// which `IBig::try_from` now accepts as plain 0.
1946    #[test]
1947    fn test_trig_tiny_negative_no_panic() {
1948        let ctx = Context::<mode::HalfAway>::new(30);
1949        for &e in &[-1isize, -2, -10, -30] {
1950            // x = -1 * BASE^e, a tiny negative value
1951            let x = Repr::<10>::new(IBig::from(-1), e);
1952            let s = ctx.sin::<10>(&x, None).unwrap().value();
1953            let c = ctx.cos::<10>(&x, None).unwrap().value();
1954            let (ss, cc) = ctx.sin_cos::<10>(&x, None);
1955            let ss = ss.unwrap().value();
1956            let cc = cc.unwrap().value();
1957            // sin is odd, cos is even: sin(x) ≈ x (negative), cos(x) ≈ 1
1958            assert_eq!(s.sign(), Sign::Negative);
1959            assert_eq!(c.sign(), Sign::Positive);
1960            assert_eq!(ss.sign(), Sign::Negative);
1961            assert_eq!(cc.sign(), Sign::Positive);
1962        }
1963    }
1964
1965    /// Regression: a 49-digit significand at precision 100 used to assertion-fail in `Context::sin`'s
1966    /// rounding logic (found during fuzzing). Promoted here from the excluded `fuzz/` crate so it runs
1967    /// in CI; rewritten to the current `Context::sin` API.
1968    #[test]
1969    fn test_sin_many_digit_rounding_no_panic() {
1970        let x = DBig::from_str("-5.525474318981006776603409487767135633516667011547942409467e-3")
1971            .unwrap();
1972        let ctx = Context::<mode::HalfEven>::new(100);
1973        let s = ctx.sin::<10>(x.repr(), None).unwrap().value();
1974        // sin(x) ≈ x for a small negative x — completing without panicking is the regression guard.
1975        assert_eq!(s.sign(), Sign::Negative);
1976    }
1977
1978    /// tan near a pole (π/2) must not panic, and its sign must follow the pole side: just below →
1979    /// large positive (→ +∞), just above → large negative (→ −∞). Guards the pole check, which
1980    /// tests `cos` with `significand.is_zero()` (not `is_pos_zero`, which would miss `-0`) and
1981    /// assigns the infinity sign as `sign(sin)·sign(cos)`.
1982    #[test]
1983    fn test_tan_near_pole_signs_and_no_panic() {
1984        let p = 53usize;
1985        let ctx = Context::<mode::HalfEven>::new(p);
1986        let half_pi = FBig::<mode::HalfEven>::pi(p) / 2u8;
1987        // a clear offset either side of the pole (≈2⁻¹⁰, far larger than half_pi's rounding error)
1988        let eps = FBig::<mode::HalfEven>::ONE >> 10;
1989        let below = ctx
1990            .tan::<2>((half_pi.clone() - &eps).repr(), None)
1991            .unwrap()
1992            .value();
1993        let above = ctx
1994            .tan::<2>((half_pi.clone() + &eps).repr(), None)
1995            .unwrap()
1996            .value();
1997        assert_eq!(below.sign(), Sign::Positive, "tan just below π/2 is large positive");
1998        assert_eq!(above.sign(), Sign::Negative, "tan just above π/2 is large negative");
1999        // sanity: tan(π/4) = 1
2000        let pi = FBig::<mode::HalfEven>::pi(p);
2001        let q = ctx.tan::<2>((pi / 4u8).repr(), None).unwrap().value();
2002        assert!(
2003            (q.clone() - FBig::ONE).abs_cmp(&(FBig::ONE >> 40)).is_le(),
2004            "tan(π/4) ≈ 1, got {q:?}"
2005        );
2006    }
2007
2008    /// Re-round a high-precision `HalfEven` oracle to precision `p` under the mode under test.
2009    /// The +60 guard bits put the oracle far from any rounding boundary, so the re-round is
2010    /// the correctly rounded value for the fixed inputs used here.
2011    fn reround<R: Round, const B: Word>(hi: &FBig<mode::HalfEven, B>, p: usize) -> FBig<R, B> {
2012        let ctx = Context::<R>::new(p);
2013        FBig::new(ctx.repr_round_ref(hi.repr()).value(), ctx)
2014    }
2015
2016    /// Assert the value is a zero and report its sign; asserts non-zero-ness by construction.
2017    fn zero_sign<R: Round, const B: Word>(v: &FBig<R, B>) -> bool {
2018        assert!(v.repr().significand.is_zero());
2019        v.repr().is_neg_zero()
2020    }
2021
2022    /// Assert the value is ±1 and return it as a sign-carrying integer.
2023    fn plus_minus_one<R: Round, const B: Word>(v: &FBig<R, B>) -> IBig {
2024        let i = IBig::try_from(v.clone()).unwrap();
2025        assert!(i == IBig::from(1) || i == IBig::from(-1));
2026        i
2027    }
2028
2029    /// The ×π exact-case table: quarter-integer arguments resolve exactly at every precision
2030    /// and rounding mode (they bypass the Ziv loop, whose containment test cannot certify an
2031    /// exact 0/±1 under directed rounding).
2032    #[test]
2033    fn test_pi_family_exact_cases() {
2034        fn check<R: ErrorBounds>(mode_ctx: &Context<R>, p: usize) {
2035            // sin_pi: integers → ±0 with the sign of x; odd half-integers → ±1
2036            for (input, neg_zero, expect_one) in [
2037                ("0", Some(false), None),
2038                ("1", Some(false), None),
2039                ("-1", Some(true), None),
2040                ("2", Some(false), None),
2041                ("-3", Some(true), None),
2042                ("0.5", None, Some(true)),
2043                ("-0.5", None, Some(false)),
2044                ("1.5", None, Some(false)),
2045                ("2.5", None, Some(true)),
2046            ] {
2047                let x = DBig::from_str(input).unwrap();
2048                let s = mode_ctx.sin_pi::<10>(x.repr(), None).unwrap().value();
2049                match (neg_zero, expect_one) {
2050                    (Some(expected), _) => {
2051                        assert_eq!(zero_sign(&s), expected, "sin_pi({input}) sign of zero, p={p}")
2052                    }
2053                    (_, Some(positive)) => assert_eq!(
2054                        plus_minus_one(&s),
2055                        if positive {
2056                            IBig::from(1)
2057                        } else {
2058                            IBig::from(-1)
2059                        },
2060                        "sin_pi({input}) magnitude, p={p}"
2061                    ),
2062                    _ => panic!("test table row misconfigured"),
2063                }
2064            }
2065
2066            // cos_pi: integers → ±1 by parity; odd half-integers → +0
2067            for (input, neg_one, is_zero) in [
2068                ("0", false, false),
2069                ("1", true, false),
2070                ("2", false, false),
2071                ("3", true, false),
2072                ("0.5", false, true),
2073                ("1.5", false, true),
2074                ("-0.5", false, true),
2075                ("2.5", false, true),
2076            ] {
2077                let x = DBig::from_str(input).unwrap();
2078                let c = mode_ctx.cos_pi::<10>(x.repr(), None).unwrap().value();
2079                if is_zero {
2080                    assert!(!zero_sign(&c), "cos_pi({input}) is +0, p={p}");
2081                } else {
2082                    let expect = if neg_one {
2083                        IBig::from(-1)
2084                    } else {
2085                        IBig::from(1)
2086                    };
2087                    assert_eq!(plus_minus_one(&c), expect, "cos_pi({input}), p={p}");
2088                }
2089            }
2090
2091            // tan_pi: every integer zero carries the sign of x (tan is odd, so
2092            // `tan_pi(−n) = −tan_pi(n)` must hold on the zeros too, matching MPFR);
2093            // quarter-integers → ±1; odd half-integers are indeterminate poles
2094            for (input, neg_zero, expect_one) in [
2095                ("0", Some(false), None),
2096                ("1", Some(false), None),
2097                ("-1", Some(true), None),
2098                ("2", Some(false), None),
2099                ("-3", Some(true), None),
2100                ("0.25", None, Some(true)),
2101                ("0.75", None, Some(false)),
2102                ("1.25", None, Some(true)),
2103                ("1.75", None, Some(false)),
2104                ("-0.25", None, Some(false)),
2105                ("-0.75", None, Some(true)),
2106            ] {
2107                let x = DBig::from_str(input).unwrap();
2108                let t = mode_ctx.tan_pi::<10>(x.repr(), None).unwrap().value();
2109                match (neg_zero, expect_one) {
2110                    (Some(expected), _) => {
2111                        assert_eq!(zero_sign(&t), expected, "tan_pi({input}), p={p}")
2112                    }
2113                    (_, Some(positive)) => assert_eq!(
2114                        plus_minus_one(&t),
2115                        if positive {
2116                            IBig::from(1)
2117                        } else {
2118                            IBig::from(-1)
2119                        },
2120                        "tan_pi({input}), p={p}"
2121                    ),
2122                    _ => panic!("test table row misconfigured"),
2123                }
2124            }
2125            for input in ["0.5", "1.5", "-0.5", "2.5"] {
2126                let x = DBig::from_str(input).unwrap();
2127                assert!(
2128                    matches!(mode_ctx.tan_pi::<10>(x.repr(), None), Err(FpError::Indeterminate)),
2129                    "tan_pi({input}) is an indeterminate pole, p={p}"
2130                );
2131            }
2132
2133            // sin_cos_pi agrees with the two tables at half-integers
2134            let x = DBig::from_str("-0.5").unwrap();
2135            let (s, c) = mode_ctx.sin_cos_pi::<10>(x.repr(), None);
2136            assert_eq!(plus_minus_one(&s.unwrap().value()), IBig::from(-1));
2137            assert!(!zero_sign(&c.unwrap().value()));
2138
2139            // the string parser collapses "-0" to +0, so probe the −0 sentinel directly
2140            let s = mode_ctx
2141                .sin_pi::<10>(&Repr::neg_zero(), None)
2142                .unwrap()
2143                .value();
2144            assert!(zero_sign(&s), "sin_pi(-0) is -0");
2145            let t = mode_ctx
2146                .tan_pi::<10>(&Repr::neg_zero(), None)
2147                .unwrap()
2148                .value();
2149            assert!(zero_sign(&t), "tan_pi(-0) is -0");
2150            let (s, _) = mode_ctx.sin_cos_pi::<10>(&Repr::neg_zero(), None);
2151            assert!(zero_sign(&s.unwrap().value()), "sin_cos_pi(-0).0 is -0");
2152        }
2153
2154        for &p in &[20usize, 50, 100, 500] {
2155            check(&Context::<mode::HalfEven>::new(p), p);
2156            check(&Context::<mode::Down>::new(p), p);
2157            check(&Context::<mode::Up>::new(p), p);
2158        }
2159    }
2160
2161    /// The ×π general path vs the high-precision oracle (p + 60 under `HalfEven`, re-rounded
2162    /// under the mode under test), across the four canonical significand widths. The inputs
2163    /// cover every reduction branch: tiny arguments (k = 0 product path), the rational split,
2164    /// the quarter-integer ties (odd `n`, so `sin_pi`/`cos_pi` still take the general path),
2165    /// large integer parts, and negative arguments.
2166    #[test]
2167    fn test_pi_family_oracle() {
2168        let inputs = [
2169            "0.1",
2170            "0.2",
2171            "0.3",
2172            "0.4",
2173            "0.6",
2174            "0.7",
2175            "0.8",
2176            "0.9",
2177            "1.1",
2178            "1.2",
2179            "2.3",
2180            "2.6",
2181            "-0.3",
2182            "-1.2",
2183            "-2.6",
2184            "10.1",
2185            "123.456",
2186            "12345678901234567890.125",
2187        ];
2188        for &p in &[20usize, 50, 100, 500] {
2189            for input in inputs {
2190                let x = DBig::from_str(input).unwrap();
2191                let hi_ctx = Context::<mode::HalfEven>::new(p + 60);
2192                let hi_s = hi_ctx.sin_pi::<10>(x.repr(), None).unwrap().value();
2193                let hi_c = hi_ctx.cos_pi::<10>(x.repr(), None).unwrap().value();
2194                let hi_t = hi_ctx.tan_pi::<10>(x.repr(), None).unwrap().value();
2195
2196                // sin under HalfEven
2197                let got = Context::<mode::HalfEven>::new(p)
2198                    .sin_pi::<10>(x.repr(), None)
2199                    .unwrap()
2200                    .value();
2201                assert_eq!(
2202                    got,
2203                    reround::<mode::HalfEven, 10>(&hi_s, p),
2204                    "sin_pi({input}) at p={p}"
2205                );
2206                // cos under Down
2207                let got = Context::<mode::Down>::new(p)
2208                    .cos_pi::<10>(x.repr(), None)
2209                    .unwrap()
2210                    .value();
2211                assert_eq!(
2212                    got,
2213                    reround::<mode::Down, 10>(&hi_c, p),
2214                    "cos_pi({input}) at p={p} under Down"
2215                );
2216                // tan under Up
2217                let got = Context::<mode::Up>::new(p)
2218                    .tan_pi::<10>(x.repr(), None)
2219                    .unwrap()
2220                    .value();
2221                assert_eq!(
2222                    got,
2223                    reround::<mode::Up, 10>(&hi_t, p),
2224                    "tan_pi({input}) at p={p} under Up"
2225                );
2226                // sin_cos under HalfEven agrees with the separate evaluations
2227                let (s, c) = Context::<mode::HalfEven>::new(p).sin_cos_pi::<10>(x.repr(), None);
2228                assert_eq!(
2229                    s.unwrap().value(),
2230                    reround::<mode::HalfEven, 10>(&hi_s, p),
2231                    "sin_cos_pi sin({input}) at p={p}"
2232                );
2233                assert_eq!(
2234                    c.unwrap().value(),
2235                    reround::<mode::HalfEven, 10>(&hi_c, p),
2236                    "sin_cos_pi cos({input}) at p={p}"
2237                );
2238            }
2239        }
2240    }
2241
2242    /// The exact reduction mod 2 holds for arbitrarily large arguments: a huge integer part
2243    /// plus a `2^-10` residue must give exactly the same result as the bare residue (the
2244    /// radian `sin` would need ~1000 extra guard digits here), and huge integers hit the
2245    /// exact-case table through the no-materialization path.
2246    #[test]
2247    fn test_sin_pi_huge_exact_reduction() {
2248        for &p in &[20usize, 50, 100, 500] {
2249            let ctx = Context::<mode::HalfEven>::new(p);
2250
2251            // x = ((2^60 + 1)·2^950 + 1)·2^-10 ≡ 2^-10 (mod 2)
2252            let sig = (((IBig::from(1) << 60usize) + 1) << 950usize) + 1;
2253            let x = Repr::<2>::new(sig, -10);
2254            let residue = Repr::<2>::new(IBig::from(1), -10);
2255            assert_eq!(
2256                ctx.sin_pi::<2>(&x, None).unwrap().value(),
2257                ctx.sin_pi::<2>(&residue, None).unwrap().value(),
2258                "sin_pi(huge) == sin_pi(2^-10) at p={p}"
2259            );
2260            assert_eq!(
2261                ctx.cos_pi::<2>(&x, None).unwrap().value(),
2262                ctx.cos_pi::<2>(&residue, None).unwrap().value(),
2263                "cos_pi(huge) == cos_pi(2^-10) at p={p}"
2264            );
2265
2266            // an even integer at a huge exponent: quarter_class must not materialize 2^940
2267            let huge_int = Repr::<2>::new((IBig::from(1) << 60usize) + 1, 940);
2268            assert_eq!(
2269                ctx.sin_pi::<2>(&huge_int, None).unwrap().value(),
2270                FBig::<mode::HalfEven>::ZERO
2271            );
2272            assert_eq!(
2273                ctx.cos_pi::<2>(&huge_int, None).unwrap().value(),
2274                FBig::<mode::HalfEven>::ONE
2275            );
2276            // ...and an odd one at a small exponent: cos_pi(2^60+1) = -1 (odd integer)
2277            assert_eq!(
2278                ctx.cos_pi::<2>(&Repr::<2>::new((IBig::from(1) << 60usize) + 1, 0), None)
2279                    .unwrap()
2280                    .value(),
2281                FBig::<mode::HalfEven>::NEG_ONE
2282            );
2283        }
2284    }
2285
2286    /// An astronomically-scaled tiny argument (`|x| < 1/4` with s ~ 10⁹) must take the
2287    /// k = 0 fast path — materializing `B^s` here would allocate ~125 MB — and still round
2288    /// correctly against the oracle.
2289    // The 10⁹-scale exponent needs the 64-bit `isize` range; on 32-bit targets the underflow
2290    // guard fires first (its range is ~4000× smaller), so the test is 64-bit-only.
2291    #[cfg(target_pointer_width = "64")]
2292    #[test]
2293    fn test_sin_pi_tiny_no_materialization() {
2294        let x = Repr::<2>::new(IBig::from(3), -1_000_000_000);
2295        for &p in &[20usize, 50, 100, 500] {
2296            let ctx = Context::<mode::HalfEven>::new(p);
2297            let s = ctx.sin_pi::<2>(&x, None).unwrap().value();
2298            assert_eq!(s.sign(), Sign::Positive);
2299
2300            let hi = Context::<mode::HalfEven>::new(p + 60)
2301                .sin_pi::<2>(&x, None)
2302                .unwrap()
2303                .value();
2304            assert_eq!(s, reround::<mode::HalfEven, 2>(&hi, p));
2305        }
2306    }
2307
2308    /// `tan_pi` just off a pole: large positive below, large negative above (the poles
2309    /// themselves are `Indeterminate`).
2310    #[test]
2311    fn test_tan_pi_near_pole_signs() {
2312        for &p in &[20usize, 50, 100, 500] {
2313            let ctx = Context::<mode::HalfEven>::new(p);
2314            let eps = FBig::<mode::HalfEven>::ONE >> 10;
2315            // both operands must carry the context precision: at precision 1 the subtraction
2316            // 0.5 − 2^-10 rounds back onto the exact pole
2317            let half: FBig<mode::HalfEven> = FBig::ONE.with_precision(p).value() / 2u8;
2318            let eps: FBig<mode::HalfEven> = eps.with_precision(p).value();
2319            let below = ctx
2320                .tan_pi::<2>((half.clone() - &eps).repr(), None)
2321                .unwrap()
2322                .value();
2323            let above = ctx
2324                .tan_pi::<2>((half.clone() + &eps).repr(), None)
2325                .unwrap()
2326                .value();
2327            assert_eq!(below.sign(), Sign::Positive, "tan_pi just below 1/2 at p={p}");
2328            assert_eq!(above.sign(), Sign::Negative, "tan_pi just above 1/2 at p={p}");
2329        }
2330    }
2331
2332    /// The ×u forward family's exact-value tables, in degrees (u = 360 — quarters, sixths and
2333    /// the tangent eighths all appear) and at u ∈ {1, 4, 12}, across the canonical precision
2334    /// sweep and rounding modes.
2335    #[test]
2336    fn test_unit_family_exact_tables() {
2337        fn check<R: ErrorBounds>(mode_ctx: &Context<R>) {
2338            let x = |v: &str| DBig::from_str(v).unwrap();
2339
2340            // degrees: quarters
2341            assert_eq!(
2342                mode_ctx
2343                    .sin_unit::<10>(x("90").repr(), 360, None)
2344                    .unwrap()
2345                    .value(),
2346                DBig::from(1)
2347            );
2348            assert_eq!(
2349                mode_ctx
2350                    .sin_unit::<10>(x("270").repr(), 360, None)
2351                    .unwrap()
2352                    .value(),
2353                -DBig::from(1)
2354            );
2355            let zero = mode_ctx
2356                .sin_unit::<10>(x("180").repr(), 360, None)
2357                .unwrap()
2358                .value();
2359            assert!(zero.repr().significand.is_zero() && !zero.repr().is_neg_zero());
2360            assert_eq!(
2361                mode_ctx
2362                    .cos_unit::<10>(x("90").repr(), 360, None)
2363                    .unwrap()
2364                    .value(),
2365                DBig::ZERO
2366            );
2367            assert_eq!(
2368                mode_ctx
2369                    .cos_unit::<10>(x("180").repr(), 360, None)
2370                    .unwrap()
2371                    .value(),
2372                -DBig::from(1)
2373            );
2374            // degrees: sixths
2375            assert_eq!(
2376                mode_ctx
2377                    .sin_unit::<10>(x("30").repr(), 360, None)
2378                    .unwrap()
2379                    .value(),
2380                DBig::from_str("0.5").unwrap()
2381            );
2382            assert_eq!(
2383                mode_ctx
2384                    .sin_unit::<10>(x("210").repr(), 360, None)
2385                    .unwrap()
2386                    .value(),
2387                DBig::from_str("-0.5").unwrap()
2388            );
2389            assert_eq!(
2390                mode_ctx
2391                    .cos_unit::<10>(x("60").repr(), 360, None)
2392                    .unwrap()
2393                    .value(),
2394                DBig::from_str("0.5").unwrap()
2395            );
2396            assert_eq!(
2397                mode_ctx
2398                    .cos_unit::<10>(x("120").repr(), 360, None)
2399                    .unwrap()
2400                    .value(),
2401                DBig::from_str("-0.5").unwrap()
2402            );
2403            // degrees: tangent eighths and poles
2404            assert_eq!(
2405                mode_ctx
2406                    .tan_unit::<10>(x("45").repr(), 360, None)
2407                    .unwrap()
2408                    .value(),
2409                DBig::from(1)
2410            );
2411            assert_eq!(
2412                mode_ctx
2413                    .tan_unit::<10>(x("135").repr(), 360, None)
2414                    .unwrap()
2415                    .value(),
2416                -DBig::from(1)
2417            );
2418            let t180 = mode_ctx
2419                .tan_unit::<10>(x("180").repr(), 360, None)
2420                .unwrap()
2421                .value();
2422            assert!(t180.repr().is_pos_zero(), "tan_unit(180°) is +0 (sign of x)");
2423            let t_180 = mode_ctx
2424                .tan_unit::<10>(x("-180").repr(), 360, None)
2425                .unwrap()
2426                .value();
2427            assert!(t_180.repr().is_neg_zero(), "tan_unit(−180°) is −0 (sign of x)");
2428            for pole in ["90", "270"] {
2429                assert!(matches!(
2430                    mode_ctx.tan_unit::<10>(x(pole).repr(), 360, None),
2431                    Err(FpError::Indeterminate)
2432                ));
2433            }
2434            // u = 1: sin_unit(x, 1) = sin(2πx), same table shape in u/4 = 0.25 steps
2435            assert_eq!(
2436                mode_ctx
2437                    .sin_unit::<10>(x("0.25").repr(), 1, None)
2438                    .unwrap()
2439                    .value(),
2440                DBig::from(1)
2441            );
2442            assert_eq!(
2443                mode_ctx
2444                    .sin_unit::<10>(x("0.50").repr(), 1, None)
2445                    .unwrap()
2446                    .value(),
2447                DBig::ZERO
2448            );
2449            // u = 12: x = 1 = u/12 (sine sixth), x = 2 = u/6 (cosine sixth), x = 3 = u/4
2450            assert_eq!(
2451                mode_ctx
2452                    .sin_unit::<10>(x("1").repr(), 12, None)
2453                    .unwrap()
2454                    .value(),
2455                DBig::from_str("0.5").unwrap()
2456            );
2457            assert_eq!(
2458                mode_ctx
2459                    .cos_unit::<10>(x("2").repr(), 12, None)
2460                    .unwrap()
2461                    .value(),
2462                DBig::from_str("0.5").unwrap() // cos(π/3) = +1/2
2463            );
2464            assert_eq!(
2465                mode_ctx
2466                    .cos_unit::<10>(x("3").repr(), 12, None)
2467                    .unwrap()
2468                    .value(),
2469                DBig::ZERO
2470            );
2471            // sin_cos_unit agrees with the separate evaluations, including at the
2472            // sixth-type points (one exact, one general)
2473            for deg in ["30", "45", "60", "90", "100", "210"] {
2474                let xd = x(deg);
2475                let (s, c) = mode_ctx.sin_cos_unit::<10>(xd.repr(), 360, None);
2476                assert_eq!(
2477                    s.unwrap().value(),
2478                    mode_ctx
2479                        .sin_unit::<10>(xd.repr(), 360, None)
2480                        .unwrap()
2481                        .value(),
2482                    "sin_cos_unit sin {deg}°"
2483                );
2484                assert_eq!(
2485                    c.unwrap().value(),
2486                    mode_ctx
2487                        .cos_unit::<10>(xd.repr(), 360, None)
2488                        .unwrap()
2489                        .value(),
2490                    "sin_cos_unit cos {deg}°"
2491                );
2492            }
2493            // u = 0 is out of domain for the forward family
2494            let r1 = mode_ctx.sin_unit::<10>(x("1").repr(), 0, None);
2495            let r2 = mode_ctx.cos_unit::<10>(x("1").repr(), 0, None);
2496            let r3 = mode_ctx.tan_unit::<10>(x("1").repr(), 0, None);
2497            let (r4s, r4c) = mode_ctx.sin_cos_unit::<10>(x("1").repr(), 0, None);
2498            for (i, r) in [r1, r2, r3, r4s, r4c].into_iter().enumerate() {
2499                assert!(
2500                    matches!(r.map(|_| ()), Err(FpError::OutOfDomain)),
2501                    "u = 0 is out of domain (slot {i})"
2502                );
2503            }
2504        }
2505
2506        for &p in &[20usize, 50, 100, 500] {
2507            check(&Context::<mode::HalfEven>::new(p));
2508            check(&Context::<mode::Down>::new(p));
2509            check(&Context::<mode::Up>::new(p));
2510        }
2511    }
2512
2513    /// The ×π family is exactly the ×u family at u = 2: every value agrees.
2514    #[test]
2515    fn test_pi_family_equals_unit_family() {
2516        for input in [
2517            "0.1", "0.25", "0.3", "0.5", "1.2", "2.5", "-0.75", "10.1", "1e50",
2518        ] {
2519            let x = DBig::from_str(input).unwrap();
2520            for &p in &[20usize, 50, 100, 500] {
2521                let ctx = Context::<mode::HalfEven>::new(p);
2522                assert_eq!(
2523                    ctx.sin_pi::<10>(x.repr(), None).unwrap().value(),
2524                    ctx.sin_unit::<10>(x.repr(), 2, None).unwrap().value(),
2525                    "sin_pi == sin_unit(2), x={input}"
2526                );
2527                assert_eq!(
2528                    ctx.cos_pi::<10>(x.repr(), None).unwrap().value(),
2529                    ctx.cos_unit::<10>(x.repr(), 2, None).unwrap().value()
2530                );
2531                let t_pi = ctx.tan_pi::<10>(x.repr(), None);
2532                let t_u = ctx.tan_unit::<10>(x.repr(), 2, None);
2533                assert!(
2534                    t_pi.is_err() == t_u.is_err()
2535                        && t_pi.map(|v| v.value()) == t_u.map(|v| v.value())
2536                );
2537                let (s_pi, c_pi) = ctx.sin_cos_pi::<10>(x.repr(), None);
2538                let (s_u, c_u) = ctx.sin_cos_unit::<10>(x.repr(), 2, None);
2539                assert_eq!(s_pi.unwrap().value(), s_u.unwrap().value());
2540                assert_eq!(c_pi.unwrap().value(), c_u.unwrap().value());
2541            }
2542        }
2543    }
2544
2545    /// The inverse ×u family: exact rows (in degrees), the u = 0 limits, and the general path
2546    /// against the high-precision oracle.
2547    #[test]
2548    fn test_inverse_unit_family() {
2549        fn check<R: ErrorBounds>(mode_ctx: &Context<R>) {
2550            let x = |v: &str| DBig::from_str(v).unwrap();
2551            let u = 360usize;
2552
2553            // exact rows
2554            assert_eq!(
2555                mode_ctx
2556                    .asin_unit::<10>(x("1").repr(), u, None)
2557                    .unwrap()
2558                    .value(),
2559                DBig::from_str("90").unwrap()
2560            );
2561            assert_eq!(
2562                mode_ctx
2563                    .asin_unit::<10>(x("-1").repr(), u, None)
2564                    .unwrap()
2565                    .value(),
2566                DBig::from_str("-90").unwrap()
2567            );
2568            assert_eq!(
2569                mode_ctx
2570                    .asin_unit::<10>(x("0.5").repr(), u, None)
2571                    .unwrap()
2572                    .value(),
2573                DBig::from_str("30").unwrap()
2574            );
2575            assert_eq!(
2576                mode_ctx
2577                    .acos_unit::<10>(x("1").repr(), u, None)
2578                    .unwrap()
2579                    .value(),
2580                DBig::ZERO
2581            );
2582            assert_eq!(
2583                mode_ctx
2584                    .acos_unit::<10>(x("0").repr(), u, None)
2585                    .unwrap()
2586                    .value(),
2587                DBig::from_str("90").unwrap()
2588            );
2589            assert_eq!(
2590                mode_ctx
2591                    .acos_unit::<10>(x("-0.5").repr(), u, None)
2592                    .unwrap()
2593                    .value(),
2594                DBig::from_str("120").unwrap()
2595            );
2596            assert_eq!(
2597                mode_ctx
2598                    .atan_unit::<10>(x("1.0").repr(), u, None)
2599                    .unwrap()
2600                    .value(),
2601                DBig::from_str("45").unwrap()
2602            );
2603            assert_eq!(
2604                mode_ctx
2605                    .atan_unit::<10>(&Repr::infinity(), u, None)
2606                    .unwrap()
2607                    .value(),
2608                DBig::from_str("90").unwrap()
2609            );
2610
2611            // atan2_unit's C99 table, in u-fractions
2612            let quarter = |a: &str, b: &str| {
2613                mode_ctx
2614                    .atan2_unit::<10>(x(a).repr(), x(b).repr(), u, None)
2615                    .unwrap()
2616                    .value()
2617            };
2618            assert_eq!(quarter("1.0", "1.0"), DBig::from_str("45").unwrap());
2619            assert_eq!(quarter("1.0", "-1.0"), DBig::from_str("135").unwrap());
2620            assert_eq!(quarter("0", "1"), DBig::ZERO);
2621            assert_eq!(quarter("1", "0"), DBig::from_str("90").unwrap());
2622            assert_eq!(quarter("0", "-1"), DBig::from_str("180").unwrap());
2623            assert_eq!(
2624                mode_ctx
2625                    .atan2_unit::<10>(&Repr::infinity(), &Repr::infinity(), u, None)
2626                    .unwrap()
2627                    .value(),
2628                DBig::from_str("45").unwrap()
2629            );
2630            assert_eq!(
2631                mode_ctx
2632                    .atan2_unit::<10>(&Repr::infinity(), &Repr::neg_infinity(), u, None)
2633                    .unwrap()
2634                    .value(),
2635                DBig::from_str("135").unwrap()
2636            );
2637            assert!(matches!(
2638                mode_ctx.atan2_unit::<10>(x("0").repr(), x("0").repr(), u, None),
2639                Err(FpError::OutOfDomain)
2640            ));
2641
2642            // u = 0: the signed-zero limits
2643            assert!(mode_ctx
2644                .asin_unit::<10>(x("0.5").repr(), 0, None)
2645                .unwrap()
2646                .value()
2647                .repr()
2648                .significand
2649                .is_zero());
2650            assert!(mode_ctx
2651                .acos_unit::<10>(x("0.5").repr(), 0, None)
2652                .unwrap()
2653                .value()
2654                .repr()
2655                .is_pos_zero());
2656
2657            // domain errors
2658            assert!(matches!(
2659                mode_ctx.asin_unit::<10>(x("1.5").repr(), u, None),
2660                Err(FpError::OutOfDomain)
2661            ));
2662            assert!(matches!(
2663                mode_ctx.acos_unit::<10>(x("-1.5").repr(), u, None),
2664                Err(FpError::OutOfDomain)
2665            ));
2666            // ... and the domain error outranks the u = 0 limit (the limit of a function
2667            // that is undefined at this x for every u > 0 is not +0)
2668            assert!(matches!(
2669                mode_ctx.asin_unit::<10>(x("1.5").repr(), 0, None),
2670                Err(FpError::OutOfDomain)
2671            ));
2672            assert!(matches!(
2673                mode_ctx.acos_unit::<10>(x("-1.5").repr(), 0, None),
2674                Err(FpError::OutOfDomain)
2675            ));
2676        }
2677
2678        for &p in &[20usize, 50, 100, 500] {
2679            check(&Context::<mode::HalfEven>::new(p));
2680            check(&Context::<mode::Down>::new(p));
2681
2682            // general path vs the oracle, under two modes
2683            for input in ["0.1", "0.3", "0.7", "-0.2", "0.9"] {
2684                let x = DBig::from_str(input).unwrap();
2685                for &u in &[2usize, 7, 360] {
2686                    let hi = Context::<mode::HalfEven>::new(p + 60);
2687                    for op in 0..3 {
2688                        match op {
2689                            0 => {
2690                                let expect = reround::<mode::HalfEven, 10>(
2691                                    &hi.asin_unit::<10>(x.repr(), u, None).unwrap().value(),
2692                                    p,
2693                                );
2694                                assert_eq!(
2695                                    Context::<mode::HalfEven>::new(p)
2696                                        .asin_unit::<10>(x.repr(), u, None)
2697                                        .unwrap()
2698                                        .value(),
2699                                    expect,
2700                                    "asin_unit({input}, {u}) at p={p}"
2701                                );
2702                            }
2703                            1 => {
2704                                let expect = reround::<mode::Down, 10>(
2705                                    &hi.acos_unit::<10>(x.repr(), u, None).unwrap().value(),
2706                                    p,
2707                                );
2708                                assert_eq!(
2709                                    Context::<mode::Down>::new(p)
2710                                        .acos_unit::<10>(x.repr(), u, None)
2711                                        .unwrap()
2712                                        .value(),
2713                                    expect,
2714                                    "acos_unit({input}, {u}) at p={p} under Down"
2715                                );
2716                            }
2717                            _ => {
2718                                let expect = reround::<mode::HalfEven, 10>(
2719                                    &hi.atan_unit::<10>(x.repr(), u, None).unwrap().value(),
2720                                    p,
2721                                );
2722                                assert_eq!(
2723                                    Context::<mode::HalfEven>::new(p)
2724                                        .atan_unit::<10>(x.repr(), u, None)
2725                                        .unwrap()
2726                                        .value(),
2727                                    expect,
2728                                    "atan_unit({input}, {u}) at p={p}"
2729                                );
2730                            }
2731                        }
2732                    }
2733                }
2734            }
2735
2736            // atan2_unit general path vs the oracle
2737            for (ya, xa) in [("1.2", "3.4"), ("-2.5", "0.7"), ("0.3", "-4.0")] {
2738                let y = DBig::from_str(ya).unwrap();
2739                let x = DBig::from_str(xa).unwrap();
2740                let hi = Context::<mode::HalfEven>::new(p + 60);
2741                let expect = reround::<mode::HalfEven, 10>(
2742                    &hi.atan2_unit::<10>(y.repr(), x.repr(), 360, None)
2743                        .unwrap()
2744                        .value(),
2745                    p,
2746                );
2747                assert_eq!(
2748                    Context::<mode::HalfEven>::new(p)
2749                        .atan2_unit::<10>(y.repr(), x.repr(), 360, None)
2750                        .unwrap()
2751                        .value(),
2752                    expect,
2753                    "atan2_unit({ya}, {xa}, 360) at p={p}"
2754                );
2755            }
2756        }
2757
2758        // roundtrip: asin_unit(sin_unit(x)) ≈ x on the degrees scale, for x in the principal
2759        // range [−90°, 90°] (beyond it asin folds into the range)
2760        type HF = FBig<mode::HalfEven, 10>;
2761        let ctx = Context::<mode::HalfEven>::new(50);
2762        let tol = HF::from_parts(IBig::from(1), -40);
2763        for deg in ["10", "30", "45", "89.9", "90", "-42.7"] {
2764            let x = HF::from_str(deg).unwrap().with_precision(50).value();
2765            let s = ctx.sin_unit::<10>(x.repr(), 360, None).unwrap().value();
2766            let back = ctx.asin_unit::<10>(s.repr(), 360, None).unwrap().value();
2767            assert!(
2768                (x.clone() - back).abs().abs_cmp(&tol).is_le(),
2769                "asin_unit(sin_unit({deg}°)) roundtrip"
2770            );
2771        }
2772    }
2773
2774    /// The ×u general path vs the high-precision oracle (p + 60 under `HalfEven`, re-rounded
2775    /// under the mode under test), across u values with no shared factors (7, 11), the
2776    /// composite 360, and the tight u = 1.
2777    #[test]
2778    fn test_unit_family_oracle() {
2779        let inputs = [
2780            "0.1", "0.2", "0.3", "0.4", "0.6", "0.7", "0.9", "1.1", "2.3", "-0.3", "-1.2", "10.1",
2781            "123.456", "1e20",
2782        ];
2783        for &p in &[20usize, 50, 100, 500] {
2784            for input in inputs {
2785                let x = DBig::from_str(input).unwrap();
2786                for &u in &[1usize, 7, 11, 360] {
2787                    let hi_ctx = Context::<mode::HalfEven>::new(p + 60);
2788                    let hi_s = hi_ctx.sin_unit::<10>(x.repr(), u, None).unwrap().value();
2789                    let hi_c = hi_ctx.cos_unit::<10>(x.repr(), u, None).unwrap().value();
2790                    let hi_t = match hi_ctx.tan_unit::<10>(x.repr(), u, None) {
2791                        Ok(v) => v.value(),
2792                        Err(_) => continue, // pole (pinpointed by the exact-table test)
2793                    };
2794
2795                    let got = Context::<mode::HalfEven>::new(p)
2796                        .sin_unit::<10>(x.repr(), u, None)
2797                        .unwrap()
2798                        .value();
2799                    assert_eq!(
2800                        got,
2801                        reround::<mode::HalfEven, 10>(&hi_s, p),
2802                        "sin_unit({input}, {u}) at p={p}"
2803                    );
2804                    let got = Context::<mode::Down>::new(p)
2805                        .cos_unit::<10>(x.repr(), u, None)
2806                        .unwrap()
2807                        .value();
2808                    assert_eq!(
2809                        got,
2810                        reround::<mode::Down, 10>(&hi_c, p),
2811                        "cos_unit({input}, {u}) at p={p} under Down"
2812                    );
2813                    let got = Context::<mode::Up>::new(p)
2814                        .tan_unit::<10>(x.repr(), u, None)
2815                        .unwrap()
2816                        .value();
2817                    assert_eq!(
2818                        got,
2819                        reround::<mode::Up, 10>(&hi_t, p),
2820                        "tan_unit({input}, {u}) at p={p} under Up"
2821                    );
2822                }
2823            }
2824        }
2825    }
2826
2827    /// Direct unit checks of the integer classification and reduction kernels, including an
2828    /// odd base (where the exact points beyond the integers are sparse, and the split's
2829    /// `r = num/den` is not representable as a base-B float).
2830    #[test]
2831    fn test_unit_class_and_reduction_kernels() {
2832        // classification j = (k·x/u) mod 2k, base 10. For u = 2 the k = 12 residue is always a
2833        // multiple of 3 (12x/2 = 6x — a half-integer), so the u = 2 rows only exercise the
2834        // quarter table; the degrees rows hit the sixths and the tangent eighths.
2835        let uc = |m: i64, e: isize, u: usize, k: usize| {
2836            unit_residue(&Repr::<10>::new(IBig::from(m), e), u, k)
2837        };
2838        // u = 2, k = 12: j = 6x mod 24
2839        assert_eq!(uc(5, -1, 2, 12), Some(3)); // 0.5 → 3 (q = 1: sin +1)
2840        assert_eq!(uc(25, -2, 2, 12), None); // 1.5 ∉ ℤ (sin(π/4) = √2/2, general)
2841        assert_eq!(uc(75, -2, 2, 12), None); // 4.5 ∉ ℤ
2842        assert_eq!(uc(-25, -2, 2, 12), None);
2843        assert_eq!(uc(3, 0, 2, 12), Some(18)); // 18 (q = 6: sin ±0)
2844        assert_eq!(uc(8, 0, 2, 12), Some(0)); // 48 ≡ 0
2845        assert_eq!(uc(1, 1, 2, 12), Some(12)); // 60 ≡ 12 (q = 4)
2846        assert_eq!(uc(1, -1, 2, 12), None); // 0.6 ∉ ℤ
2847        assert_eq!(uc(1, -1000, 2, 12), None); // huge s: rejected by bounds, no materialization
2848                                               // u = 2, k = 8: j = 4x mod 16
2849        assert_eq!(uc(5, -1, 2, 8), Some(2)); // 0.5 → 2 (m = 2: the tan pole)
2850        assert_eq!(uc(25, -2, 2, 8), Some(1)); // 0.25 → 1 (tan(π/4) = 1)
2851        assert_eq!(uc(75, -2, 2, 8), Some(3)); // 0.75 → 3 (tan(3π/4) = −1)
2852        assert_eq!(uc(15, -1, 2, 8), Some(6)); // 1.5 → 6 (m = 6: the tan pole)
2853                                               // degrees, k = 12: j = x/30 mod 24
2854        assert_eq!(uc(30, 0, 360, 12), Some(1)); // sin 30° = 1/2
2855        assert_eq!(uc(150, 0, 360, 12), Some(5)); // sin 150° = 1/2
2856        assert_eq!(uc(210, 0, 360, 12), Some(7)); // sin 210° = −1/2
2857        assert_eq!(uc(60, 0, 360, 12), Some(2)); // cos 60° = 1/2
2858        assert_eq!(uc(120, 0, 360, 12), Some(4)); // cos 120° = −1/2
2859        assert_eq!(uc(90, 0, 360, 12), Some(3)); // sin 90° = 1 (quarter row)
2860        assert_eq!(uc(45, 0, 360, 12), None); // sin 45° = √2/2 (general path)
2861        assert_eq!(uc(455, -1, 360, 12), None); // 45.5° — general path
2862                                                // degrees, k = 8: j = x/45 mod 16
2863        assert_eq!(uc(45, 0, 360, 8), Some(1)); // tan 45° = 1
2864        assert_eq!(uc(135, 0, 360, 8), Some(3)); // tan 135° = −1
2865        assert_eq!(uc(90, 0, 360, 8), Some(2)); // tan 90° — the pole
2866        assert_eq!(uc(180, 0, 360, 8), Some(4)); // tan 180° → −0 row
2867                                                 // huge integer exponent reduced by modular exponentiation (10^30 never materialized):
2868                                                 // 12·10^30/360 = 10^29/3 ∉ ℤ; 12·10^30/8 = 15·10^29 ∈ ℤ with 15·10^29 ≡ 0 (mod 24).
2869        assert_eq!(uc(1, 30, 360, 12), None);
2870        assert_eq!(uc(1, 30, 8, 12), Some(0));
2871
2872        // classification, base 3 (odd): k·x/u ∈ ℤ for integer x whenever u | k·m
2873        let uc3 = |m: i64, e: isize, u: usize, k: usize| {
2874            unit_residue(&Repr::<3>::new(IBig::from(m), e), u, k)
2875        };
2876        assert_eq!(uc3(7, 0, 2, 12), Some(18)); // 42 ≡ 18 (mod 24)
2877        assert_eq!(uc3(6, 0, 2, 12), Some(12)); // 36 ≡ 12 (mod 24)
2878                                                // 1/3 is exact in base 3: 12·(1/3)/2 = 2 ∈ ℤ — a sixth row invisible in base 2/10
2879        assert_eq!(uc3(1, -1, 2, 12), Some(2));
2880        assert_eq!(uc3(1, -1, 2, 8), None); // 8/(3·2) = 4/3 ∉ ℤ
2881
2882        // exact reduction split, base 10, u = 2: r = |x| − k/2 as an exact rational
2883        match reduce_unit_argument(&Repr::<10>::new(IBig::from(3), -1), 2) {
2884            // 0.3: k = 1, r = -0.2 = -8/40
2885            UnitReduced::Split { quadrant, num, den } => {
2886                assert_eq!(num, IBig::from(-8));
2887                assert_eq!(den, IBig::from(40));
2888                assert_eq!(quadrant, Quadrant::Second);
2889            }
2890            _ => panic!("0.3 must take the rational split"),
2891        }
2892        match reduce_unit_argument(&Repr::<10>::new(IBig::from(26), -1), 2) {
2893            // 2.6: k = 5, r = 0.1 = 4/40
2894            UnitReduced::Split { quadrant, num, den } => {
2895                assert_eq!(num, IBig::from(4));
2896                assert_eq!(den, IBig::from(40));
2897                assert_eq!(quadrant, Quadrant::Second); // k = 5 ≡ 1 (mod 4)
2898            }
2899            _ => panic!("2.6 must take the rational split"),
2900        }
2901        // the tie 0.25 (u = 2): k = 1 (half-up), r = -1/4 = -50/200
2902        match reduce_unit_argument(&Repr::<10>::new(IBig::from(25), -2), 2) {
2903            UnitReduced::Split { num, den, .. } => {
2904                assert_eq!(num, IBig::from(-100));
2905                assert_eq!(den, IBig::from(400));
2906            }
2907            _ => panic!("0.25 must take the rational split"),
2908        }
2909        // u = 8, integer argument on the general path: x = 5 ≡ 5 (mod 8), k = 3,
2910        // r = 5 − 3·8/4 = −1 = −4/4
2911        match reduce_unit_argument(&Repr::<10>::new(IBig::from(5), 0), 8) {
2912            UnitReduced::Split { quadrant, num, den } => {
2913                assert_eq!(num, IBig::from(-4));
2914                assert_eq!(den, IBig::from(4));
2915                assert_eq!(quadrant, Quadrant::Fourth); // k = 3
2916            }
2917            _ => panic!("sin_unit(5, 8) must take the rational split"),
2918        }
2919        // u = 360, a huge integer multiple of u reduces exactly: x = 10^6 degrees,
2920        // 10^6 mod 360 = 280, k = round(4·280/360) = 3, r = 280 − 270 = 10 = 40/4
2921        match reduce_unit_argument(&Repr::<10>::new(IBig::from(1_000_000), 0), 360) {
2922            UnitReduced::Split { quadrant, num, den } => {
2923                assert_eq!(num, IBig::from(40));
2924                assert_eq!(den, IBig::from(4));
2925                assert_eq!(quadrant, Quadrant::Fourth); // k = 3
2926            }
2927            _ => panic!("10^6 degrees must take the rational split"),
2928        }
2929        // tiny: the k = 0 product path, no denominator materialized
2930        assert!(matches!(
2931            reduce_unit_argument(&Repr::<10>::new(IBig::from(1), -1), 2),
2932            UnitReduced::Small(_)
2933        ));
2934        assert!(matches!(
2935            reduce_unit_argument(&Repr::<2>::new(IBig::from(3), -1_000_000_000), 2),
2936            UnitReduced::Small(_)
2937        ));
2938        // negative input reduces on |x|
2939        match reduce_unit_argument(&Repr::<10>::new(IBig::from(-3), -1), 2) {
2940            UnitReduced::Split { num, den, .. } => {
2941                assert_eq!(num, IBig::from(-8));
2942                assert_eq!(den, IBig::from(40));
2943            }
2944            _ => panic!("-0.3 must take the rational split"),
2945        }
2946    }
2947
2948    /// An argument that merely *rounds* onto `±1` is not the endpoint: the inverse functions have
2949    /// a vertical tangent there, so the true result sits `√(2δ)` away — many ulps of the result.
2950    ///
2951    /// Regression: `asin_ball`'s endpoint branch keyed on `√(1−x²)` *collapsing* onto zero, and
2952    /// `Ball::sqrt` returned that zero with the radius dropped — so `asin(1 − 10⁻¹⁵⁵)` at 100
2953    /// digits returned exactly `π/2`, a result ~10⁹ ulps off, certified by the Ziv loop because
2954    /// the endpoint branch reports only a few ulps of radius. (The `acos`/×u rows restated here
2955    /// were saved by the loop's zero-candidate guard, which refuses to certify an exact zero
2956    /// against a nonzero radius; they are asserted for the same reason.)
2957    #[test]
2958    fn test_inverse_endpoints_compare_exactly() {
2959        // `1 − 10^-n` with `n = 1.5p + 5`: beyond the initial guard (50), so the argument rounds
2960        // onto exactly `1` at the first work precision for p ≥ 100 — the case that used to snap —
2961        // yet `√(2·10^-n)` is far above a result ulp. At p = 20/50 the general path is taken
2962        // anyway; those rows are plain correctness checks.
2963        for &p in &[20usize, 50, 100, 500] {
2964            let n = 3 * p / 2 + 5;
2965            let x = DBig::from_str(&format!("0.{}", "9".repeat(n))).unwrap();
2966            let ctx = Context::<mode::HalfEven>::new(p);
2967            let hi = Context::<mode::HalfEven>::new(p + 60);
2968            let reround = |v: FBig<mode::HalfEven, 10>| v.with_precision(p).value();
2969
2970            let asin = ctx.asin::<10>(x.repr(), None).unwrap().value();
2971            let asin_hi = reround(hi.asin::<10>(x.repr(), None).unwrap().value());
2972            assert_eq!(asin, asin_hi, "asin(1 - 10^-{n}) at p={p}");
2973            let one = Repr::<10>::one();
2974            assert_ne!(
2975                asin,
2976                ctx.asin::<10>(&one, None).unwrap().value(),
2977                "asin(1 - 10^-{n}) is not the endpoint"
2978            );
2979
2980            let acos = ctx.acos::<10>(x.repr(), None).unwrap().value();
2981            let acos_hi = reround(hi.acos::<10>(x.repr(), None).unwrap().value());
2982            assert_eq!(acos, acos_hi, "acos(1 − 10^-{n}) at p={p}");
2983            assert!(!acos.repr().significand().is_zero(), "acos is not an exact 0");
2984
2985            // the ×u inverses route through the same kernels
2986            let (asin_u, asin_u_hi) = (
2987                ctx.asin_unit::<10>(x.repr(), 360, None).unwrap().value(),
2988                reround(hi.asin_unit::<10>(x.repr(), 360, None).unwrap().value()),
2989            );
2990            assert_eq!(asin_u, asin_u_hi, "asin_unit(1 − 10^-{n}, 360) at p={p}");
2991            assert_ne!(
2992                asin_u,
2993                ctx.asin_unit::<10>(&one, 360, None).unwrap().value(),
2994                "asin_unit(1 − 10^-{n}) is not the u/4 endpoint"
2995            );
2996            let (acos_u, acos_u_hi) = (
2997                ctx.acos_unit::<10>(x.repr(), 360, None).unwrap().value(),
2998                reround(hi.acos_unit::<10>(x.repr(), 360, None).unwrap().value()),
2999            );
3000            assert_eq!(acos_u, acos_u_hi, "acos_unit(1 − 10^-{n}, 360) at p={p}");
3001            assert!(!acos_u.repr().significand().is_zero(), "acos_unit is not an exact 0");
3002        }
3003    }
3004}