decimal-scaled 0.5.0

Const-generic base-10 fixed-point decimals (D18/D38/D76/D153/D307 and the half-width tiers up to D1232) with integer-only transcendentals correctly rounded to within 0.5 ULP — exact at the type's last representable place. Deterministic across every platform; no_std-friendly.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
// SPDX-FileCopyrightText: 2026 John Moxley
// SPDX-License-Identifier: MIT OR Apache-2.0

//! Tier-generic Tang-style table-driven `exp_strict` kernel.
//!
//! Tang 1989, "Table-driven implementation of the exponential function
//! in IEEE floating-point arithmetic" (ACM TOMS 16(4)):
//!
//! ```text
//! e^v = 2^k · e^s,            s = v − k·ln 2,           |s| ≤ ln 2 / 2
//!     = 2^k · e^(c_j) · e^δ,  c_j = j · ln 2 / M,       j ∈ [0, M)
//!                              δ  = s − c_j,            |δ| ≤ ln 2 / (2M)
//! ```
//!
//! A two-stage range reduction collapses the post-stage-1 Taylor into a
//! table multiply (`exp(c_j)` read from the baked `M`-entry
//! `exp_tang_table` consts, the indexed slot converted to the working
//! scale per lookup) plus a short Taylor on the tiny remainder `δ`. The
//! result is reassembled as `2^(k+k_adj) · table[j] · e^δ`.
//!
//! ## Layering
//!
//! This is an **algorithm function** (`docs/ARCHITECTURE.md` →
//! "Layering direction"): it computes only through the
//! [`WideTrigCore`] trait surface and `BigInt` arithmetic on the work
//! integer; it never calls a method on a decimal type. `policy::exp`
//! calls [`exp_tang`] *down*; the type's `exp_strict` method delegates
//! *down* through the policy. The trig hyperbolic kernels reuse
//! [`tang_exp_fixed`] directly for their shared `(e^v, e^-v)` pair.
//!
//! Collapses the four per-tier D57 (18..=22 / 45..=56), D115
//! and D153 Tang exp kernels
//! kernels into one generic over `C: WideTrigCore`, the table size `M`,
//! and the per-band reduction/narrowing flags.

use crate::algos::exp::exp_generic as eg;
use crate::algos::support::exp_tang_table::exp_table_entry_baked;
use crate::algos::support::wide_trig_core::WideTrigCore;
use crate::int::types::compute_limbs::ComputeLimbs;
use crate::int::types::traits::BigInt;
use crate::support::rounding::RoundingMode;

/// Tang-style `e^v_w` on an already-lifted working value `v_w` (`= x ·
/// 10^w`), returned at working scale `w`. Generic over the tier `C` and
/// the table size `M`.
///
/// `INTERNAL_EXTRA` selects the large-`k` mitigation. For `k > 0` the final
/// `2^k` reassembly (a LEFT shift) amplifies the reduction residual by
/// `2^k ≈ 10^(k·log10 2)` decimal digits, so a fixed narrow guard cannot
/// cover an unbounded `k`. When `true` the whole reduction runs at an
/// extended working scale `w + extra` (`extra = ceil(k·log10 2) + 12`, sized
/// for `k > 0` only) and the result is narrowed back to `w`
/// round-to-nearest; when `false` the body runs at the caller-supplied `w`
/// (the caller absorbs the `extra` lift in its own guard, or the band's `k`
/// is small enough not to need it). For `k ≤ 0` the reassembly is an
/// error-shrinking RIGHT shift, so no lift is taken (`extra = 0`) — see the
/// body comment. This is the shared surface the trig hyperbolic kernels reuse.
#[must_use]
pub(crate) fn tang_exp_fixed<
    C: WideTrigCore,
    const M: u32,
    const INTERNAL_EXTRA: bool,
    const SCALE: u32,
