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, units)` with a NON-NULL `units`
370/// (`epicsStdlib.c:149-176`): skip leading whitespace, run `strtod`, reject
371/// `ERANGE`, skip trailing whitespace, and hand back whatever remains
372/// instead of refusing it. This is the form `store_double_value` uses for
373/// filter options (`chfPlugin.c:273`), which is why `{"dbnd":{"d":"0.5 V"}}`
374/// stores 0.5 rather than failing the parse.
375pub fn epics_parse_double_units(s: &str) -> Result<(f64, &str), ParseDoubleError> {
376 let (v, used, erange) = strtod(s);
377 if used == 0 {
378 return Err(ParseDoubleError::NoConversion);
379 }
380 match erange {
381 Erange::Over => return Err(ParseDoubleError::Overflow),
382 Erange::Under => return Err(ParseDoubleError::Underflow),
383 Erange::No => {}
384 }
385 Ok((v, s[used..].trim_start_matches(c_isspace)))
386}
387
388/// C `epicsParseDouble(str, to, NULL)` — the same function with the
389/// remainder refused as `S_stdlib_extraneous` (`epicsStdlib.c:169-170`)
390/// rather than returned.
391pub fn epics_parse_double(s: &str) -> Result<f64, ParseDoubleError> {
392 match epics_parse_double_units(s)? {
393 (v, "") => Ok(v),
394 _ => Err(ParseDoubleError::Extraneous),
395 }
396}
397
398/// C `epicsScanDouble` (`epicsStdlib.h:203`) — `epicsParseDouble` with the
399/// status collapsed to a boolean.
400pub fn epics_scan_double(s: &str) -> Option<f64> {
401 epics_parse_double(s).ok()
402}
403
404/// C `epicsStrHash` (`libcom/src/misc/epicsString.c:365-376` at `R7.0.10`) —
405/// the string hash every general-purpose hash table in base keys on
406/// (`gpHashLib.c:107`, the registry, the record-name directory).
407///
408/// ```c
409/// unsigned int hash = seed;
410/// while ((c = *str++)) {
411/// hash ^= ~((hash << 11) ^ c ^ (hash >> 5));
412/// if (!(c = *str++)) break;
413/// hash ^= (hash << 7) ^ c ^ (hash >> 3);
414/// }
415/// ```
416///
417/// C's `char c` is signed on every platform base builds for, and it widens to
418/// `int` before the XOR, so a byte at or above `0x80` contributes its
419/// SIGN-EXTENDED value. `i8 as i32 as u32` is that widening; taking the byte
420/// as `u32` directly would diverge on any non-ASCII name. The shifts are
421/// C's on a 32-bit `unsigned int`, so they wrap rather than overflow.
422#[must_use]
423pub fn epics_str_hash(s: &str, seed: u32) -> u32 {
424 let mut hash = seed;
425 let bytes = s.as_bytes();
426 let mut i = 0;
427 while i < bytes.len() {
428 let c = i32::from(bytes[i] as i8) as u32;
429 hash ^= !((hash << 11) ^ c ^ (hash >> 5));
430 i += 1;
431 if i >= bytes.len() {
432 break;
433 }
434 let c = i32::from(bytes[i] as i8) as u32;
435 hash ^= (hash << 7) ^ c ^ (hash >> 3);
436 i += 1;
437 }
438 hash
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444
445 /// Boundary: the empty string is the seed untouched — C's `while` never
446 /// runs — and an odd length exits through the mid-loop `break` while an
447 /// even one exits through the `while` test, so both arms are covered.
448 #[test]
449 fn epics_str_hash_matches_the_c_reference() {
450 for &(s, seed, want) in &[
451 ("", 0u32, 0u32),
452 ("", 0x1234_5678, 0x1234_5678),
453 ("a", 0, 0xffff_ff9e),
454 ("ab", 0, 0x1fff_cf0f),
455 ("abc", 0, 0x1e87_b6eb),
456 ("operator", 0, 0xe15e_03c1),
457 ("host.example.org", 0, 0x5134_8623),
458 ("abc", 0x1234_5678, 0xc25f_c587),
459 ("host.example.org", 0x1234_5678, 0xd9df_e17d),
460 ] {
461 assert_eq!(
462 epics_str_hash(s, seed),
463 want,
464 "epicsStrHash({s:?}, {seed:#x})"
465 );
466 }
467 }
468
469 /// Boundary: a byte at or above `0x80`. C's `char` is signed, so the
470 /// byte enters the XOR sign-extended; reading it as `u32` would give
471 /// `0x1fff_9e72` here instead.
472 #[test]
473 fn epics_str_hash_sign_extends_a_high_byte() {
474 assert_eq!(epics_str_hash("\u{e9}", 0), 0xffff_e192);
475 }
476
477 /// Parse a `0x…` hex float through the accumulator, the way both callers
478 /// drive it.
479 fn hex(s: &str) -> (f64, bool) {
480 let b = s.as_bytes();
481 let mut sig = HexSignificand::new();
482 let mut i = 2; // skip "0x"
483 while i < b.len() && b[i].is_ascii_hexdigit() {
484 sig.push_digit(hex_val(b[i]), false);
485 i += 1;
486 }
487 if i < b.len() && b[i] == b'.' {
488 i += 1;
489 while i < b.len() && b[i].is_ascii_hexdigit() {
490 sig.push_digit(hex_val(b[i]), true);
491 i += 1;
492 }
493 }
494 if i < b.len() && (b[i] | 0x20) == b'p' {
495 sig.apply_binary_exponent(s[i + 1..].parse::<i32>().unwrap());
496 }
497 sig.to_f64()
498 }
499
500 fn hex_val(c: u8) -> u8 {
501 match c {
502 b'0'..=b'9' => c - b'0',
503 _ => (c | 0x20) - b'a' + 10,
504 }
505 }
506
507 /// Every row probed against the compiled glibc `strtod`: `(text, bits of
508 /// the result, errno == ERANGE)`.
509 #[test]
510 fn matches_glibc_strtod_across_the_subnormal_boundary() {
511 let rows: &[(&str, u64, bool)] = &[
512 // Exactly representable subnormals: glibc leaves errno CLEAR.
513 ("0x1p-1074", 0x0000_0000_0000_0001, false),
514 ("0x2p-1075", 0x0000_0000_0000_0001, false),
515 ("0x1p-1073", 0x0000_0000_0000_0002, false),
516 ("0x1p-1023", 0x0008_0000_0000_0000, false),
517 // Tiny AND inexact: ERANGE, non-zero result (C: overflow).
518 ("0x1.8p-1075", 0x0000_0000_0000_0001, true),
519 ("0x1.4p-1074", 0x0000_0000_0000_0001, true),
520 ("0x1.cp-1074", 0x0000_0000_0000_0002, true),
521 ("0x3p-1075", 0x0000_0000_0000_0002, true),
522 ("0x1.0000000000001p-1074", 0x0000_0000_0000_0001, true),
523 ("0x123456789abcdefp-1100", 0x0000_0000_48d1_59e2, true),
524 // Tiny and inexact, yet it rounds up to the smallest NORMAL —
525 // ERANGE is decided before rounding, so it still fires.
526 ("0x1.fffffffffffffp-1023", 0x0010_0000_0000_0000, true),
527 // Tiny, inexact, rounds to zero: ERANGE (C: underflow).
528 ("0x1p-1075", 0, true), // exactly half → ties to even → zero
529 ("0x1p-1076", 0, true),
530 ("0x1p-2000", 0, true),
531 // Normal range.
532 ("0x1p-1022", 0x0010_0000_0000_0000, false),
533 ("0x1p1023", 0x7fe0_0000_0000_0000, false),
534 ("0x10", 0x4030_0000_0000_0000, false),
535 ("0x1.8p1", 0x4008_0000_0000_0000, false),
536 // Overflow.
537 ("0x1p1024", 0x7ff0_0000_0000_0000, true),
538 ("0x1.fffffffffffff8p1023", 0x7ff0_0000_0000_0000, true),
539 // A zero significand names zero exactly, at any exponent.
540 ("0x0p0", 0, false),
541 ("0x0p-5000", 0, false),
542 // More than 53 bits of significand: one rounding, ties to even.
543 ("0x1.00000000000008p0", 0x3ff0_0000_0000_0000, false),
544 ("0x1.00000000000018p0", 0x3ff0_0000_0000_0002, false),
545 ("0x1.0000000000000fp0", 0x3ff0_0000_0000_0001, false),
546 ];
547 for &(text, bits, erange) in rows {
548 let (v, e) = hex(text);
549 assert_eq!(v.to_bits(), bits, "value of {text}");
550 assert_eq!(e, erange, "ERANGE of {text}");
551 }
552 }
553}