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