>(
    v_w: C::W,
    w: u32,
) -> C::W
where
    <C::W as BigInt>::Scratch: ComputeLimbs,
{
    // Thin `WideTrigCore`-bound wrapper over the width-generic
    // [`tang_exp_fixed_g`]: binds the work integer to `C::W` and supplies
    // `ln 2` from `C::ln2::<SCALE>` (the crate's feature-flagged default
    // rounding mode + the per-scale const-fold). One Tang `exp` kernel — the
    // wide compositions call `tang_exp_fixed_g` directly at their `Wagm` work
    // width.
    tang_exp_fixed_g::<C::W, M, INTERNAL_EXTRA>(v_w, w, |ww| C::ln2::<SCALE>(ww))
}

/// Width-generic core of [`tang_exp_fixed`] — the Tang `exp` body over any
/// [`BigInt`] work integer `S`, reusing the unified `exp_generic` fixed-point
/// arithmetic leaves (the sibling of [`crate::algos::ln::ln_tang::tang_ln_fixed_g`]).
///
/// `ln 2` is supplied by an accessor `ln2(working_scale)` so the caller owns
/// the rounding mode (the crate's feature-flagged default — never a hardcoded
/// one); the Tang `exp` table is the already-width-generic
/// [`exp_table_entry_baked`] (binary, scale-independent). `tang_exp_fixed::<C>`
/// is the thin tier-bound wrapper; the wide compositions (`powf`/`exp2`/the
/// hyperbolics) call this directly at their `Wagm` work width.
#[must_use]
pub(crate) fn tang_exp_fixed_g<S: BigInt, const M: u32, const INTERNAL_EXTRA: bool>(
    v_w: S,
    w: u32,
    ln2: impl Fn(u32) -> S,
) -> S
where
    S::Scratch: ComputeLimbs,
{
    // Stage 0 (INTERNAL_EXTRA only): size an extended working scale
    // `w_ext = w + extra` from `|k|` so the `2^k` reassembly does not
    // amplify the reduction residual past the storage LSB. Matches the
    // dynamic-margin reduction the generic `exp_fixed` uses (Muller,
    // *Elementary Functions* 3rd ed., §11.1). `k` is scale-invariant, so
    // reuse the value computed at `w` below.
    let k = {
        let one_w = eg::one::<S>(w);
        eg::round_to_nearest_int::<S>(eg::div_cached::<S>(v_w, ln2(w), one_w), w)
    };

    let (w_ext, v_ext, extra) = if INTERNAL_EXTRA {
        // Size the extended scale from `k` only for `k > 0`: the `2^k`
        // reassembly amplifies the residual only on the LEFT shift. For `k < 0`
        // (underflow) the reassembly is an error-shrinking RIGHT shift, so the
        // base scale already suffices — and inflating `w_ext` there would drive
        // the table-entry product `slot_hi · 10^w_ext` (≈ `2·w_ext·log2(10)`
        // bits) past the work integer `S`, silently wrapping `exp(c_j)`. (Same
        // asymmetry the `EXTERNAL_EXTRA` wrapper applies.)
        let extra: u32 = if k <= 0 {
            0
        } else {
            let digits = (k as u128 * 30103).div_ceil(100_000) as u32;
            digits + 12
        };
        let v_ext = if extra == 0 {
            v_w
        } else {
            v_w * eg::pow10::<S>(extra)
        };
        (w + extra, v_ext, extra)
    } else {
        (w, v_w, 0)
    };

    // Overflow gate (up front, before any `w_ext`-scale work). The body runs at
    // the extended scale `w_ext` — `one_w = 10^w_ext` and the `2^k` reassembly
    // `exp_s << k` — so a result too large to represent needs `w_ext` digits
    // (`≈ w_ext·log2(10)` bits) PLUS the `k`-bit shift to exceed `S`. Without
    // this gate the `10^w_ext` literal alone silently WRAPS once it passes `S`'s
    // width (e.g. exp2(1005) = e^696.7: `w_ext ≈ 372` ⇒ ~1236 bits > Wagm's
    // 1024 ⇒ garbage), and the result came back as 0 instead of panicking. A
    // fixed-width decimal has no infinity: PANIC, uniform across debug AND
    // release (the strict-transcendental overflow contract). In-range results
    // fit `S` (wider than storage) with room, so this never fires for a
    // representable cell. digits→bits: `log2(10) ≈ 3322/1000`.
    {
        let peak_bits =
            (w_ext as u64) * 3322 / 1000 + if k >= 0 { k as u64 } else { 0 };
        if peak_bits >= <S as BigInt>::BITS as u64 {
            panic!("tang_exp_fixed: result out of range");
        }
    }

    let one_w = eg::one::<S>(w_ext);
    let pow10_w = one_w;
    let l2 = ln2(w_ext);

    // Stage 1: v = k·ln 2 + s, |s| ≤ ln 2 / 2.
    let k_l2 = if k >= 0 {
        l2 * eg::lit::<S>(k)
    } else {
        -(l2 * eg::lit::<S>(-k))
    };
    let s = v_ext - k_l2;

    // Stage 2: s = j_signed · (ln 2 / M) + δ, |δ| ≤ ln 2 / (2M).
    let j_signed =
        eg::round_to_nearest_int::<S>(eg::div_cached::<S>(s * eg::lit::<S>(M as i128), l2, pow10_w), w_ext);
    let cj_signed_w = if j_signed >= 0 {
        (l2 * eg::lit::<S>(j_signed)) / eg::lit::<S>(M as i128)
    } else {
        -((l2 * eg::lit::<S>(-j_signed)) / eg::lit::<S>(M as i128))
    };
    let delta = s - cj_signed_w;
    let (j_idx, k_adj) = if j_signed >= 0 {
        (j_signed as u32, 0i128)
    } else {
        ((j_signed + M as i128) as u32, -1i128)
    };
    debug_assert!(j_idx < M, "tang_exp_fixed: table index out of range");

    // Taylor on δ.
    let mut sum = one_w + delta;
    let mut term = delta;
    let mut n: u128 = 2;
    loop {
        term = eg::mul::<S>(term, delta, w_ext) / eg::lit::<S>(n as i128);
        if term == eg::zero::<S>() {
            break;
        }
        sum = sum + term;
        n += 1;
        if n > 200 {
            break;
        }
    }

    let exp_cj = exp_table_entry_baked::<S>(w_ext, j_idx as usize, M, pow10_w);
    let exp_s = eg::mul::<S>(exp_cj, sum, w_ext);

    let k_total = k + k_adj;
    let scaled_at_w_ext = if k_total >= 0 {
        let shift = k_total as u32;
        // The `2^k` reassembly `exp_s << shift` wraps past `S`'s width once the
        // result is too large to represent — a genuinely out-of-range exp. A
        // fixed-width decimal has no infinity, so PANIC, uniform across debug
        // AND release (the strict-transcendental overflow contract). This was a
        // `debug_assert!`, so a RELEASE build silently WRAPPED to garbage — e.g.
        // `exp2(1005)` (= e^696.7, far beyond every tier) returned 0 instead of
        // panicking, while `exp2(200)` (overflow that still fits `S`, panicking
        // later at the storage narrow) was correct: a tier/scale-INVARIANT
        // violation the full-surface golden surfaced.
        if eg::bit_length::<S>(exp_s) + shift >= <S as BigInt>::BITS {
            panic!("tang_exp_fixed: result out of range");
        }
        exp_s << shift
    } else {
        let neg_k = (-k_total) as u32;
        if neg_k as u128 >= eg::bit_length::<S>(exp_s) as u128 {
            // Deep underflow: `e^v` (`v < 0` here, since `k_total < 0`) is
            // strictly positive but below the working resolution. Return the
            // smallest positive working value (`1 = 10^-w_ext`), NOT a bare
            // zero, so the caller's directed narrowing keeps the sign —
            // Ceiling rounds UP to one storage ULP while Floor / Trunc /
            // nearest still give 0. A bare zero loses positivity and rounds
            // Ceiling to 0 (the `powf("2","-200")` mid-scale defect). This
            // matches `exp_generic::try_exp_fixed`'s deep-underflow return.
            eg::lit::<S>(1)
        } else {
            exp_s >> neg_k
        }
    };

    // `e^v > 0` for every finite `v`: a zero result on the `k_total < 0`
    // (underflow) branch is genuine sub-resolution underflow, NOT a true
    // zero — return the smallest positive working value so the directed
    // narrowing keeps the sign (Ceiling → 1 ULP). Mirrors the `exp_generic`
    // catch-all; restricted to `k_total < 0`, the only regime where
    // underflow to 0 is physical.
    let scaled_at_w_ext = if k_total < 0 && scaled_at_w_ext == eg::zero::<S>() {
        eg::lit::<S>(1)
    } else {
        scaled_at_w_ext
    };

    if !INTERNAL_EXTRA || extra == 0 {
        scaled_at_w_ext
    } else {
        // Narrow the extended-scale result back to `w` round-to-nearest
        // (ties up via the `+ half` bias). `extra` is bounded so
        // `10^extra` stays well inside the working width.
        let p = eg::pow10::<S>(extra);
        let half = p / eg::lit::<S>(2);
        if scaled_at_w_ext >= eg::zero::<S>() {
            (scaled_at_w_ext + half) / p
        } else {
            -((-scaled_at_w_ext + half) / p)
        }
    }
}

