Skip to main content

serde_saphyr/
parse_scalars.rs

1#[cfg(feature = "deserialize")]
2use crate::de::{Error, Location};
3#[cfg(feature = "deserialize")]
4use crate::tags::SfTag;
5#[cfg(feature = "deserialize")]
6use granit_parser::ScalarStyle;
7#[cfg(feature = "deserialize")]
8use std::str::FromStr;
9
10/// Trim ASCII padding accepted during scalar conversion.
11///
12/// Besides YAML spaces and tabs, accept line breaks for quoted and block scalars.
13/// Other Unicode whitespace is scalar content and must be preserved.
14#[inline]
15pub(crate) fn yaml_trim(s: &str) -> &str {
16    s.trim_matches([' ', '\t', '\r', '\n'])
17}
18
19/// Parse a YAML 1.1 boolean from a &str (handles the "Norway problem").
20///
21/// Accepted TRUE literals (case-insensitive): "y", "yes", "true", "on"
22/// Accepted FALSE literals (case-insensitive): "n", "no", "false", "off"
23///
24/// Returns:
25/// - Ok(true/false) on success
26/// - Err(...) if the input is not a YAML 1.1 boolean literal
27pub(crate) fn parse_yaml11_bool(s: &str) -> Result<bool, String> {
28    let t = yaml_trim(s);
29    if t.eq_ignore_ascii_case("true")
30        || t.eq_ignore_ascii_case("yes")
31        || t.eq_ignore_ascii_case("y")
32        || t.eq_ignore_ascii_case("on")
33    {
34        Ok(true)
35    } else if t.eq_ignore_ascii_case("false")
36        || t.eq_ignore_ascii_case("no")
37        || t.eq_ignore_ascii_case("n")
38        || t.eq_ignore_ascii_case("off")
39    {
40        Ok(false)
41    } else {
42        Err(format!("invalid YAML 1.1 bool: `{s}`"))
43    }
44}
45
46#[cfg(feature = "deserialize")]
47fn parse_digits_u128(digits: &str, radix: u32) -> Option<u128> {
48    let mut val: u128 = 0;
49    let mut saw = false;
50    let bytes = digits.as_bytes();
51    for (i, &b) in bytes.iter().enumerate() {
52        match b {
53            b'_' => {
54                let prev_ok = i > 0 && bytes[i - 1] != b'_';
55                let next_ok = i + 1 < bytes.len() && bytes[i + 1] != b'_';
56                if !prev_ok || !next_ok {
57                    return None;
58                }
59            }
60            b'0'..=b'9' => {
61                let d = u32::from(b - b'0');
62                if d >= radix {
63                    return None;
64                }
65                val = val.checked_mul(u128::from(radix))?;
66                val = val.checked_add(u128::from(d))?;
67                saw = true;
68            }
69            b'a'..=b'f' if radix > 10 => {
70                let d = 10 + u32::from(b - b'a');
71                if d >= radix {
72                    return None;
73                }
74                val = val.checked_mul(u128::from(radix))?;
75                val = val.checked_add(u128::from(d))?;
76                saw = true;
77            }
78            b'A'..=b'F' if radix > 10 => {
79                let d = 10 + u32::from(b - b'A');
80                if d >= radix {
81                    return None;
82                }
83                val = val.checked_mul(u128::from(radix))?;
84                val = val.checked_add(u128::from(d))?;
85                saw = true;
86            }
87            _ => return None,
88        }
89    }
90    if saw { Some(val) } else { None }
91}
92
93#[cfg(feature = "deserialize")]
94fn parse_decimal_unsigned_u128(digits: &str) -> Option<u128> {
95    let mut val: u128 = 0;
96    let mut saw = false;
97    let bytes = digits.as_bytes();
98    for (i, &b) in bytes.iter().enumerate() {
99        match b {
100            b'_' => {
101                let prev_ok = i > 0 && bytes[i - 1] != b'_';
102                let next_ok = i + 1 < bytes.len() && bytes[i + 1] != b'_';
103                if !prev_ok || !next_ok {
104                    return None;
105                }
106            }
107            b'0'..=b'9' => {
108                let d = u128::from(b - b'0');
109                val = val.checked_mul(10)?;
110                val = val.checked_add(d)?;
111                saw = true;
112            }
113            _ => return None,
114        }
115    }
116    if saw { Some(val) } else { None }
117}
118
119#[cfg(feature = "deserialize")]
120fn parse_decimal_signed_i128(digits: &str, neg: bool) -> Option<i128> {
121    if neg {
122        // Accumulate as negative to allow i128::MIN
123        let mut val: i128 = 0;
124        let mut saw = false;
125        let bytes = digits.as_bytes();
126        for (i, &b) in bytes.iter().enumerate() {
127            match b {
128                b'_' => {
129                    let prev_ok = i > 0 && bytes[i - 1] != b'_';
130                    let next_ok = i + 1 < bytes.len() && bytes[i + 1] != b'_';
131                    if !prev_ok || !next_ok {
132                        return None;
133                    }
134                }
135                b'0'..=b'9' => {
136                    let d = i128::from(b - b'0');
137                    val = val.checked_mul(10)?;
138                    val = val.checked_sub(d)?;
139                    saw = true;
140                }
141                _ => return None,
142            }
143        }
144        if saw { Some(val) } else { None }
145    } else {
146        let mut val: i128 = 0;
147        let mut saw = false;
148        let bytes = digits.as_bytes();
149        for (i, &b) in bytes.iter().enumerate() {
150            match b {
151                b'_' => {
152                    let prev_ok = i > 0 && bytes[i - 1] != b'_';
153                    let next_ok = i + 1 < bytes.len() && bytes[i + 1] != b'_';
154                    if !prev_ok || !next_ok {
155                        return None;
156                    }
157                }
158                b'0'..=b'9' => {
159                    let d = i128::from(b - b'0');
160                    val = val.checked_mul(10)?;
161                    val = val.checked_add(d)?;
162                    saw = true;
163                }
164                _ => return None,
165            }
166        }
167        if saw { Some(val) } else { None }
168    }
169}
170
171#[cfg(feature = "deserialize")]
172pub(crate) fn parse_int_signed<T>(
173    s: &str,
174    ty: &'static str,
175    location: Location,
176    legacy_octal: bool,
177) -> Result<T, Error>
178where
179    T: TryFrom<i128>,
180{
181    let invalid = || Error::InvalidScalar { ty, location };
182
183    let t = yaml_trim(s);
184    let (neg, rest) = match t.strip_prefix('+') {
185        Some(r) => (false, r),
186        None => match t.strip_prefix('-') {
187            Some(r) => (true, r),
188            None => (false, t),
189        },
190    };
191
192    let (radix, digits) = radix_and_digits(legacy_octal, rest);
193    if radix == 10 {
194        // Yaml 1.2 forbids decimal integer literals starting with zero.
195        if digits.starts_with('0') && digits != "0" {
196            return Err(invalid());
197        }
198        let val_i128 = parse_decimal_signed_i128(digits, neg).ok_or_else(invalid)?;
199        return T::try_from(val_i128).map_err(|_| invalid());
200    }
201
202    let mag = parse_digits_u128(digits, radix).ok_or_else(invalid)?;
203    let val_i128: i128 = if neg {
204        if mag == (i128::MAX as u128) + 1 {
205            i128::MIN
206        } else {
207            let mag_i128: i128 = mag.try_into().map_err(|_| invalid())?;
208            mag_i128.checked_neg().ok_or_else(invalid)?
209        }
210    } else {
211        mag.try_into().map_err(|_| invalid())?
212    };
213    T::try_from(val_i128).map_err(|_| invalid())
214}
215
216#[cfg(feature = "deserialize")]
217pub(crate) fn parse_int_unsigned<T>(
218    s: &str,
219    ty: &'static str,
220    location: Location,
221    legacy_octal: bool,
222) -> Result<T, Error>
223where
224    T: TryFrom<u128>,
225{
226    let invalid = || Error::InvalidScalar { ty, location };
227
228    let t = yaml_trim(s);
229    if t.starts_with('-') {
230        return Err(invalid());
231    }
232    let rest = t.strip_prefix('+').unwrap_or(t);
233    let (radix, digits) = radix_and_digits(legacy_octal, rest);
234
235    if radix == 10 {
236        // Yaml 1.2 forbids decimal integer literals starting with zero.
237        if digits.starts_with('0') && digits != "0" {
238            return Err(invalid());
239        }
240        let val_u128 = parse_decimal_unsigned_u128(digits).ok_or_else(invalid)?;
241        return T::try_from(val_u128).map_err(|_| invalid());
242    }
243
244    let mag = parse_digits_u128(digits, radix).ok_or_else(invalid)?;
245    T::try_from(mag).map_err(|_| invalid())
246}
247
248#[cfg(feature = "deserialize")]
249fn radix_and_digits(legacy_octal: bool, rest: &str) -> (u32, &str) {
250    let (radix, digits) =
251        if let Some(r) = rest.strip_prefix("0x").or_else(|| rest.strip_prefix("0X")) {
252            (16u32, normalize_prefixed_digits(legacy_octal, r))
253        } else if let Some(r) = rest.strip_prefix("0o").or_else(|| rest.strip_prefix("0O")) {
254            (8u32, normalize_prefixed_digits(legacy_octal, r))
255        } else if let Some(r) = rest.strip_prefix("0b").or_else(|| rest.strip_prefix("0B")) {
256            (2u32, normalize_prefixed_digits(legacy_octal, r))
257        } else if legacy_octal && rest.starts_with('0') {
258            if rest == "0" {
259                // 0 is 0 and not empty string
260                (8u32, "0")
261            } else {
262                (8u32, normalize_prefixed_digits(legacy_octal, &rest[1..]))
263            }
264        } else {
265            (10u32, rest)
266        };
267    (radix, digits)
268}
269
270#[cfg(feature = "deserialize")]
271fn normalize_prefixed_digits(legacy_octal: bool, digits: &str) -> &str {
272    if legacy_octal {
273        digits.strip_prefix('_').unwrap_or(digits)
274    } else {
275        digits
276    }
277}
278
279#[cfg(feature = "deserialize")]
280fn parse_yaml12_finite_float_fallback<T>(t: &str, location: Location) -> Result<T, Error>
281where
282    T: FromStr,
283    T: num_traits::Float,
284{
285    let value = t.parse::<T>().map_err(|_| Error::InvalidScalar {
286        ty: "floating point",
287        location,
288    })?;
289
290    if value.is_finite() {
291        Ok(value)
292    } else {
293        Err(Error::InvalidScalar {
294            ty: "floating point",
295            location,
296        })
297    }
298}
299
300#[cfg(all(feature = "deserialize", feature = "robotics"))]
301pub(crate) fn parse_yaml12_float<T>(
302    s: &str,
303    location: Location,
304    tag: SfTag,
305    angle_conversions: bool,
306) -> Result<T, Error>
307where
308    T: FromStr + crate::robotics::FromF64,
309    T: num_traits::Float,
310{
311    if angle_conversions {
312        return crate::robotics::parse_yaml12_float_angle_converting(s, location, tag);
313    }
314    let t = yaml_trim(s);
315    let lower = t.to_ascii_lowercase();
316    match lower.as_str() {
317        ".nan" | "+.nan" | "-.nan" => Ok(T::nan()),
318        ".inf" | "+.inf" => Ok(T::infinity()),
319        "-.inf" => Ok(T::neg_infinity()),
320        _ => parse_yaml12_finite_float_fallback(t, location),
321    }
322}
323
324#[cfg(all(feature = "deserialize", not(feature = "robotics")))]
325pub(crate) fn parse_yaml12_float<T>(
326    s: &str,
327    location: Location,
328    _tag: SfTag,
329    _angle_conversions: bool,
330) -> Result<T, Error>
331where
332    T: FromStr,
333    T: num_traits::Float,
334{
335    let t = yaml_trim(s);
336    let lower = t.to_ascii_lowercase();
337    match lower.as_str() {
338        ".nan" | "+.nan" | "-.nan" => Ok(T::nan()),
339        ".inf" | "+.inf" => Ok(T::infinity()),
340        "-.inf" => Ok(T::neg_infinity()),
341        _ => parse_yaml12_finite_float_fallback(t, location),
342    }
343}
344
345#[cfg(feature = "deserialize")]
346/// Like [`parse_yaml12_float`], but a decimal/exponential literal that overflows `f64` to
347/// infinity (e.g. `1e999`, `9e400`) is treated as a successful non-finite parse instead of
348/// an error.
349///
350/// This is used only by `deserialize_any`'s typeless path (e.g. `serde_json::Value`), where
351/// non-finite floats are rejected by default. `NonFiniteFloatPolicy` can instead pass
352/// them to the visitor as floats or represent them as canonical strings.
353/// Elsewhere, overflowing literals continue to be rejected as invalid floats via
354/// [`parse_yaml12_float`], so this function must not replace it as the general entry point.
355///
356/// Deliberately narrower than a bare `str::parse::<f64>()`: Rust's parser also accepts
357/// alphabetic spellings (`inf`, `infinity`, `nan`) that YAML/serde-saphyr correctly keep as
358/// plain strings, so only numeral-shaped literals (optional sign, then a leading digit) are
359/// considered here.
360pub(crate) fn try_parse_float_incl_overflow(
361    s: &str,
362    location: Location,
363    tag: SfTag,
364    angle_conversions: bool,
365) -> Option<f64> {
366    if let Ok(v) = parse_yaml12_float::<f64>(s, location, tag, angle_conversions) {
367        return Some(v);
368    }
369
370    let t = yaml_trim(s);
371    let unsigned = t.strip_prefix(['+', '-']).unwrap_or(t);
372    if !unsigned.as_bytes().first().is_some_and(u8::is_ascii_digit) {
373        return None;
374    }
375
376    match t.parse::<f64>() {
377        Ok(v) if v.is_infinite() => Some(v),
378        _ => None,
379    }
380}
381
382#[cfg(feature = "deserialize")]
383/// If we are not using Rust struct as schema, check if we should not be quoting the value.
384pub(crate) fn maybe_not_string(s: &str, style: &ScalarStyle, strict_booleans: bool) -> bool {
385    let location = Location::UNKNOWN;
386    style == &ScalarStyle::Plain
387        && (parse_yaml12_float::<f64>(s, location, SfTag::None, false).is_ok()
388            || parse_int_signed::<i128>(s, "i128", location, false).is_ok()
389            || maybe_bool(s, strict_booleans)
390            || scalar_is_nullish(s, &ScalarStyle::Plain))
391}
392
393/// Check if a scalar looks like a YAML boolean, respecting `strict_booleans`.
394#[cfg(feature = "deserialize")]
395#[inline]
396fn maybe_bool(s: &str, strict: bool) -> bool {
397    if strict {
398        let s = yaml_trim(s);
399        s.eq_ignore_ascii_case("true") || s.eq_ignore_ascii_case("false")
400    } else {
401        parse_yaml11_bool(s).is_ok()
402    }
403}
404
405/// True if a scalar's text and style are YAML "null-like".
406/// Callers must also respect tags such as `!!str`, `!`, and `!!null`.
407///
408/// Arguments:
409/// - `value`: scalar text.
410/// - `style`: YAML scalar style; only plain form participates.
411///
412/// Returns:
413/// - `true` for empty, `~`, or case-insensitive `null`; `false` otherwise.
414///
415/// Used by:
416/// - Option, unit, typeless scalar handling, and other cases where absence is tolerated.
417#[cfg(feature = "deserialize")]
418#[inline]
419pub(crate) fn scalar_is_nullish(value: &str, style: &ScalarStyle) -> bool {
420    if !matches!(style, ScalarStyle::Plain) {
421        return false;
422    }
423    value.is_empty() || value == "~" || value.eq_ignore_ascii_case("null")
424}
425
426#[cfg(feature = "deserialize")]
427#[inline]
428/// Resolve null while honoring explicit core types, binary tags, and string-forcing tags.
429/// Non-null typed scalars must reach their deserializer even when their text looks null-like.
430pub(crate) fn scalar_is_null(tag: &SfTag, value: &str, style: &ScalarStyle) -> bool {
431    *tag == SfTag::Null
432        || (!tag.is_core()
433            && !tag.forces_string()
434            && *tag != SfTag::Binary
435            && scalar_is_nullish(value, style))
436}
437
438#[cfg(feature = "deserialize")]
439/// Returns `true` if the string represents a decimal number with a redundant leading zero,
440/// such as `0127`, `+0127`, or `-0127`.
441/// Explicit radices (`0x`, `0o`, `0b`) are excluded.
442/// A `true` result means this token should be avoided as an integer.
443pub(crate) fn leading_zero_decimal(t: &str) -> bool {
444    let s = yaml_trim(t);
445
446    // Handle optional sign
447    let digits = s.strip_prefix(['+', '-']).unwrap_or(s);
448
449    // Must start with 0 but not just "0"
450    if let Some(rest) = digits.strip_prefix('0') {
451        if let Some(next) = rest.chars().next() {
452            // If next char denotes radix, then allow
453            !matches!(next, 'x' | 'X' | 'o' | 'O' | 'b' | 'B')
454        } else {
455            false // "0", "+0", "-0"
456        }
457    } else {
458        false
459    }
460}
461
462#[cfg(all(test, feature = "deserialize"))]
463mod tests {
464    use super::*;
465    use rstest::rstest;
466
467    fn sample_location() -> Location {
468        Location {
469            line: 42,
470            column: 7,
471            span: crate::location::Span::UNKNOWN,
472            source_id: 0,
473        }
474    }
475
476    #[test]
477    fn null_resolution_preserves_explicit_binary_scalars() {
478        for value in ["null", "Null", "NULL", "~", ""] {
479            assert!(scalar_is_null(&SfTag::None, value, &ScalarStyle::Plain));
480            assert!(scalar_is_null(&SfTag::Null, value, &ScalarStyle::Plain));
481            assert!(
482                !scalar_is_null(&SfTag::Binary, value, &ScalarStyle::Plain),
483                "explicit binary scalar must reach its deserializer: {value:?}"
484            );
485        }
486    }
487
488    #[test]
489    fn yaml11_bool_accepts_all_literals_and_trims_whitespace() {
490        let truthy = ["true", "Yes", " y ", "ON\n"];
491        for value in truthy {
492            assert!(parse_yaml11_bool(value).unwrap());
493        }
494
495        let falsy = ["false", "No", " n ", "OFF\t"];
496        for value in falsy {
497            assert!(!parse_yaml11_bool(value).unwrap());
498        }
499    }
500
501    #[test]
502    fn yaml11_bool_reports_error_for_invalid_literal() {
503        let err = parse_yaml11_bool("maybe").unwrap_err();
504        assert!(err.contains("invalid YAML 1.1 bool"));
505    }
506
507    #[test]
508    fn parse_int_signed_supports_alternate_radices_and_underscores() {
509        let loc = sample_location();
510        let value: i64 = parse_int_signed("0x7_fF", "i64", loc, false).unwrap();
511        assert_eq!(value, 0x7ff);
512
513        let value: i32 = parse_int_signed("0b1010_1010", "i32", loc, false).unwrap();
514        assert_eq!(value, 0b1010_1010);
515    }
516
517    #[test]
518    fn parse_int_signed_supports_i128_min_in_alternate_radices() {
519        let loc = sample_location();
520        let hex: i128 =
521            parse_int_signed("-0x80000000000000000000000000000000", "i128", loc, false).unwrap();
522        let binary_min = format!("-0b1{}", "0".repeat(127));
523        let binary: i128 = parse_int_signed(&binary_min, "i128", loc, false).unwrap();
524
525        assert_eq!(hex, i128::MIN);
526        assert_eq!(binary, i128::MIN);
527        assert!(
528            parse_int_signed::<i128>("0x80000000000000000000000000000000", "i128", loc, false)
529                .is_err()
530        );
531    }
532
533    #[test]
534    fn parse_int_signed_rejects_invalid_underscores() {
535        let loc = sample_location();
536        // Leading underscore
537        assert!(parse_int_signed::<i32>("_1", "i32", loc, false).is_err());
538        // Trailing underscore
539        assert!(parse_int_signed::<i32>("1000_", "i32", loc, false).is_err());
540        // Double underscore
541        assert!(parse_int_signed::<i32>("1__0", "i32", loc, false).is_err());
542        // Valid underscores
543        assert!(parse_int_signed::<i32>("1000_1000", "i32", loc, false).is_ok());
544    }
545
546    #[test]
547    fn parse_int_signed_honors_legacy_octal_prefixes() {
548        let loc = sample_location();
549        let value: i32 = parse_int_signed("00077", "i32", loc, true).unwrap();
550        assert_eq!(value, 0o77);
551    }
552
553    #[test]
554    fn parse_int_signed_honors_legacy_prefix_underscores() {
555        let loc = sample_location();
556
557        let octal: i32 = parse_int_signed("0_10", "i32", loc, true).unwrap();
558        let plus_octal: i32 = parse_int_signed("+0_10", "i32", loc, true).unwrap();
559        let negative_octal: i32 = parse_int_signed("-0_10", "i32", loc, true).unwrap();
560        let hex: i32 = parse_int_signed("0x_10", "i32", loc, true).unwrap();
561        let explicit_octal: i32 = parse_int_signed("0o_10", "i32", loc, true).unwrap();
562        let binary: i32 = parse_int_signed("0b_10", "i32", loc, true).unwrap();
563
564        assert_eq!(octal, 0o10);
565        assert_eq!(plus_octal, 0o10);
566        assert_eq!(negative_octal, -0o10);
567        assert_eq!(hex, 0x10);
568        assert_eq!(explicit_octal, 0o10);
569        assert_eq!(binary, 0b10);
570    }
571
572    #[test]
573    fn parse_int_signed_keeps_prefix_underscores_opt_in() {
574        let loc = sample_location();
575
576        assert!(parse_int_signed::<i32>("0_10", "i32", loc, false).is_err());
577        assert!(parse_int_signed::<i32>("0x_10", "i32", loc, false).is_err());
578    }
579
580    #[test]
581    fn parse_int_signed_preserves_error_location() {
582        let loc = sample_location();
583        let err = parse_int_signed::<i64>("0x8000000000000000", "i64", loc, false).unwrap_err();
584        match err {
585            Error::InvalidScalar { location, .. } => assert_eq!(location, loc),
586            other => panic!("unexpected error variant: {:?}", other),
587        }
588    }
589
590    #[test]
591    fn parse_int_unsigned_rejects_negative_inputs() {
592        let loc = sample_location();
593        let err = parse_int_unsigned::<u32>("-5", "u32", loc, false).unwrap_err();
594        match err {
595            Error::InvalidScalar { location, .. } => assert_eq!(location, loc),
596            other => panic!("unexpected error variant: {:?}", other),
597        }
598    }
599
600    #[test]
601    fn parse_int_unsigned_honors_legacy_prefix_underscores() {
602        let loc = sample_location();
603
604        let octal: u32 = parse_int_unsigned("0_10", "u32", loc, true).unwrap();
605        let plus_octal: u32 = parse_int_unsigned("+0_10", "u32", loc, true).unwrap();
606        let hex: u32 = parse_int_unsigned("0x_10", "u32", loc, true).unwrap();
607        let explicit_octal: u32 = parse_int_unsigned("0o_10", "u32", loc, true).unwrap();
608        let binary: u32 = parse_int_unsigned("0b_10", "u32", loc, true).unwrap();
609
610        assert_eq!(octal, 0o10);
611        assert_eq!(plus_octal, 0o10);
612        assert_eq!(hex, 0x10);
613        assert_eq!(explicit_octal, 0o10);
614        assert_eq!(binary, 0b10);
615    }
616
617    #[test]
618    fn parse_yaml12_floats_handle_nan_and_infinity_forms() {
619        let loc = sample_location();
620
621        let nan: f64 = parse_yaml12_float(" .NaN ", loc, SfTag::None, false).unwrap();
622        assert!(nan.is_nan());
623
624        let inf: f64 = parse_yaml12_float("+.INF", loc, SfTag::None, false).unwrap();
625        assert!(inf.is_infinite() && inf.is_sign_positive());
626
627        let neg_inf: f64 = parse_yaml12_float("-.Inf", loc, SfTag::None, false).unwrap();
628        assert!(neg_inf.is_infinite() && neg_inf.is_sign_negative());
629    }
630
631    #[rstest]
632    #[case::nan("nan")]
633    #[case::capital_nan("NaN")]
634    #[case::inf("inf")]
635    #[case::plus_inf("+inf")]
636    #[case::minus_inf("-inf")]
637    #[case::infinity("Infinity")]
638    #[case::plus_infinity("+Infinity")]
639    #[case::minus_infinity("-Infinity")]
640    fn parse_yaml12_float_rejects_rust_nonfinite_spellings(#[case] input: &str) {
641        assert!(parse_yaml12_float::<f64>(input, loc(), SfTag::None, false).is_err());
642    }
643
644    fn loc() -> Location {
645        // Replace with how you construct Location in your code
646        Location {
647            line: 1,
648            column: 1,
649            span: crate::location::Span::UNKNOWN,
650            source_id: 0,
651        }
652    }
653
654    #[test]
655    fn test_normal_values() {
656        assert_eq!(
657            parse_yaml12_float::<f32>("1.5", loc(), SfTag::None, false).unwrap(),
658            1.5f32
659        );
660        assert_eq!(
661            parse_yaml12_float::<f32>("-123.456", loc(), SfTag::None, false).unwrap(),
662            -123.456f32
663        );
664    }
665
666    #[test]
667    fn test_zero_values() {
668        assert_eq!(
669            parse_yaml12_float::<f32>("0", loc(), SfTag::None, false).unwrap(),
670            0.0f32
671        );
672        assert_eq!(
673            parse_yaml12_float::<f32>("-0", loc(), SfTag::None, false).unwrap(),
674            -0.0f32
675        );
676    }
677
678    #[test]
679    fn test_nan_and_infinity() {
680        let nan: f32 = parse_yaml12_float(".nan", loc(), SfTag::None, false).unwrap();
681        assert!(nan.is_nan());
682
683        let inf: f64 = parse_yaml12_float(".inf", loc(), SfTag::None, false).unwrap();
684        assert!(inf.is_infinite() && inf.is_sign_positive());
685
686        let ninf: f32 = parse_yaml12_float("-.Inf", loc(), SfTag::None, false).unwrap();
687        assert!(ninf.is_infinite() && ninf.is_sign_negative());
688    }
689
690    #[test]
691    fn test_subnormal_preserved() {
692        // Smallest positive subnormal f32
693        let smallest = f32::from_bits(1) as f64;
694        let val: f32 =
695            parse_yaml12_float(&format!("{}", smallest), loc(), SfTag::None, false).unwrap();
696        assert_eq!(val, f32::from_bits(1));
697    }
698
699    #[test]
700    fn test_negative_zero_preserved() {
701        let val: f32 = parse_yaml12_float("-0.0", loc(), SfTag::None, false).unwrap();
702        assert_eq!(val.to_bits(), (-0.0f32).to_bits());
703    }
704}