1
//! Internal numeric-cast helpers.
2
//!
3
//! Each function isolates one `as` conversion that clippy flags
4
//! (`cast_precision_loss` / `cast_possible_truncation` / `cast_sign_loss` /
5
//! `cast_possible_wrap`) behind a single documented `#[allow]`, so call sites
6
//! stay lint-clean without scattering raw casts or per-file helpers. Every one is
7
//! a behaviour-preserving wrapper around `as` (float→int saturates, int→int wraps
8
//! per Rust semantics); they exist to *name the intent*, not to change behaviour.
9

            
10
/// `isize` → `f32`. Loses precision only for magnitudes above 2^24; layout
11
/// coordinates and CSS dimensions stay far within that range.
12
#[inline]
13
#[must_use]
14
#[allow(clippy::cast_precision_loss)]
15
23219161
pub(crate) const fn isize_to_f32(v: isize) -> f32 {
16
23219161
    v as f32
17
23219161
}
18

            
19
/// `usize` → `f32`. Loses precision only above 2^24 (counts/lengths stay small).
20
#[inline]
21
#[must_use]
22
#[allow(clippy::cast_precision_loss)]
23
2020
pub(crate) const fn usize_to_f32(v: usize) -> f32 {
24
2020
    v as f32
25
2020
}
26

            
27
/// `i32` → `f32`. Loses precision only for magnitudes above 2^24.
28
#[inline]
29
#[must_use]
30
#[allow(clippy::cast_precision_loss)]
31
109784
pub(crate) const fn i32_to_f32(v: i32) -> f32 {
32
109784
    v as f32
33
109784
}
34

            
35
/// `f32` → `isize` (truncating). `as` saturates NaN→0 and out-of-range to the
36
/// `isize` bounds; callers that want rounding `.round()`/`.floor()` first.
37
#[inline]
38
#[must_use]
39
#[allow(clippy::cast_possible_truncation)]
40
11431855
pub(crate) const fn f32_to_isize(v: f32) -> isize {
41
11431855
    v as isize
42
11431855
}
43

            
44
/// `f32` → `i32` (truncating). `as` saturates NaN→0 and out-of-range to `i32`
45
/// bounds; callers that want rounding `.round()` first.
46
#[inline]
47
#[must_use]
48
#[allow(clippy::cast_possible_truncation)]
49
2968978
pub(crate) const fn f32_to_i32(v: f32) -> i32 {
50
2968978
    v as i32
51
2968978
}
52

            
53
/// `f32` → `u32` (truncating, sign-dropping). `as` saturates NaN→0, negatives→0,
54
/// out-of-range→`u32::MAX`; callers validate non-negative / `.round()` first.
55
#[inline]
56
#[must_use]
57
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
58
1231
pub(crate) const fn f32_to_u32(v: f32) -> u32 {
59
1231
    v as u32
60
1231
}
61

            
62
#[cfg(test)]
63
#[allow(clippy::float_cmp, clippy::unreadable_literal)]
64
mod autotest_generated {
65
    use super::*;
66

            
67
    /// Largest integer an `f32` can represent exactly (`2^24`); above this the
68
    /// spacing between consecutive `f32`s is > 1, which is the precision loss
69
    /// the module docs warn about.
70
    const TWO_POW_24: i32 = 16_777_216;
71
    const TWO_POW_31: f32 = 2_147_483_648.0;
72
    const TWO_POW_32: f32 = 4_294_967_296.0;
73

            
74
    // ---------------------------------------------------------------- zero ---
75

            
76
    #[test]
77
    fn zero_maps_to_positive_zero_in_both_directions() {
78
        // Not just `== 0.0` (which is true for -0.0 as well): check the bits, so
79
        // a sign-flipping bug can't hide behind float equality.
80
        assert_eq!(isize_to_f32(0).to_bits(), 0_u32);
81
        assert_eq!(usize_to_f32(0).to_bits(), 0_u32);
82
        assert_eq!(i32_to_f32(0).to_bits(), 0_u32);
83

            
84
        assert_eq!(f32_to_isize(0.0), 0);
85
        assert_eq!(f32_to_i32(0.0), 0);
86
        assert_eq!(f32_to_u32(0.0), 0);
87
    }
88

            
89
    #[test]
90
    fn negative_zero_converts_to_integer_zero() {
91
        assert_eq!(f32_to_isize(-0.0), 0);
92
        assert_eq!(f32_to_i32(-0.0), 0);
93
        assert_eq!(f32_to_u32(-0.0), 0);
94
    }
95

            
96
    // ------------------------------------------------------------- nan/inf ---
97

            
98
    #[test]
99
    fn nan_saturates_to_zero_not_a_panic() {
100
        assert_eq!(f32_to_isize(f32::NAN), 0);
101
        assert_eq!(f32_to_i32(f32::NAN), 0);
102
        assert_eq!(f32_to_u32(f32::NAN), 0);
103

            
104
        // Negative NaN and a non-canonical NaN payload must behave identically.
105
        assert_eq!(f32_to_i32(-f32::NAN), 0);
106
        assert_eq!(f32_to_u32(-f32::NAN), 0);
107
        let payload_nan = f32::from_bits(0x7fc0_1234);
108
        assert!(payload_nan.is_nan());
109
        assert_eq!(f32_to_i32(payload_nan), 0);
110
        assert_eq!(f32_to_u32(payload_nan), 0);
111
    }
112

            
113
    #[test]
114
    fn infinities_saturate_to_the_integer_bounds() {
115
        assert_eq!(f32_to_isize(f32::INFINITY), isize::MAX);
116
        assert_eq!(f32_to_isize(f32::NEG_INFINITY), isize::MIN);
117

            
118
        assert_eq!(f32_to_i32(f32::INFINITY), i32::MAX);
119
        assert_eq!(f32_to_i32(f32::NEG_INFINITY), i32::MIN);
120

            
121
        // Unsigned: -inf clamps to 0, *not* to u32::MAX via a sign-reinterpret.
122
        assert_eq!(f32_to_u32(f32::INFINITY), u32::MAX);
123
        assert_eq!(f32_to_u32(f32::NEG_INFINITY), 0);
124
    }
125

            
126
    // ------------------------------------------------------------ overflow ---
127

            
128
    #[test]
129
    fn out_of_range_floats_saturate_rather_than_wrap() {
130
        // The classic UB-turned-saturation cases: a wrapping impl would give 0 /
131
        // i32::MIN here.
132
        assert_eq!(f32_to_i32(TWO_POW_31), i32::MAX);
133
        assert_eq!(f32_to_i32(-TWO_POW_31 - 256.0), i32::MIN);
134
        assert_eq!(f32_to_i32(1.0e30), i32::MAX);
135
        assert_eq!(f32_to_i32(-1.0e30), i32::MIN);
136
        assert_eq!(f32_to_i32(f32::MAX), i32::MAX);
137
        assert_eq!(f32_to_i32(f32::MIN), i32::MIN);
138

            
139
        assert_eq!(f32_to_u32(TWO_POW_32), u32::MAX);
140
        assert_eq!(f32_to_u32(1.0e30), u32::MAX);
141
        assert_eq!(f32_to_u32(f32::MAX), u32::MAX);
142

            
143
        assert_eq!(f32_to_isize(1.0e30), isize::MAX);
144
        assert_eq!(f32_to_isize(-1.0e30), isize::MIN);
145
        assert_eq!(f32_to_isize(f32::MAX), isize::MAX);
146
        assert_eq!(f32_to_isize(f32::MIN), isize::MIN);
147
    }
148

            
149
    #[test]
150
    fn negatives_clamp_to_zero_for_the_unsigned_cast() {
151
        // Sign-dropping must be a clamp, not a bit reinterpretation.
152
        assert_eq!(f32_to_u32(-1.0), 0);
153
        assert_eq!(f32_to_u32(-0.5), 0);
154
        assert_eq!(f32_to_u32(-1.0e30), 0);
155
        assert_eq!(f32_to_u32(-f32::from_bits(1)), 0); // -smallest subnormal
156
    }
157

            
158
    #[test]
159
    fn largest_in_range_floats_convert_exactly() {
160
        // Largest f32 strictly below 2^31 / 2^32 — exactly representable, so no
161
        // saturation should kick in and no off-by-one may appear.
162
        assert_eq!(f32_to_i32(2_147_483_520.0), 2_147_483_520);
163
        assert_eq!(f32_to_u32(4_294_967_040.0), 4_294_967_040);
164
    }
165

            
166
    // ---------------------------------------------------------- truncation ---
167

            
168
    #[test]
169
    fn fractional_values_truncate_toward_zero() {
170
        assert_eq!(f32_to_i32(1.9), 1);
171
        assert_eq!(f32_to_i32(-1.9), -1); // toward zero, not floor(-2)
172
        assert_eq!(f32_to_i32(0.9), 0);
173
        assert_eq!(f32_to_i32(-0.9), 0);
174
        assert_eq!(f32_to_isize(-1.9), -1);
175
        assert_eq!(f32_to_u32(1.9), 1);
176
        assert_eq!(f32_to_u32(0.9), 0);
177
    }
178

            
179
    #[test]
180
    fn subnormal_and_tiny_magnitudes_flush_to_zero() {
181
        assert_eq!(f32_to_i32(f32::MIN_POSITIVE), 0);
182
        assert_eq!(f32_to_u32(f32::MIN_POSITIVE), 0);
183
        assert_eq!(f32_to_isize(f32::from_bits(1)), 0); // smallest subnormal
184
        assert_eq!(f32_to_i32(f32::EPSILON), 0);
185
    }
186

            
187
    // ------------------------------------------------ int -> f32 boundaries ---
188

            
189
    #[test]
190
    fn integers_up_to_two_pow_24_are_exact() {
191
        // The documented "loses precision only above 2^24" claim, checked at the
192
        // boundary itself.
193
        assert_eq!(i32_to_f32(TWO_POW_24), 16_777_216.0);
194
        assert_eq!(f32_to_i32(i32_to_f32(TWO_POW_24)), TWO_POW_24);
195
        assert_eq!(i32_to_f32(TWO_POW_24 - 1), 16_777_215.0);
196
        assert_eq!(usize_to_f32(TWO_POW_24 as usize), 16_777_216.0);
197
        assert_eq!(isize_to_f32(TWO_POW_24 as isize), 16_777_216.0);
198
    }
199

            
200
    #[test]
201
    fn just_above_two_pow_24_loses_precision_by_round_to_even() {
202
        // 2^24 + 1 is not representable: it ties, and IEEE round-half-to-even
203
        // pulls it *down* to 2^24. 2^24 + 3 ties upward to 2^24 + 4.
204
        assert_eq!(i32_to_f32(TWO_POW_24 + 1), 16_777_216.0);
205
        assert_eq!(i32_to_f32(TWO_POW_24 + 3), 16_777_220.0);
206
        assert_eq!(usize_to_f32(TWO_POW_24 as usize + 1), 16_777_216.0);
207
        assert_eq!(isize_to_f32(TWO_POW_24 as isize + 1), 16_777_216.0);
208

            
209
        // Which means the round-trip is lossy exactly here — documented, not a bug.
210
        assert_eq!(f32_to_i32(i32_to_f32(TWO_POW_24 + 1)), TWO_POW_24);
211
    }
212

            
213
    #[test]
214
    fn int_min_max_convert_without_panic_and_stay_finite() {
215
        for v in [i32::MIN, i32::MIN + 1, -1, 0, 1, i32::MAX - 1, i32::MAX] {
216
            assert!(i32_to_f32(v).is_finite());
217
        }
218
        for v in [isize::MIN, isize::MIN + 1, -1, 0, 1, isize::MAX - 1, isize::MAX] {
219
            assert!(isize_to_f32(v).is_finite());
220
        }
221
        for v in [0_usize, 1, usize::MAX - 1, usize::MAX] {
222
            assert!(usize_to_f32(v).is_finite());
223
        }
224

            
225
        // i32::MIN is a power of two, so it *is* exact; i32::MAX is not, and
226
        // rounds *up* to 2^31 — past the range of the type it came from.
227
        assert_eq!(i32_to_f32(i32::MIN), -TWO_POW_31);
228
        assert_eq!(i32_to_f32(i32::MAX), TWO_POW_31);
229
        // ...i.e. it lands strictly above the largest f32 that fits in an i32.
230
        assert!(i32_to_f32(i32::MAX) > 2_147_483_520.0);
231
    }
232

            
233
    #[test]
234
    fn unsigned_max_does_not_go_negative_or_infinite() {
235
        let m = usize_to_f32(usize::MAX);
236
        assert!(m.is_finite());
237
        assert!(m.is_sign_positive());
238
        assert!(m > 0.0);
239
    }
240

            
241
    #[cfg(target_pointer_width = "64")]
242
    #[test]
243
    fn pointer_sized_extremes_round_to_the_adjacent_power_of_two() {
244
        assert_eq!(isize_to_f32(isize::MAX), 9_223_372_036_854_775_808.0); // 2^63
245
        assert_eq!(isize_to_f32(isize::MIN), -9_223_372_036_854_775_808.0);
246
        assert_eq!(usize_to_f32(usize::MAX), 18_446_744_073_709_551_616.0); // 2^64
247
    }
248

            
249
    // ---------------------------------------------------------- round-trip ---
250

            
251
    #[test]
252
    fn round_trip_is_exact_within_the_exactly_representable_range() {
253
        // Stride sweep over [0, 2^24] plus the last 512 values before the
254
        // boundary, where an off-by-one in the exactness claim would show up.
255
        let mut v: i32 = -TWO_POW_24;
256
        while v <= TWO_POW_24 {
257
            assert_eq!(f32_to_i32(i32_to_f32(v)), v, "i32 round-trip broke at {v}");
258
            assert_eq!(f32_to_isize(isize_to_f32(v as isize)), v as isize);
259
            v = v.saturating_add(4093); // prime stride, hits odd/even alike
260
        }
261
        for v in (TWO_POW_24 - 512)..=TWO_POW_24 {
262
            assert_eq!(f32_to_i32(i32_to_f32(v)), v);
263
            assert_eq!(f32_to_u32(i32_to_f32(v)), v as u32);
264
        }
265
    }
266

            
267
    #[test]
268
    fn round_trip_survives_the_int_extremes_via_saturation() {
269
        // i32::MAX -> 2^31 (out of range!) -> saturates *back* to i32::MAX. The
270
        // two roundings cancel; assert that so a "fix" to either side gets caught.
271
        assert_eq!(f32_to_i32(i32_to_f32(i32::MAX)), i32::MAX);
272
        assert_eq!(f32_to_i32(i32_to_f32(i32::MIN)), i32::MIN);
273
        assert_eq!(f32_to_isize(isize_to_f32(isize::MAX)), isize::MAX);
274
        assert_eq!(f32_to_isize(isize_to_f32(isize::MIN)), isize::MIN);
275
    }
276

            
277
    #[test]
278
    fn powers_of_two_round_trip_exactly() {
279
        for exp in 0..31_u32 {
280
            let v = 1_i32 << exp;
281
            assert_eq!(f32_to_i32(i32_to_f32(v)), v, "2^{exp} round-trip broke");
282
            assert_eq!(f32_to_i32(i32_to_f32(-v)), -v);
283
            assert_eq!(f32_to_u32(i32_to_f32(v)), v as u32);
284
        }
285
    }
286

            
287
    // ---------------------------------------------------------- invariants ---
288

            
289
    #[test]
290
    fn int_to_float_is_monotonic() {
291
        // Must stay sorted ascending -- the windows(2) check below compares each
292
        // neighbouring pair. TWO_POW_24 (16_777_216) is smaller than 1e9.
293
        let samples = [
294
            isize::MIN,
295
            -1_000_000_000,
296
            -1,
297
            0,
298
            1,
299
            TWO_POW_24 as isize,
300
            1_000_000_000,
301
            isize::MAX,
302
        ];
303
        for w in samples.windows(2) {
304
            assert!(
305
                isize_to_f32(w[0]) <= isize_to_f32(w[1]),
306
                "monotonicity broke between {} and {}",
307
                w[0],
308
                w[1]
309
            );
310
        }
311
    }
312

            
313
    #[test]
314
    fn signed_and_unsigned_paths_agree_where_the_ranges_overlap() {
315
        for v in [0.0_f32, 1.0, 0.5, 42.7, 65_535.0, 16_777_216.0, 2_147_483_520.0] {
316
            assert_eq!(f32_to_u32(v), f32_to_i32(v) as u32, "disagreement at {v}");
317
            assert_eq!(f32_to_isize(v), f32_to_i32(v) as isize);
318
        }
319
        for v in [0_i32, 1, -1, 12_345, i32::MIN, i32::MAX] {
320
            assert_eq!(i32_to_f32(v), isize_to_f32(v as isize), "disagreement at {v}");
321
        }
322
    }
323

            
324
    #[test]
325
    fn sign_is_preserved_by_the_int_to_float_casts() {
326
        assert!(isize_to_f32(-1).is_sign_negative());
327
        assert!(isize_to_f32(1).is_sign_positive());
328
        assert!(i32_to_f32(i32::MIN).is_sign_negative());
329
        assert!(usize_to_f32(usize::MAX).is_sign_positive());
330
    }
331

            
332
    // -------------------------------------------------------- const context ---
333

            
334
    #[test]
335
    fn usable_in_const_context_with_the_same_saturating_semantics() {
336
        // These are `const fn`; const-eval must saturate identically to runtime,
337
        // and must not refuse to compile on NaN/out-of-range.
338
        const NAN_I32: i32 = f32_to_i32(f32::NAN);
339
        const INF_I32: i32 = f32_to_i32(f32::INFINITY);
340
        const NEG_U32: u32 = f32_to_u32(-5.0);
341
        const BIG_ISIZE: isize = f32_to_isize(1.0e30);
342
        const FROM_I32: f32 = i32_to_f32(-42);
343
        const FROM_USIZE: f32 = usize_to_f32(42);
344
        const FROM_ISIZE: f32 = isize_to_f32(-42);
345

            
346
        assert_eq!(NAN_I32, 0);
347
        assert_eq!(INF_I32, i32::MAX);
348
        assert_eq!(NEG_U32, 0);
349
        assert_eq!(BIG_ISIZE, isize::MAX);
350
        assert_eq!(FROM_I32, -42.0);
351
        assert_eq!(FROM_USIZE, 42.0);
352
        assert_eq!(FROM_ISIZE, -42.0);
353
    }
354
}