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}