/// Tier-generic Tang-style `e^x` strict kernel.
///
/// - `C` — the per-tier [`WideTrigCore`] marker (`wide_trig_d*::Core`).
/// - `SCALE` — the decimal storage scale.
/// - `M` — the Tang table size (`128` or `512`).
/// - `GUARD` — the narrow guard for this band (`8`, `10`, or the tier's
///   canonical `30`).
/// - `DIRECTED` — route the final narrowing through the directed-rounding
///   Ziv escalation (`true`), else narrow once with
///   `round_to_storage_with` (`false`).
/// - `EXTERNAL_EXTRA` — compute the large-`|k|` working-scale lift `extra`
///   in this wrapper and fold it into the directed base guard (the D115
///   shape; requires `DIRECTED`).
/// - `INTERNAL_EXTRA` — let [`tang_exp_fixed`] do the `extra` lift +
///   narrow-back internally (the D153 shape).
#[inline]
#[must_use]
pub(crate) fn exp_tang<
    C: WideTrigCore,
    const SCALE: u32,
    const M: u32,
    const GUARD: u32,
    const DIRECTED: bool,
    const EXTERNAL_EXTRA: bool,
    const INTERNAL_EXTRA: bool,
>(
    raw: C::Storage,
    mode: RoundingMode,
) -> C::Storage
where
    <C::W as BigInt>::Scratch: ComputeLimbs,
{
    if raw == C::storage_zero() {
        return C::storage_one(SCALE);
    }

    if !DIRECTED && crate::support::rounding::is_nearest_mode(mode) {
        // Single-shot narrowing (D57 18..=22 and 45..=56) — NEAREST modes
        // only. Reduction runs at the const-folded `w = SCALE + GUARD`; the
        // band guard keeps the working error well under half a storage ULP,
        // so a single narrow is correctly rounded to nearest. Directed modes
        // (which must decide which SIDE of a grid line the true value lies,
        // and can sit a sub-resolution residual below the work-int's
        // resolution — `exp(-10^-S)` just under `1.0` at MAX scale) fall
        // through to the never-exact Ziv path below.
        let w = SCALE + GUARD;
        let v_w = C::to_work_scaled(raw, GUARD);
        let result = tang_exp_fixed::<C, M, INTERNAL_EXTRA, SCALE>(v_w, w);
        return C::round_to_storage_with(result, w, SCALE, mode);
    }

    let base_guard = if EXTERNAL_EXTRA {
        // The final reassembly is `exp_s << k` for `k ≥ 0` and `exp_s >> |k|`
        // for `k < 0`. Only the LEFT shift (`k ≥ 0`) amplifies the
        // working-scale rounding error by `2^k` (≈ `|k|·log10 2` digits), so
        // only there must the base guard widen by `extra` to keep the
        // post-shift residual inside the guard. For `k < 0` (the underflow
        // direction — `e^(large negative)`) the reassembly is a RIGHT shift
        // that shrinks the value and its absolute error, so the base `GUARD`
        // already covers it with vast margin; inflating the guard there is not
        // only needless but HARMFUL — it drives the working scale `w = SCALE +
        // base_guard` high enough that the Tang table-entry product
        // (`exp_table_entry_baked`'s `slot_hi · 10^w`, ≈ `2·w·log2(10)` bits)
        // overflows the work integer `S`, silently wrapping the `exp(c_j)`
        // factor (the deep-underflow misround at the wide tiers' max scale).
        // So size `extra` from `k` only when `k > 0`.
        let w = SCALE + GUARD;
        let one_w = C::one(w);
        let v_w_probe = C::to_work_scaled(raw, GUARD);
        let k = C::round_to_nearest_int(C::div_cached(v_w_probe, C::ln2::<SCALE>(w), one_w), w);
        let extra: u32 = if k <= 0 {
            0
        } else {
            let abs_k = k as u128;
            let digits = (abs_k * 30103).div_ceil(100_000);
            let capped = digits.min((C::w_bits() / 4) as u128) as u32;
            capped + 12 + (capped >> 2)
        };
        GUARD + extra
    } else {
        GUARD
    };

    // Directed modes decide which side of a storage grid line the true
    // result falls; near a grid line the working-scale approximation can
    // land on the wrong side, so route through the shared Ziv escalation.
    // Nearest modes narrow once. `exp(x)` for `x != 0` is transcendental
    // (never exactly on a grid line — `raw == 0` is pinned above), so use the
    // never-exact narrowing: a zero working residual is a sub-resolution
    // artifact, and Ceiling must still round up (Floor / Trunc keep the floor)
    // on inputs whose deciding residual is below the work-int resolution
    // (`exp(-10^-S)` just under `1.0`).
    C::round_to_storage_directed_never_exact(base_guard, SCALE, mode, &mut |guard| {
        tang_exp_fixed::<C, M, INTERNAL_EXTRA, SCALE>(C::to_work_scaled(raw, guard), SCALE + guard)
    })
}

