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}