Skip to main content

epics_libcom_rs/runtime/
stdlib.rs

1//! The numeric core of C's `strtod` for C99 hex floats, shared by every
2//! `strtod` port in the workspace (`calc::engine::strtod`, the CALC literal
3//! scanner, and `epics-ca-rs`'s `estdlib`, the env-knob parser).
4//!
5//! Both ports used to build the significand in an `f64` (`mant = mant * 16.0 +
6//! digit`) and then scale it with `mant * 2.0f64.powi(exp)`. That composition
7//! is wrong twice over, and the whole subnormal range paid for it:
8//!
9//! * `powi` with a negative exponent evaluates `1.0 / 2^-exp`, and `2^1074`
10//!   is already infinite — so every exponent below about -1023 scaled the
11//!   significand by `1/inf == 0`. `0x1p-1074` came back as an underflow to
12//!   zero where glibc returns the exact smallest subnormal.
13//! * ERANGE was then *guessed back* from the result: any subnormal was
14//!   reported as an ERANGE overflow. glibc raises ERANGE only when the exact
15//!   value is tiny AND inexact, so `0x1p-1023` — an exactly representable
16//!   subnormal — leaves errno clear.
17//!
18//! [`HexSignificand`] keeps the digits as an exact integer `m * 2^e2` with a
19//! sticky bit for anything that falls off the bottom, so the conversion to
20//! `f64` is a SINGLE correctly-rounded step (ties to even) and knows precisely
21//! whether it was inexact. Every row of the boundary table in this module's
22//! tests was probed against the compiled glibc `strtod` on this platform.
23
24/// A C99 hex float's significand, accumulated exactly.
25///
26/// The value is `m * 2^e2`, plus a non-zero tail below `m`'s window when
27/// `sticky` is set. `m` holds up to 60 bits, well past `f64`'s 53 plus the
28/// guard and round bits, so the sticky tail is all the rounding needs.
29#[derive(Debug, Clone, Copy, Default)]
30pub struct HexSignificand {
31    m: u64,
32    e2: i32,
33    sticky: bool,
34}
35
36impl HexSignificand {
37    pub fn new() -> Self {
38        Self::default()
39    }
40
41    /// Fold in one hex digit. `fractional` marks the digits after the `.`,
42    /// which lower the exponent instead of raising the value.
43    pub fn push_digit(&mut self, digit: u8, fractional: bool) {
44        if self.m >> 60 == 0 {
45            self.m = (self.m << 4) | u64::from(digit);
46            if fractional {
47                self.e2 = self.e2.saturating_sub(4);
48            }
49        } else {
50            // Past 60 bits the digit lies below what `f64` can hold; it can
51            // only ever break a rounding tie, which is what `sticky` records.
52            self.sticky |= digit != 0;
53            if !fractional {
54                self.e2 = self.e2.saturating_add(4);
55            }
56        }
57    }
58
59    /// Apply the `p<exp>` binary exponent.
60    pub fn apply_binary_exponent(&mut self, exp: i32) {
61        self.e2 = self.e2.saturating_add(exp);
62    }
63
64    /// Round to the nearest `f64` (ties to even) and report `errno == ERANGE`.
65    ///
66    /// glibc sets ERANGE when the result overflows to infinity, and when the
67    /// EXACT value is tiny (below the smallest normal, `2^-1022`) and the
68    /// conversion is inexact. Note both halves: `0x1p-1074` is tiny but exact
69    /// (no ERANGE), and `0x1.fffffffffffffp-1023` is tiny-and-inexact yet
70    /// rounds up to the smallest *normal* — ERANGE all the same, because the
71    /// test is on the value before rounding.
72    pub fn to_f64(&self) -> (f64, bool) {
73        let m = self.m;
74        if m == 0 {
75            return (0.0, false);
76        }
77        // Normalize so the top bit of `mm` is the significand's MSB, whose
78        // weight is 2^e: the value is `(mm / 2^63) * 2^e`, a number in [1, 2)
79        // scaled by 2^e.
80        let shift = m.leading_zeros();
81        let mm = m << shift;
82        let e = i64::from(self.e2) + 63 - i64::from(shift);
83        if e > 1023 {
84            return (f64::INFINITY, true);
85        }
86
87        let tiny = e < -1022;
88        // Bits of significand this exponent can carry: 53 for a normal, fewer
89        // once the subnormal floor (the lowest bit is worth 2^-1074) eats into
90        // the bottom.
91        let keep = if tiny { e + 1075 } else { 53 };
92        if keep <= 0 {
93            // Below half of the smallest subnormal, or exactly half — a tie
94            // rounds to even, i.e. to zero. Only a strict majority rounds up.
95            let round_up = keep == 0 && (mm != 1 << 63 || self.sticky);
96            return if round_up {
97                (pow2(-1074), true)
98            } else {
99                (0.0, true)
100            };
101        }
102
103        let drop = 64 - keep as u32; // keep is 1..=53, so drop is 11..=63
104        let kept = mm >> drop;
105        let rest = mm & ((1u64 << drop) - 1);
106        let half = 1u64 << (drop - 1);
107        let inexact = rest != 0 || self.sticky;
108        let round_up = rest > half || (rest == half && (self.sticky || kept & 1 == 1));
109        // `kept` is at most 53 bits and the scale is an exact power of two, so
110        // this product is the single correctly-rounded result — subnormal
111        // results included, where the product is still exact.
112        let value = (kept + u64::from(round_up)) as f64 * pow2((e - keep + 1) as i32);
113
114        let erange = value.is_infinite() || (tiny && inexact);
115        (value, erange)
116    }
117}
118
119/// Exact `2^k` for `k` in `[-1074, 1023]` — the only range [`HexSignificand`]
120/// asks for. `2.0f64.powi(k)` cannot serve: below -1023 it takes the
121/// reciprocal of an already-infinite `2^-k` and collapses to zero.
122fn pow2(k: i32) -> f64 {
123    debug_assert!((-1074..=1023).contains(&k));
124    if k >= -1022 {
125        f64::from_bits(((k + 1023) as u64) << 52)
126    } else {
127        f64::from_bits(1u64 << (k + 1074))
128    }
129}
130
131/// C `isspace()` in the "C" locale — `strtod`'s leading/trailing skip set.
132fn is_c_space(c: u8) -> bool {
133    matches!(c, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')
134}
135
136/// The `epicsParseDouble` failure codes (`epicsStdlib.h`).
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum ParseDoubleError {
139    /// `S_stdlib_noConversion` — `strtod` consumed nothing.
140    NoConversion,
141    /// `S_stdlib_overflow` — `errno == ERANGE` with a non-zero result.
142    Overflow,
143    /// `S_stdlib_underflow` — `errno == ERANGE` with a zero result.
144    Underflow,
145    /// `S_stdlib_extraneous` — non-space characters trail the number.
146    Extraneous,
147}
148
149/// `errno` after `strtod`: unset, or `ERANGE` on either side.
150#[derive(Clone, Copy, PartialEq, Eq)]
151enum Erange {
152    No,
153    Over,
154    Under,
155}
156
157/// ERANGE classification for a value the DECIMAL path computed from digits.
158///
159/// glibc raises ERANGE when the result overflows to infinity, when it
160/// underflows to zero, and when it is inexactly representable as a
161/// subnormal. It does NOT raise it for the `inf` / `nan` *words*, which
162/// is why those are classified separately at their parse site.
163///
164/// The "inexactly" is the whole rule in glibc — a subnormal it can name
165/// exactly leaves errno clear. Writing one in decimal takes some 750
166/// significant digits (`2^-1074`), so every decimal literal short enough to
167/// appear in an env var and land in the subnormal range is inexact, and this
168/// value-only test agrees with C on all of them. The hex path, where such a
169/// literal is three characters long, does NOT use this: it gets the exact
170/// inexactness from [`HexSignificand::to_f64`].
171fn classify(v: f64, mantissa_nonzero: bool) -> Erange {
172    if v.is_infinite() {
173        Erange::Over
174    } else if v == 0.0 && mantissa_nonzero {
175        Erange::Under
176    } else if v != 0.0 && v.is_subnormal() {
177        // `epicsParseDouble` maps a non-zero ERANGE to overflow.
178        Erange::Over
179    } else {
180        Erange::No
181    }
182}
183
184/// C `strtod` (glibc; `epicsStrtod` is `#define`d to it on every platform
185/// with a working one — `osi/os/posix/osdStrtod.h`). Returns the value, the
186/// number of bytes consumed (0 == no conversion, C's `endp == str`), and the
187/// `errno` outcome.
188///
189/// Accepts what glibc accepts, verified against the compiled C: decimal and
190/// scientific notation, C99 hex floats (`0x10` → 16, `0X1p4` → 16), the
191/// `inf` / `infinity` / `nan` words (case-insensitive, optional `nan(...)`
192/// payload), each with an optional sign.
193fn strtod(s: &str) -> (f64, usize, Erange) {
194    let b = s.as_bytes();
195    let mut i = 0;
196    while i < b.len() && is_c_space(b[i]) {
197        i += 1;
198    }
199    let sign_at = i;
200    let mut neg = false;
201    if i < b.len() && (b[i] == b'+' || b[i] == b'-') {
202        neg = b[i] == b'-';
203        i += 1;
204    }
205    let num = i;
206
207    // C99 hex float: 0x <hexdigits> [. <hexdigits>] [p [+-] <digits>]
208    if num + 1 < b.len() && b[num] == b'0' && (b[num + 1] | 0x20) == b'x' {
209        let mut j = num + 2;
210        let mut sig = HexSignificand::new();
211        let mut digits = 0usize;
212        while j < b.len() && b[j].is_ascii_hexdigit() {
213            sig.push_digit(hex_val(b[j]), false);
214            digits += 1;
215            j += 1;
216        }
217        if j < b.len() && b[j] == b'.' {
218            let mut k = j + 1;
219            let mut frac = 0usize;
220            while k < b.len() && b[k].is_ascii_hexdigit() {
221                sig.push_digit(hex_val(b[k]), true);
222                frac += 1;
223                k += 1;
224            }
225            if digits > 0 || frac > 0 {
226                digits += frac;
227                j = k;
228            }
229        }
230        if digits == 0 {
231            // Bare "0x": glibc converts the leading "0" and stops at 'x'.
232            return (if neg { -0.0 } else { 0.0 }, num + 1, Erange::No);
233        }
234        if j < b.len() && (b[j] | 0x20) == b'p' {
235            let mut k = j + 1;
236            let mut eneg = false;
237            if k < b.len() && (b[k] == b'+' || b[k] == b'-') {
238                eneg = b[k] == b'-';
239                k += 1;
240            }
241            let digits_at = k;
242            let mut e: i32 = 0;
243            while k < b.len() && b[k].is_ascii_digit() {
244                e = e.saturating_mul(10).saturating_add((b[k] - b'0') as i32);
245                k += 1;
246            }
247            if k > digits_at {
248                sig.apply_binary_exponent(if eneg { -e } else { e });
249                j = k;
250            }
251        }
252        // The significand is exact and rounds to `f64` in one step, so ERANGE
253        // is known rather than guessed back from the value: an exactly
254        // representable subnormal (`0x1p-1074`, `0x1p-1023`) leaves errno
255        // clear, as it does in glibc.
256        let (mut v, erange) = sig.to_f64();
257        if neg {
258            v = -v;
259        }
260        // C derives underflow-vs-overflow from the value alone
261        // (`epicsStdlib.c:164`), and so does `epics_parse_double` below.
262        let erange = if !erange {
263            Erange::No
264        } else if v == 0.0 {
265            Erange::Under
266        } else {
267            Erange::Over
268        };
269        return (v, j, erange);
270    }
271
272    // The `inf` / `nan` words. glibc leaves errno clear for these, so an
273    // explicit `EPICS_CA_CONN_TMO=inf` is a VALID (never-expiring) timeout
274    // in C, not a parse failure.
275    let rest = &s[num..];
276    if starts_ci(rest, "infinity") {
277        return (inf(neg), num + 8, Erange::No);
278    }
279    if starts_ci(rest, "inf") {
280        return (inf(neg), num + 3, Erange::No);
281    }
282    if starts_ci(rest, "nan") {
283        let mut j = num + 3;
284        if j < b.len() && b[j] == b'(' {
285            let mut k = j + 1;
286            while k < b.len() && b[k] != b')' {
287                k += 1;
288            }
289            if k < b.len() {
290                j = k + 1;
291            }
292        }
293        return (f64::NAN, j, Erange::No);
294    }
295
296    // Decimal / scientific.
297    let mut j = num;
298    let mut digits = 0usize;
299    let mut nonzero = false;
300    while j < b.len() && b[j].is_ascii_digit() {
301        nonzero |= b[j] != b'0';
302        digits += 1;
303        j += 1;
304    }
305    if j < b.len() && b[j] == b'.' {
306        let mut k = j + 1;
307        let mut frac = 0usize;
308        while k < b.len() && b[k].is_ascii_digit() {
309            nonzero |= b[k] != b'0';
310            frac += 1;
311            k += 1;
312        }
313        if digits > 0 || frac > 0 {
314            digits += frac;
315            j = k;
316        }
317    }
318    if digits == 0 {
319        return (0.0, 0, Erange::No);
320    }
321    let mut end = j;
322    if j < b.len() && (b[j] | 0x20) == b'e' {
323        let mut k = j + 1;
324        if k < b.len() && (b[k] == b'+' || b[k] == b'-') {
325            k += 1;
326        }
327        let digits_at = k;
328        while k < b.len() && b[k].is_ascii_digit() {
329            k += 1;
330        }
331        if k > digits_at {
332            end = k;
333        }
334    }
335    // Rust's `f64::from_str` accepts exactly this grammar (sign, digits,
336    // optional point, optional exponent) and, like `strtod`, saturates to
337    // ±inf on overflow and to 0 on underflow — `classify` turns those into
338    // the ERANGE codes.
339    let v = s[sign_at..end].parse::<f64>().unwrap_or(f64::NAN);
340    let erange = classify(v, nonzero);
341    (v, end, erange)
342}
343
344fn hex_val(c: u8) -> u8 {
345    match c {
346        b'0'..=b'9' => c - b'0',
347        _ => (c | 0x20) - b'a' + 10,
348    }
349}
350
351fn inf(neg: bool) -> f64 {
352    if neg {
353        f64::NEG_INFINITY
354    } else {
355        f64::INFINITY
356    }
357}
358
359fn starts_ci(s: &str, word: &str) -> bool {
360    s.len() >= word.len() && s.as_bytes()[..word.len()].eq_ignore_ascii_case(word.as_bytes())
361}
362
363/// C `epicsParseDouble(str, to, NULL)` (`epicsStdlib.c:149-176`): skip
364/// leading whitespace, run `strtod`, reject `ERANGE`, skip trailing
365/// whitespace, reject anything left over.
366pub fn epics_parse_double(s: &str) -> Result<f64, ParseDoubleError> {
367    let (v, used, erange) = strtod(s);
368    if used == 0 {
369        return Err(ParseDoubleError::NoConversion);
370    }
371    match erange {
372        Erange::Over => return Err(ParseDoubleError::Overflow),
373        Erange::Under => return Err(ParseDoubleError::Underflow),
374        Erange::No => {}
375    }
376    if !s.as_bytes()[used..].iter().all(|&c| is_c_space(c)) {
377        return Err(ParseDoubleError::Extraneous);
378    }
379    Ok(v)
380}
381
382/// C `epicsScanDouble` (`epicsStdlib.h:203`) — `epicsParseDouble` with the
383/// status collapsed to a boolean.
384pub fn epics_scan_double(s: &str) -> Option<f64> {
385    epics_parse_double(s).ok()
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391
392    /// Parse a `0x…` hex float through the accumulator, the way both callers
393    /// drive it.
394    fn hex(s: &str) -> (f64, bool) {
395        let b = s.as_bytes();
396        let mut sig = HexSignificand::new();
397        let mut i = 2; // skip "0x"
398        while i < b.len() && b[i].is_ascii_hexdigit() {
399            sig.push_digit(hex_val(b[i]), false);
400            i += 1;
401        }
402        if i < b.len() && b[i] == b'.' {
403            i += 1;
404            while i < b.len() && b[i].is_ascii_hexdigit() {
405                sig.push_digit(hex_val(b[i]), true);
406                i += 1;
407            }
408        }
409        if i < b.len() && (b[i] | 0x20) == b'p' {
410            sig.apply_binary_exponent(s[i + 1..].parse::<i32>().unwrap());
411        }
412        sig.to_f64()
413    }
414
415    fn hex_val(c: u8) -> u8 {
416        match c {
417            b'0'..=b'9' => c - b'0',
418            _ => (c | 0x20) - b'a' + 10,
419        }
420    }
421
422    /// Every row probed against the compiled glibc `strtod`: `(text, bits of
423    /// the result, errno == ERANGE)`.
424    #[test]
425    fn matches_glibc_strtod_across_the_subnormal_boundary() {
426        let rows: &[(&str, u64, bool)] = &[
427            // Exactly representable subnormals: glibc leaves errno CLEAR.
428            ("0x1p-1074", 0x0000_0000_0000_0001, false),
429            ("0x2p-1075", 0x0000_0000_0000_0001, false),
430            ("0x1p-1073", 0x0000_0000_0000_0002, false),
431            ("0x1p-1023", 0x0008_0000_0000_0000, false),
432            // Tiny AND inexact: ERANGE, non-zero result (C: overflow).
433            ("0x1.8p-1075", 0x0000_0000_0000_0001, true),
434            ("0x1.4p-1074", 0x0000_0000_0000_0001, true),
435            ("0x1.cp-1074", 0x0000_0000_0000_0002, true),
436            ("0x3p-1075", 0x0000_0000_0000_0002, true),
437            ("0x1.0000000000001p-1074", 0x0000_0000_0000_0001, true),
438            ("0x123456789abcdefp-1100", 0x0000_0000_48d1_59e2, true),
439            // Tiny and inexact, yet it rounds up to the smallest NORMAL —
440            // ERANGE is decided before rounding, so it still fires.
441            ("0x1.fffffffffffffp-1023", 0x0010_0000_0000_0000, true),
442            // Tiny, inexact, rounds to zero: ERANGE (C: underflow).
443            ("0x1p-1075", 0, true), // exactly half → ties to even → zero
444            ("0x1p-1076", 0, true),
445            ("0x1p-2000", 0, true),
446            // Normal range.
447            ("0x1p-1022", 0x0010_0000_0000_0000, false),
448            ("0x1p1023", 0x7fe0_0000_0000_0000, false),
449            ("0x10", 0x4030_0000_0000_0000, false),
450            ("0x1.8p1", 0x4008_0000_0000_0000, false),
451            // Overflow.
452            ("0x1p1024", 0x7ff0_0000_0000_0000, true),
453            ("0x1.fffffffffffff8p1023", 0x7ff0_0000_0000_0000, true),
454            // A zero significand names zero exactly, at any exponent.
455            ("0x0p0", 0, false),
456            ("0x0p-5000", 0, false),
457            // More than 53 bits of significand: one rounding, ties to even.
458            ("0x1.00000000000008p0", 0x3ff0_0000_0000_0000, false),
459            ("0x1.00000000000018p0", 0x3ff0_0000_0000_0002, false),
460            ("0x1.0000000000000fp0", 0x3ff0_0000_0000_0001, false),
461        ];
462        for &(text, bits, erange) in rows {
463            let (v, e) = hex(text);
464            assert_eq!(v.to_bits(), bits, "value of {text}");
465            assert_eq!(e, erange, "ERANGE of {text}");
466        }
467    }
468}