#[cfg(all(test, feature = "wide"))]
mod tests {
    //! Deep-underflow correctness for the Tang `exp` path.
    //!
    //! At D76<75> the Tang
    //! `wide_tang_gate` admits large negative arguments (`e^(−34..−58)`, all
    //! representable). An `EXTERNAL_EXTRA` guard must NOT inflate the working
    //! scale `w` by `≈ |k|·log10 2` digits for these — the `k < 0`
    //! reassembly is an error-shrinking RIGHT shift that needs no such guard.
    //! Such inflation pushes the table-entry product `slot_hi · 10^w` past the tier work
    //! integer `Int<16>` (1024 bits), silently wrapping the `exp(c_j)` factor
    //! (~25 % error). The wider D307<75> tier runs the Series path and is the
    //! oracle: `exp` rounded to scale 75 is the same value at every storage
    //! width, so the two decimal renderings must be identical.

    use crate::types::widths::{D307, D76};
    use crate::RoundingMode;

    const MODES: [RoundingMode; 6] = [
        RoundingMode::HalfToEven,
        RoundingMode::HalfAwayFromZero,
        RoundingMode::HalfTowardZero,
        RoundingMode::Trunc,
        RoundingMode::Floor,
        RoundingMode::Ceiling,
    ];

    #[test]
    fn tang_deep_underflow_matches_wide_series_oracle_d76_s75() {
        // Negative args spanning the underflow regime the Tang gate routes —
        // from below the overflow boundary (~−33) up to the storage edge
        // (max |x| ≈ 57.9 at D76<75>).
        let args = ["-20.0", "-33.5", "-34.37", "-40.0", "-45.123", "-50.25", "-55.0", "-57.5"];
        for s in args {
            let a76: D76<75> = s.parse().unwrap();
            let a307: D307<75> = s.parse().unwrap();
            for m in MODES {
                let got = a76.exp_strict_with(m).to_string();
                let want = a307.exp_strict_with(m).to_string();
                assert_eq!(got, want, "exp({s}) D76<75> vs D307<75> oracle, mode {m:?}");
            }
        }
    }
}

