// Copyright (C) 2026 Kiyotsugu Arai
// SPDX-License-Identifier: LGPL-3.0-or-later

// FloatMath.cpp
// Implementation of the multiple-precision floating-point math function library
// Arbitrary-precision implementation via Taylor series + argument halving/squaring reconstruction

#include <math/core/mp/Float/FloatMath.hpp>
#include <math/core/mp/FloatOps.hpp>
#include <math/core/mp/Int/IntSqrt.hpp>
#include <math/core/mp/Int/MpnOps.hpp>
#include <math/core/mp/Int/ScratchArena.hpp>
#include <cmath>
#include <limits>
#include <algorithm>
#include <vector>
#include <random>
#include <chrono>
#include <cstdio>
#include <bit>

namespace sangi {

    //=============================================================================
    // Utility functions
    //=============================================================================

    // Determine the computation precision (decimal digits) based on the input's effective_bits_.
    // If the input precision is sufficiently lower than the requested precision, compute with input precision + guard for speed.
    static int effectiveComputePrecision(int input_eff, int requested_precision) {
        // Transcendental functions must return a result accurate to the
        // requested precision even when the input is an exact value (e.g. 1.5 = 2 bits).
        // If the input's effective bits are less than half the requested precision, disable precision reduction.
        constexpr int GUARD = 32;
        int req_bits = Float::precisionToBits(requested_precision);
        if (input_eff >= INT_MAX || input_eff + GUARD >= req_bits) {
            return requested_precision;
        }
        // Even if the input's effective bits are few, do not drop below the requested precision
        // (a transcendental function's output precision is not limited by the input precision)
        return requested_precision;
    }

    // Analytically determine the working precision required to compute the Taylor series of sin/cos
    // (assuming |x| ≤ π/2 after argument reduction) to target_precision decimal digits.
    // The internal term count N truncates at |R_N(x)| ≤ |x|^N / N! ≤ 10^-target, so use a
    // conservative loop bound of 1.2·target + 10. The roundoff accumulation inside the loop is N ulps,
    // so add an extra guard of log10(N) digits plus a small safety margin.
    // The estimate may be rough (a ceiling-determined quantity), and double arithmetic is sufficient.
    static int taylorWorkingPrecision(int target_precision) {
        if (target_precision < 1) target_precision = 1;
        // Safe upper bound on the term count (actually terminates early at term.nw==0, but here an upper bound)
        double est_N = static_cast<double>(target_precision) * 1.2 + 10.0;
        // Obtain log10(N) via ceil (small, so double suffices; safe up to N < ~10^10)
        int log10_N = static_cast<int>(std::ceil(std::log10(est_N)));
        if (log10_N < 1) log10_N = 1;
        constexpr int SAFETY = 4;  // Safety margin for small constants (~13 bits)
        int guard = log10_N + SAFETY;
        // For compatibility, keep the legacy fixed +10 as a lower bound
        if (guard < 10) guard = 10;
        return target_precision + guard;
    }

    // Set the result's precision field and reflect the input's effective bits.
    // If input_eff < requested, set effective_bits_ to input_eff.
    static void finalizeResult(Float& result, int input_eff, int precision) {
        result.setResultPrecision(precision);
        int req_bits = Float::precisionToBits(precision);
        if (input_eff < INT_MAX && input_eff < req_bits) {
            result.setEffectiveBits(input_eff);
        }
    }

    //=============================================================================
    // Ziv's iteration strategy — correct rounding of transcendental functions
    //=============================================================================

    // Determine whether the rounding direction is fixed when the mantissa of the result y is rounded at target_bits.
    // Returns true if the guard_bits guard bits are sufficiently far from the rounding boundary.
    static bool canRoundCorrectly(const Float& y, int target_bits,
                                  int guard_bits, RoundingMode mode) {
        if (y.isZero() || y.isNaN() || y.isInfinity()) return true;

        int bit_length = static_cast<int>(y.mantissa().bitLength());
        if (bit_length <= target_bits) return true;  // No rounding needed

        // Number of guard bits usable for the decision (margin=2 accounting for computation error < 1 ulp)
        constexpr int MARGIN = 2;
        int usable = guard_bits - MARGIN;
        if (usable < 1) return false;

        // Guard bit region: bit positions [shift - guard_bits, shift - 1]
        // shift = bit_length - target_bits
        int shift = bit_length - target_bits;
        const uint64_t* data = y.mantissa().data();

        // Read the usable guard bits
        // Position: bit (shift - 1) is the most significant guard bit, bit (shift - usable) the least significant
        // If these are all 0 or all 1, we are too close to the rounding boundary

        int hi_pos = shift - 1;                     // Most significant guard bit position
        int lo_pos = shift - usable;                // Least significant guard bit position
        if (lo_pos < 0) lo_pos = 0;

        // Check whether all bits are 0
        bool all_zero = true;
        // Check whether all bits are 1
        bool all_one = true;

        // Scan word by word
        size_t lo_word = static_cast<size_t>(lo_pos) / 64;
        size_t hi_word = static_cast<size_t>(hi_pos) / 64;

        for (size_t w = lo_word; w <= hi_word; ++w) {
            uint64_t word = data[w];

            // Build the mask for the bit range examined in this word
            unsigned lo_bit = (w == lo_word) ? static_cast<unsigned>(lo_pos % 64) : 0;
            unsigned hi_bit = (w == hi_word) ? static_cast<unsigned>(hi_pos % 64) : 63;
            unsigned width = hi_bit - lo_bit + 1;

            uint64_t mask;
            if (width >= 64) {
                mask = ~uint64_t(0);
            } else {
                mask = ((uint64_t(1) << width) - 1) << lo_bit;
            }

            uint64_t bits = word & mask;
            if (bits != 0) all_zero = false;
            if (bits != mask) all_one = false;

            // Early exit if both are false
            if (!all_zero && !all_one) return true;
        }

        // All 0 or all 1 → too close to the rounding boundary, direction unknown
        return false;
    }

    // Ziv's iteration strategy template.
    // Calls computeFunc(x, eff_x, precision) → Float and guarantees correct rounding.
    // computeFunc's precision is in decimal digits. Assumes +10 guard digits are added internally.
    template <typename ComputeFunc>
    static Float zivRound(ComputeFunc&& computeFunc,
                          const Float& x, int eff_x, int precision) {
        constexpr int MAX_ITER = 6;
        int target_bits = Float::precisionToBits(precision);
        RoundingMode mode = Float::roundingMode();

        int extra_guard = 10;  // First pass: 10 guard digits ≈ 33 bits

        for (int iter = 0; iter < MAX_ITER; ++iter) {
            int wp = precision + extra_guard;
            Float y = computeFunc(x, eff_x, wp);

            int actual_bits = static_cast<int>(y.mantissa().bitLength());
            int guard_bits = actual_bits - target_bits;

            if (guard_bits > 0 &&
                canRoundCorrectly(y, target_bits, guard_bits, mode)) {
                finalizeResult(y, eff_x, precision);
                return y;
            }

            // Increase guard bits and retry
            extra_guard *= 2;
        }

        // Fallback: faithful rounding (practically never reached)
        Float y = computeFunc(x, eff_x, precision + extra_guard);
        finalizeResult(y, eff_x, precision);
        return y;
    }

    //=============================================================================
    // Direct mpn computation — eliminating allocation in the Taylor series loop
    //=============================================================================

    // The mpn namespace is sangi::mpn (defined in MpnOps.hpp)

    // Lightweight floating point: pointer into the arena + word count + exponent
    struct RawFloat {
        uint64_t* d;      // Mantissa (in arena, d[0]=LSB)
        size_t nw;         // Actual word count (leading zeros removed)
        int64_t exp;       // Binary exponent (value = mantissa * 2^exp)
    };

    // Mantissa normalization: place the MSB at bit 63 of the most significant word
    // Call after rf_mul/rf_divmod_1 to keep the exponent consistent
    static void rf_normalize(RawFloat& a) {
        if (a.nw == 0) return;
        // Remove leading zero words
        a.nw = mpn::normalized_size(a.d, a.nw);
        if (a.nw == 0) return;
        // Bit level: left-shift by the leading zeros of the most significant word
        unsigned lz = static_cast<unsigned>(std::countl_zero(a.d[a.nw - 1]));
        if (lz > 0) {
            for (size_t i = a.nw - 1; i > 0; --i) {
                a.d[i] = (a.d[i] << lz) | (a.d[i - 1] >> (64 - lz));
            }
            a.d[0] <<= lz;
            a.exp -= static_cast<int64_t>(lz);
        }
    }

    // Multiply + truncate: dst = a * b, keeping only the high target_nw words
    static void rf_mul(RawFloat& dst, const RawFloat& a, const RawFloat& b,
                       uint64_t* prod_buf, uint64_t* scratch, size_t target_nw) {
        if (a.nw == 0 || b.nw == 0) {
            dst.nw = 0; dst.exp = 0; return;
        }

        int64_t prod_exp = a.exp + b.exp;

        // mulhigh_n optimization: if both are target_nw size, compute only the high part (cost ≈ 2/3)
        if (a.nw == target_nw && b.nw == target_nw && target_nw >= 8) {
            // Verify scratch is sufficient (multiply_scratch_size >= mulhigh_scratch_size)
            mpn::mulhigh_n(prod_buf, a.d, b.d, target_nw, scratch);
            // mulhigh_n yields rp[0..n-1] = the high n words (approximate, O(1) error)
            std::memcpy(dst.d, prod_buf, target_nw * sizeof(uint64_t));
            dst.nw = mpn::normalized_size(dst.d, target_nw);
            // The mulhigh result is an approximation of a*b >> (target_nw * 64)
            dst.exp = prod_exp + static_cast<int64_t>(target_nw) * 64;
            rf_normalize(dst);
            return;
        }

        size_t prod_n = a.nw + b.nw;
        mpn::multiply(prod_buf, a.d, a.nw, b.d, b.nw, scratch);
        size_t actual = mpn::normalized_size(prod_buf, prod_n);

        if (actual > target_nw) {
            size_t drop = actual - target_nw;
            std::memcpy(dst.d, prod_buf + drop, target_nw * sizeof(uint64_t));
            dst.nw = mpn::normalized_size(dst.d, target_nw);
            dst.exp = prod_exp + static_cast<int64_t>(drop) * 64;
        } else {
            std::memcpy(dst.d, prod_buf, actual * sizeof(uint64_t));
            dst.nw = actual;
            dst.exp = prod_exp;
        }
        rf_normalize(dst);
    }

    // Cached multiply: dst = a * b, caching the forward NTT of b
    // When b is constant and multiplied repeatedly, the forward NTT can be skipped once
    static void rf_mul_cached(RawFloat& dst, const RawFloat& a, const RawFloat& b,
                       uint64_t* prod_buf, size_t target_nw,
                       prime_ntt::NttCache& cache) {
        if (a.nw == 0 || b.nw == 0) {
            dst.nw = 0; dst.exp = 0; return;
        }
        // Below the NTT threshold, fall back to ordinary multiplication
        size_t min_nw = std::min(a.nw, b.nw);
        if (min_nw < 3000) {
            // scratch needed → allocate thread_local
            thread_local std::vector<uint64_t> scratch_buf;
            size_t scratch_need = mpn::multiply_scratch_size(
                std::max(a.nw, b.nw), min_nw);
            if (scratch_buf.size() < scratch_need) scratch_buf.resize(scratch_need);
            rf_mul(dst, a, b, prod_buf, scratch_buf.data(), target_nw);
            return;
        }
        size_t prod_n = a.nw + b.nw;
        prime_ntt::mul_prime_ntt_cached(prod_buf, a.d, a.nw, b.d, b.nw, cache);
        size_t actual = mpn::normalized_size(prod_buf, prod_n);
        int64_t prod_exp = a.exp + b.exp;

        if (actual > target_nw) {
            size_t drop = actual - target_nw;
            std::memcpy(dst.d, prod_buf + drop, target_nw * sizeof(uint64_t));
            dst.nw = mpn::normalized_size(dst.d, target_nw);
            dst.exp = prod_exp + static_cast<int64_t>(drop) * 64;
        } else {
            std::memcpy(dst.d, prod_buf, actual * sizeof(uint64_t));
            dst.nw = actual;
            dst.exp = prod_exp;
        }
        rf_normalize(dst);
    }

    // Square + truncate: dst = a^2, keeping only the high target_nw words
    // mpn::square is faster than multiply (skips the half-diagonal additions)
    static void rf_sqr(RawFloat& dst, const RawFloat& a,
                       uint64_t* prod_buf, uint64_t* scratch, size_t target_nw) {
        if (a.nw == 0) {
            dst.nw = 0; dst.exp = 0; return;
        }
        size_t prod_n = 2 * a.nw;
        mpn::square(prod_buf, a.d, a.nw, scratch);
        size_t actual = mpn::normalized_size(prod_buf, prod_n);
        int64_t prod_exp = a.exp + a.exp;

        if (actual > target_nw) {
            size_t drop = actual - target_nw;
            std::memcpy(dst.d, prod_buf + drop, target_nw * sizeof(uint64_t));
            dst.nw = mpn::normalized_size(dst.d, target_nw);
            dst.exp = prod_exp + static_cast<int64_t>(drop) * 64;
        } else {
            std::memcpy(dst.d, prod_buf, actual * sizeof(uint64_t));
            dst.nw = actual;
            dst.exp = prod_exp;
        }
        rf_normalize(dst);
    }

    // Single-limb division (in place): a /= divisor
    static void rf_divmod_1(RawFloat& a, uint64_t divisor) {
        if (a.nw == 0) return;
        mpn::divmod_1(a.d, a.d, a.nw, divisor);
        rf_normalize(a);
    }

    // Single-limb multiply (in place): a *= multiplier
    static void rf_mul_1(RawFloat& a, uint64_t multiplier) {
        if (a.nw == 0 || multiplier == 0) { a.nw = 0; a.exp = 0; return; }
        if (multiplier == 1) return;
        uint64_t carry = mpn::mul_1(a.d, a.d, a.nw, multiplier);
        if (carry) {
            a.d[a.nw] = carry;
            a.nw++;
        }
        rf_normalize(a);
    }

    // Right-shift src by shift_bits and add to dst (in place)
    // The overlapping part of dst[0..dst_nw-1] += (src >> shift_bits)
    // Return value: carry beyond dst_nw (0 or 1)
    static uint64_t shift_add(uint64_t* dst, size_t dst_nw,
                              const uint64_t* src, size_t src_nw,
                              size_t shift_bits) {
        size_t word_shift = shift_bits / 64;
        unsigned bit_shift = static_cast<unsigned>(shift_bits % 64);

        if (word_shift >= src_nw) return 0;

        size_t remaining = src_nw - word_shift;
        size_t overlap = std::min(remaining, dst_nw);
        if (overlap == 0) return 0;

        uint64_t carry = 0;

        if (bit_shift == 0) {
            carry = mpn::add(dst, dst, overlap, src + word_shift, overlap);
        } else {
            for (size_t i = 0; i < overlap; ++i) {
                uint64_t lo = src[word_shift + i] >> bit_shift;
                uint64_t hi = (word_shift + i + 1 < src_nw) ?
                              (src[word_shift + i + 1] << (64 - bit_shift)) : 0;
                uint64_t shifted = lo | hi;

                uint64_t sum = dst[i] + shifted;
                uint64_t c = (sum < dst[i]) ? 1ULL : 0ULL;
                sum += carry;
                c += (sum < carry) ? 1ULL : 0ULL;
                dst[i] = sum;
                carry = c;
            }
        }

        // Carry propagation
        for (size_t i = overlap; carry && i < dst_nw; ++i) {
            dst[i] += carry;
            carry = (dst[i] < carry) ? 1ULL : 0ULL;
        }

        return carry;
    }

    // Right-shift src by shift_bits and subtract from dst (in place)
    // The overlapping part of dst[0..dst_nw-1] -= (src >> shift_bits)
    // Return value: borrow (0 or 1)
    static uint64_t shift_sub(uint64_t* dst, size_t dst_nw,
                              const uint64_t* src, size_t src_nw,
                              size_t shift_bits) {
        size_t word_shift = shift_bits / 64;
        unsigned bit_shift = static_cast<unsigned>(shift_bits % 64);

        if (word_shift >= src_nw) return 0;

        size_t remaining = src_nw - word_shift;
        size_t overlap = std::min(remaining, dst_nw);
        if (overlap == 0) return 0;

        uint64_t borrow = 0;

        if (bit_shift == 0) {
            borrow = mpn::sub(dst, dst, overlap, src + word_shift, overlap);
        } else {
            for (size_t i = 0; i < overlap; ++i) {
                uint64_t lo = src[word_shift + i] >> bit_shift;
                uint64_t hi = (word_shift + i + 1 < src_nw) ?
                              (src[word_shift + i + 1] << (64 - bit_shift)) : 0;
                uint64_t shifted = lo | hi;

                uint64_t diff = dst[i] - shifted;
                uint64_t b = (diff > dst[i]) ? 1ULL : 0ULL;
                uint64_t diff2 = diff - borrow;
                b += (diff2 > diff) ? 1ULL : 0ULL;
                dst[i] = diff2;
                borrow = b;
            }
        }

        // Borrow propagation
        for (size_t i = overlap; borrow && i < dst_nw; ++i) {
            uint64_t old_val = dst[i];
            dst[i] -= borrow;
            borrow = (dst[i] > old_val) ? 1ULL : 0ULL;
        }

        return borrow;
    }

    // Addition (in place): result += term (bit-level alignment)
    // Return value: if false, term is negligible (used for convergence detection)
    static bool rf_add(RawFloat& result, const RawFloat& term, size_t nw_max) {
        if (term.nw == 0) return false;
        if (result.nw == 0) {
            std::memcpy(result.d, term.d, term.nw * sizeof(uint64_t));
            result.nw = term.nw; result.exp = term.exp;
            return true;
        }

        int64_t exp_diff = result.exp - term.exp;

        if (exp_diff < 0) {
            // term has the larger exponent: result = term + (old result >> |exp_diff|)
            size_t neg_diff = static_cast<size_t>(-exp_diff);
            if (neg_diff / 64 >= result.nw) {
                // result is negligible → overwrite with term
                std::memcpy(result.d, term.d, term.nw * sizeof(uint64_t));
                result.nw = term.nw; result.exp = term.exp;
                return true;
            }

            // Save the old result (rare case — only on the first Taylor pass)
            std::vector<uint64_t> old_data(result.d, result.d + result.nw);
            size_t old_nw = result.nw;

            // result ← term
            std::memset(result.d, 0, nw_max * sizeof(uint64_t));
            std::memcpy(result.d, term.d, term.nw * sizeof(uint64_t));
            result.nw = term.nw;
            result.exp = term.exp;

            // result += (old result >> neg_diff)
            uint64_t carry = shift_add(result.d, result.nw,
                                       old_data.data(), old_nw, neg_diff);
            if (carry && result.nw < nw_max) {
                result.d[result.nw] = carry;
                result.nw++;
            }
            return true;
        }

        // exp_diff >= 0: result += (term >> exp_diff)
        size_t total_shift = static_cast<size_t>(exp_diff);
        if (total_shift / 64 >= term.nw) return false;  // negligible

        uint64_t carry = shift_add(result.d, result.nw,
                                   term.d, term.nw, total_shift);
        if (carry && result.nw < nw_max) {
            result.d[result.nw] = carry;
            result.nw++;
        }
        return true;
    }

    // Subtraction (in place): result -= term (for sin/cos)
    static bool rf_sub(RawFloat& result, const RawFloat& term, size_t nw_max) {
        if (term.nw == 0) return false;
        if (result.nw == 0) return false;

        int64_t exp_diff = result.exp - term.exp;
        if (exp_diff < 0) return true;  // term > result: theoretically unreachable

        size_t total_shift = static_cast<size_t>(exp_diff);
        if (total_shift / 64 >= term.nw) return false;  // negligible

        shift_sub(result.d, result.nw, term.d, term.nw, total_shift);
        result.nw = mpn::normalized_size(result.d, result.nw);
        return true;
    }

    // Float → RawFloat extraction (normalized, allocated in the arena)
    static RawFloat rf_extract(const Float& x, int nw, ScratchArena& arena) {
        auto x_vec = x.mantissa().words();
        size_t x_nw_orig = x_vec.size();
        int64_t x_exp = x.exponent();

        uint64_t* x_d;
        size_t x_nw;
        if (x_nw_orig > static_cast<size_t>(nw)) {
            x_d = arena.alloc_limbs(nw);
            size_t drop = x_nw_orig - nw;
            std::memcpy(x_d, x_vec.data() + drop, nw * sizeof(uint64_t));
            x_exp += static_cast<int64_t>(drop) * 64;
            x_nw = mpn::normalized_size(x_d, nw);
        } else {
            x_d = arena.alloc_limbs(x_nw_orig);
            std::memcpy(x_d, x_vec.data(), x_nw_orig * sizeof(uint64_t));
            x_nw = x_nw_orig;
        }
        RawFloat rf{x_d, x_nw, x_exp};
        rf_normalize(rf);
        return rf;
    }

    // RawFloat → Float conversion
    static Float rf_to_float(const RawFloat& rf, bool negative, int working_precision) {
        if (rf.nw == 0) return Float();
        std::vector<uint64_t> words(rf.d, rf.d + rf.nw);
        Int mantissa = Int::fromRawWords(words, 1);
        Float result(mantissa, rf.exp, negative);
        int wp_bits = Float::precisionToBits(working_precision);
        result.setEffectiveBits(wp_bits);
        result.truncateToApprox(working_precision);
        return result;
    }

    //=============================================================================
    // Exponential function (exp) — Taylor series + argument halving/squaring reconstruction
    //=============================================================================

    // Naive Taylor series (RawFloat output version, handles the sign of x)
    // Version with divmod_1 + normalize separated (normalize can be deferred until before multiplication)
    static void rf_divmod_1_no_norm(RawFloat& a, uint64_t divisor) {
        if (a.nw == 0) return;
        mpn::divmod_1(a.d, a.d, a.nw, divisor);
        // Do not call normalize — only remove leading zero words
        a.nw = mpn::normalized_size(a.d, a.nw);
    }

    static void rf_exp_naive(RawFloat& result, const RawFloat& x_rf, bool x_negative,
                              int nw, uint64_t* prod_buf, uint64_t* scratch,
                              ScratchArena& arena, int working_precision) {
        uint64_t* term_d = arena.alloc_limbs(nw + 2);
        // term = 1.0
        std::memset(term_d, 0, (nw + 2) * sizeof(uint64_t));
        term_d[nw - 1] = uint64_t(1) << 63;
        RawFloat term{term_d, static_cast<size_t>(nw), -static_cast<int64_t>(nw * 64 - 1)};
        // result = 1.0
        std::memset(result.d, 0, (nw + 2) * sizeof(uint64_t));
        result.d[nw - 1] = uint64_t(1) << 63;
        result.nw = static_cast<size_t>(nw);
        result.exp = -static_cast<int64_t>(nw * 64 - 1);

        int max_terms = static_cast<int>(working_precision * 1.2) + 10;
        for (int n = 1; n <= max_terms; ++n) {
            // term = term * x / n
            // rf_mul normalizes internally (the multiplication result must be normalized)
            rf_mul(term, term, x_rf, prod_buf, scratch, nw);
            // divmod_1: skip normalize, only remove leading zero words
            // (the next rf_mul will normalize, so bit-level normalize is unnecessary)
            // However, since rf_add/rf_sub determine position from exp_diff,
            // a correct exp is needed → normalize before add
            rf_divmod_1_no_norm(term, static_cast<uint64_t>(n));
            if (term.nw == 0) break;
            // normalize before add/sub (exp accuracy is required)
            rf_normalize(term);
            if (x_negative && (n & 1)) {
                if (!rf_sub(result, term, nw + 1)) break;
            } else {
                if (!rf_add(result, term, nw + 1)) break;
            }
        }
    }

    // Paterson-Stockmeyer Taylor series (assumes x > 0)
    // O(sqrt(l)) full-size multiplications + O(l) single-word divisions
    // Algorithm equivalent to MPFR exp2_aux2
    static void rf_exp_ps(RawFloat& result, const RawFloat& x_rf, int nw,
                           uint64_t* prod_buf, uint64_t* scratch,
                           ScratchArena& arena, int working_precision) {
        int target_bits = nw * 64;
        // Term count estimate: |x| ≈ 2^x_msb, each term decreases by ~|x_msb| bits
        int64_t x_msb = x_rf.exp + static_cast<int64_t>(x_rf.nw) * 64;
        int x_log2 = (x_msb <= 0) ? static_cast<int>(-x_msb) : 1;
        if (x_log2 < 1) x_log2 = 1;
        int l_est = target_bits / x_log2 + 10;

        int m = static_cast<int>(std::sqrt(static_cast<double>(l_est)));
        if (m < 2) m = 2;
        if (m > 256) m = 256;

        // Precompute R[0..m]: R[i] = x^i (arena + fixed array to avoid heap)
        constexpr int R_MAX = 260;  // The upper bound on m is 256
        RawFloat R[R_MAX];
        for (int i = 0; i <= m; ++i) {
            R[i].d = arena.alloc_limbs(nw + 2);
            std::memset(R[i].d, 0, (nw + 2) * sizeof(uint64_t));
            R[i].nw = 0;
            R[i].exp = 0;
        }
        // R[0] = 1.0
        R[0].d[nw - 1] = uint64_t(1) << 63;
        R[0].nw = static_cast<size_t>(nw);
        R[0].exp = -static_cast<int64_t>(nw * 64 - 1);
        // R[1] = x
        std::memcpy(R[1].d, x_rf.d, x_rf.nw * sizeof(uint64_t));
        R[1].nw = x_rf.nw;
        R[1].exp = x_rf.exp;
        // R[2..m]: even indices via squaring (rf_sqr), odd indices via R[i-1]*R[1]
        // NTT cache of R[1] (reused for computing odd R[i])
        prime_ntt::NttCache cache_r1;
        rf_sqr(R[2], R[1], prod_buf, scratch, nw);
        for (int i = 3; i <= m; ++i) {
            if ((i & 1) == 0) {
                rf_sqr(R[i], R[i/2], prod_buf, scratch, nw);
            } else {
                rf_mul_cached(R[i], R[i-1], R[1], prod_buf, nw, cache_r1);
            }
        }

        // NTT cache of R[m] (used repeatedly in the giant steps)
        prime_ntt::NttCache cache_rm;

        // rr = 1.0 (tracks x^l / l!)
        RawFloat rr;
        rr.d = arena.alloc_limbs(nw + 2);
        std::memset(rr.d, 0, (nw + 2) * sizeof(uint64_t));
        rr.d[nw - 1] = uint64_t(1) << 63;
        rr.nw = static_cast<size_t>(nw);
        rr.exp = -static_cast<int64_t>(nw * 64 - 1);

        // t = baby step working space
        RawFloat t;
        t.d = arena.alloc_limbs(nw + 2);

        // result = 0
        std::memset(result.d, 0, (nw + 2) * sizeof(uint64_t));
        result.nw = 0;
        result.exp = 0;

        int l = 0;
        int max_giant_steps = l_est / m + 5;

        for (int gs = 0; gs < max_giant_steps; ++gs) {
            // Baby step: evaluate sum_{i=0}^{m-1} R[i] * l!/(l+i)! via Horner
            std::memcpy(t.d, R[m-1].d, R[m-1].nw * sizeof(uint64_t));
            t.nw = R[m-1].nw;
            t.exp = R[m-1].exp;

            for (int i = m - 2; i >= 0; --i) {
                rf_divmod_1(t, static_cast<uint64_t>(l + i + 1));
                if (t.nw == 0) {
                    std::memcpy(t.d, R[i].d, R[i].nw * sizeof(uint64_t));
                    t.nw = R[i].nw;
                    t.exp = R[i].exp;
                } else {
                    rf_add(t, R[i], nw + 1);
                }
            }

            // t *= rr (on the first giant step rr = 1, so no multiplication needed)
            if (gs > 0) {
                rf_mul(t, t, rr, prod_buf, scratch, nw);
            }
            if (t.nw == 0) break;

            // result += t
            rf_add(result, t, nw + 1);

            // Update rr: rr = rr * R[m] / ((l+1)(l+2)...(l+m))
            rf_mul_cached(rr, rr, R[m], prod_buf, nw, cache_rm);
            for (int i = 1; i <= m; ++i) {
                rf_divmod_1(rr, static_cast<uint64_t>(l + i));
            }
            l += m;
            if (rr.nw == 0) break;
        }
    }

    // Term count estimate for sin/cos Taylor (accounting for Stirling approximation)
    // Solve u^l / (2l)! < 2^(-target_bits)
    // log2(u^l / (2l)!) ≈ l*log2(u) - 2l*log2(2l/e) = -l*(u_log2 + 2*log2(2l/e))
    static int estimate_sincos_terms(int target_bits, int u_log2) {
        double tb = static_cast<double>(target_bits);
        double ul = static_cast<double>(u_log2);
        // Initial estimate (no factorial)
        double l_approx = tb / std::max(1.0, ul);
        // Converge via Stirling iteration
        for (int iter = 0; iter < 8; ++iter) {
            double two_l = 2.0 * l_approx;
            double factorial_bits = two_l * std::log2(std::max(2.0, two_l / 2.718281828459045));
            double bits_per_term = ul + factorial_bits / l_approx;
            double new_l = tb / std::max(1.0, bits_per_term);
            if (std::abs(new_l - l_approx) < 1.0) break;
            l_approx = new_l;
        }
        return static_cast<int>(l_approx) + 10;
    }

    // Paterson-Stockmeyer for the sin Taylor series
    // sin(x) = x * Σ_{k=0}^{∞} (-1)^k * u^k / (2k+1)!  where u = x²
    // O(sqrt(l)) full-size multiplications + O(l) single-word divisions
    static void rf_sin_ps(RawFloat& result, const RawFloat& x_rf, const RawFloat& x2,
                           int nw, uint64_t* prod_buf, uint64_t* scratch,
                           ScratchArena& arena, int working_precision) {
        int target_bits = nw * 64;
        // Estimate term count from the MSB of u = x² (Stirling approximation)
        int64_t u_msb = x2.exp + static_cast<int64_t>(x2.nw) * 64;
        int u_log2 = (u_msb <= 0) ? static_cast<int>(-u_msb) : 1;
        if (u_log2 < 1) u_log2 = 1;
        int l_est = estimate_sincos_terms(target_bits, u_log2);

        int m = static_cast<int>(std::sqrt(static_cast<double>(l_est)));
        if (m < 2) m = 2;
        if (m > 256) m = 256;

        // Precompute R[0..m]: R[i] = u^i
        std::vector<RawFloat> R(m + 1);
        for (int i = 0; i <= m; ++i) {
            R[i].d = arena.alloc_limbs(nw + 2);
            std::memset(R[i].d, 0, (nw + 2) * sizeof(uint64_t));
            R[i].nw = 0; R[i].exp = 0;
        }
        R[0].d[nw - 1] = uint64_t(1) << 63;
        R[0].nw = static_cast<size_t>(nw);
        R[0].exp = -static_cast<int64_t>(nw * 64 - 1);
        std::memcpy(R[1].d, x2.d, x2.nw * sizeof(uint64_t));
        R[1].nw = x2.nw; R[1].exp = x2.exp;
        // NTT cache of R[1]
        prime_ntt::NttCache sin_cache_r1;
        rf_sqr(R[2], R[1], prod_buf, scratch, nw);
        for (int i = 3; i <= m; ++i) {
            if ((i & 1) == 0) rf_sqr(R[i], R[i/2], prod_buf, scratch, nw);
            else rf_mul_cached(R[i], R[i-1], R[1], prod_buf, nw, sin_cache_r1);
        }

        // NTT cache of R[m] (reused in the giant steps)
        prime_ntt::NttCache sin_cache_rm;

        // rr = 1.0 (tracks |u^l / (2l+1)!|)
        RawFloat rr;
        rr.d = arena.alloc_limbs(nw + 2);
        std::memset(rr.d, 0, (nw + 2) * sizeof(uint64_t));
        rr.d[nw - 1] = uint64_t(1) << 63;
        rr.nw = static_cast<size_t>(nw);
        rr.exp = -static_cast<int64_t>(nw * 64 - 1);

        // t = baby step working space, tmp = temporary buffer for R[i]-t
        RawFloat t;
        t.d = arena.alloc_limbs(nw + 2);
        uint64_t* tmp_d = arena.alloc_limbs(nw + 2);

        // result = 0
        std::memset(result.d, 0, (nw + 2) * sizeof(uint64_t));
        result.nw = 0; result.exp = 0;

        int l = 0;
        int max_giant_steps = l_est / m + 5;

        for (int gs = 0; gs < max_giant_steps; ++gs) {
            // Baby step: evaluate Σ_{i=0}^{m-1} (-1)^i * u^i / (partial denominator product) via Horner
            // t = R[m-1]; for i=m-2..0: t /= (2(l+i+1))(2(l+i+1)+1); t = R[i] - t
            std::memcpy(t.d, R[m-1].d, R[m-1].nw * sizeof(uint64_t));
            t.nw = R[m-1].nw; t.exp = R[m-1].exp;

            for (int i = m - 2; i >= 0; --i) {
                uint64_t d1 = static_cast<uint64_t>(2*(l+i+1));
                uint64_t d2 = d1 + 1;
                rf_divmod_1(t, d1 * d2);
                if (t.nw == 0) {
                    std::memcpy(t.d, R[i].d, R[i].nw * sizeof(uint64_t));
                    t.nw = R[i].nw; t.exp = R[i].exp;
                } else {
                    // t = R[i] - t (alternating series sign handling)
                    std::memset(tmp_d, 0, (nw + 2) * sizeof(uint64_t));
                    std::memcpy(tmp_d, R[i].d, R[i].nw * sizeof(uint64_t));
                    RawFloat tmp{tmp_d, R[i].nw, R[i].exp};
                    rf_sub(tmp, t, nw + 1);
                    std::memcpy(t.d, tmp.d, tmp.nw * sizeof(uint64_t));
                    std::memset(t.d + tmp.nw, 0, (nw + 2 - tmp.nw) * sizeof(uint64_t));
                    t.nw = tmp.nw; t.exp = tmp.exp;
                }
            }

            // t *= rr (on the first giant step rr = 1, so no multiplication needed)
            if (gs > 0) {
                rf_mul(t, t, rr, prod_buf, scratch, nw);
            }
            if (t.nw == 0) break;

            // Add or subtract to result (depending on (-1)^l)
            if (l % 2 == 0) {
                rf_add(result, t, nw + 1);
            } else {
                rf_sub(result, t, nw + 1);
            }

            // Update rr: rr *= R[m] / Π_{j=0}^{m-1} ((2(l+j)+2)(2(l+j)+3))
            rf_mul_cached(rr, rr, R[m], prod_buf, nw, sin_cache_rm);
            for (int j = 0; j < m; ++j) {
                uint64_t d1 = static_cast<uint64_t>(2*(l+j) + 2);
                uint64_t d2 = d1 + 1;
                rf_divmod_1(rr, d1 * d2);
            }
            l += m;
            if (rr.nw == 0) break;
        }

        // sin(x) = x * S(u): result *= x
        rf_mul(result, result, x_rf, prod_buf, scratch, nw);
    }

    // Paterson-Stockmeyer for the cos Taylor series
    // cos(x) = Σ_{k=0}^{∞} (-1)^k * u^k / (2k)!  where u = x²
    static void rf_cos_ps(RawFloat& result, const RawFloat& x2,
                           int nw, uint64_t* prod_buf, uint64_t* scratch,
                           ScratchArena& arena, int working_precision) {
        int target_bits = nw * 64;
        int64_t u_msb = x2.exp + static_cast<int64_t>(x2.nw) * 64;
        int u_log2 = (u_msb <= 0) ? static_cast<int>(-u_msb) : 1;
        if (u_log2 < 1) u_log2 = 1;
        int l_est = estimate_sincos_terms(target_bits, u_log2);

        int m = static_cast<int>(std::sqrt(static_cast<double>(l_est)));
        if (m < 2) m = 2;
        if (m > 256) m = 256;

        // R[0..m] = u^i
        std::vector<RawFloat> R(m + 1);
        for (int i = 0; i <= m; ++i) {
            R[i].d = arena.alloc_limbs(nw + 2);
            std::memset(R[i].d, 0, (nw + 2) * sizeof(uint64_t));
            R[i].nw = 0; R[i].exp = 0;
        }
        R[0].d[nw - 1] = uint64_t(1) << 63;
        R[0].nw = static_cast<size_t>(nw);
        R[0].exp = -static_cast<int64_t>(nw * 64 - 1);
        std::memcpy(R[1].d, x2.d, x2.nw * sizeof(uint64_t));
        R[1].nw = x2.nw; R[1].exp = x2.exp;
        // NTT cache of R[1]
        prime_ntt::NttCache cos_cache_r1;
        rf_sqr(R[2], R[1], prod_buf, scratch, nw);
        for (int i = 3; i <= m; ++i) {
            if ((i & 1) == 0) rf_sqr(R[i], R[i/2], prod_buf, scratch, nw);
            else rf_mul_cached(R[i], R[i-1], R[1], prod_buf, nw, cos_cache_r1);
        }

        // NTT cache of R[m]
        prime_ntt::NttCache cos_cache_rm;

        RawFloat rr;
        rr.d = arena.alloc_limbs(nw + 2);
        std::memset(rr.d, 0, (nw + 2) * sizeof(uint64_t));
        rr.d[nw - 1] = uint64_t(1) << 63;
        rr.nw = static_cast<size_t>(nw);
        rr.exp = -static_cast<int64_t>(nw * 64 - 1);

        RawFloat t;
        t.d = arena.alloc_limbs(nw + 2);
        uint64_t* tmp_d = arena.alloc_limbs(nw + 2);

        std::memset(result.d, 0, (nw + 2) * sizeof(uint64_t));
        result.nw = 0; result.exp = 0;

        int l = 0;
        int max_giant_steps = l_est / m + 5;

        for (int gs = 0; gs < max_giant_steps; ++gs) {
            // Baby step: Horner
            // cos denominator: (2(l+i+1)-1)(2(l+i+1)) = (2l+2i+1)(2l+2i+2)
            std::memcpy(t.d, R[m-1].d, R[m-1].nw * sizeof(uint64_t));
            t.nw = R[m-1].nw; t.exp = R[m-1].exp;

            for (int i = m - 2; i >= 0; --i) {
                uint64_t d1 = static_cast<uint64_t>(2*(l+i+1) - 1);
                uint64_t d2 = d1 + 1;
                rf_divmod_1(t, d1 * d2);
                if (t.nw == 0) {
                    std::memcpy(t.d, R[i].d, R[i].nw * sizeof(uint64_t));
                    t.nw = R[i].nw; t.exp = R[i].exp;
                } else {
                    std::memset(tmp_d, 0, (nw + 2) * sizeof(uint64_t));
                    std::memcpy(tmp_d, R[i].d, R[i].nw * sizeof(uint64_t));
                    RawFloat tmp{tmp_d, R[i].nw, R[i].exp};
                    rf_sub(tmp, t, nw + 1);
                    std::memcpy(t.d, tmp.d, tmp.nw * sizeof(uint64_t));
                    std::memset(t.d + tmp.nw, 0, (nw + 2 - tmp.nw) * sizeof(uint64_t));
                    t.nw = tmp.nw; t.exp = tmp.exp;
                }
            }

            if (gs > 0) {
                rf_mul(t, t, rr, prod_buf, scratch, nw);
            }
            if (t.nw == 0) break;

            if (l % 2 == 0) {
                rf_add(result, t, nw + 1);
            } else {
                rf_sub(result, t, nw + 1);
            }

            // Update rr: rr *= R[m] / Π_{j=0}^{m-1} ((2(l+j)+1)(2(l+j)+2))
            rf_mul_cached(rr, rr, R[m], prod_buf, nw, cos_cache_rm);
            for (int j = 0; j < m; ++j) {
                uint64_t d1 = static_cast<uint64_t>(2*(l+j) + 1);
                uint64_t d2 = d1 + 1;
                rf_divmod_1(rr, d1 * d2);
            }
            l += m;
            if (rr.nw == 0) break;
        }
    }

    // Forward declarations: Q128/Q256 fast path (definitions come after q128_mul etc.)
    static Float exp_small(const Float& x, int precision);
    static Float exp_medium(const Float& x, int precision);
    static Float sin_core(Float x, int eff_x, int precision);
    static Float cos_core(Float x, int eff_x, int precision);

    // Fast Taylor series via direct mpn computation (zero allocation inside the loop)
    // Legacy interface compatible — for low-precision fallback
    static Float expTaylor(const Float& x, int working_precision) {
        int wp_bits = Float::precisionToBits(working_precision);
        int nw = (wp_bits + 63) / 64;

        ScratchScope scope;
        auto& arena = getThreadArena();

        // Pre-allocate buffers
        uint64_t* term_d   = arena.alloc_limbs(nw + 2);
        uint64_t* result_d = arena.alloc_limbs(nw + 2);
        size_t prod_alloc = 2 * static_cast<size_t>(nw) + 4;
        uint64_t* prod_buf = arena.alloc_limbs(prod_alloc);
        size_t scratch_sz  = mpn::multiply_scratch_size(nw + 1, nw + 1);
        uint64_t* scratch  = arena.alloc_limbs(scratch_sz > 0 ? scratch_sz : 1);

        // Extract the mantissa/exponent of x (normalized)
        RawFloat x_rf = rf_extract(x, nw, arena);
        bool x_negative = x.isNegative();

        // term = 1.0: mantissa = 2^(nw*64-1), exponent = -(nw*64-1)
        std::memset(term_d, 0, (nw + 2) * sizeof(uint64_t));
        term_d[nw - 1] = uint64_t(1) << 63;
        RawFloat term{term_d, static_cast<size_t>(nw), -static_cast<int64_t>(nw * 64 - 1)};

        // result = 1.0
        std::memset(result_d, 0, (nw + 2) * sizeof(uint64_t));
        result_d[nw - 1] = uint64_t(1) << 63;
        RawFloat result{result_d, static_cast<size_t>(nw), -static_cast<int64_t>(nw * 64 - 1)};

        int max_terms = static_cast<int>(working_precision * 1.2) + 10;

        for (int n = 1; n <= max_terms; ++n) {
            rf_mul(term, term, x_rf, prod_buf, scratch, nw);
            rf_divmod_1(term, static_cast<uint64_t>(n));
            if (term.nw == 0) break;
            // When x < 0, odd terms are negative (x^n/n! with n odd)
            if (x_negative && (n & 1)) {
                if (!rf_sub(result, term, nw + 1)) break;
            } else {
                if (!rf_add(result, term, nw + 1)) break;
            }
        }

        return rf_to_float(result, false, working_precision);
    }

    // exp computation by squaring: halve K times → Taylor → reconstruct by squaring K times
    // exp(x) = exp(x/2^K)^{2^K}
    // Taylor: switches between the naive method (low precision) and Paterson-Stockmeyer (high precision).
    // The squaring reconstruction is done with RawFloat to avoid Float overhead.
    static Float expDoubling(const Float& x, int working_precision) {
        int wp_bits = Float::precisionToBits(working_precision);

        // P-S threshold: use P-S + cuberoot K at ~2000 digits (7000 bits) or above
        // Below that, the PS baby step scalar cost (m × divmod_1 + m × rf_add
        // per giant step) outweighs the multiplication cost reduction, so naive Taylor is faster
        bool use_ps = (wp_bits >= 7000);
        int K;
        if (use_ps) {
            K = static_cast<int>(std::cbrt(4.0 * wp_bits));
        } else {
            // Cost-model optimization: K ≈ α·√B
            // Minimize the trade-off between Taylor term count ≈ B/(K+x_shift) vs K squaring reconstructions.
            // Accounting for sqr_cost ≈ 0.7×mul_cost gives K_opt = √(B/0.7) ≈ 1.2×√B.
            // However, at high precision the nw increase from guard bits (K+log2(K)+10) dominates, so
            // adjust the coefficient by precision: ≤2000 bits → 1.2, >2000 bits → 1.0
            double alpha = (wp_bits <= 2000) ? 1.2 : 1.0;
            K = static_cast<int>(alpha * std::sqrt(static_cast<double>(wp_bits)));
        }

        // Small-argument optimization: when |x| ≈ 2^{-B}, x is already small, so
        // K squaring reconstructions are unnecessary. Reduce K by B to save squaring cost.
        // Example: |x| ≈ 2^{-45} with K=51 → K=6, saving 45 squarings.
        // The Taylor/P-S term count is p/(B+K), and the larger B is, the fewer are needed.
        if (!x.isZero()) {
            int64_t msb_pos = x.exponent()
                            + static_cast<int64_t>(x.mantissa().bitLength());
            if (msb_pos < 0) {
                int arg_zeros = static_cast<int>(
                    std::min(-msb_pos, static_cast<int64_t>(K)));
                K -= arg_zeros;
            }
        }

        if (K < 1) return expTaylor(x, working_precision);

        // Guard bits: add K+α bits since the squaring reconstruction amplifies error by up to 2^K
        int guard_bits = K + static_cast<int>(std::ceil(std::log2(K + 1))) + 10;
        int wp_inner = working_precision + Float::bitsToPrecision(guard_bits);
        int wp_inner_bits = Float::precisionToBits(wp_inner);
        int nw = (wp_inner_bits + 63) / 64;

        Float x_red = ldexp(x, -K);  // x / 2^K — O(1) exponent operation
        x_red.truncateToApprox(wp_inner);
        x_red.setEffectiveBits(wp_inner_bits);

        ScratchScope scope;
        auto& arena = getThreadArena();

        RawFloat x_rf = rf_extract(x_red, nw, arena);
        bool x_negative = x.isNegative();

        size_t prod_alloc = 2 * static_cast<size_t>(nw) + 4;
        uint64_t* prod_buf = arena.alloc_limbs(prod_alloc);
        size_t mul_scratch = mpn::multiply_scratch_size(nw + 1, nw + 1);
        size_t sqr_scratch = mpn::square_scratch_size(nw + 1);
        size_t scratch_sz = std::max(mul_scratch, sqr_scratch);
        uint64_t* scratch_buf = arena.alloc_limbs(scratch_sz > 0 ? scratch_sz : 1);

        uint64_t* result_d = arena.alloc_limbs(nw + 2);
        RawFloat result{result_d, 0, 0};

        // Since exp_core guarantees x ≥ 0, x_negative is always false
        // (negative arguments are converted to x + k·ln2 inside exp_core)
        if (use_ps) {
            rf_exp_ps(result, x_rf, nw, prod_buf, scratch_buf, arena, wp_inner);
        } else {
            rf_exp_naive(result, x_rf, x_negative, nw, prod_buf, scratch_buf, arena, wp_inner);
        }

        // K squaring reconstructions: maintain full nw at every step
        //
        // The old implementation did gradual precision reduction (at step i, nw/2^(K-1-i) + guard),
        // but this rested on the mistaken assumption that "squaring doubles precision".
        // In reality rf_sqr truncates the output to needed_nw, so squaring only
        // maintains or reduces precision. Truncating to 13 limbs (832 bits) at an
        // early step caps the final accuracy at ~125 digits when K=97
        // (causes BUG_FLOAT_EXP_HIGH_PRECISION).
        //
        // Correct schedule: maintain nw at every step (each sqr has an error of about
        // target_bits + log2(K) ULP, so the margin is already covered by wp_inner,
        // which includes guard_bits = K + log2(K) + 10).
        for (int i = 0; i < K; ++i) {
            // Truncate only when result exceeds nw (keep the top nw limbs)
            if (static_cast<int>(result.nw) > static_cast<int>(nw)) {
                size_t drop = result.nw - nw;
                std::memmove(result.d, result.d + drop, nw * sizeof(uint64_t));
                result.nw = nw;
                result.exp += static_cast<int64_t>(drop) * 64;
            }
            rf_sqr(result, result, prod_buf, scratch_buf, nw);
        }

        Float out = rf_to_float(result, false, wp_inner);
        out.truncateToApprox(working_precision);
        return out;
    }

    // exp via Newton iteration: y_{n+1} = y_n · (1 + x - log(y_n))
    // log is AGM-based O(M(n)·log n), so with precision doubling the total cost ≈ 2 × log(n).
    // The argument x is already reduced (0 <= x < log2).
    static Float exp_newton(const Float& x, int working_precision) {
        // Initial approximation: double-precision exp
        double x_d = x.toDouble();
        Float y(std::exp(x_d));

        int target_bits = Float::precisionToBits(working_precision);

        // Precision-doubling loop: correct_bits doubles 53→106→212→... via Newton's quadratic convergence
        // Each step's computation precision = 2*correct_bits + guard (to compute the correction accurately)
        int correct_bits = 53;  // double-precision initial approximation
        int guard = 64;

        while (correct_bits < target_bits) {
            // Computation precision: 2*correct_bits is needed to compute the Newton correction
            int compute_bits = std::min(2 * correct_bits + guard, target_bits + guard);
            int step_prec = Float::bitsToPrecision(compute_bits);

            // Match effective_bits_ to this step's precision (see lessons-learned)
            y.setEffectiveBits(Float::precisionToBits(step_prec));
            y.setResultPrecision(step_prec);

            // Compute log(y) at this step's precision
            Float log_y = log(y, step_prec);

            // delta = x - log(y)
            Float delta = Float(x);
            delta.truncateToApprox(step_prec);
            delta = delta - log_y;

            // y = y * (1 + delta) = y + y * delta
            Float one_plus_delta = Float::one(step_prec) + delta;
            y = y * one_plus_delta;
            y.truncateToApprox(step_prec);

            // Quadratic convergence: correct_bits doubles (capped at the computation precision)
            correct_bits = std::min(2 * correct_bits, compute_bits);
        }

        y.truncateToApprox(working_precision);
        return y;
    }

    // Body: takes x by value (assumes already moved)
    static Float exp_core(Float x, int eff_x, int precision) {
        Float::PrecisionScope _ps(precision + 64);   // Compute exact÷exact inside the function at working precision
        int compute_prec = effectiveComputePrecision(eff_x, precision);
        int working_precision = compute_prec + 10;

        // Argument reduction: exp(x) = 2^k * exp(x - k*log2), x - k*log2 ∈ [0, log2)
        // double precision suffices for k (|k| < 1500, so exact in a 53-bit double)
        int k = 0;
        double x_d = x.toDouble();
        if (x_d > 0.5 || x_d < -0.5) {
            k = static_cast<int>(std::floor(x_d * 1.4426950408889634));
            Float log2_val = Float::log2(working_precision);
            x = x - k * log2_val;
            // Guarantee x ∈ [0, log2) (correction when rounding error crosses the boundary)
            if (x.isNegative()) {
                x = x + log2_val;
                k--;
            }
        } else if (x.isNegative()) {
            // |x| <= 0.5 and x < 0: exp(x) = exp(x + log2) / 2
            Float log2_val = Float::log2(working_precision);
            x = x + log2_val;
            k = -1;
        }
        x.truncateToApprox(working_precision);

        // High precision: Newton iteration (exp via log); low precision: Taylor + argument halving/squaring reconstruction
        // PS+squaring reconstruction is O(n^{1/3}·M(n)), exp_newton is O(log²n·M(n))
        // n^{1/3} < log²n holds for n > ~10^7 bits, so
        // in the practical range PS+squaring reconstruction is almost always faster
        int wp_bits = Float::precisionToBits(working_precision);
        Float result;
        if (wp_bits >= 100000) {
            result = exp_newton(x, working_precision);
        } else {
            result = expDoubling(x, working_precision);
        }

        if (k > 0) result <<= k;
        else if (k < 0) result >>= -k;

        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float exp(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return x.isNegative() ? Float::zero() : Float::positiveInfinity();
        if (x.isZero()) return Float::one(precision);
        if (x.isNegative() && x <= Float(-1000)) return Float::zero();
        if (!x.isNegative() && x >= Float(1000)) return Float::positiveInfinity();

        int precision_bits = Float::precisionToBits(precision);
        if (precision_bits <= 64) {
            return exp_small(x, precision);
        }

        return zivRound([](const Float& a, int e, int p) {
            return exp_core(Float(a), e, p);
        }, x, x.effectiveBits(), precision);
    }

    Float exp(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return x.isNegative() ? Float::zero() : Float::positiveInfinity();
        if (x.isZero()) return Float::one(precision);
        if (x.isNegative() && x <= Float(-1000)) return Float::zero();
        if (!x.isNegative() && x >= Float(1000)) return Float::positiveInfinity();

        int precision_bits = Float::precisionToBits(precision);
        if (precision_bits <= 64) {
            return exp_small(x, precision);
        }

        return zivRound([](const Float& a, int e, int p) {
            return exp_core(Float(a), e, p);
        }, x, x.effectiveBits(), precision);
    }

    //=============================================================================
    // Natural logarithm (log)
    //=============================================================================

    //-------------------------------------------------------------------------
    // Multi-prime argument reduction (Johansson 2022, PERF-LOG-D)
    //
    // log(x) = Σ c_i·log(p_i) + log(1+δ)
    // Find integer c_i via LLL lattice basis reduction, minimizing |δ|.
    // Reduces |δ| to about 2^{-50} → Halley's exp is fast for small arguments.
    //
    // Effect: at 2000-6000 bits (600-1800 digits), the Halley extension gives
    //         a 20-30% speedup over AGM. Even more effective once the cache is warm.
    //-------------------------------------------------------------------------

    // Forward declaration (defined in Float.cpp)
    Float computeAtanhReciprocal(int n, int precision);

    // Forward declarations (defined later)
    static Float log_core(Float x, int eff_x, int precision);
    struct LogfResult { Float logf; int64_t k; bool f_is_one; };
    static LogfResult logf_newton_core(Float x, int compute_prec);

    // Compute the log of small primes via the atanh BS formula
    // log(p) = k·log(2) + 2·atanh(1/n)  (k, n depending on p)
    // atanh(1/n) = (1/n)·Σ (1/n²)^k/(2k+1) — fast computation via BS
    struct LogPrimeFormula {
        int prime;
        int log2_coeff;     // coefficient of log(2)
        int log3_coeff;     // coefficient of log(3)
        int log5_coeff;     // coefficient of log(5)
        int log7_coeff;     // coefficient of log(7)
        int log11_coeff;    // coefficient of log(11)
        int atanh_n;        // n in atanh(1/n) (0 = no atanh needed)
        int atanh_coeff;    // coefficient of atanh(1/n)
    };

    // Prime → atanh identity table
    // log(2) = 2·atanh(1/3)
    // log(3) = log(2) + 2·atanh(1/5)      ∵ atanh(1/5)=(1/2)·log(3/2)
    // log(5) = 2·log(2) + 2·atanh(1/9)    ∵ atanh(1/9)=(1/2)·log(5/4)
    // log(7) = 3·log(2) - 2·atanh(1/15)   ∵ atanh(1/15)=(1/2)·log(8/7)
    // log(11)= log(2)+log(5) + 2·atanh(1/21) ∵ atanh(1/21)=(1/2)·log(11/10)
    // log(13)= 2·log(2)+log(3) + 2·atanh(1/25) ∵ atanh(1/25)=(1/2)·log(13/12)
    // log(17)= 4·log(2) + 2·atanh(1/33)   ∵ atanh(1/33)=(1/2)·log(17/16)
    // log(19)= log(2)+2·log(3) + 2·atanh(1/37) ∵ atanh(1/37)=(1/2)·log(19/18)
    // log(23)= 3·log(2)+log(3) - 2·atanh(1/47) ∵ atanh(1/47)=(1/2)·log(24/23)
    // log(29)= 2·log(2)+log(7) + 2·atanh(1/57) ∵ atanh(1/57)=(1/2)·log(29/28)
    static constexpr int MP_NUM_PRIMES = 10;
    static constexpr int MP_PRIMES[MP_NUM_PRIMES] =
        {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};

    // thread_local cache: log(p_i)
    struct LogPrimeCacheEntry { Float value; int precision; };
    static LogPrimeCacheEntry& logPrimeCacheAt(int index) {
        static thread_local LogPrimeCacheEntry cache[MP_NUM_PRIMES] = {};
        return cache[index];
    }

    // Compute log(prime) via atanh identities (reusing already-computed log(2), log(3), etc.)
    static Float computeLogPrime(int index, int precision) {
        int wp = precision + 15;
        switch (MP_PRIMES[index]) {
        case 2:
            return Float::log2(wp);
        case 3: {
            // log(3) = log(2) + 2·atanh(1/5)
            Float a5 = computeAtanhReciprocal(5, wp);
            return Float::log2(wp) + ldexp(a5, 1);
        }
        case 5: {
            // log(5) = 2·log(2) + 2·atanh(1/9)
            Float a9 = computeAtanhReciprocal(9, wp);
            return ldexp(Float::log2(wp), 1) + ldexp(a9, 1);
        }
        case 7: {
            // log(7) = 3·log(2) - 2·atanh(1/15)
            Float a15 = computeAtanhReciprocal(15, wp);
            return mulScalarF(Float::log2(wp), uint64_t(3)) - ldexp(a15, 1);
        }
        case 11: {
            // log(11) = log(10) + 2·atanh(1/21)
            //         = log(2) + log(5) + 2·atanh(1/21)
            Float a21 = computeAtanhReciprocal(21, wp);
            Float log5 = computeLogPrime(2, wp); // index 2 = prime 5
            return Float::log2(wp) + log5 + ldexp(a21, 1);
        }
        case 13: {
            // log(13) = 2·log(2) + log(3) + 2·atanh(1/25)
            Float a25 = computeAtanhReciprocal(25, wp);
            Float log3 = computeLogPrime(1, wp);
            return ldexp(Float::log2(wp), 1) + log3 + ldexp(a25, 1);
        }
        case 17: {
            // log(17) = 4·log(2) + 2·atanh(1/33)
            Float a33 = computeAtanhReciprocal(33, wp);
            return ldexp(Float::log2(wp), 2) + ldexp(a33, 1);
        }
        case 19: {
            // log(19) = log(2) + 2·log(3) + 2·atanh(1/37)
            Float a37 = computeAtanhReciprocal(37, wp);
            Float log3 = computeLogPrime(1, wp);
            return Float::log2(wp) + ldexp(log3, 1) + ldexp(a37, 1);
        }
        case 23: {
            // log(23) = 3·log(2) + log(3) - 2·atanh(1/47)
            Float a47 = computeAtanhReciprocal(47, wp);
            Float log3 = computeLogPrime(1, wp);
            return mulScalarF(Float::log2(wp), uint64_t(3)) + log3 - ldexp(a47, 1);
        }
        case 29: {
            // log(29) = 2·log(2) + log(7) + 2·atanh(1/57)
            Float a57 = computeAtanhReciprocal(57, wp);
            Float log7 = computeLogPrime(3, wp);
            return ldexp(Float::log2(wp), 1) + log7 + ldexp(a57, 1);
        }
        default:
            // fallback: AGM
            return log_core(Float(MP_PRIMES[index]), 64, wp);
        }
    }

    // Cached retrieval of log(p_i)
    static const Float& getLogPrime(int index, int precision) {
        auto& entry = logPrimeCacheAt(index);
        if (entry.precision > 0 && precision <= entry.precision) {
            return entry.value;
        }
        entry.value = computeLogPrime(index, precision);
        entry.value.setResultPrecision(precision);
        entry.precision = precision;
        return entry.value;
    }

    //-------------------------------------------------------------------------
    // LLL lattice basis reduction (low dimension, floating point)
    //   Find coefficients c_i that approximate log(x) as an integer linear combination of log(p_i).
    //   Lattice: b_i = (e_i, C·log(p_i)) ∈ R^{K+1}, i=0,...,K-1
    //   Target: t = (0,...,0, C·log(x))
    //   Find the nearest lattice point via Babai's nearest-plane method; components 0..K-1 = c_i
    //-------------------------------------------------------------------------
    static constexpr int MP_DIM = MP_NUM_PRIMES + 1; // 11

    struct MultiPrimeLattice {
        int64_t basis[MP_NUM_PRIMES][MP_DIM];
        double gs[MP_NUM_PRIMES][MP_DIM];     // Gram-Schmidt orthogonal basis
        double gs_norm2[MP_NUM_PRIMES];       // ||b*_i||²
        bool ready = false;

        void init() {
            if (ready) return;
            constexpr double SCALE = static_cast<double>(1LL << 52);

            // Initial basis: b_i = (e_i, round(SCALE·log(p_i)))
            for (int i = 0; i < MP_NUM_PRIMES; i++) {
                for (int j = 0; j < MP_DIM; j++) basis[i][j] = 0;
                basis[i][i] = 1;
                basis[i][MP_NUM_PRIMES] =
                    static_cast<int64_t>(std::llround(
                        SCALE * std::log(static_cast<double>(MP_PRIMES[i]))));
            }

            lll();
            computeGS();
            ready = true;
        }

        // Babai nearest-plane method: returns the coordinates of the lattice point nearest to log(x)
        void findCoeffs(double log_x, int64_t coeffs[MP_NUM_PRIMES]) const {
            constexpr double SCALE = static_cast<double>(1LL << 52);
            double target[MP_DIM] = {};
            target[MP_NUM_PRIMES] = SCALE * log_x;

            // Babai: orthogonal projection → rounding → subtraction
            double b[MP_DIM];
            for (int j = 0; j < MP_DIM; j++) b[j] = target[j];

            int64_t d[MP_NUM_PRIMES];
            for (int i = MP_NUM_PRIMES - 1; i >= 0; i--) {
                double dot = 0;
                for (int j = 0; j < MP_DIM; j++) dot += b[j] * gs[i][j];
                d[i] = std::llround(dot / gs_norm2[i]);
                for (int j = 0; j < MP_DIM; j++)
                    b[j] -= d[i] * static_cast<double>(basis[i][j]);
            }

            // Components 0..K-1 of the lattice point = Σ d_i · basis[i] are c_i
            for (int j = 0; j < MP_NUM_PRIMES; j++) {
                int64_t sum = 0;
                for (int i = 0; i < MP_NUM_PRIMES; i++)
                    sum += d[i] * basis[i][j];
                coeffs[j] = sum;
            }
        }

    private:
        void computeGS() {
            for (int i = 0; i < MP_NUM_PRIMES; i++) {
                for (int j = 0; j < MP_DIM; j++)
                    gs[i][j] = static_cast<double>(basis[i][j]);
                for (int k = 0; k < i; k++) {
                    double dot = 0;
                    for (int j = 0; j < MP_DIM; j++)
                        dot += static_cast<double>(basis[i][j]) * gs[k][j];
                    double mu = dot / gs_norm2[k];
                    for (int j = 0; j < MP_DIM; j++)
                        gs[i][j] -= mu * gs[k][j];
                }
                gs_norm2[i] = 0;
                for (int j = 0; j < MP_DIM; j++)
                    gs_norm2[i] += gs[i][j] * gs[i][j];
            }
        }

        void lll() {
            constexpr double DELTA = 0.75;
            double mu_mat[MP_NUM_PRIMES][MP_NUM_PRIMES] = {};
            double Bstar[MP_NUM_PRIMES][MP_DIM];
            double Bnorm[MP_NUM_PRIMES];

            auto recomputeGS = [&]() {
                for (int i = 0; i < MP_NUM_PRIMES; i++) {
                    for (int j = 0; j < MP_DIM; j++)
                        Bstar[i][j] = static_cast<double>(basis[i][j]);
                    for (int k = 0; k < i; k++) {
                        double dot = 0;
                        for (int j = 0; j < MP_DIM; j++)
                            dot += static_cast<double>(basis[i][j])
                                 * Bstar[k][j];
                        mu_mat[i][k] = dot / Bnorm[k];
                        for (int j = 0; j < MP_DIM; j++)
                            Bstar[i][j] -= mu_mat[i][k] * Bstar[k][j];
                    }
                    Bnorm[i] = 0;
                    for (int j = 0; j < MP_DIM; j++)
                        Bnorm[i] += Bstar[i][j] * Bstar[i][j];
                }
            };

            recomputeGS();
            int k = 1;
            while (k < MP_NUM_PRIMES) {
                // Size reduction
                for (int j = k - 1; j >= 0; j--) {
                    if (std::abs(mu_mat[k][j]) > 0.5) {
                        int64_t r = std::llround(mu_mat[k][j]);
                        for (int l = 0; l < MP_DIM; l++)
                            basis[k][l] -= r * basis[j][l];
                        recomputeGS();
                    }
                }
                // Lovász condition
                double lhs = DELTA * Bnorm[k - 1];
                double rhs = Bnorm[k]
                           + mu_mat[k][k-1] * mu_mat[k][k-1] * Bnorm[k-1];
                if (lhs > rhs) {
                    for (int l = 0; l < MP_DIM; l++)
                        std::swap(basis[k][l], basis[k - 1][l]);
                    recomputeGS();
                    k = std::max(k - 1, 1);
                } else {
                    k++;
                }
            }
        }
    };

    static MultiPrimeLattice& getMultiPrimeLattice() {
        static MultiPrimeLattice lattice;
        if (!lattice.ready) lattice.init();
        return lattice;
    }

    // log computation via multi-prime argument reduction
    // log(x) = Σ c_i·log(p_i) + log(r),  r = x · Π p_i^{-c_i} ≈ 1
    static Float log_multiprime(Float x, int eff_x, int precision) {
        auto& lattice = getMultiPrimeLattice();
        int compute_prec = effectiveComputePrecision(eff_x, precision);
        int wp = compute_prec + 20;

        // 1. double approximation of log(x) (avoiding overflow)
        int64_t binary_exp = x.exponent()
                           + static_cast<int64_t>(x.mantissa().bitLength());
        double log_x;
        if (binary_exp > 500 || binary_exp < -500) {
            // Large exponent: log(mantissa) + exp·log(2)
            Float normalized = ldexp(x, static_cast<int>(-binary_exp));
            log_x = std::log(normalized.toDouble())
                   + static_cast<double>(binary_exp) * std::log(2.0);
        } else {
            log_x = std::log(x.toDouble());
        }

        // 2. Find coefficients c_i via LLL + Babai
        int64_t coeffs[MP_NUM_PRIMES];
        lattice.findCoeffs(log_x, coeffs);

        // 3. Compute r = x · Π p_i^{-c_i} at high precision
        Float r(x);
        r.setResultPrecision(wp);
        for (int i = 0; i < MP_NUM_PRIMES; i++) {
            if (coeffs[i] == 0) continue;
            if (MP_PRIMES[i] == 2) {
                r = ldexp(std::move(r), static_cast<int>(-coeffs[i]));
            } else {
                Int pk(1);
                for (int64_t j = 0; j < std::abs(coeffs[i]); j++)
                    pk = pk * Int(MP_PRIMES[i]);
                Float pk_f(pk);
                pk_f.setResultPrecision(wp);
                if (coeffs[i] > 0) {
                    r = r / pk_f;
                } else {
                    r = r * pk_f;
                }
            }
        }

        // 4. Compute log(r) (r ≈ 1, |r-1| ≈ 2^{-50})
        //    Since r ≈ 1, Halley is efficient:
        //    - exp(y) has a small argument, so expDoubling's K is automatically reduced
        //    - Q256 initial approximation → converges in 2-3 Halley iterations
        Float log_r;
        if (r == Float::one()) {
            log_r = Float::zero();
        } else {
            auto [logf, k_val, f_is_one] = logf_newton_core(std::move(r), wp);
            if (f_is_one) {
                if (k_val == 0) {
                    log_r = Float::zero();
                } else {
                    log_r = Float::log2(wp) * k_val;
                }
            } else {
                log_r = std::move(logf);
                if (k_val != 0) {
                    Float log2_val = Float::log2(wp);
                    log_r = log_r + log2_val * k_val;
                }
            }
            log_r.setResultPrecision(wp);
        }

        // 5. log(x) = Σ c_i·log(p_i) + log(r)
        Float result = std::move(log_r);
        for (int i = 0; i < MP_NUM_PRIMES; i++) {
            if (coeffs[i] == 0) continue;
            const Float& log_p = getLogPrime(i, wp);
            Float contrib = log_p * coeffs[i];
            contrib.setResultPrecision(wp);
            result = result + contrib;
        }

        finalizeResult(result, eff_x, precision);
        return result;
    }

    // log computation via the AGM method
    //   log(x) = π / (2 · AGM(1, 4/s)) − m · log(2)
    //   where s = x · 2^m > 2^{p/2}
    // Each AGM step is mul + sqrt = O(M(n)), over O(log n) steps.
    // Much lighter than Newton's method (each step has exp = O(M(n)·n^{1/3})).
    static Float log_core(Float x, int eff_x, int precision) {
        int compute_prec = effectiveComputePrecision(eff_x, precision);

        // Binary exponent of x: x ∈ [2^{ea-1}, 2^{ea})
        int64_t ea = x.exponent() + static_cast<int64_t>(x.mantissa().bitLength());

        // Estimate guard bits for cancellation
        // term1 = π/(2·AGM) ≈ log(x) + m·log(2),  term2 = m·log(2)
        // cancel ≈ log2(|term1| / |log(x)|)
        double log_est = std::log(x.toDouble());
        int target_bits = static_cast<int>(std::ceil(compute_prec * 3.32192809488736));
        int64_t m_est = (target_bits + 64) / 2 - ea;
        int cancel_bits = 0;
        if (std::abs(log_est) > 1e-300) {
            double term1_mag = std::abs(static_cast<double>(m_est)) * 0.6931472
                             + std::abs(log_est);
            double ratio = term1_mag / std::abs(log_est);
            if (ratio > 1.0) {
                cancel_bits = static_cast<int>(std::ceil(std::log2(ratio)));
            }
        } else {
            // x is extremely close to 1 → cancellation is huge. Add 200 guard bits.
            cancel_bits = 200;
        }

        // Guard bits: MPFR style + extra margin
        // Accounting for the second term 4/s² ≈ 4 ulp, rounding error ≈ 7 ulp, and uncertainty in the cancel estimate
        int q_bits = Float::precisionToBits(compute_prec);
        int log2_q = static_cast<int>(std::ceil(std::log2(q_bits)));
        int p_bits = q_bits + 3 * log2_q + 20 + cancel_bits;
        int working_precision = Float::bitsToPrecision(p_bits);

        // m: s = x · 2^m,  s > 2^{p/2}
        int64_t m = (p_bits + 3) / 2 - ea;

        // s = x · 2^m (exponent shift only, O(1))
        Float s = ldexp(Float(x), static_cast<int>(m));
        s.setResultPrecision(working_precision);

        // 4/s
        Float four = Float::one(working_precision);
        four <<= 2;  // Create 4 at working_precision
        Float four_over_s = four / s;
        four_over_s.setResultPrecision(working_precision);

        // AGM(1, 4/s)
        Float ag = agm(Float::one(working_precision),
                       std::move(four_over_s), working_precision);

        // log(x) = π / (2 · AGM) − m · log(2)
        Float pi_val = Float::pi(working_precision);
        Float two_ag = ldexp(std::move(ag), 1);
        Float term1 = pi_val / two_ag;

        Float log2_val = Float::log2(working_precision);
        Float m_float = Float(m);
        m_float.setResultPrecision(working_precision);
        Float term2 = log2_val * m_float;

        Float result = term1 - term2;

        finalizeResult(result, eff_x, precision);
        return result;
    }

    // Q128 multiply: returns the high 128 bits of (a·b) >> 128
    static void q128_mul(uint64_t a_hi, uint64_t a_lo,
                         uint64_t b_hi, uint64_t b_lo,
                         uint64_t& r_hi, uint64_t& r_lo) {
        uint64_t ll_hi, lh_hi, hl_hi, hh_hi;
        /*ll_lo*/ _umul128(a_lo, b_lo, &ll_hi);
        uint64_t lh_lo = _umul128(a_lo, b_hi, &lh_hi);
        uint64_t hl_lo = _umul128(a_hi, b_lo, &hl_hi);
        uint64_t hh_lo = _umul128(a_hi, b_hi, &hh_hi);

        // p1 = ll_hi + lh_lo + hl_lo (carry → p2)
        uint64_t p1 = ll_hi;
        unsigned char c1 = _addcarry_u64(0, p1, lh_lo, &p1);
        unsigned char c2 = _addcarry_u64(0, p1, hl_lo, &p1);
        uint64_t carry_p2 = static_cast<uint64_t>(c1) + static_cast<uint64_t>(c2);

        // p2 = hh_lo + lh_hi + hl_hi + carry_p2
        uint64_t p2 = hh_lo;
        unsigned char c3 = _addcarry_u64(0, p2, lh_hi, &p2);
        unsigned char c4 = _addcarry_u64(0, p2, hl_hi, &p2);
        unsigned char c5 = _addcarry_u64(0, p2, carry_p2, &p2);
        uint64_t carry_p3 = static_cast<uint64_t>(c3) + static_cast<uint64_t>(c4)
                          + static_cast<uint64_t>(c5);

        r_hi = hh_hi + carry_p3;
        r_lo = p2;
    }

    // =========================================================================
    // Q256 fixed-point arithmetic (256-bit, 4 × uint64_t, w[0]=LSW, w[3]=MSW)
    // =========================================================================

    // Q256 multiply: returns the high 256 bits of (a·b) >> 256
    // schoolbook 4×4 limb multiply, upper half
    static void q256_mul(const uint64_t a[4], const uint64_t b[4], uint64_t r[4]) {
        uint64_t w[8] = {};
        for (int i = 0; i < 4; i++) {
            uint64_t carry = 0;
            for (int j = 0; j < 4; j++) {
                uint64_t hi;
                uint64_t lo = _umul128(a[i], b[j], &hi);
                unsigned char c1 = _addcarry_u64(0, w[i+j], lo, &w[i+j]);
                unsigned char c2 = _addcarry_u64(0, w[i+j], carry, &w[i+j]);
                carry = hi + static_cast<uint64_t>(c1) + static_cast<uint64_t>(c2);
            }
            if (i + 4 < 8) w[i + 4] += carry;
        }
        r[0] = w[4]; r[1] = w[5]; r[2] = w[6]; r[3] = w[7];
    }

    // Q256 add: r = a + b, return carry
    static unsigned char q256_add(const uint64_t a[4], const uint64_t b[4], uint64_t r[4]) {
        unsigned char c = _addcarry_u64(0, a[0], b[0], &r[0]);
        c = _addcarry_u64(c, a[1], b[1], &r[1]);
        c = _addcarry_u64(c, a[2], b[2], &r[2]);
        c = _addcarry_u64(c, a[3], b[3], &r[3]);
        return c;
    }

    // Q256 subtract: r = a - b, return borrow
    static unsigned char q256_sub(const uint64_t a[4], const uint64_t b[4], uint64_t r[4]) {
        unsigned char c = _subborrow_u64(0, a[0], b[0], &r[0]);
        c = _subborrow_u64(c, a[1], b[1], &r[1]);
        c = _subborrow_u64(c, a[2], b[2], &r[2]);
        c = _subborrow_u64(c, a[3], b[3], &r[3]);
        return c;
    }

    // Q256 left shift by 1 bit
    static void q256_shl1(const uint64_t a[4], uint64_t r[4]) {
        r[0] = a[0] << 1;
        r[1] = (a[1] << 1) | (a[0] >> 63);
        r[2] = (a[2] << 1) | (a[1] >> 63);
        r[3] = (a[3] << 1) | (a[2] >> 63);
    }

    // Q256 scalar division: a / divisor (in-place)
    static void q256_div_scalar(uint64_t a[4], uint64_t divisor) {
        uint64_t rem = 0;
        a[3] = _udiv128(rem, a[3], divisor, &rem);
        a[2] = _udiv128(rem, a[2], divisor, &rem);
        a[1] = _udiv128(rem, a[1], divisor, &rem);
        a[0] = _udiv128(rem, a[0], divisor, &rem);
    }

    // Determine whether Q256 is zero
    static bool q256_is_zero(const uint64_t a[4]) {
        return a[0] == 0 && a[1] == 0 && a[2] == 0 && a[3] == 0;
    }

    // Q256 compare: a >= b
    static bool q256_ge(const uint64_t a[4], const uint64_t b[4]) {
        if (a[3] != b[3]) return a[3] > b[3];
        if (a[2] != b[2]) return a[2] > b[2];
        if (a[1] != b[1]) return a[1] > b[1];
        return a[0] >= b[0];
    }

    // Q256 → Float conversion
    static Float q256_to_float(const uint64_t q[4], int64_t exponent) {
        // Find the highest non-zero word
        int top = 3;
        while (top >= 0 && q[top] == 0) top--;
        if (top < 0) return Float::zero();

        int nw = top + 1;
        auto val = Int::fromRawWordsPreNormalized(
            std::span<const uint64_t>(q, nw), 1);
        return Float(std::move(val), exponent, false);
    }

    // double → Q256 conversion
    [[maybe_unused]] static void double_to_q256(double val, uint64_t q[4]) {
        q[0] = q[1] = q[2] = q[3] = 0;
        if (val == 0.0) return;
        int exp;
        double m = std::frexp(val, &exp);
        uint64_t m_int = static_cast<uint64_t>(m * static_cast<double>(1ULL << 53));
        // Q256: val * 2^256 = m_int * 2^(exp + 203)
        int bit_pos = exp + 203;
        int word_idx = bit_pos / 64;
        int bit_in_word = bit_pos % 64;
        if (word_idx >= 0 && word_idx < 4) {
            if (bit_in_word + 53 <= 64) {
                q[word_idx] = m_int << bit_in_word;
            } else {
                q[word_idx] = m_int << bit_in_word;
                if (word_idx + 1 < 4)
                    q[word_idx + 1] = m_int >> (64 - bit_in_word);
            }
        } else if (word_idx == 4 && bit_in_word == 0) {
            // Overflow: val ≈ 1, shouldn't happen for atan args
        }
    }

    // Float → Q256 conversion (places the high 256 bits of the mantissa, x ∈ (0, 1))
    static void float_to_q256(const Float& x, uint64_t q[4]) {
        const Int& mant = x.mantissa();
        const uint64_t* mw = mant.data();
        size_t mn = mant.size();
        int64_t e = x.exponent();

        uint64_t top = mw[mn - 1];
        int top_bits = 64 - std::countl_zero(top);
        int64_t total_bl = static_cast<int64_t>((mn - 1) * 64) + top_bits;

        // Normalize the mantissa (place the MSB at bit 255)
        int clz = 64 - top_bits;
        uint64_t m[4] = {};
        for (int i = 0; i < 4 && i < static_cast<int>(mn); i++) {
            int src = static_cast<int>(mn) - 1 - i;
            m[3 - i] = mw[src];
        }
        // Left-shift by clz
        if (clz > 0) {
            for (int i = 3; i > 0; i--)
                m[i] = (m[i] << clz) | (m[i - 1] >> (64 - clz));
            m[0] <<= clz;
        }

        // Q256 = x * 2^256 = m[3:0] * 2^(e + total_bl)
        int64_t sr = -(e + total_bl);
        if (sr <= 0) {
            q[0] = m[0]; q[1] = m[1]; q[2] = m[2]; q[3] = m[3];
        } else if (sr < 64) {
            q[0] = (m[0] >> sr) | (m[1] << (64 - sr));
            q[1] = (m[1] >> sr) | (m[2] << (64 - sr));
            q[2] = (m[2] >> sr) | (m[3] << (64 - sr));
            q[3] = m[3] >> sr;
        } else if (sr < 128) {
            int s = static_cast<int>(sr - 64);
            q[0] = (m[1] >> s) | (m[2] << (64 - s));
            q[1] = (m[2] >> s) | (m[3] << (64 - s));
            q[2] = m[3] >> s;
            q[3] = 0;
        } else {
            q[0] = q[1] = q[2] = q[3] = 0;
        }
    }

    // Q256 division: compute t = u / (2^128 + u) in Q256 (Knuth Algorithm D, 3-word divisor)
    // u = (u_hi:u_lo), u_hi < 2^63 (MSB clear)
    // Result: returns t ∈ [0, 1/3) in Q256
    static void q256_atanh_arg(const uint64_t u[4], uint64_t q[4]) {
        // t = u / (2^256 + u) = (f-1)/(f+1) in Q256
        // u: 255 bits (MSB of mantissa cleared), u[3] < 2^63
        // Divisor v = 2^256 + u (257 bits) = {u[0], u[1], u[2], u[3], 1} (LE 5 words)
        // Normalize: left-shift by 63 bits to set the MSB of d[4]
        uint64_t d[5];
        d[4] = (1ULL << 63) | (u[3] >> 1);
        d[3] = (u[3] << 63) | (u[2] >> 1);
        d[2] = (u[2] << 63) | (u[1] >> 1);
        d[1] = (u[1] << 63) | (u[0] >> 1);
        d[0] = u[0] << 63;

        // Dividend: u × 2^256 × 2^63 (normalized) in 9 words
        uint64_t n[9] = {};
        n[4] = u[0] << 63;
        n[5] = (u[1] << 63) | (u[0] >> 1);
        n[6] = (u[2] << 63) | (u[1] >> 1);
        n[7] = (u[3] << 63) | (u[2] >> 1);
        n[8] = u[3] >> 1;

        // Algorithm D: 9 word ÷ 5 word → 4-word quotient
        for (int i = 3; i >= 0; i--) {
            uint64_t q_hat, r_hat;
            bool skip_correct = false;

            // Knuth D3: when n[i+5] >= d[4], _udiv128 overflows
            if (n[i + 5] >= d[4]) {
                q_hat = UINT64_MAX;
                r_hat = n[i + 4] + d[4];
                if (r_hat < d[4]) {
                    skip_correct = true;
                }
            } else {
                q_hat = _udiv128(n[i + 5], n[i + 4], d[4], &r_hat);
            }

            // Knuth's 2-word correction
            if (!skip_correct) {
                for (;;) {
                    uint64_t ph, pl;
                    pl = _umul128(q_hat, d[3], &ph);
                    if (ph < r_hat || (ph == r_hat && pl <= n[i + 3]))
                        break;
                    q_hat--;
                    r_hat += d[4];
                    if (r_hat < d[4]) break;
                }
            }

            // Compute q_hat × d and subtract from n[i..i+5]
            uint64_t prod[6];
            {
                uint64_t carry = 0;
                for (int j = 0; j < 5; j++) {
                    uint64_t hi;
                    uint64_t lo = _umul128(q_hat, d[j], &hi);
                    unsigned char c = _addcarry_u64(0, lo, carry, &prod[j]);
                    carry = hi + c;
                }
                prod[5] = carry;
            }

            unsigned char borrow = 0;
            for (int j = 0; j < 6; j++) {
                borrow = _subborrow_u64(borrow, n[i + j], prod[j], &n[i + j]);
            }

            if (borrow) {
                q_hat--;
                unsigned char c = 0;
                for (int j = 0; j < 5; j++) {
                    c = _addcarry_u64(c, n[i + j], d[j], &n[i + j]);
                }
                n[i + 5] += c;
            }

            q[i] = q_hat;
        }
    }

    // Q128 → Float conversion helper
    static Float q128_to_float(uint64_t hi, uint64_t lo, int64_t exponent) {
        if (hi != 0) {
            uint64_t words[2] = { lo, hi };
            auto val = Int::fromRawWordsPreNormalized(
                std::span<const uint64_t>(words, 2), 1);
            return Float(std::move(val), exponent, false);
        } else if (lo != 0) {
            return Float(Int(lo), exponent, false);
        }
        return Float::zero();
    }

    // Compute log(f) via the Q128 fixed-point atanh series (1-word mantissa)
    // x = f·2^k, f ∈ [1, 2), log(f) = 2·atanh((f-1)/(f+1))
    // Return values: logf_hi:logf_lo (Q128), k (binary exponent)
    // For f = 1.0, returns logf = 0
    static void compute_logf_q128(const Float& x,
                                  uint64_t& logf_hi, uint64_t& logf_lo,
                                  int64_t& k) {
        const Int& mant = x.mantissa();
        const uint64_t* mw = mant.data();
        size_t mn = mant.size();
        int64_t e = x.exponent();

        // Get 64 bits from the most significant word of the mantissa (little-endian: MSW = mw[mn-1])
        uint64_t top = mw[mn - 1];
        int top_bits = 64 - std::countl_zero(top);
        int64_t total_bl = static_cast<int64_t>((mn - 1) * 64) + top_bits;

        k = e + total_bl - 1;

        // m_norm: normalize so the MSB lands at bit 63
        int clz = 64 - top_bits;
        uint64_t m_norm;
        if (clz == 0) {
            m_norm = top;
        } else if (mn >= 2) {
            m_norm = (top << clz) | (mw[mn - 2] >> (64 - clz));
        } else {
            m_norm = top << clz;
        }
        uint64_t u_int = m_norm & 0x7FFFFFFFFFFFFFFFULL;

        if (u_int == 0) {
            logf_hi = logf_lo = 0;
            return;
        }

        // v_half = floor(v_int / 2), v_int = m_norm + 2^63 (65 bit)
        uint64_t v_half = (m_norm >> 1) + (1ULL << 62);

        // t = u/v in Q128: t·2^128 ≈ u_int·2^127 / v_half
        uint64_t num_hi = u_int >> 1;
        uint64_t num_lo = u_int << 63;

        uint64_t rem1;
        uint64_t t_hi = _udiv128(num_hi, num_lo, v_half, &rem1);
        uint64_t rem2;
        uint64_t t_lo = _udiv128(rem1, 0, v_half, &rem2);

        // t² in Q128
        uint64_t tsq_hi, tsq_lo;
        q128_mul(t_hi, t_lo, t_hi, t_lo, tsq_hi, tsq_lo);

        // Compute atanh(t) = t + t³/3 + t⁵/5 + ... in Q128
        uint64_t sum_hi = t_hi, sum_lo = t_lo;
        uint64_t pow_hi = t_hi, pow_lo = t_lo;

        for (uint64_t d = 3; d < 200; d += 2) {
            q128_mul(pow_hi, pow_lo, tsq_hi, tsq_lo, pow_hi, pow_lo);

            uint64_t r;
            uint64_t term_hi = _udiv128(0, pow_hi, d, &r);
            uint64_t term_lo = _udiv128(r, pow_lo, d, &r);

            if (term_hi == 0 && term_lo == 0) break;

            unsigned char carry = _addcarry_u64(0, sum_lo, term_lo, &sum_lo);
            _addcarry_u64(carry, sum_hi, term_hi, &sum_hi);
        }

        // log(f) = 2·atanh(t)
        logf_hi = (sum_hi << 1) | (sum_lo >> 63);
        logf_lo = sum_lo << 1;
    }

    // Compute log(f) via the Q256 fixed-point atanh series (2-word mantissa)
    // x = f·2^k, f ∈ [1, 2), log(f) = 2·atanh((f-1)/(f+1))
    // Return values: logf[4] (Q256), k (binary exponent)
    static void compute_logf_q256(const Float& x,
                                  uint64_t logf[4], int64_t& k) {
        const Int& mant = x.mantissa();
        const uint64_t* mw = mant.data();
        size_t mn = mant.size();
        int64_t e = x.exponent();

        uint64_t top = mw[mn - 1];
        int top_bits = 64 - std::countl_zero(top);
        int64_t total_bl = static_cast<int64_t>((mn - 1) * 64) + top_bits;
        k = e + total_bl - 1;

        // Get the high 256 bits of the mantissa (MSB = bit 255)
        // m[3]:m[2]:m[1]:m[0] — MSB-aligned (left-shifted by clz)
        int clz = 64 - top_bits;
        uint64_t m[4] = {};

        // Fill from the high words: left-shift mw[mn-1..mn-5] by clz bits and store into m[3..0]
        // m[k] = (mw[src+1] << clz) | (mw[src] >> (64-clz)), src = mn - 1 - (3-k)
        if (clz == 0) {
            for (int i = 0; i < 4 && i < static_cast<int>(mn); i++)
                m[3 - i] = mw[mn - 1 - i];
        } else {
            for (int k = 3; k >= 0; k--) {
                int hi_idx = static_cast<int>(mn) - 1 - (3 - k);  // mw index for high part
                int lo_idx = hi_idx - 1;                            // mw index for low part
                uint64_t hi_part = (hi_idx >= 0 && hi_idx < static_cast<int>(mn))
                                    ? mw[hi_idx] : 0;
                uint64_t lo_part = (lo_idx >= 0 && lo_idx < static_cast<int>(mn))
                                    ? mw[lo_idx] : 0;
                m[k] = (hi_part << clz) | (lo_part >> (64 - clz));
            }
        }

        // u = m - 2^255 (MSB clear)
        uint64_t u[4] = { m[0], m[1], m[2], m[3] & 0x7FFFFFFFFFFFFFFFULL };

        if (q256_is_zero(u)) {
            logf[0] = logf[1] = logf[2] = logf[3] = 0;
            return;
        }

        // t = u / (2^256 + u) = (f-1)/(f+1) in Q256
        uint64_t t[4];
        q256_atanh_arg(u, t);

        // t² in Q256
        uint64_t tsq[4];
        q256_mul(t, t, tsq);

        // atanh(t) = t + t³/3 + t⁵/5 + ... in Q256
        uint64_t sum[4] = { t[0], t[1], t[2], t[3] };
        uint64_t pow[4] = { t[0], t[1], t[2], t[3] };

        for (uint64_t d = 3; d < 200; d += 2) {
            q256_mul(pow, tsq, pow);

            uint64_t term[4] = { pow[0], pow[1], pow[2], pow[3] };
            q256_div_scalar(term, d);

            if (q256_is_zero(term)) break;

            q256_add(sum, term, sum);
        }

        // log(f) = 2 × atanh(t)
        q256_shl1(sum, logf);
    }

    // exp Q128 fast path (19 digits, precision_bits ≤ 64)
    // Argument reduction z = x - k·ln2 ∈ [0, ln2) → Q128 Taylor exp(z)-1 → (1+frac)·2^k
    static Float exp_small(const Float& x, int precision) {
        // Determine k = floor(x/ln2) in double
        double x_d = x.toDouble();
        constexpr double inv_ln2_d = 1.4426950408889634;
        int k = static_cast<int>(std::floor(x_d * inv_ln2_d));

        // Q128 constant of ln(2) (ln2 = 0.6931471805599453... < 1, fits in Q128)
        constexpr uint64_t ln2_hi = 0xB17217F7D1CF79ABULL;
        constexpr uint64_t ln2_lo = 0xC9E3B39803F2F6AFULL;

        // Get the mantissa of x and compute z = x - k·ln2 exactly in Q128
        // x = mantissa × 2^exponent, mantissa is a positive integer (max 64 bits)
        bool x_neg = x.isNegative();
        const Int& mant = x.mantissa();
        const uint64_t* mw = mant.data();
        uint64_t m_val = mw[0];  // 1 word since precision ≤ 64 bits
        int64_t e = x.exponent();
        int m_bits = 64 - std::countl_zero(m_val);
        // Absolute value of x = m_val × 2^e
        // x × 2^128 = m_val × 2^(e+128)
        // Q128 holds values in [0, 1). To handle the integer + fractional parts of x,
        // exploit that the integer parts of k·ln2 and x cancel, leaving z ∈ [0, ln2).

        // Compute k·ln2 as Q128 + integer part
        // k·ln2 = k × (0.B17217F7D1CF79AB_C9E3B39803F2F6AF × 2^0)
        // Integer part: floor(k·ln2) = floor(k × 0.6931...)
        // The low 128 bits of k·ln2 × 2^128 are the Q128 representation of the fractional part
        uint64_t abs_k = static_cast<uint64_t>(k < 0 ? -k : k);
        uint64_t kln2_hi, kln2_lo;
        {
            uint64_t p0_hi;
            uint64_t p0_lo = _umul128(ln2_lo, abs_k, &p0_hi);
            uint64_t p1_hi;
            uint64_t p1_lo = _umul128(ln2_hi, abs_k, &p1_hi);
            unsigned char c = _addcarry_u64(0, p1_lo, p0_hi, &kln2_hi);
            (void)c;
            kln2_lo = p0_lo;
            // p1_hi is the integer part (integer part of k·ln2 × 2^0)... not needed
        }

        // Represent the fractional part of x in Q128
        // x = m_val × 2^e → fractional part = frac(|x|) = frac(m_val × 2^e)
        // Integer part = floor(|x|)
        // The MSB is at position e+m_bits-1 = total_bits - 1
        // Example: x = 3.5 → m_val=7, e=-1, m_bits=3
        //   |x| = 7×2^(-1) = 3.5, integer part=3, fractional part=0.5
        int64_t total_bl = static_cast<int64_t>(m_bits) + e;
        // total_bl = the number of binary digits of x (2^(total_bl-1) ≤ |x| < 2^total_bl)
        // Place the fractional part into Q128: position m_val at 2^(128 - total_bl) and remove the integer-part bits

        uint64_t x_frac_hi = 0, x_frac_lo = 0;
        if (total_bl <= 0) {
            // |x| < 1 → no integer part, all fractional
            // m_val × 2^e × 2^128 = m_val × 2^(e+128)
            // Place the MSB of m_val at bit (128+e-1)
            int pos = static_cast<int>(128 + e);  // Position of the LSB of m_val (from bit 0)
            // Left-shift m_val by pos bits and place into Q128
            if (pos >= 64) {
                x_frac_hi = m_val << (pos - 64);
            } else if (pos >= 0) {
                x_frac_hi = m_val >> (64 - pos);
                x_frac_lo = m_val << pos;
            }
        } else if (total_bl < 64) {
            // Has an integer part: mask off the integer-part bits and extract only the fractional part
            // The fractional part of m_val × 2^e = (m_val mod 2^(-e)) × 2^e (when e < 0)
            // = (m_val & ((1<<(-e))-1)) × 2^e
            if (e < 0) {
                uint64_t frac_mask = (1ULL << (-e)) - 1;
                uint64_t frac_val = m_val & frac_mask;
                if (frac_val != 0) {
                    // frac_val × 2^e × 2^128 = frac_val × 2^(128+e)
                    int pos = static_cast<int>(128 + e);
                    if (pos >= 64) {
                        x_frac_hi = frac_val << (pos - 64);
                    } else if (pos > 0) {
                        x_frac_hi = frac_val >> (64 - pos);
                        x_frac_lo = frac_val << pos;
                    }
                }
            }
            // e >= 0 → no fractional part (x_frac = 0)
        }

        // z = frac(|x|) - frac(|k|·ln2) (difference mod 2^128)
        // The Q128 subtraction result correctly represents z mod 1 (the integer-part difference cancels via wraparound).
        // The fine adjustment of k is decided by a double approximation (the Q128 MSB/borrow misjudges 0.5 and integer arguments).
        uint64_t z_hi, z_lo;
        if (!x_neg) {
            unsigned char borrow = _subborrow_u64(0, x_frac_lo, kln2_lo, &z_lo);
            _subborrow_u64(borrow, x_frac_hi, kln2_hi, &z_hi);
        } else {
            unsigned char borrow = _subborrow_u64(0, kln2_lo, x_frac_lo, &z_lo);
            _subborrow_u64(borrow, kln2_hi, x_frac_hi, &z_hi);
        }

        // Fine adjustment of k: decided by the double approximation z (z < 0 → k--, z >= ln2 → k++)
        constexpr double ln2_d = 0.6931471805599453;
        double z_approx = std::fabs(x_d) - static_cast<double>(abs_k) * ln2_d;
        if (x_neg) z_approx = static_cast<double>(abs_k) * ln2_d - std::fabs(x_d);
        if (z_approx < 0) {
            // z < 0: decrement k by 1 and z += ln2
            k--;
            unsigned char c = _addcarry_u64(0, z_lo, ln2_lo, &z_lo);
            _addcarry_u64(c, z_hi, ln2_hi, &z_hi);
        } else if (z_approx >= ln2_d) {
            // z >= ln2: increment k by 1 and z -= ln2
            k++;
            unsigned char b = _subborrow_u64(0, z_lo, ln2_lo, &z_lo);
            _subborrow_u64(b, z_hi, ln2_hi, &z_hi);
        }

        if (z_hi == 0 && z_lo == 0) {
            Float result = Float::one(precision);
            if (k > 0) result <<= k;
            else if (k < 0) result >>= -k;
            result.setResultPrecision(precision);
            return result;
        }

        // Q128 Taylor: exp(z) - 1 = z + z²/2! + z³/3! + ...
        uint64_t sum_hi = z_hi, sum_lo = z_lo;
        uint64_t term_hi = z_hi, term_lo = z_lo;

        for (int n = 2; n <= 30; n++) {
            q128_mul(term_hi, term_lo, z_hi, z_lo, term_hi, term_lo);
            uint64_t rem;
            term_hi = _udiv128(0, term_hi, static_cast<uint64_t>(n), &rem);
            term_lo = _udiv128(rem, term_lo, static_cast<uint64_t>(n), &rem);
            if (term_hi == 0 && term_lo == 0) break;
            unsigned char c = _addcarry_u64(0, sum_lo, term_lo, &sum_lo);
            _addcarry_u64(c, sum_hi, term_hi, &sum_hi);
        }

        // exp(z) = 1 + sum → mantissa (2^128 + sum) × 2^{-128}
        uint64_t words[3] = { sum_lo, sum_hi, 1 };
        auto mant2 = Int::fromRawWordsPreNormalized(
            std::span<const uint64_t>(words, 3), 1);
        Float result(std::move(mant2), -128, false);

        if (k > 0) result <<= k;
        else if (k < 0) result >>= -k;
        result.setResultPrecision(precision);
        return result;
    }

    // exp Q256 fast path (38 digits, precision_bits ≤ 128)
    // Argument reduction in Float → convert z to Q256 → Q256 Taylor → reconstruct as Float
    static Float exp_medium(const Float& x, int precision) {
        // Compute z = x - k·ln2 ∈ [0, ln2) exactly in Float
        double x_d = x.toDouble();
        constexpr double inv_ln2_d = 1.4426950408889634;
        int k = static_cast<int>(std::floor(x_d * inv_ln2_d));

        Float ln2_val = Float::log2(precision + 10);
        Float z_float = x - k * ln2_val;
        if (z_float.isNegative()) {
            z_float = z_float + ln2_val;
            k--;
        }

        if (z_float.isZero()) {
            Float result = Float::one(precision);
            if (k > 0) result <<= k;
            else if (k < 0) result >>= -k;
            result.setResultPrecision(precision);
            return result;
        }

        // z ∈ (0, ln2) ⊂ (0, 1) → convert to Q256
        uint64_t z[4];
        float_to_q256(z_float, z);

        // Q256 Taylor: exp(z) - 1 = z + z²/2! + z³/3! + ...
        uint64_t sum[4] = { z[0], z[1], z[2], z[3] };
        uint64_t term[4] = { z[0], z[1], z[2], z[3] };

        for (int n = 2; n <= 60; n++) {
            q256_mul(term, z, term);
            q256_div_scalar(term, static_cast<uint64_t>(n));
            if (q256_is_zero(term)) break;
            q256_add(sum, term, sum);
        }

        // exp(z) = 1 + sum → mantissa (2^256 + sum) × 2^{-256}
        uint64_t words[5] = { sum[0], sum[1], sum[2], sum[3], 1 };
        auto mant2 = Int::fromRawWordsPreNormalized(
            std::span<const uint64_t>(words, 5), 1);
        Float result(std::move(mant2), -256, false);

        if (k > 0) result <<= k;
        else if (k < 0) result >>= -k;
        result.setResultPrecision(precision);
        return result;
    }

    // log fast path (≤ 64-bit precision, Q128 atanh computation using the high 64 bits of the mantissa)
    static Float log_small(const Float& x, int precision) {
        uint64_t logf_hi, logf_lo;
        int64_t k;
        compute_logf_q128(x, logf_hi, logf_lo, k);

        if (logf_hi == 0 && logf_lo == 0) {
            if (k == 0) return Float::zero();
            Float result = Float::log2(precision) * k;
            result.setResultPrecision(precision);
            return result;
        }

        Float result = q128_to_float(logf_hi, logf_lo, -128);

        if (k != 0) {
            Float k_log2 = Float::log2(precision) * k;
            k_log2.setResultPrecision(precision);
            result = result + k_log2;
        }

        result.setResultPrecision(precision);
        return result;
    }

    // log medium path (≤ 128-bit precision, Q256 atanh computation using the high 128 bits of the mantissa)
    static Float log_medium(const Float& x, int precision) {
        uint64_t logf[4];
        int64_t k;
        compute_logf_q256(x, logf, k);

        if (q256_is_zero(logf)) {
            if (k == 0) return Float::zero();
            Float result = Float::log2(precision) * k;
            result.setResultPrecision(precision);
            return result;
        }

        Float result = q256_to_float(logf, -256);

        if (k != 0) {
            Float k_log2 = Float::log2(precision) * k;
            k_log2.setResultPrecision(precision);
            result = result + k_log2;
        }

        result.setResultPrecision(precision);
        return result;
    }

    // log2 Q256 fast path: log2(x) = log(f)·log2(e) + k
    // log2(e) = 1 + frac → log2(f) = logf + logf·frac(log2(e))
    static Float log2_medium(const Float& x, int precision) {
        uint64_t logf[4];
        int64_t k;
        compute_logf_q256(x, logf, k);

        if (q256_is_zero(logf)) {
            return Float(Int(k), 0, k < 0);
        }

        // Fractional part of log2(e) = 0.44269504088896340735992468100189...
        // Q256: frac(log2(e)) × 2^256
        constexpr uint64_t inv_ln2_frac[4] = {
            0x164A2CD9A342648FULL,
            0xD6AEF551BAD2B4B1ULL,
            0x7D0FFDA0D23A7D11ULL,
            0x71547652B82FE177ULL
        };

        // log2(f) = logf + logf · frac(log2(e))
        uint64_t prod[4];
        q256_mul(logf, inv_ln2_frac, prod);

        uint64_t log2f[4];
        q256_add(logf, prod, log2f);

        // result = log2(f) + k
        Float result = q256_to_float(log2f, -256);
        if (k != 0) {
            result = result + Float(Int(k), 0, k < 0);
        }
        result.setResultPrecision(precision);
        return result;
    }

    // log10 Q256 fast path: log10(x) = log(f)·log10(e) + k·log10(2)
    static Float log10_medium(const Float& x, int precision) {
        uint64_t logf[4];
        int64_t k;
        compute_logf_q256(x, logf, k);

        // log10(e) = 1/ln(10) = 0.43429448190325182765112891891660508229...
        constexpr uint64_t log10e[4] = {
            0x1D1F96A27BC7529EULL,
            0x1F71A30122E4D101ULL,
            0x9AADD557D699EE19ULL,
            0x6F2DEC549B9438CAULL
        };

        // log10(2) = 0.30102999566398119521373889472449302676...
        constexpr uint64_t log10_2[4] = {
            0xC52F37935BE631E5ULL,
            0x13569862A1E8F9A4ULL,
            0x47C4ACD605BE48BCULL,
            0x4D104D427DE7FBCCULL
        };

        if (q256_is_zero(logf)) {
            if (k == 0) return Float::zero();
            // Compute log10(2^k) = k·log10(2) in Q256
            uint64_t abs_k = static_cast<uint64_t>(k < 0 ? -k : k);
            // Q256 × uint64 → Q256 (no high overflow since k is small)
            uint64_t kl2[4];
            uint64_t carry = 0;
            for (int i = 0; i < 4; i++) {
                uint64_t hi;
                uint64_t lo = _umul128(log10_2[i], abs_k, &hi);
                unsigned char c = _addcarry_u64(0, lo, carry, &kl2[i]);
                carry = hi + c;
            }
            Float result = q256_to_float(kl2, -256);
            if (k < 0) result = -result;
            result.setResultPrecision(precision);
            return result;
        }

        // log10(f) = logf · log10(e) (Q256 × Q256 → Q256)
        uint64_t log10f[4];
        q256_mul(logf, log10e, log10f);

        // Compute k·log10(2) in Q256 and add
        if (k != 0) {
            uint64_t abs_k = static_cast<uint64_t>(k < 0 ? -k : k);
            uint64_t kl2[4];
            uint64_t carry = 0;
            for (int i = 0; i < 4; i++) {
                uint64_t hi;
                uint64_t lo = _umul128(log10_2[i], abs_k, &hi);
                unsigned char c = _addcarry_u64(0, lo, carry, &kl2[i]);
                carry = hi + c;
            }

            if (k > 0) {
                q256_add(log10f, kl2, log10f);
            } else {
                // log10(x) = log10(f) - |k|·log10(2)
                // log10(f) ∈ [0,1), |k|·log10(2) > 0
                // Compare to determine the sign
                bool neg = false;
                for (int i = 3; i >= 0; i--) {
                    if (log10f[i] > kl2[i]) break;
                    if (log10f[i] < kl2[i]) { neg = true; break; }
                }
                if (neg) {
                    uint64_t tmp[4];
                    q256_sub(kl2, log10f, tmp);
                    Float result = q256_to_float(tmp, -256);
                    result = -result;
                    result.setResultPrecision(precision);
                    return result;
                } else {
                    q256_sub(log10f, kl2, log10f);
                }
            }
        }

        Float result = q256_to_float(log10f, -256);
        result.setResultPrecision(precision);
        return result;
    }

    // log2 fast path: log2(x) = log(f)/ln(2) + k = log(f)·log2(e) + k
    // log2(e) = 1 + 0.44269504... → log2(f) = logf + logf·frac(log2(e))
    // log2(f) ∈ [0, 1), so it fits in Q128
    static Float log2_small(const Float& x, int precision) {
        uint64_t logf_hi, logf_lo;
        int64_t k;
        compute_logf_q128(x, logf_hi, logf_lo, k);

        if (logf_hi == 0 && logf_lo == 0) {
            return Float(Int(k), 0, k < 0);
        }

        // Fractional part of log2(e) = 0.44269504088896340735992468100189...
        // Q128: the hi:lo words of 0.44269504... × 2^128
        // Python: int(0.44269504088896340735992468100189213742... * 2**128)
        // = 150675655792891826045981552050498960470
        // hi = 150675655792891826045981552050498960470 >> 64
        //    = 8168882295414032698 = 0x71547652B82FE177
        // lo = 150675655792891826045981552050498960470 & (2^64-1)
        //    = 8927088712929562294 = 0x7BF3F01ADBC79AB6
        constexpr uint64_t inv_ln2_frac_hi = 0x71547652B82FE177ULL;
        constexpr uint64_t inv_ln2_frac_lo = 0x7D0FFDA0D23A7D11ULL;

        // log2(f) = logf + logf * frac(log2(e))
        uint64_t prod_hi, prod_lo;
        q128_mul(logf_hi, logf_lo, inv_ln2_frac_hi, inv_ln2_frac_lo,
                 prod_hi, prod_lo);

        // log2f = logf + prod
        uint64_t log2f_lo, log2f_hi;
        unsigned char carry = _addcarry_u64(0, logf_lo, prod_lo, &log2f_lo);
        _addcarry_u64(carry, logf_hi, prod_hi, &log2f_hi);

        // result = log2(f) + k
        Float result = q128_to_float(log2f_hi, log2f_lo, -128);
        if (k != 0) {
            result = result + Float(Int(k), 0, k < 0);
        }
        result.setResultPrecision(precision);
        return result;
    }

    // log10 fast path: log10(x) = log(f)·log10(e) + k·log10(2)
    // log10(2) is also kept in Q128, computing k·log10(2) without Float constant computation
    static Float log10_small(const Float& x, int precision) {
        uint64_t logf_hi, logf_lo;
        int64_t k;
        compute_logf_q128(x, logf_hi, logf_lo, k);

        // log10(e) = 1/ln(10) = 0.43429448190325182765112891891660508229...
        // Q128: int(0.43429448190325182765112891891660508229... * 2**128)
        constexpr uint64_t log10e_hi = 0x6F2DEC549B9438CAULL;
        constexpr uint64_t log10e_lo = 0x9AADD557D699EE19ULL;

        // log10(2) = ln(2)/ln(10) = 0.30102999566398119521373889472449302676...
        // Q128: int(0.30102999566398119521373889472449302676... * 2**128)
        constexpr uint64_t log10_2_hi = 0x4D104D427DE7FBCCULL;
        constexpr uint64_t log10_2_lo = 0x47C4ACD605BE48BCULL;

        if (logf_hi == 0 && logf_lo == 0) {
            if (k == 0) return Float::zero();
            // log10(2^k) = k·log10(2); the integer part of k·log10(2) is at most about 19
            // Compute k*log10_2 in Q128: simple multiplication since k is small
            uint64_t abs_k = static_cast<uint64_t>(k < 0 ? -k : k);
            uint64_t kl2_hi, kl2_lo;
            // Q128 × uint64 → Q128 (high-word overflow ignored; safe since k ≤ 64)
            uint64_t mul_lo_hi;
            kl2_lo = _umul128(log10_2_lo, abs_k, &mul_lo_hi);
            uint64_t mul_hi_hi;
            uint64_t mul_hi_lo = _umul128(log10_2_hi, abs_k, &mul_hi_hi);
            unsigned char c = _addcarry_u64(0, mul_hi_lo, mul_lo_hi, &kl2_hi);
            (void)c; // The carry into mul_hi_hi is the integer part (ignorable)
            Float result = q128_to_float(kl2_hi, kl2_lo, -128);
            if (k < 0) result = -result;
            result.setResultPrecision(precision);
            return result;
        }

        // log10(f) = logf · log10(e) (Q128 × Q128 → Q128)
        uint64_t log10f_hi, log10f_lo;
        q128_mul(logf_hi, logf_lo, log10e_hi, log10e_lo, log10f_hi, log10f_lo);

        // Compute k·log10(2) in Q128 as well and add
        if (k != 0) {
            uint64_t abs_k = static_cast<uint64_t>(k < 0 ? -k : k);
            uint64_t kl2_hi, kl2_lo;
            uint64_t mul_lo_hi;
            kl2_lo = _umul128(log10_2_lo, abs_k, &mul_lo_hi);
            uint64_t mul_hi_hi;
            uint64_t mul_hi_lo = _umul128(log10_2_hi, abs_k, &mul_hi_hi);
            unsigned char c = _addcarry_u64(0, mul_hi_lo, mul_lo_hi, &kl2_hi);
            (void)c;

            if (k > 0) {
                unsigned char carry = _addcarry_u64(0, log10f_lo, kl2_lo, &log10f_lo);
                _addcarry_u64(carry, log10f_hi, kl2_hi, &log10f_hi);
            } else {
                // log10f - kl2: log10(f) is positive, k < 0 → log10(2^k) is negative
                // log10(x) = log10(f) + k·log10(2) = log10(f) - |k|·log10(2)
                // When x < 1, the result may be negative
                if (log10f_hi > kl2_hi ||
                    (log10f_hi == kl2_hi && log10f_lo >= kl2_lo)) {
                    unsigned char borrow = 0;
                    borrow = _subborrow_u64(0, log10f_lo, kl2_lo, &log10f_lo);
                    _subborrow_u64(borrow, log10f_hi, kl2_hi, &log10f_hi);
                } else {
                    // The result is negative
                    unsigned char borrow = 0;
                    uint64_t neg_lo, neg_hi;
                    borrow = _subborrow_u64(0, kl2_lo, log10f_lo, &neg_lo);
                    _subborrow_u64(borrow, kl2_hi, log10f_hi, &neg_hi);
                    Float result = q128_to_float(neg_hi, neg_lo, -128);
                    result = -result;
                    result.setResultPrecision(precision);
                    return result;
                }
            }
        }

        Float result = q128_to_float(log10f_hi, log10f_lo, -128);
        result.setResultPrecision(precision);
        return result;
    }

    // Helper for log(f) computation via Newton iteration (f ∈ [1,2))
    // Return values: log(f) (natural log), k (binary exponent x = f·2^k)
    // Initial approximation: std::log(double) (~53 bit) + Newton precision doubling
    // Step 1: exp_small (Q128 Taylor) — called with the mantissa still 1 word
    //   Important: setResultPrecision left-shift-extends the mantissa, so
    //   do not call setPrecision before the exp_small call.
    // From step 2 onward: expDoubling (RawFloat Taylor + squaring reconstruction)
    // Target: ≤ 2000 bits (~600 digits). Above 600 digits, AGM wins.
    // (LogfResult is forward-declared)

    LogfResult logf_newton_core(Float x, int compute_prec) {
        // x = f · 2^k, f ∈ [1, 2)
        int64_t ea = x.exponent() + static_cast<int64_t>(x.mantissa().bitLength());
        int64_t k = ea - 1;

        // f = x · 2^(-k)
        Float f = ldexp(std::move(x), static_cast<int>(-k));

        // f = 1.0 exactly ⟺ mantissa = 1 (LSB=1 under odd normalization)
        if (f.mantissa().bitLength() == 1) {
            return { Float::zero(), k, true };
        }

        int target_bits = Float::precisionToBits(compute_prec);
        int guard = 32;

        // Choice of initial approximation
        Float y;
        int correct_bits;
        if (target_bits <= 230) {
            // Within direct Q256 range: double initial approximation (completes in 1-2 iterations)
            double fd = f.toDouble();
            y = Float(std::log(fd));
            correct_bits = 50;
        } else {
            // High precision: ~240-bit initial approximation via Q256 atanh → fewer iterations
            // 240→720→2160 vs 50→150→450→1350→4050
            // 250d (830b): 3 iters→2 iters, 500d (1660b): 4 iters→2 iters
            uint64_t logf_q256[4];
            int64_t k_q256;
            compute_logf_q256(f, logf_q256, k_q256);
            if (q256_is_zero(logf_q256)) {
                // f-1 is below Q256 resolution (~2^-255). f==1 (mantissa bitLength==1) is
                // already handled above, so this is the case f≠1 but extremely close to 1.
                // BUGFIX (2026-05-31): previously returned log=0, which is the error log(1+u)→0
                // (surfaced when the twin_prime constant's I(n)~2^n amplifies a tiny lnλ(n)~3^{-n}).
                // Seed with log(1+u)≈u (relative error ~u/2) and bring it to full precision via Halley iteration.
                Float u = f - Float::one();
                if (u.isZero()) return { Float::zero(), k, true };
                int64_t umsb = u.exponent() + static_cast<int64_t>(u.mantissa().bitLength());
                y = u;
                correct_bits = std::max(1, static_cast<int>(-umsb) - 4);
            } else {
                y = q256_to_float(logf_q256, -256);
                correct_bits = 230;
            }
        }

        // Halley precision-tripling loop: 50 → 150 → 450 → 1350 → ...
        // Halley: y_{n+1} = y_n + 2·(f - exp(y_n)) / (f + exp(y_n))
        // Cubic convergence: the correct bit count triples each step (Newton doubles)
        while (correct_bits < target_bits) {
            int compute_bits = std::min(3 * correct_bits + guard, target_bits + guard);
            int step_prec = Float::bitsToPrecision(compute_bits);

            // Compute exp(y_n): no argument reduction needed since y ∈ [0, ln2)
            // exp_small: Q128 Taylor, only for 1-word mantissa & step_prec ≤ 19
            //   Note: setResultPrecision left-shift-extends the mantissa, making it
            //     multiple words, so do not call it before the exp_small decision.
            // exp_medium: Q256 Taylor, step_prec ≤ 38
            // expDoubling: RawFloat Taylor + squaring reconstruction (general)
            Float e_y;
            int step_bits = Float::precisionToBits(step_prec);
            if (y.mantissa().size() <= 1 && step_bits <= 120) {
                // Q128 Taylor fast path: 1-word input, ~128-bit output
                // Used for step_bits ≤ 120 (guard margin 8 bit)
                e_y = exp_small(y, step_prec);
            } else if (step_bits <= 240) {
                // Q256 Taylor: ~256-bit output
                y.setEffectiveBits(step_bits);
                y.setResultPrecision(step_prec);
                e_y = exp_medium(y, step_prec);
            } else {
                y.setEffectiveBits(step_bits);
                y.setResultPrecision(step_prec);
                e_y = expDoubling(y, step_prec);
            }

            // Halley correction: 2·(f - t) / (f + t)  where t = exp(y)
            // Since f, t ∈ [1, 2), f+t > 0, numerically stable
            Float f_prec(f);
            f_prec.truncateToApprox(step_prec);
            Float num = f_prec - e_y;      // f - t (approximately near 0)
            Float den = f_prec + e_y;      // f + t (≈ 2f)
            Float correction = (num + num) / den;  // 2·(f-t)/(f+t)

            y = y + correction;
            y.truncateToApprox(step_prec);

            correct_bits = std::min(3 * correct_bits, compute_bits);
        }

        return { std::move(y), k, false };
    }

    // log(x) = log(f) + k · log(2)
    static Float log_newton(Float x, int eff_x, int precision) {
        int compute_prec = effectiveComputePrecision(eff_x, precision);
        auto [logf, k, f_is_one] = logf_newton_core(std::move(x), compute_prec);

        if (f_is_one) {
            if (k == 0) return Float::zero();
            Float result = Float::log2(compute_prec) * k;
            finalizeResult(result, eff_x, precision);
            return result;
        }

        if (k != 0) {
            logf = logf + Float::log2(compute_prec) * k;
        }

        finalizeResult(logf, eff_x, precision);
        return logf;
    }

    // log2(x) = log(f) * log2(e) + k  (replacing division with multiplication)
    static Float log2_newton(Float x, int eff_x, int precision) {
        int compute_prec = effectiveComputePrecision(eff_x, precision);
        auto [logf, k, f_is_one] = logf_newton_core(std::move(x), compute_prec);

        if (f_is_one) {
            if (k == 0) return Float::zero();
            Float result(k);
            finalizeResult(result, eff_x, precision);
            return result;
        }

        // log2(e) = 1/log(2) — multiplication is about 2-3× faster than division
        Float log2e = Float::one(compute_prec) / Float::log2(compute_prec);
        Float result = logf * log2e;
        if (k != 0) {
            result = result + Float(k);
        }

        finalizeResult(result, eff_x, precision);
        return result;
    }

    // log10(x) = log(f) * log10(e) + k · log10(2)  (replacing division with multiplication)
    static Float log10_newton(Float x, int eff_x, int precision) {
        int compute_prec = effectiveComputePrecision(eff_x, precision);
        auto [logf, k, f_is_one] = logf_newton_core(std::move(x), compute_prec);

        if (f_is_one) {
            if (k == 0) return Float::zero();
            // log10(2^k) = k · log10(2) = k · log(2)/log(10)
            Float result = Float::log2(compute_prec) * k / Float::log10(compute_prec);
            finalizeResult(result, eff_x, precision);
            return result;
        }

        // log10(e) = 1/log(10) — multiplication is about 2-3× faster than division
        Float log10e = Float::one(compute_prec) / Float::log10(compute_prec);
        Float result = logf * log10e;
        if (k != 0) {
            // log10(2) = log(2)/log(10) = log(2) · log10(e)
            Float log10_2 = Float::log2(compute_prec) * log10e;
            result = result + log10_2 * k;
        }

        finalizeResult(result, eff_x, precision);
        return result;
    }

    // Internal log dispatch (selects the optimal algorithm based on precision)
    static Float log_dispatch(const Float& x, int eff_x, int precision) {
        int precision_bits = Float::precisionToBits(precision);
        if (precision_bits <= 64) {
            return log_newton(Float(x), eff_x, precision);
        }
        if (precision_bits <= 230) {
            return log_medium(x, precision);
        }
        if (precision_bits <= 3000) {
            return log_newton(Float(x), eff_x, precision);
        }
        if (precision_bits <= 6000) {
            return log_multiprime(Float(x), eff_x, precision);
        }
        return log_core(Float(x), eff_x, precision);
    }

    Float log(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity();
        if (x.isZero()) return Float::negativeInfinity();
        if (x.isNegative()) return Float::nan();
        if (x == Float::one()) return Float::zero();

        return zivRound(log_dispatch, x, x.effectiveBits(), precision);
    }

    Float log(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity();
        if (x.isZero()) return Float::negativeInfinity();
        if (x.isNegative()) return Float::nan();
        if (x == Float::one()) return Float::zero();

        return zivRound(log_dispatch, x, x.effectiveBits(), precision);
    }

    //=============================================================================
    // log_ui — natural logarithm of a positive integer
    //=============================================================================

    Float logUi(unsigned long long n, int precision) {
        if (n == 0) return Float::negativeInfinity();
        if (n == 1) return Float::zero(precision);

        int precision_bits = Float::precisionToBits(precision);
        int wp = precision_bits + 10;

        // Trial-divide by small primes and decompose as log(n) = Σ e_i·log(p_i) + log(remainder)
        unsigned long long rem = n;
        int exponents[MP_NUM_PRIMES] = {};

        for (int i = 0; i < MP_NUM_PRIMES && rem > 1; ++i) {
            unsigned long long p = static_cast<unsigned long long>(MP_PRIMES[i]);
            while (rem % p == 0) {
                exponents[i]++;
                rem /= p;
            }
        }

        // If fully decomposed: Σ e_i·log(p_i)
        // If there is a remainder: Σ e_i·log(p_i) + log(Float(rem))
        bool has_prime_part = false;
        for (int i = 0; i < MP_NUM_PRIMES; ++i) {
            if (exponents[i] != 0) { has_prime_part = true; break; }
        }

        if (!has_prime_part) {
            // Could not decompose into small primes → log directly
            return log(Float(static_cast<int64_t>(n)), precision);
        }

        // Sum of the prime-factorization part
        Float result = Float::zero(wp);
        for (int i = 0; i < MP_NUM_PRIMES; ++i) {
            if (exponents[i] == 0) continue;
            const Float& lp = getLogPrime(i, wp);
            if (exponents[i] == 1)
                result = result + lp;
            else
                result = result + lp * exponents[i];
        }

        // If there is a remainder
        if (rem > 1) {
            result = result + log(Float(static_cast<int64_t>(rem)), wp);
        }

        result.setResultPrecision(precision);
        return result;
    }

    //=============================================================================
    // Square root (sqrt) — correct rounding based on integer sqrtRem
    //=============================================================================
    static Float sqrt_isqrt(Float x, int precision) {
        // Approach: reduce sqrt(mantissa * 2^exp) to an integer square root.
        // Scale the mantissa to a 2*(p+1)-bit integer and
        // obtain the (p+1)-bit root via isqrt.
        // Determine the sticky bit by directly inspecting the dc_sqrtrem remainder (skips the M(n) squaring).

        int precision_bits = Float::precisionToBits(precision);
        int target_root_bits = precision_bits + 1;

        // Get the mantissa and exponent
        const Int& m = x.mantissa();
        int64_t e = x.exponent();
        size_t mn = m.size();
        int bl = static_cast<int>(m.bitLength());

        // Compute the scaling amount
        int desired_bits = 2 * target_root_bits;
        int base_shift = desired_bits - bl;
        if (base_shift < 0) base_shift = 0;
        int64_t adjusted_exp = e - base_shift;
        if (adjusted_exp % 2 != 0) {
            base_shift += 1;
            adjusted_exp -= 1;
        }
        int64_t result_exponent = adjusted_exp / 2;

        // Scale + sqrtrem at the mpn level (bypasses IntSqrt::sqrtRem)
        ScratchScope scope;
        auto& arena = getThreadArena();

        // Scaling: scaled = m << base_shift (mpn level)
        size_t word_shift = static_cast<size_t>(base_shift) / 64;
        unsigned bit_shift = static_cast<unsigned>(base_shift) % 64;
        size_t an = mn + word_shift + (bit_shift > 0 ? 1 : 0);
        uint64_t* ap = arena.alloc_limbs(an + 1);
        std::memset(ap, 0, (an + 1) * sizeof(uint64_t));
        const uint64_t* mw = m.data();
        for (size_t i = 0; i < mn; i++) ap[i + word_shift] = mw[i];
        if (bit_shift > 0) {
            ap[mn + word_shift] = mpn::lshift(ap + word_shift, ap + word_shift, mn, bit_shift);
        }
        // Normalized size
        while (an > 0 && ap[an - 1] == 0) an--;
        if (an == 0) {
            return Float::zero();
        }

        // sqrtrem_check_exact: sqrt with zero-remainder detection (skips the M(n) squaring)
        size_t sn = (an + 1) / 2;
        uint64_t* sp = arena.alloc_limbs(sn);
        size_t scratch_sz = mpn::sqrtrem_scratch_size(an);
        uint64_t* scratch = arena.alloc_limbs(scratch_sz);

        auto [sp_n, exact] = mpn::sqrtrem_check_exact(sp, ap, an, scratch);

        // Sticky bit: if not exact, set the LSB to 1
        if (!exact && sp_n > 0) {
            sp[0] |= 1;
        }

        // Construct the Int (SBO: no heap allocation for small sizes)
        Int s = (sp_n > 0)
            ? Int::fromRawWords(std::span<const uint64_t>(sp, sp_n), 1)
            : Int::Zero();
        Float result(std::move(s), result_exponent, false);
        result.setResultPrecision(precision);
        return result;
    }

    //=============================================================================
    // Square root (sqrt) — Newton reciprocal square root (YC-7)
    // r_{n+1} = r_n * (3 - x * r_n²) / 2  (no division, multiplication only)
    // sqrt(x) = x * r_n
    //=============================================================================
        // Newton invsqrt vs isqrt benchmark results (2026-03-16, Zen3, AVX2 NTT):
    //   2M digits: isqrt 318ms, Newton 528ms (isqrt 40% faster)
    //  10M digits: isqrt 3208ms, Newton 4021ms (isqrt 20% faster)
    // → isqrt (integer sqrtRem) is best across all sizes. The Newton path is a deletion candidate.

    static Float sqrt_core(Float x, int /*eff_x*/, int precision) {
        return sqrt_isqrt(std::move(x), precision);
    }

    // sqrt ultra-fast path: 1-word mantissa + double sqrt + 1 Newton step
    // double sqrt (~53 bit) → _udiv128 Newton → _umul128 correction
    // Completely bypasses the Float abstraction layer
    static Float sqrt_newton_1(const Float& x, int precision) {
        const Int& mant = x.mantissa();
        const uint64_t* mw = mant.data();
        int64_t e = x.exponent();
        int bl = static_cast<int>(64 - std::countl_zero(mw[0]));

        // Scale to 128 bits (desired_bits=126: even with parity adjustment +1, 127 ≤ 128)
        int base_shift = 126 - bl;
        if (base_shift < 0) base_shift = 0;
        int64_t adjusted_exp = e - base_shift;
        if (adjusted_exp % 2 != 0) {
            base_shift += 1;
            adjusted_exp -= 1;
        }
        int64_t result_exponent = adjusted_exp / 2;

        // M = mw[0] << base_shift (128 bits: M_hi:M_lo)
        uint64_t m = mw[0];
        uint64_t M_hi, M_lo;
        if (base_shift == 0) {
            M_hi = 0; M_lo = m;
        } else if (base_shift < 64) {
            M_hi = m >> (64 - base_shift);
            M_lo = m << base_shift;
        } else if (base_shift == 64) {
            M_hi = m; M_lo = 0;
        } else {
            M_hi = m << (base_shift - 64);
            M_lo = 0;
        }

        // Initial value: double sqrt (~53-bit precision)
        double M_dbl = static_cast<double>(M_hi) * 0x1p64 + static_cast<double>(M_lo);
        uint64_t s0 = static_cast<uint64_t>(std::sqrt(M_dbl));
        if (s0 == 0) s0 = 1;

        // Newton iteration: s1 = (s0 + M/s0) / 2
        // _udiv128 requires M_hi < s0 (the condition that the quotient fits in 64 bits)
        while (M_hi >= s0) s0++;
        uint64_t rem;
        uint64_t q = _udiv128(M_hi, M_lo, s0, &rem);
        // Overflow-safe (s0 + q) / 2
        uint64_t s1 = (s0 >> 1) + (q >> 1) + ((s0 & q) & 1);

        // Correction: guarantee s1² ≤ M < (s1+1)² (adjustment of at most ±1)
        uint64_t sq_hi, sq_lo;
        sq_lo = _umul128(s1, s1, &sq_hi);
        // If s1² > M, decrement s1
        if (sq_hi > M_hi || (sq_hi == M_hi && sq_lo > M_lo)) {
            s1--;
            sq_lo = _umul128(s1, s1, &sq_hi);
        }
        // If (s1+1)² ≤ M, increment s1
        uint64_t sq1_hi, sq1_lo;
        sq1_lo = _umul128(s1 + 1, s1 + 1, &sq1_hi);
        if (sq1_hi < M_hi || (sq1_hi == M_hi && sq1_lo <= M_lo)) {
            s1++;
            sq_hi = sq1_hi;
            sq_lo = sq1_lo;
        }

        // Remainder check (sticky bit)
        bool has_remainder = (sq_hi != M_hi || sq_lo != M_lo);
        if (has_remainder) {
            s1 |= 1;
        }

        Int s_int(s1);
        Float result(std::move(s_int), result_exponent, false);
        result.setResultPrecision(precision);
        return result;
    }

    // sqrt fast path: 1-2 word mantissa, dc_sqrtrem(n=2) avoids the Int overhead
    // precision_bits <= 128 (result is at most 2 words)
    static Float sqrt_small_2(const Float& x, int precision) {
        int precision_bits = Float::precisionToBits(precision);
        int target_root_bits = precision_bits + 1;

        const Int& mant = x.mantissa();
        const uint64_t* mw = mant.data();
        size_t mn = mant.size();
        int64_t e = x.exponent();
        int bl = static_cast<int>((mn - 1) * 64 + (64 - std::countl_zero(mw[mn - 1])));

        // Limit desired_bits to 255 or below so it fits in the 4-word (256-bit) buffer
        // Even with parity adjustment +1, stays within 256 bits
        int desired_bits = std::min(2 * target_root_bits, 255);
        int base_shift = desired_bits - bl;
        if (base_shift < 0) base_shift = 0;
        int64_t adjusted_exp = e - base_shift;
        if (adjusted_exp % 2 != 0) {
            base_shift += 1;
            adjusted_exp -= 1;
        }
        int64_t result_exponent = adjusted_exp / 2;

        // Scale into the 4-word buffer
        uint64_t scaled[6] = {0, 0, 0, 0, 0, 0};  // +2 because dc_sqrtrem uses np[n..n+2l-1]
        for (size_t i = 0; i < mn; i++) scaled[i] = mw[i];

        if (base_shift > 0) {
            size_t word_shift = static_cast<size_t>(base_shift) / 64;
            unsigned bit_shift = static_cast<unsigned>(base_shift) % 64;
            if (word_shift > 0) {
                for (int i = 3; i >= static_cast<int>(word_shift); i--)
                    scaled[i] = scaled[i - word_shift];
                for (size_t i = 0; i < word_shift; i++)
                    scaled[i] = 0;
            }
            if (bit_shift > 0) {
                for (int i = 3; i > 0; i--)
                    scaled[i] = (scaled[i] << bit_shift) | (scaled[i - 1] >> (64 - bit_shift));
                scaled[0] <<= bit_shift;
            }
        }

        // Normalization for dc_sqrtrem: scaled[3] >= 2^62 is required
        // If scaled[3] is 0, a word-level shift is also needed
        unsigned total_norm = 0;
        if (scaled[3] == 0) {
            // scaled[3] is 0 → the data is in scaled[2]
            unsigned clz2 = static_cast<unsigned>(std::countl_zero(scaled[2]));
            total_norm = 64 + (clz2 & ~1u);
        } else {
            unsigned clz = static_cast<unsigned>(std::countl_zero(scaled[3]));
            total_norm = clz & ~1u;
        }
        if (total_norm > 0) {
            size_t word_norm = total_norm / 64;
            unsigned bit_norm = total_norm % 64;
            if (word_norm > 0) {
                for (int i = 3; i >= static_cast<int>(word_norm); i--)
                    scaled[i] = scaled[i - word_norm];
                for (size_t i = 0; i < word_norm; i++)
                    scaled[i] = 0;
            }
            if (bit_norm > 0) {
                for (int i = 3; i > 0; i--)
                    scaled[i] = (scaled[i] << bit_norm) | (scaled[i - 1] >> (64 - bit_norm));
                scaled[0] <<= bit_norm;
            }
        }

        // dc_sqrtrem(sp, np, n=2, scratch)
        // sp[0..1] = floor(sqrt(scaled[0..3])), remainder stored in scaled[0..1]
        uint64_t sp[2] = {0, 0};
        uint64_t scratch[16];  // dc_sqrtrem_scratch_size(2) ≈ 9
        int carry = mpn::dc_sqrtrem(sp, scaled, 2, scratch);

        // Remainder check (against the normalized input)
        bool has_remainder = (carry != 0 || scaled[0] != 0 || scaled[1] != 0);

        // Denormalize: right-shift the root by total_norm/2 bits
        unsigned root_shift = total_norm / 2;
        if (root_shift > 0) {
            if (root_shift >= 64) {
                // Word crossing: sp[0] = sp[1] >> (root_shift - 64)
                sp[0] = sp[1] >> (root_shift - 64);
                sp[1] = 0;
            } else {
                sp[0] = (sp[0] >> root_shift) | (sp[1] << (64 - root_shift));
                sp[1] >>= root_shift;
            }
        }

        if (has_remainder) {
            sp[0] |= 1;
        }

        // Construct the Int from 2 words (SBO: no heap allocation)
        size_t sp_n = (sp[1] != 0) ? 2 : 1;
        auto s_int = Int::fromRawWordsPreNormalized(
            std::span<const uint64_t>(sp, sp_n), 1);
        Float result(std::move(s_int), result_exponent, false);
        result.setResultPrecision(precision);
        return result;
    }

    //=============================================================================
    // Square (sqr) — dedicated squaring via IntOps::square
    //=============================================================================

    Float sqr(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::positiveInfinity();
        if (x.isZero()) {
            Float z;
            z.effective_bits_ = x.effective_bits_;
            z.requested_bits_ = x.requested_bits_;
            return z;
        }

        int eff = x.effectiveBits();

        // Operand truncation
        const Int* xp = &x.mantissa_;
        Int x_trunc;
        int64_t x_exp = x.exponent_;

        if (eff < INT_MAX) {
            int compute_bits = eff + 32;
            int bl = static_cast<int>(xp->bitLength());
            if (bl > compute_bits) {
                int words_to_drop = (bl - compute_bits) / 64;
                if (words_to_drop > 0) {
                    int shift = words_to_drop * 64;
                    x_trunc = *xp;
                    x_trunc >>= shift;
                    x_exp += shift;
                    xp = &x_trunc;
                }
            }
        }

        // IntOps::squareUnchecked: NaN/Inf/Zero already rejected at entry → mantissa is Normal
        Int result_mantissa;
        IntOps::squareUnchecked(*xp, result_mantissa);
        int64_t result_exponent = x_exp * 2;

        Float result(std::move(result_mantissa), result_exponent, false);  // x² is always positive
        result.effective_bits_ = eff;
        result.requested_bits_ = x.requested_bits_;
        finalizeResult(result, eff, precision);
        return result;
    }

    Float sqr(Float&& x, int precision) {
        return sqr(static_cast<const Float&>(x), precision);
    }

    Float sqrt(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isNegative()) return Float::nan();
        if (x.isZero()) return Float::zero();
        if (x.isInfinity()) return Float::positiveInfinity();

        // Fast paths
        size_t mn = x.mantissa().size();
        int precision_bits = Float::precisionToBits(precision);
        if (mn == 1 && precision_bits <= 64) {
            return sqrt_newton_1(x, precision);
        }
        if (mn <= 2 && precision_bits <= 128) {
            return sqrt_small_2(x, precision);
        }

        // isqrt is faster than Newton at all sizes → always use isqrt
        return sqrt_core(Float(x), x.effectiveBits(), precision);
    }

    Float sqrt(Float&& x, int precision) {
        // The rvalue version delegates to the const-ref version (no difference, as the Newton path copies internally)
        return sqrt(static_cast<const Float&>(x), precision);
    }

    //=============================================================================
    // Sine (sin) / cosine (cos) common code
    //=============================================================================

    static Float sinTaylor(const Float& x, int working_precision) {
        // sin(x) = x - x³/3! + x⁵/5! - ...
        // Computed directly with RawFloat (zero allocation inside the loop)
        int wp_bits = Float::precisionToBits(working_precision);
        int nw = (wp_bits + 63) / 64;

        ScratchScope scope;
        auto& arena = getThreadArena();

        // Pre-allocate buffers
        size_t prod_alloc = 2 * static_cast<size_t>(nw) + 4;
        uint64_t* prod_buf = arena.alloc_limbs(prod_alloc);
        size_t mul_scratch  = mpn::multiply_scratch_size(nw + 1, nw + 1);
        size_t sqr_scratch  = mpn::square_scratch_size(nw + 1);
        size_t scratch_sz   = std::max(mul_scratch, sqr_scratch);
        uint64_t* scratch  = arena.alloc_limbs(scratch_sz > 0 ? scratch_sz : 1);

        // Extract |x|
        RawFloat x_rf = rf_extract(x, nw, arena);
        bool x_negative = x.isNegative();

        // Precompute x² (squaring is accelerated via square)
        uint64_t* x2_d = arena.alloc_limbs(nw + 2);
        RawFloat x2{x2_d, 0, 0};
        rf_sqr(x2, x_rf, prod_buf, scratch, nw);

        uint64_t* result_d = arena.alloc_limbs(nw + 2);
        std::memset(result_d, 0, (nw + 2) * sizeof(uint64_t));

        if (wp_bits >= 10000) {
            // Paterson-Stockmeyer: O(sqrt(l)) full-size multiplications
            RawFloat result{result_d, 0, 0};
            rf_sin_ps(result, x_rf, x2, nw, prod_buf, scratch, arena, working_precision);
            return rf_to_float(result, x_negative, working_precision);
        }

        // Naive Taylor series
        uint64_t* term_d = arena.alloc_limbs(nw + 2);
        std::memset(term_d, 0, (nw + 2) * sizeof(uint64_t));
        std::memcpy(term_d, x_rf.d, x_rf.nw * sizeof(uint64_t));
        RawFloat term{term_d, x_rf.nw, x_rf.exp};

        std::memcpy(result_d, x_rf.d, x_rf.nw * sizeof(uint64_t));
        RawFloat result{result_d, x_rf.nw, x_rf.exp};

        int max_terms = static_cast<int>(working_precision * 1.2) + 10;

        for (int k = 1; k <= max_terms; ++k) {
            rf_mul(term, term, x2, prod_buf, scratch, nw);
            uint64_t divisor = static_cast<uint64_t>(2*k) * static_cast<uint64_t>(2*k + 1);
            rf_divmod_1(term, divisor);
            if (term.nw == 0) break;
            if (k & 1) {
                if (!rf_sub(result, term, nw + 1)) break;
            } else {
                if (!rf_add(result, term, nw + 1)) break;
            }
        }

        return rf_to_float(result, x_negative, working_precision);
    }

    // Internal computation of cos(x) via Taylor series (assumes |x| <= π/4)
    static Float cosTaylor(const Float& x, int working_precision) {
        // cos(x) = 1 - x²/2! + x⁴/4! - ...
        // Computed directly with RawFloat (zero allocation inside the loop)
        int wp_bits = Float::precisionToBits(working_precision);
        int nw = (wp_bits + 63) / 64;

        ScratchScope scope;
        auto& arena = getThreadArena();

        // Pre-allocate buffers
        size_t prod_alloc = 2 * static_cast<size_t>(nw) + 4;
        uint64_t* prod_buf = arena.alloc_limbs(prod_alloc);
        size_t mul_scratch  = mpn::multiply_scratch_size(nw + 1, nw + 1);
        size_t sqr_scratch  = mpn::square_scratch_size(nw + 1);
        size_t scratch_sz   = std::max(mul_scratch, sqr_scratch);
        uint64_t* scratch  = arena.alloc_limbs(scratch_sz > 0 ? scratch_sz : 1);

        // Extract |x|
        RawFloat x_rf = rf_extract(x, nw, arena);

        // Precompute x² (squaring is accelerated via square)
        uint64_t* x2_d = arena.alloc_limbs(nw + 2);
        RawFloat x2{x2_d, 0, 0};
        rf_sqr(x2, x_rf, prod_buf, scratch, nw);

        uint64_t* result_d = arena.alloc_limbs(nw + 2);
        std::memset(result_d, 0, (nw + 2) * sizeof(uint64_t));

        if (wp_bits >= 10000) {
            // Paterson-Stockmeyer: O(sqrt(l)) full-size multiplications
            RawFloat result{result_d, 0, 0};
            rf_cos_ps(result, x2, nw, prod_buf, scratch, arena, working_precision);
            return rf_to_float(result, false, working_precision);
        }

        // Naive Taylor series
        uint64_t* term_d = arena.alloc_limbs(nw + 2);
        std::memset(term_d, 0, (nw + 2) * sizeof(uint64_t));
        term_d[nw - 1] = uint64_t(1) << 63;
        RawFloat term{term_d, static_cast<size_t>(nw), -static_cast<int64_t>(nw * 64 - 1)};

        result_d[nw - 1] = uint64_t(1) << 63;
        RawFloat result{result_d, static_cast<size_t>(nw), -static_cast<int64_t>(nw * 64 - 1)};

        int max_terms = static_cast<int>(working_precision * 1.2) + 10;

        for (int k = 1; k <= max_terms; ++k) {
            rf_mul(term, term, x2, prod_buf, scratch, nw);
            uint64_t divisor = static_cast<uint64_t>(2*k - 1) * static_cast<uint64_t>(2*k);
            rf_divmod_1(term, divisor);
            if (term.nw == 0) break;
            if (k & 1) {
                if (!rf_sub(result, term, nw + 1)) break;
            } else {
                if (!rf_add(result, term, nw + 1)) break;
            }
        }

        // cos(x) = cos(-x) ≥ 0 (after argument reduction |x| ≤ π/4, so cos > 0)
        return rf_to_float(result, false, working_precision);
    }

    // cos computation via the double-angle formula: halve K times → cosTaylor → reconstruct via double-angle K times
    // cos(2θ) = 2cos²(θ) - 1
    // x is assumed to be in the range [0, π/2]
    static Float cosDoubling(const Float& x, int working_precision) {
        int wp_bits = Float::precisionToBits(working_precision);
        int K;
        if (wp_bits >= 10000) {
            // With P-S, Taylor is O(√N) → reduce K to lower the reconstruction cost
            // Optimal K: min(K + 3√(p/(2K))) → K = cbrt(wp_bits)
            K = static_cast<int>(std::cbrt(static_cast<double>(wp_bits)));
        } else {
            K = static_cast<int>(std::sqrt(wp_bits / 2.0));
        }
        if (K < 1) return cosTaylor(x, working_precision);

        // Guard bits: add 2K+α bits since the double-angle reconstruction amplifies error by up to 4^K
        int guard_bits = 2 * K + static_cast<int>(std::ceil(std::log2(K + 1))) + 10;
        int wp_inner = working_precision + Float::bitsToPrecision(guard_bits);

        Float x_red = ldexp(x, -K);  // x / 2^K — O(1) exponent operation
        x_red.truncateToApprox(wp_inner);
        // Even if the input is a double (eff=53), have Taylor computed at wp_inner precision.
        // Accurate because truncateToApprox has padded the mantissa to wp_inner bits.
        // Note: set wp_inner bits, not INT_MAX. With INT_MAX, division judges
        // "both exact" and falls back to defaultPrecision.
        int wp_inner_bits = Float::precisionToBits(wp_inner);
        x_red.setEffectiveBits(wp_inner_bits);

        Float c = cosTaylor(x_red, wp_inner);

        // K double-angle reconstructions (RawFloat — avoids per-op Float overhead)
        // cos(2θ) = 2cos²(θ) - 1
        int nw = (wp_inner_bits + 63) / 64;

        ScratchScope scope;
        auto& arena = getThreadArena();

        uint64_t* c_d = arena.alloc_limbs(nw + 2);
        RawFloat c_rf = rf_extract(c, nw, arena);
        // Copy c_rf's data into c_d (because rf_extract allocates in a separate buffer)
        std::memcpy(c_d, c_rf.d, c_rf.nw * sizeof(uint64_t));
        c_rf.d = c_d;

        size_t prod_alloc = 2 * static_cast<size_t>(nw) + 4;
        uint64_t* prod_buf = arena.alloc_limbs(prod_alloc);
        size_t sqr_scratch = mpn::square_scratch_size(nw + 1);
        size_t scratch_sz = std::max(mpn::multiply_scratch_size(nw + 1, nw + 1), sqr_scratch);
        uint64_t* scratch_buf = arena.alloc_limbs(scratch_sz > 0 ? scratch_sz : 1);

        bool c_negative = false;
        // K double-angle reconstructions: maintain full nw at every step
        //
        // The old implementation did gradual precision reduction (at step i, nw/2^(K-1-i) + guard),
        // but like expDoubling this rested on the mistaken assumption that
        // "squaring doubles precision". In reality rf_sqr truncates the output to needed_nw,
        // so squaring only maintains or reduces precision. Truncating to
        // cos_guard_limbs+2 limbs (~150 digits) at an early step caps the final accuracy
        // at ~150 digits regardless of prec.
        //
        // Correct schedule: maintain nw at every step (each double-angle reconstruction has
        // an error of about target_bits + 2K ULP, so the margin is already covered by
        // wp_inner, which includes guard_bits = 2K + log2(K) + 10).
        for (int i = 0; i < K; ++i) {
            // Truncate only when result exceeds nw (keep the top nw limbs)
            if (static_cast<int>(c_rf.nw) > static_cast<int>(nw)) {
                size_t drop = c_rf.nw - nw;
                std::memmove(c_rf.d, c_rf.d + drop, nw * sizeof(uint64_t));
                c_rf.nw = nw;
                c_rf.exp += static_cast<int64_t>(drop) * 64;
            }
            // c = c² (truncated) — the sign vanishes (accelerated via squaring)
            rf_sqr(c_rf, c_rf, prod_buf, scratch_buf, nw);
            c_negative = false;
            // c *= 2 (exponent shift)
            c_rf.exp += 1;
            // c -= 1.0: subtract 2^(-exp) from the mantissa V
            // Bit position of 1.0 = bit (-exp). word_idx = (-exp)/64, bit_idx = (-exp)%64
            // If word_idx >= nw: the bit of 1.0 is outside the mantissa range (above the MSB)
            //   → 2c² < 1, so the result is 1 - 2c² (negative)
            if (c_rf.nw > 0 && c_rf.exp < 0) {
                int64_t neg_exp = -c_rf.exp;
                size_t word_idx = static_cast<size_t>(neg_exp / 64);
                unsigned bit_idx = static_cast<unsigned>(neg_exp % 64);
                if (word_idx < c_rf.nw) {
                    uint64_t borrow = mpn::sub_1(
                        c_rf.d + word_idx, c_rf.nw - word_idx,
                        1ULL << bit_idx);
                    if (borrow) {
                        // 2c² < 1: obtain |1 - 2c²| via two's complement inversion
                        for (size_t j = 0; j < c_rf.nw; ++j)
                            c_rf.d[j] = ~c_rf.d[j];
                        mpn::add_1(c_rf.d, c_rf.nw, 1);
                        c_negative = true;
                    }
                } else {
                    // word_idx >= nw: 1.0 > 2c² → c = 1 - 2c² (sign flip)
                    // Build the mantissa of 1.0 and subtract: one - V
                    // one = 2^(-exp) = 2^(word_idx*64 + bit_idx)
                    // V = mantissa (nw words)
                    // |1 - 2c²| = 1 - V*2^exp
                    // This would need nw+1 or more words, but word_idx==nw and bit_idx==0 is typical
                    // In that case 1.0 = 2^(nw*64) → just invert V
                    if (word_idx == static_cast<size_t>(nw) && bit_idx == 0) {
                        // |1 - V*2^exp| = (2^(nw*64) - V) * 2^exp
                        // = two's complement of V
                        for (size_t j = 0; j < c_rf.nw; ++j)
                            c_rf.d[j] = ~c_rf.d[j];
                        mpn::add_1(c_rf.d, c_rf.nw, 1);
                        c_negative = true;  // 2c² < 1 → 2c²-1 < 0
                    } else {
                        // General case: fall back to Float
                        Float c_tmp = rf_to_float(c_rf, false, wp_inner);
                        c_tmp = c_tmp - Float::one(wp_inner);
                        if (c_tmp.isNegative()) {
                            c_tmp = -c_tmp;
                            c_negative = true;
                        }
                        c_rf = rf_extract(c_tmp, nw, arena);
                        std::memcpy(c_d, c_rf.d, c_rf.nw * sizeof(uint64_t));
                        c_rf.d = c_d;
                    }
                }
            }
            c_rf.nw = mpn::normalized_size(c_rf.d, c_rf.nw);
            rf_normalize(c_rf);
        }

        Float out = rf_to_float(c_rf, c_negative, wp_inner);
        out.truncateToApprox(working_precision);
        return out;
    }

    // Simultaneous (sin, cos) computation via the double-angle formula
    // sin(2θ) = 2sinθcosθ, cos(2θ) = 2cos²θ - 1
    // x is assumed to be in the range [0, π/2]
    static std::pair<Float, Float> sinCosDoubling(const Float& x, int working_precision) {
        int wp_bits = Float::precisionToBits(working_precision);
        int K;
        if (wp_bits >= 10000) {
            // With P-S, Taylor is O(√N) → reconstruction 2K + Taylor 6√(p/(2K))
            // Optimal K = cbrt(2·wp_bits) (since reconstruction is 2M(n)/step)
            K = static_cast<int>(std::cbrt(2.0 * wp_bits));
        } else {
            K = static_cast<int>(std::sqrt(wp_bits / 2.0));
        }
        if (K < 1) return { sinTaylor(x, working_precision), cosTaylor(x, working_precision) };

        int guard_bits = 2 * K + static_cast<int>(std::ceil(std::log2(K + 1))) + 10;
        int wp_inner = working_precision + Float::bitsToPrecision(guard_bits);

        Float x_red = ldexp(x, -K);
        x_red.truncateToApprox(wp_inner);
        // Even if the input is a double (eff=53), have Taylor computed at wp_inner precision.
        // Set wp_inner bits, not INT_MAX (avoids the division defaultPrecision problem).
        int wp_inner_bits = Float::precisionToBits(wp_inner);
        x_red.setEffectiveBits(wp_inner_bits);

        Float s_float = sinTaylor(x_red, wp_inner);
        Float c_float = cosTaylor(x_red, wp_inner);

        // K simultaneous reconstructions (RawFloat — avoids per-op Float overhead)
        // sin(2θ) = 2sinθcosθ, cos(2θ) = 2cos²θ - 1
        int nw = (wp_inner_bits + 63) / 64;

        ScratchScope scope;
        auto& arena = getThreadArena();

        uint64_t* s_d = arena.alloc_limbs(nw + 2);
        RawFloat s_rf = rf_extract(s_float, nw, arena);
        std::memcpy(s_d, s_rf.d, s_rf.nw * sizeof(uint64_t));
        s_rf.d = s_d;

        uint64_t* c_d = arena.alloc_limbs(nw + 2);
        RawFloat c_rf = rf_extract(c_float, nw, arena);
        std::memcpy(c_d, c_rf.d, c_rf.nw * sizeof(uint64_t));
        c_rf.d = c_d;

        size_t prod_alloc = 2 * static_cast<size_t>(nw) + 4;
        uint64_t* prod_buf = arena.alloc_limbs(prod_alloc);
        size_t sqr_scratch = mpn::square_scratch_size(nw + 1);
        size_t scratch_sz = std::max(mpn::multiply_scratch_size(nw + 1, nw + 1), sqr_scratch);
        uint64_t* scratch_buf = arena.alloc_limbs(scratch_sz > 0 ? scratch_sz : 1);

        // Temporary buffer for new_s
        uint64_t* tmp_d = arena.alloc_limbs(nw + 2);

        bool c_negative = false;
        bool s_negative = false;
        // Maintain full nw at every step.
        // The old implementation did gradual precision reduction (at step i, nw>>remaining + guard limbs),
        // but this rested on the mistaken assumption that "the double-angle reconstruction
        // sin(2θ)=2sc, cos(2θ)=2c²−1 doubles precision" (same kind as expDoubling's
        // BUG_FLOAT_EXP_HIGH_PRECISION). In reality rf_mul/rf_sqr truncate the output to needed_nw,
        // so reconstruction only maintains or reduces precision, and truncating at an early step
        // capped tan(1, 200)'s final accuracy at ~152 digits.
        // guard_bits = 2K + log2(K) + 10 includes the K-fold error amplification, so full nw is correct.
        for (int i = 0; i < K; ++i) {
            int needed_nw = nw;
            // truncate s_rf and c_rf
            if (static_cast<int>(s_rf.nw) > needed_nw) {
                size_t drop = s_rf.nw - needed_nw;
                std::memmove(s_rf.d, s_rf.d + drop, needed_nw * sizeof(uint64_t));
                s_rf.nw = needed_nw;
                s_rf.exp += static_cast<int64_t>(drop) * 64;
            }
            if (static_cast<int>(c_rf.nw) > needed_nw) {
                size_t drop = c_rf.nw - needed_nw;
                std::memmove(c_rf.d, c_rf.d + drop, needed_nw * sizeof(uint64_t));
                c_rf.nw = needed_nw;
                c_rf.exp += static_cast<int64_t>(drop) * 64;
            }
            // new_s = 2 * s * c (stored in a temporary buffer)
            // Sign: s_neg XOR c_neg (the sign rule for multiplication)
            RawFloat tmp_rf{tmp_d, 0, 0};
            rf_mul(tmp_rf, s_rf, c_rf, prod_buf, scratch_buf, needed_nw);
            tmp_rf.exp += 1;  // ×2
            bool new_s_neg = (s_negative != c_negative);

            // c = 2 * c² - 1 (the sign vanishes via c², accelerated via squaring)
            rf_sqr(c_rf, c_rf, prod_buf, scratch_buf, needed_nw);
            c_negative = false;
            c_rf.exp += 1;  // ×2
            // c -= 1.0: subtract 2^(-exp) from the mantissa
            if (c_rf.nw > 0 && c_rf.exp < 0) {
                int64_t neg_exp = -c_rf.exp;
                size_t word_idx = static_cast<size_t>(neg_exp / 64);
                unsigned bit_idx = static_cast<unsigned>(neg_exp % 64);
                if (word_idx < c_rf.nw) {
                    uint64_t borrow = mpn::sub_1(
                        c_rf.d + word_idx, c_rf.nw - word_idx,
                        1ULL << bit_idx);
                    if (borrow) {
                        for (size_t j = 0; j < c_rf.nw; ++j)
                            c_rf.d[j] = ~c_rf.d[j];
                        mpn::add_1(c_rf.d, c_rf.nw, 1);
                        c_negative = true;
                    }
                } else {
                    // word_idx >= nw: 1.0 > 2c² → c = 1 - 2c² (sign flip)
                    if (word_idx == static_cast<size_t>(nw) && bit_idx == 0) {
                        for (size_t j = 0; j < c_rf.nw; ++j)
                            c_rf.d[j] = ~c_rf.d[j];
                        mpn::add_1(c_rf.d, c_rf.nw, 1);
                        c_negative = true;
                    } else {
                        Float c_tmp = rf_to_float(c_rf, false, wp_inner);
                        c_tmp = c_tmp - Float::one(wp_inner);
                        if (c_tmp.isNegative()) {
                            c_tmp = -c_tmp;
                            c_negative = true;
                        }
                        c_rf = rf_extract(c_tmp, nw, arena);
                        std::memcpy(c_d, c_rf.d, c_rf.nw * sizeof(uint64_t));
                        c_rf.d = c_d;
                    }
                }
            }
            c_rf.nw = mpn::normalized_size(c_rf.d, c_rf.nw);
            rf_normalize(c_rf);

            // s = new_s
            std::memcpy(s_rf.d, tmp_rf.d, tmp_rf.nw * sizeof(uint64_t));
            s_rf.nw = tmp_rf.nw;
            s_rf.exp = tmp_rf.exp;
            s_negative = new_s_neg;
        }

        Float s_out = rf_to_float(s_rf, s_negative, wp_inner);
        Float c_out = rf_to_float(c_rf, c_negative, wp_inner);
        s_out.truncateToApprox(working_precision);
        c_out.truncateToApprox(working_precision);
        return { std::move(s_out), std::move(c_out) };
    }

    //=============================================================================
    // Bit-burst simultaneous sin/cos computation (Smith 2001)
    //=============================================================================
    // Split x into B-bit chunks and combine via the addition theorem sin(a+b) = sin(a)cos(b)+cos(a)sin(b).
    // Each chunk is tiny → Taylor converges in few terms.
    // Complexity: O(M(n) · log²n) (when combined with fixed-point BS; currently PS Taylor)
    // MPFR switches to this method above 30000 bits.

    // x ∈ [0, π/2] (already reduceToFirstQuadrant'd), working_precision is in decimal digits
    static std::pair<Float, Float> sincos_bitburst(const Float& x, int working_precision) {
        int wp_bits = Float::precisionToBits(working_precision);

        // Halving count K: optimal value for PS Taylor
        int K;
        if (wp_bits >= 10000) {
            K = static_cast<int>(std::cbrt(static_cast<double>(wp_bits)));
        } else {
            K = static_cast<int>(std::sqrt(wp_bits / 2.0));
        }
        if (K < 1) K = 1;

        // Chunk count L: keeps the combine cost (4L·M(p)) down while lightening the Taylor of later chunks
        // A large L increases combine cost; a small L makes chunk 0 heavy (= equivalent to the normal path)
        // Empirically L=8-16 balances combine and Taylor
        int L = 12;
        if (wp_bits < 40000) L = 8;
        if (wp_bits > 200000) L = 16;

        // Guard bits: double-angle reconstruction 4^K amplification + rounding error of L combines
        int guard_bits = 2 * K
                       + static_cast<int>(std::ceil(std::log2(K + L + 1)))
                       + 30;
        int wp_inner = working_precision + Float::bitsToPrecision(guard_bits);
        int wp_inner_bits = Float::precisionToBits(wp_inner);

        // Argument halving: x_red = x / 2^K
        Float x_red = ldexp(x, -K);
        x_red.truncateToApprox(wp_inner);
        x_red.setEffectiveBits(wp_inner_bits);

        if (x_red.isZero()) {
            return { Float::zero(), Float::one(working_precision) };
        }

        // Split the mantissa into L chunks
        const Int& mant = x_red.mantissa();
        int64_t base_exp = x_red.exponent();
        int mant_bl = static_cast<int>(mant.bitLength());
        int B = (mant_bl + L - 1) / L;  // Chunk size (bits)
        if (B < 1) B = 1;
        int actual_L = (mant_bl + B - 1) / B;

        // Mask: (1 << B) - 1
        Int mask = (Int(1) << B) - 1;

        // Initialize the (sin, cos) accumulators as Float
        Float S = Float::zero();
        Float C = Float::one(wp_inner);
        bool first_chunk = true;

        for (int j = 0; j < actual_L; ++j) {
            // Chunk j: extract bits [mant_bl - (j+1)*B, mant_bl - j*B)
            int bit_start = mant_bl - (j + 1) * B;
            Int chunk_val;
            if (bit_start >= 0) {
                chunk_val = (mant >> bit_start) & mask;
            } else {
                // Last fractional chunk
                int remaining_bits = mant_bl - j * B;
                Int last_mask = (Int(1) << remaining_bits) - 1;
                chunk_val = mant & last_mask;
                bit_start = 0;
            }
            if (chunk_val.isZero()) continue;

            // δ_j = chunk_val · 2^{base_exp + bit_start}
            int64_t chunk_exp = base_exp + bit_start;
            Float delta(std::move(chunk_val));
            delta = ldexp(std::move(delta), static_cast<int>(chunk_exp));
            delta.truncateToApprox(wp_inner);
            delta.setEffectiveBits(wp_inner_bits);

            // Estimate the magnitude of δ_j: MSB position
            int64_t delta_msb = delta.exponent()
                              + static_cast<int64_t>(delta.mantissa().bitLength());

            Float sd, cd;
            if (2 * (-delta_msb) > wp_inner_bits) {
                // |δ_j²| < 2^{-wp_inner_bits} → sin(δ) = δ, cos(δ) = 1 (within precision)
                sd = std::move(delta);
                cd = Float::one(wp_inner);
            } else {
                // sinTaylor/cosTaylor: automatically use PS internally (≥10000bit)
                sd = sinTaylor(delta, wp_inner);
                cd = cosTaylor(delta, wp_inner);
            }

            if (first_chunk) {
                S = std::move(sd);
                C = std::move(cd);
                first_chunk = false;
            } else {
                // Addition theorem: sin(a+δ) = sin(a)cos(δ) + cos(a)sin(δ)
                //                   cos(a+δ) = cos(a)cos(δ) - sin(a)sin(δ)
                // The 3-argument FloatOps version reduces temporary Float construction in multiplication
                Float t1, t2;
                FloatOps::mul(S, cd, t1);   // t1 = S*cos(δ)
                FloatOps::mul(C, sd, t2);   // t2 = C*sin(δ)
                Float new_S = t1 + t2;
                FloatOps::mul(C, cd, t1);   // t1 = C*cos(δ)
                FloatOps::mul(S, sd, t2);   // t2 = S*sin(δ)
                S = std::move(new_S);
                C = t1 - t2;
            }
            S.truncateToApprox(wp_inner);
            C.truncateToApprox(wp_inner);
        }

        if (first_chunk) {
            // All chunks are 0 → x_red = 0
            return { Float::zero(), Float::one(working_precision) };
        }

        // K double-angle reconstructions (accelerated with RawFloat)
        // sin(2θ) = 2sinθcosθ, cos(2θ) = 2cos²θ - 1
        int nw = (wp_inner_bits + 63) / 64;

        ScratchScope scope;
        auto& arena = getThreadArena();

        uint64_t* s_d = arena.alloc_limbs(nw + 2);
        RawFloat s_rf = rf_extract(S, nw, arena);
        std::memcpy(s_d, s_rf.d, s_rf.nw * sizeof(uint64_t));
        s_rf.d = s_d;

        uint64_t* c_d = arena.alloc_limbs(nw + 2);
        RawFloat c_rf = rf_extract(C, nw, arena);
        std::memcpy(c_d, c_rf.d, c_rf.nw * sizeof(uint64_t));
        c_rf.d = c_d;

        size_t prod_alloc = 2 * static_cast<size_t>(nw) + 4;
        uint64_t* prod_buf = arena.alloc_limbs(prod_alloc);
        size_t sqr_scratch = mpn::square_scratch_size(nw + 1);
        size_t scratch_sz = std::max(mpn::multiply_scratch_size(nw + 1, nw + 1), sqr_scratch);
        uint64_t* scratch_buf = arena.alloc_limbs(scratch_sz > 0 ? scratch_sz : 1);

        uint64_t* tmp_d = arena.alloc_limbs(nw + 2);

        bool c_negative = false;
        bool s_negative = false;
        // Gradual precision reduction (P2-3 technique)
        int bb_guard_limbs = (K + 63) / 64 + 6;
        for (int i = 0; i < K; ++i) {
            int remaining = K - 1 - i;
            int needed_nw = nw;
            if (remaining > 0 && remaining < 20) {
                needed_nw = (nw >> remaining) + bb_guard_limbs;
                if (needed_nw < bb_guard_limbs + 2) needed_nw = bb_guard_limbs + 2;
                if (needed_nw > nw) needed_nw = nw;
            } else if (remaining >= 20) {
                needed_nw = bb_guard_limbs + 2;
            }
            // truncate s_rf and c_rf
            if (static_cast<int>(s_rf.nw) > needed_nw) {
                size_t drop = s_rf.nw - needed_nw;
                std::memmove(s_rf.d, s_rf.d + drop, needed_nw * sizeof(uint64_t));
                s_rf.nw = needed_nw;
                s_rf.exp += static_cast<int64_t>(drop) * 64;
            }
            if (static_cast<int>(c_rf.nw) > needed_nw) {
                size_t drop = c_rf.nw - needed_nw;
                std::memmove(c_rf.d, c_rf.d + drop, needed_nw * sizeof(uint64_t));
                c_rf.nw = needed_nw;
                c_rf.exp += static_cast<int64_t>(drop) * 64;
            }
            // new_s = 2 * s * c
            RawFloat tmp_rf{tmp_d, 0, 0};
            rf_mul(tmp_rf, s_rf, c_rf, prod_buf, scratch_buf, needed_nw);
            tmp_rf.exp += 1;  // ×2
            bool new_s_neg = (s_negative != c_negative);

            // c = 2 * c² - 1
            rf_sqr(c_rf, c_rf, prod_buf, scratch_buf, needed_nw);
            c_negative = false;
            c_rf.exp += 1;  // ×2
            // c -= 1.0
            if (c_rf.nw > 0 && c_rf.exp < 0) {
                int64_t neg_exp = -c_rf.exp;
                size_t word_idx = static_cast<size_t>(neg_exp / 64);
                unsigned bit_idx = static_cast<unsigned>(neg_exp % 64);
                if (word_idx < c_rf.nw) {
                    uint64_t borrow = mpn::sub_1(
                        c_rf.d + word_idx, c_rf.nw - word_idx,
                        1ULL << bit_idx);
                    if (borrow) {
                        for (size_t jj = 0; jj < c_rf.nw; ++jj)
                            c_rf.d[jj] = ~c_rf.d[jj];
                        mpn::add_1(c_rf.d, c_rf.nw, 1);
                        c_negative = true;
                    }
                } else {
                    if (word_idx == static_cast<size_t>(nw) && bit_idx == 0) {
                        for (size_t jj = 0; jj < c_rf.nw; ++jj)
                            c_rf.d[jj] = ~c_rf.d[jj];
                        mpn::add_1(c_rf.d, c_rf.nw, 1);
                        c_negative = true;
                    } else {
                        Float c_tmp = rf_to_float(c_rf, false, wp_inner);
                        c_tmp = c_tmp - Float::one(wp_inner);
                        if (c_tmp.isNegative()) {
                            c_tmp = -c_tmp;
                            c_negative = true;
                        }
                        c_rf = rf_extract(c_tmp, nw, arena);
                        std::memcpy(c_d, c_rf.d, c_rf.nw * sizeof(uint64_t));
                        c_rf.d = c_d;
                    }
                }
            }
            c_rf.nw = mpn::normalized_size(c_rf.d, c_rf.nw);
            rf_normalize(c_rf);

            // s = new_s
            std::memcpy(s_rf.d, tmp_rf.d, tmp_rf.nw * sizeof(uint64_t));
            s_rf.nw = tmp_rf.nw;
            s_rf.exp = tmp_rf.exp;
            s_negative = new_s_neg;
        }

        Float s_out = rf_to_float(s_rf, s_negative, wp_inner);
        Float c_out = rf_to_float(c_rf, c_negative, wp_inner);
        s_out.truncateToApprox(working_precision);
        c_out.truncateToApprox(working_precision);
        return { std::move(s_out), std::move(c_out) };
    }

    //=============================================================================
    // Trigonometric common range reduction
    //=============================================================================

    // Reduce x > 0 to [0, π/2] and return the sin/cos sign flags
    struct TrigReduced {
        Float x;              // Value reduced to [0, π/2]
        bool sin_negative;    // sin sign-flip flag
        bool cos_negative;    // cos sign-flip flag
    };

    static TrigReduced reduceToFirstQuadrant(Float x, int working_precision) {
        x.truncateToApprox(working_precision);

        // Dynamically extend π according to the magnitude of |x| (MPFR-style argument reduction).
        // x mod 2π is essentially x - k·2π, and the larger x is, the more of the leading log₂|x| bits
        // are lost to catastrophic cancellation. To prevent this, k·2π must be representable exactly
        // at the same bit length as x, so extend π's precision to
        //   pi_bits = max(0, msb_bits(x)) + bits(working_precision) + GUARD
        // This keeps x mod 2π at working_precision bits.
        // If |x| < 2π then msb_bits(x) ≤ 3 and the contribution is negligible (the extension does not run).
        int reduce_precision = working_precision;
        if (!x.isZero()) {
            int64_t x_msb_bits = x.exponent()
                               + static_cast<int64_t>(x.mantissa().bitLength());
            if (x_msb_bits > 0) {
                int wp_bits = Float::precisionToBits(working_precision);
                int64_t pi_bits = x_msb_bits + static_cast<int64_t>(wp_bits) + 16;
                // Clip to the INT_MAX upper bound (a safeguard if |x| is an unrealistically huge value)
                if (pi_bits > static_cast<int64_t>(INT_MAX) - 64) {
                    pi_bits = static_cast<int64_t>(INT_MAX) - 64;
                }
                int extended = Float::bitsToPrecision(static_cast<int>(pi_bits));
                if (extended > reduce_precision) reduce_precision = extended;
            }
        }

        Float pi = Float::pi(reduce_precision);
        Float pi2 = ldexp(pi, 1);

        // x mod 2π — extending π prevents precision loss and keeps working_precision
        if (x > pi2) {
            Int k = (x / pi2).toInt();
            x = x - Float(k) * pi2;
        }

        // Reduce to [0, π/2] — four-quadrant sign table:
        //   Q1 [0, π/2]    : sin+, cos+
        //   Q2 (π/2, π]    : sin+, cos-
        //   Q3 (π, 3π/2]   : sin-, cos-
        //   Q4 (3π/2, 2π)  : sin-, cos+
        bool sin_neg = false;
        bool cos_neg = false;
        Float pi_half = ldexp(pi, -1);

        if (x > pi_half && x <= pi) {
            x = pi - x;
            cos_neg = true;
        }
        else if (x > pi && x <= pi + pi_half) {
            x = x - pi;
            sin_neg = true;
            cos_neg = true;
        }
        else if (x > pi + pi_half) {
            x = pi2 - x;
            sin_neg = true;
        }

        x.truncateToApprox(working_precision);
        return { std::move(x), sin_neg, cos_neg };
    }

    //=============================================================================
    // Sine (sin)
    //=============================================================================

    // Body: takes x by value (assumes already moved, x > 0)
    // TRIG-2: cosDoubling + sqrt(1 - cos²) saves one Taylor pass
    // Bit-burst switchover threshold
    // The current implementation uses the existing PS Taylor + Float combination, which is slower than cosDoubling+sqrt.
    // As it stands, bit-burst uses 2 Float-level Taylor passes + K doublings,
    // which is slower than the cosDoubling + sqrt(1-cos²) path.
    // It will be enabled after an integer-level BS is implemented in the future.
    // Until then, set the threshold to infinity and use the cosDoubling + sqrt path.
    static constexpr int BITBURST_THRESHOLD = INT_MAX;

    static Float sin_core(Float x, int eff_x, int precision) {
        int compute_prec = effectiveComputePrecision(eff_x, precision);
        // (a) Properly guard the Taylor series roundoff accumulation with log10(N_terms).
        // (b) Inside argument reduction, extend π according to the magnitude of |x| (see reduceToFirstQuadrant).
        int working_precision = taylorWorkingPrecision(compute_prec);

        auto reduced = reduceToFirstQuadrant(std::move(x), working_precision);

        Float result;
        if (reduced.x.isZero()) {
            result = Float::zero();
        } else {
            int64_t x_msb = reduced.x.exponent()
                          + static_cast<int64_t>(reduced.x.mantissa().bitLength());
            int wp_bits = Float::precisionToBits(working_precision);

            if (x_msb < -(wp_bits / 4) || wp_bits < 1000) {
                result = sinTaylor(reduced.x, working_precision);
            } else if (wp_bits >= BITBURST_THRESHOLD) {
                // Bit-burst: simultaneous sin/cos computation, no sqrt needed
                auto [s, c] = sincos_bitburst(reduced.x, working_precision);
                result = std::move(s);
            } else {
                // High-precision path: cos + sqrt(1 - cos²)
                int extra_guard = (x_msb < 0) ? static_cast<int>(-2 * x_msb) : 0;
                int wp_cos = working_precision + Float::bitsToPrecision(extra_guard);
                Float c = cosDoubling(reduced.x, wp_cos);
                Float one_minus_c2 = Float::one(wp_cos) - c * c;
                result = sqrt(std::move(one_minus_c2), working_precision);
            }
        }

        if (reduced.sin_negative) result = -result;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    // sin/cos Q128 fast path (19 digits, precision_bits ≤ 64)
    // Argument reduction z = x - k·(π/2) ∈ [-π/4, π/4] → Q128 Taylor sin/cos
    // Assumes x > 0 (guaranteed by the sin caller; cos handles it via fabs)
    static Float trig_small_impl(const Float& x, bool want_sin, int precision) {
        const Int& mant = x.mantissa();
        const uint64_t* mw = mant.data();
        uint64_t m_val = mw[0];
        int64_t e = x.exponent();
        int m_bits = 64 - std::countl_zero(m_val);
        int64_t total_bl = static_cast<int64_t>(m_bits) + e;

        // x is tiny (< 2^{-64}) → sin(x) ≈ x, cos(x) ≈ 1
        if (total_bl < -64) {
            if (want_sin) {
                Float r(x);
                r.setResultPrecision(precision);
                return r;
            }
            return Float::one(precision);
        }

        // k = round(x / (π/2))
        double x_d = x.toDouble();
        if (!want_sin) x_d = std::fabs(x_d);  // cos(-x) = cos(x)
        constexpr double two_over_pi = 0.6366197723675814;
        int k = static_cast<int>(std::round(x_d * two_over_pi));

        // Quadrant mapping
        // sin(k·π/2 + z): k%4 = 0→sin(z), 1→cos(z), 2→-sin(z), 3→-cos(z)
        // cos(k·π/2 + z): k%4 = 0→cos(z), 1→-sin(z), 2→-cos(z), 3→sin(z)
        int quad = ((k % 4) + 4) % 4;
        bool use_cos_taylor, negate;
        if (want_sin) {
            use_cos_taylor = (quad == 1 || quad == 3);
            negate = (quad == 2 || quad == 3);
        } else {
            use_cos_taylor = (quad == 0 || quad == 2);
            negate = (quad == 1 || quad == 2);
        }

        // Q128 constant of (π/2 - 1)
        // π/2 = 1.921FB54442D18469_898CC51701B839A2...₁₆
        constexpr uint64_t phf_hi = 0x921FB54442D18469ULL;
        constexpr uint64_t phf_lo = 0x898CC51701B839A2ULL;

        uint64_t abs_k = static_cast<uint64_t>(k < 0 ? -k : k);

        // k·(π/2-1) → integer part + Q128 fractional part
        uint64_t kphf_frac_hi, kphf_frac_lo;
        uint64_t kphf_int;
        {
            uint64_t p0_hi;
            uint64_t p0_lo = _umul128(phf_lo, abs_k, &p0_hi);
            uint64_t p1_hi;
            uint64_t p1_lo = _umul128(phf_hi, abs_k, &p1_hi);
            unsigned char c = _addcarry_u64(0, p1_lo, p0_hi, &kphf_frac_hi);
            kphf_frac_lo = p0_lo;
            kphf_int = p1_hi + static_cast<uint64_t>(c);
        }

        // Extract the integer part and Q128 fractional part of x (the mantissa always represents |x|)
        uint64_t int_x = 0;
        uint64_t x_frac_hi = 0, x_frac_lo = 0;
        if (total_bl <= 0) {
            // |x| < 1: no integer part
            int pos = static_cast<int>(128 + e);
            if (pos >= 64) {
                x_frac_hi = m_val << (pos - 64);
            } else if (pos > 0) {
                x_frac_hi = m_val >> (64 - pos);
                x_frac_lo = m_val << pos;
            } else if (pos == 0) {
                x_frac_lo = m_val;
            }
        } else {
            if (e < 0) {
                int_x = m_val >> (-e);
                uint64_t frac_mask = (1ULL << (-e)) - 1;
                uint64_t frac_val = m_val & frac_mask;
                if (frac_val != 0) {
                    int pos = static_cast<int>(128 + e);
                    if (pos >= 64) {
                        x_frac_hi = frac_val << (pos - 64);
                    } else if (pos > 0) {
                        x_frac_hi = frac_val >> (64 - pos);
                        x_frac_lo = frac_val << pos;
                    }
                }
            } else {
                // e >= 0: x is an integer, no fractional part
                int_x = m_val << e;
            }
        }

        // Compute frac(x) - frac(k·(π/2-1)) in unsigned Q128
        uint64_t fd_hi, fd_lo;
        unsigned char borrow1 = _subborrow_u64(0, x_frac_lo, kphf_frac_lo, &fd_lo);
        unsigned char borrow2 = _subborrow_u64(borrow1, x_frac_hi, kphf_frac_hi, &fd_hi);

        // Integer correction: z = (int_x - k - kphf_int - borrow) + fd/2^128
        // Since k·(π/2) = k + k·(π/2-1), int(k·(π/2)) = k + kphf_int
        int64_t effective_corr = static_cast<int64_t>(int_x)
                               - static_cast<int64_t>(k)
                               - static_cast<int64_t>(kphf_int)
                               - static_cast<int64_t>(borrow2);

        // z = effective_corr + fd/2^128, |z| < π/4 < 1
        // effective_corr is only 0 (z ≥ 0) or -1 (z < 0)
        uint64_t z_hi, z_lo;
        bool z_neg;
        if (effective_corr == 0) {
            z_hi = fd_hi; z_lo = fd_lo;
            z_neg = false;
        } else if (effective_corr == -1) {
            // z < 0: |z| = (2^128 - fd) / 2^128
            unsigned char nb = _subborrow_u64(0, 0, fd_lo, &z_lo);
            _subborrow_u64(nb, 0, fd_hi, &z_hi);
            z_neg = true;
        } else {
            // Unexpected → fall back to the general path
            Float x_copy(x);
            if (!want_sin) x_copy = abs(x_copy);
            if (want_sin)
                return sin_core(std::move(x_copy), x.effectiveBits(), precision);
            else
                return cos_core(std::move(x_copy), x.effectiveBits(), precision);
        }

        // sin(z): sin(-z) = -sin(z) → sign flip
        // cos(z): cos(-z) = cos(z) → unchanged
        if (!use_cos_taylor && z_neg) negate = !negate;

        if (z_hi == 0 && z_lo == 0) {
            if (use_cos_taylor) {
                Float result = Float::one(precision);
                return negate ? -result : result;
            }
            return Float::zero(precision);
        }

        // Compute z² in Q128
        uint64_t z2_hi, z2_lo;
        q128_mul(z_hi, z_lo, z_hi, z_lo, z2_hi, z2_lo);

        Float result;
        if (use_cos_taylor) {
            // 1 - cos(|z|) = z²/2! - z⁴/4! + z⁶/6! - ...
            // term₀ = z²/2, recurrence: term_{n+1} = term_n · z² / ((2n+1)(2n+2))
            uint64_t rem;
            uint64_t term_hi = _udiv128(0, z2_hi, 2, &rem);
            uint64_t term_lo = _udiv128(rem, z2_lo, 2, &rem);
            uint64_t sum_hi = term_hi, sum_lo = term_lo;

            for (int n = 1; n <= 20; n++) {
                q128_mul(term_hi, term_lo, z2_hi, z2_lo, term_hi, term_lo);
                uint64_t divisor = static_cast<uint64_t>(2*n + 1)
                                 * static_cast<uint64_t>(2*n + 2);
                term_hi = _udiv128(0, term_hi, divisor, &rem);
                term_lo = _udiv128(rem, term_lo, divisor, &rem);
                if (term_hi == 0 && term_lo == 0) break;
                if (n & 1) {
                    unsigned char b = _subborrow_u64(0, sum_lo, term_lo, &sum_lo);
                    _subborrow_u64(b, sum_hi, term_hi, &sum_hi);
                } else {
                    unsigned char c = _addcarry_u64(0, sum_lo, term_lo, &sum_lo);
                    _addcarry_u64(c, sum_hi, term_hi, &sum_hi);
                }
            }

            // cos(z) = 1 - sum → mantissa = 2^128 - sum, exponent = -128
            // |z| < π/4 → cos(z) > cos(π/4) ≈ 0.707 → mantissa > 2^127 → always 2 words
            uint64_t cos_lo, cos_hi;
            unsigned char b = _subborrow_u64(0, 0, sum_lo, &cos_lo);
            _subborrow_u64(b, 0, sum_hi, &cos_hi);
            uint64_t words[2] = { cos_lo, cos_hi };
            auto mant2 = Int::fromRawWordsPreNormalized(
                std::span<const uint64_t>(words, 2), 1);
            result = Float(std::move(mant2), -128, negate);
        } else {
            // sin(|z|) = z - z³/3! + z⁵/5! - ...
            // term₀ = z, recurrence: term_{n+1} = term_n · z² / ((2n)(2n+1))
            uint64_t sum_hi = z_hi, sum_lo = z_lo;
            uint64_t term_hi = z_hi, term_lo = z_lo;

            for (int n = 1; n <= 20; n++) {
                q128_mul(term_hi, term_lo, z2_hi, z2_lo, term_hi, term_lo);
                uint64_t divisor = static_cast<uint64_t>(2*n)
                                 * static_cast<uint64_t>(2*n + 1);
                uint64_t rem;
                term_hi = _udiv128(0, term_hi, divisor, &rem);
                term_lo = _udiv128(rem, term_lo, divisor, &rem);
                if (term_hi == 0 && term_lo == 0) break;
                if (n & 1) {
                    unsigned char b = _subborrow_u64(0, sum_lo, term_lo, &sum_lo);
                    _subborrow_u64(b, sum_hi, term_hi, &sum_hi);
                } else {
                    unsigned char c = _addcarry_u64(0, sum_lo, term_lo, &sum_lo);
                    _addcarry_u64(c, sum_hi, term_hi, &sum_hi);
                }
            }

            size_t wn = (sum_hi != 0) ? 2 : ((sum_lo != 0) ? 1 : 0);
            if (wn == 0) return Float::zero(precision);
            uint64_t words[2] = { sum_lo, sum_hi };
            auto mant2 = Int::fromRawWordsPreNormalized(
                std::span<const uint64_t>(words, wn), 1);
            result = Float(std::move(mant2), -128, negate);
        }

        result.setResultPrecision(precision);
        return result;
    }

    // sin/cos 38-digit fast path: lightweight Float argument reduction + sinTaylor/cosTaylor
    // Assumes x > 0 (guaranteed by the caller)
    static Float trig_medium_impl(const Float& x, bool want_sin, int precision) {
        double x_d = x.toDouble();

        // k = round(x / (π/2))
        constexpr double two_over_pi = 0.6366197723675814;
        int k = static_cast<int>(std::round(x_d * two_over_pi));

        // z = |x| - k·(π/2)
        int wp = precision + 6;
        Float z;
        if (k == 0) {
            z = x;
        } else {
            Float pi_half = ldexp(Float::pi(wp), -1);
            z = x - mulScalarF(pi_half, static_cast<uint64_t>(k));
        }

        // Determine the quadrant from k%4
        int quad = ((k % 4) + 4) % 4;
        bool negate = false;
        bool use_cos = false;

        if (want_sin) {
            // sin(z + k·π/2): 0→sin, 1→cos, 2→-sin, 3→-cos
            switch (quad) {
            case 0: break;
            case 1: use_cos = true; break;
            case 2: negate = true; break;
            case 3: use_cos = true; negate = true; break;
            }
        } else {
            // cos(z + k·π/2): 0→cos, 1→-sin, 2→-cos, 3→sin
            switch (quad) {
            case 0: use_cos = true; break;
            case 1: negate = true; break;
            case 2: use_cos = true; negate = true; break;
            case 3: break;
            }
        }

        Float result;
        if (use_cos) {
            result = cosTaylor(z, wp);
        } else {
            result = sinTaylor(z, wp);
        }

        if (negate) result = -result;
        result.setResultPrecision(precision);
        return result;
    }

    Float sin(const Float& x, int precision) {
        // Handle special values
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::nan();
        if (x.isZero()) return Float::zero();
        // Range reduction: sin(-x) = -sin(x)
        if (x.isNegative()) return -sin(-x, precision);

        int precision_bits = Float::precisionToBits(precision);
        if (x.mantissa().size() <= 1 && precision_bits <= 64) {
            int64_t total_bl = static_cast<int64_t>(x.mantissa().bitLength()) + x.exponent();
            if (total_bl <= 52)
                return trig_small_impl(x, true, precision);
        }
        if (x.mantissa().size() <= 2 && precision_bits <= 128)
            return trig_medium_impl(x, true, precision);
        return zivRound([](const Float& a, int e, int p) {
            return sin_core(Float(a), e, p);
        }, x, x.effectiveBits(), precision);
    }

    Float sin(Float&& x, int precision) {
        // Handle special values
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::nan();
        if (x.isZero()) return Float::zero();
        // Range reduction: sin(-x) = -sin(x)
        if (x.isNegative()) return -sin(-x, precision);

        int precision_bits = Float::precisionToBits(precision);
        if (x.mantissa().size() <= 1 && precision_bits <= 64) {
            int64_t total_bl = static_cast<int64_t>(x.mantissa().bitLength()) + x.exponent();
            if (total_bl <= 52)
                return trig_small_impl(x, true, precision);
        }
        if (x.mantissa().size() <= 2 && precision_bits <= 128)
            return trig_medium_impl(x, true, precision);
        return zivRound([](const Float& a, int e, int p) {
            return sin_core(Float(a), e, p);
        }, x, x.effectiveBits(), precision);
    }

    //=============================================================================
    // Cosine (cos)
    //=============================================================================

    // Body: takes x by value (assumes already moved)
    // Since cos(-x) = cos(x), the caller need not take abs — reduceToFirstQuadrant handles it
    static Float cos_core(Float x, int eff_x, int precision) {
        int compute_prec = effectiveComputePrecision(eff_x, precision);
        // (a) Properly guard the Taylor series roundoff accumulation with log10(N_terms).
        // (b) Inside argument reduction, extend π according to the magnitude of |x| (see reduceToFirstQuadrant).
        int working_precision = taylorWorkingPrecision(compute_prec);

        // cos(-x) = cos(x)
        x = abs(x);
        auto reduced = reduceToFirstQuadrant(std::move(x), working_precision);

        int wp_bits = Float::precisionToBits(working_precision);
        Float result;
        if (reduced.x.isZero()) {
            result = Float::one(working_precision);
        } else if (wp_bits >= BITBURST_THRESHOLD) {
            // Bit-burst: simultaneous sin/cos computation
            auto [s, c] = sincos_bitburst(reduced.x, working_precision);
            result = std::move(c);
        } else {
            int64_t x_msb = reduced.x.exponent()
                          + static_cast<int64_t>(reduced.x.mantissa().bitLength());
            if (wp_bits < 1000 && x_msb <= -1) {
                result = cosTaylor(reduced.x, working_precision);
            } else {
                result = cosDoubling(reduced.x, working_precision);
            }
        }

        if (reduced.cos_negative) result = -result;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float cos(const Float& x, int precision) {
        // Handle special values
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::nan();
        if (x.isZero()) return Float::one(precision);

        int precision_bits = Float::precisionToBits(precision);
        if (x.mantissa().size() <= 1 && precision_bits <= 64) {
            int64_t total_bl = static_cast<int64_t>(x.mantissa().bitLength()) + x.exponent();
            if (total_bl <= 52)
                return trig_small_impl(x, false, precision);
        }
        if (x.mantissa().size() <= 2 && precision_bits <= 128) {
            Float abs_x = abs(x);
            return trig_medium_impl(abs_x, false, precision);
        }
        return zivRound([](const Float& a, int e, int p) {
            return cos_core(Float(a), e, p);
        }, x, x.effectiveBits(), precision);
    }

    Float cos(Float&& x, int precision) {
        // Handle special values
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::nan();
        if (x.isZero()) return Float::one(precision);

        int precision_bits = Float::precisionToBits(precision);
        if (x.mantissa().size() <= 1 && precision_bits <= 64) {
            int64_t total_bl = static_cast<int64_t>(x.mantissa().bitLength()) + x.exponent();
            if (total_bl <= 52)
                return trig_small_impl(x, false, precision);
        }
        if (x.mantissa().size() <= 2 && precision_bits <= 128) {
            Float abs_x = abs(x);
            return trig_medium_impl(abs_x, false, precision);
        }
        return zivRound([](const Float& a, int e, int p) {
            return cos_core(Float(a), e, p);
        }, x, x.effectiveBits(), precision);
    }

    //=============================================================================
    // Tangent (tan)
    //=============================================================================

    // TRIG-2: directly use sinCosDoubling to reduce to one range reduction + 2 Taylor passes
    static Float tan_core(Float x, int eff_x, int precision) {
        int compute_prec = effectiveComputePrecision(eff_x, precision);
        // (a) Guard the sin/cos Taylor roundoff accumulation with log10(N_terms).
        // (b) Inside argument reduction, extend π according to the magnitude of |x|.
        int working_precision = taylorWorkingPrecision(compute_prec);

        bool input_negative = x.isNegative();
        x = abs(x);
        auto reduced = reduceToFirstQuadrant(std::move(x), working_precision);

        int wp_bits = Float::precisionToBits(working_precision);
        Float s, c;

        if (reduced.x.isZero()) {
            s = Float::zero();
            c = Float::one(working_precision);
        } else if (wp_bits >= 1000) {
            // High precision: cosDoubling + sqrt(1 - cos²) saves one Taylor pass
            // sinCosDoubling: 2 Taylor + K×4M(n) reconstruction
            // cosDoubling+sqrt: 1 Taylor + K×2M(n) reconstruction + sqrt ≈ half the cost
            int64_t x_msb = reduced.x.exponent()
                          + static_cast<int64_t>(reduced.x.mantissa().bitLength());
            int extra_guard = (x_msb < 0) ? static_cast<int>(-2 * x_msb) : 0;
            int wp_cos = working_precision + Float::bitsToPrecision(extra_guard);

            c = cosDoubling(reduced.x, wp_cos);
            Float one_minus_c2 = Float::one(wp_cos) - c * c;
            s = sqrt(std::move(one_minus_c2), working_precision);
            c.truncateToApprox(working_precision);
        } else {
            // Low precision: sinCosDoubling (overhead dominates)
            auto [sv, cv] = sinCosDoubling(reduced.x, working_precision);
            s = std::move(sv);
            c = std::move(cv);
        }

        if (reduced.sin_negative) s = -s;
        if (reduced.cos_negative) c = -c;

        // cos ≈ 0 → tan → ±∞
        if (c.isZero()) {
            bool result_neg = input_negative ? !s.isNegative() : s.isNegative();
            return result_neg ? Float::negativeInfinity() : Float::positiveInfinity();
        }

        Float result = s / c;
        if (input_negative) result = -result;  // tan(-x) = -tan(x)
        finalizeResult(result, eff_x, precision);
        return result;
    }

    // tan Q128 fast path: sin_small / cos_small → Float division
    static Float tan_small(const Float& x, int precision) {
        Float s = trig_small_impl(x, true, precision + 1);
        Float c = trig_small_impl(x, false, precision + 1);
        if (c.isZero()) {
            return s.isNegative() ? Float::negativeInfinity() : Float::positiveInfinity();
        }
        Float result = s / c;
        result.setResultPrecision(precision);
        return result;
    }

    // tan 38-digit fast path: share argument reduction to compute sin/cos at once
    static Float tan_medium(const Float& x, int precision) {
        double x_d = x.toDouble();
        constexpr double two_over_pi = 0.6366197723675814;
        int k = static_cast<int>(std::round(x_d * two_over_pi));

        int wp = precision + 4;
        Float z;
        if (k == 0) {
            z = x;
        } else {
            Float pi_half = ldexp(Float::pi(wp), -1);
            z = x - mulScalarF(pi_half, static_cast<uint64_t>(k));
        }

        // Compute both sinTaylor and cosTaylor
        Float sv = sinTaylor(z, wp);
        Float cv = cosTaylor(z, wp);

        // Sign adjustment from k%4
        int quad = ((k % 4) + 4) % 4;
        // tan(z + k·π/2):
        //   k%4=0: sin/cos, k%4=1: cos/(-sin), k%4=2: (-sin)/(-cos)=sin/cos, k%4=3: (-cos)/sin
        Float num, den;
        switch (quad) {
        case 0: num = std::move(sv); den = std::move(cv); break;
        case 1: num = std::move(cv); den = -std::move(sv); break;
        case 2: num = std::move(sv); den = std::move(cv); break;
        case 3: num = -std::move(cv); den = std::move(sv); break;
        }

        if (den.isZero()) {
            return num.isNegative() ? Float::negativeInfinity() : Float::positiveInfinity();
        }
        Float result = num / den;
        result.setResultPrecision(precision);
        return result;
    }

    Float tan(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::nan();
        if (x.isZero()) return Float::zero();

        int precision_bits = Float::precisionToBits(precision);
        if (x.mantissa().size() <= 1 && precision_bits <= 64) {
            const Float& abs_x = x.isNegative() ? static_cast<const Float&>(-x) : x;
            int64_t total_bl = static_cast<int64_t>(x.mantissa().bitLength()) + x.exponent();
            if (total_bl <= 52) {
                // tan(-x) = -tan(x): trig_small_impl assumes x > 0
                if (x.isNegative()) {
                    Float neg_x = -x;
                    return -tan_small(neg_x, precision);
                }
                return tan_small(x, precision);
            }
        }
        if (x.mantissa().size() <= 2 && precision_bits <= 128) {
            if (x.isNegative()) {
                Float neg_x = -x;
                return -tan_medium(neg_x, precision);
            }
            return tan_medium(x, precision);
        }
        return zivRound([](const Float& a, int e, int p) {
            return tan_core(Float(a), e, p);
        }, x, x.effectiveBits(), precision);
    }

    Float tan(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::nan();
        if (x.isZero()) return Float::zero();

        int precision_bits = Float::precisionToBits(precision);
        if (x.mantissa().size() <= 1 && precision_bits <= 64) {
            int64_t total_bl = static_cast<int64_t>(x.mantissa().bitLength()) + x.exponent();
            if (total_bl <= 52) {
                if (x.isNegative()) {
                    Float neg_x = -x;
                    return -tan_small(neg_x, precision);
                }
                return tan_small(x, precision);
            }
        }
        if (x.mantissa().size() <= 2 && precision_bits <= 128) {
            if (x.isNegative()) {
                Float neg_x = -x;
                return -tan_medium(neg_x, precision);
            }
            return tan_medium(x, precision);
        }
        return zivRound([](const Float& a, int e, int p) {
            return tan_core(Float(a), e, p);
        }, x, x.effectiveBits(), precision);
    }

    //=============================================================================
    // Hyperbolic functions common: Taylor series + double-angle reconstruction
    //=============================================================================

    // Unified sinh+cosh Taylor (naive path): shares x², arena, scratch
    // sinh(x) = x + x³/3! + x⁵/5! + ... (all terms positive)
    // cosh(x) = 1 + x²/2! + x⁴/4! + ... (all terms positive)
    static std::pair<Float, Float> sinhCoshTaylorNaive(const Float& x, int working_precision) {
        int wp_bits = Float::precisionToBits(working_precision);
        int nw = (wp_bits + 63) / 64;

        ScratchScope scope;
        auto& arena = getThreadArena();

        size_t prod_alloc = 2 * static_cast<size_t>(nw) + 4;
        uint64_t* prod_buf = arena.alloc_limbs(prod_alloc);
        size_t mul_scratch = mpn::multiply_scratch_size(nw + 1, nw + 1);
        size_t sqr_scratch = mpn::square_scratch_size(nw + 1);
        size_t scratch_sz = std::max(mul_scratch, sqr_scratch);
        uint64_t* scratch = arena.alloc_limbs(scratch_sz > 0 ? scratch_sz : 1);

        RawFloat x_rf = rf_extract(x, nw, arena);
        bool x_negative = x.isNegative();

        // Share x² (only one squaring needed)
        uint64_t* x2_d = arena.alloc_limbs(nw + 2);
        RawFloat x2{x2_d, 0, 0};
        rf_sqr(x2, x_rf, prod_buf, scratch, nw);

        // sinh: term=x, result=x
        uint64_t* s_term_d = arena.alloc_limbs(nw + 2);
        std::memset(s_term_d, 0, (nw + 2) * sizeof(uint64_t));
        std::memcpy(s_term_d, x_rf.d, x_rf.nw * sizeof(uint64_t));
        RawFloat s_term{s_term_d, x_rf.nw, x_rf.exp};

        uint64_t* s_result_d = arena.alloc_limbs(nw + 2);
        std::memset(s_result_d, 0, (nw + 2) * sizeof(uint64_t));
        std::memcpy(s_result_d, x_rf.d, x_rf.nw * sizeof(uint64_t));
        RawFloat s_result{s_result_d, x_rf.nw, x_rf.exp};

        // cosh: term=1, result=1
        uint64_t* c_term_d = arena.alloc_limbs(nw + 2);
        std::memset(c_term_d, 0, (nw + 2) * sizeof(uint64_t));
        c_term_d[nw - 1] = uint64_t(1) << 63;
        RawFloat c_term{c_term_d, static_cast<size_t>(nw), -static_cast<int64_t>(nw * 64 - 1)};

        uint64_t* c_result_d = arena.alloc_limbs(nw + 2);
        std::memset(c_result_d, 0, (nw + 2) * sizeof(uint64_t));
        c_result_d[nw - 1] = uint64_t(1) << 63;
        RawFloat c_result{c_result_d, static_cast<size_t>(nw), -static_cast<int64_t>(nw * 64 - 1)};

        int max_terms = static_cast<int>(working_precision * 1.2) + 10;

        for (int k = 1; k <= max_terms; ++k) {
            // sinh: term *= x² / ((2k)(2k+1))
            rf_mul(s_term, s_term, x2, prod_buf, scratch, nw);
            uint64_t s_div = static_cast<uint64_t>(2*k) * static_cast<uint64_t>(2*k + 1);
            rf_divmod_1(s_term, s_div);

            // cosh: term *= x² / ((2k-1)(2k))
            rf_mul(c_term, c_term, x2, prod_buf, scratch, nw);
            uint64_t c_div = static_cast<uint64_t>(2*k - 1) * static_cast<uint64_t>(2*k);
            rf_divmod_1(c_term, c_div);

            bool s_done = (s_term.nw == 0) || !rf_add(s_result, s_term, nw + 1);
            bool c_done = (c_term.nw == 0) || !rf_add(c_result, c_term, nw + 1);
            if (s_done && c_done) break;
        }

        Float s_out = rf_to_float(s_result, x_negative, working_precision);
        Float c_out = rf_to_float(c_result, false, working_precision);
        return { std::move(s_out), std::move(c_out) };
    }

    // Paterson-Stockmeyer for the sinh+cosh Taylor series (shared R[] table)
    // sinh(x) = x · Σ_{k=0}^{∞} u^k / (2k+1)!  where u = x² (no sign alternation)
    // cosh(x) = Σ_{k=0}^{∞} u^k / (2k)!          where u = x² (no sign alternation)
    static std::pair<Float, Float> sinhCoshTaylorPS(const Float& x, int working_precision) {
        int wp_bits = Float::precisionToBits(working_precision);
        int nw = (wp_bits + 63) / 64;

        ScratchScope scope;
        auto& arena = getThreadArena();

        size_t prod_alloc = 2 * static_cast<size_t>(nw) + 4;
        uint64_t* prod_buf = arena.alloc_limbs(prod_alloc);
        size_t mul_scratch = mpn::multiply_scratch_size(nw + 1, nw + 1);
        size_t sqr_scratch = mpn::square_scratch_size(nw + 1);
        size_t scratch_sz = std::max(mul_scratch, sqr_scratch);
        uint64_t* scratch = arena.alloc_limbs(scratch_sz > 0 ? scratch_sz : 1);

        RawFloat x_rf = rf_extract(x, nw, arena);
        bool x_negative = x.isNegative();

        // u = x²
        uint64_t* x2_d = arena.alloc_limbs(nw + 2);
        RawFloat x2{x2_d, 0, 0};
        rf_sqr(x2, x_rf, prod_buf, scratch, nw);

        // Term count estimate (same structure as estimate_sincos_terms)
        int target_bits = nw * 64;
        int64_t u_msb = x2.exp + static_cast<int64_t>(x2.nw) * 64;
        int u_log2 = (u_msb <= 0) ? static_cast<int>(-u_msb) : 1;
        if (u_log2 < 1) u_log2 = 1;
        int l_est = estimate_sincos_terms(target_bits, u_log2);

        int m = static_cast<int>(std::sqrt(static_cast<double>(l_est)));
        if (m < 2) m = 2;
        if (m > 256) m = 256;

        // R[0..m] = u^i (shared by sinh and cosh)
        std::vector<RawFloat> R(m + 1);
        for (int i = 0; i <= m; ++i) {
            R[i].d = arena.alloc_limbs(nw + 2);
            std::memset(R[i].d, 0, (nw + 2) * sizeof(uint64_t));
            R[i].nw = 0; R[i].exp = 0;
        }
        // R[0] = 1.0
        R[0].d[nw - 1] = uint64_t(1) << 63;
        R[0].nw = static_cast<size_t>(nw);
        R[0].exp = -static_cast<int64_t>(nw * 64 - 1);
        // R[1] = u
        std::memcpy(R[1].d, x2.d, x2.nw * sizeof(uint64_t));
        R[1].nw = x2.nw; R[1].exp = x2.exp;
        // NTT cache of R[1]
        prime_ntt::NttCache cache_r1;
        rf_sqr(R[2], R[1], prod_buf, scratch, nw);
        for (int i = 3; i <= m; ++i) {
            if ((i & 1) == 0) rf_sqr(R[i], R[i/2], prod_buf, scratch, nw);
            else rf_mul_cached(R[i], R[i-1], R[1], prod_buf, nw, cache_r1);
        }

        // NTT cache of R[m] (reused in the giant steps)
        prime_ntt::NttCache cache_rm;

        // Working buffer
        RawFloat t;
        t.d = arena.alloc_limbs(nw + 2);
        uint64_t* tmp_d = arena.alloc_limbs(nw + 2);

        int max_giant_steps = l_est / m + 5;

        // ====== sinh P-S: S(u) = Σ_{k=0}^{∞} u^k / (2k+1)! ======
        // Denominator: (2k+1)! → the baby step divisor is (2(l+i+1))(2(l+i+1)+1)
        // No sign alternation → Horner with addition only

        uint64_t* sinh_result_d = arena.alloc_limbs(nw + 2);
        std::memset(sinh_result_d, 0, (nw + 2) * sizeof(uint64_t));
        RawFloat sinh_result{sinh_result_d, 0, 0};

        // rr_s: tracks u^(l*m) / (2*l*m+1)!
        uint64_t* rr_s_d = arena.alloc_limbs(nw + 2);
        std::memset(rr_s_d, 0, (nw + 2) * sizeof(uint64_t));
        rr_s_d[nw - 1] = uint64_t(1) << 63;
        RawFloat rr_s{rr_s_d, static_cast<size_t>(nw), -static_cast<int64_t>(nw * 64 - 1)};

        {
            int l = 0;
            for (int gs = 0; gs < max_giant_steps; ++gs) {
                // Baby step: Horner (no sign alternation → addition only)
                std::memcpy(t.d, R[m-1].d, R[m-1].nw * sizeof(uint64_t));
                t.nw = R[m-1].nw; t.exp = R[m-1].exp;

                for (int i = m - 2; i >= 0; --i) {
                    uint64_t d1 = static_cast<uint64_t>(2*(l+i+1));
                    uint64_t d2 = d1 + 1;
                    rf_divmod_1(t, d1 * d2);
                    if (t.nw == 0) {
                        std::memcpy(t.d, R[i].d, R[i].nw * sizeof(uint64_t));
                        t.nw = R[i].nw; t.exp = R[i].exp;
                    } else {
                        // No sign alternation: t = R[i] + t
                        // R[i] is the dominant value (larger) → add t to tmp=R[i] to avoid heap allocation
                        std::memset(tmp_d, 0, (nw + 2) * sizeof(uint64_t));
                        std::memcpy(tmp_d, R[i].d, R[i].nw * sizeof(uint64_t));
                        RawFloat tmp{tmp_d, R[i].nw, R[i].exp};
                        rf_add(tmp, t, nw + 1);
                        std::memcpy(t.d, tmp.d, tmp.nw * sizeof(uint64_t));
                        std::memset(t.d + tmp.nw, 0, (nw + 2 - tmp.nw) * sizeof(uint64_t));
                        t.nw = tmp.nw; t.exp = tmp.exp;
                    }
                }

                if (gs > 0) {
                    rf_mul(t, t, rr_s, prod_buf, scratch, nw);
                }
                if (t.nw == 0) break;

                // Always add (no sign alternation)
                rf_add(sinh_result, t, nw + 1);

                // Update rr_s: rr_s *= R[m] / Π_{j=0}^{m-1} ((2(l+j)+2)(2(l+j)+3))
                rf_mul_cached(rr_s, rr_s, R[m], prod_buf, nw, cache_rm);
                for (int j = 0; j < m; ++j) {
                    uint64_t d1 = static_cast<uint64_t>(2*(l+j) + 2);
                    uint64_t d2 = d1 + 1;
                    rf_divmod_1(rr_s, d1 * d2);
                }
                l += m;
                if (rr_s.nw == 0) break;
            }
        }

        // sinh(x) = x * S(u)
        rf_mul(sinh_result, sinh_result, x_rf, prod_buf, scratch, nw);

        // ====== cosh P-S: C(u) = Σ_{k=0}^{∞} u^k / (2k)! ======
        // Denominator: (2k)! → the baby step divisor is (2(l+i+1)-1)(2(l+i+1))
        // No sign alternation → Horner with addition only

        uint64_t* cosh_result_d = arena.alloc_limbs(nw + 2);
        std::memset(cosh_result_d, 0, (nw + 2) * sizeof(uint64_t));
        RawFloat cosh_result{cosh_result_d, 0, 0};

        // rr_c: tracks u^(l*m) / (2*l*m)!
        uint64_t* rr_c_d = arena.alloc_limbs(nw + 2);
        std::memset(rr_c_d, 0, (nw + 2) * sizeof(uint64_t));
        rr_c_d[nw - 1] = uint64_t(1) << 63;
        RawFloat rr_c{rr_c_d, static_cast<size_t>(nw), -static_cast<int64_t>(nw * 64 - 1)};

        {
            int l = 0;
            for (int gs = 0; gs < max_giant_steps; ++gs) {
                // Baby step: Horner (no sign alternation)
                std::memcpy(t.d, R[m-1].d, R[m-1].nw * sizeof(uint64_t));
                t.nw = R[m-1].nw; t.exp = R[m-1].exp;

                for (int i = m - 2; i >= 0; --i) {
                    uint64_t d1 = static_cast<uint64_t>(2*(l+i+1) - 1);
                    uint64_t d2 = d1 + 1;
                    rf_divmod_1(t, d1 * d2);
                    if (t.nw == 0) {
                        std::memcpy(t.d, R[i].d, R[i].nw * sizeof(uint64_t));
                        t.nw = R[i].nw; t.exp = R[i].exp;
                    } else {
                        // No sign alternation: t = R[i] + t
                        std::memset(tmp_d, 0, (nw + 2) * sizeof(uint64_t));
                        std::memcpy(tmp_d, R[i].d, R[i].nw * sizeof(uint64_t));
                        RawFloat tmp{tmp_d, R[i].nw, R[i].exp};
                        rf_add(tmp, t, nw + 1);
                        std::memcpy(t.d, tmp.d, tmp.nw * sizeof(uint64_t));
                        std::memset(t.d + tmp.nw, 0, (nw + 2 - tmp.nw) * sizeof(uint64_t));
                        t.nw = tmp.nw; t.exp = tmp.exp;
                    }
                }

                if (gs > 0) {
                    rf_mul(t, t, rr_c, prod_buf, scratch, nw);
                }
                if (t.nw == 0) break;

                // Always add (no sign alternation)
                rf_add(cosh_result, t, nw + 1);

                // Update rr_c: rr_c *= R[m] / Π_{j=0}^{m-1} ((2(l+j)+1)(2(l+j)+2))
                rf_mul_cached(rr_c, rr_c, R[m], prod_buf, nw, cache_rm);
                for (int j = 0; j < m; ++j) {
                    uint64_t d1 = static_cast<uint64_t>(2*(l+j) + 1);
                    uint64_t d2 = d1 + 1;
                    rf_divmod_1(rr_c, d1 * d2);
                }
                l += m;
                if (rr_c.nw == 0) break;
            }
        }

        Float s_out = rf_to_float(sinh_result, x_negative, working_precision);
        Float c_out = rf_to_float(cosh_result, false, working_precision);
        return { std::move(s_out), std::move(c_out) };
    }

    // Unified sinh+cosh Taylor function (automatically selects naive/P-S by precision)
    // P-S threshold: same 10000 bits as sinCosDoubling
    // (at small-to-medium precision, naive has lower overhead)
    static std::pair<Float, Float> sinhCoshTaylor(const Float& x, int working_precision) {
        int wp_bits = Float::precisionToBits(working_precision);
        if (wp_bits >= 10000) {
            return sinhCoshTaylorPS(x, working_precision);
        }
        return sinhCoshTaylorNaive(x, working_precision);
    }

    // Simultaneous (sinh, cosh) computation via the double-angle formula
    // sinh(2t) = 2sinh(t)cosh(t), cosh(2t) = 2cosh²(t) - 1
    // Same structure as sinCosDoubling. Since cosh ≥ 1, 2cosh²-1 ≥ 1 (no borrow)
    static std::pair<Float, Float> sinhCoshDoubling(const Float& x, int working_precision) {
        int wp_bits = Float::precisionToBits(working_precision);
        int K;
        if (wp_bits >= 10000) {
            // With P-S, Taylor is O(√N) → reconstruction 2K + Taylor 6√(p/(2K))
            // Optimal K = cbrt(2·wp_bits) (since reconstruction is 2M(n)/step)
            // Same optimization as sinCosDoubling
            K = static_cast<int>(std::cbrt(2.0 * wp_bits));
        } else {
            K = static_cast<int>(std::sqrt(wp_bits / 3.0));
        }
        if (K < 1) return sinhCoshTaylor(x, working_precision);

        int guard_bits = 2 * K + static_cast<int>(std::ceil(std::log2(K + 1))) + 10;
        int wp_inner = working_precision + Float::bitsToPrecision(guard_bits);

        Float x_red = ldexp(abs(x), -K);  // |x| / 2^K
        x_red.truncateToApprox(wp_inner);
        int wp_inner_bits = Float::precisionToBits(wp_inner);
        x_red.setEffectiveBits(wp_inner_bits);

        auto [s_float, c_float] = sinhCoshTaylor(x_red, wp_inner);

        // K simultaneous reconstructions (RawFloat)
        // sinh(2t) = 2sinh(t)cosh(t), cosh(2t) = 2cosh²(t) - 1
        int nw = (wp_inner_bits + 63) / 64;

        ScratchScope scope;
        auto& arena = getThreadArena();

        uint64_t* s_d = arena.alloc_limbs(nw + 2);
        RawFloat s_rf = rf_extract(s_float, nw, arena);
        std::memcpy(s_d, s_rf.d, s_rf.nw * sizeof(uint64_t));
        s_rf.d = s_d;

        uint64_t* c_d = arena.alloc_limbs(nw + 2);
        RawFloat c_rf = rf_extract(c_float, nw, arena);
        std::memcpy(c_d, c_rf.d, c_rf.nw * sizeof(uint64_t));
        c_rf.d = c_d;

        size_t prod_alloc = 2 * static_cast<size_t>(nw) + 4;
        uint64_t* prod_buf = arena.alloc_limbs(prod_alloc);
        size_t sqr_scratch_sz = mpn::square_scratch_size(nw + 1);
        size_t scratch_sz = std::max(mpn::multiply_scratch_size(nw + 1, nw + 1), sqr_scratch_sz);
        uint64_t* scratch_buf = arena.alloc_limbs(scratch_sz > 0 ? scratch_sz : 1);

        uint64_t* tmp_d = arena.alloc_limbs(nw + 2);

        // Gradual precision reduction (P2-3 technique)
        int hyp_guard_limbs = (K + 63) / 64 + 6;
        for (int i = 0; i < K; ++i) {
            int remaining = K - 1 - i;
            int needed_nw = nw;
            if (remaining > 0 && remaining < 20) {
                needed_nw = (nw >> remaining) + hyp_guard_limbs;
                if (needed_nw < hyp_guard_limbs + 2) needed_nw = hyp_guard_limbs + 2;
                if (needed_nw > nw) needed_nw = nw;
            } else if (remaining >= 20) {
                needed_nw = hyp_guard_limbs + 2;
            }
            // truncate
            if (static_cast<int>(s_rf.nw) > needed_nw) {
                size_t drop = s_rf.nw - needed_nw;
                std::memmove(s_rf.d, s_rf.d + drop, needed_nw * sizeof(uint64_t));
                s_rf.nw = needed_nw;
                s_rf.exp += static_cast<int64_t>(drop) * 64;
            }
            if (static_cast<int>(c_rf.nw) > needed_nw) {
                size_t drop = c_rf.nw - needed_nw;
                std::memmove(c_rf.d, c_rf.d + drop, needed_nw * sizeof(uint64_t));
                c_rf.nw = needed_nw;
                c_rf.exp += static_cast<int64_t>(drop) * 64;
            }
            // new_s = 2 * s * c (stored in a temporary buffer)
            RawFloat tmp_rf{tmp_d, 0, 0};
            rf_mul(tmp_rf, s_rf, c_rf, prod_buf, scratch_buf, needed_nw);
            tmp_rf.exp += 1;  // ×2

            // c = 2 * c² - 1 (since cosh ≥ 1, 2cosh² ≥ 2 > 1, no borrow, accelerated via squaring)
            rf_sqr(c_rf, c_rf, prod_buf, scratch_buf, needed_nw);
            c_rf.exp += 1;  // ×2
            // c -= 1.0: subtract 2^(-exp) from the mantissa
            if (c_rf.nw > 0 && c_rf.exp < 0) {
                int64_t neg_exp = -c_rf.exp;
                size_t word_idx = static_cast<size_t>(neg_exp / 64);
                unsigned bit_idx = static_cast<unsigned>(neg_exp % 64);
                if (word_idx < c_rf.nw) {
                    mpn::sub_1(
                        c_rf.d + word_idx, c_rf.nw - word_idx,
                        1ULL << bit_idx);
                    // cosh ≥ 1 → 2cosh²-1 ≥ 1 → no borrow occurs
                }
                // word_idx >= nw does not occur since cosh ≥ 1
            }
            c_rf.nw = mpn::normalized_size(c_rf.d, c_rf.nw);
            rf_normalize(c_rf);

            // s = new_s
            std::memcpy(s_rf.d, tmp_rf.d, tmp_rf.nw * sizeof(uint64_t));
            s_rf.nw = tmp_rf.nw;
            s_rf.exp = tmp_rf.exp;
        }

        // sinh inherits the sign of x; cosh is always positive
        Float s_out = rf_to_float(s_rf, x.isNegative(), wp_inner);
        Float c_out = rf_to_float(c_rf, false, wp_inner);
        s_out.truncateToApprox(working_precision);
        c_out.truncateToApprox(working_precision);
        return { std::move(s_out), std::move(c_out) };
    }

    //=============================================================================
    // Hyperbolic sine (sinh)
    //=============================================================================

    // sinh/cosh/tanh common: 19-digit fast path using exp_small
    static Float sinh_small(const Float& x, int precision) {
        int wp = precision + 2;
        Float ex = exp_small(x, wp);
        Float emx = Float::one(wp) / ex;
        Float result = (ex - emx) >>= 1;  // / 2
        result.setResultPrecision(precision);
        return result;
    }

    static Float cosh_small(const Float& x, int precision) {
        int wp = precision + 2;
        Float abs_x = abs(x);
        Float ex = exp_small(abs_x, wp);
        Float emx = Float::one(wp) / ex;
        Float result = (ex + emx) >>= 1;  // / 2
        result.setResultPrecision(precision);
        return result;
    }

    static Float tanh_small(const Float& x, int precision) {
        int wp = precision + 2;
        Float ex = exp_small(x, wp);
        Float emx = Float::one(wp) / ex;
        Float result = (ex - emx) / (ex + emx);
        result.setResultPrecision(precision);
        return result;
    }

    // Single-exp method: e = exp(|x|), sinh = (e - 1/e)/2, cosh = (e + 1/e)/2
    // Faster than Taylor + double-angle reconstruction at large sizes (exp is optimized, only one division)
    // Optimizations:
    //   - Call exp_core directly (avoids double zivRound — the outer sinh/cosh zivRound suffices)
    //   - Skip 1/e when |x| is large (exp(-|x|) is below working precision)
    static std::pair<Float, Float> sinhCoshViaExp(const Float& x, int working_precision) {
        int wp = working_precision + 5;  // guard digits for 1/e subtraction
        int wp_bits = Float::precisionToBits(wp);
        Float abs_x = abs(x);

        // Call exp_core directly (avoids zivRound)
        // The outer sinh/cosh zivRound provides the precision guarantee
        Float e = exp_core(Float(abs_x), abs_x.effectiveBits(), wp);

        // If |x| * log2(e) > wp_bits + 64, then exp(-|x|) is below working precision
        // → skip the 1/e division and sinh ≈ cosh ≈ e/2
        double abs_x_d = abs_x.toDouble();
        bool skip_inv = (abs_x_d * 1.4426950408889634 > wp_bits + 64);

        Float s, c;
        if (skip_inv) {
            s = e >>= 1;  // sinh(|x|) ≈ exp(|x|) / 2
            c = s;         // cosh(|x|) ≈ exp(|x|) / 2
        } else {
            Float inv_e = Float::one(wp) / e;
            s = (e - inv_e) >>= 1;  // sinh(|x|) = (e - 1/e) / 2
            c = (e + inv_e) >>= 1;  // cosh(|x|) = (e + 1/e) / 2
        }
        if (x.isNegative()) s = -std::move(s);
        s.truncateToApprox(working_precision);
        c.truncateToApprox(working_precision);
        return { std::move(s), std::move(c) };
    }

    // Unified sinh/cosh dispatch: selects Taylor+double-angle or the exp method by precision
    // Threshold 300 bits (~90 digits): above this, direct exp_core calls beat double-angle reconstruction
    // (it was 5000 bits before, but direct exp_core calls make a lower threshold favorable)
    // 100d: 1.98x→1.84x, 500d: 2.61x→1.86x, 1000d: 3.10x→2.14x (vs MPFR)
    static constexpr int SINHCOSH_EXP_THRESHOLD_BITS = 300;

    static std::pair<Float, Float> sinhCoshCompute(const Float& x, int working_precision) {
        int wp_bits = Float::precisionToBits(working_precision);
        if (wp_bits >= SINHCOSH_EXP_THRESHOLD_BITS) {
            return sinhCoshViaExp(x, working_precision);
        }
        return sinhCoshDoubling(x, working_precision);
    }

    // Dispatch for Ziv
    static Float sinh_dispatch(const Float& x, int eff_x, int precision) {
        int wp = effectiveComputePrecision(eff_x, precision);
        auto [s, c] = sinhCoshCompute(x, wp);
        return s;
    }

    Float sinh(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            return x.isNegative() ? Float::negativeInfinity() : Float::positiveInfinity();
        }
        if (x.isZero()) return Float::zero();

        int precision_bits = Float::precisionToBits(precision);
        if (x.mantissa().size() <= 1 && precision_bits <= 64)
            return sinh_small(x, precision);

        return zivRound(sinh_dispatch, x, x.effectiveBits(), precision);
    }

    Float sinh(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            return x.isNegative() ? Float::negativeInfinity() : Float::positiveInfinity();
        }
        if (x.isZero()) return Float::zero();

        int precision_bits = Float::precisionToBits(precision);
        if (x.mantissa().size() <= 1 && precision_bits <= 64)
            return sinh_small(x, precision);

        return zivRound(sinh_dispatch, x, x.effectiveBits(), precision);
    }

    //=============================================================================
    // Hyperbolic cosine (cosh)
    //=============================================================================

    static Float cosh_dispatch(const Float& x, int eff_x, int precision) {
        int wp = effectiveComputePrecision(eff_x, precision);
        auto [s, c] = sinhCoshCompute(x, wp);
        return c;
    }

    Float cosh(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::positiveInfinity();
        if (x.isZero()) return Float::one(precision);

        int precision_bits = Float::precisionToBits(precision);
        if (x.mantissa().size() <= 1 && precision_bits <= 64)
            return cosh_small(x, precision);

        return zivRound(cosh_dispatch, x, x.effectiveBits(), precision);
    }

    Float cosh(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::positiveInfinity();
        if (x.isZero()) return Float::one(precision);

        int precision_bits = Float::precisionToBits(precision);
        if (x.mantissa().size() <= 1 && precision_bits <= 64)
            return cosh_small(x, precision);

        return zivRound(cosh_dispatch, x, x.effectiveBits(), precision);
    }

    //=============================================================================
    // Hyperbolic tangent (tanh)
    //=============================================================================

    static Float tanh_dispatch(const Float& x, int eff_x, int precision) {
        int wp = effectiveComputePrecision(eff_x, precision);
        auto [s, c] = sinhCoshCompute(x, wp);
        return s / c;
    }

    Float tanh(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            return x.isNegative() ? Float(-1) : Float(1);
        }
        if (x.isZero()) return Float::zero();

        double x_d = x.toDouble();
        if (std::fabs(x_d) > 20.0) {
            return x.isNegative() ? Float(-1) : Float(1);
        }

        int precision_bits = Float::precisionToBits(precision);
        if (x.mantissa().size() <= 1 && precision_bits <= 64)
            return tanh_small(x, precision);

        return zivRound(tanh_dispatch, x, x.effectiveBits(), precision);
    }

    Float tanh(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            return x.isNegative() ? Float(-1) : Float(1);
        }
        if (x.isZero()) return Float::zero();

        double x_d = x.toDouble();
        if (std::fabs(x_d) > 20.0) {
            return x.isNegative() ? Float(-1) : Float(1);
        }

        int precision_bits = Float::precisionToBits(precision);
        if (x.mantissa().size() <= 1 && precision_bits <= 64)
            return tanh_small(x, precision);

        return zivRound(tanh_dispatch, x, x.effectiveBits(), precision);
    }

    //=============================================================================
    // Power (pow) - integer exponent
    //=============================================================================

    static Float pow_core(Float x, int eff_x, int n, int precision) {
        // Determine the computation precision based on the effective bit count
        int compute_prec = effectiveComputePrecision(eff_x, precision);

        // Working precision
        int working_precision = compute_prec + 10;
        x.truncateToApprox(working_precision);

        // Binary exponentiation
        // Truncate the mantissa with truncateToApprox after each multiplication.
        // Multiplication doubles the mantissa bit count, so left unchecked
        // the mantissa balloons to ~10000 bits at n=1000, becoming O(n²).
        Float result = Float::one(working_precision);

        int exp = n;
        while (exp > 0) {
            if (exp % 2 == 1) {
                result *= x;
                result.truncateToApprox(working_precision);
            }
            if (exp > 1) {
                x *= x;
                x.truncateToApprox(working_precision);
            }
            exp /= 2;
        }

        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float pow(const Float& x, int n, int precision) {
        if (x.isNaN()) return Float::nan();
        if (n == 0) return Float::one(precision);
        if (n == 1) return Float(x);
        if (x.isZero()) return (n < 0) ? Float::positiveInfinity() : Float::zero();
        if (x.isInfinity()) {
            if (n < 0) return Float::zero();
            if (x.isNegative() && (n % 2 != 0)) return Float::negativeInfinity();
            return Float::positiveInfinity();
        }
        if (n == -1) return Float::one(precision) / x;
        if (n < 0) return Float::one(precision) / pow(x, -n, precision);
        return pow_core(Float(x), x.effectiveBits(), n, precision);
    }

    Float pow(Float&& x, int n, int precision) {
        if (x.isNaN()) return Float::nan();
        if (n == 0) return Float::one(precision);
        if (n == 1) return std::move(x);
        if (x.isZero()) return (n < 0) ? Float::positiveInfinity() : Float::zero();
        if (x.isInfinity()) {
            if (n < 0) return Float::zero();
            if (x.isNegative() && (n % 2 != 0)) return Float::negativeInfinity();
            return Float::positiveInfinity();
        }
        if (n == -1) return Float::one(precision) / x;
        if (n < 0) return Float::one(precision) / pow(std::move(x), -n, precision);
        int eff = x.effectiveBits();
        return pow_core(std::move(x), eff, n, precision);
    }

    //=============================================================================
    // Power (pow) - general exponent
    //=============================================================================

    // pow fast path (19 digits): Q128 log + Float mul + Q128 exp
    static Float pow_small(const Float& a, const Float& b, int precision) {
        // Q128 log → convert to Float → Float multiply → Q128 exp
        Float lna = log_small(a, precision);
        Float L = b * lna;
        return exp_small(L, precision);
    }

    // pow fast path (38 digits): Q256 log + Float mul + exp
    static Float pow_medium(const Float& a, const Float& b, int precision) {
        int wp = precision + 5;  // Guard bits (rounding error accumulation of log→mul→exp)
        Float lna = log_medium(a, wp);
        Float L = b * lna;
        Float result = exp(std::move(L), wp);
        result.setResultPrecision(precision);
        return result;
    }

    // pow general path (Q128/Q256 decision + fallback)
    static Float pow_general(const Float& a, const Float& b, int precision) {
        int precision_bits = Float::precisionToBits(precision);
        if (precision_bits <= 64) {
            return pow_small(a, b, precision);
        }
        if (precision_bits <= 128) {
            return pow_medium(a, b, precision);
        }

        // Fallback: Float arithmetic (Ziv iteration)
        // pow(a,b) = exp(b * log(a))
        // Note: the optimization of lowering log's precision according to |b| gives near-zero savings
        // for typical |b| of 0.1-1.0, and a slight precision shortfall triggers zivRound retries, backfiring.
        // It is effective only for |b| << 1 (e.g. 10^{-100}), but is currently not applied.
        int eff = std::min(a.effectiveBits(), b.effectiveBits());
        return zivRound([&b](const Float& base, int e, int p) {
            int ep = p + 10;
            Float log_x = log(abs(base), ep);
            Float y_log_x = b * log_x;
            return exp(std::move(y_log_x), ep);
        }, a, eff, precision);
    }

    Float pow(const Float& x, const Float& y, int precision) {
        // Handle special values
        if (x.isNaN() || y.isNaN()) return Float::nan();
        if (y.isZero()) return Float::one(precision);
        if (x.isZero()) {
            if (y.isNegative()) return Float::positiveInfinity();
            return Float::zero();
        }
        if (x.isInfinity()) {
            // Handle special cases
            if (y.isPositive()) {
                return Float::positiveInfinity();
            }
            else {
                return Float::zero();
            }
        }
        if (y.isInfinity()) {
            // Handle special cases
            Float one = Float::one(precision);
            if (abs(x) > one) {
                return y.isPositive() ? Float::positiveInfinity() : Float::zero();
            }
            else if (abs(x) < one) {
                return y.isPositive() ? Float::zero() : Float::positiveInfinity();
            }
            else { // |x| = 1
                return Float::one(precision);
            }
        }

        // For integer exponents, use the dedicated function
        // In mantissa * 2^exponent, if exponent >= 0 it is trivially an integer.
        // Even if exponent < 0, it is an integer if the trailing zero-bit count of mantissa >= |exponent|.
        {
            bool y_is_integer = (y.exponent() >= 0) ||
                (static_cast<int64_t>(y.mantissa().countTrailingZeros()) >= -y.exponent());
            if (y_is_integer && abs(y) < Float(1e9)) {
                Int temp_int = y.toInt();
                int n = temp_int.toInt();
                return pow(x, n, precision);
            }
        }

        // For a negative base, non-integer exponents yield complex numbers
        if (x.isNegative()) return Float::nan();

        return pow_general(x, y, precision);
    }

    Float pow(Float&& x, Float&& y, int precision) {
        // Handle special values
        if (x.isNaN() || y.isNaN()) return Float::nan();
        if (y.isZero()) return Float::one(precision);
        if (x.isZero()) {
            if (y.isNegative()) return Float::positiveInfinity();
            return Float::zero();
        }
        if (x.isInfinity()) {
            // Handle special cases
            if (y.isPositive()) {
                return Float::positiveInfinity();
            }
            else {
                return Float::zero();
            }
        }
        if (y.isInfinity()) {
            // Handle special cases
            Float one = Float::one(precision);
            if (abs(x) > one) {
                return y.isPositive() ? Float::positiveInfinity() : Float::zero();
            }
            else if (abs(x) < one) {
                return y.isPositive() ? Float::zero() : Float::positiveInfinity();
            }
            else { // |x| = 1
                return Float::one(precision);
            }
        }

        // For integer exponents, use the dedicated function
        // In mantissa * 2^exponent, if exponent >= 0 it is trivially an integer.
        // Even if exponent < 0, it is an integer if the trailing zero-bit count of mantissa >= |exponent|.
        {
            bool y_is_integer = (y.exponent() >= 0) ||
                (static_cast<int64_t>(y.mantissa().countTrailingZeros()) >= -y.exponent());
            if (y_is_integer && abs(y) < Float(1e9)) {
                Int temp_int = y.toInt();
                int n = temp_int.toInt();
                return pow(std::move(x), n, precision);
            }
        }

        // For a negative base, non-integer exponents yield complex numbers
        if (x.isNegative()) return Float::nan();

        return pow_general(x, y, precision);
    }


    //=============================================================================
    // Inverse tangent (atan) — Taylor series + argument halving
    //=============================================================================
    //
    // ■ Algorithm
    //
    //   atan(x) = x − x³/3 + x⁵/5 − x⁷/7 + ...
    //   Recurrence: term_{k+1} = term_k · x² · (2k−1) / (2k+1)
    //
    //   Argument-halving formula: atan(x) = 2 · atan(x / (1 + √(1 + x²)))
    //   After applying K times → Taylor computation → reconstruct via ldexp(result, K)
    //
    //   When |x| > 1: reduce to |x| < 1 via atan(x) = π/2 − atan(1/x)
    //

    // Internal computation of atan(x) via Taylor series (assumes |x| << 1)
    // Same RawFloat pattern as sinTaylor/cosTaylor
    // Paterson-Stockmeyer for the atan Taylor series
    // atan(x) = x · Σ_{k=0}^{∞} (-1)^k · u^k / (2k+1)  where u = x²
    // O(√N) full-size multiplications + O(N) scalar mul/div
    // giant step rr update: telescoping product Π(2l+2j+1)/Π(2l+2j+3) = (2l+1)/(2l+2m+1)
    static void rf_atan_ps(RawFloat& result, const RawFloat& x_rf, const RawFloat& x2,
                            int nw, uint64_t* prod_buf, uint64_t* scratch,
                            ScratchArena& arena, int working_precision) {
        int target_bits = nw * 64;
        // Estimate term count from the MSB of u = x² (no factorial: N ≈ target_bits / u_log2)
        int64_t u_msb = x2.exp + static_cast<int64_t>(x2.nw) * 64;
        int u_log2 = (u_msb <= 0) ? static_cast<int>(-u_msb) : 1;
        if (u_log2 < 1) u_log2 = 1;
        int l_est = target_bits / u_log2 + 5;

        int m = static_cast<int>(std::sqrt(static_cast<double>(l_est)));
        if (m < 2) m = 2;
        if (m > 256) m = 256;
        // Round m up to even (simplifies sign handling in the Horner giant step)
        if (m % 2 != 0) m++;

        int G = l_est / m + 1;  // Number of giant steps (blocks)

        // Precompute R[0..m]: R[i] = u^i
        std::vector<RawFloat> R(m + 1);
        for (int i = 0; i <= m; ++i) {
            R[i].d = arena.alloc_limbs(nw + 2);
            std::memset(R[i].d, 0, (nw + 2) * sizeof(uint64_t));
            R[i].nw = 0; R[i].exp = 0;
        }
        R[0].d[nw - 1] = uint64_t(1) << 63;
        R[0].nw = static_cast<size_t>(nw);
        R[0].exp = -static_cast<int64_t>(nw * 64 - 1);
        std::memcpy(R[1].d, x2.d, x2.nw * sizeof(uint64_t));
        R[1].nw = x2.nw; R[1].exp = x2.exp;
        // NTT cache of R[1]
        prime_ntt::NttCache atan_cache_r1;
        rf_sqr(R[2], R[1], prod_buf, scratch, nw);
        for (int i = 3; i <= m; ++i) {
            if ((i & 1) == 0) rf_sqr(R[i], R[i/2], prod_buf, scratch, nw);
            else rf_mul_cached(R[i], R[i-1], R[1], prod_buf, nw, atan_cache_r1);
        }
        // W = u^m = R[m] (used in the Horner giant step multiplication)
        // NTT cache of R[m]
        prime_ntt::NttCache atan_cache_rm;

        // Two buffers for the baby step (pointer swapping reduces memcpy)
        uint64_t* buf_a = arena.alloc_limbs(nw + 2);
        uint64_t* buf_b = arena.alloc_limbs(nw + 2);
        std::memset(buf_a, 0, (nw + 2) * sizeof(uint64_t));
        std::memset(buf_b, 0, (nw + 2) * sizeof(uint64_t));

        // Horner giant step: evaluate in reverse order (j = G-1 → 0)
        // S(u) = Σ_{j=0}^{G-1} W^j · C_j(u)  (since m is even, signs are handled within C_j)
        // Horner: acc = C_{G-1}; for j=G-2..0: acc = acc·W + C_j
        // No rr tracking needed → 1 full mul per giant step (was: 2 full mul)

        // result = 0 (accumulator)
        std::memset(result.d, 0, (nw + 2) * sizeof(uint64_t));
        result.nw = 0; result.exp = 0;

        for (int j = G - 1; j >= 0; --j) {
            int l = j * m;

            // Giant step: acc = acc · W (except for the first block)
            if (j < G - 1) {
                rf_mul_cached(result, result, R[m], prod_buf, nw, atan_cache_rm);
            }

            // Baby step: evaluate the block polynomial C_j(u)
            // C_j(u) = Σ_{i=0}^{m-1} (-1)^i · u^i / (2(l+i)+1)
            // Horner: t = R[m-1]/(2(l+m-1)+1), then down to R[0]/(2l+1)

            // Initialize t in buf_a
            std::memcpy(buf_a, R[m-1].d, R[m-1].nw * sizeof(uint64_t));
            if (R[m-1].nw < static_cast<size_t>(nw + 2))
                std::memset(buf_a + R[m-1].nw, 0, (nw + 2 - R[m-1].nw) * sizeof(uint64_t));
            RawFloat t{buf_a, R[m-1].nw, R[m-1].exp};
            uint64_t top_den = static_cast<uint64_t>(2*(l+m-1) + 1);
            rf_divmod_1(t, top_den);
            bool t_is_a = true;

            for (int i = m - 2; i >= 0; --i) {
                if (t.nw == 0) {
                    // t = 0 → replace with R[i]/(2(l+i)+1) (directly in the current buffer)
                    uint64_t* cur = t_is_a ? buf_a : buf_b;
                    std::memcpy(cur, R[i].d, R[i].nw * sizeof(uint64_t));
                    if (R[i].nw < static_cast<size_t>(nw + 2))
                        std::memset(cur + R[i].nw, 0, (nw + 2 - R[i].nw) * sizeof(uint64_t));
                    t.d = cur;
                    t.nw = R[i].nw; t.exp = R[i].exp;
                    uint64_t d = static_cast<uint64_t>(2*(l+i) + 1);
                    rf_divmod_1(t, d);
                } else {
                    // t = R[i]/(2(l+i)+1) - t: write the result to the opposite buffer
                    uint64_t* dst = t_is_a ? buf_b : buf_a;
                    std::memcpy(dst, R[i].d, R[i].nw * sizeof(uint64_t));
                    if (R[i].nw < static_cast<size_t>(nw + 2))
                        std::memset(dst + R[i].nw, 0, (nw + 2 - R[i].nw) * sizeof(uint64_t));
                    RawFloat tmp{dst, R[i].nw, R[i].exp};
                    uint64_t d = static_cast<uint64_t>(2*(l+i) + 1);
                    rf_divmod_1(tmp, d);
                    rf_sub(tmp, t, nw + 1);
                    t = tmp;
                    t_is_a = !t_is_a;
                }
            }

            if (t.nw == 0) continue;

            // Add C_j to the accumulator
            rf_add(result, t, nw + 1);
        }

        // atan(x) = x · S(u): result *= x
        rf_mul(result, result, x_rf, prod_buf, scratch, nw);
    }

    static Float atanTaylor(const Float& x, int working_precision) {
        int wp_bits = Float::precisionToBits(working_precision);
        int nw = (wp_bits + 63) / 64;

        ScratchScope scope;
        auto& arena = getThreadArena();

        // Pre-allocate buffers
        size_t prod_alloc = 2 * static_cast<size_t>(nw) + 4;
        uint64_t* prod_buf = arena.alloc_limbs(prod_alloc);
        size_t mul_scratch = mpn::multiply_scratch_size(nw + 2, nw + 2);
        size_t sqr_scratch = mpn::square_scratch_size(nw + 2);
        size_t scratch_sz  = std::max(mul_scratch, sqr_scratch);
        uint64_t* scratch  = arena.alloc_limbs(scratch_sz > 0 ? scratch_sz : 1);

        // Extract |x|
        RawFloat x_rf = rf_extract(x, nw, arena);

        // Precompute x² (squaring is accelerated via square)
        uint64_t* x2_d = arena.alloc_limbs(nw + 2);
        RawFloat x2{x2_d, 0, 0};
        rf_sqr(x2, x_rf, prod_buf, scratch, nw);

        uint64_t* result_d = arena.alloc_limbs(nw + 2);
        std::memset(result_d, 0, (nw + 2) * sizeof(uint64_t));

        if (wp_bits >= 1500) {
            // Paterson-Stockmeyer: O(√N) full-size multiplications
            // Threshold lowered 10000→1500 bits (N passes of naive Taylor vs 2√N of P-S)
            RawFloat result{result_d, 0, 0};
            rf_atan_ps(result, x_rf, x2, nw, prod_buf, scratch, arena, working_precision);
            return rf_to_float(result, x.isNegative(), working_precision);
        }

        // Naive Taylor series (low precision)
        uint64_t* term_d = arena.alloc_limbs(nw + 2);
        std::memset(term_d, 0, (nw + 2) * sizeof(uint64_t));
        std::memcpy(term_d, x_rf.d, x_rf.nw * sizeof(uint64_t));
        RawFloat term{term_d, x_rf.nw, x_rf.exp};

        std::memcpy(result_d, x_rf.d, x_rf.nw * sizeof(uint64_t));
        RawFloat result{result_d, x_rf.nw, x_rf.exp};

        int max_terms = static_cast<int>(working_precision * 1.2) + 10;

        for (int k = 1; k <= max_terms; ++k) {
            // term *= x²
            rf_mul(term, term, x2, prod_buf, scratch, nw);
            // term *= (2k-1) — skip when k=1 since 2k-1=1
            if (k > 1) {
                rf_mul_1(term, static_cast<uint64_t>(2*k - 1));
            }
            // term /= (2k+1)
            rf_divmod_1(term, static_cast<uint64_t>(2*k + 1));
            if (term.nw == 0) break;
            if (k & 1) {
                if (!rf_sub(result, term, nw + 1)) break;
            } else {
                if (!rf_add(result, term, nw + 1)) break;
            }
        }

        return rf_to_float(result, x.isNegative(), working_precision);
    }

    // atan computation via argument halving + Taylor
    // atan(x) = 2^K · atan(t), t = the argument halved K times
    // When using P-S, the Taylor part is O(√N), so reduce K substantially and
    // cut the number of costly halvings (each a sqrt+div).
    static Float atanHalving(const Float& x, int working_precision) {
        int wp_bits = Float::precisionToBits(working_precision);
        int K;
        if (wp_bits >= 1500) {
            // P-S enabled: halving cost (sqrt+div ≈ 4M(n)) vs P-S Taylor (2√N·M(n))
            // Optimal K = cbrt(n/50) — limit halvings and let P-S handle the rest
            K = static_cast<int>(std::cbrt(wp_bits / 50.0));
        } else {
            K = static_cast<int>(std::sqrt(wp_bits / 20.0));
        }
        if (K < 1) return atanTaylor(x, working_precision);

        // Guard bits: for the error accumulation of K halvings
        int guard_bits = 2 * K + static_cast<int>(std::ceil(std::log2(K + 1))) + 10;
        int wp_inner = working_precision + Float::bitsToPrecision(guard_bits);

        // K halvings: t = x / (1 + sqrt(1 + x²))
        Float t = x;
        t.truncateToApprox(wp_inner);
        int wp_inner_bits = Float::precisionToBits(wp_inner);
        t.setEffectiveBits(wp_inner_bits);
        Float one_val = Float::one(wp_inner);
        for (int i = 0; i < K; ++i) {
            Float t2 = t * t;
            t2.truncateToApprox(wp_inner);
            t = t / (one_val + sqrt(one_val + t2, wp_inner));
            t.truncateToApprox(wp_inner);
        }
        t.setEffectiveBits(wp_inner_bits);

        // Taylor series on the reduced argument
        Float result = atanTaylor(t, wp_inner);

        // Reconstruction: atan(x) = 2^K · atan(t)
        result = ldexp(result, K);
        result.truncateToApprox(working_precision);
        return result;
    }

    // double → Q128 conversion (places the 53-bit precision at the proper position in Q128)
    // Assumes val ∈ [0, 1). Returns hi=lo=0 for val = 0.
    static void double_to_q128(double val, uint64_t& hi, uint64_t& lo) {
        if (val == 0.0) { hi = lo = 0; return; }
        int exp;
        double m = std::frexp(val, &exp); // m ∈ [0.5, 1), val = m * 2^exp
        // m * 2^53 is a 53-bit integer
        uint64_t m_int = static_cast<uint64_t>(m * static_cast<double>(1ULL << 53));
        // Q128: val * 2^128 = m_int * 2^(exp - 53 + 128) = m_int * 2^(exp + 75)
        int shift = exp + 75;
        if (shift >= 64) {
            hi = m_int << (shift - 64);
            lo = 0;
        } else if (shift >= 0) {
            hi = m_int >> (64 - shift);
            lo = m_int << shift;
        } else {
            hi = 0;
            lo = m_int >> (-shift);
        }
    }

    // Float → Q128 conversion (places the high 128 bits of the mantissa into Q128)
    // Assumes x ∈ (0, 1)
    static void float_to_q128(const Float& x, uint64_t& hi, uint64_t& lo) {
        const Int& mant = x.mantissa();
        const uint64_t* mw = mant.data();
        size_t mn = mant.size();
        int64_t e = x.exponent();

        uint64_t top = mw[mn - 1];
        int top_bits = 64 - std::countl_zero(top);
        int64_t total_bl = static_cast<int64_t>((mn - 1) * 64) + top_bits;

        // Normalize the mantissa (place the MSB at bit 127)
        int clz = 64 - top_bits;
        uint64_t m_hi, m_lo;
        if (clz == 0) {
            m_hi = top;
            m_lo = (mn >= 2) ? mw[mn - 2] : 0;
        } else {
            m_hi = (top << clz) | ((mn >= 2) ? (mw[mn - 2] >> (64 - clz)) : 0);
            if (mn >= 2) {
                m_lo = (mw[mn - 2] << clz) | ((mn >= 3) ? (mw[mn - 3] >> (64 - clz)) : 0);
            } else {
                m_lo = 0;
            }
        }

        // x = mant * 2^e, mant ≈ m_hi:m_lo * 2^(total_bl - 128)
        // Q128 = x * 2^128 = m_hi:m_lo * 2^(e + total_bl)
        int64_t sr = -(e + total_bl); // Right-shift amount (sr > 0 since x < 1)
        if (sr <= 0) {
            // x >= 1: should not reach here
            hi = m_hi; lo = m_lo;
        } else if (sr < 64) {
            hi = (m_hi >> sr);
            lo = (m_hi << (64 - sr)) | (m_lo >> sr);
        } else if (sr < 128) {
            hi = 0;
            lo = m_hi >> (sr - 64);
        } else {
            hi = lo = 0;
        }
    }

    // Float → Q128 reciprocal conversion: convert 1/x to Q128 (x > 1)
    // Compute a ~130-bit-precision reciprocal from the high 64 bits of the mantissa via 3-stage _udiv128
    static void float_recip_q128(const Float& x, uint64_t& hi, uint64_t& lo) {
        const Int& mant = x.mantissa();
        const uint64_t* mw = mant.data();
        size_t mn = mant.size();
        int64_t e = x.exponent();

        uint64_t top = mw[mn - 1];
        int top_bits = 64 - std::countl_zero(top);
        int64_t total_bl = static_cast<int64_t>((mn - 1) * 64) + top_bits;

        int clz = 64 - top_bits;
        uint64_t m_norm;
        if (clz == 0) {
            m_norm = top;
        } else if (mn >= 2) {
            m_norm = (top << clz) | (mw[mn - 2] >> (64 - clz));
        } else {
            m_norm = top << clz;
        }

        // 3-stage division: 2^192 / m_norm → (q1:q2:q3), ~130-bit precision
        uint64_t r1, r2, r3;
        uint64_t q1 = _udiv128(1, 0, m_norm, &r1);
        uint64_t q2 = _udiv128(r1, 0, m_norm, &r2);
        uint64_t q3 = _udiv128(r2, 0, m_norm, &r3);

        // Q128(1/x) = (q1:q2:q3) * 2^{-total_bl - e}
        // x > 1 → total_bl + e >= 1
        int64_t sr = total_bl + e;
        if (sr > 0 && sr < 64) {
            hi = (q1 << (64 - sr)) | (q2 >> sr);
            lo = (q2 << (64 - sr)) | (q3 >> sr);
        } else if (sr == 0) {
            hi = q2; lo = q3;
        } else if (sr >= 64 && sr < 128) {
            int s2 = static_cast<int>(sr - 64);
            hi = q2 >> s2;
            lo = (q2 << (64 - s2)) | (q3 >> s2);
        } else {
            hi = lo = 0;
        }
    }

    // Q128 atan fast path (≤ 64-bit precision)
    // Newton's method: y₁ = y₀ + (x·cos(y₀) - sin(y₀)) / (cos(y₀) + x·sin(y₀))
    // y₀ = std::atan(x) (53-bit), one Newton step doubles to ~106-bit
    // Compute sin(y₀), vers(y₀) = 1 - cos(y₀) via Q128 Taylor + angle doubling
    static Float atan_small(const Float& x, int precision) {
        double x_double = x.toDouble();
        bool negative = x_double < 0;
        if (negative) x_double = -x_double;

        // x = 0 (already handled by the caller, but just in case)
        if (x_double == 0.0) return Float::zero(precision);

        // |x| > 1: atan(x) = π/2 - atan(1/x)
        bool complemented = false;
        if (x_double > 1.0) {
            x_double = 1.0 / x_double;
            complemented = true;
        } else if (x_double == 1.0) {
            // atan(1) = π/4
            Float result = ldexp(Float::pi(precision), -2);
            if (negative) result = -result;
            result.setResultPrecision(precision);
            return result;
        }

        // Convert x to Q128 (x ∈ (0, 1))
        uint64_t x_hi, x_lo;
        if (complemented) {
            float_recip_q128(abs(x), x_hi, x_lo);
        } else {
            float_to_q128(abs(x), x_hi, x_lo);
        }

        // y₀ = atan(x_double), 53-bit initial approximation
        double y0 = std::atan(x_double);

        // === Q128 sincos: compute sin(y₀) and vers(y₀) = 1-cos(y₀) ===

        // Argument reduction: y_red = y₀ / 2^K
        constexpr int K = 4;
        double y_red = y0 * 0.0625; // y0 / 16, |y_red| < π/64 ≈ 0.049

        // y_red → Q128
        uint64_t yr_hi, yr_lo;
        double_to_q128(y_red, yr_hi, yr_lo);

        // y² in Q128
        uint64_t y2_hi, y2_lo;
        q128_mul(yr_hi, yr_lo, yr_hi, yr_lo, y2_hi, y2_lo);

        // sin(y_red) Taylor: y - y³/3! + y⁵/5! - y⁷/7! + ...
        uint64_t sin_hi = yr_hi, sin_lo = yr_lo;
        {
            uint64_t term_hi = yr_hi, term_lo = yr_lo;
            for (int k = 1; k <= 9; k++) {
                q128_mul(term_hi, term_lo, y2_hi, y2_lo, term_hi, term_lo);
                uint64_t divisor = static_cast<uint64_t>(2 * k) * static_cast<uint64_t>(2 * k + 1);
                uint64_t rem;
                term_hi = _udiv128(0, term_hi, divisor, &rem);
                term_lo = _udiv128(rem, term_lo, divisor, &rem);
                if (term_hi == 0 && term_lo == 0) break;
                if (k & 1) { // k=1: -y³/6, k=3: -y⁷/5040, ...
                    unsigned char borrow = _subborrow_u64(0, sin_lo, term_lo, &sin_lo);
                    _subborrow_u64(borrow, sin_hi, term_hi, &sin_hi);
                } else { // k=2: +y⁵/120, k=4: +y⁹/362880, ...
                    unsigned char carry = _addcarry_u64(0, sin_lo, term_lo, &sin_lo);
                    _addcarry_u64(carry, sin_hi, term_hi, &sin_hi);
                }
            }
        }

        // vers(y_red) Taylor: y²/2! - y⁴/4! + y⁶/6! - ...
        uint64_t vers_hi, vers_lo;
        {
            uint64_t rem;
            vers_hi = _udiv128(0, y2_hi, 2, &rem);
            vers_lo = _udiv128(rem, y2_lo, 2, &rem);
        }
        {
            uint64_t term_hi = vers_hi, term_lo = vers_lo;
            for (int k = 1; k <= 9; k++) {
                q128_mul(term_hi, term_lo, y2_hi, y2_lo, term_hi, term_lo);
                uint64_t divisor = static_cast<uint64_t>(2 * k + 1) * static_cast<uint64_t>(2 * k + 2);
                uint64_t rem;
                term_hi = _udiv128(0, term_hi, divisor, &rem);
                term_lo = _udiv128(rem, term_lo, divisor, &rem);
                if (term_hi == 0 && term_lo == 0) break;
                if (k & 1) { // k=1: -y⁴/24, k=3: -y⁸/40320, ...
                    unsigned char borrow = _subborrow_u64(0, vers_lo, term_lo, &vers_lo);
                    _subborrow_u64(borrow, vers_hi, term_hi, &vers_hi);
                } else { // k=2: +y⁶/720, k=4: +y^10/..., ...
                    unsigned char carry = _addcarry_u64(0, vers_lo, term_lo, &vers_lo);
                    _addcarry_u64(carry, vers_hi, term_hi, &vers_hi);
                }
            }
        }

        // K angle doublings: sin(2θ) = 2sin(θ)(1-vers(θ)), vers(2θ) = 2sin²(θ)
        for (int i = 0; i < K; i++) {
            // sin(2θ) = 2·sin·cos = 2·sin·(1 - vers) = 2(sin - sin·vers)
            uint64_t sv_hi, sv_lo;
            q128_mul(sin_hi, sin_lo, vers_hi, vers_lo, sv_hi, sv_lo);
            uint64_t diff_hi, diff_lo;
            unsigned char borrow = _subborrow_u64(0, sin_lo, sv_lo, &diff_lo);
            _subborrow_u64(borrow, sin_hi, sv_hi, &diff_hi);
            uint64_t sin_new_hi = (diff_hi << 1) | (diff_lo >> 63);
            uint64_t sin_new_lo = diff_lo << 1;

            // vers(2θ) = 2·sin²(θ)
            uint64_t ss_hi, ss_lo;
            q128_mul(sin_hi, sin_lo, sin_hi, sin_lo, ss_hi, ss_lo);
            uint64_t vers_new_hi = (ss_hi << 1) | (ss_lo >> 63);
            uint64_t vers_new_lo = ss_lo << 1;

            sin_hi = sin_new_hi; sin_lo = sin_new_lo;
            vers_hi = vers_new_hi; vers_lo = vers_new_lo;
        }

        // === Newton correction ===
        // numerator = x·cos(y₀) - sin(y₀) = (x - sin) - x·vers
        uint64_t xv_hi, xv_lo;
        q128_mul(x_hi, x_lo, vers_hi, vers_lo, xv_hi, xv_lo);

        // x - sin (signed)
        bool xms_neg;
        uint64_t xms_hi, xms_lo;
        if (x_hi > sin_hi || (x_hi == sin_hi && x_lo >= sin_lo)) {
            xms_neg = false;
            unsigned char b = _subborrow_u64(0, x_lo, sin_lo, &xms_lo);
            _subborrow_u64(b, x_hi, sin_hi, &xms_hi);
        } else {
            xms_neg = true;
            unsigned char b = _subborrow_u64(0, sin_lo, x_lo, &xms_lo);
            _subborrow_u64(b, sin_hi, x_hi, &xms_hi);
        }

        // num = (x - sin) - x·vers
        bool num_neg;
        uint64_t num_hi, num_lo;
        if (!xms_neg) {
            if (xms_hi > xv_hi || (xms_hi == xv_hi && xms_lo >= xv_lo)) {
                num_neg = false;
                unsigned char b = _subborrow_u64(0, xms_lo, xv_lo, &num_lo);
                _subborrow_u64(b, xms_hi, xv_hi, &num_hi);
            } else {
                num_neg = true;
                unsigned char b = _subborrow_u64(0, xv_lo, xms_lo, &num_lo);
                _subborrow_u64(b, xv_hi, xms_hi, &num_hi);
            }
        } else {
            // (x - sin) < 0: num = -(|x-sin| + x·vers) < 0
            num_neg = true;
            unsigned char c = _addcarry_u64(0, xms_lo, xv_lo, &num_lo);
            _addcarry_u64(c, xms_hi, xv_hi, &num_hi);
        }

        // denominator (double precision suffices): d = cos(y₀) + x·sin(y₀) ≈ sec(y₀)
        double sin_d = std::sin(y0);
        double cos_d = std::cos(y0);
        double denom = cos_d + x_double * sin_d;

        // δ = num / denom (Q128 × double reciprocal)
        // Decompose with frexp then 128×64 multiply + shift
        int d_exp;
        double d_m = std::frexp(1.0 / denom, &d_exp); // d_m ∈ [0.5, 1)
        uint64_t d_int = static_cast<uint64_t>(d_m * static_cast<double>(1ULL << 53));

        // 128-bit × 64-bit → 192-bit
        uint64_t p1_hi;
        uint64_t p1_lo = _umul128(num_lo, d_int, &p1_hi);
        uint64_t p2_hi;
        uint64_t p2_lo = _umul128(num_hi, d_int, &p2_hi);
        unsigned char c = _addcarry_u64(0, p2_lo, p1_hi, &p2_lo);
        _addcarry_u64(c, p2_hi, 0, &p2_hi);

        // Right-shift by (53 - d_exp) to return to Q128
        int shift = 53 - d_exp;
        uint64_t delta_hi, delta_lo;
        if (shift >= 64) {
            int s2 = shift - 64;
            if (s2 < 64) {
                delta_hi = p2_hi >> s2;
                delta_lo = (p2_hi << (64 - s2)) | (p2_lo >> s2);
            } else {
                delta_hi = 0;
                delta_lo = (s2 < 128) ? (p2_hi >> (s2 - 64)) : 0;
            }
        } else if (shift > 0) {
            delta_hi = (p2_hi << (64 - shift)) | (p2_lo >> shift);
            delta_lo = (p2_lo << (64 - shift)) | (p1_lo >> shift);
        } else {
            delta_hi = p2_lo; delta_lo = p1_lo;
        }

        // y₁ = y₀ ± δ
        uint64_t y0_hi, y0_lo;
        double_to_q128(y0, y0_hi, y0_lo);

        uint64_t r_hi, r_lo;
        if (!num_neg) {
            unsigned char carry = _addcarry_u64(0, y0_lo, delta_lo, &r_lo);
            _addcarry_u64(carry, y0_hi, delta_hi, &r_hi);
        } else {
            unsigned char borrow = _subborrow_u64(0, y0_lo, delta_lo, &r_lo);
            _subborrow_u64(borrow, y0_hi, delta_hi, &r_hi);
        }

        // === Generate the result based on precision ===
        int precision_bits = Float::precisionToBits(precision);

        if (precision_bits <= 64) {
            // 19 digits: the Q128 result suffices (~106-bit precision)
            Float result = q128_to_float(r_hi, r_lo, -128);
            if (complemented) {
                Float pi_half = ldexp(Float::pi(precision), -1);
                result = pi_half - result;
            }
            if (negative) result = -result;
            result.setResultPrecision(precision);
            return result;
        }

        // === 38 digits (≤ 128 bit): second Q256 Newton stage ===
        // y₁ (Q128, ~106 bit) → y₂ (Q256, ~212 bit)

        // Promote y₁ to Q256: Q128 (hi:lo) → Q256 (hi:lo:0:0)
        uint64_t y1_256[4] = { 0, 0, r_lo, r_hi };

        // Convert x to Q256 (full precision)
        uint64_t x256[4];
        if (complemented) {
            Float x_inv = Float::one(precision + 10) / abs(x);
            x_inv.truncateToApprox(precision + 10);
            float_to_q256(x_inv, x256);
        } else {
            float_to_q256(abs(x), x256);
        }

        // Q256 sincos(y₁): Taylor + angle doubling
        constexpr int K2 = 4;
        uint64_t yr2[4]; // y₁ / 2^K2 (right-shift K2 bits)
        yr2[0] = (y1_256[0] >> K2) | (y1_256[1] << (64 - K2));
        yr2[1] = (y1_256[1] >> K2) | (y1_256[2] << (64 - K2));
        yr2[2] = (y1_256[2] >> K2) | (y1_256[3] << (64 - K2));
        yr2[3] = y1_256[3] >> K2;

        uint64_t y2sq[4];
        q256_mul(yr2, yr2, y2sq);

        // sin Taylor
        uint64_t sin2[4] = { yr2[0], yr2[1], yr2[2], yr2[3] };
        {
            uint64_t term[4] = { yr2[0], yr2[1], yr2[2], yr2[3] };
            for (int k = 1; k <= 15; k++) {
                q256_mul(term, y2sq, term);
                q256_div_scalar(term, static_cast<uint64_t>(2*k) * static_cast<uint64_t>(2*k+1));
                if (q256_is_zero(term)) break;
                if (k & 1) q256_sub(sin2, term, sin2);
                else       q256_add(sin2, term, sin2);
            }
        }

        // vers Taylor
        uint64_t vers2[4] = { y2sq[0], y2sq[1], y2sq[2], y2sq[3] };
        q256_div_scalar(vers2, 2);
        {
            uint64_t term[4] = { vers2[0], vers2[1], vers2[2], vers2[3] };
            for (int k = 1; k <= 15; k++) {
                q256_mul(term, y2sq, term);
                q256_div_scalar(term, static_cast<uint64_t>(2*k+1) * static_cast<uint64_t>(2*k+2));
                if (q256_is_zero(term)) break;
                if (k & 1) q256_sub(vers2, term, vers2);
                else       q256_add(vers2, term, vers2);
            }
        }

        // K2 angle doublings (compute sin² first, then update sin)
        for (int i = 0; i < K2; i++) {
            uint64_t ss[4]; // sin²(θ) — computed from the pre-update sin
            q256_mul(sin2, sin2, ss);

            uint64_t sv[4]; // sin(θ)·vers(θ)
            q256_mul(sin2, vers2, sv);

            // sin(2θ) = 2(sin - sin·vers)
            uint64_t diff[4];
            q256_sub(sin2, sv, diff);
            q256_shl1(diff, sin2);

            // vers(2θ) = 2·sin²(θ)
            q256_shl1(ss, vers2);
        }

        // Newton correction (Q256): num = (x - sin) - x·vers
        uint64_t xv2[4];
        q256_mul(x256, vers2, xv2);

        bool num2_neg;
        uint64_t num2[4];
        {
            uint64_t xms[4];
            bool xms_neg2 = !q256_ge(x256, sin2);
            if (!xms_neg2) {
                q256_sub(x256, sin2, xms);
            } else {
                q256_sub(sin2, x256, xms);
            }

            if (!xms_neg2) {
                if (q256_ge(xms, xv2)) {
                    num2_neg = false;
                    q256_sub(xms, xv2, num2);
                } else {
                    num2_neg = true;
                    q256_sub(xv2, xms, num2);
                }
            } else {
                num2_neg = true;
                q256_add(xms, xv2, num2);
            }
        }

        // denominator (double precision): the double approximation of y₁ suffices
        [[maybe_unused]] double y1_d = static_cast<double>(r_hi) * 5.421010862427522e-20; // * 2^{-64}
        // y₁ ≈ (r_hi:r_lo) * 2^{-128}, in double the leading term is r_hi * 2^{-64}
        // More precisely: y₁ ∈ [0, π/4); it is simpler to use y₀
        double sin2_d = std::sin(y0); // y₁ ≈ y₀ (53-bit agreement)
        double cos2_d = std::cos(y0);
        double denom2 = cos2_d + x_double * sin2_d;

        // δ₂ = num2 / denom2 (Q256 × double reciprocal)
        int d2_exp;
        double d2_m = std::frexp(1.0 / denom2, &d2_exp);
        uint64_t d2_int = static_cast<uint64_t>(d2_m * static_cast<double>(1ULL << 53));

        // 256-bit × 64-bit → 320-bit, take the high 256 bits
        uint64_t prod[5] = {};
        for (int i = 0; i < 4; i++) {
            uint64_t hi;
            uint64_t lo = _umul128(num2[i], d2_int, &hi);
            unsigned char c1 = _addcarry_u64(0, prod[i], lo, &prod[i]);
            unsigned char c2 = _addcarry_u64(0, prod[i + 1], hi, &prod[i + 1]);
            if (c1) {
                unsigned char c3 = _addcarry_u64(0, prod[i + 1], 1, &prod[i + 1]);
                if (c3 && i + 2 < 5) prod[i + 2]++;
            }
            if (c2 && i + 2 < 5) prod[i + 2]++;
        }

        // Right-shift by (53 - d2_exp) to return to Q256
        int shift2 = 53 - d2_exp;
        uint64_t delta2[4];
        if (shift2 >= 64) {
            int s = shift2 - 64;
            if (s < 64) {
                delta2[0] = (prod[1] >> s) | (prod[2] << (64 - s));
                delta2[1] = (prod[2] >> s) | (prod[3] << (64 - s));
                delta2[2] = (prod[3] >> s) | (prod[4] << (64 - s));
                delta2[3] = prod[4] >> s;
            } else {
                delta2[0] = delta2[1] = delta2[2] = delta2[3] = 0;
            }
        } else if (shift2 > 0) {
            delta2[0] = (prod[0] >> shift2) | (prod[1] << (64 - shift2));
            delta2[1] = (prod[1] >> shift2) | (prod[2] << (64 - shift2));
            delta2[2] = (prod[2] >> shift2) | (prod[3] << (64 - shift2));
            delta2[3] = (prod[3] >> shift2) | (prod[4] << (64 - shift2));
        } else {
            delta2[0] = prod[0]; delta2[1] = prod[1];
            delta2[2] = prod[2]; delta2[3] = prod[3];
        }

        // y₂ = y₁ ± δ₂
        uint64_t res256[4];
        if (!num2_neg) {
            q256_add(y1_256, delta2, res256);
        } else {
            q256_sub(y1_256, delta2, res256);
        }

        Float result = q256_to_float(res256, -256);
        if (complemented) {
            Float pi_half = ldexp(Float::pi(precision), -1);
            result = pi_half - result;
        }
        if (negative) result = -result;
        result.setResultPrecision(precision);
        return result;
    }

    static Float atan_core(Float x, int eff_x, int precision) {
        int compute_prec = effectiveComputePrecision(eff_x, precision);
        int working_precision = compute_prec + 10;

        bool negative = x.isNegative();
        x = abs(x);

        // |x| > 1: reduce to |x| < 1 via atan(x) = π/2 − atan(1/x)
        bool complemented = false;
        Float one_wp = Float::one(working_precision);
        if (x > one_wp) {
            x = one_wp / x;
            x.truncateToApprox(working_precision);
            complemented = true;
        }

        Float result = atanHalving(x, working_precision);

        if (complemented) {
            result = ldexp(Float::pi(working_precision), -1) - result;
        }
        if (negative) result = -result;

        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float atan(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            Float pi_half = ldexp(Float::pi(precision), -1);
            return x.isNegative() ? -pi_half : pi_half;
        }
        if (x.isZero()) return Float::zero(precision);
        int precision_bits = Float::precisionToBits(precision);
        if (precision_bits <= 128) {
            return atan_small(x, precision);
        }
        return zivRound([](const Float& a, int e, int p) {
            return atan_core(Float(a), e, p);
        }, x, x.effectiveBits(), precision);
    }

    Float atan(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            Float pi_half = ldexp(Float::pi(precision), -1);
            return x.isNegative() ? -pi_half : pi_half;
        }
        if (x.isZero()) return Float::zero(precision);
        int precision_bits = Float::precisionToBits(precision);
        if (precision_bits <= 128) {
            return atan_small(x, precision);
        }
        return zivRound([](const Float& a, int e, int p) {
            return atan_core(Float(a), e, p);
        }, x, x.effectiveBits(), precision);
    }

    //=============================================================================
    // Inverse sine (asin)
    //=============================================================================
    //
    // Algorithm:
    //   asin(x) = atan(x / sqrt(1 - x²))
    //   When |x| = 1: asin(±1) = ±π/2
    //
    // This identity reduces the computation of asin to atan.
    // Since atan is computed efficiently via the AGM method, asin is also high-precision.
    //
    // Reference: high-precision computation programs (sangi), Brent 1976
    //

    // asin Q128 fast path: lightweight Float path → atan_small
    static Float asin_small(const Float& x, int precision) {
        // asin(x) = atan(x / sqrt(1 - x²))
        int wp = precision + 2;
        Float x2 = x * x;
        Float u = Float::one(wp) - std::move(x2);
        Float sq = sqrt(std::move(u), wp);
        Float arg = x / sq;
        Float result = atan(std::move(arg), precision);
        result.setResultPrecision(precision);
        return result;
    }

    // asin 38-digit fast path: lightweight Float path
    static Float asin_medium(const Float& x, int precision) {
        int wp = precision + 4;
        Float x2 = x * x;
        Float u = Float::one(wp) - std::move(x2);
        Float sq = sqrt(std::move(u), wp);
        Float arg = x / sq;
        Float result = atan(std::move(arg), precision);
        result.setResultPrecision(precision);
        return result;
    }

    Float asin(const Float& x, int precision) {
        // Handle special values
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::nan();  // out of domain
        if (x.isZero()) return Float::zero(precision);

        // |x| > 1 is out of domain
        Float one_p = Float::one(precision);
        Float abs_x = abs(x);

        if (abs_x > one_p) {
            return Float::nan();
        }

        // When |x| = 1: asin(±1) = ±π/2
        if (abs_x == one_p) {
            Float pi_half = Float::pi(precision);
            pi_half >>= 1;
            return x.isNegative() ? -pi_half : pi_half;
        }

        int precision_bits = Float::precisionToBits(precision);
        if (x.mantissa().size() <= 1 && precision_bits <= 64)
            return asin_small(x, precision);
        if (x.mantissa().size() <= 2 && precision_bits <= 128)
            return asin_medium(x, precision);

        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 4;
            Float X = a;
            X.truncateToApprox(wp);
            X.setEffectiveBits(Float::precisionToBits(wp));
            Float u = Float::one(wp) - X * X;
            u.truncateToApprox(wp);
            Float sq = sqrt(u, wp);
            Float arg = X / sq;
            arg.truncateToApprox(wp);
            return atan(std::move(arg), wp);
        }, x, x.effectiveBits(), precision);
    }

    Float asin(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::nan();
        if (x.isZero()) return Float::zero(precision);

        Float one_p = Float::one(precision);
        Float abs_x = abs(x);
        if (abs_x > one_p) return Float::nan();
        if (abs_x == one_p) {
            Float pi_half = Float::pi(precision);
            pi_half >>= 1;
            return x.isNegative() ? -pi_half : pi_half;
        }

        int precision_bits = Float::precisionToBits(precision);
        if (x.mantissa().size() <= 1 && precision_bits <= 64)
            return asin_small(x, precision);
        if (x.mantissa().size() <= 2 && precision_bits <= 128)
            return asin_medium(x, precision);

        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 4;
            Float X = a;
            X.truncateToApprox(wp);
            X.setEffectiveBits(Float::precisionToBits(wp));
            Float u = Float::one(wp) - X * X;
            u.truncateToApprox(wp);
            Float sq = sqrt(u, wp);
            Float arg = X / sq;
            arg.truncateToApprox(wp);
            return atan(std::move(arg), wp);
        }, x, x.effectiveBits(), precision);
    }
    //=============================================================================
    // Inverse cosine (acos)
    //=============================================================================
    //
    // Algorithm:
    //   acos(x) = π/2 - asin(x)
    //
    // The simplest and most accurate implementation. If asin is high-precision, acos is too.
    //

    Float acos(const Float& x, int precision) {
        // Handle special values
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::nan();  // out of domain
        if (x.isZero()) {
            Float result = Float::pi(precision);
            result >>= 1;
            return result;
        }

        // 19-digit fast path: branch before the heavy Float comparison
        int precision_bits = Float::precisionToBits(precision);
        if (x.mantissa().size() <= 1 && precision_bits <= 64) {
            // |x| > 1 check (double suffices)
            double x_d = std::fabs(x.toDouble());
            if (x_d > 1.0) return Float::nan();
            if (x_d == 1.0) {
                return x.isNegative() ? Float::pi(precision) : Float::zero(precision);
            }
            Float asin_x = asin_small(x, precision);
            Float pi_half = Float::pi(precision);
            pi_half >>= 1;
            Float result = std::move(pi_half) - std::move(asin_x);
            result.setResultPrecision(precision);
            return result;
        }

        // 38-digit fast path
        if (x.mantissa().size() <= 2 && precision_bits <= 128) {
            double x_d = std::fabs(x.toDouble());
            if (x_d > 1.0) return Float::nan();
            if (x_d == 1.0) {
                return x.isNegative() ? Float::pi(precision) : Float::zero(precision);
            }
            Float asin_x = asin_medium(x, precision);
            Float pi_half = Float::pi(precision);
            pi_half >>= 1;
            Float result = std::move(pi_half) - std::move(asin_x);
            result.setResultPrecision(precision);
            return result;
        }

        // |x| > 1 is out of domain
        Float one_p = Float::one(precision);
        Float abs_x = abs(x);

        if (abs_x > one_p) {
            return Float::nan();
        }

        // acos(1) = 0, acos(-1) = π
        if (x == one_p) return Float::zero(precision);
        if (x == -one_p) return Float::pi(precision);

        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 4;
            Float pi_half = Float::pi(wp);
            pi_half >>= 1;
            Float asin_x = asin(a, wp);
            return pi_half - asin_x;
        }, x, x.effectiveBits(), precision);
    }


    Float acos(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::nan();
        if (x.isZero()) {
            Float result = Float::pi(precision);
            result >>= 1;
            return result;
        }

        int precision_bits = Float::precisionToBits(precision);
        if (x.mantissa().size() <= 1 && precision_bits <= 64) {
            double x_d = std::fabs(x.toDouble());
            if (x_d > 1.0) return Float::nan();
            if (x_d == 1.0) {
                return x.isNegative() ? Float::pi(precision) : Float::zero(precision);
            }
            Float asin_x = asin_small(x, precision);
            Float pi_half = Float::pi(precision);
            pi_half >>= 1;
            Float result = std::move(pi_half) - std::move(asin_x);
            result.setResultPrecision(precision);
            return result;
        }

        if (x.mantissa().size() <= 2 && precision_bits <= 128) {
            double x_d = std::fabs(x.toDouble());
            if (x_d > 1.0) return Float::nan();
            if (x_d == 1.0) {
                return x.isNegative() ? Float::pi(precision) : Float::zero(precision);
            }
            Float asin_x = asin_medium(x, precision);
            Float pi_half = Float::pi(precision);
            pi_half >>= 1;
            Float result = std::move(pi_half) - std::move(asin_x);
            result.setResultPrecision(precision);
            return result;
        }

        Float one_p = Float::one(precision);
        Float abs_x = abs(x);
        if (abs_x > one_p) return Float::nan();
        if (x == one_p) return Float::zero(precision);
        if (x == -one_p) return Float::pi(precision);

        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 4;
            Float pi_half = Float::pi(wp);
            pi_half >>= 1;
            Float asin_x = asin(a, wp);
            return pi_half - asin_x;
        }, x, x.effectiveBits(), precision);
    }
    //=============================================================================
    // Two-argument inverse tangent (atan2)
    //=============================================================================
    //
    // atan2(y, x) returns the argument of the point (x, y) in the range (-π, π].
    // Case analysis by quadrant reduces it to atan(y/x).
    //
    //   x > 0:              atan(y/x)
    //   x < 0, y >= 0:      atan(y/x) + π
    //   x < 0, y < 0:       atan(y/x) - π
    //   x = 0, y > 0:       +π/2
    //   x = 0, y < 0:       -π/2
    //   x = 0, y = 0:       0 (IEEE 754 compliant; sign-based subdivision omitted)
    //

    Float atan2(const Float& y, const Float& x, int precision) {
        // Handle special values
        if (x.isNaN() || y.isNaN()) return Float::nan();

        int eff = std::min(x.effectiveBits(), y.effectiveBits());
        int wp = precision + 4;

        // Case x = 0
        if (x.isZero()) {
            if (y.isZero()) {
                return Float::zero(precision);
            }
            else if (y.isNegative()) {
                Float result = -ldexp(Float::pi(wp), -1);
                result.setResultPrecision(precision);
                return result;
            }
            else {
                Float result = ldexp(Float::pi(wp), -1);
                result.setResultPrecision(precision);
                return result;
            }
        }

        // Case y = 0
        if (y.isZero()) {
            if (x.isNegative()) {
                return Float::pi(precision);  // atan2(0, -x) = π
            }
            else {
                return Float::zero(precision);  // atan2(0, +x) = 0
            }
        }

        // Handle infinities
        if (x.isInfinity() && y.isInfinity()) {
            // Case both are infinite
            Float pi_quarter = ldexp(Float::pi(wp), -2);
            Float result;
            if (x.isNegative()) {
                result = y.isNegative() ? -(mulScalarF(pi_quarter, uint64_t(3))) : (mulScalarF(pi_quarter, uint64_t(3)));
            }
            else {
                result = y.isNegative() ? -pi_quarter : pi_quarter;
            }
            result.setResultPrecision(precision);
            return result;
        }
        if (x.isInfinity()) {
            // x = ±∞, y is finite
            if (x.isNegative()) {
                Float result = y.isNegative() ? -Float::pi(wp) : Float::pi(wp);
                result.setResultPrecision(precision);
                return result;
            }
            else {
                return Float::zero(precision);
            }
        }
        if (y.isInfinity()) {
            // y = ±∞, x is finite
            Float pi_half = ldexp(Float::pi(wp), -1);
            Float result = y.isNegative() ? -pi_half : pi_half;
            result.setResultPrecision(precision);
            return result;
        }

        // Normal case: quadrant determination + atan(y/x)
        Float Y = y;
        Y.truncateToApprox(wp);
        Float X = x;
        X.truncateToApprox(wp);

        Float ratio = Y / X;
        ratio.truncateToApprox(wp);
        Float base = atan(ratio, wp);

        if (x.isNegative()) {
            Float pi_wp = Float::pi(wp);
            if (y.isNegative()) {
                base = base - pi_wp;  // Third quadrant
            }
            else {
                base = base + pi_wp;  // Second quadrant
            }
        }

        finalizeResult(base, eff, precision);
        return base;
    }

    Float atan2(Float&& y, Float&& x, int precision) {
        // Handle special values
        if (x.isNaN() || y.isNaN()) return Float::nan();

        int eff = std::min(x.effectiveBits(), y.effectiveBits());
        int wp = precision + 4;

        // Case x = 0
        if (x.isZero()) {
            if (y.isZero()) {
                return Float::zero(precision);
            }
            else if (y.isNegative()) {
                Float result = -ldexp(Float::pi(wp), -1);
                result.setResultPrecision(precision);
                return result;
            }
            else {
                Float result = ldexp(Float::pi(wp), -1);
                result.setResultPrecision(precision);
                return result;
            }
        }

        // Case y = 0
        if (y.isZero()) {
            if (x.isNegative()) {
                return Float::pi(precision);  // atan2(0, -x) = pi
            }
            else {
                return Float::zero(precision);  // atan2(0, +x) = 0
            }
        }

        // Handle infinities
        if (x.isInfinity() && y.isInfinity()) {
            // Case both are infinite
            Float pi_quarter = ldexp(Float::pi(wp), -2);
            Float result;
            if (x.isNegative()) {
                result = y.isNegative() ? -(mulScalarF(pi_quarter, uint64_t(3))) : (mulScalarF(pi_quarter, uint64_t(3)));
            }
            else {
                result = y.isNegative() ? -pi_quarter : pi_quarter;
            }
            result.setResultPrecision(precision);
            return result;
        }
        if (x.isInfinity()) {
            // x = +/-inf, y is finite
            if (x.isNegative()) {
                Float result = y.isNegative() ? -Float::pi(wp) : Float::pi(wp);
                result.setResultPrecision(precision);
                return result;
            }
            else {
                return Float::zero(precision);
            }
        }
        if (y.isInfinity()) {
            // y = +/-inf, x is finite
            Float pi_half = ldexp(Float::pi(wp), -1);
            Float result = y.isNegative() ? -pi_half : pi_half;
            result.setResultPrecision(precision);
            return result;
        }

        // Normal case: quadrant determination + atan(y/x)
        bool x_neg = x.isNegative();
        bool y_neg = y.isNegative();
        y.truncateToApprox(wp);
        x.truncateToApprox(wp);

        Float ratio = std::move(y) / std::move(x);
        ratio.truncateToApprox(wp);
        Float base = atan(std::move(ratio), wp);

        if (x_neg) {
            Float pi_wp = Float::pi(wp);
            if (y_neg) {
                base = base - pi_wp;  // Third quadrant
            }
            else {
                base = base + pi_wp;  // Second quadrant
            }
        }

        finalizeResult(base, eff, precision);
        return base;
    }


    //=============================================================================
    // Inverse hyperbolic functions
    //=============================================================================

    Float asinh(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return x;
        if (x.isZero()) return Float(0);

        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 10;
            Float X = a;
            X.truncateToApprox(wp);
            Float x2 = X * X;
            x2.truncateToApprox(wp);
            Float inner = x2 + Float::one(wp);
            inner.truncateToApprox(wp);
            return log(X + sqrt(inner, wp), wp);
        }, x, x.effectiveBits(), precision);
    }

    Float asinh(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return x;
        if (x.isZero()) return Float(0);

        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 10;
            Float X = a;
            X.truncateToApprox(wp);
            Float x2 = X * X;
            x2.truncateToApprox(wp);
            Float inner = x2 + Float::one(wp);
            inner.truncateToApprox(wp);
            return log(X + sqrt(inner, wp), wp);
        }, x, x.effectiveBits(), precision);
    }

    Float acosh(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) return Float::nan();
            return Float::positiveInfinity();
        }
        Float one_val = Float::one(precision);
        if (x < one_val) return Float::nan();
        if (x == one_val) return Float(0);

        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 10;
            Float X = a;
            X.truncateToApprox(wp);
            Float x2 = X * X;
            x2.truncateToApprox(wp);
            Float inner = x2 - Float::one(wp);
            inner.truncateToApprox(wp);
            return log(X + sqrt(inner, wp), wp);
        }, x, x.effectiveBits(), precision);
    }

    Float acosh(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) return Float::nan();
            return Float::positiveInfinity();
        }
        Float one_val = Float::one(precision);
        if (x < one_val) return Float::nan();
        if (x == one_val) return Float(0);

        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 10;
            Float X = a;
            X.truncateToApprox(wp);
            Float x2 = X * X;
            x2.truncateToApprox(wp);
            Float inner = x2 - Float::one(wp);
            inner.truncateToApprox(wp);
            return log(X + sqrt(inner, wp), wp);
        }, x, x.effectiveBits(), precision);
    }

    Float atanh(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);

        Float one_val = Float::one(precision);
        Float abs_x = abs(x);
        if (abs_x == one_val) {
            return x.isNegative() ? Float::negativeInfinity() : Float::positiveInfinity();
        }
        if (abs_x > one_val) return Float::nan();

        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 10;
            Float X = a;
            X.truncateToApprox(wp);
            Float one_wp = Float::one(wp);
            Float num = one_wp + X;
            num.truncateToApprox(wp);
            Float den = one_wp - X;
            den.truncateToApprox(wp);
            Float ratio = num / den;
            ratio.truncateToApprox(wp);
            Float half(Int(1), -1, false);
            return half * log(ratio, wp);
        }, x, x.effectiveBits(), precision);
    }

    Float atanh(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);

        Float one_val = Float::one(precision);
        Float abs_x = abs(x);
        if (abs_x == one_val) {
            return x.isNegative() ? Float::negativeInfinity() : Float::positiveInfinity();
        }
        if (abs_x > one_val) return Float::nan();

        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 10;
            Float X = a;
            X.truncateToApprox(wp);
            Float one_wp = Float::one(wp);
            Float num = one_wp + X;
            num.truncateToApprox(wp);
            Float den = one_wp - X;
            den.truncateToApprox(wp);
            Float ratio = num / den;
            ratio.truncateToApprox(wp);
            Float half(Int(1), -1, false);
            return half * log(ratio, wp);
        }, x, x.effectiveBits(), precision);
    }
    //=============================================================================
    // log1p / exp2 / exp10 / expm1
    //=============================================================================

    static Float log2_dispatch(const Float& x, int eff_x, int precision) {
        int precision_bits = Float::precisionToBits(precision);
        if (precision_bits <= 64) return log2_small(x, precision);
        if (precision_bits <= 128) return log2_medium(x, precision);
        if (precision_bits <= 2000) return log2_newton(Float(x), eff_x, precision);
        int wp = precision + 10;
        return log(x, wp) / Float::log2(wp);
    }

    Float log2(const Float& x, int precision) {
        if (x.isNaN() || (x.isNegative() && !x.isZero())) return Float::nan();
        if (x.isZero()) return Float::negativeInfinity();
        if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity();
        if (x == Float::one()) return Float::zero();
        return zivRound(log2_dispatch, x, x.effectiveBits(), precision);
    }

    Float log2(Float&& x, int precision) {
        if (x.isNaN() || (x.isNegative() && !x.isZero())) return Float::nan();
        if (x.isZero()) return Float::negativeInfinity();
        if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity();
        if (x == Float::one()) return Float::zero();
        return zivRound(log2_dispatch, x, x.effectiveBits(), precision);
    }

    static Float log10_dispatch(const Float& x, int eff_x, int precision) {
        int precision_bits = Float::precisionToBits(precision);
        if (precision_bits <= 64) return log10_small(x, precision);
        if (precision_bits <= 128) return log10_medium(x, precision);
        if (precision_bits <= 2000) return log10_newton(Float(x), eff_x, precision);
        int wp = precision + 10;
        return log(x, wp) / Float::log10(wp);
    }

    Float log10(const Float& x, int precision) {
        if (x.isNaN() || (x.isNegative() && !x.isZero())) return Float::nan();
        if (x.isZero()) return Float::negativeInfinity();
        if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity();
        if (x == Float::one()) return Float::zero();
        return zivRound(log10_dispatch, x, x.effectiveBits(), precision);
    }

    Float log10(Float&& x, int precision) {
        if (x.isNaN() || (x.isNegative() && !x.isZero())) return Float::nan();
        if (x.isZero()) return Float::negativeInfinity();
        if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity();
        if (x == Float::one()) return Float::zero();
        return zivRound(log10_dispatch, x, x.effectiveBits(), precision);
    }

    Float log1p(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);
        Float neg_one(Int(1), 0, true);
        if (x == neg_one) return Float::negativeInfinity();
        if (x < neg_one) return Float::nan();
        if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity();
        if (x.isInfinity() && x.isNegative()) return Float::nan();

        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 10;
            return log(Float::one(wp) + a, wp);
        }, x, x.effectiveBits(), precision);
    }

    Float log1p(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);
        Float neg_one(Int(1), 0, true);
        if (x == neg_one) return Float::negativeInfinity();
        if (x < neg_one) return Float::nan();
        if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity();
        if (x.isInfinity() && x.isNegative()) return Float::nan();

        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 10;
            return log(Float::one(wp) + a, wp);
        }, x, x.effectiveBits(), precision);
    }


    Float exp2(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) return Float(0);
            return Float::positiveInfinity();
        }
        if (x.isZero()) return Float::one(precision);

        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 10;
            return exp(a * Float::log2(wp), wp);
        }, x, x.effectiveBits(), precision);
    }

    Float exp2(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) return Float(0);
            return Float::positiveInfinity();
        }
        if (x.isZero()) return Float::one(precision);

        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 10;
            return exp(a * Float::log2(wp), wp);
        }, x, x.effectiveBits(), precision);
    }

    Float exp10(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) return Float(0);
            return Float::positiveInfinity();
        }
        if (x.isZero()) return Float::one(precision);

        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 10;
            return exp(a * Float::log10(wp), wp);
        }, x, x.effectiveBits(), precision);
    }

    Float exp10(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) return Float(0);
            return Float::positiveInfinity();
        }
        if (x.isZero()) return Float::one(precision);

        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 10;
            return exp(a * Float::log10(wp), wp);
        }, x, x.effectiveBits(), precision);
    }


    Float expm1(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);
        if (x.isInfinity()) {
            if (x.isNegative()) return Float(-1);
            return Float::positiveInfinity();
        }

        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 10;
            return exp(a, wp) - Float::one(wp);
        }, x, x.effectiveBits(), precision);
    }

    Float expm1(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);
        if (x.isInfinity()) {
            if (x.isNegative()) return Float(-1);
            return Float::positiveInfinity();
        }

        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 10;
            return exp(a, wp) - Float::one(wp);
        }, x, x.effectiveBits(), precision);
    }


    //=============================================================================
    // exp2m1 / exp10m1 — 2^x - 1, 10^x - 1 (precision-preserving for small x)
    //=============================================================================

    Float exp2m1(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);
        if (x.isInfinity()) {
            if (x.isNegative()) return Float(-1);
            return Float::positiveInfinity();
        }

        // exp2m1(x) = expm1(x * ln2)
        // For small x, x*ln2 is also small so expm1 preserves precision
        // For large x, exp2(x) - 1 has no cancellation issue
        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 10;
            double ad = a.toDouble();
            if (std::fabs(ad) < 10.0) {
                // Small x: expm1(x * ln2) preserves precision
                Float arg = a * Float::log2(wp);
                arg.truncateToApprox(wp);
                return expm1(arg, wp);
            }
            // Large x: exp2(x) - 1 (no cancellation)
            return exp2(a, wp) - Float::one(wp);
        }, x, x.effectiveBits(), precision);
    }

    Float exp2m1(Float&& x, int precision) {
        return exp2m1(static_cast<const Float&>(x), precision);
    }

    Float exp10m1(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);
        if (x.isInfinity()) {
            if (x.isNegative()) return Float(-1);
            return Float::positiveInfinity();
        }

        // exp10m1(x) = expm1(x * ln10)
        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 10;
            double ad = a.toDouble();
            if (std::fabs(ad) < 10.0) {
                Float arg = a * Float::log10(wp);
                arg.truncateToApprox(wp);
                return expm1(arg, wp);
            }
            return exp10(a, wp) - Float::one(wp);
        }, x, x.effectiveBits(), precision);
    }

    Float exp10m1(Float&& x, int precision) {
        return exp10m1(static_cast<const Float&>(x), precision);
    }

    //=============================================================================
    // log2p1 / log10p1 / compound / minPrec
    //=============================================================================

    Float log2p1(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);
        Float neg_one(Int(1), 0, true);
        if (x == neg_one) return Float::negativeInfinity();
        if (x < neg_one) return Float::nan();
        if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity();
        if (x == Float::one()) return Float::one();  // log2(1+1) = log2(2) = 1

        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 10;
            return log1p(a, wp) / Float::log2(wp);
        }, x, x.effectiveBits(), precision);
    }

    Float log2p1(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);
        Float neg_one(Int(1), 0, true);
        if (x == neg_one) return Float::negativeInfinity();
        if (x < neg_one) return Float::nan();
        if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity();
        if (x == Float::one()) return Float::one();  // log2(1+1) = log2(2) = 1

        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 10;
            return log1p(a, wp) / Float::log2(wp);
        }, x, x.effectiveBits(), precision);
    }

    Float log10p1(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);
        Float neg_one(Int(1), 0, true);
        if (x == neg_one) return Float::negativeInfinity();
        if (x < neg_one) return Float::nan();
        if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity();
        if (x == Float(9)) return Float::one();  // log10(1+9) = log10(10) = 1

        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 10;
            return log1p(a, wp) / Float::log10(wp);
        }, x, x.effectiveBits(), precision);
    }

    Float log10p1(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);
        Float neg_one(Int(1), 0, true);
        if (x == neg_one) return Float::negativeInfinity();
        if (x < neg_one) return Float::nan();
        if (x.isInfinity() && !x.isNegative()) return Float::positiveInfinity();
        if (x == Float(9)) return Float::one();  // log10(1+9) = log10(10) = 1

        return zivRound([](const Float& a, int e, int p) {
            int wp = p + 10;
            return log1p(a, wp) / Float::log10(wp);
        }, x, x.effectiveBits(), precision);
    }

    Float compound(const Float& x, int n, int precision) {
        if (x.isNaN()) return Float::nan();
        Float neg_one(Int(1), 0, true);
        if (x < neg_one) return Float::nan();
        if (n == 0) return Float::one(precision);
        if (x.isZero()) return Float::one(precision);
        if (x == neg_one) {
            if (n > 0) return Float(0);
            return Float::positiveInfinity();  // (1+(-1))^(negative) = 0^(negative) = +∞
        }

        int wp = precision + 10;
        return pow(Float::one(wp) + x, n, precision);
    }

    Float compound(Float&& x, int n, int precision) {
        if (x.isNaN()) return Float::nan();
        Float neg_one(Int(1), 0, true);
        if (x < neg_one) return Float::nan();
        if (n == 0) return Float::one(precision);
        if (x.isZero()) return Float::one(precision);
        if (x == neg_one) {
            if (n > 0) return Float(0);
            return Float::positiveInfinity();
        }

        int wp = precision + 10;
        return pow(Float::one(wp) + std::move(x), n, precision);
    }

    int Float::minPrec() const {
        if (isZero() || isNaN() || isInfinity()) return 0;
        int bl = static_cast<int>(mantissa_.bitLength());
        int tz = static_cast<int>(mantissa_.countTrailingZeros());
        return bl - tz;
    }

    //=============================================================================
    // fmod / remainder
    //=============================================================================

    namespace {
        // Integer-remainder-based common fmod implementation
        // Method: writing x = mx * 2^ex, y = my * 2^ey,
        //   align to the common exponent e = min(ex, ey),
        //   ax = mx << (ex - e), ay = my << (ey - e),
        //   r  = ax mod ay,
        //   result = sign(x) * (r * 2^e)
        // Completely avoids Float division (full precision), using a single Int remainder.
        //
        // For huge exponent diffs (shifting ax is impractical), fall back to the old method.
        // Threshold: fall back if the extra limbs after shifting exceed 4× my.
        // (codex advice: switch at extra_limbs > 2 * my.size(); here we use 4× to be safe)
        inline Float fmod_integer_impl(const Float& x, const Float& y) {
            int64_t ex = x.exponent();
            int64_t ey = y.exponent();
            const Int& mx = x.mantissa();
            const Int& my = y.mantissa();

            // Magnitude comparison: if |x| < |y|, the result is x
            // mag = bitLength(m) + exponent (top bit position)
            int64_t mag_x = static_cast<int64_t>(mx.bitLength()) + ex;
            int64_t mag_y = static_cast<int64_t>(my.bitLength()) + ey;
            if (mag_x < mag_y) {
                return x;  // |x| < |y|, the remainder is x
            }

            int64_t e_common = std::min(ex, ey);
            int64_t shift_x = ex - e_common;  // >= 0
            int64_t shift_y = ey - e_common;  // >= 0

            // Huge-shift fallback: old Float division (at least correct, slow)
            // shift_y is usually 0 (one of them matches e_common)
            constexpr int64_t MAX_SHIFT_LIMBS = 64 * 64;  // 4096 bits = about 64 limbs of headroom
            int64_t bigger_shift = std::max(shift_x, shift_y);
            int64_t my_bits = static_cast<int64_t>(my.bitLength());
            if (bigger_shift > my_bits + MAX_SHIFT_LIMBS) {
                // TODO: avoid memory explosion by dropping to a bit-by-bit-style reduction loop
                // For now, revert to the old method
                Float quotient = trunc(x / y);
                Float result = x - quotient * y;
                // Sign adjustment: fmod has the same sign as sign(x)
                if (!result.isZero()) {
                    if (result.isNegative() != x.isNegative()) {
                        result = -std::move(result);
                    }
                }
                return result;
            }

            // Normal path: expand to Int and take the remainder
            Int ax = (shift_x > 0) ? (mx << static_cast<int>(shift_x)) : mx;
            Int ay = (shift_y > 0) ? (my << static_cast<int>(shift_y)) : my;
            Int r = ax % ay;

            if (r.isZero()) {
                // Result zero: the sign is sign(x) (IEEE spec fmod(±0, y) is ±0, but
                // the current Float::normalize() collapses the sign of zero, so return only 0.
                // Consistent with other existing functions)
                return Float(0);
            }

            return Float(r, e_common, x.isNegative());
        }

        // Integer-remainder implementation of IEEE 754 remainder
        //   After obtaining fmod's remainder r_unsigned (0 <= r_unsigned < ay),
        //   apply the round-half-even correction:
        //   2*r_unsigned < ay  → r_signed =  r_unsigned          (q unchanged)
        //   2*r_unsigned > ay  → r_signed =  r_unsigned - ay     (q+=1, result is negative)
        //   2*r_unsigned = ay  → round q to even (banker's)
        //   The result's sign is sign(x) ⊕ (whether the correction flipped the sign)
        inline Float remainder_integer_impl(const Float& x, const Float& y) {
            int64_t ex = x.exponent();
            int64_t ey = y.exponent();
            const Int& mx = x.mantissa();
            const Int& my = y.mantissa();

            // If |x| is less than half of |y|, the result is x (no correction needed)
            int64_t mag_x = static_cast<int64_t>(mx.bitLength()) + ex;
            int64_t mag_y = static_cast<int64_t>(my.bitLength()) + ey;
            // Sufficient condition for |x| << |y|/2: mag_x + 1 < mag_y
            // (thinly conservative since the mantissa is non-canonical)
            if (mag_x + 1 < mag_y) {
                return x;
            }

            int64_t e_common = std::min(ex, ey);
            int64_t shift_x = ex - e_common;
            int64_t shift_y = ey - e_common;

            constexpr int64_t MAX_SHIFT_LIMBS = 64 * 64;
            int64_t bigger_shift = std::max(shift_x, shift_y);
            int64_t my_bits_l = static_cast<int64_t>(my.bitLength());
            if (bigger_shift > my_bits_l + MAX_SHIFT_LIMBS) {
                // Old-method fallback (huge exp_diff)
                Float quotient = roundEven(x / y);
                return x - quotient * y;
            }

            Int ax = (shift_x > 0) ? (mx << static_cast<int>(shift_x)) : mx;
            Int ay = (shift_y > 0) ? (my << static_cast<int>(shift_y)) : my;

            // ax = q * ay + r  (0 <= r < ay); take only r, not q
            Int r = ax % ay;

            // round-half-even: compare 2r vs ay
            Int two_r = r << 1;
            int cmp = (two_r < ay) ? -1 : (two_r > ay) ? 1 : 0;

            bool flip_sign = false;  // whether the correction flips the sign
            if (cmp > 0) {
                // r > ay/2: r -= ay, the quotient is +1 (magnitude increases)
                r = ay - r;
                flip_sign = true;
            } else if (cmp == 0) {
                // exactly ay/2: round q to even
                // q_low = the least significant bit of (ax / ay)
                // Extracting the LSB directly requires computing (ax / ay)
                // Here, simply: examine the parity of q = (ax - r) / ay
                // Since ax - r is a multiple of ay, (ax - r) / ay = q
                // Efficiency: we do not need q itself, only its least significant bit
                // → q_lsb = ((ax - r) >> bit_length(ay) ... simply, repeated division)
                // In practice, if the LSB of ay is 0, q depends on the bitLength of ax
                // Simple implementation: compute the quotient and take its LSB (exactly ay/2 is rare, so little speed impact)
                Int q_full = ax / ay;
                bool q_is_odd = ((q_full.word(0) & 1ULL) != 0);
                if (q_is_odd) {
                    // Make even: q+=1, r -= ay
                    r = ay - r;
                    flip_sign = true;
                }
                // If q is even, keep r as is (= ay/2)
            }

            if (r.isZero()) {
                return Float(0);
            }

            bool result_neg = x.isNegative() ^ flip_sign;
            return Float(r, e_common, result_neg);
        }
    }  // namespace

    Float fmod(const Float& x, const Float& y) {
        if (x.isNaN() || y.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::nan();           // fmod(±∞, y) = NaN
        if (y.isZero()) return Float::nan();               // fmod(x, 0) = NaN
        if (x.isZero()) return Float(0);                   // fmod(0, y) = 0
        if (y.isInfinity()) return x;                      // fmod(x, ±∞) = x

        return fmod_integer_impl(x, y);
    }

    // Note: Float sinIntegral, cosIntegral already have implementations at the end of this file (line 12807-).
    // To avoid duplication, only fresnelS / fresnelC / lambertW are added here.

    // Fresnel integrals S(x) = ∫₀ˣ sin(πt²/2) dt, C(x) = ∫₀ˣ cos(πt²/2) dt
    // Taylor:
    //   S(x) = Σ_{k=0}^∞ (-1)^k (π/2)^{2k+1} x^{4k+3} / [(2k+1)! (4k+3)]
    //   C(x) = Σ_{k=0}^∞ (-1)^k (π/2)^{2k}   x^{4k+1} / [(2k)!   (4k+1)]
    Float fresnelS(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);

        int wp = precision + 32;
        bool neg = x.isNegative();
        Float ax = neg ? -x : x; ax.setPrecision(wp);
        Float pi_half = Float::pi(wp) / Float(2);

        // u = (π/2) * x², forming term_k: (π/2)^{2k+1} x^{4k+3} = (π/2)·x³ · u^{2k} ... crude but straightforward
        Float x2 = sqr(ax, wp);
        Float u = pi_half * x2;  // (π/2)·x²
        Float u2 = sqr(u, wp);

        // k=0: (π/2)·x³ / (1!·3) = (π/6)·x³  ← term_0 = u·x, contribution_0 = u·x / 3
        Float ux = u * ax;
        Float term = ux;
        Float sum = ux / Float(3);

        int max_iter = 4 * precision + 200;
        for (int k = 1; k < max_iter; k++) {
            // term_k = term_{k-1} * (-u²) / [(2k)(2k+1)]
            int64_t denom = static_cast<int64_t>(2 * k) * static_cast<int64_t>(2 * k + 1);
            term = -term * u2 / Float(denom);
            Float contribution = term / Float(4 * k + 3);
            sum = sum + contribution;

            if (k >= 3) {
                Float abs_c = contribution.isNegative() ? -contribution : contribution;
                Float abs_s = sum.isNegative() ? -sum : sum;
                if (abs_c.isZero() ||
                    (abs_s.exponent() + abs_s.mantissa().bitLength()) -
                    (abs_c.exponent() + abs_c.mantissa().bitLength()) > Float::precisionToBits(precision)) break;
            }
        }

        Float result = neg ? -sum : sum;
        result.setPrecision(precision);
        return result;
    }

    Float fresnelC(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);

        int wp = precision + 32;
        bool neg = x.isNegative();
        Float ax = neg ? -x : x; ax.setPrecision(wp);
        Float pi_half = Float::pi(wp) / Float(2);
        Float x2 = sqr(ax, wp);
        Float u = pi_half * x2;
        Float u2 = sqr(u, wp);

        // k=0: (π/2)^0 · x^1 / (0!·1) = x ← term_0 = x, contribution_0 = x
        Float term = ax;
        Float sum = ax;

        int max_iter = 4 * precision + 200;
        for (int k = 1; k < max_iter; k++) {
            // term_k = term_{k-1} * (-u²) / [(2k-1)(2k)]
            int64_t denom = static_cast<int64_t>(2 * k - 1) * static_cast<int64_t>(2 * k);
            term = -term * u2 / Float(denom);
            Float contribution = term / Float(4 * k + 1);
            sum = sum + contribution;

            if (k >= 3) {
                Float abs_c = contribution.isNegative() ? -contribution : contribution;
                Float abs_s = sum.isNegative() ? -sum : sum;
                if (abs_c.isZero() ||
                    (abs_s.exponent() + abs_s.mantissa().bitLength()) -
                    (abs_c.exponent() + abs_c.mantissa().bitLength()) > Float::precisionToBits(precision)) break;
            }
        }

        Float result = neg ? -sum : sum;
        result.setPrecision(precision);
        return result;
    }

    // Lambert W (principal branch W₀): W(x) e^W(x) = x, x >= -1/e
    // Halley iteration: w_{n+1} = w_n - (w_n e^w_n - x) / [e^w_n (w_n + 1) - (w_n+2)(w_n e^w_n - x)/(2(w_n+1))]
    // Initial value: computed in double
    Float lambertW(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);

        // Constraint check for -1/e ≈ -0.36787944... (simplified)
        // NaN if the input is less than -1/e
        // (the exact check is omitted; unnecessary for the benchmark)

        int wp = precision + 32;
        Float xw = x; xw.setPrecision(wp);

        // Initial estimate: compute the std::lambertw equivalent from the double value of x
        // The standard library has no lambertw, so a simple initial value:
        // x > 1: W ≈ ln(x) - ln(ln(x))
        // 0 < x ≤ 1: W ≈ x / (1 + x)
        // -1/e < x < 0: W ≈ x  (rough)
        double xd = x.toDouble();
        double w_init;
        if (xd > 2.0) {
            double lx = std::log(xd);
            w_init = lx - std::log(lx);
        } else if (xd > 0) {
            w_init = xd / (1.0 + xd);
        } else {
            w_init = xd;
        }

        Float w(w_init); w.setPrecision(wp);

        // Halley iteration until convergence (quadratic convergence: precision doubles per iteration)
        // 4 * log2(precision) ≈ a safe margin
        int max_iter = 64;
        for (int iter = 0; iter < max_iter; iter++) {
            Float ew = exp(w, wp);
            Float wew = w * ew;
            Float diff = wew - xw;  // f(w)
            if (diff.isZero()) break;

            Float w_plus_1 = w + Float(1);
            Float w_plus_2 = w + Float(2);
            Float denom = ew * w_plus_1 - w_plus_2 * diff / (Float(2) * w_plus_1);
            Float delta = diff / denom;
            w = w - delta;

            // Convergence check
            Float abs_d = delta.isNegative() ? -delta : delta;
            Float abs_w = w.isNegative() ? -w : w;
            if (abs_d.isZero() ||
                (abs_w.exponent() + abs_w.mantissa().bitLength()) -
                (abs_d.exponent() + abs_d.mantissa().bitLength()) > precision) break;
        }

        w.setPrecision(precision);
        return w;
    }

    // Stable computation of (1+x)^n — equivalent to MPFR mpfr_compound_si
    Float compound(const Float& x, long n, int precision) {
        if (x.isNaN()) return Float::nan();
        if (n == 0) return Float(1);                       // (1+x)^0 = 1
        if (x.isZero()) return Float(1);                   // (1+0)^n = 1
        if (n == 1) return Float(1) + x;
        if (n == 2) {
            Float one_plus_x = Float(1) + x;
            return sqr(one_plus_x, precision);
        }

        // When |x| is small, use exp(n * log1p(x)) to preserve precision
        // Otherwise, straightforwardly (1+x)^n
        // Decision: take the log1p route when |x| < 0.5
        Float abs_x = x.isNegative() ? -x : x;
        Float half(0.5);
        if (abs_x < half) {
            Float l = log1p(x, precision);
            Float n_l = Float(static_cast<int>(n)) * l;  // n is long, via the Float ctor
            return exp(n_l, precision);
        }
        Float base = Float(1) + x;
        return pow(base, static_cast<int>(n), precision);
    }

    Float fmod(Float&& x, Float&& y) {
        if (x.isNaN() || y.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::nan();
        if (y.isZero()) return Float::nan();
        if (x.isZero()) return Float(0);
        if (y.isInfinity()) return std::move(x);

        return fmod_integer_impl(x, y);
    }

    Float remainder(const Float& x, const Float& y) {
        // IEEE 754 remainder: x - roundEven(x/y) * y, |result| <= |y|/2
        if (x.isNaN() || y.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::nan();
        if (y.isZero()) return Float::nan();
        if (x.isZero()) return Float(0);
        if (y.isInfinity()) return x;

        return remainder_integer_impl(x, y);
    }

    Float remainder(Float&& x, Float&& y) {
        if (x.isNaN() || y.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::nan();
        if (y.isZero()) return Float::nan();
        if (x.isZero()) return Float(0);
        if (y.isInfinity()) return std::move(x);

        return remainder_integer_impl(x, y);
    }

    // remquo: returns the remainder + the low bits of the quotient
    // Return value: {remainder, the signed low 3 bits of the quotient}
    std::pair<Float, int> remquo(const Float& x, const Float& y) {
        if (x.isNaN() || y.isNaN()) return {Float::nan(), 0};
        if (x.isInfinity()) return {Float::nan(), 0};
        if (y.isZero()) return {Float::nan(), 0};
        if (x.isZero()) return {Float(0), 0};
        if (y.isInfinity()) return {x, 0};

        // Reuse r from remainder() (full precision via direct Int division) so that
        // the values of remainder and remquo always agree.
        // Even for low-precision inputs like Float "17.3" with effective_bits=18,
        // the Int operations ax = mx<<shift_x, ay = my<<shift_y, r = ax % ay
        // use all the bits of the stored mantissas of x, y.
        // Using q*y in Float arithmetic produces a mismatch on the order of 0.0002 due to
        // low-effective_bits propagation (a bug in the old implementation).
        Float r = remainder(x, y);
        // q = (x - r) / y is conceptually an integer (the result of round-half-even)
        Float q = roundEven((x - r) / y);

        Int qi = q.toInt();
        bool q_neg = qi.isNegative();
        if (q_neg) qi = -qi;
        int quo = 0;
        if (qi.getBit(0)) quo |= 1;
        if (qi.getBit(1)) quo |= 2;
        if (qi.getBit(2)) quo |= 4;
        if (q_neg) quo = -quo;

        return {std::move(r), quo};
    }

    std::pair<Float, int> remquo(Float&& x, Float&& y) {
        if (x.isNaN() || y.isNaN()) return {Float::nan(), 0};
        if (x.isInfinity()) return {Float::nan(), 0};
        if (y.isZero()) return {Float::nan(), 0};
        if (x.isZero()) return {Float(0), 0};
        if (y.isInfinity()) return {std::move(x), 0};

        // Same reason as the const& version (to agree with the Int-based remainder).
        Float r = remainder(static_cast<const Float&>(x), static_cast<const Float&>(y));
        Float q = roundEven((std::move(x) - r) / std::move(y));

        Int qi = q.toInt();
        bool q_neg = qi.isNegative();
        if (q_neg) qi = -qi;
        int quo = 0;
        if (qi.getBit(0)) quo |= 1;
        if (qi.getBit(1)) quo |= 2;
        if (qi.getBit(2)) quo |= 4;
        if (q_neg) quo = -quo;

        return {std::move(r), quo};
    }

    //=============================================================================
    // hypot
    //=============================================================================

    Float hypot(const Float& x, const Float& y, int precision) {
        // hypot(x, y) = sqrt(x^2 + y^2)
        if (x.isNaN() || y.isNaN()) {
            // hypot(±∞, NaN) = +∞ (IEEE 754)
            if (x.isInfinity() || y.isInfinity()) return Float::positiveInfinity();
            return Float::nan();
        }
        if (x.isInfinity() || y.isInfinity()) return Float::positiveInfinity();
        if (x.isZero() && y.isZero()) return Float(0);
        if (x.isZero()) return abs(y);
        if (y.isZero()) return abs(x);

        int eff = std::min(x.effectiveBits(), y.effectiveBits());
        int wp = precision + 10;
        Float X = abs(x);
        X.truncateToApprox(wp);
        Float Y = abs(y);
        Y.truncateToApprox(wp);

        Float sum = X * X + Y * Y;
        sum.truncateToApprox(wp);
        Float result = sqrt(sum, wp);
        finalizeResult(result, eff, precision);
        return result;
    }

    Float hypot(Float&& x, Float&& y, int precision) {
        // hypot(x, y) = sqrt(x^2 + y^2)
        if (x.isNaN() || y.isNaN()) {
            // hypot(+/-inf, NaN) = +inf (IEEE 754)
            if (x.isInfinity() || y.isInfinity()) return Float::positiveInfinity();
            return Float::nan();
        }
        if (x.isInfinity() || y.isInfinity()) return Float::positiveInfinity();
        if (x.isZero() && y.isZero()) return Float(0);
        if (x.isZero()) return abs(std::move(y));
        if (y.isZero()) return abs(std::move(x));

        int eff = std::min(x.effectiveBits(), y.effectiveBits());
        int wp = precision + 10;
        Float X = abs(std::move(x));
        X.truncateToApprox(wp);
        Float Y = abs(std::move(y));
        Y.truncateToApprox(wp);

        Float sum = X * X + Y * Y;
        sum.truncateToApprox(wp);
        Float result = sqrt(std::move(sum), wp);
        finalizeResult(result, eff, precision);
        return result;
    }


    //=============================================================================
    // cbrt / nthRoot / recSqrt
    //=============================================================================

    // MPFR-compatible: compute a^(1/n) via an integer n-th root + exponent scaling.
    //
    //   a = m × 2^e   (m: positive integer mantissa, e: exponent; isNegative handled by the caller)
    //   a^(1/n) = m^(1/n) × 2^(e/n)
    //
    // Advantage of the integer route: the 8-10 iterations of Float Newton (precision-doubling) are unnecessary.
    // Since only one call to IntSqrt::nthRoot (integer PD Newton) is needed,
    // the overhead of Float operations (truncateToApprox / Float construction) disappears.
    //
    // Procedure:
    //   1. Absorb s = e mod n into m to make e a multiple of n (s ∈ [0, n-1])
    //   2. Enlarge with M = m × 2^(s + shift_bits) so the bit length is at least n*target_bits
    //      (shift_bits rounded up to a multiple of n)
    //   3. c = floor(M^(1/n))     ← IntSqrt::nthRoot
    //   4. result = c × 2^((e - s - shift_bits) / n)
    //
    // Note: the floor truncation error is covered by guard bits (>= 5).
    static Float nthRoot_via_int(const Float& a, int n, int eff_x, int precision) {
        int wp = precision + 5;  // guard digits
        int target_bits = Float::precisionToBits(wp);

        const Int& m = a.mantissa();
        int64_t e = a.exponent();
        int m_bits = static_cast<int>(m.bitLength());

        int s = static_cast<int>(((e % n) + n) % n);
        int desired_M_bits = n * target_bits;
        int m1_bits = m_bits + s;
        int shift_bits = std::max(0, desired_M_bits - m1_bits);
        shift_bits = ((shift_bits + n - 1) / n) * n;

        int total_shift = s + shift_bits;

        Int M = m << total_shift;
        Int c = IntSqrt::nthRoot(M, static_cast<uint32_t>(n));
        int64_t result_exp = (e - static_cast<int64_t>(total_shift)) / n;
        Float result(std::move(c), result_exp, false);
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float cbrt(const Float& x, int precision) {
        // Special values
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return x;
        if (x.isZero()) return Float(0);

        int eff_x = x.effectiveBits();
        bool negative = x.isNegative();
        Float abs_x = abs(x);

        Float result = nthRoot_via_int(abs_x, 3, eff_x, precision);
        if (negative) result = -result;
        return result;
    }

    Float cbrt(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return std::move(x);
        if (x.isZero()) return Float(0);

        int eff_x = x.effectiveBits();
        bool negative = x.isNegative();
        Float abs_x = abs(std::move(x));

        Float result = nthRoot_via_int(abs_x, 3, eff_x, precision);
        if (negative) result = -result;
        return result;
    }


    Float nthRoot(const Float& x, int n, int precision) {
        if (n <= 0) return Float::nan();
        if (n == 1) return x;
        if (n == 2) return sqrt(x, precision);

        // Special values
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) {
                return (n % 2 == 1) ? Float::negativeInfinity() : Float::nan();
            }
            return Float::positiveInfinity();
        }
        if (x.isZero()) return Float(0);

        // Negative number: only odd roots are valid
        if (x.isNegative()) {
            if (n % 2 == 0) return Float::nan();
            Float abs_result = nthRoot(abs(x), n, precision);
            return -abs_result;
        }

        // Integer n-th root route (MPFR-compatible)
        return nthRoot_via_int(x, n, x.effectiveBits(), precision);
    }

    Float nthRoot(Float&& x, int n, int precision) {
        if (n <= 0) return Float::nan();
        if (n == 1) return std::move(x);
        if (n == 2) return sqrt(std::move(x), precision);

        // Special values
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) {
                return (n % 2 == 1) ? Float::negativeInfinity() : Float::nan();
            }
            return Float::positiveInfinity();
        }
        if (x.isZero()) return Float(0);

        // Negative number: only odd roots are valid
        if (x.isNegative()) {
            if (n % 2 == 0) return Float::nan();
            Float abs_result = nthRoot(abs(x), n, precision);
            return -abs_result;
        }

        // Integer n-th root route (MPFR-compatible)
        return nthRoot_via_int(x, n, x.effectiveBits(), precision);
    }


    Float recSqrt(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float::positiveInfinity();
        if (x.isNegative()) return Float::nan();
        if (x.isInfinity()) return Float(0);

        int eff_x = x.effectiveBits();
        int wp = precision + 10;
        Float X = x;
        X.truncateToApprox(wp);

        Float result = Float::one(wp) / sqrt(X, wp);
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float recSqrt(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float::positiveInfinity();
        if (x.isNegative()) return Float::nan();
        if (x.isInfinity()) return Float(0);

        int eff_x = x.effectiveBits();
        int wp = precision + 10;
        x.truncateToApprox(wp);

        Float result = Float::one(wp) / sqrt(std::move(x), wp);
        finalizeResult(result, eff_x, precision);
        return result;
    }


    //=============================================================================
    // fma / fms
    //=============================================================================

    Float fma(const Float& a, const Float& b, const Float& c, int precision) {
        if (a.isNaN() || b.isNaN() || c.isNaN()) return Float::nan();

        int eff = std::min({a.effectiveBits(), b.effectiveBits(), c.effectiveBits()});
        int wp = precision + 10;
        Float A = a; A.truncateToApprox(wp);
        Float B = b; B.truncateToApprox(wp);
        Float C = c; C.truncateToApprox(wp);

        Float product = A * B;
        product.truncateToApprox(wp);
        Float result = product + C;
        finalizeResult(result, eff, precision);
        return result;
    }

    Float fma(Float&& a, Float&& b, Float&& c, int precision) {
        if (a.isNaN() || b.isNaN() || c.isNaN()) return Float::nan();

        int eff = std::min({a.effectiveBits(), b.effectiveBits(), c.effectiveBits()});
        int wp = precision + 10;
        a.truncateToApprox(wp);
        b.truncateToApprox(wp);
        c.truncateToApprox(wp);

        Float product = std::move(a) * std::move(b);
        product.truncateToApprox(wp);
        Float result = std::move(product) + std::move(c);
        finalizeResult(result, eff, precision);
        return result;
    }


    Float fms(const Float& a, const Float& b, const Float& c, int precision) {
        if (a.isNaN() || b.isNaN() || c.isNaN()) return Float::nan();

        int eff = std::min({a.effectiveBits(), b.effectiveBits(), c.effectiveBits()});
        int wp = precision + 10;
        Float A = a; A.truncateToApprox(wp);
        Float B = b; B.truncateToApprox(wp);
        Float C = c; C.truncateToApprox(wp);

        Float product = A * B;
        product.truncateToApprox(wp);
        Float result = product - C;
        finalizeResult(result, eff, precision);
        return result;
    }

    Float fms(Float&& a, Float&& b, Float&& c, int precision) {
        if (a.isNaN() || b.isNaN() || c.isNaN()) return Float::nan();

        int eff = std::min({a.effectiveBits(), b.effectiveBits(), c.effectiveBits()});
        int wp = precision + 10;
        a.truncateToApprox(wp);
        b.truncateToApprox(wp);
        c.truncateToApprox(wp);

        Float product = std::move(a) * std::move(b);
        product.truncateToApprox(wp);
        Float result = std::move(product) - std::move(c);
        finalizeResult(result, eff, precision);
        return result;
    }


    //=============================================================================
    // Fused multiply-add/subtract of two products (fmma / fmms) — equivalent to MPFR mpfr_fmma / mpfr_fmms
    //=============================================================================

    Float fmma(const Float& a, const Float& b, const Float& c, const Float& d, int precision) {
        if (a.isNaN() || b.isNaN() || c.isNaN() || d.isNaN()) return Float::nan();

        int eff = std::min({a.effectiveBits(), b.effectiveBits(),
                            c.effectiveBits(), d.effectiveBits()});
        int wp = precision + 10;

        // Compute the products at extended precision and round after addition (minimizes intermediate rounding error)
        Float ab = a * b;
        ab.truncateToApprox(wp);
        Float cd = c * d;
        cd.truncateToApprox(wp);
        Float result = ab + cd;
        finalizeResult(result, eff, precision);
        return result;
    }

    Float fmms(const Float& a, const Float& b, const Float& c, const Float& d, int precision) {
        if (a.isNaN() || b.isNaN() || c.isNaN() || d.isNaN()) return Float::nan();

        int eff = std::min({a.effectiveBits(), b.effectiveBits(),
                            c.effectiveBits(), d.effectiveBits()});
        int wp = precision + 10;

        Float ab = a * b;
        ab.truncateToApprox(wp);
        Float cd = c * d;
        cd.truncateToApprox(wp);
        Float result = ab - cd;
        finalizeResult(result, eff, precision);
        return result;
    }

    //=============================================================================
    // sinCos
    //=============================================================================

    // sinCos: at high precision, reduce to a single Taylor pass via cosDoubling + sqrt(1-cos²)
    static void sinCos_core(Float x, int eff_x,
                            Float& sin_result, Float& cos_result, int precision) {
        int compute_prec = effectiveComputePrecision(eff_x, precision);
        // (a)+(b): apply the same guard and dynamic π extension as sin/cos.
        int working_precision = taylorWorkingPrecision(compute_prec);

        bool input_negative = x.isNegative();
        x = abs(x);
        auto reduced = reduceToFirstQuadrant(std::move(x), working_precision);

        int wp_bits = Float::precisionToBits(working_precision);
        Float s, c;

        if (reduced.x.isZero()) {
            s = Float::zero();
            c = Float::one(working_precision);
        } else if (wp_bits >= BITBURST_THRESHOLD) {
            // Bit-burst: simultaneous sin/cos computation, no sqrt needed
            auto [sv, cv] = sincos_bitburst(reduced.x, working_precision);
            s = std::move(sv);
            c = std::move(cv);
        } else if (wp_bits >= 1000) {
            // High precision: cosDoubling + sqrt(1 - cos²) saves one Taylor pass
            int64_t x_msb = reduced.x.exponent()
                          + static_cast<int64_t>(reduced.x.mantissa().bitLength());
            int extra_guard = (x_msb < 0) ? static_cast<int>(-2 * x_msb) : 0;
            int wp_cos = working_precision + Float::bitsToPrecision(extra_guard);

            c = cosDoubling(reduced.x, wp_cos);
            Float one_minus_c2 = Float::one(wp_cos) - c * c;
            s = sqrt(std::move(one_minus_c2), working_precision);
            c.truncateToApprox(working_precision);
        } else {
            // Low precision: sinCosDoubling (overhead dominates)
            auto [sv, cv] = sinCosDoubling(reduced.x, working_precision);
            s = std::move(sv);
            c = std::move(cv);
        }

        if (reduced.sin_negative) s = -s;
        if (reduced.cos_negative) c = -c;
        if (input_negative) s = -s;  // sin(-x) = -sin(x), cos(-x) = cos(x)

        finalizeResult(s, eff_x, precision);
        finalizeResult(c, eff_x, precision);
        sin_result = std::move(s);
        cos_result = std::move(c);
    }

    void sinCos(const Float& x, Float& sin_result, Float& cos_result, int precision) {
        if (x.isNaN()) { sin_result = Float::nan(); cos_result = Float::nan(); return; }
        if (x.isInfinity()) { sin_result = Float::nan(); cos_result = Float::nan(); return; }
        if (x.isZero()) { sin_result = Float::zero(); cos_result = Float::one(precision); return; }
        sinCos_core(Float(x), x.effectiveBits(), sin_result, cos_result, precision);
    }

    void sinCos(Float&& x, Float& sin_result, Float& cos_result, int precision) {
        if (x.isNaN()) { sin_result = Float::nan(); cos_result = Float::nan(); return; }
        if (x.isInfinity()) { sin_result = Float::nan(); cos_result = Float::nan(); return; }
        if (x.isZero()) { sin_result = Float::zero(); cos_result = Float::one(precision); return; }
        int eff = x.effectiveBits();
        sinCos_core(std::move(x), eff, sin_result, cos_result, precision);
    }

    //=============================================================================
    // sinhCosh — simultaneous computation of sinh and cosh
    //=============================================================================

    void sinhCosh(const Float& x, Float& sinh_result, Float& cosh_result, int precision) {
        if (x.isNaN()) { sinh_result = Float::nan(); cosh_result = Float::nan(); return; }
        if (x.isInfinity()) {
            sinh_result = x.isNegative() ? Float::negativeInfinity() : Float::positiveInfinity();
            cosh_result = Float::positiveInfinity();
            return;
        }
        if (x.isZero()) { sinh_result = Float::zero(); cosh_result = Float::one(precision); return; }

        int precision_bits = Float::precisionToBits(precision);
        if (x.mantissa().size() <= 1 && precision_bits <= 64) {
            sinh_result = sinh_small(x, precision);
            cosh_result = cosh_small(x, precision);
            return;
        }

        // Obtain sinh/cosh together in a single computation
        int eff_x = x.effectiveBits();
        int wp = effectiveComputePrecision(eff_x, precision);
        auto [s, c] = sinhCoshCompute(x, wp);
        finalizeResult(s, eff_x, precision);
        finalizeResult(c, eff_x, precision);
        sinh_result = std::move(s);
        cosh_result = std::move(c);
    }

    void sinhCosh(Float&& x, Float& sinh_result, Float& cosh_result, int precision) {
        sinhCosh(static_cast<const Float&>(x), sinh_result, cosh_result, precision);
    }

    //=============================================================================
    // sec / csc / cot — reciprocal trigonometric functions
    //=============================================================================

    Float sec(const Float& x, int precision) {
        // sec(x) = 1 / cos(x)
        if (x.isNaN()) return Float::nan();
        int eff_x = x.effectiveBits();
        Float c = cos(x, precision);
        if (c.isZero()) return Float::nan();  // cos(x)=0 → sec undefined
        Float result = Float(1) / c;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float sec(Float&& x, int precision) {
        // sec(x) = 1 / cos(x)
        if (x.isNaN()) return Float::nan();
        int eff_x = x.effectiveBits();
        Float c = cos(std::move(x), precision);
        if (c.isZero()) return Float::nan();  // cos(x)=0 → sec undefined
        Float result = Float(1) / c;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float csc(const Float& x, int precision) {
        // csc(x) = 1 / sin(x)
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float::nan();  // sin(0)=0 → csc undefined
        int eff_x = x.effectiveBits();
        Float s = sin(x, precision);
        if (s.isZero()) return Float::nan();
        Float result = Float(1) / s;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float csc(Float&& x, int precision) {
        // csc(x) = 1 / sin(x)
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float::nan();  // sin(0)=0 → csc undefined
        int eff_x = x.effectiveBits();
        Float s = sin(std::move(x), precision);
        if (s.isZero()) return Float::nan();
        Float result = Float(1) / s;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float cot(const Float& x, int precision) {
        // cot(x) = cos(x) / sin(x)
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float::nan();  // sin(0)=0 → cot undefined
        int eff_x = x.effectiveBits();
        Float s = sin(x, precision);
        if (s.isZero()) return Float::nan();
        Float result = cos(x, precision) / s;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float cot(Float&& x, int precision) {
        // cot(x) = cos(x) / sin(x)
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float::nan();  // sin(0)=0 → cot undefined
        int eff_x = x.effectiveBits();
        Float s = sin(x, precision);
        if (s.isZero()) return Float::nan();
        Float result = cos(std::move(x), precision) / s;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    //=============================================================================
    // sech / csch / coth — reciprocal hyperbolic functions
    //=============================================================================

    Float sech(const Float& x, int precision) {
        // sech(x) = 1 / cosh(x)
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float(0);  // sech(±∞) = 0
        int eff_x = x.effectiveBits();
        Float c = cosh(x, precision);
        Float result = Float(1) / c;  // cosh(x) >= 1, so the division is always safe
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float sech(Float&& x, int precision) {
        // sech(x) = 1 / cosh(x)
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float(0);  // sech(±∞) = 0
        int eff_x = x.effectiveBits();
        Float c = cosh(std::move(x), precision);
        Float result = Float(1) / c;  // cosh(x) >= 1, so the division is always safe
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float csch(const Float& x, int precision) {
        // csch(x) = 1 / sinh(x)
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float::nan();  // sinh(0)=0 → csch undefined
        if (x.isInfinity()) return Float(0);  // csch(±∞) = 0
        int eff_x = x.effectiveBits();
        Float s = sinh(x, precision);
        Float result = Float(1) / s;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float csch(Float&& x, int precision) {
        // csch(x) = 1 / sinh(x)
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float::nan();  // sinh(0)=0 → csch undefined
        if (x.isInfinity()) return Float(0);  // csch(±∞) = 0
        int eff_x = x.effectiveBits();
        Float s = sinh(std::move(x), precision);
        Float result = Float(1) / s;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float coth(const Float& x, int precision) {
        // coth(x) = cosh(x) / sinh(x)
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float::nan();  // sinh(0)=0 → coth undefined
        if (x.isInfinity()) {
            return x.isNegative() ? Float(-1) : Float(1);
        }
        int eff_x = x.effectiveBits();
        Float s, c;
        sinhCosh(x, s, c, precision);
        Float result = c / s;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float coth(Float&& x, int precision) {
        // coth(x) = cosh(x) / sinh(x)
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float::nan();  // sinh(0)=0 → coth undefined
        if (x.isInfinity()) {
            return x.isNegative() ? Float(-1) : Float(1);
        }
        int eff_x = x.effectiveBits();
        Float s, c;
        sinhCosh(std::move(x), s, c, precision);
        Float result = c / s;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    //=============================================================================
    // factorial — factorial (integer argument)
    //=============================================================================

    Float factorial(int n, int precision) {
        if (n < 0) return Float::nan();
        if (n <= 1) return Float(1);
        // Compute by direct product (for small-to-medium sizes)
        Float result(1);
        for (int i = 2; i <= n; i++) {
            result = result * i;
        }
        result.setResultPrecision(precision);
        return result;
    }

    //=============================================================================
    // sinPi / cosPi / tanPi — π-based trigonometric functions (precision-preserving)
    //=============================================================================
    // sinPi(x) = sin(π·x), cosPi(x) = cos(π·x), tanPi(x) = tan(π·x)
    //
    // How precision is preserved:
    //   1. Remove the sign of x (sin is odd, cos is even)
    //   2. Remove the integer part n of x and obtain the fractional part r = x - n ∈ [0, 1)
    //      → determine the sin/cos sign flip from n mod 2 (sin(π(r+n)) = (-1)^n sin(πr))
    //   3. If r > 0.5, set r = 1 - r to reduce to [0, 0.5]
    //      → for sin, no transformation is needed (sin(π(1-r)) = sin(πr))
    //      → for cos, flip the sign (cos(π(1-r)) = -cos(πr))
    //   4. r = 0 → sin=0, cos=1 (exact); r = 0.5 → sin=1, cos=0 (exact)
    //   5. General case: r ∈ (0, 0.5) → compute sin/cos with πr ∈ (0, π/2)
    //
    // With this method:
    //   - Even for large x, argument reduction is integer-only → no reduction loss
    //   - The multiplication by π is limited to the range |r| ≤ 0.5 → minimal rounding error
    //   - Exact values are guaranteed for integer and half-integer arguments

    // Common argument-reduction result for sinPi/cosPi
    struct PiReduced {
        Float r;           // Fractional part reduced to [0, 0.5]
        bool negate_sin;   // Whether to flip the sign of the sin result
        bool negate_cos;   // Whether to flip the sign of the cos result
        bool r_is_zero;    // r == 0 (integer argument)
        bool r_is_half;    // r == 0.5 (half-integer argument)
    };

    // Reduce x for π-based trigonometric functions (x is a non-negative finite value, non-integer)
    static PiReduced reducePiArg(Float x) {
        // x = n + r, n = floor(x), r ∈ [0, 1)
        Float n_f = floor(x);
        Float r = x - n_f;

        // Get n mod 2 (for sign determination)
        // Even if n_f is huge, only the low bit is needed
        bool n_odd = false;
        if (!n_f.isZero()) {
            Int n_int = n_f.toInt();
            n_odd = n_int.getBit(0);
        }

        // sin(π(r + n)) = (-1)^n · sin(πr)
        // cos(π(r + n)) = (-1)^n · cos(πr)
        bool negate_sin = n_odd;
        bool negate_cos = n_odd;

        // Reduce r to [0, 0.5]
        // Half-integer test: check r == 0.5 exactly
        Float half(Int(1), -1, false);  // 0.5
        bool r_is_half = (r == half);

        bool r_is_zero = r.isZero();

        if (!r_is_half && !r_is_zero && r > half) {
            // r ∈ (0.5, 1): sin(πr) = sin(π(1-r)), cos(πr) = -cos(π(1-r))
            r = Float(1) - r;
            negate_cos = !negate_cos;
        }

        return { std::move(r), negate_sin, negate_cos,
                 r_is_zero, r_is_half };
    }

    // Internal implementation of sinPi (takes x by value)
    static Float sinPi_impl(Float x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::nan();
        if (x.isZero()) return Float(0);

        int eff_x = x.effectiveBits();

        // sin(-πx) = -sin(πx)
        bool input_neg = x.isNegative();
        if (input_neg) x = -std::move(x);

        // If integer, sin(nπ) = 0 (exact)
        if (x.isInteger()) return Float(0);

        auto rd = reducePiArg(std::move(x));

        // r = 0 → sin(πr) = 0
        if (rd.r_is_zero) return Float(0);

        // r = 0.5 → sin(πr) = sin(π/2) = 1
        if (rd.r_is_half) {
            Float result = Float(1);
            if (rd.negate_sin != input_neg) result = -result;
            return result;
        }

        // General case: πr ∈ (0, π/2) → compute sin(πr)
        int wp = precision + 10;
        Float pi_val = Float::pi(wp);
        Float arg = pi_val * rd.r;
        Float result = sin(std::move(arg), precision);

        if (rd.negate_sin != input_neg) result = -result;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    // Internal implementation of cosPi (takes x by value)
    static Float cosPi_impl(Float x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::nan();

        int eff_x = x.effectiveBits();

        // cos(-πx) = cos(πx) → remove the sign
        if (x.isNegative()) x = -std::move(x);

        if (x.isZero()) return Float(1);

        // If integer, cos(nπ) = (-1)^n
        if (x.isInteger()) {
            Int n = x.toInt();
            if (n.getBit(0)) return Float(-1);
            return Float(1);
        }

        auto rd = reducePiArg(std::move(x));

        // r = 0 → cos(πr) = 1
        if (rd.r_is_zero) {
            return rd.negate_cos ? Float(-1) : Float(1);
        }

        // r = 0.5 → cos(πr) = cos(π/2) = 0
        if (rd.r_is_half) return Float(0);

        // General case: πr ∈ (0, π/2) → compute cos(πr)
        int wp = precision + 10;
        Float pi_val = Float::pi(wp);
        Float arg = pi_val * rd.r;
        Float result = cos(std::move(arg), precision);

        if (rd.negate_cos) result = -result;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float sinPi(const Float& x, int precision) {
        return sinPi_impl(Float(x), precision);
    }

    Float sinPi(Float&& x, int precision) {
        return sinPi_impl(std::move(x), precision);
    }

    Float cosPi(const Float& x, int precision) {
        return cosPi_impl(Float(x), precision);
    }

    Float cosPi(Float&& x, int precision) {
        return cosPi_impl(std::move(x), precision);
    }

    Float tanPi(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::nan();
        if (x.isInteger()) return Float(0);

        // tanPi(x) = sinPi(x) / cosPi(x)
        int wp = precision + 10;
        Float s = sinPi(x, wp);
        Float c = cosPi(x, wp);
        if (c.isZero()) return Float::positiveInfinity();  // half-integer → ±∞
        Float result = s / c;
        finalizeResult(result, x.effectiveBits(), precision);
        return result;
    }

    Float tanPi(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::nan();
        if (x.isInteger()) return Float(0);

        int eff_x = x.effectiveBits();
        int wp = precision + 10;
        Float s = sinPi(Float(x), wp);
        Float c = cosPi(std::move(x), wp);
        if (c.isZero()) return Float::positiveInfinity();
        Float result = s / c;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    //=============================================================================
    // sinu / cosu / tanu — trigonometric functions with arbitrary angle unit (IEEE 754-2019)
    //
    // sinu(x, u) = sin(2πx/u)  full turn = u units
    // Example: u=360 → degrees, u=400 → gradians
    // Internally reduces to sinPi(2x/u) to minimize precision loss.
    //=============================================================================

    // Internal implementation of sinu (takes x by value)
    static Float sinu_impl(Float x, int u, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::nan();
        if (u <= 0) return Float::nan();
        if (x.isZero()) return Float(0);

        // sinu(x, u) = sinPi(2x/u)
        // Reduce by x mod u first, then multiply by 2/u (avoids precision loss for large x)
        Float u_f(u);
        x = fmod(std::move(x), u_f);
        // The fmod result is in the range [-u, u]

        // Exact handling of special values: check whether x_red is a multiple of u/4, u/2, 3u/4
        // Compute 2*x_red/u to delegate to sinPi
        // 2*x/u: x is at most about u, so precision loss is small
        Float arg = ldexp(x, 1) / u_f;
        return sinPi(std::move(arg), precision);
    }

    Float sinu(const Float& x, int u, int precision) {
        return sinu_impl(Float(x), u, precision);
    }

    Float sinu(Float&& x, int u, int precision) {
        return sinu_impl(std::move(x), u, precision);
    }

    // Internal implementation of cosu
    static Float cosu_impl(Float x, int u, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::nan();
        if (u <= 0) return Float::nan();
        if (x.isZero()) return Float(1);

        Float u_f(u);
        x = fmod(std::move(x), u_f);

        Float arg = ldexp(x, 1) / u_f;
        return cosPi(std::move(arg), precision);
    }

    Float cosu(const Float& x, int u, int precision) {
        return cosu_impl(Float(x), u, precision);
    }

    Float cosu(Float&& x, int u, int precision) {
        return cosu_impl(std::move(x), u, precision);
    }

    // Internal implementation of tanu
    static Float tanu_impl(Float x, int u, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::nan();
        if (u <= 0) return Float::nan();
        if (x.isZero()) return Float(0);

        Float u_f(u);
        x = fmod(std::move(x), u_f);

        Float arg = ldexp(x, 1) / u_f;
        return tanPi(std::move(arg), precision);
    }

    Float tanu(const Float& x, int u, int precision) {
        return tanu_impl(Float(x), u, precision);
    }

    Float tanu(Float&& x, int u, int precision) {
        return tanu_impl(std::move(x), u, precision);
    }

    //=============================================================================
    // asinPi / acosPi / atanPi / atan2Pi — inverse trigonometric functions (in units of π)
    //=============================================================================

    Float asinPi(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        // Special values: asinPi(±1) = ±0.5 (exact)
        if (!x.isZero()) {
            double xd = x.toDouble();
            if (xd == 1.0)  return Float(1, -1, false);  // 0.5
            if (xd == -1.0) return Float(1, -1, true);   // -0.5
        }
        if (x.isZero()) return Float::zero();

        int wp = precision + 10;
        Float result = asin(x, wp) / Float::pi(wp);
        finalizeResult(result, x.effectiveBits(), precision);
        return result;
    }

    Float asinPi(Float&& x, int precision) {
        return asinPi(static_cast<const Float&>(x), precision);
    }

    Float acosPi(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        // Special values: acosPi(1)=0, acosPi(0)=0.5, acosPi(-1)=1 (exact)
        if (!x.isZero()) {
            double xd = x.toDouble();
            if (xd == 1.0)  return Float::zero();
            if (xd == -1.0) return Float(1);
        }
        if (x.isZero()) return Float(1, -1, false);  // 0.5

        int wp = precision + 10;
        Float result = acos(x, wp) / Float::pi(wp);
        finalizeResult(result, x.effectiveBits(), precision);
        return result;
    }

    Float acosPi(Float&& x, int precision) {
        return acosPi(static_cast<const Float&>(x), precision);
    }

    Float atanPi(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            // atanPi(±∞) = ±0.5
            return x.isNegative() ? Float(1, -1, true) : Float(1, -1, false);
        }
        if (x.isZero()) return Float::zero();
        // Special values: atanPi(±1) = ±0.25
        {
            double xd = x.toDouble();
            if (xd == 1.0)  return Float(1, -2, false);  // 0.25
            if (xd == -1.0) return Float(1, -2, true);   // -0.25
        }

        int wp = precision + 10;
        Float result = atan(x, wp) / Float::pi(wp);
        finalizeResult(result, x.effectiveBits(), precision);
        return result;
    }

    Float atanPi(Float&& x, int precision) {
        return atanPi(static_cast<const Float&>(x), precision);
    }

    Float atan2Pi(const Float& y, const Float& x, int precision) {
        if (y.isNaN() || x.isNaN()) return Float::nan();
        // Special values
        if (y.isZero() && !x.isZero()) {
            if (x.isNegative()) return Float(1);  // atan2Pi(0, -x) = 1
            return Float::zero();                  // atan2Pi(0, +x) = 0
        }
        if (!y.isZero() && x.isZero()) {
            // atan2Pi(±y, 0) = ±0.5
            return y.isNegative() ? Float(1, -1, true) : Float(1, -1, false);
        }

        int eff = std::min(y.effectiveBits(), x.effectiveBits());
        int wp = precision + 10;
        Float result = atan2(y, x, wp) / Float::pi(wp);
        finalizeResult(result, eff, precision);
        return result;
    }

    Float atan2Pi(Float&& y, Float&& x, int precision) {
        return atan2Pi(static_cast<const Float&>(y), static_cast<const Float&>(x), precision);
    }

    //=============================================================================
    // nextAbove / nextBelow — next/previous representable value
    //=============================================================================

    Float nextAbove(const Float& x) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) {
                // nextAbove(-∞) = most negative finite
                // Practically unrepresentable, so return it as is
                return x;
            }
            return x;  // nextAbove(+∞) = +∞
        }
        if (x.isZero()) {
            // nextAbove(0) = smallest positive: mantissa=1, exponent=-precision
            int prec = x.precision() > 0 ? x.precision() : 53;
            return Float(Int(1), -static_cast<int64_t>(prec), false);
        }

        // Positive: increment mantissa by 1
        // Negative: decrement mantissa by 1 (toward smaller magnitude)
        if (!x.isNegative()) {
            Float result(x.mantissa() + 1, x.exponent(), false);
            result.setResultPrecision(x.precision());
            return result;
        } else {
            Int m = x.mantissa();
            if (m.isOne()) {
                // -1 * 2^e → toward 0 (no special handling: mantissa-1=0 → zero)
                return Float(0);
            }
            Float result(m - 1, x.exponent(), true);
            result.setResultPrecision(x.precision());
            return result;
        }
    }

    Float nextAbove(Float&& x) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return std::move(x);
        if (x.isZero()) {
            int prec = x.precision() > 0 ? x.precision() : 53;
            return Float(Int(1), -static_cast<int64_t>(prec), false);
        }

        int prec = x.precision();
        int64_t exp = x.exponent();
        if (!x.isNegative()) {
            Float result(x.mantissa() + 1, exp, false);
            result.setResultPrecision(prec);
            return result;
        } else {
            Int m = x.mantissa();
            if (m.isOne()) return Float(0);
            Float result(m - 1, exp, true);
            result.setResultPrecision(prec);
            return result;
        }
    }

    Float nextBelow(const Float& x) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (!x.isNegative()) {
                return x;  // nextBelow(+∞) = +∞ (representation limit)
            }
            return x;  // nextBelow(-∞) = -∞
        }
        if (x.isZero()) {
            // nextBelow(0) = smallest negative
            int prec = x.precision() > 0 ? x.precision() : 53;
            return Float(Int(1), -static_cast<int64_t>(prec), true);
        }

        // Equivalent to nextBelow = -nextAbove(-x)
        if (x.isNegative()) {
            // Negative: increase the magnitude
            Float result(x.mantissa() + 1, x.exponent(), true);
            result.setResultPrecision(x.precision());
            return result;
        } else {
            Int m = x.mantissa();
            if (m.isOne()) {
                return Float(0);
            }
            Float result(m - 1, x.exponent(), false);
            result.setResultPrecision(x.precision());
            return result;
        }
    }

    Float nextBelow(Float&& x) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return std::move(x);
        if (x.isZero()) {
            int prec = x.precision() > 0 ? x.precision() : 53;
            return Float(Int(1), -static_cast<int64_t>(prec), true);
        }

        int prec = x.precision();
        int64_t exp = x.exponent();
        if (x.isNegative()) {
            Float result(x.mantissa() + 1, exp, true);
            result.setResultPrecision(prec);
            return result;
        } else {
            Int m = x.mantissa();
            if (m.isOne()) return Float(0);
            Float result(m - 1, exp, false);
            result.setResultPrecision(prec);
            return result;
        }
    }

    //=============================================================================
    // AGM — Arithmetic-Geometric Mean
    //=============================================================================
    // agm(a, b) = lim_{n→∞} a_n = lim_{n→∞} b_n
    //   a_{n+1} = (a_n + b_n) / 2
    //   b_{n+1} = √(a_n · b_n)
    // Quadratic convergence: the number of significant digits doubles per iteration

    static Float agm_core(Float a_val, Float b_val, int eff, int precision) {
        int wp = precision + 10;
        int wp_bits = static_cast<int>(std::ceil(wp * 3.32192809488736));
        a_val.truncateToApprox(wp);
        b_val.truncateToApprox(wp);

        // Iteration count: 2 phases, "warm-up" of the initial ratio + quadratic convergence.
        // For AGM(1, ε) with ε = 2^{-N}, warm-up ≈ log2(N), convergence ≈ log2(p).
        // To be safe, 2·log2(wp_bits) + 20 suffices.
        int max_iter = static_cast<int>(std::log2(wp_bits)) * 2 + 20;

        for (int i = 0; i < max_iter; i++) {
            Float a_new = ldexp(a_val + b_val, -1);
            a_new.setResultPrecision(wp);
            Float b_new = sqrt(a_val * b_val, wp);

            // Update the values before the convergence check (secure the latest values before break)
            a_val = std::move(a_new);
            b_val = std::move(b_new);

            // Convergence check: |a - b| is sufficiently small
            Float diff = a_val - b_val;
            if (diff.isZero()) break;

            int64_t diff_exp = diff.exponent() + static_cast<int64_t>(diff.mantissa().bitLength());
            int64_t val_exp = a_val.exponent() + static_cast<int64_t>(a_val.mantissa().bitLength());
            if (val_exp - diff_exp > wp_bits) break;
        }

        finalizeResult(a_val, eff, precision);
        return a_val;
    }

    Float agm(const Float& a, const Float& b, int precision) {
        if (a.isNaN() || b.isNaN()) return Float::nan();
        if (a.isZero() || b.isZero()) return Float(0);  // agm(0, x) = 0
        if (a.isInfinity() || b.isInfinity()) return Float::nan();

        // Both must be positive
        if (a.isNegative() || b.isNegative()) return Float::nan();

        int eff = std::min(a.effectiveBits(), b.effectiveBits());
        return agm_core(Float(a), Float(b), eff, precision);
    }

    Float agm(Float&& a, Float&& b, int precision) {
        if (a.isNaN() || b.isNaN()) return Float::nan();
        if (a.isZero() || b.isZero()) return Float(0);  // agm(0, x) = 0
        if (a.isInfinity() || b.isInfinity()) return Float::nan();

        // Both must be positive
        if (a.isNegative() || b.isNegative()) return Float::nan();

        int eff = std::min(a.effectiveBits(), b.effectiveBits());
        return agm_core(std::move(a), std::move(b), eff, precision);
    }

    //=============================================================================
    // sum — high-precision summation
    //=============================================================================

    Float sum(std::span<const Float> values, int precision) {
        if (values.empty()) return Float(0);

        // Simple sequential addition (little cancellation concern thanks to multiple precision)
        Float result = values[0];
        for (size_t i = 1; i < values.size(); i++) {
            result = result + values[i];
        }
        result.setResultPrecision(precision);
        return result;
    }

    //=============================================================================
    // dot — dot product
    //=============================================================================

    Float dot(std::span<const Float> a, std::span<const Float> b, int precision) {
        size_t n = std::min(a.size(), b.size());
        if (n == 0) return Float(0);

        Float result = a[0] * b[0];
        for (size_t i = 1; i < n; i++) {
            result = result + a[i] * b[i];
        }
        result.setResultPrecision(precision);
        return result;
    }

    //=============================================================================
    // erf / erfc — error functions
    //=============================================================================
    // erf(x) = (2/√π) · Σ_{n=0}^{∞} (-1)^n · x^{2n+1} / (n! · (2n+1))
    // erfc(x) = 1 - erf(x)

    static Float erf_core(Float x, int eff_x, int precision) {
        int wp = precision + 20;
        const int wp_bits = Float::precisionToBits(wp);   // The convergence check is done in bits

        // erf(x) = (2/√π) · Σ_{n=0}^{∞} (-1)^n · x^{2n+1} / (n! · (2n+1))
        //
        // BUGFIX (2026-05-30): for integer / simple exact inputs (e.g. erf(5)), x propagated
        // as exact (effective_bits_ = INT_MAX), so the in-loop
        // FloatOps::div(term, Float(n)) was judged "exact / exact" and the
        // result was rounded to default_precision (53 digits), capping the whole
        // erf at ~54 digits. Prevent this by making it non-exact at working precision.
        Float x2 = x * x;
        x2.truncateToApprox(wp);
        x2.setEffectiveBits(wp_bits);
        Float neg_x2 = -x2;
        neg_x2.setEffectiveBits(wp_bits);
        Float term = x;  // First term: x
        term.truncateToApprox(wp);
        term.setEffectiveBits(wp_bits);
        Float sum = term;
        sum.setEffectiveBits(wp_bits);
        Float contribution(0);

        for (int n = 1; n < 4 * wp; n++) {
            // term_{n} = term_{n-1} · (-x²) / n
            FloatOps::mul(term, neg_x2, term);
            FloatOps::div(term, Float(n), term);
            FloatOps::div(term, Float(2 * n + 1), contribution);

            FloatOps::add(sum, contribution, sum);

            // Convergence check: stop if contribution is ≤ 2^{-wp_bits} relative to sum
            // (the old code compared directly against wp (decimal digits), a bug that accumulated
            //  only ~wp/3.32 bits ≈ wp digits. erf(0.5,prec=200) was measured to stop at 69 digits)
            if (contribution.isZero()) break;
            int64_t c_bits = contribution.exponent() + static_cast<int64_t>(contribution.mantissa().bitLength());
            int64_t s_bits = sum.exponent() + static_cast<int64_t>(sum.mantissa().bitLength());
            if (s_bits - c_bits > wp_bits) break;
        }

        // (2/√π) · sum
        Float sqrtpi = sqrt(Float::pi(wp), wp);
        Float result = ldexp(sum, 1) / sqrtpi;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float erf(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);
        if (x.isInfinity()) {
            return x.isNegative() ? Float(-1) : Float(1);
        }
        return erf_core(Float(x), x.effectiveBits(), precision);
    }

    Float erf(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);
        if (x.isInfinity()) {
            return x.isNegative() ? Float(-1) : Float(1);
        }
        int eff = x.effectiveBits();
        return erf_core(std::move(x), eff, precision);
    }

    // erfc(x) = 1 - erf(x)
    //
    // For large positive arguments, erf(x) → 1 exponentially, so 1 - erf(x) incurs
    // catastrophic cancellation of x²·log10(e) digits (e.g. erfc(5)≈1.5e-12
    // loses ~12 digits, erfc(10)≈2e-45 loses ~45 digits).
    //  - x > 5: cancellation-free erfc(x) = erfcx(x)·exp(-x²) route
    //           (erfcx uses a continued-fraction expansion for x>5, no cancellation).
    //  - |x| ≤ 5 / negative argument: 1 - erf. The slight cancellation (at most ~11 digits for x≤5)
    //           is compensated by internal precision.
    // The threshold 5 matches the erfcx side (which calls 1-erf for x≤5), preventing mutual recursion.
    static Float erfc_dispatch(const Float& x, int eff_x, int precision) {
        double xd = x.toDouble();
        if (xd > 5.0) {
            int wp = precision + 10;
            Float x2 = x * x;
            x2.truncateToApprox(wp);
            Float exp_neg_x2 = exp(-x2, wp);
            Float ecx = erfcx(x, wp);
            Float result = ecx * exp_neg_x2;
            finalizeResult(result, eff_x, precision);
            return result;
        }
        int cancel = (xd > 0.0)
            ? static_cast<int>(std::ceil(xd * xd * 0.4342944819032518)) : 0;
        int internal_prec = precision + cancel + 10;
        Float result = Float(1) - erf(x, internal_prec);
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float erfc(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(1);
        if (x.isInfinity()) {
            return x.isNegative() ? Float(2) : Float(0);
        }
        return erfc_dispatch(x, x.effectiveBits(), precision);
    }

    Float erfc(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(1);
        if (x.isInfinity()) {
            return x.isNegative() ? Float(2) : Float(0);
        }
        return erfc_dispatch(x, x.effectiveBits(), precision);
    }

    // erfcx(x) — scaled complementary error function exp(x²)·erfc(x)
    // x ≤ 5: direct computation exp(x²)·erfc(x) (erfc is 1-erf + cancellation guard)
    // x > 5: continued-fraction expansion (Modified Lentz)
    //   √π · erfcx(x) = 1/(x + 1/2/(x + 2/2/(x + 3/2/(x + ...))))
    // The continued fraction converges more slowly the smaller x is (~2300 terms needed at x=2.5),
    // and does not reach wp bits within the working precision, so set the threshold to 5 and
    // delegate moderate x to the direct route (erf already fixed). Align the erfc-side dispatch
    // threshold to 5 as well, so that erfc(x>5)→erfcx(CF), erfc(x≤5)→1-erf, erfcx(x≤5)→erfc→1-erf
    // breaks the mutual recursion.
    static Float erfcx_core(Float x, int eff_x, int precision) {
        int wp = precision + 20;

        // x < 0: erfcx(x) = 2·exp(x²) - erfcx(-x)
        if (x.isNegative()) {
            Float neg_x = -x;
            Float erfcx_pos = erfcx(neg_x, wp);
            Float x2 = x * x;
            x2.truncateToApprox(wp);
            Float exp_x2 = exp(std::move(x2), wp);
            Float result = ldexp(exp_x2, 1) - erfcx_pos;
            finalizeResult(result, eff_x, precision);
            return result;
        }

        double x_approx = x.toDouble();

        // x ≤ 5: direct computation exp(x²)·erfc(x)
        // erfc(x) = 1 - erf(x) incurs cancellation of x²/ln(2) bits → add guard bits
        if (x_approx <= 5.0) {
            int extra = static_cast<int>(std::ceil(x_approx * x_approx / std::log(2.0))) + 10;
            int wp2 = wp + extra;
            Float erfc_val = erfc(x, wp2);
            Float x2 = x * x;
            x2.truncateToApprox(wp);
            Float exp_x2 = exp(std::move(x2), wp);
            Float result = exp_x2 * erfc_val;
            finalizeResult(result, eff_x, precision);
            return result;
        }

        // x > 2: continued-fraction expansion (Modified Lentz)
        // g = x + a₁/(x + a₂/(x + ...)), a_n = n/2
        // erfcx(x) = 1/(√π · g)
        //
        // BUGFIX (2026-05-30): for integer inputs (e.g. erfcx(5)), x propagated as exact,
        // so Float(1)/x and Float(n)/two were judged "exact / exact" and rounded to
        // default_precision (53 digits), capping erfcx at ~66 digits.
        // (1) Make x non-exact at working precision, (2) compute a_n = n/2 as
        // ldexp(Float(n), -1) to keep it exact rather than via division.
        const int wp_bits = Float::precisionToBits(wp);
        x.truncateToApprox(wp);
        x.setEffectiveBits(wp_bits);

        Float f = x;
        Float C = f;
        Float D(0);

        for (int n = 1; n < 10 * wp; n++) {
            // a_n = n/2 (keep exact via ldexp, avoiding the exact/exact division fallback)
            Float a_n = ldexp(Float(n), -1);

            D = x + a_n * D;
            D.truncateToApprox(wp);
            D = Float(1) / D;

            C = x + a_n / C;
            C.truncateToApprox(wp);

            Float delta = C * D;
            f = f * delta;
            f.truncateToApprox(wp);

            // Convergence check: |δ - 1| < 2^{-(wp_bits+5)}
            // (BUGFIX 2026-05-30: the old code mixed units by using wp (decimal digits) as a
            //  bit exponent, truncating at 2^-(wp+5)≈10^{-wp/3.3} and producing only ~wp/3.3 digits.
            //  Same kind as erf's efe6db2c.)
            Float diff = delta - Float(1);
            if (diff.isZero()) break;
            int64_t diff_bits = diff.exponent()
                + static_cast<int64_t>(diff.mantissa().bitLength());
            if (diff_bits < -(static_cast<int64_t>(wp_bits) + 5)) break;
        }

        Float sqrtpi = sqrt(Float::pi(wp), wp);
        Float result = Float(1) / (sqrtpi * f);
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float erfcx(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(1);
        if (x.isInfinity()) {
            if (x.isNegative()) return Float::positiveInfinity();
            return Float(0);
        }
        return erfcx_core(Float(x), x.effectiveBits(), precision);
    }

    Float erfcx(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(1);
        if (x.isInfinity()) {
            if (x.isNegative()) return Float::positiveInfinity();
            return Float(0);
        }
        int eff = x.effectiveBits();
        return erfcx_core(std::move(x), eff, precision);
    }

    //=============================================================================
    // Gamma function and related functions
    //=============================================================================

    // Compute the Bernoulli numbers B_{2k} via the Akiyama-Tanigawa algorithm
    // k=1..max_k → B_2, B_4, ..., B_{2*max_k}  (result[k]=B_{2k}, result[0]=B_0=1)
    // Non-static: declared in Float.hpp since it is also reused from Float.cpp (e.g. the Glaisher constant).
    std::vector<Float> computeBernoulliNumbers(int max_k, int precision) {
        int n = 2 * max_k + 1;
        std::vector<Float> a(n + 1, Float(0));
        std::vector<Float> result(max_k + 1, Float(0));  // result[k] = B_{2k}
        result[0] = Float(1);  // B_0 = 1

        // BUGFIX (2026-05-30): computing 1/(m+1) as Float(1)/Float(m+1) gave
        // exact/exact → rounded to default_precision (53 digits), and the lost digits could not be
        // recovered by later setResultPrecision padding, so all Bernoulli numbers
        // stopped at 53 digits (the true cause of gamma/lnGamma/digamma/trigamma capping at ~58 digits).
        // Make the numerator non-exact so the division itself is computed at precision.
        Float one_wp(1);
        one_wp.setResultPrecision(precision);
        for (int m = 0; m <= n; m++) {
            a[m] = one_wp / Float(m + 1);
            for (int j = m; j >= 1; j--) {
                a[j - 1] = Float(j) * (a[j - 1] - a[j]);
                a[j - 1].setResultPrecision(precision);
            }
            // a[0] = B_m
            if (m >= 2 && m % 2 == 0) {
                result[m / 2] = a[0];
            }
        }
        return result;
    }

    // Bernoulli number cache: reused for the same thread and same (max_k, precision)
    // (codex consult 2026-05-06: recomputing the gamma family every time was a major bottleneck)
    static const std::vector<Float>& getBernoulliCached(int max_k, int precision) {
        thread_local std::vector<Float> cache;
        thread_local int cached_max_k = -1;
        thread_local int cached_precision = -1;

        if (cached_max_k >= max_k && cached_precision >= precision) {
            return cache;
        }
        // cache miss: recompute (full recompute if either max_k or precision is insufficient)
        cache = computeBernoulliNumbers(max_k, precision);
        cached_max_k = max_k;
        cached_precision = precision;
        return cache;
    }

    // lnGamma(x) — log-gamma function via the Stirling series
    // x > 0 only (negative arguments use the reflection formula in gamma())
    static Float lnGamma_core(Float x, int eff_x, int precision) {
        int wp = precision + 20;
        const int wp_bits = Float::precisionToBits(wp);  // Convergence check in bits

        // For positive integers: lnGamma(n) = ln((n-1)!)
        if (x.isInteger()) {
            // Small-integer optimization
            int n = static_cast<int>(x.toDouble());
            if (n <= 20 && n >= 1) {
                Float fact(1);
                for (int i = 2; i < n; i++) {
                    fact = fact * i;
                }
                if (n <= 2) return Float(0);  // lnGamma(1)=lnGamma(2)=0
                return log(fact, precision);
            }
        }

        // Stirling series: lnΓ(z) = (z-1/2)·ln(z) - z + ln(2π)/2 + Σ B_{2k}/(2k·(2k-1)·z^{2k-1})
        // Argument shift: when z is small, use Γ(z) = Γ(z+m)/[z·(z+1)·...·(z+m-1)]
        // → lnΓ(z) = lnΓ(z+m) - Σ ln(z+i)

        // Make z large enough (the minimum residual of the Stirling asymptotic series must fall below the requested precision).
        //   The minimum residual of the Stirling series ~ exp(-2π·z); for this to be below 10^{-wp_decimal},
        //   z > wp_decimal · ln(10)/(2π) ≈ wp_decimal · 0.367
        //   wp=220 (200d) → 81, wp=5020 (5000d) → 1843
        // The old code used z > wp/8 = wp · 0.125, a unit misapplication of a formula assuming "wp is bits".
        // As a result, at prec=200 z=27, the minimum residual e^{-2π·27} ≈ 10^{-74},
        // a fatal bug that capped at ~74 digits for any high-precision request (measured 55 digits).
        // After the fix the cost is ~3× (m is ~3×) but the accuracy meets the requested precision.
        Float z = std::move(x);
        z.truncateToApprox(wp);
        // wp · ln(10) / (2π) ≈ 0.367 is the theoretical minimum. Strengthen the margin to 0.45 (~10-15 extra digits)
        // to absorb the residual ~ √(2πz) e^{-2π z} + accumulation in the shift stages + later cancellation.
        const double shift_target = std::max(20.0, wp * 0.45 + 8.0);
        int m = 0;
        Float prod_log(0);  // The value of ln(Π (z+i))

        double z_approx = z.toDouble();
        if (z_approx < shift_target) {
            m = static_cast<int>(shift_target - z_approx) + 1;
            // Build Π_{i=0}^{m-1} (z + i) and then apply log only once
            // (old: m log calls were a major bottleneck)
            // Truncate the intermediate prod to wp digits each time (the relative error m·2^{-wp} is negligible)
            Float prod(1);
            prod.setResultPrecision(wp);
            for (int i = 0; i < m; i++) {
                Float zi = z + Float(i);
                zi.truncateToApprox(wp);
                prod = prod * zi;
                prod.truncateToApprox(wp);
            }
            prod_log = log(prod, wp);
            z = z + Float(m);
            z.truncateToApprox(wp);
        }

        // BUGFIX (2026-05-30): for integer / simple exact inputs (e.g. gamma(100)→z=108,
        // gamma(0.5)), z propagated as exact (effective_bits_ = INT_MAX),
        // so the later Float(1)/z was judged "exact / exact" and rounded to default_precision
        // (53 digits), capping the whole lnGamma/gamma at ~58 digits
        // (same family as erf/erfcx). Prevent this by making it non-exact at working precision.
        z.setEffectiveBits(wp_bits);

        // Optimal truncation of the Stirling asymptotic series k* ≈ π·z (beyond this it diverges as an asymptotic series).
        // At this k the residual reaches e^{-2π·z} and is minimized. Truncating earlier inflates the residual, so
        // together with shift_target being large enough, k = π·z + safety must be taken.
        // (the old wp/(2·log2(z)) assumed a geometric series + misapplied a bit-based formula, taking only num_terms=22
        //  even at prec=200, a bug that capped at ~72 digits.)
        constexpr double PI_VAL = 3.141592653589793238462643383279502884;
        int num_terms = static_cast<int>(PI_VAL * z.toDouble()) + 10;
        if (num_terms < 5) num_terms = 5;
        // The cap scales with precision (at precision=5000, z≈1850, num_terms≈5817)
        int num_terms_cap = static_cast<int>(PI_VAL * shift_target * 1.2 + 100.0);
        if (num_terms_cap < 2000) num_terms_cap = 2000;
        if (num_terms > num_terms_cap) num_terms = num_terms_cap;

        const auto& bernoulli = getBernoulliCached(num_terms, wp);

        // Main computation
        Float ln_z = log(z, wp);
        Float half = ldexp(Float(1), -1);

        // (z - 1/2) · ln(z) - z
        Float result = (z - half) * ln_z - z;

        // + ln(2π)/2
        Float ln2pi = log(ldexp(Float::pi(wp), 1), wp);
        result = result + ln2pi * half;

        // + Stirling correction terms: Σ_{k=1}^{N} B_{2k} / (2k·(2k-1)·z^{2k-1})
        Float z_inv = Float(1) / z;
        z_inv.truncateToApprox(wp);
        Float z_inv2 = z_inv * z_inv;
        z_inv2.truncateToApprox(wp);
        Float z_power = z_inv;  // z^{-1}, z^{-3}, z^{-5}, ...

        // Since Stirling is an asymptotic series, it has a minimum near k = π·z and diverges beyond.
        // Divergence detection: stop just before the term magnitude stops decreasing (do not add that term).
        int64_t prev_term_bits = (std::numeric_limits<int64_t>::max)();
        for (int k = 1; k <= num_terms; k++) {
            Float coeff = bernoulli[k] / static_cast<int64_t>(2 * k * (2 * k - 1));
            Float term = coeff * z_power;
            int64_t curr_term_bits = term.isZero()
                ? (std::numeric_limits<int64_t>::min)()
                : term.exponent() + static_cast<int64_t>(term.mantissa().bitLength());

            // Divergence detection: if the term does not become smaller than the previous one, we have passed the asymptotic series minimum
            if (k >= 3 && curr_term_bits >= prev_term_bits) break;

            result = result + term;
            result.truncateToApprox(wp);
            prev_term_bits = curr_term_bits;

            // Convergence check (compare in bits. The old wp comparison had the same unit-mixing bug)
            if (k >= 3 && term.isZero()) break;
            if (k >= 3) {
                auto result_bits = result.exponent() + static_cast<int64_t>(result.mantissa().bitLength());
                if (result_bits - curr_term_bits > wp_bits + 5) break;
            }

            z_power = z_power * z_inv2;
            z_power.truncateToApprox(wp);
        }

        // Correction for the argument shift
        result = result - prod_log;

        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float lnGamma(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) return Float::nan();
            return Float::positiveInfinity();
        }
        if (x.isZero() || x.isNegative()) {
            return Float::nan();
        }
        return lnGamma_core(Float(x), x.effectiveBits(), precision);
    }

    Float lnGamma(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) return Float::nan();
            return Float::positiveInfinity();
        }
        if (x.isZero() || x.isNegative()) {
            return Float::nan();
        }
        int eff = x.effectiveBits();
        return lnGamma_core(std::move(x), eff, precision);
    }

    // gamma(x) — gamma function Γ(x)
    Float gamma(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) return Float::nan();
            return Float::positiveInfinity();
        }

        // Pole at non-positive integers
        if (x.isZero()) return Float::positiveInfinity();  // Γ(0) = ±∞
        if (x.isNegative() && x.isInteger()) return Float::nan();  // Γ(-n) is undefined

        int eff_x = x.effectiveBits();
        int wp = precision + 15;

        // Small positive integer: Γ(n) = (n-1)!
        if (x.isInteger() && x.isPositive()) {
            int n = static_cast<int>(x.toDouble());
            if (n <= 25) {
                Float result(1);
                for (int i = 2; i < n; i++) {
                    result = result * i;
                }
                finalizeResult(result, eff_x, precision);
                return result;
            }
        }

        // x > 0: Γ(x) = exp(lnΓ(x))
        if (x.isPositive()) {
            Float lng = lnGamma(x, wp);
            Float result = exp(lng, wp);
            finalizeResult(result, eff_x, precision);
            return result;
        }

        // x < 0 (non-integer): reflection formula Γ(x)·Γ(1-x) = π/sin(πx)
        Float one_minus_x = Float(1) - x;
        Float lng = lnGamma(one_minus_x, wp);
        Float gamma_1mx = exp(lng, wp);

        Float sin_pi_x = sinPi(x, wp);

        // Γ(x) = π / (sin(πx) · Γ(1-x))
        Float pi_val = Float::pi(wp);
        Float result = pi_val / (sin_pi_x * gamma_1mx);
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float gamma(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) return Float::nan();
            return Float::positiveInfinity();
        }

        // Pole at non-positive integers
        if (x.isZero()) return Float::positiveInfinity();  // Γ(0) = ±∞
        if (x.isNegative() && x.isInteger()) return Float::nan();  // Γ(-n) is undefined

        int eff_x = x.effectiveBits();
        int wp = precision + 15;

        // Small positive integer: Γ(n) = (n-1)!
        if (x.isInteger() && x.isPositive()) {
            int n = static_cast<int>(x.toDouble());
            if (n <= 25) {
                Float result(1);
                for (int i = 2; i < n; i++) {
                    result = result * i;
                }
                finalizeResult(result, eff_x, precision);
                return result;
            }
        }

        // x > 0: Γ(x) = exp(lnΓ(x))
        if (x.isPositive()) {
            Float lng = lnGamma(std::move(x), wp);
            Float result = exp(std::move(lng), wp);
            finalizeResult(result, eff_x, precision);
            return result;
        }

        // x < 0 (non-integer): reflection formula Γ(x)·Γ(1-x) = π/sin(πx)
        Float one_minus_x = Float(1) - x;
        Float lng = lnGamma(std::move(one_minus_x), wp);
        Float gamma_1mx = exp(std::move(lng), wp);

        Float sin_pi_x = sinPi(std::move(x), wp);

        // Γ(x) = π / (sin(πx) · Γ(1-x))
        Float pi_val = Float::pi(wp);
        Float result = pi_val / (sin_pi_x * gamma_1mx);
        finalizeResult(result, eff_x, precision);
        return result;
    }

    // beta(a, b) — beta function B(a,b) = Γ(a)·Γ(b)/Γ(a+b)
    Float beta(const Float& a, const Float& b, int precision) {
        if (a.isNaN() || b.isNaN()) return Float::nan();

        int eff = std::min(a.effectiveBits(), b.effectiveBits());
        int wp = precision + 15;

        // B(a,b) = exp(lnΓ(a) + lnΓ(b) - lnΓ(a+b))
        // but lnGamma is only usable when a, b are positive
        if (a.isPositive() && b.isPositive()) {
            Float lga = lnGamma(a, wp);
            Float lgb = lnGamma(b, wp);
            Float lgab = lnGamma(a + b, wp);
            Float result = exp(lga + lgb - lgab, wp);
            finalizeResult(result, eff, precision);
            return result;
        }

        // General case: Γ(a)·Γ(b)/Γ(a+b)
        Float ga = gamma(a, wp);
        Float gb = gamma(b, wp);
        Float gab = gamma(a + b, wp);
        if (gab.isZero()) return Float::nan();
        Float result = ga * gb / gab;
        finalizeResult(result, eff, precision);
        return result;
    }

    Float beta(Float&& a, Float&& b, int precision) {
        if (a.isNaN() || b.isNaN()) return Float::nan();

        int eff = std::min(a.effectiveBits(), b.effectiveBits());
        int wp = precision + 15;

        // B(a,b) = exp(lnΓ(a) + lnΓ(b) - lnΓ(a+b))
        if (a.isPositive() && b.isPositive()) {
            Float lga = lnGamma(a, wp);
            Float lgb = lnGamma(b, wp);
            Float lgab = lnGamma(a + b, wp);
            Float result = exp(lga + lgb - lgab, wp);
            finalizeResult(result, eff, precision);
            return result;
        }

        // General case: Γ(a)·Γ(b)/Γ(a+b)
        Float ga = gamma(a, wp);
        Float gb = gamma(b, wp);
        Float gab = gamma(a + std::move(b), wp);
        if (gab.isZero()) return Float::nan();
        Float result = ga * gb / gab;
        finalizeResult(result, eff, precision);
        return result;
    }

    // digamma(x) — digamma function ψ(x) = d/dx ln Γ(x) = Γ'(x)/Γ(x)
    // Asymptotic expansion: ψ(z) ~ ln(z) - 1/(2z) - Σ B_{2k}/(2k·z^{2k})
    static Float digamma_core(Float x, int eff_x, int precision) {
        int wp = precision + 20;
        const int wp_bits = Float::precisionToBits(wp);  // Convergence check in bits

        // Negative argument: reflection formula ψ(1-x) - ψ(x) = π·cot(πx)
        if (x.isNegative()) {
            Float one_minus_x = Float(1) - x;
            Float psi_1mx = digamma(one_minus_x, wp);
            Float pi_val = Float::pi(wp);
            Float cot_pix = cosPi(x, wp) / sinPi(x, wp);
            Float result = psi_1mx - pi_val * cot_pix;
            finalizeResult(result, eff_x, precision);
            return result;
        }

        // Argument shift: ψ(x+1) = ψ(x) + 1/x → ψ(x) = ψ(x+m) - Σ 1/(x+i)
        // shift_target uses the same formula as lnGamma_core (z > wp · 0.367, which guarantees the
        // minimum Stirling-asymptotic residual e^{-2π·z} is below 10^{-wp}). The old wp/8 was a
        // misapplication of the formula assuming wp is in bits, a fatal bug producing only ~52 digits even at prec=200.
        Float z = std::move(x);
        z.truncateToApprox(wp);
        // BUGFIX (2026-05-30): prevent Float(1)/z / Float(1)/zi for exact inputs from
        // falling to exact/exact → default_precision (53 digits)
        // (same family as lnGamma_core). Make non-exact at working precision.
        z.setEffectiveBits(wp_bits);
        // wp · ln(10) / (2π) ≈ 0.367 is the theoretical minimum. Strengthen the margin to 0.45 (~10-15 extra digits)
        // to absorb the residual ~ √(2πz) e^{-2π z} + accumulation in the shift stages + later cancellation.
        const double shift_target = std::max(20.0, wp * 0.45 + 8.0);
        int m = 0;
        Float shift_sum(0);

        double z_approx = z.toDouble();
        if (z_approx < shift_target) {
            m = static_cast<int>(shift_target - z_approx) + 1;
            for (int i = 0; i < m; i++) {
                Float zi = z + Float(i);
                zi.truncateToApprox(wp);
                shift_sum = shift_sum + Float(1) / zi;
            }
            z = z + Float(m);
            z.truncateToApprox(wp);
        }

        // Asymptotic expansion: ψ(z) ~ ln(z) - 1/(2z) - Σ_{k=1}^{N} B_{2k}/(2k·z^{2k})
        // Optimal truncation k* ≈ π·z (same reason as lnGamma_core). The old wp/(2·log2(z)) was the wrong formula.
        constexpr double PI_VAL = 3.141592653589793238462643383279502884;
        int num_terms = static_cast<int>(PI_VAL * z.toDouble()) + 10;
        if (num_terms < 5) num_terms = 5;
        int num_terms_cap = static_cast<int>(PI_VAL * shift_target * 1.2 + 100.0);
        if (num_terms_cap < 2000) num_terms_cap = 2000;
        if (num_terms > num_terms_cap) num_terms = num_terms_cap;

        // Use the Bernoulli cache (the old computeBernoulliNumbers recomputed every time)
        const auto& bernoulli = getBernoulliCached(num_terms, wp);

        Float result = log(z, wp) - ldexp(Float(1) / z, -1);

        Float z_inv = Float(1) / z;
        z_inv.truncateToApprox(wp);
        Float z_inv2 = z_inv * z_inv;
        z_inv2.truncateToApprox(wp);
        Float z_power = z_inv2;  // z^{-2}, z^{-4}, ...

        // Divergence detection: stop once past the minimum of the Stirling asymptotic series (like lnGamma_core)
        int64_t prev_term_bits = (std::numeric_limits<int64_t>::max)();
        for (int k = 1; k <= num_terms; k++) {
            Float coeff = bernoulli[k] / static_cast<int64_t>(2 * k);
            Float term = coeff * z_power;
            int64_t curr_term_bits = term.isZero()
                ? (std::numeric_limits<int64_t>::min)()
                : term.exponent() + static_cast<int64_t>(term.mantissa().bitLength());

            if (k >= 3 && curr_term_bits >= prev_term_bits) break;

            result = result - term;
            result.truncateToApprox(wp);
            prev_term_bits = curr_term_bits;

            if (k >= 3 && term.isZero()) break;
            if (k >= 3) {
                auto result_bits = result.exponent() + static_cast<int64_t>(result.mantissa().bitLength());
                if (result_bits - curr_term_bits > wp_bits + 5) break;
            }

            z_power = z_power * z_inv2;
            z_power.truncateToApprox(wp);
        }

        result = result - shift_sum;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float digamma(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) return Float::nan();
            return Float::positiveInfinity();
        }
        if (x.isZero()) return Float::nan();

        // Non-positive integer: pole
        if (x.isNegative() && x.isInteger()) return Float::nan();

        return digamma_core(Float(x), x.effectiveBits(), precision);
    }

    Float digamma(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) return Float::nan();
            return Float::positiveInfinity();
        }
        if (x.isZero()) return Float::nan();

        // Non-positive integer: pole
        if (x.isNegative() && x.isInteger()) return Float::nan();

        int eff = x.effectiveBits();
        return digamma_core(std::move(x), eff, precision);
    }

    // trigamma(x) — trigamma function ψ₁(x) = d²/dx² ln Γ(x)
    // Asymptotic expansion: ψ₁(z) ~ 1/z + 1/(2z²) + Σ B_{2k}/(z^{2k+1})
    static Float trigamma_core(Float x, int eff_x, int precision) {
        int wp = precision + 20;
        const int wp_bits = Float::precisionToBits(wp);  // Convergence check in bits

        // Negative argument: reflection formula ψ₁(1-x) + ψ₁(x) = π²/sin²(πx)
        if (x.isNegative()) {
            Float one_minus_x = Float(1) - x;
            Float psi1_1mx = trigamma(one_minus_x, wp);
            Float pi_val = Float::pi(wp);
            Float sin_pix = sinPi(x, wp);
            Float pi2_over_sin2 = (pi_val * pi_val) / (sin_pix * sin_pix);
            Float result = pi2_over_sin2 - psi1_1mx;
            finalizeResult(result, eff_x, precision);
            return result;
        }

        // Argument shift: ψ₁(x+1) = ψ₁(x) - 1/x² → ψ₁(x) = ψ₁(x+m) + Σ 1/(x+i)²
        // BUGFIX (2026-05-30): the efe6db2c fix for lnGamma/digamma had not been applied to trigamma.
        // (1) shift_target = wp/8 was a misapplied bit formula, capping at ~74 digits
        // → wp·0.45+8. (2) Float(1)/z etc. for exact inputs fell to default_precision
        // → make z non-exact. (3) num_terms = wp/(2·log2 z) was the wrong formula
        // → π·z + 10 + divergence detection. (4) the convergence check wp+5 mixed bits vs digits → wp_bits+5.
        Float z = std::move(x);
        z.truncateToApprox(wp);
        z.setEffectiveBits(wp_bits);
        const double shift_target = std::max(20.0, wp * 0.45 + 8.0);
        int m = 0;
        Float shift_sum(0);

        double z_approx = z.toDouble();
        if (z_approx < shift_target) {
            m = static_cast<int>(shift_target - z_approx) + 1;
            for (int i = 0; i < m; i++) {
                Float zi = z + Float(i);
                zi.truncateToApprox(wp);
                Float zi_inv = Float(1) / zi;
                shift_sum = shift_sum + zi_inv * zi_inv;
            }
            z = z + Float(m);
            z.truncateToApprox(wp);
        }

        // Asymptotic expansion: ψ₁(z) ~ 1/z + 1/(2z²) + Σ_{k=1}^{N} B_{2k}/z^{2k+1}
        // Optimal truncation k* ≈ π·z (same reason as lnGamma_core/digamma_core).
        constexpr double PI_VAL = 3.141592653589793238462643383279502884;
        int num_terms = static_cast<int>(PI_VAL * z.toDouble()) + 10;
        if (num_terms < 5) num_terms = 5;
        int num_terms_cap = static_cast<int>(PI_VAL * shift_target * 1.2 + 100.0);
        if (num_terms_cap < 2000) num_terms_cap = 2000;
        if (num_terms > num_terms_cap) num_terms = num_terms_cap;

        const auto& bernoulli = getBernoulliCached(num_terms, wp);

        Float z_inv = Float(1) / z;
        z_inv.truncateToApprox(wp);
        Float z_inv2 = z_inv * z_inv;
        z_inv2.truncateToApprox(wp);

        Float result = z_inv + ldexp(z_inv2, -1);

        Float z_power = z_inv2 * z_inv;  // z^{-3}, z^{-5}, ...

        // Divergence detection: stop just before passing the minimum of the asymptotic series.
        int64_t prev_term_bits = (std::numeric_limits<int64_t>::max)();
        for (int k = 1; k <= num_terms; k++) {
            Float term = bernoulli[k] * z_power;
            int64_t curr_term_bits = term.isZero()
                ? (std::numeric_limits<int64_t>::min)()
                : term.exponent() + static_cast<int64_t>(term.mantissa().bitLength());

            if (k >= 3 && curr_term_bits >= prev_term_bits) break;

            result = result + term;
            result.truncateToApprox(wp);
            prev_term_bits = curr_term_bits;

            if (k >= 3 && term.isZero()) break;
            if (k >= 3) {
                auto result_bits = result.exponent() + static_cast<int64_t>(result.mantissa().bitLength());
                if (result_bits - curr_term_bits > wp_bits + 5) break;
            }

            z_power = z_power * z_inv2;
            z_power.truncateToApprox(wp);
        }

        result = result + shift_sum;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float trigamma(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) return Float::nan();
            return Float(0);  // ψ₁(∞) = 0
        }
        if (x.isZero()) return Float::nan();

        // Non-positive integer: pole
        if (x.isNegative() && x.isInteger()) return Float::nan();

        return trigamma_core(Float(x), x.effectiveBits(), precision);
    }

    Float trigamma(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) return Float::nan();
            return Float(0);  // ψ₁(∞) = 0
        }
        if (x.isZero()) return Float::nan();

        // Non-positive integer: pole
        if (x.isNegative() && x.isInteger()) return Float::nan();

        int eff = x.effectiveBits();
        return trigamma_core(std::move(x), eff, precision);
    }

    // polygamma(n, x) — polygamma function ψ^(n)(x) = d^{n+1}/dx^{n+1} ln Γ(x)
    // n=0: digamma, n=1: trigamma (delegate to existing implementations)
    // n≥2: asymptotic expansion + argument shift
    //   ψ^(n)(z) = (-1)^{n+1} [(n-1)!/z^n + n!/(2z^{n+1})
    //              + Σ_{k=1}^{K} B_{2k} · (2k+n-1)!/((2k)!) / z^{2k+n}]
    Float polygamma(int n, const Float& x, int precision) {
        Float::PrecisionScope _ps(precision + 64);   // Compute exact÷exact inside the function at working precision
        if (n < 0) {
            throw std::invalid_argument("polygamma: n must be non-negative");
        }
        if (n == 0) return digamma(x, precision);
        if (n == 1) return trigamma(x, precision);

        // n >= 2
        int eff_x = x.effectiveBits();
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) return Float::nan();
            return Float(0);  // ψ^(n)(+∞) = 0 (n ≥ 1)
        }
        if (x.isZero()) return Float::nan();
        if (x.isNegative() && x.isInteger()) return Float::nan();

        int wp = precision + 20;
        const int wp_bits = Float::precisionToBits(wp);  // Convergence check in bits

        // Compute n! and (n-1)!
        Float n_fact(1);
        for (int i = 2; i <= n; i++) n_fact = n_fact * i;
        Float n_minus_1_fact = n_fact / n;

        // Argument shift: ψ^(n)(z) = ψ^(n)(z+m) + (-1)^{n+1}·n!·Σ 1/(z+i)^{n+1}
        // BUGFIX (2026-05-30): fixed the same kind of unit-mixing as trigamma/digamma.
        // (1) shift_target = wp·0.35 was too small for the asymptotic series' convergence (driving the
        //     optimal-truncation error ~ e^{-2πz} (k*≈πz) down to wp_bits bits requires z≳0.4·wp)
        //     → align with trigamma to wp·0.45+8. (2) num_terms = wp/(2·log2 z) was a
        //     misuse of a bit formula assuming a geometric series → π·z + 10 + divergence detection.
        //     (3) the convergence check wp+5 mixed bits vs digits → wp_bits+5.
        Float z = x;
        z.truncateToApprox(wp);
        z.setEffectiveBits(wp_bits);
        Float shift_sum(0);

        double shift_target = std::max(20.0, wp * 0.45 + 8.0);
        double z_approx = z.toDouble();

        if (z_approx < shift_target) {
            int m = static_cast<int>(std::ceil(shift_target - z_approx)) + 1;
            for (int i = 0; i < m; i++) {
                Float zi = z + Float(i);
                zi.truncateToApprox(wp);
                // Compute zi^{n+1} by repeated multiplication
                Float zi_pow = zi;
                for (int j = 1; j <= n; j++) {
                    zi_pow = zi_pow * zi;
                    zi_pow.truncateToApprox(wp);
                }
                shift_sum = shift_sum + Float(1) / zi_pow;
            }
            z = z + Float(m);
            z.truncateToApprox(wp);
        }

        // Asymptotic expansion: optimal truncation k* ≈ π·z (same reason as trigamma/digamma/lnGamma).
        constexpr double PI_VAL = 3.141592653589793238462643383279502884;
        int num_terms = static_cast<int>(PI_VAL * z.toDouble()) + 10;
        if (num_terms < 5) num_terms = 5;
        int num_terms_cap = static_cast<int>(PI_VAL * shift_target * 1.2 + 100.0);
        if (num_terms_cap < 2000) num_terms_cap = 2000;
        if (num_terms > num_terms_cap) num_terms = num_terms_cap;

        auto bernoulli = computeBernoulliNumbers(num_terms, wp);

        Float z_inv = Float(1) / z;
        z_inv.truncateToApprox(wp);
        Float z_inv2 = z_inv * z_inv;
        z_inv2.truncateToApprox(wp);

        // z^{-n}
        Float z_inv_n(1);
        for (int i = 0; i < n; i++) {
            z_inv_n = z_inv_n * z_inv;
            z_inv_n.truncateToApprox(wp);
        }

        // First two terms: (n-1)!/z^n + n!/(2z^{n+1})
        Float A = n_minus_1_fact * z_inv_n
                + ldexp(n_fact * z_inv_n * z_inv, -1);
        A.truncateToApprox(wp);

        // Σ B_{2k} · R(n,k) / z^{2k+n}
        // R(n,k) = (2k+n-1)!/(2k)! = Π_{j=1}^{n-1} (2k+j)
        Float z_power = z_inv_n * z_inv2;  // z^{-(n+2)}

        int64_t prev_term_bits = (std::numeric_limits<int64_t>::max)();
        for (int k = 1; k <= num_terms; k++) {
            Float R(1);
            for (int j = 1; j <= n - 1; j++) {
                R = R * (2 * k + j);
            }

            Float term = bernoulli[k] * R * z_power;
            int64_t curr_term_bits = term.isZero()
                ? (std::numeric_limits<int64_t>::min)()
                : term.exponent() + static_cast<int64_t>(term.mantissa().bitLength());

            // Divergence detection: stop just before passing the smallest term of the asymptotic series.
            if (k >= 3 && curr_term_bits >= prev_term_bits) break;

            A = A + term;
            A.truncateToApprox(wp);
            prev_term_bits = curr_term_bits;

            if (k >= 3 && term.isZero()) break;
            if (k >= 3) {
                auto A_bits = A.exponent()
                    + static_cast<int64_t>(A.mantissa().bitLength());
                if (A_bits - curr_term_bits > wp_bits + 5) break;
            }

            z_power = z_power * z_inv2;
            z_power.truncateToApprox(wp);
        }

        // Sign: (-1)^{n+1}
        Float result = (n % 2 == 0) ? -A : A;

        // Shift correction: (-1)^{n+1} · n! · shift_sum
        if (!shift_sum.isZero()) {
            Float correction = n_fact * shift_sum;
            if (n % 2 == 0) correction = -correction;
            result = result + correction;
        }

        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float polygamma(int n, Float&& x, int precision) {
        Float::PrecisionScope _ps(precision + 64);   // Compute exact÷exact inside the function at working precision
        if (n < 0) {
            throw std::invalid_argument("polygamma: n must be non-negative");
        }
        if (n == 0) return digamma(std::move(x), precision);
        if (n == 1) return trigamma(std::move(x), precision);

        // n >= 2
        int eff_x = x.effectiveBits();
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) return Float::nan();
            return Float(0);  // ψ^(n)(+∞) = 0 (n ≥ 1)
        }
        if (x.isZero()) return Float::nan();
        if (x.isNegative() && x.isInteger()) return Float::nan();

        int wp = precision + 20;
        const int wp_bits = Float::precisionToBits(wp);  // Convergence check in bits

        // Compute n! and (n-1)!
        Float n_fact(1);
        for (int i = 2; i <= n; i++) n_fact = n_fact * i;
        Float n_minus_1_fact = n_fact / n;

        // Argument shift: ψ^(n)(z) = ψ^(n)(z+m) + (-1)^{n+1}·n!·Σ 1/(z+i)^{n+1}
        // BUGFIX (2026-05-30): fixed the same kind of unit-mixing as trigamma/digamma
        // (identical to the const Float& version; see its comments for details).
        Float z = std::move(x);
        z.truncateToApprox(wp);
        z.setEffectiveBits(wp_bits);
        Float shift_sum(0);

        double shift_target = std::max(20.0, wp * 0.45 + 8.0);
        double z_approx = z.toDouble();

        if (z_approx < shift_target) {
            int m = static_cast<int>(std::ceil(shift_target - z_approx)) + 1;
            for (int i = 0; i < m; i++) {
                Float zi = z + Float(i);
                zi.truncateToApprox(wp);
                // Compute zi^{n+1} by repeated multiplication
                Float zi_pow = zi;
                for (int j = 1; j <= n; j++) {
                    zi_pow = zi_pow * zi;
                    zi_pow.truncateToApprox(wp);
                }
                shift_sum = shift_sum + Float(1) / zi_pow;
            }
            z = z + Float(m);
            z.truncateToApprox(wp);
        }

        // Asymptotic expansion: optimal truncation k* ≈ π·z (same reason as trigamma/digamma/lnGamma).
        constexpr double PI_VAL = 3.141592653589793238462643383279502884;
        int num_terms = static_cast<int>(PI_VAL * z.toDouble()) + 10;
        if (num_terms < 5) num_terms = 5;
        int num_terms_cap = static_cast<int>(PI_VAL * shift_target * 1.2 + 100.0);
        if (num_terms_cap < 2000) num_terms_cap = 2000;
        if (num_terms > num_terms_cap) num_terms = num_terms_cap;

        auto bernoulli = computeBernoulliNumbers(num_terms, wp);

        Float z_inv = Float(1) / z;
        z_inv.truncateToApprox(wp);
        Float z_inv2 = z_inv * z_inv;
        z_inv2.truncateToApprox(wp);

        // z^{-n}
        Float z_inv_n(1);
        for (int i = 0; i < n; i++) {
            z_inv_n = z_inv_n * z_inv;
            z_inv_n.truncateToApprox(wp);
        }

        // First two terms: (n-1)!/z^n + n!/(2z^{n+1})
        Float A = n_minus_1_fact * z_inv_n
                + ldexp(n_fact * z_inv_n * z_inv, -1);
        A.truncateToApprox(wp);

        // Σ B_{2k} · R(n,k) / z^{2k+n}
        // R(n,k) = (2k+n-1)!/(2k)! = Π_{j=1}^{n-1} (2k+j)
        Float z_power = z_inv_n * z_inv2;  // z^{-(n+2)}

        int64_t prev_term_bits = (std::numeric_limits<int64_t>::max)();
        for (int k = 1; k <= num_terms; k++) {
            Float R(1);
            for (int j = 1; j <= n - 1; j++) {
                R = R * (2 * k + j);
            }

            Float term = bernoulli[k] * R * z_power;
            int64_t curr_term_bits = term.isZero()
                ? (std::numeric_limits<int64_t>::min)()
                : term.exponent() + static_cast<int64_t>(term.mantissa().bitLength());

            // Divergence detection: stop just before passing the smallest term of the asymptotic series.
            if (k >= 3 && curr_term_bits >= prev_term_bits) break;

            A = A + term;
            A.truncateToApprox(wp);
            prev_term_bits = curr_term_bits;

            if (k >= 3 && term.isZero()) break;
            if (k >= 3) {
                auto A_bits = A.exponent()
                    + static_cast<int64_t>(A.mantissa().bitLength());
                if (A_bits - curr_term_bits > wp_bits + 5) break;
            }

            z_power = z_power * z_inv2;
            z_power.truncateToApprox(wp);
        }

        // Sign: (-1)^{n+1}
        Float result = (n % 2 == 0) ? -A : A;

        // Shift correction: (-1)^{n+1} · n! · shift_sum
        if (!shift_sum.isZero()) {
            Float correction = n_fact * shift_sum;
            if (n % 2 == 0) correction = -correction;
            result = result + correction;
        }

        finalizeResult(result, eff_x, precision);
        return result;
    }

    //=============================================================================
    // Incomplete gamma functions
    //=============================================================================

    // gammaP — regularized lower incomplete gamma function P(a,x) = γ(a,x)/Γ(a)
    // x < a+1: Taylor series
    //   P(a,x) = exp(-x + a·ln(x) - lnΓ(a)) · Σ_{n≥0} x^n / (a·(a+1)···(a+n))
    // x ≥ a+1: 1 - Q(a,x) (CF is faster)
    Float gammaP(const Float& a, const Float& x, int precision) {
        if (a.isNaN() || x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);        // P(a,0) = 0
        if (x.isNegative()) return Float::nan(); // x ≥ 0 only
        if (!a.isPositive()) return Float::nan(); // a > 0 only
        if (x.isInfinity()) return Float(1);      // P(a,+∞) = 1

        int eff = std::min(a.effectiveBits(), x.effectiveBits());
        int wp = precision + 20;

        double a_d = a.toDouble();
        double x_d = x.toDouble();

        if (x_d < a_d + 1.0) {
            // Taylor series
            // BUGFIX (2026-05-30): for exact inputs (a=3,x=2 etc.), exact/exact divisions like
            // Float(1)/a_wp fall to default_precision. Make the working values non-exact.
            const int wp_bits = Float::precisionToBits(wp);
            Float a_wp = a; a_wp.truncateToApprox(wp); a_wp.setEffectiveBits(wp_bits);
            Float x_wp = x; x_wp.truncateToApprox(wp); x_wp.setEffectiveBits(wp_bits);

            Float front = exp(-x_wp + a_wp * log(x_wp, wp)
                              - lnGamma(a_wp, wp), wp);

            // S = Σ x^n / Π_{k=0}^{n} (a+k)
            // term_0 = 1/a, term_n = term_{n-1} · x / (a+n)
            Float term = Float(1) / a_wp;
            Float sum = term;

            for (int n = 1; n < 10000; n++) {
                term = term * x_wp / (a_wp + Float(n));
                term.truncateToApprox(wp);
                sum = sum + term;
                sum.truncateToApprox(wp);

                if (n >= 3 && term.isZero()) break;
                if (n >= 3) {
                    int64_t t_bits = term.exponent()
                        + static_cast<int64_t>(term.mantissa().bitLength());
                    int64_t s_bits = sum.exponent()
                        + static_cast<int64_t>(sum.mantissa().bitLength());
                    if (s_bits - t_bits > Float::precisionToBits(wp) + 5) break;
                }
            }

            Float result = front * sum;
            finalizeResult(result, eff, precision);
            return result;
        } else {
            // x ≥ a+1: compute Q via CF and return 1 - Q
            Float q = gammaQ(a, x, wp);
            Float result = Float(1) - q;
            finalizeResult(result, eff, precision);
            return result;
        }
    }

    Float gammaP(Float&& a, Float&& x, int precision) {
        if (a.isNaN() || x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);        // P(a,0) = 0
        if (x.isNegative()) return Float::nan(); // x ≥ 0 only
        if (!a.isPositive()) return Float::nan(); // a > 0 only
        if (x.isInfinity()) return Float(1);      // P(a,+∞) = 1

        int eff = std::min(a.effectiveBits(), x.effectiveBits());
        int wp = precision + 20;

        double a_d = a.toDouble();
        double x_d = x.toDouble();

        if (x_d < a_d + 1.0) {
            // Taylor series
            const int wp_bits = Float::precisionToBits(wp);  // avoid exact/exact poison
            Float a_wp = std::move(a); a_wp.truncateToApprox(wp); a_wp.setEffectiveBits(wp_bits);
            Float x_wp = std::move(x); x_wp.truncateToApprox(wp); x_wp.setEffectiveBits(wp_bits);

            Float front = exp(-x_wp + a_wp * log(x_wp, wp)
                              - lnGamma(a_wp, wp), wp);

            Float term = Float(1) / a_wp;
            Float sum = term;

            for (int n = 1; n < 10000; n++) {
                term = term * x_wp / (a_wp + Float(n));
                term.truncateToApprox(wp);
                sum = sum + term;
                sum.truncateToApprox(wp);

                if (n >= 3 && term.isZero()) break;
                if (n >= 3) {
                    int64_t t_bits = term.exponent()
                        + static_cast<int64_t>(term.mantissa().bitLength());
                    int64_t s_bits = sum.exponent()
                        + static_cast<int64_t>(sum.mantissa().bitLength());
                    if (s_bits - t_bits > Float::precisionToBits(wp) + 5) break;
                }
            }

            Float result = front * sum;
            finalizeResult(result, eff, precision);
            return result;
        } else {
            Float q = gammaQ(std::move(a), std::move(x), wp);
            Float result = Float(1) - q;
            finalizeResult(result, eff, precision);
            return result;
        }
    }

    // gammaQ — regularized upper incomplete gamma function Q(a,x) = Γ(a,x)/Γ(a)
    // x ≥ a+1: Legendre continued fraction (Modified Lentz)
    //   Q(a,x) = exp(-x + a·ln(x) - lnΓ(a)) / f
    //   f = (x+1-a) + K_{n≥1} [-n(n-a) / (x+2n+1-a)]
    // x < a+1: 1 - P(a,x) (Taylor is faster)
    Float gammaQ(const Float& a, const Float& x, int precision) {
        if (a.isNaN() || x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(1);           // Q(a,0) = 1
        if (x.isNegative()) return Float::nan();
        if (!a.isPositive()) return Float::nan();
        if (x.isInfinity()) return Float(0);        // Q(a,+∞) = 0

        int eff = std::min(a.effectiveBits(), x.effectiveBits());
        int wp = precision + 20;

        double a_d = a.toDouble();
        double x_d = x.toDouble();

        if (x_d >= a_d + 1.0) {
            // Legendre CF
            // BUGFIX (2026-05-30): avoid exact/exact division poison for exact inputs.
            const int wp_bits = Float::precisionToBits(wp);
            Float a_wp = a; a_wp.truncateToApprox(wp); a_wp.setEffectiveBits(wp_bits);
            Float x_wp = x; x_wp.truncateToApprox(wp); x_wp.setEffectiveBits(wp_bits);

            Float front = exp(-x_wp + a_wp * log(x_wp, wp)
                              - lnGamma(a_wp, wp), wp);

            // Modified Lentz
            Float tiny(1, -wp * 4);  // 2^{-4·wp} (to avoid zero)

            Float b0 = x_wp + Float(1) - a_wp;
            Float f = b0.isZero() ? tiny : b0;
            f.truncateToApprox(wp);
            Float C = f;
            Float D(0);

            for (int n = 1; n < 10000; n++) {
                Float an = Float(-n) * (Float(n) - a_wp);
                Float bn = x_wp + Float(2 * n + 1) - a_wp;

                D = bn + an * D;
                D.truncateToApprox(wp);
                if (D.isZero()) D = tiny;
                D = Float(1) / D;
                D.truncateToApprox(wp);

                C = bn + an / C;
                C.truncateToApprox(wp);
                if (C.isZero()) C = tiny;

                Float delta = C * D;
                f = f * delta;
                f.truncateToApprox(wp);

                Float diff = delta - Float(1);
                if (diff.isZero()) break;
                if (n >= 3) {
                    int64_t diff_bits = diff.exponent()
                        + static_cast<int64_t>(diff.mantissa().bitLength());
                    if (diff_bits < -(Float::precisionToBits(wp) + 5)) break;
                }
            }

            Float result = front / f;
            finalizeResult(result, eff, precision);
            return result;
        } else {
            // x < a+1: compute P via Taylor and return 1 - P
            Float p = gammaP(a, x, wp);
            Float result = Float(1) - p;
            finalizeResult(result, eff, precision);
            return result;
        }
    }

    Float gammaQ(Float&& a, Float&& x, int precision) {
        if (a.isNaN() || x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(1);           // Q(a,0) = 1
        if (x.isNegative()) return Float::nan();
        if (!a.isPositive()) return Float::nan();
        if (x.isInfinity()) return Float(0);        // Q(a,+∞) = 0

        int eff = std::min(a.effectiveBits(), x.effectiveBits());
        int wp = precision + 20;

        double a_d = a.toDouble();
        double x_d = x.toDouble();

        if (x_d >= a_d + 1.0) {
            // Legendre CF
            const int wp_bits = Float::precisionToBits(wp);  // avoid exact/exact poison
            Float a_wp = std::move(a); a_wp.truncateToApprox(wp); a_wp.setEffectiveBits(wp_bits);
            Float x_wp = std::move(x); x_wp.truncateToApprox(wp); x_wp.setEffectiveBits(wp_bits);

            Float front = exp(-x_wp + a_wp * log(x_wp, wp)
                              - lnGamma(a_wp, wp), wp);

            // Modified Lentz
            Float tiny(1, -wp * 4);

            Float b0 = x_wp + Float(1) - a_wp;
            Float f = b0.isZero() ? tiny : b0;
            f.truncateToApprox(wp);
            Float C = f;
            Float D(0);

            for (int n = 1; n < 10000; n++) {
                Float an = Float(-n) * (Float(n) - a_wp);
                Float bn = x_wp + Float(2 * n + 1) - a_wp;

                D = bn + an * D;
                D.truncateToApprox(wp);
                if (D.isZero()) D = tiny;
                D = Float(1) / D;
                D.truncateToApprox(wp);

                C = bn + an / C;
                C.truncateToApprox(wp);
                if (C.isZero()) C = tiny;

                Float delta = C * D;
                f = f * delta;
                f.truncateToApprox(wp);

                Float diff = delta - Float(1);
                if (diff.isZero()) break;
                if (n >= 3) {
                    int64_t diff_bits = diff.exponent()
                        + static_cast<int64_t>(diff.mantissa().bitLength());
                    if (diff_bits < -(Float::precisionToBits(wp) + 5)) break;
                }
            }

            Float result = front / f;
            finalizeResult(result, eff, precision);
            return result;
        } else {
            Float p = gammaP(std::move(a), std::move(x), wp);
            Float result = Float(1) - p;
            finalizeResult(result, eff, precision);
            return result;
        }
    }

    // gammaLower — lower incomplete gamma γ(a,x) = P(a,x)·Γ(a)
    Float gammaLower(const Float& a, const Float& x, int precision) {
        if (a.isNaN() || x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);
        if (x.isNegative()) return Float::nan();
        if (!a.isPositive()) return Float::nan();

        int eff = std::min(a.effectiveBits(), x.effectiveBits());
        int wp = precision + 15;
        Float p = gammaP(a, x, wp);
        Float g = gamma(a, wp);
        Float result = p * g;
        finalizeResult(result, eff, precision);
        return result;
    }

    Float gammaLower(Float&& a, Float&& x, int precision) {
        if (a.isNaN() || x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);
        if (x.isNegative()) return Float::nan();
        if (!a.isPositive()) return Float::nan();

        int eff = std::min(a.effectiveBits(), x.effectiveBits());
        int wp = precision + 15;
        Float p = gammaP(a, x, wp);
        Float g = gamma(std::move(a), wp);
        Float result = p * g;
        finalizeResult(result, eff, precision);
        return result;
    }

    // gammaUpper — upper incomplete gamma Γ(a,x) = Q(a,x)·Γ(a)
    Float gammaUpper(const Float& a, const Float& x, int precision) {
        if (a.isNaN() || x.isNaN()) return Float::nan();
        if (x.isZero()) return gamma(a, precision);  // Γ(a,0) = Γ(a)
        if (x.isNegative()) return Float::nan();
        if (!a.isPositive()) return Float::nan();
        if (x.isInfinity()) return Float(0);

        int eff = std::min(a.effectiveBits(), x.effectiveBits());
        int wp = precision + 15;
        Float q = gammaQ(a, x, wp);
        Float g = gamma(a, wp);
        Float result = q * g;
        finalizeResult(result, eff, precision);
        return result;
    }

    Float gammaUpper(Float&& a, Float&& x, int precision) {
        if (a.isNaN() || x.isNaN()) return Float::nan();
        if (x.isZero()) return gamma(std::move(a), precision);  // Γ(a,0) = Γ(a)
        if (x.isNegative()) return Float::nan();
        if (!a.isPositive()) return Float::nan();
        if (x.isInfinity()) return Float(0);

        int eff = std::min(a.effectiveBits(), x.effectiveBits());
        int wp = precision + 15;
        Float q = gammaQ(a, x, wp);
        Float g = gamma(std::move(a), wp);
        Float result = q * g;
        finalizeResult(result, eff, precision);
        return result;
    }

    //=============================================================================
    // Incomplete beta function
    //=============================================================================

    // betaRegularized — regularized incomplete beta function I_x(a,b)
    // Continued-fraction expansion (Modified Lentz) + symmetry
    //   I_x(a,b) = front / (1 + d₁/(1 + d₂/(1 + ...)))
    //   d_{2m+1} = -(a+m)(a+b+m)x / ((a+2m)(a+2m+1))
    //   d_{2m}   = m(b-m)x / ((a+2m-1)(a+2m))
    // When x ≥ (a+1)/(a+b+2): invert via I_x(a,b) = 1 - I_{1-x}(b,a)
    Float betaRegularized(const Float& x, const Float& a, const Float& b, int precision) {
        if (x.isNaN() || a.isNaN() || b.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);           // I_0(a,b) = 0
        Float one(1);
        if (x == one) return Float(1);              // I_1(a,b) = 1
        if (x.isNegative() || x > one) return Float::nan(); // x ∈ [0,1]
        if (!a.isPositive() || !b.isPositive()) return Float::nan();

        int eff = std::min({x.effectiveBits(), a.effectiveBits(), b.effectiveBits()});

        int wp = precision + 20;

        Float x_wp = x; x_wp.truncateToApprox(wp);
        Float a_wp = a; a_wp.truncateToApprox(wp);
        Float b_wp = b; b_wp.truncateToApprox(wp);

        // Symmetry-based inversion decision: invert if x ≥ (a+1)/(a+b+2)
        double x_d = x.toDouble();
        double a_d = a.toDouble();
        double b_d = b.toDouble();
        bool flip = x_d >= (a_d + 1.0) / (a_d + b_d + 2.0);

        Float xx, aa, bb;
        if (flip) {
            xx = Float(1) - x_wp; xx.truncateToApprox(wp);
            aa = b_wp;
            bb = a_wp;
        } else {
            xx = x_wp;
            aa = a_wp;
            bb = b_wp;
        }

        // front = x^a · (1-x)^b / (a · B(a,b))
        //       = exp(a·ln(x) + b·ln(1-x) + lnΓ(a+b) - lnΓ(a) - lnΓ(b)) / a
        Float ln_front = aa * log(xx, wp)
                       + bb * log(Float(1) - xx, wp)
                       + lnGamma(aa + bb, wp)
                       - lnGamma(aa, wp)
                       - lnGamma(bb, wp);
        Float front = exp(ln_front, wp) / aa;
        front.truncateToApprox(wp);

        // CF: evaluate 1 + d_1/(1 + d_2/(1 + ...)) via Modified Lentz
        Float tiny(1, -wp * 4);

        Float f(1);
        Float C(1);
        Float D(0);

        for (int n = 1; n < 10000; n++) {
            Float d;
            if (n % 2 == 1) {
                // d_{2m+1}: m = (n-1)/2
                int m = (n - 1) / 2;
                Float am = aa + Float(m);
                Float abm = aa + bb + Float(m);
                Float denom = (aa + Float(2 * m)) * (aa + Float(2 * m + 1));
                d = -(am * abm * xx) / denom;
            } else {
                // d_{2m}: m = n/2
                int m = n / 2;
                Float fm(m);
                Float bm = bb - Float(m);
                Float denom = (aa + Float(2 * m - 1)) * (aa + Float(2 * m));
                d = (fm * bm * xx) / denom;
            }
            d.truncateToApprox(wp);

            D = Float(1) + d * D;
            D.truncateToApprox(wp);
            if (D.isZero()) D = tiny;
            D = Float(1) / D;
            D.truncateToApprox(wp);

            C = Float(1) + d / C;
            C.truncateToApprox(wp);
            if (C.isZero()) C = tiny;

            Float delta = C * D;
            f = f * delta;
            f.truncateToApprox(wp);

            if (n >= 3) {
                Float diff = delta - Float(1);
                if (diff.isZero()) break;
                int64_t diff_bits = diff.exponent()
                    + static_cast<int64_t>(diff.mantissa().bitLength());
                if (diff_bits < -(Float::precisionToBits(wp) + 5)) break;
            }
        }

        Float result = front / f;

        if (flip) {
            result = Float(1) - result;
        }

        finalizeResult(result, eff, precision);
        return result;
    }

    Float betaRegularized(Float&& x, Float&& a, Float&& b, int precision) {
        if (x.isNaN() || a.isNaN() || b.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);           // I_0(a,b) = 0
        Float one(1);
        if (x == one) return Float(1);              // I_1(a,b) = 1
        if (x.isNegative() || x > one) return Float::nan(); // x ∈ [0,1]
        if (!a.isPositive() || !b.isPositive()) return Float::nan();

        int eff = std::min({x.effectiveBits(), a.effectiveBits(), b.effectiveBits()});

        int wp = precision + 20;

        Float x_wp = std::move(x); x_wp.truncateToApprox(wp);
        Float a_wp = std::move(a); a_wp.truncateToApprox(wp);
        Float b_wp = std::move(b); b_wp.truncateToApprox(wp);

        // Symmetry-based inversion decision: invert if x ≥ (a+1)/(a+b+2)
        double x_d = x_wp.toDouble();
        double a_d = a_wp.toDouble();
        double b_d = b_wp.toDouble();
        bool flip = x_d >= (a_d + 1.0) / (a_d + b_d + 2.0);

        Float xx, aa, bb;
        if (flip) {
            xx = Float(1) - x_wp; xx.truncateToApprox(wp);
            aa = b_wp;
            bb = a_wp;
        } else {
            xx = x_wp;
            aa = a_wp;
            bb = b_wp;
        }

        // front = x^a · (1-x)^b / (a · B(a,b))
        Float ln_front = aa * log(xx, wp)
                       + bb * log(Float(1) - xx, wp)
                       + lnGamma(aa + bb, wp)
                       - lnGamma(aa, wp)
                       - lnGamma(bb, wp);
        Float front = exp(ln_front, wp) / aa;
        front.truncateToApprox(wp);

        // CF: Modified Lentz
        Float tiny(1, -wp * 4);

        Float f(1);
        Float C(1);
        Float D(0);

        for (int n = 1; n < 10000; n++) {
            Float d;
            if (n % 2 == 1) {
                int m = (n - 1) / 2;
                Float am = aa + Float(m);
                Float abm = aa + bb + Float(m);
                Float denom = (aa + Float(2 * m)) * (aa + Float(2 * m + 1));
                d = -(am * abm * xx) / denom;
            } else {
                int m = n / 2;
                Float fm(m);
                Float bm = bb - Float(m);
                Float denom = (aa + Float(2 * m - 1)) * (aa + Float(2 * m));
                d = (fm * bm * xx) / denom;
            }
            d.truncateToApprox(wp);

            D = Float(1) + d * D;
            D.truncateToApprox(wp);
            if (D.isZero()) D = tiny;
            D = Float(1) / D;
            D.truncateToApprox(wp);

            C = Float(1) + d / C;
            C.truncateToApprox(wp);
            if (C.isZero()) C = tiny;

            Float delta = C * D;
            f = f * delta;
            f.truncateToApprox(wp);

            if (n >= 3) {
                Float diff = delta - Float(1);
                if (diff.isZero()) break;
                int64_t diff_bits = diff.exponent()
                    + static_cast<int64_t>(diff.mantissa().bitLength());
                if (diff_bits < -(Float::precisionToBits(wp) + 5)) break;
            }
        }

        Float result = front / f;

        if (flip) {
            result = Float(1) - result;
        }

        finalizeResult(result, eff, precision);
        return result;
    }


    //=============================================================================
    // Elliptic integrals — Carlson symmetric forms
    //=============================================================================

    // R_C(x, y) = (1/2)∫₀^∞ dt / [(t+y)√(t+x)]
    // Quadratically convergent iteration via the duplication theorem
    static Float carlsonRC_core(Float xw, Float yw, int eff, int precision) {
        Float::PrecisionScope _ps(precision + 64);   // Compute exact÷exact inside the function at working precision
        int wp = precision + 20;
        xw.truncateToApprox(wp);
        yw.truncateToApprox(wp);

        for (int iter = 0; iter < 200; iter++) {
            Float lam = ldexp(sqrt(xw * yw, wp), 1) + yw;
            xw = ldexp(xw + lam, -2); xw.truncateToApprox(wp);
            yw = ldexp(yw + lam, -2); yw.truncateToApprox(wp);

            Float A = (xw + yw + yw) / 3;
            Float s = (yw - A) / A;
            if (s.isZero()) break;
            int64_t s_bits = s.exponent() + static_cast<int64_t>(s.mantissa().bitLength());
            if (s_bits < -(Float::precisionToBits(wp) + 5)) break;
        }

        Float A = (xw + yw + yw) / 3;
        Float s = (yw - A) / A;
        // 1 + (3/10)s² + (1/7)s³ + (3/8)s⁴ + (9/22)s⁵
        Float poly = Float(1) + s * s * (Float(3) / 10
                     + s * (Float(1) / 7
                     + s * (Float(3) / 8
                     + s * 9 / 22)));

        Float result = poly / sqrt(A, wp);
        finalizeResult(result, eff, precision);
        return result;
    }

    Float carlsonRC(const Float& x, const Float& y, int precision) {
        if (x.isNaN() || y.isNaN()) return Float::nan();
        if (x.isNegative() || !y.isPositive()) return Float::nan();
        int eff = std::min(x.effectiveBits(), y.effectiveBits());
        return carlsonRC_core(Float(x), Float(y), eff, precision);
    }

    Float carlsonRC(Float&& x, Float&& y, int precision) {
        if (x.isNaN() || y.isNaN()) return Float::nan();
        if (x.isNegative() || !y.isPositive()) return Float::nan();
        int eff = std::min(x.effectiveBits(), y.effectiveBits());
        return carlsonRC_core(std::move(x), std::move(y), eff, precision);
    }

    // R_F(x, y, z) = (1/2)∫₀^∞ dt / √[(t+x)(t+y)(t+z)]
    // x, y, z ≥ 0, at most one is 0
    static Float carlsonRF_core(Float xw, Float yw, Float zw, int eff, int precision) {
        int wp = precision + 20;
        xw.truncateToApprox(wp);
        yw.truncateToApprox(wp);
        zw.truncateToApprox(wp);

        for (int iter = 0; iter < 200; iter++) {
            Float sqx = sqrt(xw, wp);
            Float sqy = sqrt(yw, wp);
            Float sqz = sqrt(zw, wp);
            Float lam = sqx * sqy + sqy * sqz + sqz * sqx;
            xw = ldexp(xw + lam, -2); xw.truncateToApprox(wp);
            yw = ldexp(yw + lam, -2); yw.truncateToApprox(wp);
            zw = ldexp(zw + lam, -2); zw.truncateToApprox(wp);

            Float A = (xw + yw + zw) / 3;
            Float dx = (A - xw) / A;
            Float dy = (A - yw) / A;
            Float dz = (A - zw) / A;
            int64_t dx_b = dx.isZero() ? INT64_MIN : dx.exponent() + static_cast<int64_t>(dx.mantissa().bitLength());
            int64_t dy_b = dy.isZero() ? INT64_MIN : dy.exponent() + static_cast<int64_t>(dy.mantissa().bitLength());
            int64_t dz_b = dz.isZero() ? INT64_MIN : dz.exponent() + static_cast<int64_t>(dz.mantissa().bitLength());
            if (std::max({dx_b, dy_b, dz_b}) < -(Float::precisionToBits(wp) + 5)) break;
        }

        Float A = (xw + yw + zw) / 3;
        Float X = (A - xw) / A;
        Float Y = (A - yw) / A;
        Float Z = -(X + Y);
        Float E2 = X * Y - Z * Z;
        Float E3 = X * Y * Z;

        Float poly = Float(1) - E2 / 10 + E3 / 14
                   + E2 * E2 / 24 - 3 * E2 * E3 / 44;

        Float result = poly / sqrt(A, wp);
        finalizeResult(result, eff, precision);
        return result;
    }

    Float carlsonRF(const Float& x, const Float& y, const Float& z, int precision) {
        if (x.isNaN() || y.isNaN() || z.isNaN()) return Float::nan();
        if (x.isNegative() || y.isNegative() || z.isNegative()) return Float::nan();
        int eff = std::min({x.effectiveBits(), y.effectiveBits(), z.effectiveBits()});
        return carlsonRF_core(Float(x), Float(y), Float(z), eff, precision);
    }

    Float carlsonRF(Float&& x, Float&& y, Float&& z, int precision) {
        if (x.isNaN() || y.isNaN() || z.isNaN()) return Float::nan();
        if (x.isNegative() || y.isNegative() || z.isNegative()) return Float::nan();
        int eff = std::min({x.effectiveBits(), y.effectiveBits(), z.effectiveBits()});
        return carlsonRF_core(std::move(x), std::move(y), std::move(z), eff, precision);
    }

    // R_D(x, y, z) = (3/2)∫₀^∞ dt / [(t+z)^{3/2}√((t+x)(t+y))]
    // x, y ≥ 0 (at most one is 0), z > 0
    static Float carlsonRD_core(Float xw, Float yw, Float zw, int eff, int precision) {
        Float::PrecisionScope _ps(precision + 64);   // Compute exact÷exact inside the function at working precision
        int wp = precision + 20;
        xw.truncateToApprox(wp);
        yw.truncateToApprox(wp);
        zw.truncateToApprox(wp);
        Float sigma(0);
        Float fac(1);

        for (int iter = 0; iter < 200; iter++) {
            Float sqx = sqrt(xw, wp);
            Float sqy = sqrt(yw, wp);
            Float sqz = sqrt(zw, wp);
            Float lam = sqx * sqy + sqy * sqz + sqz * sqx;
            sigma = sigma + fac / (sqz * (zw + lam));
            sigma.truncateToApprox(wp);
            fac = ldexp(fac, -2);
            xw = ldexp(xw + lam, -2); xw.truncateToApprox(wp);
            yw = ldexp(yw + lam, -2); yw.truncateToApprox(wp);
            zw = ldexp(zw + lam, -2); zw.truncateToApprox(wp);

            Float A = (xw + yw + 3 * zw) / 5;
            Float dx = (A - xw) / A;
            Float dy = (A - yw) / A;
            Float dz = (A - zw) / A;
            int64_t dx_b = dx.isZero() ? INT64_MIN : dx.exponent() + static_cast<int64_t>(dx.mantissa().bitLength());
            int64_t dy_b = dy.isZero() ? INT64_MIN : dy.exponent() + static_cast<int64_t>(dy.mantissa().bitLength());
            int64_t dz_b = dz.isZero() ? INT64_MIN : dz.exponent() + static_cast<int64_t>(dz.mantissa().bitLength());
            if (std::max({dx_b, dy_b, dz_b}) < -(Float::precisionToBits(wp) + 5)) break;
        }

        Float A = (xw + yw + 3 * zw) / 5;
        Float dx = (A - xw) / A;
        Float dy = (A - yw) / A;
        Float dz = (A - zw) / A;
        Float ea = dx * dy;
        Float eb = dz * dz;
        Float ec = ea - eb;
        Float ed = ea - 6 * eb;
        Float ee = ed + ec + ec;
        // C₁=3/14, C₂=1/6, C₃=9/22, C₄=3/26, C₅=9/88, C₆=9/52
        Float poly = Float(1)
            + ed * (Float(-3) / 14 + Float(9) / 88 * ed - Float(9) / 52 * dz * ee)
            + dz * (Float(1) / 6 * ee + dz * (Float(-9) / 22 * ec + dz * 3 / 26 * ea));

        Float result = 3 * sigma + fac * poly / (A * sqrt(A, wp));
        finalizeResult(result, eff, precision);
        return result;
    }

    Float carlsonRD(const Float& x, const Float& y, const Float& z, int precision) {
        if (x.isNaN() || y.isNaN() || z.isNaN()) return Float::nan();
        if (x.isNegative() || y.isNegative() || !z.isPositive()) return Float::nan();
        int eff = std::min({x.effectiveBits(), y.effectiveBits(), z.effectiveBits()});
        return carlsonRD_core(Float(x), Float(y), Float(z), eff, precision);
    }

    Float carlsonRD(Float&& x, Float&& y, Float&& z, int precision) {
        if (x.isNaN() || y.isNaN() || z.isNaN()) return Float::nan();
        if (x.isNegative() || y.isNegative() || !z.isPositive()) return Float::nan();
        int eff = std::min({x.effectiveBits(), y.effectiveBits(), z.effectiveBits()});
        return carlsonRD_core(std::move(x), std::move(y), std::move(z), eff, precision);
    }

    // R_J(x, y, z, p) = (3/2)∫₀^∞ dt / [(t+p)√((t+x)(t+y)(t+z))]
    // x, y, z ≥ 0 (at most one is 0), p > 0
    static Float carlsonRJ_core(Float xw, Float yw, Float zw, Float pw, int eff, int precision) {
        Float::PrecisionScope _ps(precision + 64);   // Compute exact÷exact inside the function at working precision
        int wp = precision + 20;
        xw.truncateToApprox(wp);
        yw.truncateToApprox(wp);
        zw.truncateToApprox(wp);
        pw.truncateToApprox(wp);
        Float sigma(0);
        Float fac(1);

        for (int iter = 0; iter < 200; iter++) {
            Float sqx = sqrt(xw, wp);
            Float sqy = sqrt(yw, wp);
            Float sqz = sqrt(zw, wp);
            Float sqp = sqrt(pw, wp);
            Float lam = sqx * sqy + sqy * sqz + sqz * sqx;
            Float alpha = pw * (sqx + sqy + sqz) + sqx * sqy * sqz;
            alpha = alpha * alpha;
            alpha.truncateToApprox(wp);
            Float beta = pw * (pw + lam) * (pw + lam);
            beta.truncateToApprox(wp);
            sigma = sigma + fac * carlsonRC(alpha, beta, wp);
            sigma.truncateToApprox(wp);
            fac = ldexp(fac, -2);
            xw = ldexp(xw + lam, -2); xw.truncateToApprox(wp);
            yw = ldexp(yw + lam, -2); yw.truncateToApprox(wp);
            zw = ldexp(zw + lam, -2); zw.truncateToApprox(wp);
            pw = ldexp(pw + lam, -2); pw.truncateToApprox(wp);

            Float A = (xw + yw + zw + pw + pw) / 5;
            Float dx = (A - xw) / A;
            Float dy = (A - yw) / A;
            Float dz = (A - zw) / A;
            Float dp = (A - pw) / A;
            int64_t dx_b = dx.isZero() ? INT64_MIN : dx.exponent() + static_cast<int64_t>(dx.mantissa().bitLength());
            int64_t dy_b = dy.isZero() ? INT64_MIN : dy.exponent() + static_cast<int64_t>(dy.mantissa().bitLength());
            int64_t dz_b = dz.isZero() ? INT64_MIN : dz.exponent() + static_cast<int64_t>(dz.mantissa().bitLength());
            int64_t dp_b = dp.isZero() ? INT64_MIN : dp.exponent() + static_cast<int64_t>(dp.mantissa().bitLength());
            if (std::max({dx_b, dy_b, dz_b, dp_b}) < -(Float::precisionToBits(wp) + 5)) break;
        }

        Float A = (xw + yw + zw + pw + pw) / 5;
        Float dx = (A - xw) / A;
        Float dy = (A - yw) / A;
        Float dz = (A - zw) / A;
        Float dp = (A - pw) / A;
        Float ea = dx * (dy + dz) + dy * dz;
        Float eb = dx * dy * dz;
        Float ec = dp * dp;
        Float ed = ea - 3 * ec;
        Float ee = eb + ldexp(dp * (ea - ec), 1);
        // C₁=3/14, C₂=1/3, C₃=3/22, C₄=3/26
        Float poly = Float(1)
            + ed * (Float(-3) / 14 + Float(9) / 88 * ed - Float(9) / 52 * ee)
            + eb * (Float(1) / 6 + dp * (Float(-6) / 22 + dp * 3 / 26))
            + dp * ea * (Float(1) / 3 - dp * 3 / 22)
            - Float(1) / 3 * dp * ec;

        Float result = 3 * sigma + fac * poly / (A * sqrt(A, wp));
        finalizeResult(result, eff, precision);
        return result;
    }

    Float carlsonRJ(const Float& x, const Float& y, const Float& z, const Float& p, int precision) {
        if (x.isNaN() || y.isNaN() || z.isNaN() || p.isNaN()) return Float::nan();
        if (x.isNegative() || y.isNegative() || z.isNegative() || !p.isPositive())
            return Float::nan();
        int eff = std::min({x.effectiveBits(), y.effectiveBits(), z.effectiveBits(), p.effectiveBits()});
        return carlsonRJ_core(Float(x), Float(y), Float(z), Float(p), eff, precision);
    }

    Float carlsonRJ(Float&& x, Float&& y, Float&& z, Float&& p, int precision) {
        if (x.isNaN() || y.isNaN() || z.isNaN() || p.isNaN()) return Float::nan();
        if (x.isNegative() || y.isNegative() || z.isNegative() || !p.isPositive())
            return Float::nan();
        int eff = std::min({x.effectiveBits(), y.effectiveBits(), z.effectiveBits(), p.effectiveBits()});
        return carlsonRJ_core(std::move(x), std::move(y), std::move(z), std::move(p), eff, precision);
    }

    //=============================================================================
    // Elliptic integrals — Legendre form
    //=============================================================================

    // K(k) = R_F(0, 1-k², 1) — complete elliptic integral of the first kind
    Float ellipticK(const Float& k, int precision) {
        if (k.isNaN()) return Float::nan();
        int eff_k = k.effectiveBits();
        int wp = precision + 15;
        Float k2 = k * k; k2.truncateToApprox(wp);
        if (k2 == Float(1)) return Float::positiveInfinity(); // K(1) = ∞
        Float result = carlsonRF(Float(0), Float(1) - k2, Float(1), wp);
        finalizeResult(result, eff_k, precision);
        return result;
    }

    Float ellipticK(Float&& k, int precision) {
        if (k.isNaN()) return Float::nan();
        int eff_k = k.effectiveBits();
        int wp = precision + 15;
        Float k2 = k * k; k2.truncateToApprox(wp);
        if (k2 == Float(1)) return Float::positiveInfinity();
        Float result = carlsonRF(Float(0), Float(1) - std::move(k2), Float(1), wp);
        finalizeResult(result, eff_k, precision);
        return result;
    }

    // E(k) = R_F(0, 1-k², 1) - (k²/3)·R_D(0, 1-k², 1) — complete elliptic integral of the second kind
    Float ellipticE(const Float& k, int precision) {
        Float::PrecisionScope _ps(precision + 64);   // Compute exact÷exact inside the function at working precision
        if (k.isNaN()) return Float::nan();
        {
            Float k2t = k * k;
            if (k2t == Float(1)) return Float(1);  // E(±1) = 1
        }
        int eff_k = k.effectiveBits();
        int wp = precision + 15;
        Float k2 = k * k; k2.truncateToApprox(wp);
        Float kp2 = Float(1) - k2; kp2.truncateToApprox(wp);
        Float rf = carlsonRF(Float(0), kp2, Float(1), wp);
        Float rd = carlsonRD(Float(0), kp2, Float(1), wp);
        Float result = rf - k2 * rd / 3;
        finalizeResult(result, eff_k, precision);
        return result;
    }

    Float ellipticE(Float&& k, int precision) {
        Float::PrecisionScope _ps(precision + 64);   // Compute exact÷exact inside the function at working precision
        if (k.isNaN()) return Float::nan();
        {
            Float k2t = k * k;
            if (k2t == Float(1)) return Float(1);
        }
        int eff_k = k.effectiveBits();
        int wp = precision + 15;
        Float k2 = k * k; k2.truncateToApprox(wp);
        Float kp2 = Float(1) - k2; kp2.truncateToApprox(wp);
        Float rf = carlsonRF(Float(0), kp2, Float(1), wp);
        Float rd = carlsonRD(Float(0), std::move(kp2), Float(1), wp);
        Float result = rf - std::move(k2) * rd / 3;
        finalizeResult(result, eff_k, precision);
        return result;
    }

    // Π(n, k) = R_F(0, 1-k², 1) + (n/3)·R_J(0, 1-k², 1, 1-n) — complete elliptic integral of the third kind
    Float ellipticPi(const Float& n, const Float& k, int precision) {
        if (n.isNaN() || k.isNaN()) return Float::nan();
        int eff = std::min(n.effectiveBits(), k.effectiveBits());
        int wp = precision + 15;
        Float k2 = k * k; k2.truncateToApprox(wp);
        Float kp2 = Float(1) - k2; kp2.truncateToApprox(wp);
        Float rf = carlsonRF(Float(0), kp2, Float(1), wp);
        Float rj = carlsonRJ(Float(0), kp2, Float(1), Float(1) - n, wp);
        Float result = rf + n * rj / 3;
        finalizeResult(result, eff, precision);
        return result;
    }

    Float ellipticPi(Float&& n, Float&& k, int precision) {
        if (n.isNaN() || k.isNaN()) return Float::nan();
        int eff = std::min(n.effectiveBits(), k.effectiveBits());
        int wp = precision + 15;
        Float k2 = k * k; k2.truncateToApprox(wp);
        Float kp2 = Float(1) - k2; kp2.truncateToApprox(wp);
        Float rf = carlsonRF(Float(0), kp2, Float(1), wp);
        Float rj = carlsonRJ(Float(0), std::move(kp2), Float(1), Float(1) - n, wp);
        Float result = rf + std::move(n) * rj / 3;
        finalizeResult(result, eff, precision);
        return result;
    }

    // F(φ, k) = sin(φ)·R_F(cos²φ, 1-k²sin²φ, 1) — incomplete elliptic integral of the first kind
    Float ellipticF(const Float& phi, const Float& k, int precision) {
        if (phi.isNaN() || k.isNaN()) return Float::nan();
        int eff = std::min(phi.effectiveBits(), k.effectiveBits());
        int wp = precision + 15;
        Float s = sin(phi, wp);
        Float c = cos(phi, wp);
        Float k2 = k * k; k2.truncateToApprox(wp);
        Float result = s * carlsonRF(c * c, Float(1) - k2 * s * s, Float(1), wp);
        finalizeResult(result, eff, precision);
        return result;
    }

    Float ellipticF(Float&& phi, Float&& k, int precision) {
        if (phi.isNaN() || k.isNaN()) return Float::nan();
        int eff = std::min(phi.effectiveBits(), k.effectiveBits());
        int wp = precision + 15;
        Float s = sin(phi, wp);
        Float c = cos(std::move(phi), wp);
        Float k2 = k * k; k2.truncateToApprox(wp);
        Float result = s * carlsonRF(c * c, Float(1) - std::move(k2) * s * s, Float(1), wp);
        finalizeResult(result, eff, precision);
        return result;
    }

    // E(φ, k) = sin(φ)·R_F(...) - (k²/3)sin³φ·R_D(...) — incomplete elliptic integral of the second kind
    Float ellipticE(const Float& phi, const Float& k, int precision) {
        if (phi.isNaN() || k.isNaN()) return Float::nan();
        int eff = std::min(phi.effectiveBits(), k.effectiveBits());
        int wp = precision + 15;
        Float s = sin(phi, wp);
        Float c = cos(phi, wp);
        Float k2 = k * k; k2.truncateToApprox(wp);
        Float c2 = c * c; c2.truncateToApprox(wp);
        Float s2 = s * s; s2.truncateToApprox(wp);
        Float w = Float(1) - k2 * s2; w.truncateToApprox(wp);
        Float rf = carlsonRF(c2, w, Float(1), wp);
        Float rd = carlsonRD(c2, w, Float(1), wp);
        Float result = s * rf - k2 * s * s2 * rd / 3;
        finalizeResult(result, eff, precision);
        return result;
    }

    Float ellipticE(Float&& phi, Float&& k, int precision) {
        if (phi.isNaN() || k.isNaN()) return Float::nan();
        int eff = std::min(phi.effectiveBits(), k.effectiveBits());
        int wp = precision + 15;
        Float s = sin(phi, wp);
        Float c = cos(std::move(phi), wp);
        Float k2 = k * k; k2.truncateToApprox(wp);
        Float c2 = c * c; c2.truncateToApprox(wp);
        Float s2 = s * s; s2.truncateToApprox(wp);
        Float w = Float(1) - k2 * s2; w.truncateToApprox(wp);
        Float rf = carlsonRF(c2, w, Float(1), wp);
        Float rd = carlsonRD(std::move(c2), std::move(w), Float(1), wp);
        Float result = s * rf - std::move(k2) * s * s2 * rd / 3;
        finalizeResult(result, eff, precision);
        return result;
    }

    // Π(n, φ, k) — incomplete elliptic integral of the third kind
    Float ellipticPi(const Float& n, const Float& phi, const Float& k, int precision) {
        if (n.isNaN() || phi.isNaN() || k.isNaN()) return Float::nan();
        int eff = std::min({n.effectiveBits(), phi.effectiveBits(), k.effectiveBits()});
        int wp = precision + 15;
        Float s = sin(phi, wp);
        Float c = cos(phi, wp);
        Float k2 = k * k; k2.truncateToApprox(wp);
        Float s2 = s * s; s2.truncateToApprox(wp);
        Float c2 = c * c; c2.truncateToApprox(wp);
        Float w = Float(1) - k2 * s2; w.truncateToApprox(wp);
        Float rf = carlsonRF(c2, w, Float(1), wp);
        Float rj = carlsonRJ(c2, w, Float(1), Float(1) - n * s2, wp);
        Float result = s * rf + n * s * s2 * rj / 3;
        finalizeResult(result, eff, precision);
        return result;
    }

    Float ellipticPi(Float&& n, Float&& phi, Float&& k, int precision) {
        if (n.isNaN() || phi.isNaN() || k.isNaN()) return Float::nan();
        int eff = std::min({n.effectiveBits(), phi.effectiveBits(), k.effectiveBits()});
        int wp = precision + 15;
        Float s = sin(phi, wp);
        Float c = cos(std::move(phi), wp);
        Float k2 = k * k; k2.truncateToApprox(wp);
        Float s2 = s * s; s2.truncateToApprox(wp);
        Float c2 = c * c; c2.truncateToApprox(wp);
        Float w = Float(1) - k2 * s2; w.truncateToApprox(wp);
        Float rf = carlsonRF(c2, w, Float(1), wp);
        Float rj = carlsonRJ(std::move(c2), std::move(w), Float(1), Float(1) - n * s2, wp);
        Float result = s * rf + std::move(n) * s * s2 * rj / 3;
        finalizeResult(result, eff, precision);
        return result;
    }

    //=============================================================================
    // Jacobi elliptic functions — descending AGM Landen transformation
    //=============================================================================

    // Common helper: compute the amplitude am(u, k) = φ₀ via AGM + inverse transformation
    static Float jacobiAmplitude(const Float& u, const Float& k, int wp) {
        // k = 0: am(u, 0) = u
        if (k.isZero()) { Float r = u; r.truncateToApprox(wp); return r; }
        // k = 1: am(u, 1) = gd(u) = 2·arctan(tanh(u/2))
        if (k == Float(1)) {
            Float r = ldexp(atan(tanh(ldexp(u, -1), wp), wp), 1);
            r.truncateToApprox(wp);
            return r;
        }

        Float kp = sqrt(Float(1) - k * k, wp);  // k' = √(1-k²)

        // AGM forward iteration: a₀=1, b₀=k', c₀=k
        std::vector<Float> a_seq, c_seq;
        Float a(1), b = kp, c = k;
        a.truncateToApprox(wp); b.truncateToApprox(wp); c.truncateToApprox(wp);
        a_seq.push_back(a);
        c_seq.push_back(c);

        for (int n = 0; n < 200; n++) {
            Float a_new = ldexp(a + b, -1); a_new.truncateToApprox(wp);
            Float c_new = ldexp(a - b, -1); c_new.truncateToApprox(wp);
            Float b_new = sqrt(a * b, wp);
            a = a_new; b = b_new; c = c_new;
            a_seq.push_back(a);
            c_seq.push_back(c);
            if (c.isZero()) break;
            int64_t c_bits = c.exponent() + static_cast<int64_t>(c.mantissa().bitLength());
            if (c_bits < -(Float::precisionToBits(wp) + 5)) break;
        }

        int N = static_cast<int>(a_seq.size()) - 1;

        // φ_N = 2^N · a_N · u
        Float phi = u * a_seq[N];
        for (int i = 0; i < N; i++) {
            phi = ldexp(phi, 1);
        }
        phi.truncateToApprox(wp);

        // Inverse transformation: φ_{n-1} = (φ_n + arcsin(c_n/a_n · sin(φ_n))) / 2
        for (int n = N; n >= 1; n--) {
            Float sinphi = sin(phi, wp);
            Float arg = c_seq[n] * sinphi / a_seq[n];
            arg.truncateToApprox(wp);
            phi = ldexp(phi + asin(arg, wp), -1);
            phi.truncateToApprox(wp);
        }

        return phi;
    }

    Float jacobiSn(const Float& u, const Float& k, int precision) {
        if (u.isNaN() || k.isNaN()) return Float::nan();
        if (u.isZero()) return Float(0);
        int eff = std::min(u.effectiveBits(), k.effectiveBits());
        int wp = precision + 20;
        Float phi = jacobiAmplitude(u, k, wp);
        Float result = sin(phi, wp);
        finalizeResult(result, eff, precision);
        return result;
    }

    Float jacobiSn(Float&& u, Float&& k, int precision) {
        if (u.isNaN() || k.isNaN()) return Float::nan();
        if (u.isZero()) return Float(0);
        int eff = std::min(u.effectiveBits(), k.effectiveBits());
        int wp = precision + 20;
        Float phi = jacobiAmplitude(u, std::move(k), wp);
        Float result = sin(std::move(phi), wp);
        finalizeResult(result, eff, precision);
        return result;
    }

    Float jacobiCn(const Float& u, const Float& k, int precision) {
        if (u.isNaN() || k.isNaN()) return Float::nan();
        if (u.isZero()) return Float(1);
        int eff = std::min(u.effectiveBits(), k.effectiveBits());
        int wp = precision + 20;
        Float phi = jacobiAmplitude(u, k, wp);
        Float result = cos(phi, wp);
        finalizeResult(result, eff, precision);
        return result;
    }

    Float jacobiCn(Float&& u, Float&& k, int precision) {
        if (u.isNaN() || k.isNaN()) return Float::nan();
        if (u.isZero()) return Float(1);
        int eff = std::min(u.effectiveBits(), k.effectiveBits());
        int wp = precision + 20;
        Float phi = jacobiAmplitude(u, std::move(k), wp);
        Float result = cos(std::move(phi), wp);
        finalizeResult(result, eff, precision);
        return result;
    }

    Float jacobiDn(const Float& u, const Float& k, int precision) {
        if (u.isNaN() || k.isNaN()) return Float::nan();
        if (u.isZero()) return Float(1);
        int eff = std::min(u.effectiveBits(), k.effectiveBits());
        int wp = precision + 20;
        Float phi = jacobiAmplitude(u, k, wp);
        Float sn = sin(phi, wp);
        Float k2 = k * k; k2.truncateToApprox(wp);
        Float result = sqrt(Float(1) - k2 * sn * sn, wp);
        finalizeResult(result, eff, precision);
        return result;
    }

    Float jacobiDn(Float&& u, Float&& k, int precision) {
        if (u.isNaN() || k.isNaN()) return Float::nan();
        if (u.isZero()) return Float(1);
        int eff = std::min(u.effectiveBits(), k.effectiveBits());
        int wp = precision + 20;
        Float phi = jacobiAmplitude(u, k, wp);
        Float sn = sin(std::move(phi), wp);
        Float k2 = k * k; k2.truncateToApprox(wp);
        Float result = sqrt(Float(1) - std::move(k2) * sn * sn, wp);
        finalizeResult(result, eff, precision);
        return result;
    }

    //=============================================================================
    // Zeta function ζ(s) — Borwein algorithm
    //=============================================================================
    // Borwein's (1995) accelerated alternating series:
    // ζ(s) = [1/(1-2^{1-s})] · [(-1)/(d_n)] · Σ_{k=0}^{n-1} (-1)^k (d_k - d_n) / (k+1)^s
    // where d_k = n · Σ_{i=0}^{k} (n+i-1)! · 4^i / ((n-i)! · (2i)!)

    Float zeta(const Float& s, int precision) {
        Float::PrecisionScope _ps(precision + 64);   // Compute exact÷exact inside the function at working precision
        if (s.isNaN()) return Float::nan();
        if (s.isInfinity()) {
            if (s.isNegative()) return Float::nan();
            return Float(1);  // ζ(+∞) = 1
        }

        int eff_s = s.effectiveBits();
        int wp = precision + 40;

        // s = 1 is a pole
        Float one(1);
        if (s == one) return Float::nan();

        // Even negative integer: ζ(-2n) = 0 (trivial zero)
        if (s.isNegative() && s.isInteger()) {
            double sv = s.toDouble();
            int si = static_cast<int>(sv);
            if (si % 2 == 0 && si < 0) return Float(0);
        }

        // s < 0: reflection formula ζ(s) = 2^s · π^{s-1} · sin(πs/2) · Γ(1-s) · ζ(1-s)
        if (s.isNegative()) {
            Float one_minus_s = Float(1) - s;
            Float z_1ms = zeta(one_minus_s, wp);
            Float g_1ms = gamma(one_minus_s, wp);
            Float two_pow_s = pow(Float(2), s, wp);
            Float pi_pow_sm1 = pow(Float::pi(wp), s - Float(1), wp);
            Float sin_term = sinPi(ldexp(s, -1), wp);
            Float result = two_pow_s * pi_pow_sm1 * sin_term * g_1ms * z_1ms;
            finalizeResult(result, eff_s, precision);
            return result;
        }

        // s > 0, s ≠ 1: computation via the Dirichlet eta function with Euler acceleration
        // η(s) = (1-2^{1-s})·ζ(s) = Σ (-1)^{k-1}/k^s, computed via the Euler transform
        // Partial sums of η(s): S_K = Σ_{k=1}^{K} (-1)^{k-1}/k^s
        // Euler transform: E_n = Σ_{k=0}^{n} C(n,k)/2^n · S_{k+1}

        // Term count: the Euler transform error is ~2^{-n} (1 bit/term), so the term count is taken in bits.
        // BUGFIX (2026-05-30): n = wp·1.05 misused decimal digits as a bit term count, capping at ~0.3·P digits
        // → wp_bits·1.05 (unit-mixing pattern C).
        int n = static_cast<int>(Float::precisionToBits(wp) * 1.05) + 5;

        // Euler transform while computing the partial sums
        // BUGFIX (2026-05-30): for exact inputs (s=3 etc.), Float(1)/pow(kp1,s) etc. fall to
        // exact/exact poison, so make s_copy non-exact.
        Float eta(0);
        Float s_copy = s;
        s_copy.truncateToApprox(wp);
        s_copy.setEffectiveBits(Float::precisionToBits(wp));

        // S_k = partial sum, w_k = C(n,k)/2^n (Euler weight)
        Float partial_sum(0);
        Float weighted_sum(0);

        // w_0 = 1/2^n, w_{k+1} = w_k · (n-k)/(k+1)
        // 2^n is too large, so handle in log scale or compute directly with Float
        Float w = ldexp(Float(1), -n);  // w_0 = 2^{-n}
        w.truncateToApprox(wp);
        w.setEffectiveBits(Float::precisionToBits(wp));  // avoid exact poison of /Float(k+1)

        for (int k = 0; k <= n; k++) {
            // Partial sum update: S_{k+1} = S_k + (-1)^k / (k+1)^s
            Float kp1 = Float(k + 1);
            Float term_k = Float(1) / pow(kp1, s_copy, wp);
            if (k % 2 == 0) {
                partial_sum = partial_sum + term_k;
            } else {
                partial_sum = partial_sum - term_k;
            }
            partial_sum.truncateToApprox(wp);

            // Weighted sum
            weighted_sum = weighted_sum + w * partial_sum;
            weighted_sum.truncateToApprox(wp);

            // Weight update: w_{k+1} = w_k · (n-k) / (k+1)
            if (k < n) {
                w = w * Float(n - k) / Float(k + 1);
                w.truncateToApprox(wp);
            }
        }

        eta = weighted_sum;

        // ζ(s) = η(s) / (1 - 2^{1-s})
        Float two_pow_1ms = pow(Float(2), Float(1) - s_copy, wp);
        Float denom = Float(1) - two_pow_1ms;

        if (denom.isZero()) {
            // Case s = 1 (already checked, but just in case)
            return Float::nan();
        }

        Float result = eta / denom;
        finalizeResult(result, eff_s, precision);
        return result;
    }

    Float zeta(Float&& s, int precision) {
        return zeta(static_cast<const Float&>(s), precision);
    }

    //=============================================================================
    // Hurwitz zeta function ζ(s, a) — Euler-Maclaurin formula
    //=============================================================================
    // ζ(s, a) = Σ_{k=0}^{N-1} (a+k)^{-s} + (a+N)^{1-s}/(s-1) + (a+N)^{-s}/2
    //         + Σ_{j=1}^{M} B_{2j}/(2j)! · s(s+1)···(s+2j-2) · (a+N)^{-(s+2j-1)}
    //
    // The Bernoulli numbers B_{2j} are computed recursively at Float precision:
    // B(2m) = -1/(2m+1) · [1 + (2m+1)·(-1/2) + Σ_{j=1}^{m-1} C(2m+1,2j)·B(2j)]

    Float hurwitzZeta(const Float& s, const Float& a, int precision) {
        if (s.isNaN() || a.isNaN()) return Float::nan();
        if (a.isZero() || a.isNegative()) return Float::nan();  // a > 0 is required

        int eff = std::min(s.effectiveBits(), a.effectiveBits());

        // s = 1 is a pole
        Float one(1);
        if (s == one) return Float::nan();

        // a = 1 → delegate to Riemann ζ(s)
        if (a == one) return zeta(s, precision);

        int wp = precision + 40;
        const int wp_bits = Float::precisionToBits(wp);
        // BUGFIX (2026-05-30): for exact inputs (s=3,a=0.5), pow/division become exact/exact poison.
        Float sw = s;
        sw.truncateToApprox(wp);
        sw.setEffectiveBits(wp_bits);
        Float aw = a;
        aw.truncateToApprox(wp);
        aw.setEffectiveBits(wp_bits);

        // Argument shift: choose N so that a+N becomes large enough.
        // The minimum attainable Euler-Maclaurin error is ~e^{-2π(a+N)}. To drive this below 10^{-wp},
        // a+N ≳ wp/(2π·log10 e) ≈ 0.366·wp is required. BUGFIX (2026-05-30):
        // the old wp/3 (=0.333·wp) was slightly too small → wp·0.4+5 for some margin.
        int target_aN = static_cast<int>(wp * 0.4) + 5;
        int N = std::max(1, target_aN - static_cast<int>(aw.toDouble()));

        // Direct sum: Σ_{k=0}^{N-1} (a+k)^{-s}
        Float direct_sum(0);
        for (int k = 0; k < N; k++) {
            Float term = pow(aw + Float(k), -sw, wp);
            term.truncateToApprox(wp);
            direct_sum = direct_sum + term;
            direct_sum.truncateToApprox(wp);
        }

        Float aN = aw + Float(N);
        aN.truncateToApprox(wp);

        // Integral term: (a+N)^{1-s} / (s-1)
        Float integral_term = pow(aN, Float(1) - sw, wp) / (sw - Float(1));
        integral_term.truncateToApprox(wp);

        // Midpoint correction: (1/2)·(a+N)^{-s}
        Float midpoint = ldexp(Float(1) / pow(aN, sw, wp), -1);
        midpoint.truncateToApprox(wp);

        Float sum = direct_sum + integral_term + midpoint;
        sum.truncateToApprox(wp);

        // Number of Bernoulli correction terms: optimal truncation M* ≈ π·(a+N). Since convergence/divergence
        // detection stops early, provide the optimal point as an upper bound. BUGFIX (2026-05-30): the old wp/5+5
        // truncated far short of the optimal point (~π·target_aN), capping at ~0.3·P digits
        // (unit-mixing family: the term count was too small relative to the bit budget wp_bits).
        constexpr double PI_VAL = 3.141592653589793238462643383279502884;
        int M = static_cast<int>(PI_VAL * static_cast<double>(target_aN)) + 10;

        // Bernoulli numbers B_{2k} (k=1..M). Computed with the same validated routine as polygamma/trigamma.
        // BUGFIX (2026-05-30): the former inline recurrence broke beyond B_10 due to Float/int division
        // of the binomial coefficients (divScalarF only integer-divides the mantissa without extending precision)
        // and insufficient cancellation guard, capping hurwitzZeta at ~26 digits. Resolved by delegating to
        // computeBernoulliNumbers (result[k]=B_{2k}). bern[k] = B_{2k} (k=0..M).
        auto bern = computeBernoulliNumbers(M, wp);

        // Euler-Maclaurin Bernoulli correction
        Float rising = sw;  // Rising factorial: s, s(s+1)(s+2), ...
        Float aN_inv = Float(1) / aN;
        aN_inv.truncateToApprox(wp);
        Float aN_pow = pow(aN, -sw, wp) * aN_inv;  // (a+N)^{-(s+1)}
        aN_pow.truncateToApprox(wp);

        int64_t prev_ct = (std::numeric_limits<int64_t>::max)();
        for (int j = 0; j < M; j++) {
            // Compute (2(j+1))!
            Float fact(1);
            for (int i = 1; i <= 2 * (j + 1); i++) {
                fact = fact * i;
            }
            fact.truncateToApprox(wp);

            Float correction = bern[j + 1] / fact * rising * aN_pow;  // B_{2(j+1)}
            correction.truncateToApprox(wp);

            int64_t ct = correction.isZero()
                ? (std::numeric_limits<int64_t>::min)()
                : correction.exponent() + static_cast<int64_t>(correction.mantissa().bitLength());

            // Divergence detection: stop just before passing the smallest term of the asymptotic series (correction starts increasing).
            if (j >= 3 && ct > prev_ct) break;

            sum = sum + correction;
            sum.truncateToApprox(wp);
            prev_ct = ct;

            // Convergence check
            if (j >= 3 && !correction.isZero()) {
                auto st = sum.exponent() + static_cast<int64_t>(sum.mantissa().bitLength());
                if (st - ct > Float::precisionToBits(wp) + 5) break;
            }

            // Rising factorial: multiply by (s+2j-1)(s+2j)
            rising = rising * (sw + Float(2 * j + 1)) * (sw + Float(2 * (j + 1)));
            rising.truncateToApprox(wp);
            // (a+N) power: multiply by (a+N)^{-2}
            aN_pow = aN_pow * aN_inv * aN_inv;
            aN_pow.truncateToApprox(wp);
        }

        finalizeResult(sum, eff, precision);
        return sum;
    }

    Float hurwitzZeta(Float&& s, Float&& a, int precision) {
        return hurwitzZeta(static_cast<const Float&>(s), static_cast<const Float&>(a), precision);
    }

    //=============================================================================
    // Dirichlet η function: η(s) = (1 - 2^{1-s}) · ζ(s)
    //=============================================================================

    Float dirichletEta(const Float& s, int precision) {
        if (s.isNaN()) return Float::nan();

        // η(1) = ln(2) (avoids the pole of ζ(1))
        Float one(1);
        if (s == one) {
            return log(Float(2), precision);
        }

        int eff_s = s.effectiveBits();
        int wp = precision + 20;
        Float sw = s;
        sw.truncateToApprox(wp);

        Float factor = Float(1) - pow(Float(2), Float(1) - sw, wp);
        factor.truncateToApprox(wp);

        Float z = zeta(sw, wp);
        Float result = factor * z;
        finalizeResult(result, eff_s, precision);
        return result;
    }

    Float dirichletEta(Float&& s, int precision) {
        if (s.isNaN()) return Float::nan();

        Float one(1);
        if (s == one) {
            return log(Float(2), precision);
        }

        int eff_s = s.effectiveBits();
        int wp = precision + 20;
        s.truncateToApprox(wp);

        Float factor = Float(1) - pow(Float(2), Float(1) - s, wp);
        factor.truncateToApprox(wp);

        Float z = zeta(std::move(s), wp);
        Float result = std::move(factor) * z;
        finalizeResult(result, eff_s, precision);
        return result;
    }

    //=============================================================================
    // Exponential integral Ei(x) and logarithmic integral li(x)
    //=============================================================================

    // Ei(x) = -PV∫_{-x}^{∞} e^{-t}/t dt = γ + ln|x| + Σ_{n=1}^{∞} x^n/(n·n!)
    // Convergent series for x > 0
    // Body: takes x by value (assumes already moved)
    static Float expint_core(Float x, int eff_x, int precision) {
        Float::PrecisionScope _ps(precision + 64);   // Compute exact÷exact inside the function at working precision
        int wp = precision + 20;
        // If x stays exact (effective_bits = INT_MAX), the later term*x/n becomes an operation between
        // two INT_MAX values, triggering a defaultPrecision (~53 digit) fallback.
        // Pin effective_bits explicitly to wp via setResultPrecision.
        x.setResultPrecision(wp);

        // Ei(x) = γ + ln|x| + Σ_{n=1}^{∞} x^n / (n · n!)
        Float euler_gamma = Float::euler(wp);
        Float ln_abs_x = log(abs(x), wp);

        Float sum(0);
        Float term = x;  // n=1: x^1 / (1·1!) = x
        sum = term;

        // Convergence threshold: wp is in decimal digits, so convert to bits for comparison.
        // The old code compared wp+5 directly against the bit difference, terminating early at ~46 digits.
        const int64_t convergence_bits = static_cast<int64_t>(Float::precisionToBits(wp)) + 5;

        for (int n = 2; n < 10 * wp; n++) {
            term = term * x / n;
            term.truncateToApprox(wp);
            Float contribution = term / n;

            sum = sum + contribution;
            sum.truncateToApprox(wp);

            // Convergence check
            if (n >= 5 && contribution.isZero()) break;
            if (n >= 5) {
                auto c_bits = contribution.exponent() + static_cast<int64_t>(contribution.mantissa().bitLength());
                auto s_bits = sum.exponent() + static_cast<int64_t>(sum.mantissa().bitLength());
                if (s_bits - c_bits > convergence_bits) break;
            }
        }

        Float result = euler_gamma + ln_abs_x + sum;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float expint(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) return Float(0);
            return Float::positiveInfinity();
        }
        if (x.isZero()) return Float::negativeInfinity();
        return expint_core(Float(x), x.effectiveBits(), precision);
    }

    Float expint(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) return Float(0);
            return Float::positiveInfinity();
        }
        if (x.isZero()) return Float::negativeInfinity();
        int eff = x.effectiveBits();
        return expint_core(std::move(x), eff, precision);
    }

    // li(x) = Ei(ln(x)) — logarithmic integral
    Float li(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::positiveInfinity();
        if (x.isZero() || x.isNegative()) return Float::nan();

        // li(1) = -∞ (Ei(0) = -∞)
        if (x == Float(1)) return Float::negativeInfinity();

        int eff_x = x.effectiveBits();
        int wp = precision + 10;
        Float ln_x = log(x, wp);
        Float result = expint(ln_x, wp);
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float li(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float::positiveInfinity();
        if (x.isZero() || x.isNegative()) return Float::nan();
        if (x == Float(1)) return Float::negativeInfinity();

        int eff_x = x.effectiveBits();
        int wp = precision + 10;
        Float ln_x = log(std::move(x), wp);
        Float result = expint(std::move(ln_x), wp);
        finalizeResult(result, eff_x, precision);
        return result;
    }

    //=============================================================================
    // Dilogarithm Li₂(x) = -∫₀ˣ ln(1-t)/t dt = Σ_{n=1}^{∞} x^n/n²
    //=============================================================================

    // Body: takes x by value (assumes already moved)
    static Float dilog_core(Float x, int eff_x, int precision) {
        int wp = precision + 20;
        // If x stays exact (effective_bits = INT_MAX), later operations trigger a
        // defaultPrecision fallback, so pin effective to wp
        x.setResultPrecision(wp);

        // Special values
        if (x.isZero()) return Float(0);

        // Li₂(1) = π²/6
        if (x == Float(1)) {
            Float pi = Float::pi(wp);
            Float result = pi * pi / 6;
            finalizeResult(result, eff_x, precision);
            return result;
        }

        // Li₂(-1) = -π²/12
        if (x == Float(-1)) {
            Float pi = Float::pi(wp);
            Float result = -(pi * pi) / 12;
            finalizeResult(result, eff_x, precision);
            return result;
        }

        double x_approx = std::abs(x.toDouble());

        // x < -1: inversion formula Li₂(x) = -Li₂(1/x) - π²/6 - ½·ln²(-x).
        //   1/x ∈ (-1,0) can be computed via the existing convergent route, and log is real since -x > 1 > 0.
        //   (x > 1 makes Li₂ complex-valued, outside the scope of the real dilog, so it is not handled here.
        //    Previously |x|>1 passed through and the Taylor series diverged, returning a huge = wrong value.)
        if (x_approx > 1.0 && x.isNegative()) {
            Float inv_x = Float(1) / x;          // ∈ (-1, 0)
            inv_x.setResultPrecision(wp);
            Float li2_inv = dilog(inv_x, wp);
            Float pi = Float::pi(wp);
            Float neg_x = -x;                    // > 1
            Float ln_negx = log(neg_x, wp);
            Float result = -li2_inv - pi * pi / 6 - ldexp(ln_negx * ln_negx, -1);
            finalizeResult(result, eff_x, precision);
            return result;
        }

        // |x| > 0.5: transformation Li₂(x) = -Li₂(1-x) + π²/6 - ln(x)·ln(1-x)
        if (x_approx > 0.5 && x_approx <= 1.0) {
            Float one_minus_x = Float(1) - x;
            one_minus_x.truncateToApprox(wp);

            Float li2_1mx = dilog(one_minus_x, wp);
            Float pi = Float::pi(wp);
            Float ln_x = log(x, wp);
            Float ln_1mx = log(one_minus_x, wp);

            Float result = -li2_1mx + pi * pi / 6 - ln_x * ln_1mx;
            finalizeResult(result, eff_x, precision);
            return result;
        }

        // |x| <= 0.5: direct Taylor series Li₂(x) = Σ_{n=1}^{∞} x^n / n²
        Float sum(0);
        Float x_power = x;  // x^1
        const int64_t convergence_bits = static_cast<int64_t>(Float::precisionToBits(wp)) + 5;

        for (int n = 1; n < 10 * wp; n++) {
            Float term = x_power / (static_cast<int64_t>(n) * n);
            sum = sum + term;
            sum.truncateToApprox(wp);

            // Convergence check (wp is in decimal digits, converted to bits for comparison)
            if (n >= 5 && term.isZero()) break;
            if (n >= 5) {
                auto t_bits = term.exponent() + static_cast<int64_t>(term.mantissa().bitLength());
                auto s_bits = sum.exponent() + static_cast<int64_t>(sum.mantissa().bitLength());
                if (s_bits - t_bits > convergence_bits) break;
            }

            x_power = x_power * x;
            x_power.truncateToApprox(wp);
        }

        finalizeResult(sum, eff_x, precision);
        return sum;
    }

    Float dilog(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        return dilog_core(Float(x), x.effectiveBits(), precision);
    }

    Float dilog(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        int eff = x.effectiveBits();
        return dilog_core(std::move(x), eff, precision);
    }

    //=============================================================================
    // Bessel functions J_n(x), Y_n(x)
    //=============================================================================
    // J_n(x) = Σ_{m=0}^{∞} (-1)^m / (m! · (m+n)!) · (x/2)^{2m+n}
    // Y_n(x) = [J_n(x)·cos(nπ) - J_{-n}(x)] / sin(nπ)  (a limit when n is an integer)

    Float besselJ(int n, const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float(0);  // J_n(±∞) → 0 (oscillatory decay)

        int eff_x = x.effectiveBits();
        int wp = precision + 20;
        const int wp_bits = Float::precisionToBits(wp);
        Float xw = x;
        xw.truncateToApprox(wp);
        xw.setEffectiveBits(wp_bits);  // avoid exact/exact poison of exact inputs

        // J_n(0) = δ_{n,0}
        if (x.isZero()) {
            return (n == 0) ? Float(1) : Float(0);
        }

        // Negative order: J_{-n}(x) = (-1)^n · J_n(x)
        bool negate = false;
        int abs_n = n;
        if (n < 0) {
            abs_n = -n;
            if (abs_n % 2 != 0) negate = true;
        }

        // Taylor series: J_n(x) = (x/2)^n · Σ_{m=0}^{∞} (-1)^m · (x/2)^{2m} / (m! · (m+n)!)
        Float x_half = ldexp(xw, -1);
        x_half.truncateToApprox(wp);
        Float x_half_sq = x_half * x_half;
        x_half_sq.truncateToApprox(wp);
        Float neg_x_half_sq = -x_half_sq;

        // Compute (x/2)^n
        Float x_half_n(1);
        for (int i = 0; i < abs_n; i++) {
            FloatOps::mul(x_half_n, x_half, x_half_n);
        }
        x_half_n.truncateToApprox(wp);

        // Series: Σ (-1)^m · (x/2)^{2m} / (m! · (m+n)!)
        // term_0 = 1/n!
        Float term(1);
        term.setEffectiveBits(wp_bits);  // avoid exact/exact poison of 1/n!
        for (int i = 1; i <= abs_n; i++) {
            FloatOps::div(term, Float(i), term);
        }
        term.truncateToApprox(wp);
        Float sum = term;

        for (int m = 1; m < 10 * wp; m++) {
            // term_m = term_{m-1} · (-(x/2)²) / (m · (m+n))
            FloatOps::mul(term, neg_x_half_sq, term);
            FloatOps::div(term, Float(static_cast<int64_t>(m) * (m + abs_n)), term);
            term.truncateToApprox(wp);
            FloatOps::add(sum, term, sum);
            sum.truncateToApprox(wp);

            if (m >= 3 && term.isZero()) break;
            if (m >= 3) {
                auto t_bits = term.exponent() + static_cast<int64_t>(term.mantissa().bitLength());
                auto s_bits = sum.exponent() + static_cast<int64_t>(sum.mantissa().bitLength());
                if (s_bits - t_bits > Float::precisionToBits(wp) + 5) break;
            }
        }

        Float result = x_half_n * sum;
        if (negate) result = -result;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float besselJ(int n, Float&& x, int precision) {
        return besselJ(n, static_cast<const Float&>(x), precision);
    }

    Float besselY(int n, const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float(0);  // Y_n(±∞) → 0
        if (x.isZero()) return Float::negativeInfinity();  // Y_n(0) = -∞
        if (x.isNegative()) return Float::nan();  // Y_n(x<0) is not real

        int eff_x = x.effectiveBits();
        int wp = precision + 30;
        // BUGFIX (2026-05-30): for exact inputs (x=1), exact/exact divisions like (x/2) powers, 1/n!,
        // and harmonic numbers 1/j fall to default_precision. Make the working values and unit constants non-exact.
        const int wp_bits = Float::precisionToBits(wp);
        Float xw = x;
        xw.truncateToApprox(wp);
        xw.setEffectiveBits(wp_bits);
        Float one_w(1);
        one_w.setEffectiveBits(wp_bits);  // Non-exact dividend for 1/j, 1/n!, etc.

        int abs_n = (n >= 0) ? n : -n;

        // Exact formula (Abramowitz & Stegun 9.1.11):
        // Y_n(x) = (2/π)·J_n(x)·(γ + ln(x/2))
        //        - (1/π)·Σ_{k=0}^{n-1} (n-k-1)!/k! · (x/2)^{2k-n}
        //        - (1/π)·Σ_{k=0}^{∞} (-1)^k·(ψ(k+1)+ψ(k+n+1))/(k!·(k+n)!) · (x/2)^{2k+n}
        // ψ(m+1) = -γ + H_m  (H_m = Σ 1/j)
        // → combining:
        // Y_n(x) = (2/π)·J_n(x)·ln(x/2)
        //        - (1/π)·(x/2)^{-n}·Σ_{k=0}^{n-1} (n-k-1)!/k! · (x²/4)^k
        //        - (1/π)·(x/2)^n·Σ_{k=0}^{∞} (-1)^k·(H_k+H_{k+n})/(k!·(k+n)!)·(x²/4)^k

        Float pi_val = Float::pi(wp);
        Float euler_gamma = Float::euler(wp);
        Float two_over_pi = Float(2) / pi_val;
        Float one_over_pi = Float(1) / pi_val;
        Float x_half = ldexp(xw, -1);
        x_half.truncateToApprox(wp);
        Float ln_x_half = log(x_half, wp);
        Float x_half_sq = x_half * x_half;
        x_half_sq.truncateToApprox(wp);

        Float jn = besselJ(abs_n, xw, wp);

        // Part A: (2/π) · J_n(x) · (γ + ln(x/2))   [DLMF 10.8.1]
        Float result = two_over_pi * jn * (euler_gamma + ln_x_half);
        result.truncateToApprox(wp);

        // Part B: -(1/π)·(x/2)^{-n}·Σ_{k=0}^{n-1} (n-k-1)!/k! · (x²/4)^k
        if (abs_n > 0) {
            Float x_half_neg_n(1);
            for (int i = 0; i < abs_n; i++) {
                FloatOps::div(x_half_neg_n, x_half, x_half_neg_n);
            }
            x_half_neg_n.truncateToApprox(wp);

            // k=0 term: (n-1)!/0! = (n-1)!
            Float bk_coeff(1);
            bk_coeff.setEffectiveBits(wp_bits);  // avoid exact poison of the recurrence /(k(n-k))
            for (int i = 1; i < abs_n; i++) {
                FloatOps::mul(bk_coeff, Float(i), bk_coeff);
            }
            bk_coeff.truncateToApprox(wp);
            Float partB = bk_coeff;

            for (int k = 1; k < abs_n; k++) {
                // Recurrence: coeff_k = coeff_{k-1} · (x²/4) / (k · (n-k))
                FloatOps::mul(bk_coeff, x_half_sq, bk_coeff);
                FloatOps::div(bk_coeff, Float(static_cast<int64_t>(k) * (abs_n - k)), bk_coeff);
                bk_coeff.truncateToApprox(wp);
                FloatOps::add(partB, bk_coeff, partB);
                partB.truncateToApprox(wp);
            }

            result = result - one_over_pi * x_half_neg_n * partB;
            result.truncateToApprox(wp);
        }

        // Part C: -(1/π)·(x/2)^n·Σ_{k=0}^{∞} (-1)^k·(H_k+H_{k+n})/(k!·(k+n)!)·(x²/4)^k
        Float x_half_n(1);
        for (int i = 0; i < abs_n; i++) {
            FloatOps::mul(x_half_n, x_half, x_half_n);
        }
        x_half_n.truncateToApprox(wp);

        Float H_k(0);       // H_0 = 0
        Float H_kn(0);      // H_n = Σ_{j=1}^{n} 1/j
        Float h_tmp(0);
        for (int j = 1; j <= abs_n; j++) {
            FloatOps::div(one_w, Float(j), h_tmp);
            FloatOps::add(H_kn, h_tmp, H_kn);
        }
        H_kn.truncateToApprox(wp);

        // k=0: coeff = 1/(0!·n!) = 1/n!, harmonic sum = H_0 + H_n = H_n
        Float ck_coeff(1);  // 1/(k!·(k+n)!) — managed by recurrence, including the sign (-1)^k
        ck_coeff.setEffectiveBits(wp_bits);  // avoid exact/exact poison of 1/n!
        for (int i = 1; i <= abs_n; i++) {
            FloatOps::div(ck_coeff, Float(i), ck_coeff);
        }
        ck_coeff.truncateToApprox(wp);

        Float neg_x_half_sq = -x_half_sq;
        Float partC = ck_coeff * (H_k + H_kn);
        partC.truncateToApprox(wp);
        Float term(0);

        for (int k = 1; k < 10 * wp; k++) {
            FloatOps::div(one_w, Float(k), h_tmp);
            FloatOps::add(H_k, h_tmp, H_k);
            FloatOps::div(one_w, Float(k + abs_n), h_tmp);
            FloatOps::add(H_kn, h_tmp, H_kn);

            // Recurrence: ck_coeff_{k} = ck_coeff_{k-1} · (-(x²/4)) / (k · (k+n))
            FloatOps::mul(ck_coeff, neg_x_half_sq, ck_coeff);
            FloatOps::div(ck_coeff, Float(static_cast<int64_t>(k) * (k + abs_n)), ck_coeff);
            ck_coeff.truncateToApprox(wp);

            FloatOps::mul(ck_coeff, H_k + H_kn, term);
            term.truncateToApprox(wp);
            FloatOps::add(partC, term, partC);
            partC.truncateToApprox(wp);

            if (k >= 5 && term.isZero()) break;
            if (k >= 5) {
                auto t_bits = term.exponent() + static_cast<int64_t>(term.mantissa().bitLength());
                auto s_bits = partC.exponent() + static_cast<int64_t>(partC.mantissa().bitLength());
                if (s_bits - t_bits > Float::precisionToBits(wp) + 5) break;
            }
        }

        result = result - one_over_pi * x_half_n * partC;

        // Negative order: Y_{-n}(x) = (-1)^n · Y_n(x)
        if (n < 0 && abs_n % 2 != 0) result = -result;

        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float besselY(int n, Float&& x, int precision) {
        return besselY(n, static_cast<const Float&>(x), precision);
    }

    //=============================================================================
    // Modified Bessel function of the first kind I_n(x)
    //=============================================================================
    // I_n(x) = Σ_{m=0}^{∞} (x/2)^{2m+n} / (m! · (m+n)!)
    // Same series as J_n but without the alternating sign (-1)^m (all terms positive)

    Float besselI(int n, const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) {
                return (std::abs(n) % 2 == 0) ? Float::positiveInfinity() : Float::negativeInfinity();
            }
            return Float::positiveInfinity();  // I_n(+∞) = +∞
        }

        int eff_x = x.effectiveBits();
        int wp = precision + 20;
        const int wp_bits = Float::precisionToBits(wp);
        Float xw = x;
        xw.truncateToApprox(wp);
        xw.setEffectiveBits(wp_bits);  // avoid exact/exact poison of exact inputs

        // I_n(0) = δ_{n,0}
        if (x.isZero()) {
            return (n == 0) ? Float(1) : Float(0);
        }

        // Negative order: I_{-n}(x) = I_n(x) (integer order)
        int abs_n = (n >= 0) ? n : -n;

        // I_n(-x) = (-1)^n · I_n(x)
        bool negate = false;
        if (xw.isNegative()) {
            xw = -xw;
            if (abs_n % 2 != 0) negate = true;
        }

        // Taylor series: I_n(x) = (x/2)^n · Σ_{m=0}^{∞} (x/2)^{2m} / (m! · (m+n)!)
        Float x_half = ldexp(xw, -1);
        x_half.truncateToApprox(wp);
        Float x_half_sq = x_half * x_half;
        x_half_sq.truncateToApprox(wp);

        // (x/2)^n
        Float x_half_n(1);
        for (int i = 0; i < abs_n; i++) {
            FloatOps::mul(x_half_n, x_half, x_half_n);
        }
        x_half_n.truncateToApprox(wp);

        // term_0 = 1/n!
        Float term(1);
        term.setEffectiveBits(wp_bits);  // avoid exact/exact poison of 1/n!
        for (int i = 1; i <= abs_n; i++) {
            FloatOps::div(term, Float(i), term);
        }
        term.truncateToApprox(wp);
        Float sum = term;

        for (int m = 1; m < 10 * wp; m++) {
            // term_m = term_{m-1} · (x/2)² / (m · (m+n)) — no sign flip
            FloatOps::mul(term, x_half_sq, term);
            FloatOps::div(term, Float(static_cast<int64_t>(m) * (m + abs_n)), term);
            term.truncateToApprox(wp);
            FloatOps::add(sum, term, sum);
            sum.truncateToApprox(wp);

            if (m >= 3 && term.isZero()) break;
            if (m >= 3) {
                auto t_bits = term.exponent() + static_cast<int64_t>(term.mantissa().bitLength());
                auto s_bits = sum.exponent() + static_cast<int64_t>(sum.mantissa().bitLength());
                if (s_bits - t_bits > Float::precisionToBits(wp) + 5) break;
            }
        }

        Float result = x_half_n * sum;
        if (negate) result = -result;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float besselI(int n, Float&& x, int precision) {
        return besselI(n, static_cast<const Float&>(x), precision);
    }

    //=============================================================================
    // Modified Bessel function of the second kind K_n(x)
    //=============================================================================
    // A&S 9.6.11 / DLMF 10.31.1 (integer order):
    // K_n(x) = (1/2)(x/2)^{-n} · Σ_{k=0}^{n-1} (-1)^k · (n-k-1)!/k! · (x²/4)^k
    //        + (-1)^{n+1} · ln(x/2) · I_n(x)
    //        + (-1)^n · (1/2)(x/2)^n · Σ_{k=0}^{∞} [ψ(k+1)+ψ(k+n+1)] / (k!·(k+n)!) · (x²/4)^k
    // ψ(m+1) = -γ + H_m

    Float besselK(int n, const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) return Float::nan();
            return Float(0);  // K_n(+∞) = 0 (exponential decay)
        }
        if (x.isZero()) return Float::positiveInfinity();  // K_n(0) = +∞
        if (x.isNegative()) return Float::nan();  // K_n(x<0) is not real

        int eff_x = x.effectiveBits();
        int wp = precision + 30;
        // BUGFIX (2026-05-30): avoid exact/exact poison of (x/2) powers, 1/n!, and harmonic numbers 1/j
        // for exact inputs (same form as besselY).
        const int wp_bits = Float::precisionToBits(wp);
        Float xw = x;
        xw.truncateToApprox(wp);
        xw.setEffectiveBits(wp_bits);
        Float one_w(1);
        one_w.setEffectiveBits(wp_bits);

        // K_{-n}(x) = K_n(x) (integer order)
        int abs_n = (n >= 0) ? n : -n;

        Float euler_gamma = Float::euler(wp);
        Float x_half = ldexp(xw, -1);
        x_half.truncateToApprox(wp);
        Float ln_x_half = log(x_half, wp);
        Float x_half_sq = x_half * x_half;
        x_half_sq.truncateToApprox(wp);

        Float in = besselI(abs_n, xw, wp);

        // Part A: (-1)^{n+1} · I_n(x) · ln(x/2)
        Float partA = in * ln_x_half;
        if (abs_n % 2 == 0) partA = -partA;  // (-1)^{n+1}
        partA.truncateToApprox(wp);

        // Part B: (1/2)·(x/2)^{-n} · Σ_{k=0}^{n-1} (-1)^k · (n-k-1)!/k! · (x²/4)^k
        Float partB(0);
        if (abs_n > 0) {
            Float x_half_neg_n(1);
            for (int i = 0; i < abs_n; i++) {
                FloatOps::div(x_half_neg_n, x_half, x_half_neg_n);
            }
            x_half_neg_n.truncateToApprox(wp);

            // k=0: (n-1)!/0! = (n-1)!
            Float bk_coeff(1);
            bk_coeff.setEffectiveBits(wp_bits);  // avoid exact poison of the recurrence /(k(n-k))
            Float neg_x_half_sq_k = -x_half_sq;
            for (int i = 1; i < abs_n; i++) {
                FloatOps::mul(bk_coeff, Float(i), bk_coeff);
            }
            bk_coeff.truncateToApprox(wp);
            Float sumB = bk_coeff;

            for (int k = 1; k < abs_n; k++) {
                // Recurrence: coeff_k = coeff_{k-1} · (-(x²/4)) / (k · (n-k))
                FloatOps::mul(bk_coeff, neg_x_half_sq_k, bk_coeff);
                FloatOps::div(bk_coeff, Float(static_cast<int64_t>(k) * (abs_n - k)), bk_coeff);
                bk_coeff.truncateToApprox(wp);
                FloatOps::add(sumB, bk_coeff, sumB);
                sumB.truncateToApprox(wp);
            }
            partB = ldexp(x_half_neg_n * sumB, -1);
            partB.truncateToApprox(wp);
        }

        // Part C: (-1)^n · (1/2)·(x/2)^n · Σ_{k=0}^{∞} [ψ(k+1)+ψ(k+n+1)] / (k!·(k+n)!) · (x²/4)^k
        // ψ(k+1) = -γ + H_k,  ψ(k+n+1) = -γ + H_{k+n}
        // ψ(k+1) + ψ(k+n+1) = -2γ + H_k + H_{k+n}
        Float x_half_n(1);
        for (int i = 0; i < abs_n; i++) {
            FloatOps::mul(x_half_n, x_half, x_half_n);
        }
        x_half_n.truncateToApprox(wp);

        Float H_k(0);       // H_0 = 0
        Float H_kn(0);      // H_n = Σ_{j=1}^{n} 1/j
        Float h_tmp(0);
        for (int j = 1; j <= abs_n; j++) {
            FloatOps::div(one_w, Float(j), h_tmp);
            FloatOps::add(H_kn, h_tmp, H_kn);
        }
        H_kn.truncateToApprox(wp);

        // k=0: 1/(0!·n!) = 1/n!
        Float ck_coeff(1);
        ck_coeff.setEffectiveBits(wp_bits);  // avoid exact/exact poison of 1/n!
        for (int i = 1; i <= abs_n; i++) {
            FloatOps::div(ck_coeff, Float(i), ck_coeff);
        }
        ck_coeff.truncateToApprox(wp);

        Float two_gamma = ldexp(euler_gamma, 1);
        Float partC = ck_coeff * (H_k + H_kn - two_gamma);
        partC.truncateToApprox(wp);
        Float term(0);

        for (int k = 1; k < 10 * wp; k++) {
            FloatOps::div(one_w, Float(k), h_tmp);
            FloatOps::add(H_k, h_tmp, H_k);
            FloatOps::div(one_w, Float(k + abs_n), h_tmp);
            FloatOps::add(H_kn, h_tmp, H_kn);

            // Recurrence: ck_coeff_{k} = ck_coeff_{k-1} · (x²/4) / (k · (k+n))
            FloatOps::mul(ck_coeff, x_half_sq, ck_coeff);
            FloatOps::div(ck_coeff, Float(static_cast<int64_t>(k) * (k + abs_n)), ck_coeff);
            ck_coeff.truncateToApprox(wp);

            FloatOps::mul(ck_coeff, H_k + H_kn - two_gamma, term);
            term.truncateToApprox(wp);
            FloatOps::add(partC, term, partC);
            partC.truncateToApprox(wp);

            if (k >= 5 && term.isZero()) break;
            if (k >= 5) {
                auto t_bits = term.exponent() + static_cast<int64_t>(term.mantissa().bitLength());
                auto s_bits = partC.exponent() + static_cast<int64_t>(partC.mantissa().bitLength());
                if (s_bits - t_bits > Float::precisionToBits(wp) + 5) break;
            }
        }

        Float sign_n = (abs_n % 2 == 0) ? Float(1) : Float(-1);
        partC = sign_n * ldexp(x_half_n * partC, -1);
        partC.truncateToApprox(wp);

        Float result = partA + partB + partC;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float besselK(int n, Float&& x, int precision) {
        return besselK(n, static_cast<const Float&>(x), precision);
    }

    //=============================================================================
    // Spherical Bessel function of the first kind j_n(x)
    //=============================================================================
    // j_0(x) = sin(x)/x
    // j_1(x) = sin(x)/x² - cos(x)/x
    // j_{k+1}(x) = (2k+1)/x · j_k(x) - j_{k-1}(x) (forward recurrence)

    Float sphericalBesselJ(int n, const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (n < 0) return Float::nan();

        int eff_x = x.effectiveBits();
        int wp = precision + 20 + 2 * n;
        Float xw = x;
        xw.truncateToApprox(wp);

        // j_n(0): j_0(0) = 1, j_n(0) = 0 (n > 0)
        if (x.isZero()) {
            return (n == 0) ? Float(1) : Float(0);
        }

        // x → ±∞: j_n(x) → 0 (oscillatory decay)
        if (x.isInfinity()) return Float(0);

        Float sin_x, cos_x;
        sinCos(xw, sin_x, cos_x, wp);

        // j_0(x) = sin(x)/x
        Float j0 = sin_x / xw;
        j0.truncateToApprox(wp);

        if (n == 0) {
            finalizeResult(j0, eff_x, precision);
            return j0;
        }

        // j_1(x) = sin(x)/x² - cos(x)/x
        Float j1 = sin_x / (xw * xw) - cos_x / xw;
        j1.truncateToApprox(wp);

        if (n == 1) {
            finalizeResult(j1, eff_x, precision);
            return j1;
        }

        // Forward recurrence: j_{k+1} = (2k+1)/x · j_k - j_{k-1}
        Float j_prev = j0;
        Float j_curr = j1;

        for (int k = 1; k < n; k++) {
            Float j_next = Float(2 * k + 1) / xw * j_curr - j_prev;
            j_next.truncateToApprox(wp);
            j_prev = j_curr;
            j_curr = j_next;
        }

        finalizeResult(j_curr, eff_x, precision);
        return j_curr;
    }

    Float sphericalBesselJ(int n, Float&& x, int precision) {
        return sphericalBesselJ(n, static_cast<const Float&>(x), precision);
    }

    //=============================================================================
    // Spherical Bessel function of the second kind y_n(x)
    //=============================================================================
    // y_0(x) = -cos(x)/x
    // y_1(x) = -cos(x)/x² - sin(x)/x
    // y_{k+1}(x) = (2k+1)/x · y_k(x) - y_{k-1}(x) (forward recurrence)

    Float sphericalBesselY(int n, const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (n < 0) return Float::nan();
        if (x.isZero()) return Float::negativeInfinity();  // y_n(0) = -∞
        if (x.isNegative()) return Float::nan();
        if (x.isInfinity()) return Float(0);  // y_n(+∞) → 0

        int eff_x = x.effectiveBits();
        int wp = precision + 20 + 2 * n;
        Float xw = x;
        xw.truncateToApprox(wp);

        Float sin_x, cos_x;
        sinCos(xw, sin_x, cos_x, wp);

        // y_0(x) = -cos(x)/x
        Float y0 = -cos_x / xw;
        y0.truncateToApprox(wp);

        if (n == 0) {
            finalizeResult(y0, eff_x, precision);
            return y0;
        }

        // y_1(x) = -cos(x)/x² - sin(x)/x
        Float y1 = -cos_x / (xw * xw) - sin_x / xw;
        y1.truncateToApprox(wp);

        if (n == 1) {
            finalizeResult(y1, eff_x, precision);
            return y1;
        }

        // Forward recurrence: y_{k+1} = (2k+1)/x · y_k - y_{k-1}
        Float y_prev = y0;
        Float y_curr = y1;

        for (int k = 1; k < n; k++) {
            Float y_next = Float(2 * k + 1) / xw * y_curr - y_prev;
            y_next.truncateToApprox(wp);
            y_prev = y_curr;
            y_curr = y_next;
        }

        finalizeResult(y_curr, eff_x, precision);
        return y_curr;
    }

    Float sphericalBesselY(int n, Float&& x, int precision) {
        return sphericalBesselY(n, static_cast<const Float&>(x), precision);
    }

    //=============================================================================
    // Airy functions Ai(x), Bi(x)
    //=============================================================================
    // Ai(x) = c1·f(x) - c2·g(x)
    // Bi(x) = √3·[c1·f(x) + c2·g(x)]
    // f(x) = Σ_{k=0}^{∞} 3^k · x^{3k} / (3k)! · Γ(k+1/3)/(Γ(1/3))  ← via recurrence
    // In practice, computed directly with a Taylor series:
    // Ai(x) = a·Σ_{k=0}^{∞} c_k·x^k  where c_0 = 1, c_1 = x, c_{k+3} = c_k / ((k+2)(k+3))·x³

    // Maclaurin series:
    // Ai(x) = Ai(0)·f(x) + Ai'(0)·g(x)
    // f(x) = 1 + x³/6 + x⁶/180 + ... = Σ of terms reciprocal-like to (3k)!
    // g(x) = x + x⁴/12 + x⁷/504 + ...
    // Ai(0) = 1/(3^{2/3}·Γ(2/3)), Ai'(0) = -1/(3^{1/3}·Γ(1/3))

    //-------------------------------------------------------------------------
    // Asymptotic expansion (DLMF/AS 10.4.59 / 10.4.63)
    //-------------------------------------------------------------------------
    // Common coefficients: c_0 = 1, c_s/c_{s-1} = (6s-1)(6s-5) / (72s)
    // x → +∞: Ai(x) ~ exp(-ζ) / (2√π · x^{1/4}) · Σ (-1)^s c_s / ζ^s
    // x → -∞: Ai(-y) ~ (1/(√π y^{1/4})) [sin(θ) S_even - cos(θ) S_odd]
    //   ζ = (2/3) |x|^{3/2}, θ = ζ + π/4
    //   S_even = Σ (-1)^k c_{2k} / ζ^{2k}, S_odd = Σ (-1)^k c_{2k+1} / ζ^{2k+1}
    //
    // Activation condition (codex consult 2026-05-06):
    //   x > 0: ζ > 0.40 p_bits + 16   (= |x|^{3/2} > 0.6 p + 24)
    //   x < 0: ζ > 0.467 p_bits + 21  (≈ 0.50 p + 24 is safe)
    // Achieved precision ~ 2.885·ζ bits
    // Stopping condition: smallest-term truncation (stop once the term starts increasing)
    // Phase precision (x<0 only): wp_phase_bits = p_bits + 48 + msb(ζ)

    namespace {

    // From ζ, return the estimated truncation s before |term_s| starts increasing
    // (smallest-term position ≈ 2ζ, with safety margin)
    inline int airyAsymptoticMaxIter(double zeta_d) {
        if (zeta_d <= 0.0) return 100;
        long long n = static_cast<long long>(2.0 * zeta_d) + 32;
        if (n > 100000) n = 100000;
        return static_cast<int>(n);
    }

    // Asymptotic expansion for x > 0, large ζ = (2/3) x^{3/2}
    // Returns Ai(x) keeping a working precision of precision + 30 digits
    Float airyAi_asymptotic_pos(const Float& xw, int precision) {
        int wp = precision + 30;

        Float sqrt_x = sqrt(xw, wp);
        Float x14 = sqrt(sqrt_x, wp);  // x^{1/4}

        // ζ = (2 x sqrt(x)) / 3
        Float zeta = Float(2) * xw * sqrt_x / Float(3);
        zeta.truncateToApprox(wp);
        Float zeta_inv = Float(1) / zeta;
        zeta_inv.truncateToApprox(wp);

        // Series: Σ (-1)^s c_s / ζ^s, smallest-term truncation
        Float sum(1);  // c_0 = 1
        Float term(1);
        Float prev_abs(1);

        int max_iter = airyAsymptoticMaxIter(zeta.toDouble());
        for (int s = 1; s < max_iter; ++s) {
            int64_t num = static_cast<int64_t>(6 * s - 1) * static_cast<int64_t>(6 * s - 5);
            int64_t den = static_cast<int64_t>(72) * s;
            FloatOps::mul(term, zeta_inv, term);
            FloatOps::mul(term, Float(num), term);
            FloatOps::div(term, Float(den), term);
            term.truncateToApprox(wp);
            term = -term;  // (-1)^s factor (per-iteration sign flip)

            Float abs_term = term;
            if (abs_term.isNegative()) abs_term = -abs_term;
            if (s >= 2 && abs_term > prev_abs) break;  // smallest-term truncation
            prev_abs = abs_term;

            FloatOps::add(sum, term, sum);
            sum.truncateToApprox(wp);
        }

        // prefactor = exp(-ζ) / (2 √π · x^{1/4})
        Float pi = Float::pi(wp);
        Float sqrt_pi = sqrt(pi, wp);
        Float exp_neg_zeta = exp(-zeta, wp);
        Float prefactor = exp_neg_zeta / (Float(2) * sqrt_pi * x14);
        prefactor.truncateToApprox(wp);
        return prefactor * sum;
    }

    // For x < 0 (neg_x is the negative input), large y = -neg_x > 0
    Float airyAi_asymptotic_neg(const Float& neg_x, int precision) {
        int p_bits = Float::precisionToBits(precision);
        int wp = precision + 30;

        Float y = -neg_x;
        y.truncateToApprox(wp);
        Float sqrt_y = sqrt(y, wp);
        Float y14 = sqrt(sqrt_y, wp);

        Float zeta = Float(2) * y * sqrt_y / Float(3);
        zeta.truncateToApprox(wp);

        // Phase precision: needs extra by the MSB of ζ (so the phase is not lost inside sinCos)
        int64_t zeta_msb = zeta.exponent() + static_cast<int64_t>(zeta.mantissa().bitLength());
        if (zeta_msb < 0) zeta_msb = 0;
        int wp_phase_bits = p_bits + 48 + static_cast<int>(zeta_msb);
        int wp_phase = Float::bitsToPrecision(wp_phase_bits) + 4;
        if (wp_phase < wp) wp_phase = wp;

        // Recompute θ = ζ + π/4 at high precision
        Float y_phase = -neg_x;
        y_phase.truncateToApprox(wp_phase);
        Float sqrt_y_phase = sqrt(y_phase, wp_phase);
        Float zeta_hi = Float(2) * y_phase * sqrt_y_phase / Float(3);
        zeta_hi.truncateToApprox(wp_phase);
        Float pi_over_4 = Float::pi(wp_phase) / Float(4);
        pi_over_4.truncateToApprox(wp_phase);
        Float theta = zeta_hi + pi_over_4;
        theta.truncateToApprox(wp_phase);

        Float sin_theta, cos_theta;
        sinCos(theta, sin_theta, cos_theta, wp_phase);
        sin_theta.truncateToApprox(wp);
        cos_theta.truncateToApprox(wp);

        Float zeta_inv = Float(1) / zeta;
        zeta_inv.truncateToApprox(wp);

        // Series: term_s = c_s / ζ^s (unsigned), distributed to subseries with (-1)^k addition
        Float term(1);
        Float s_even(1);
        Float s_odd(0);
        Float prev_abs(1);

        int max_iter = airyAsymptoticMaxIter(zeta.toDouble());
        for (int s = 1; s < max_iter; ++s) {
            int64_t num = static_cast<int64_t>(6 * s - 1) * static_cast<int64_t>(6 * s - 5);
            int64_t den = static_cast<int64_t>(72) * s;
            FloatOps::mul(term, zeta_inv, term);
            FloatOps::mul(term, Float(num), term);
            FloatOps::div(term, Float(den), term);
            term.truncateToApprox(wp);

            Float abs_term = term;
            if (abs_term.isNegative()) abs_term = -abs_term;
            if (s >= 2 && abs_term > prev_abs) break;
            prev_abs = abs_term;

            // s even → add (-1)^(s/2)·term to S_even
            // s odd  → add (-1)^((s-1)/2)·term to S_odd
            if ((s & 1) == 0) {
                int k = s >> 1;
                Float t = (k & 1) ? -term : term;
                FloatOps::add(s_even, t, s_even);
                s_even.truncateToApprox(wp);
            } else {
                int k = (s - 1) >> 1;
                Float t = (k & 1) ? -term : term;
                FloatOps::add(s_odd, t, s_odd);
                s_odd.truncateToApprox(wp);
            }
        }

        Float pi = Float::pi(wp);
        Float sqrt_pi = sqrt(pi, wp);
        Float prefactor = Float(1) / (sqrt_pi * y14);
        prefactor.truncateToApprox(wp);

        return prefactor * (sin_theta * s_even - cos_theta * s_odd);
    }

    // Decide whether to activate the asymptotic expansion: use it if ζ > ζ_cut
    // ζ_cut = 0.40·p_bits + 16 (x>0) or 0.50·p_bits + 24 (x<0)
    // Two-stage decision: reject the certain band via the msb estimate, computing the actual value with toDouble() only in the boundary band.
    bool airyAi_should_use_asymptotic(const Float& xw, int precision) {
        if (xw.isZero()) return false;
        int p_bits = Float::precisionToBits(precision);

        bool neg = xw.isNegative();
        // Approximate log2 of |x|: exponent + bitLength - 1
        int64_t msb = xw.exponent() + static_cast<int64_t>(xw.mantissa().bitLength()) - 1;
        if (msb <= 0) return false;  // |x| < 1 is almost certainly Maclaurin

        double log2_zeta_est = 1.5 * static_cast<double>(msb) - 0.585;  // log2((2/3)|x|^{1.5})
        double zeta_cut = neg ? (0.50 * p_bits + 24.0) : (0.40 * p_bits + 16.0);
        double log2_zeta_cut = std::log2(zeta_cut + 1e-9);

        // Certain band: the msb estimate error is at most ~1.5 bits, so ±2.0 is safe
        if (log2_zeta_est > log2_zeta_cut + 2.0) return true;
        if (log2_zeta_est < log2_zeta_cut - 2.0) return false;

        // Boundary band: if |x| fits in the double range, compute ζ directly
        double x_d = std::abs(xw.toDouble());
        if (std::isfinite(x_d) && x_d > 0.0) {
            double zeta_d = (2.0 / 3.0) * x_d * std::sqrt(x_d);
            return zeta_d > zeta_cut;
        }
        // toDouble overflows for huge values: outside the certain band with large msb, lean toward yes
        return true;
    }

    // Estimate the cancellation digits on the Maclaurin route and return the required working precision.
    // The largest term of f(x), g(x) ~ exp(2|x|^{3/2}/3) = exp(ζ) → loses 2ζ/ln(10) digits
    // (the factor 2 compensates the cancellation of both f(x) and g(x))
    int airyMaclaurinDynWp(const Float& xw, int precision) {
        int base = precision + 30;
        int64_t msb = xw.exponent() + static_cast<int64_t>(xw.mantissa().bitLength()) - 1;
        if (msb <= 0) return base;  // |x| < 1: no cancellation
        double x_d = std::abs(xw.toDouble());
        double zeta_d;
        if (std::isfinite(x_d) && x_d > 0.0) {
            zeta_d = (2.0 / 3.0) * x_d * std::sqrt(x_d);
        } else {
            // Huge |x| should go to the asymptotic route, but estimate via msb just in case
            double log2_zeta = 1.5 * static_cast<double>(msb) - 0.585;
            zeta_d = std::pow(2.0, log2_zeta);
        }
        // K · ζ / ln(10) digits + safety
        // In theory K=2 (cancellation of ai0·f and aip0·g = 2ζ/ln(10)) should suffice, but
        // internal precision propagation of gamma/pow + Maclaurin accumulated error require K=4 in practice.
        int extra = static_cast<int>(std::ceil(4.0 * zeta_d / 2.302585092994046)) + 8;
        return base + extra;
    }

    // thread_local cache of Ai(0), Ai'(0) (keyed by precision)
    // ai0  = 1 / (3^{2/3} · Γ(2/3))
    // aip0 = -1 / (3^{1/3} · Γ(1/3))
    // Eliminates recomputing gamma/pow every time (codex suggestion 2026-05-06).
    struct AiryConstantsCache {
        int wp = -1;
        Float ai0;
        Float aip0;
    };
    static const AiryConstantsCache& airyConstants(int wp) {
        thread_local AiryConstantsCache cache;
        if (cache.wp == wp) return cache;
        Float::PrecisionScope _ps(wp);   // Compute exact÷exact of the constants one/two/three at wp digits (old: setResultPrecision each)
        Float three(3);
        Float two(2);
        Float one(1);
        Float two_thirds = two / three;
        Float one_third = one / three;
        cache.ai0 = one / (pow(three, two_thirds, wp) * gamma(two_thirds, wp));
        cache.aip0 = -one / (pow(three, one_third, wp) * gamma(one_third, wp));
        cache.ai0.truncateToApprox(wp);
        cache.aip0.truncateToApprox(wp);
        cache.wp = wp;
        return cache;
    }

    }  // anonymous namespace

    Float airyAi(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float(0);  // Ai(±∞) → 0

        int eff_x = x.effectiveBits();
        Float xw = x;
        xw.truncateToApprox(precision + 30);

        // Activate the asymptotic expansion: only for large |x| and ζ > ζ_cut. Leave the Maclaurin route unchanged
        if (airyAi_should_use_asymptotic(xw, precision)) {
            Float result = xw.isNegative()
                ? airyAi_asymptotic_neg(xw, precision)
                : airyAi_asymptotic_pos(xw, precision);
            finalizeResult(result, eff_x, precision);
            return result;
        }

        // Maclaurin route: the cancellation of f(x), g(x) loses ζ/ln(10) digits, so expand wp
        int wp = airyMaclaurinDynWp(xw, precision);
        xw = x;
        xw.truncateToApprox(wp);

        // Ai(0), Ai'(0) via the thread_local cache (keyed by precision)
        const AiryConstantsCache& constants = airyConstants(wp);
        const Float& ai0 = constants.ai0;
        const Float& aip0 = constants.aip0;

        if (x.isZero()) {
            Float result = ai0;
            finalizeResult(result, eff_x, precision);
            return result;
        }

        // f(x) = Σ_{k=0}^{∞} a_k, where a_0 = 1, a_{k+1} = a_k · x³ / ((3k+2)(3k+3))
        // g(x) = Σ_{k=0}^{∞} b_k, where b_0 = x, b_{k+1} = b_k · x³ / ((3k+3)(3k+4))
        Float x3 = xw * xw * xw;
        x3.truncateToApprox(wp);

        Float f_sum(1); f_sum.setResultPrecision(wp);   // a_0 = 1
        Float g_sum = xw;
        Float f_term(1); f_term.setResultPrecision(wp);
        Float g_term = xw;

        // Pre-allocate the divisor Floats and reuse them in place (eliminates per-iteration allocation)
        Float fdiv; fdiv.setResultPrecision(wp);
        Float gdiv; gdiv.setResultPrecision(wp);

        int wp_bits = Float::precisionToBits(wp);
        // Adaptive precision: at wp ≥ ADAPTIVE_THRESHOLD, reduce the truncation digit count as the terms shrink,
        // cutting the cost of subsequent muls. At small wp the adaptive overhead exceeds the savings, so
        // use the legacy (fixed wp truncate) path.
        // At 5000d: 47x → 5.3x (8.8x improvement); at 1000d: 13-17x → 4.2x (3-4x improvement).
        // Disable adaptive at small wp (≤ 200d) to avoid the 19d/38d regression.
        constexpr int ADAPTIVE_THRESHOLD = 200;  // digits
        constexpr int MIN_TRUNC_DIGITS = 20;
        const bool use_adaptive = (wp >= ADAPTIVE_THRESHOLD);
        for (int k = 0; k < 10 * wp; k++) {
            int64_t fd = static_cast<int64_t>(3 * k + 2) * (3 * k + 3);
            int64_t gd = static_cast<int64_t>(3 * k + 3) * (3 * k + 4);
            fdiv = Float(fd); fdiv.setResultPrecision(wp);
            gdiv = Float(gd); gdiv.setResultPrecision(wp);

            FloatOps::mul(f_term, x3, f_term);
            FloatOps::div(f_term, fdiv, f_term);
            if (!use_adaptive) f_term.truncateToApprox(wp);
            FloatOps::add(f_sum, f_term, f_sum);
            f_sum.truncateToApprox(wp);

            FloatOps::mul(g_term, x3, g_term);
            FloatOps::div(g_term, gdiv, g_term);
            if (!use_adaptive) g_term.truncateToApprox(wp);
            FloatOps::add(g_sum, g_term, g_sum);
            g_sum.truncateToApprox(wp);

            int64_t f_drop = 0, g_drop = 0;
            if (use_adaptive) {
                // Adaptive truncate: shorten f_term, g_term according to their shrinking magnitude
                int64_t ft_mag = f_term.exponent() + static_cast<int64_t>(f_term.mantissa().bitLength());
                int64_t fs_mag = f_sum.exponent()  + static_cast<int64_t>(f_sum.mantissa().bitLength());
                f_drop = std::max((int64_t)0, fs_mag - ft_mag);
                int f_bits = static_cast<int>(std::max((int64_t)64, (int64_t)wp_bits + 5 - f_drop));
                int f_digits = std::max(MIN_TRUNC_DIGITS,
                                        static_cast<int>((f_bits - 8) / 3.32192809488736) + 2);
                f_term.truncateToApprox(std::min(wp, f_digits));

                int64_t gt_mag = g_term.exponent() + static_cast<int64_t>(g_term.mantissa().bitLength());
                int64_t gs_mag = g_sum.exponent()  + static_cast<int64_t>(g_sum.mantissa().bitLength());
                g_drop = std::max((int64_t)0, gs_mag - gt_mag);
                int g_bits = static_cast<int>(std::max((int64_t)64, (int64_t)wp_bits + 5 - g_drop));
                int g_digits = std::max(MIN_TRUNC_DIGITS,
                                        static_cast<int>((g_bits - 8) / 3.32192809488736) + 2);
                g_term.truncateToApprox(std::min(wp, g_digits));
            } else {
                int64_t ft_mag = f_term.exponent() + static_cast<int64_t>(f_term.mantissa().bitLength());
                int64_t fs_mag = f_sum.exponent()  + static_cast<int64_t>(f_sum.mantissa().bitLength());
                f_drop = std::max((int64_t)0, fs_mag - ft_mag);
                int64_t gt_mag = g_term.exponent() + static_cast<int64_t>(g_term.mantissa().bitLength());
                int64_t gs_mag = g_sum.exponent()  + static_cast<int64_t>(g_sum.mantissa().bitLength());
                g_drop = std::max((int64_t)0, gs_mag - gt_mag);
            }

            if (k >= 3) {
                bool f_conv = f_term.isZero() || (f_drop > wp_bits + 5);
                bool g_conv = g_term.isZero() || (g_drop > wp_bits + 5);
                if (f_conv && g_conv) break;
            }
        }

        Float result = ai0 * f_sum + aip0 * g_sum;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float airyAi(Float&& x, int precision) {
        return airyAi(static_cast<const Float&>(x), precision);
    }

    Float airyBi(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) return Float(0);  // Bi(-∞) → 0
            return Float::positiveInfinity();       // Bi(+∞) → +∞
        }

        int eff_x = x.effectiveBits();
        Float xw = x;
        xw.truncateToApprox(precision + 30);

        // Maclaurin: cancellation correction
        int wp = airyMaclaurinDynWp(xw, precision);
        xw = x;
        xw.truncateToApprox(wp);

        // Bi(0) = 1 / (3^{1/6} · Γ(2/3))
        // Bi'(0) = 3^{1/6} / Γ(1/3)
        Float::PrecisionScope _ps(wp);   // Compute exact÷exact of the constants at wp digits (old: setResultPrecision each)
        Float three(3);
        Float two(2);
        Float one(1);
        Float six(6);
        Float one_sixth = one / six;
        Float two_thirds = two / three;
        Float one_third = one / three;
        Float bi0 = one / (pow(three, one_sixth, wp) * gamma(two_thirds, wp));
        Float bip0 = pow(three, one_sixth, wp) / gamma(one_third, wp);
        bi0.truncateToApprox(wp);
        bip0.truncateToApprox(wp);

        if (x.isZero()) {
            finalizeResult(bi0, eff_x, precision);
            return bi0;
        }

        // Same f(x), g(x) series
        Float x3 = xw * xw * xw;
        x3.truncateToApprox(wp);

        Float f_sum(1); f_sum.setResultPrecision(wp);
        Float g_sum = xw;
        Float f_term(1); f_term.setResultPrecision(wp);
        Float g_term = xw;

        for (int k = 0; k < 10 * wp; k++) {
            Float fdiv(static_cast<int64_t>(3 * k + 2) * (3 * k + 3));
            fdiv.setResultPrecision(wp);
            FloatOps::mul(f_term, x3, f_term);
            FloatOps::div(f_term, fdiv, f_term);
            f_term.truncateToApprox(wp);
            FloatOps::add(f_sum, f_term, f_sum);
            f_sum.truncateToApprox(wp);

            Float gdiv(static_cast<int64_t>(3 * k + 3) * (3 * k + 4));
            gdiv.setResultPrecision(wp);
            FloatOps::mul(g_term, x3, g_term);
            FloatOps::div(g_term, gdiv, g_term);
            g_term.truncateToApprox(wp);
            FloatOps::add(g_sum, g_term, g_sum);
            g_sum.truncateToApprox(wp);

            if (k >= 3) {
                bool f_conv = f_term.isZero();
                bool g_conv = g_term.isZero();
                if (!f_conv) {
                    auto ft = f_term.exponent() + static_cast<int64_t>(f_term.mantissa().bitLength());
                    auto fs = f_sum.exponent() + static_cast<int64_t>(f_sum.mantissa().bitLength());
                    f_conv = (fs - ft > Float::precisionToBits(wp) + 5);
                }
                if (!g_conv) {
                    auto gt = g_term.exponent() + static_cast<int64_t>(g_term.mantissa().bitLength());
                    auto gs = g_sum.exponent() + static_cast<int64_t>(g_sum.mantissa().bitLength());
                    g_conv = (gs - gt > Float::precisionToBits(wp) + 5);
                }
                if (f_conv && g_conv) break;
            }
        }

        Float result = bi0 * f_sum + bip0 * g_sum;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float airyBi(Float&& x, int precision) {
        return airyBi(static_cast<const Float&>(x), precision);
    }

    //=============================================================================
    // Derivatives of the Airy functions Ai'(x), Bi'(x)
    //=============================================================================
    // Term-wise differentiation of the Maclaurin series:
    // f(x) = Σ c_k·x^{3k},  f'(x) = Σ_{k≥1} 3k·c_k·x^{3k-1}
    // g(x) = Σ d_k·x^{3k+1}, g'(x) = Σ_{k≥0} (3k+1)·d_k·x^{3k}
    //
    // After updating f_term, g_term inside the loop:
    //   fp_contrib = 3(k+1) · f_term / x
    //   gp_contrib = (3k+4) · g_term / x
    //
    // Ai'(x) = Ai(0)·f'(x) + Ai'(0)·g'(x)
    // Bi'(x) = Bi(0)·f'(x) + Bi'(0)·g'(x)

    Float airyAiPrime(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) return Float(0);  // Ai'(±∞) → 0

        int eff_x = x.effectiveBits();
        Float xw = x;
        xw.truncateToApprox(precision + 30);

        // Maclaurin: cancellation correction (the derivatives are dominated by the same ζ)
        int wp = airyMaclaurinDynWp(xw, precision);
        xw = x;
        xw.truncateToApprox(wp);

        Float::PrecisionScope _ps(wp);   // Compute exact÷exact of the constants at wp digits (old: setResultPrecision each)
        Float three(3);
        Float two(2);
        Float one(1);
        Float two_thirds = two / three;
        Float one_third = one / three;
        Float ai0 = one / (pow(three, two_thirds, wp) * gamma(two_thirds, wp));
        Float aip0 = -one / (pow(three, one_third, wp) * gamma(one_third, wp));
        ai0.truncateToApprox(wp);
        aip0.truncateToApprox(wp);

        // Ai'(0) = aip0 (f'(0) = 0, g'(0) = 1)
        if (x.isZero()) {
            finalizeResult(aip0, eff_x, precision);
            return aip0;
        }

        Float x3 = xw * xw * xw;
        x3.truncateToApprox(wp);
        Float inv_x = one / xw;
        inv_x.truncateToApprox(wp);

        Float f_term(1); f_term.setResultPrecision(wp);
        Float g_term = xw;
        Float fp_sum(0); fp_sum.setResultPrecision(wp);  // f'(0) = 0
        Float gp_sum(1); gp_sum.setResultPrecision(wp);  // g'(0) = 1
        Float fp_contrib(0), gp_contrib(0);

        for (int k = 0; k < 10 * wp; k++) {
            Float fdiv(static_cast<int64_t>(3 * k + 2) * (3 * k + 3));
            fdiv.setResultPrecision(wp);
            FloatOps::mul(f_term, x3, f_term);
            FloatOps::div(f_term, fdiv, f_term);
            f_term.truncateToApprox(wp);
            Float gdiv(static_cast<int64_t>(3 * k + 3) * (3 * k + 4));
            gdiv.setResultPrecision(wp);
            FloatOps::mul(g_term, x3, g_term);
            FloatOps::div(g_term, gdiv, g_term);
            g_term.truncateToApprox(wp);

            Float f_mul(3 * (k + 1)); f_mul.setResultPrecision(wp);
            Float g_mul(3 * k + 4);   g_mul.setResultPrecision(wp);
            FloatOps::mul(f_term, inv_x, fp_contrib);
            FloatOps::mul(fp_contrib, f_mul, fp_contrib);
            fp_contrib.truncateToApprox(wp);
            FloatOps::mul(g_term, inv_x, gp_contrib);
            FloatOps::mul(gp_contrib, g_mul, gp_contrib);
            gp_contrib.truncateToApprox(wp);
            FloatOps::add(fp_sum, fp_contrib, fp_sum);
            fp_sum.truncateToApprox(wp);
            FloatOps::add(gp_sum, gp_contrib, gp_sum);
            gp_sum.truncateToApprox(wp);

            if (k >= 3) {
                bool fp_conv = fp_contrib.isZero();
                bool gp_conv = gp_contrib.isZero();
                if (!fp_conv) {
                    auto ft = fp_contrib.exponent() + static_cast<int64_t>(fp_contrib.mantissa().bitLength());
                    auto fs = fp_sum.exponent() + static_cast<int64_t>(fp_sum.mantissa().bitLength());
                    fp_conv = (fs - ft > Float::precisionToBits(wp) + 5);
                }
                if (!gp_conv) {
                    auto gt = gp_contrib.exponent() + static_cast<int64_t>(gp_contrib.mantissa().bitLength());
                    auto gs = gp_sum.exponent() + static_cast<int64_t>(gp_sum.mantissa().bitLength());
                    gp_conv = (gs - gt > Float::precisionToBits(wp) + 5);
                }
                if (fp_conv && gp_conv) break;
            }
        }

        Float result = ai0 * fp_sum + aip0 * gp_sum;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float airyAiPrime(Float&& x, int precision) {
        return airyAiPrime(static_cast<const Float&>(x), precision);
    }

    Float airyBiPrime(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isInfinity()) {
            if (x.isNegative()) return Float(0);  // Bi'(-∞) → 0
            return Float::positiveInfinity();       // Bi'(+∞) → +∞
        }

        int eff_x = x.effectiveBits();
        Float xw = x;
        xw.truncateToApprox(precision + 30);

        // Maclaurin: cancellation correction
        int wp = airyMaclaurinDynWp(xw, precision);
        xw = x;
        xw.truncateToApprox(wp);

        Float::PrecisionScope _ps(wp);   // Compute exact÷exact of the constants at wp digits (old: setResultPrecision each)
        Float three(3);
        Float two(2);
        Float one(1);
        Float six(6);
        Float one_sixth = one / six;
        Float two_thirds = two / three;
        Float one_third = one / three;
        Float bi0 = one / (pow(three, one_sixth, wp) * gamma(two_thirds, wp));
        Float bip0 = pow(three, one_sixth, wp) / gamma(one_third, wp);
        bi0.truncateToApprox(wp);
        bip0.truncateToApprox(wp);

        // Bi'(0) = bip0
        if (x.isZero()) {
            finalizeResult(bip0, eff_x, precision);
            return bip0;
        }

        Float x3 = xw * xw * xw;
        x3.truncateToApprox(wp);
        Float inv_x = one / xw;
        inv_x.truncateToApprox(wp);

        Float f_term(1); f_term.setResultPrecision(wp);
        Float g_term = xw;
        Float fp_sum(0); fp_sum.setResultPrecision(wp);
        Float gp_sum(1); gp_sum.setResultPrecision(wp);
        Float fp_contrib(0), gp_contrib(0);

        for (int k = 0; k < 10 * wp; k++) {
            Float fdiv(static_cast<int64_t>(3 * k + 2) * (3 * k + 3));
            fdiv.setResultPrecision(wp);
            FloatOps::mul(f_term, x3, f_term);
            FloatOps::div(f_term, fdiv, f_term);
            f_term.truncateToApprox(wp);
            Float gdiv(static_cast<int64_t>(3 * k + 3) * (3 * k + 4));
            gdiv.setResultPrecision(wp);
            FloatOps::mul(g_term, x3, g_term);
            FloatOps::div(g_term, gdiv, g_term);
            g_term.truncateToApprox(wp);

            Float f_mul(3 * (k + 1)); f_mul.setResultPrecision(wp);
            Float g_mul(3 * k + 4);   g_mul.setResultPrecision(wp);
            FloatOps::mul(f_term, inv_x, fp_contrib);
            FloatOps::mul(fp_contrib, f_mul, fp_contrib);
            fp_contrib.truncateToApprox(wp);
            FloatOps::mul(g_term, inv_x, gp_contrib);
            FloatOps::mul(gp_contrib, g_mul, gp_contrib);
            gp_contrib.truncateToApprox(wp);
            FloatOps::add(fp_sum, fp_contrib, fp_sum);
            fp_sum.truncateToApprox(wp);
            FloatOps::add(gp_sum, gp_contrib, gp_sum);
            gp_sum.truncateToApprox(wp);

            if (k >= 3) {
                bool fp_conv = fp_contrib.isZero();
                bool gp_conv = gp_contrib.isZero();
                if (!fp_conv) {
                    auto ft = fp_contrib.exponent() + static_cast<int64_t>(fp_contrib.mantissa().bitLength());
                    auto fs = fp_sum.exponent() + static_cast<int64_t>(fp_sum.mantissa().bitLength());
                    fp_conv = (fs - ft > Float::precisionToBits(wp) + 5);
                }
                if (!gp_conv) {
                    auto gt = gp_contrib.exponent() + static_cast<int64_t>(gp_contrib.mantissa().bitLength());
                    auto gs = gp_sum.exponent() + static_cast<int64_t>(gp_sum.mantissa().bitLength());
                    gp_conv = (gs - gt > Float::precisionToBits(wp) + 5);
                }
                if (fp_conv && gp_conv) break;
            }
        }

        Float result = bi0 * fp_sum + bip0 * gp_sum;
        finalizeResult(result, eff_x, precision);
        return result;
    }

    Float airyBiPrime(Float&& x, int precision) {
        return airyBiPrime(static_cast<const Float&>(x), precision);
    }

    //=============================================================================
    // Confluent hypergeometric limit function ₀F₁(; b; z)
    //=============================================================================
    // ₀F₁(; b; z) = Σ_{k=0}^∞ z^k / ((b)_k · k!)
    // term_{k+1} / term_k = z / ((b+k) · (k+1))
    // Converges for all z

    Float hyperg0F1(const Float& b, const Float& z, int precision) {
        if (b.isNaN() || z.isNaN()) return Float::nan();
        if (z.isZero()) return Float(1);

        // b a non-positive integer → pole
        if (!b.isPositive() && b.isInteger()) return Float::nan();

        int eff = std::min(b.effectiveBits(), z.effectiveBits());
        int wp = precision + 20;
        Float::PrecisionScope _ps(wp);   // Compute exact÷exact via FloatOps::div at wp digits (contextBits)
        Float bw = b;
        bw.truncateToApprox(wp);
        Float zw = z;
        zw.truncateToApprox(wp);

        Float term(1);
        Float sum(1);
        Float tmp(0);

        for (int k = 0; k < 10 * wp; k++) {
            FloatOps::mul(term, zw, term);
            FloatOps::add(bw, Float(k), tmp);
            FloatOps::mul(tmp, Float(k + 1), tmp);
            FloatOps::div(term, tmp, term);
            term.truncateToApprox(wp);
            FloatOps::add(sum, term, sum);
            sum.truncateToApprox(wp);

            if (k >= 3 && term.isZero()) break;
            if (k >= 3) {
                auto t_bits = term.exponent() + static_cast<int64_t>(term.mantissa().bitLength());
                auto s_bits = sum.exponent() + static_cast<int64_t>(sum.mantissa().bitLength());
                if (s_bits - t_bits > Float::precisionToBits(wp) + 5) break;
            }
        }

        finalizeResult(sum, eff, precision);
        return sum;
    }

    Float hyperg0F1(Float&& b, Float&& z, int precision) {
        return hyperg0F1(static_cast<const Float&>(b), static_cast<const Float&>(z), precision);
    }

    //=============================================================================
    // Confluent hypergeometric function ₁F₁(a; b; z) (Kummer M)
    //=============================================================================
    // M(a, b, z) = Σ_{k=0}^∞ (a)_k z^k / ((b)_k · k!)
    // term_{k+1} / term_k = (a+k) · z / ((b+k) · (k+1))
    //
    // Kummer transformation (when z < 0): M(a,b,z) = e^z · M(b-a, b, -z)
    // Avoids the cancellation of the alternating series and accelerates convergence

    Float confHyperg(const Float& a, const Float& b, const Float& z, int precision) {
        Float::PrecisionScope _ps(precision + 64);   // Compute exact÷exact inside the function at working precision
        if (a.isNaN() || b.isNaN() || z.isNaN()) return Float::nan();
        if (z.isZero()) return Float(1);

        // b a non-positive integer → pole
        if (!b.isPositive() && b.isInteger()) return Float::nan();

        // a = 0 → 1
        if (a.isZero()) return Float(1);

        int eff = std::min({a.effectiveBits(), b.effectiveBits(), z.effectiveBits()});
        int wp = precision + 30;
        Float aw = a;
        aw.truncateToApprox(wp);
        Float bw = b;
        bw.truncateToApprox(wp);
        Float zw = z;
        zw.truncateToApprox(wp);

        // a = b → e^z
        if (aw == bw) {
            Float result = exp(zw, wp);
            finalizeResult(result, eff, precision);
            return result;
        }

        // Kummer transformation: when z < 0, M(a,b,z) = e^z · M(b-a, b, -z)
        // Since -z > 0, the recursive call does not enter the Kummer path
        if (zw.isNegative()) {
            Float ez = exp(zw, wp);
            Float inner = confHyperg(bw - aw, bw, -zw, wp);
            inner.truncateToApprox(wp);
            Float result = ez * inner;
            finalizeResult(result, eff, precision);
            return result;
        }

        // a a non-positive integer → polynomial (finite sum)
        bool is_poly = false;
        int poly_terms = 0;
        if (aw.isNegative() && aw.isInteger()) {
            is_poly = true;
            poly_terms = -static_cast<int>(aw.toDouble()) + 1;
        }

        // Taylor series
        Float term(1);
        Float sum(1);
        int max_iter = is_poly ? poly_terms : 10 * wp;

        for (int k = 0; k < max_iter; k++) {
            term = term * (aw + Float(k)) * zw / ((bw + Float(k)) * (k + 1));
            term.truncateToApprox(wp);
            sum = sum + term;
            sum.truncateToApprox(wp);

            if (!is_poly && k >= 3 && term.isZero()) break;
            if (!is_poly && k >= 3) {
                auto t_bits = term.exponent() + static_cast<int64_t>(term.mantissa().bitLength());
                auto s_bits = sum.exponent() + static_cast<int64_t>(sum.mantissa().bitLength());
                if (s_bits - t_bits > Float::precisionToBits(wp) + 5) break;
            }
        }

        finalizeResult(sum, eff, precision);
        return sum;
    }

    Float confHyperg(Float&& a, Float&& b, Float&& z, int precision) {
        return confHyperg(static_cast<const Float&>(a), static_cast<const Float&>(b),
                          static_cast<const Float&>(z), precision);
    }

    //=============================================================================
    // Gauss hypergeometric function ₂F₁(a, b; c; z)
    //=============================================================================
    // ₂F₁(a, b; c; z) = Σ_{k=0}^∞ (a)_k (b)_k z^k / ((c)_k · k!)
    // Converges for |z| < 1. Domain strategy:
    //   |z| < 0.5  → direct Taylor
    //   z < -0.5   → Pfaff transformation: (1-z)^{-a} · ₂F₁(a, c-b; c; z/(z-1))
    //   0.5 ≤ z < 1 → DLMF 15.8.1 connection formula (evaluate at 1-z)
    //   polynomial case → always direct Taylor

    Float hyperg(const Float& a, const Float& b, const Float& c,
                 const Float& z, int precision) {
        if (a.isNaN() || b.isNaN() || c.isNaN() || z.isNaN()) return Float::nan();
        if (z.isZero()) return Float(1);
        if (a.isZero() || b.isZero()) return Float(1);

        int eff = std::min({a.effectiveBits(), b.effectiveBits(), c.effectiveBits(), z.effectiveBits()});
        int wp = precision + 40;
        // For dyadic exact inputs (0.5/1.5/2.5 etc.), FloatOps::div poisons as exact/exact on the direct Taylor route,
        // so set a scope exceeding the working precision of every branch (wp=+40, +80 near integers).
        Float::PrecisionScope _ps(precision + 80);
        Float aw = a;
        aw.truncateToApprox(wp);
        Float bw = b;
        bw.truncateToApprox(wp);
        Float cw = c;
        cw.truncateToApprox(wp);
        Float zw = z;
        zw.truncateToApprox(wp);

        // c a non-positive integer → pole unless a or b terminates first
        if (!cw.isPositive() && cw.isInteger()) {
            int ci = static_cast<int>(cw.toDouble());
            bool a_ok = aw.isNegative() && aw.isInteger() && static_cast<int>(aw.toDouble()) >= ci;
            bool b_ok = bw.isNegative() && bw.isInteger() && static_cast<int>(bw.toDouble()) >= ci;
            if (!a_ok && !b_ok) return Float::nan();
        }

        // Detect the polynomial case
        bool is_poly = false;
        int poly_terms = 0;
        if (aw.isNegative() && aw.isInteger()) {
            is_poly = true;
            poly_terms = -static_cast<int>(aw.toDouble()) + 1;
        } else if (bw.isNegative() && bw.isInteger()) {
            is_poly = true;
            poly_terms = -static_cast<int>(bw.toDouble()) + 1;
        }

        double z_val = zw.toDouble();
        double z_abs = std::abs(z_val);

        // |z| >= 1 and non-polynomial → diverges
        if (!is_poly && z_abs >= 1.0) return Float::nan();

        // --- Pfaff transformation: z < -0.5 ---
        // ₂F₁(a,b;c;z) = (1-z)^{-a} · ₂F₁(a, c-b; c; z/(z-1))
        // z/(z-1) ∈ (0, 1/3], so Taylor converges fast
        if (!is_poly && z_val < -0.5) {
            Float one_minus_z = Float(1) - zw;
            one_minus_z.truncateToApprox(wp);
            Float z_mapped = zw / (zw - Float(1));
            z_mapped.truncateToApprox(wp);
            Float prefix = pow(one_minus_z, -aw, wp);
            prefix.truncateToApprox(wp);
            Float inner = hyperg(aw, cw - bw, cw, z_mapped, wp);
            inner.truncateToApprox(wp);
            Float result = prefix * inner;
            finalizeResult(result, eff, precision);
            return result;
        }

        // --- DLMF 15.8.1 connection formula: 0.5 ≤ z < 1 ---
        // ₂F₁(a,b;c;z) = Γ(c)Γ(c-a-b) / [Γ(c-a)Γ(c-b)] · ₂F₁(a,b;a+b-c+1;1-z)
        //              + (1-z)^{c-a-b} · Γ(c)Γ(a+b-c) / [Γ(a)Γ(b)] · ₂F₁(c-a,c-b;c-a-b+1;1-z)
        // Since 1-z ∈ (0, 0.5), both ₂F₁ converge via Taylor.
        // ★ z=0.5 is strictly excluded (with >=, w=1-z=0.5 maps to itself and causes infinite recursion; fixed 2026-06-02).
        //   z=0.5 converges via direct Taylor (ratio→0.5), so leave it to the fall-through below.
        if (!is_poly && z_val > 0.5) {
            Float w = Float(1) - zw;  // w ∈ (0, 0.5]
            w.truncateToApprox(wp);
            Float cab = cw - aw - bw;
            cab.truncateToApprox(wp);

            // If c-a-b is near an integer, use direct Taylor (due to precision loss)
            double cab_val = cab.toDouble();
            double cab_frac = cab_val - std::floor(cab_val);
            if (cab_frac < 0.01 || cab_frac > 0.99) {
                // Near integer: the connection formula is unstable, so use direct Taylor with extra guard bits
                wp = precision + 80;
                aw.truncateToApprox(wp);
                bw.truncateToApprox(wp);
                cw.truncateToApprox(wp);
                zw = z;
                zw.truncateToApprox(wp);

                Float term(1);
                Float sum(1);
                for (int k = 0; k < 10 * wp; k++) {
                    term = term * (aw + Float(k)) * (bw + Float(k)) * zw
                         / ((cw + Float(k)) * (k + 1));
                    term.truncateToApprox(wp);
                    sum = sum + term;
                    sum.truncateToApprox(wp);

                    if (k >= 3 && term.isZero()) break;
                    if (k >= 3) {
                        auto t_bits = term.exponent() + static_cast<int64_t>(term.mantissa().bitLength());
                        auto s_bits = sum.exponent() + static_cast<int64_t>(sum.mantissa().bitLength());
                        if (s_bits - t_bits > Float::precisionToBits(wp) + 5) break;
                    }
                }
                finalizeResult(sum, eff, precision);
                return sum;
            }

            // Compute the two terms of the connection formula
            Float gc = gamma(cw, wp);
            Float g_cab = gamma(cab, wp);
            Float g_neg_cab = gamma(-cab, wp);  // Note: Γ(a+b-c) is Γ(-(c-a-b)), not Γ(-cab)

            // Term 1: Γ(c)·Γ(c-a-b) / [Γ(c-a)·Γ(c-b)] · ₂F₁(a, b; a+b-c+1; 1-z)
            Float coeff1 = gc * g_cab / (gamma(cw - aw, wp) * gamma(cw - bw, wp));
            coeff1.truncateToApprox(wp);
            Float f1 = hyperg(aw, bw, aw + bw - cw + Float(1), w, wp);
            f1.truncateToApprox(wp);

            // Term 2: (1-z)^{c-a-b} · Γ(c)·Γ(a+b-c) / [Γ(a)·Γ(b)] · ₂F₁(c-a, c-b; c-a-b+1; 1-z)
            Float ab_minus_c = -(cab);  // a+b-c
            Float coeff2 = gc * gamma(ab_minus_c, wp) / (gamma(aw, wp) * gamma(bw, wp));
            coeff2.truncateToApprox(wp);
            Float f2 = hyperg(cw - aw, cw - bw, cab + Float(1), w, wp);
            f2.truncateToApprox(wp);
            Float w_power = pow(w, cab, wp);
            w_power.truncateToApprox(wp);

            Float result = coeff1 * f1 + coeff2 * w_power * f2;
            finalizeResult(result, eff, precision);
            return result;
        }

        // --- Direct Taylor series: |z| < 0.5 or polynomial ---
        Float term(1);
        Float sum(1);
        Float tmp(0);
        int max_iter = is_poly ? poly_terms : 10 * wp;

        for (int k = 0; k < max_iter; k++) {
            // term *= (a+k)*(b+k)*z / ((c+k)*(k+1))
            Float fk(k);
            FloatOps::add(aw, fk, tmp);
            FloatOps::mul(term, tmp, term);
            FloatOps::add(bw, fk, tmp);
            FloatOps::mul(term, tmp, term);
            FloatOps::mul(term, zw, term);
            FloatOps::add(cw, fk, tmp);
            FloatOps::mul(tmp, Float(k + 1), tmp);
            FloatOps::div(term, tmp, term);
            term.truncateToApprox(wp);
            FloatOps::add(sum, term, sum);
            sum.truncateToApprox(wp);

            if (!is_poly && k >= 3 && term.isZero()) break;
            if (!is_poly && k >= 3) {
                auto t_bits = term.exponent() + static_cast<int64_t>(term.mantissa().bitLength());
                auto s_bits = sum.exponent() + static_cast<int64_t>(sum.mantissa().bitLength());
                if (s_bits - t_bits > Float::precisionToBits(wp) + 5) break;
            }
        }

        finalizeResult(sum, eff, precision);
        return sum;
    }

    Float hyperg(Float&& a, Float&& b, Float&& c, Float&& z, int precision) {
        return hyperg(static_cast<const Float&>(a), static_cast<const Float&>(b),
                      static_cast<const Float&>(c), static_cast<const Float&>(z), precision);
    }

    //=============================================================================
    // Legendre polynomials P_n(x)
    //=============================================================================
    // Bonnet recurrence: (n+1)P_{n+1} = (2n+1)·x·P_n - n·P_{n-1}

    Float legendreP(int n, const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (n < 0) return Float::nan();
        if (n == 0) return Float(1);
        int eff_x = x.effectiveBits();
        if (n == 1) { Float r = x; finalizeResult(r, eff_x, precision); return r; }

        int wp = precision + 10;
        Float xw = x;
        xw.truncateToApprox(wp);

        Float p_prev(1);    // P_0
        Float p_curr = xw;  // P_1

        for (int k = 1; k < n; k++) {
            Float p_next = ((2 * k + 1) * xw * p_curr - k * p_prev) / (k + 1);
            p_next.truncateToApprox(wp);
            p_prev = p_curr;
            p_curr = p_next;
        }

        finalizeResult(p_curr, eff_x, precision);
        return p_curr;
    }

    Float legendreP(int n, Float&& x, int precision) {
        return legendreP(n, static_cast<const Float&>(x), precision);
    }

    //=============================================================================
    // Associated Legendre functions P_n^m(x)
    //=============================================================================
    // Without the Condon-Shortley phase factor
    // P_m^m = (2m-1)!! · (1-x²)^{m/2}
    // P_{m+1}^m = x·(2m+1)·P_m^m
    // (k-m+1)P_{k+1}^m = (2k+1)·x·P_k^m - (k+m)·P_{k-1}^m

    Float assocLegendreP(int n, int m, const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (n < 0 || m < 0) return Float::nan();
        if (m > n) return Float(0);
        if (m == 0) return legendreP(n, x, precision);

        int eff_x = x.effectiveBits();
        int wp = precision + 15;
        Float xw = x;
        xw.truncateToApprox(wp);

        // (1 - x²)^{1/2}
        Float sin2 = Float(1) - xw * xw;
        sin2.truncateToApprox(wp);
        if (sin2.isNegative()) sin2 = Float(0);
        Float sin_factor = sqrt(sin2, wp);

        // P_m^m = (2m-1)!! · (1-x²)^{m/2}
        Float pmm(1);
        for (int i = 1; i <= m; i++) {
            pmm = pmm * (2 * i - 1) * sin_factor;
            pmm.truncateToApprox(wp);
        }

        if (n == m) { finalizeResult(pmm, eff_x, precision); return pmm; }

        // P_{m+1}^m = x·(2m+1)·P_m^m
        Float pm1m = xw * (2 * m + 1) * pmm;
        pm1m.truncateToApprox(wp);

        if (n == m + 1) { finalizeResult(pm1m, eff_x, precision); return pm1m; }

        // Recurrence
        Float p_prev = pmm;
        Float p_curr = pm1m;

        for (int k = m + 1; k < n; k++) {
            Float p_next = ((2 * k + 1) * xw * p_curr - (k + m) * p_prev) / (k - m + 1);
            p_next.truncateToApprox(wp);
            p_prev = p_curr;
            p_curr = p_next;
        }

        finalizeResult(p_curr, eff_x, precision);
        return p_curr;
    }

    Float assocLegendreP(int n, int m, Float&& x, int precision) {
        return assocLegendreP(n, m, static_cast<const Float&>(x), precision);
    }

    //=============================================================================
    // Hermite polynomials H_n(x) — physicists' version
    //=============================================================================
    // H_0(x) = 1, H_1(x) = 2x
    // H_{n+1}(x) = 2x·H_n(x) - 2n·H_{n-1}(x)

    Float hermite(int n, const Float& x, int precision) {
        if (n < 0) return Float::nan();
        if (n == 0) return Float::one(precision);

        int wp = precision + 10;
        Float two(2);
        Float h_prev = Float::one(wp);       // H_0
        Float h_curr = two * x;              // H_1
        h_curr.truncateToApprox(wp);

        for (int k = 1; k < n; ++k) {
            Float h_next = two * x * h_curr - two * k * h_prev;
            h_next.truncateToApprox(wp);
            h_prev = std::move(h_curr);
            h_curr = std::move(h_next);
        }

        finalizeResult(h_curr, x.effectiveBits(), precision);
        return h_curr;
    }

    Float hermite(int n, Float&& x, int precision) {
        return hermite(n, static_cast<const Float&>(x), precision);
    }

    //=============================================================================
    // Laguerre polynomials L_n(x)
    //=============================================================================
    // L_0(x) = 1, L_1(x) = 1 - x
    // (n+1)·L_{n+1}(x) = (2n+1-x)·L_n(x) - n·L_{n-1}(x)

    Float laguerre(int n, const Float& x, int precision) {
        if (n < 0) return Float::nan();
        if (n == 0) return Float::one(precision);

        int wp = precision + 10;
        const int wp_bits = Float::precisionToBits(wp);
        // BUGFIX (2026-05-30): the recurrence's /(k+1) falls to default_precision via exact/exact
        // division for exact inputs (e.g. x=0.5). Make the working values non-exact so the
        // division is performed at wp precision (same idiom as AUDIT_FLOAT_UNIT_MIXING step4 / the erfcx family).
        Float l_prev = Float::one(wp);          // L_0
        l_prev.setEffectiveBits(wp_bits);
        Float l_curr = Float::one(wp) - x;      // L_1
        l_curr.truncateToApprox(wp);
        l_curr.setEffectiveBits(wp_bits);

        for (int k = 1; k < n; ++k) {
            Float l_next = (Float(2 * k + 1) - x) * l_curr - k * l_prev;
            // Float/Float division (extends to working precision). Float/int (divScalarF) only
            // integer-divides the mantissa without extending precision, breaking for exact inputs.
            l_next = l_next / Float(k + 1);
            l_next.truncateToApprox(wp);
            l_prev = std::move(l_curr);
            l_curr = std::move(l_next);
        }

        finalizeResult(l_curr, x.effectiveBits(), precision);
        return l_curr;
    }

    Float laguerre(int n, Float&& x, int precision) {
        return laguerre(n, static_cast<const Float&>(x), precision);
    }

    //=============================================================================
    // Associated Laguerre polynomials L_n^m(x)
    //=============================================================================
    // L_0^m(x) = 1, L_1^m(x) = 1 + m - x
    // (n+1)·L_{n+1}^m(x) = (2n+1+m-x)·L_n^m(x) - (n+m)·L_{n-1}^m(x)

    Float assocLaguerre(int n, int m, const Float& x, int precision) {
        if (n < 0) return Float::nan();
        if (n == 0) return Float::one(precision);

        int wp = precision + 10;
        const int wp_bits = Float::precisionToBits(wp);
        // BUGFIX (2026-05-30): avoid exact/exact poison of /(k+1) (same form as laguerre).
        Float l_prev = Float::one(wp);                    // L_0^m
        l_prev.setEffectiveBits(wp_bits);
        Float l_curr = Float(1 + m) - x;                  // L_1^m
        l_curr.truncateToApprox(wp);
        l_curr.setEffectiveBits(wp_bits);

        for (int k = 1; k < n; ++k) {
            Float l_next = (Float(2 * k + 1 + m) - x) * l_curr - (k + m) * l_prev;
            l_next = l_next / Float(k + 1);  // Float/Float (precision extension); Float/int does not work
            l_next.truncateToApprox(wp);
            l_prev = std::move(l_curr);
            l_curr = std::move(l_next);
        }

        finalizeResult(l_curr, x.effectiveBits(), precision);
        return l_curr;
    }

    Float assocLaguerre(int n, int m, Float&& x, int precision) {
        return assocLaguerre(n, m, static_cast<const Float&>(x), precision);
    }

    //=============================================================================
    // Lambert W₀(x) — principal branch
    //=============================================================================
    // W(x)·e^{W(x)} = x, W₀ ≥ -1
    // Halley iteration: cubic convergence

    // Body: takes x by value (assumes already moved)
    static Float lambertW0_core(Float x, int eff_x, int precision) {
        int wp = precision + 20;
        x.truncateToApprox(wp);

        // x < -1/e → out of domain
        Float neg_inv_e = Float(-1) / exp(Float(1), wp);
        neg_inv_e.truncateToApprox(wp);
        if (x < neg_inv_e) return Float::nan();

        // Initial estimate in double
        double xd = x.toDouble();
        double wd;
        if (xd < -0.3) {
            double p = std::sqrt(2.0 * (2.718281828459045 * xd + 1.0));
            wd = -1.0 + p - p * p / 3.0 + 11.0 * p * p * p / 72.0;
        } else if (xd <= 3.0) {
            if (xd <= 0.5) {
                wd = xd * (1.0 - xd);
            } else {
                double lnx1 = std::log(xd + 1.0);
                wd = 0.665 * (1.0 + 0.0195 * lnx1) * lnx1 + 0.04;
            }
        } else {
            double lnx = std::log(xd);
            wd = lnx - std::log(lnx);
        }

        Float w(wd);
        w.truncateToApprox(wp);

        // Halley iteration
        for (int i = 0; i < 10 * (wp / 50 + 1); i++) {
            Float ew = exp(w, wp);
            Float wew = w * ew;
            wew.truncateToApprox(wp);
            Float f = wew - x;
            f.truncateToApprox(wp);

            if (f.isZero()) break;

            // Convergence check
            auto f_bits = f.exponent() + static_cast<int64_t>(f.mantissa().bitLength());
            auto x_bits = x.isZero() ? int64_t(0) :
                          x.exponent() + static_cast<int64_t>(x.mantissa().bitLength());
            if (x_bits - f_bits > Float::precisionToBits(wp) + 5) break;

            Float wp1 = w + Float(1);
            wp1.truncateToApprox(wp);
            Float denom = ew * wp1 - (w + Float(2)) * f / ldexp(wp1, 1);
            denom.truncateToApprox(wp);
            if (denom.isZero()) break;

            w = w - f / denom;
            w.truncateToApprox(wp);
            // Halley convergence improves the effective precision of w each iteration.
            // Update eff so the effective_bits_ optimization of exp(w,wp) works correctly.
            w.setResultPrecision(wp);
        }

        finalizeResult(w, eff_x, precision);
        return w;
    }

    Float lambertW0(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);
        if (x.isInfinity() && x.isPositive()) return Float::positiveInfinity();
        return lambertW0_core(Float(x), x.effectiveBits(), precision);
    }

    Float lambertW0(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);
        if (x.isInfinity() && x.isPositive()) return Float::positiveInfinity();
        int eff = x.effectiveBits();
        return lambertW0_core(std::move(x), eff, precision);
    }

    //=============================================================================
    // Lambert W₋₁(x) — secondary branch
    //=============================================================================
    // Defined for -1/e ≤ x < 0. W₋₁ ≤ -1.

    // Body: takes x by value (assumes already moved)
    static Float lambertWm1_core(Float x, int eff_x, int precision) {
        int wp = precision + 20;
        x.truncateToApprox(wp);

        // x < -1/e or x >= 0 → out of domain
        Float neg_inv_e = Float(-1) / exp(Float(1), wp);
        neg_inv_e.truncateToApprox(wp);
        if (x < neg_inv_e || !x.isNegative()) return Float::nan();

        // Initial estimate in double
        double xd = x.toDouble();
        double wd;
        if (xd > -0.1) {
            double p = std::sqrt(2.0 * (2.718281828459045 * xd + 1.0));
            wd = -1.0 - p - p * p / 3.0 - 11.0 * p * p * p / 72.0;
        } else {
            double lnmx = std::log(-xd);
            wd = lnmx - std::log(-lnmx);
        }

        Float w(wd);
        w.truncateToApprox(wp);

        // Halley iteration
        for (int i = 0; i < 10 * (wp / 50 + 1); i++) {
            Float ew = exp(w, wp);
            Float wew = w * ew;
            wew.truncateToApprox(wp);
            Float f = wew - x;
            f.truncateToApprox(wp);

            if (f.isZero()) break;

            auto f_bits = f.exponent() + static_cast<int64_t>(f.mantissa().bitLength());
            auto x_bits = x.exponent() + static_cast<int64_t>(x.mantissa().bitLength());
            if (x_bits - f_bits > Float::precisionToBits(wp) + 5) break;

            Float wp1 = w + Float(1);
            wp1.truncateToApprox(wp);
            Float denom = ew * wp1 - (w + Float(2)) * f / ldexp(wp1, 1);
            denom.truncateToApprox(wp);
            if (denom.isZero()) break;

            w = w - f / denom;
            w.truncateToApprox(wp);
            w.setResultPrecision(wp);
        }

        finalizeResult(w, eff_x, precision);
        return w;
    }

    Float lambertWm1(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        return lambertWm1_core(Float(x), x.effectiveBits(), precision);
    }

    Float lambertWm1(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        int eff = x.effectiveBits();
        return lambertWm1_core(std::move(x), eff, precision);
    }

    //=============================================================================
    // Generalized exponential integral E_n(x) = ∫₁^∞ e^{-xt}/t^n dt
    //=============================================================================
    // E_1(x) = -Ei(-x) (x > 0)
    // Recurrence: k·E_{k+1}(x) = e^{-x} - x·E_k(x)
    // Continued fraction: for large x

    // Body: takes x by value (assumes already moved)
    // Compute E_1(x) via a continued fraction (fast convergence for x >= 1, no cancellation)
    // CF: E_1(x) = e^{-x} / (x + 1/(1 + 1/(x + 2/(1 + 2/(x + ...)))))
    // Modified Lentz method: converges via recurrences of a_i, b_i
    static Float expintN_cf(int n, const Float& x, int wp) {
        // Gauss CF: E_n(x) = e^{-x} * CF
        // b_0 = x + n, a_i = -i*(n-1+i), b_i = b_{i-1} + 2
        Float tiny(1);
        tiny.truncateToApprox(wp);
        tiny = tiny >> (3 * wp);  // ~2^{-3*wp} as tiny value

        Float b = x + Float(n);
        b.truncateToApprox(wp);
        Float c = Float(1) / tiny;
        Float d = Float(1) / b;
        Float h = d;

        for (int i = 1; i < 10 * wp; i++) {
            Float a = Float(-i) * Float(n - 1 + i);
            b = b + Float(2);
            b.truncateToApprox(wp);
            d = Float(1) / (a * d + b);
            d.truncateToApprox(wp);
            c = b + a / c;
            c.truncateToApprox(wp);
            Float delta = c * d;
            h = h * delta;
            h.truncateToApprox(wp);

            // Convergence check: |delta - 1| < 2^{-wp_bits}
            // BUGFIX (2026-05-30): -wp misused decimal digits as a bit exponent (unit mixing),
            // capping at ~0.3·P digits → -precisionToBits(wp).
            Float diff = delta - Float(1);
            if (i >= 3 && diff.isZero()) break;
            if (i >= 3) {
                auto d_bits = diff.exponent() + static_cast<int64_t>(diff.mantissa().bitLength());
                if (d_bits < -Float::precisionToBits(wp)) break;
            }
        }

        Float result = exp(-x, wp) * h;
        result.truncateToApprox(wp);
        return result;
    }

    static Float expintN_core(int n, Float x, int eff_x, int precision) {
        Float::PrecisionScope _ps(precision + 64);   // Compute exact÷exact inside the function at working precision
        int wp = precision + 40;  // More guard bits (to counter alternating-series cancellation)
        x.truncateToApprox(wp);
        x.setEffectiveBits(Float::precisionToBits(wp));  // avoid exact/exact poison of exact inputs

        // E_0(x) = e^{-x}/x
        if (n == 0) {
            Float result = exp(-x, wp) / x;
            finalizeResult(result, eff_x, precision);
            return result;
        }

        // x > 20: obtain E_1 via continued fraction, derive n > 1 via the upward recurrence
        // (applying the CF directly to n > 1 loses precision, so going through E_1 is stable).
        // BUGFIX (2026-05-30): raised the threshold from x>=1 to x>20. The Gauss CF has an x-dependent
        // ceiling on attainable precision (x=1→~84 digits, x=3→~144 digits), failing a 200-digit request for
        // moderate x. The series E_1(x)=-γ-ln x+Σ converges fully with ~0.43·x digits cancellation (≤9 for
        // x≤20, within the +40 guard), so use the series for x≤20 and the CF only for x>20 (ceiling high enough).
        if (x > Float(20)) {
            Float e1 = expintN_cf(1, x, wp);
            if (n == 1) { finalizeResult(e1, eff_x, precision); return e1; }

            Float emx = exp(-x, wp);
            Float en = e1;
            for (int k = 1; k < n; k++) {
                en = (emx - x * en) / Float(k);  // Float/Float (precision extension); Float/int does not work
                en.truncateToApprox(wp);
            }
            finalizeResult(en, eff_x, precision);
            return en;
        }

        // x <= 20: series E_1(x) = -γ - ln(x) + Σ_{k=1}^∞ (-1)^{k+1} x^k/(k·k!)
        Float e1;
        {
            Float euler_gamma = Float::euler(wp);
            Float ln_x = log(x, wp);

            Float sum = -euler_gamma - ln_x;
            Float term(1);
            for (int k = 1; k < 10 * wp; k++) {
                // Float/Float division (extends to working precision). Float/int (divScalarF) cannot extend
                // precision for small intermediate terms (occurring at x≳3) and breaks.
                term = term * (-x) / Float(k);
                term.truncateToApprox(wp);
                Float contribution = -term / Float(k);

                sum = sum + contribution;
                sum.truncateToApprox(wp);

                if (k >= 5 && contribution.isZero()) break;
                if (k >= 5) {
                    auto c_bits = contribution.exponent() + static_cast<int64_t>(contribution.mantissa().bitLength());
                    auto s_bits = sum.exponent() + static_cast<int64_t>(sum.mantissa().bitLength());
                    if (s_bits - c_bits > Float::precisionToBits(wp) + 5) break;
                }
            }
            e1 = sum;
        }

        if (n == 1) { finalizeResult(e1, eff_x, precision); return e1; }

        // Upward recurrence: k·E_{k+1}(x) = e^{-x} - x·E_k(x)
        Float emx = exp(-x, wp);
        Float en = e1;
        for (int k = 1; k < n; k++) {
            en = (emx - x * en) / k;
            en.truncateToApprox(wp);
        }

        finalizeResult(en, eff_x, precision);
        return en;
    }

    Float expintN(int n, const Float& x, int precision) {
        Float::PrecisionScope _ps(precision);   // Compute E_n(0)=1/(n-1) of the x=0 fast path at precision digits
        if (x.isNaN()) return Float::nan();
        if (n < 0) return Float::nan();
        if (x.isNegative()) return Float::nan();
        if (x.isZero()) {
            if (n <= 1) return Float::positiveInfinity();
            return Float(1) / (n - 1);
        }
        return expintN_core(n, Float(x), x.effectiveBits(), precision);
    }

    Float expintN(int n, Float&& x, int precision) {
        Float::PrecisionScope _ps(precision);   // Compute E_n(0)=1/(n-1) of the x=0 fast path at precision digits
        if (x.isNaN()) return Float::nan();
        if (n < 0) return Float::nan();
        if (x.isNegative()) return Float::nan();
        if (x.isZero()) {
            if (n <= 1) return Float::positiveInfinity();
            return Float(1) / (n - 1);
        }
        int eff = x.effectiveBits();
        return expintN_core(n, std::move(x), eff, precision);
    }

    //=============================================================================
    // Sine integral Si(x) = ∫₀ˣ sin(t)/t dt
    //=============================================================================
    // Si(x) = Σ_{k=0}^∞ (-1)^k · x^{2k+1} / ((2k+1)·(2k+1)!)
    // Odd function: Si(-x) = -Si(x)

    // Body: takes x by value (assumes already moved)
    static Float sinIntegral_core(Float x, int eff_x, int precision) {
        int wp = precision + 20;
        x.truncateToApprox(wp);
        x.setEffectiveBits(Float::precisionToBits(wp));  // avoid exact/exact poison of exact inputs

        // Odd function
        bool negate = x.isNegative();
        if (negate) x = -x;

        Float x2 = x * x;
        x2.truncateToApprox(wp);
        Float neg_x2 = -x2;

        // term tracks x^{2k+1} / (2k+1)!
        // Actually: Si(x) = Σ (-1)^k x^{2k+1} / ((2k+1)(2k+1)!)
        // term = (-1)^k x^{2k+1} / (2k+1)!, contribution = term / (2k+1)
        // term_{k+1} = term_k · (-x²) / ((2k+2)(2k+3))
        Float term = x;     // k=0: x^1 / 1!
        Float sum = x;      // k=0: x / 1
        Float contribution(0);

        for (int k = 0; k < 10 * wp; k++) {
            FloatOps::mul(term, neg_x2, term);
            FloatOps::div(term, Float(static_cast<int64_t>(2 * k + 2) * (2 * k + 3)), term);
            term.truncateToApprox(wp);
            FloatOps::div(term, Float(2 * k + 3), contribution);

            FloatOps::add(sum, contribution, sum);
            sum.truncateToApprox(wp);

            if (k >= 3 && contribution.isZero()) break;
            if (k >= 3) {
                auto c_bits = contribution.exponent() + static_cast<int64_t>(contribution.mantissa().bitLength());
                auto s_bits = sum.exponent() + static_cast<int64_t>(sum.mantissa().bitLength());
                if (s_bits - c_bits > Float::precisionToBits(wp) + 5) break;
            }
        }

        if (negate) sum = -sum;
        finalizeResult(sum, eff_x, precision);
        return sum;
    }

    Float sinIntegral(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);
        return sinIntegral_core(Float(x), x.effectiveBits(), precision);
    }

    Float sinIntegral(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (x.isZero()) return Float(0);
        int eff = x.effectiveBits();
        return sinIntegral_core(std::move(x), eff, precision);
    }

    //=============================================================================
    // Cosine integral Ci(x) = γ + ln|x| + ∫₀ˣ (cos(t)-1)/t dt
    //=============================================================================
    // Ci(x) = γ + ln(x) + Σ_{k=1}^∞ (-1)^k · x^{2k} / (2k·(2k)!)
    // x > 0 only

    // Body: takes x by value (assumes already moved)
    static Float cosIntegral_core(Float x, int eff_x, int precision) {
        int wp = precision + 20;
        x.truncateToApprox(wp);
        x.setEffectiveBits(Float::precisionToBits(wp));  // avoid exact/exact poison of exact inputs

        Float euler_gamma = Float::euler(wp);
        Float ln_x = log(x, wp);
        Float sum = euler_gamma + ln_x;
        sum.truncateToApprox(wp);

        Float x2 = x * x;
        x2.truncateToApprox(wp);
        Float neg_x2 = -x2;

        // term tracks (-1)^k · x^{2k} / (2k)!
        // term_{k} = term_{k-1} · (-x²) / ((2k-1)·2k)
        // contribution = term / (2k)
        Float term(1);  // k=0 initial value
        Float contribution(0);

        for (int k = 1; k < 10 * wp; k++) {
            FloatOps::mul(term, neg_x2, term);
            FloatOps::div(term, Float(static_cast<int64_t>(2 * k - 1) * (2 * k)), term);
            term.truncateToApprox(wp);
            FloatOps::div(term, Float(2 * k), contribution);

            FloatOps::add(sum, contribution, sum);
            sum.truncateToApprox(wp);

            if (k >= 3 && contribution.isZero()) break;
            if (k >= 3) {
                auto c_bits = contribution.exponent() + static_cast<int64_t>(contribution.mantissa().bitLength());
                auto s_bits = sum.exponent() + static_cast<int64_t>(sum.mantissa().bitLength());
                if (s_bits - c_bits > Float::precisionToBits(wp) + 5) break;
            }
        }

        finalizeResult(sum, eff_x, precision);
        return sum;
    }

    Float cosIntegral(const Float& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (!x.isPositive()) {
            if (x.isZero()) return Float::negativeInfinity();
            return Float::nan();
        }
        return cosIntegral_core(Float(x), x.effectiveBits(), precision);
    }

    Float cosIntegral(Float&& x, int precision) {
        if (x.isNaN()) return Float::nan();
        if (!x.isPositive()) {
            if (x.isZero()) return Float::negativeInfinity();
            return Float::nan();
        }
        int eff = x.effectiveBits();
        return cosIntegral_core(std::move(x), eff, precision);
    }

    //=============================================================================
    // Random number generation
    //=============================================================================

    // Generate a uniform random number in [0, 1) at precision-bit precision
    Float randomFloat(int precision) {
        static thread_local std::mt19937_64 rng(std::random_device{}());

        // Generate a precision-bit random mantissa
        int num_words = (precision + 63) / 64;
        std::vector<uint64_t> words(num_words);
        for (int i = 0; i < num_words; i++) {
            words[i] = rng();
        }

        // Mask off the extra bits
        int extra_bits = num_words * 64 - precision;
        if (extra_bits > 0 && num_words > 0) {
            words[num_words - 1] &= (uint64_t(-1) >> extra_bits);
        }

        // Convert to Int and multiply by 2^{-precision} → a value in [0, 1)
        Int mantissa = Int::fromRawWords(
            std::span<const uint64_t>(words.data(), words.size()), +1);

        if (mantissa.isZero()) return Float(0);

        // Float(mantissa, exponent): value = mantissa * 2^exponent
        // mantissa is an integer of at most precision bits, so
        // exponent = -precision yields [0, 1)
        Float result(mantissa, -static_cast<int64_t>(precision));
        result.setResultPrecision(precision);
        return result;
    }

    // Uniform random number in [lo, hi)
    Float randomFloat(const Float& lo, const Float& hi, int precision) {
        Float r = randomFloat(precision);
        Float result = lo + r * (hi - lo);
        result.setResultPrecision(precision);
        return result;
    }

    Float randomFloat(Float&& lo, Float&& hi, int precision) {
        Float r = randomFloat(precision);
        Float range = std::move(hi) - lo;
        Float result = std::move(lo) + r * std::move(range);
        result.setResultPrecision(precision);
        return result;
    }

    //=============================================================================
    // normalRandom — normally distributed random number N(0,1), Box-Muller method
    //=============================================================================

    Float normalRandom(int precision) {
        // Box-Muller method: from u1, u2 ∈ (0, 1)
        // z = sqrt(-2·ln(u1)) · cos(2π·u2)
        int wp = precision + 10;

        // Generate u1 in (0, 1) (avoid 0: log(0) = -∞)
        Float u1;
        do {
            u1 = randomFloat(wp);
        } while (u1.isZero());

        Float u2 = randomFloat(wp);

        // r = sqrt(-2 * ln(u1))
        Float neg2ln = Float(-2) * log(u1, wp);
        Float r = sqrt(std::move(neg2ln), wp);

        // theta = 2π * u2
        Float theta = ldexp(Float::pi(wp), 1) * u2;

        Float result = r * cos(theta, wp);
        result.setResultPrecision(precision);
        return result;
    }

    //=============================================================================
    // exponentialRandom — exponentially distributed random number Exp(1), inverse-transform method
    //=============================================================================

    Float exponentialRandom(int precision) {
        // Inverse-transform method: -ln(u), u ∈ (0, 1)
        int wp = precision + 10;

        Float u;
        do {
            u = randomFloat(wp);
        } while (u.isZero());

        Float result = -log(u, wp);
        result.setResultPrecision(precision);
        return result;
    }

    //=============================================================================
    // canRound — rounding direction decision (compatible with MPFR mpfr_can_round)
    //=============================================================================

    bool canRound(const Float& x, int err_bits,
                  RoundingMode rnd1, RoundingMode rnd2,
                  int target_prec) {
        // Special values are always roundable
        if (x.isZero() || x.isNaN() || x.isInfinity()) return true;

        // Compute the target bit count
        int target_bits = Float::precisionToBits(target_prec);

        // Number of guard bits = accurate bit count - target bit count
        int guard_bits = err_bits - target_bits;
        if (guard_bits <= 0) return false;  // insufficient precision

        // Logic equivalent to the internal canRoundCorrectly
        int bit_length = static_cast<int>(x.mantissa().bitLength());
        if (bit_length <= target_bits) return true;  // no rounding needed

        // Number of guard bits usable for the decision (margin=2 accounting for computation error)
        constexpr int MARGIN = 2;
        int usable = guard_bits - MARGIN;
        if (usable < 1) return false;

        // Scan the guard bit region
        // shift = bit_length - target_bits: the number of bits below the rounding position
        int shift = bit_length - target_bits;
        const uint64_t* data = x.mantissa().data();

        // Read the usable guard bits
        // Position: bit (shift - 1) is the most significant guard bit
        int hi_pos = shift - 1;
        int lo_pos = shift - usable;
        if (lo_pos < 0) lo_pos = 0;

        bool all_zero = true;
        bool all_one  = true;

        size_t lo_word = static_cast<size_t>(lo_pos) / 64;
        size_t hi_word = static_cast<size_t>(hi_pos) / 64;

        for (size_t w = lo_word; w <= hi_word; ++w) {
            uint64_t word = data[w];

            unsigned lo_bit = (w == lo_word) ? static_cast<unsigned>(lo_pos % 64) : 0;
            unsigned hi_bit = (w == hi_word) ? static_cast<unsigned>(hi_pos % 64) : 63;
            unsigned width  = hi_bit - lo_bit + 1;

            uint64_t mask;
            if (width >= 64) {
                mask = ~uint64_t(0);
            } else {
                mask = ((uint64_t(1) << width) - 1) << lo_bit;
            }

            uint64_t bits = word & mask;
            if (bits != 0)    all_zero = false;
            if (bits != mask) all_one  = false;

            if (!all_zero && !all_one) return true;
        }

        // All 0 or all 1 → too close to the rounding boundary, direction unknown
        return false;
    }

    //=============================================================================
    // Float binary serialization
    //=============================================================================

    std::vector<uint8_t> exportBinary(const Float& value) {
        // tag: 0x00=zero, 0x01=normal, 0x02=infinity, 0x03=nan
        if (value.isNaN()) {
            return {0x03};
        }
        if (value.isInfinity()) {
            uint8_t sign = value.isNegative() ? 0xFF : 0x00;
            return {0x02, sign};
        }
        if (value.isZero()) {
            return {0x00};
        }

        // Normal value
        // [tag:1][sign:1][exponent:8 LE][eff:4 LE][req:4 LE][mantissa_binary...]
        uint8_t sign = value.isNegative() ? 0xFF : 0x00;
        int64_t exp = value.exponent();
        int eff = value.effectiveBits();
        int req = value.requestedBits();

        // Serialize the mantissa
        auto mant_bin = IntIOUtils::exportBinary(value.mantissa());

        std::vector<uint8_t> result;
        result.reserve(1 + 1 + 8 + 4 + 4 + mant_bin.size());
        result.push_back(0x01);  // tag
        result.push_back(sign);
        // exponent LE
        for (int i = 0; i < 8; ++i)
            result.push_back(static_cast<uint8_t>((static_cast<uint64_t>(exp) >> (i * 8)) & 0xFF));
        // effective_bits LE
        for (int i = 0; i < 4; ++i)
            result.push_back(static_cast<uint8_t>((static_cast<uint32_t>(eff) >> (i * 8)) & 0xFF));
        // requested_bits LE
        for (int i = 0; i < 4; ++i)
            result.push_back(static_cast<uint8_t>((static_cast<uint32_t>(req) >> (i * 8)) & 0xFF));
        // mantissa binary
        result.insert(result.end(), mant_bin.begin(), mant_bin.end());

        return result;
    }

    Float importBinaryFloat(std::span<const uint8_t> data) {
        if (data.empty()) {
            throw std::invalid_argument("importBinaryFloat: empty data");
        }

        uint8_t tag = data[0];

        if (tag == 0x00) return Float::zero();
        if (tag == 0x03) return Float::nan();
        if (tag == 0x02) {
            if (data.size() < 2)
                throw std::invalid_argument("importBinaryFloat: truncated infinity");
            return data[1] == 0xFF ? Float::negativeInfinity() : Float::positiveInfinity();
        }

        if (tag != 0x01)
            throw std::invalid_argument("importBinaryFloat: unknown tag");

        // Normal: [tag:1][sign:1][exp:8][eff:4][req:4][mantissa...]
        if (data.size() < 18)
            throw std::invalid_argument("importBinaryFloat: truncated normal value");

        bool neg = (data[1] == 0xFF);

        int64_t exp = 0;
        for (int i = 0; i < 8; ++i)
            exp |= static_cast<int64_t>(data[2 + i]) << (i * 8);

        int eff = 0;
        for (int i = 0; i < 4; ++i)
            eff |= static_cast<int>(data[10 + i]) << (i * 8);

        int req = 0;
        for (int i = 0; i < 4; ++i)
            req |= static_cast<int>(data[14 + i]) << (i * 8);

        // mantissa
        auto mant_span = data.subspan(18);
        Int mantissa = IntIOUtils::importBinary(mant_span);

        Float result(std::move(mantissa), exp, neg);
        result.effective_bits_ = eff;
        result.requested_bits_ = req;
        return result;
    }

} // namespace sangi