#[cfg(all(test, any(feature = "d57", feature = "d76", feature = "wide")))]
mod powf_deep_underflow_regression {
    //! Guard (powf.golden:8048): `powf("2","-200") = 2^-200
    //! ≈ 6.223e-61` at a mid storage scale is a sub-resolution positive — it
    //! MUST round to 0 under the nearest / Floor / Trunc modes and to one
    //! storage ULP under Ceiling at every scale `< 61`. The `exp(y·ln x)`
    //! composition's `k_lift` sizer must account for the exp argument's SIGN:
    //! sizing a deeply
    //! negative argument (`-200·ln 2 ≈ -138.6`, whose result `e^-138.6 < 1`
    //! needs zero lift) a ~90-digit lift would inflate the working
    //! scale until the non-widening `mul_agm(y, ln_x, w)` low product
    //! overflows the `Wagm` work integer and WRAPS the exp argument to
    //! ≈ -0.21, returning `e^-0.21 ≈ 0.808` — a magnitude-class error at
    //! D57<28/30/42> and D76 mid-scales. Two coordinated guards: (a) the powf
    //! shell takes no lift for a negative argument (mirrors
    //! `exp2_result_int_digits`); (b) the Tang deep-underflow branch returns
    //! the smallest positive working value (mirrors `exp_generic`), not bare
    //! zero, so Ceiling rounds the sub-resolution positive up to 1 ULP.

    use crate::RoundingMode;

    /// The five modes that round a sub-resolution positive DOWN to 0.
    const DOWN_MODES: [RoundingMode; 5] = [
        RoundingMode::HalfToEven,
        RoundingMode::HalfAwayFromZero,
        RoundingMode::HalfTowardZero,
        RoundingMode::Trunc,
        RoundingMode::Floor,
    ];

    /// `"0.00…01"` — one storage ULP at `scale` (the smallest positive).
    fn one_ulp_str(scale: usize) -> String {
        let mut s = String::from("0.");
        for _ in 0..scale - 1 {
            s.push('0');
        }
        s.push('1');
        s
    }

    #[cfg(any(feature = "d57", feature = "wide"))]
    #[test]
    fn powf_2_neg200_d57_mid_scales_six_modes() {
        use crate::types::widths::D57;
        // s28 is the exact golden cell; s42 a second mid-scale, both < 61.
        macro_rules! check_d57 {
            ($s:literal) => {{
                let base: D57<$s> = "2".parse().unwrap();
                let exp: D57<$s> = "-200".parse().unwrap();
                let zero: D57<$s> = "0".parse().unwrap();
                let one_ulp: D57<$s> = one_ulp_str($s).parse().unwrap();
                for m in DOWN_MODES {
                    assert_eq!(
                        base.powf_strict_with(exp, m), zero,
                        "D57<{}> powf(2,-200) {m:?} must round the sub-resolution positive to 0", $s
                    );
                }
                assert_eq!(
                    base.powf_strict_with(exp, RoundingMode::Ceiling), one_ulp,
                    "D57<{}> powf(2,-200) Ceiling must round the sub-resolution positive up to 1 ULP", $s
                );
            }};
        }
        check_d57!(28);
        check_d57!(42);
    }

    #[cfg(any(feature = "d76", feature = "wide"))]
    #[test]
    fn powf_2_neg200_d76_mid_scales_six_modes() {
        use crate::types::widths::D76;
        macro_rules! check_d76 {
            ($s:literal) => {{
                let base: D76<$s> = "2".parse().unwrap();
                let exp: D76<$s> = "-200".parse().unwrap();
                let zero: D76<$s> = "0".parse().unwrap();
                let one_ulp: D76<$s> = one_ulp_str($s).parse().unwrap();
                for m in DOWN_MODES {
                    assert_eq!(
                        base.powf_strict_with(exp, m), zero,
                        "D76<{}> powf(2,-200) {m:?} must round the sub-resolution positive to 0", $s
                    );
                }
                assert_eq!(
                    base.powf_strict_with(exp, RoundingMode::Ceiling), one_ulp,
                    "D76<{}> powf(2,-200) Ceiling must round the sub-resolution positive up to 1 ULP", $s
                );
            }};
        }
        check_d76!(40);
        check_d76!(50);
    }
}