Skip to main content

gam_problem/
serde_finite.rs

1//! Structural non-finite-float guard for anything that implements [`Serialize`].
2//!
3//! ## Why this exists
4//!
5//! `serde_json` renders `f64::NAN` and `±f64::INFINITY` as the JSON literal
6//! `null` — JSON has no encoding for them and `serde_json`'s serializer takes
7//! the lossy branch silently. A persisted model carrying one non-finite scalar
8//! therefore *writes* without complaint and only fails on the way back in, as
9//!
10//! ```text
11//! invalid type: null, expected f64
12//! ```
13//!
14//! — an error that names neither the field nor the fit that produced it, and
15//! that surfaces arbitrarily far from the computation at fault (#2601).
16//!
17//! ## Why it is structural rather than a field list
18//!
19//! The pre-existing guards (`ensure_finite_scalar`, `validate_all_finite`, and
20//! the hand-maintained `FittedModel::validate_numeric_finiteness`) each name one
21//! field. A hand-maintained enumeration over a struct with hundreds of optional
22//! numeric fields cannot stay complete: every new field is opted OUT by default,
23//! so the guard silently stops covering the payload as the payload grows. That
24//! is exactly how #2601's `null` reached a saved model.
25//!
26//! [`ensure_serialized_floats_are_finite`] instead walks the value through
27//! `serde`'s own data model — the same traversal the JSON writer performs — so
28//! *every* float that would be written is checked, by construction, with no
29//! per-field opt-in. It tracks the struct-field / map-key / sequence-index path
30//! as it descends, so the error names the offending scalar the way the
31//! scalar-at-a-time guards do:
32//!
33//! ```text
34//! payload.fit_result.blocks[3].edf must be finite, got NaN
35//! ```
36//!
37//! The walk allocates nothing per scalar; only the current path (bounded by the
38//! nesting depth) and the borrowed field names are held.
39
40use serde::ser::{
41    Impossible, Serialize, SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant,
42    SerializeTuple, SerializeTupleStruct, SerializeTupleVariant, Serializer,
43};
44use std::fmt::{self, Display, Write as _};
45
46/// A non-finite float found at `path` while walking a serializable value.
47#[derive(Debug, Clone, PartialEq)]
48pub struct NonFiniteFloat {
49    /// Dotted / indexed path to the offending scalar, e.g.
50    /// `payload.fit_result.blocks[3].edf`. Empty when the value serialized is
51    /// itself a bare float.
52    pub path: String,
53    /// The offending value, widened to `f64` (`f32` inputs keep their class:
54    /// a `f32::NAN` reports as `NaN`).
55    pub value: f64,
56}
57
58impl Display for NonFiniteFloat {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        if self.path.is_empty() {
61            write!(f, "value must be finite, got {}", self.value)
62        } else {
63            write!(f, "{} must be finite, got {}", self.path, self.value)
64        }
65    }
66}
67
68impl std::error::Error for NonFiniteFloat {}
69
70/// Walk `value` through `serde`'s data model and fail on the FIRST non-finite
71/// `f32`/`f64` that a serializer would emit, reporting its path.
72///
73/// This is the write-side counterpart of the load-side type error: it converts
74/// "a `null` will silently appear in the output" into a typed refusal at the
75/// point of origin.
76pub fn ensure_serialized_floats_are_finite<T>(value: &T) -> Result<(), NonFiniteFloat>
77where
78    T: Serialize + ?Sized,
79{
80    let mut walker = FloatWalker { path: String::new() };
81    match value.serialize(&mut walker) {
82        Ok(()) => Ok(()),
83        Err(WalkError::NonFinite(found)) => Err(found),
84        // `Custom` can only arise from a `Serialize` impl that itself reports an
85        // error (e.g. a map with an unrepresentable key). Such a value cannot be
86        // serialized to JSON either, so there is no float verdict to give and
87        // the writer downstream will surface the same failure with its own
88        // message. Treat it as "nothing non-finite found here".
89        Err(WalkError::Custom(_)) => Ok(()),
90    }
91}
92
93/// Error channel of the walking serializer.
94#[derive(Debug)]
95enum WalkError {
96    NonFinite(NonFiniteFloat),
97    Custom(String),
98}
99
100impl Display for WalkError {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        match self {
103            WalkError::NonFinite(found) => Display::fmt(found, f),
104            WalkError::Custom(message) => f.write_str(message),
105        }
106    }
107}
108
109impl std::error::Error for WalkError {}
110
111impl serde::ser::Error for WalkError {
112    fn custom<T: Display>(msg: T) -> Self {
113        WalkError::Custom(msg.to_string())
114    }
115}
116
117/// Cursor over the serde data model carrying the path to the current value.
118struct FloatWalker {
119    path: String,
120}
121
122impl FloatWalker {
123    /// Append `.name` (or `name` at the root) and return the previous length so
124    /// the caller can truncate back after descending.
125    fn push_field(&mut self, name: &str) -> usize {
126        let restore = self.path.len();
127        if !self.path.is_empty() {
128            self.path.push('.');
129        }
130        self.path.push_str(name);
131        restore
132    }
133
134    fn push_index(&mut self, index: usize) -> usize {
135        let restore = self.path.len();
136        // `fmt::Write` for `String` never returns `Err`, so this names an
137        // invariant of the sink rather than hiding a failure mode. Do not
138        // "remove the Result" by writing `push_str(&index.to_string())`: that
139        // allocates once per sequence element and breaks this module's
140        // no-allocation-per-scalar promise.
141        write!(self.path, "[{index}]").expect("`String`'s `fmt::Write` impl is infallible");
142        restore
143    }
144
145    fn pop_to(&mut self, restore: usize) {
146        self.path.truncate(restore);
147    }
148
149    fn check(&self, value: f64) -> Result<(), WalkError> {
150        if value.is_finite() {
151            Ok(())
152        } else {
153            Err(WalkError::NonFinite(NonFiniteFloat {
154                path: self.path.clone(),
155                value,
156            }))
157        }
158    }
159}
160
161/// Render a map key into the path. Only string and integer keys are
162/// representable in JSON objects, which is the format this guard protects; any
163/// other key shape falls back to a positional index so the path stays useful.
164struct KeyRenderer;
165
166impl KeyRenderer {
167    /// A key serde offered as a compound value. JSON object keys are strings,
168    /// so there is no spelling for it; name the shape that was offered so the
169    /// `<key>` placeholder that lands in the path can be traced back to the type
170    /// that produced it.
171    fn not_a_scalar(shape: impl Display) -> WalkError {
172        WalkError::Custom(format!("map key is not a scalar: {shape}"))
173    }
174
175    /// Spelling of an enum variant used as a map key. `variant` is what the JSON
176    /// writer emits as the object key, so it is what the path segment must be —
177    /// but `derive` is not the only source of `Serialize` impls, and an empty
178    /// name would splice an invisible segment into the path. Fall back to the
179    /// enum and the variant index, which serde always supplies.
180    fn variant_key(name: &'static str, variant_index: u32, variant: &'static str) -> String {
181        if variant.is_empty() {
182            format!("{name}#{variant_index}")
183        } else {
184            variant.to_string()
185        }
186    }
187}
188
189impl Serializer for KeyRenderer {
190    type Ok = String;
191    type Error = WalkError;
192    type SerializeSeq = Impossible<String, WalkError>;
193    type SerializeTuple = Impossible<String, WalkError>;
194    type SerializeTupleStruct = Impossible<String, WalkError>;
195    type SerializeTupleVariant = Impossible<String, WalkError>;
196    type SerializeMap = Impossible<String, WalkError>;
197    type SerializeStruct = Impossible<String, WalkError>;
198    type SerializeStructVariant = Impossible<String, WalkError>;
199
200    fn serialize_str(self, value: &str) -> Result<String, WalkError> {
201        Ok(value.to_string())
202    }
203
204    fn serialize_bool(self, value: bool) -> Result<String, WalkError> {
205        Ok(value.to_string())
206    }
207
208    fn serialize_i64(self, value: i64) -> Result<String, WalkError> {
209        Ok(value.to_string())
210    }
211
212    fn serialize_i128(self, value: i128) -> Result<String, WalkError> {
213        Ok(value.to_string())
214    }
215
216    fn serialize_u64(self, value: u64) -> Result<String, WalkError> {
217        Ok(value.to_string())
218    }
219
220    fn serialize_u128(self, value: u128) -> Result<String, WalkError> {
221        Ok(value.to_string())
222    }
223
224    fn serialize_i8(self, value: i8) -> Result<String, WalkError> {
225        self.serialize_i64(i64::from(value))
226    }
227
228    fn serialize_i16(self, value: i16) -> Result<String, WalkError> {
229        self.serialize_i64(i64::from(value))
230    }
231
232    fn serialize_i32(self, value: i32) -> Result<String, WalkError> {
233        self.serialize_i64(i64::from(value))
234    }
235
236    fn serialize_u8(self, value: u8) -> Result<String, WalkError> {
237        self.serialize_u64(u64::from(value))
238    }
239
240    fn serialize_u16(self, value: u16) -> Result<String, WalkError> {
241        self.serialize_u64(u64::from(value))
242    }
243
244    fn serialize_u32(self, value: u32) -> Result<String, WalkError> {
245        self.serialize_u64(u64::from(value))
246    }
247
248    fn serialize_f32(self, value: f32) -> Result<String, WalkError> {
249        Ok(value.to_string())
250    }
251
252    fn serialize_f64(self, value: f64) -> Result<String, WalkError> {
253        Ok(value.to_string())
254    }
255
256    fn serialize_char(self, value: char) -> Result<String, WalkError> {
257        Ok(value.to_string())
258    }
259
260    fn serialize_bytes(self, value: &[u8]) -> Result<String, WalkError> {
261        // Byte strings have no JSON object-key spelling. The length is the one
262        // property that distinguishes two such keys in the path without
263        // rendering an unbounded blob into it.
264        Ok(format!("<{} bytes>", value.len()))
265    }
266
267    fn serialize_none(self) -> Result<String, WalkError> {
268        Ok("null".to_string())
269    }
270
271    fn serialize_some<T>(self, value: &T) -> Result<String, WalkError>
272    where
273        T: Serialize + ?Sized,
274    {
275        value.serialize(self)
276    }
277
278    fn serialize_unit(self) -> Result<String, WalkError> {
279        Ok("null".to_string())
280    }
281
282    fn serialize_unit_struct(self, name: &'static str) -> Result<String, WalkError> {
283        Ok(name.to_string())
284    }
285
286    fn serialize_unit_variant(
287        self,
288        name: &'static str,
289        variant_index: u32,
290        variant: &'static str,
291    ) -> Result<String, WalkError> {
292        Ok(Self::variant_key(name, variant_index, variant))
293    }
294
295    fn serialize_newtype_struct<T>(self, name: &'static str, value: &T) -> Result<String, WalkError>
296    where
297        T: Serialize + ?Sized,
298    {
299        // A newtype struct is transparent in JSON: the key is the inner value's
300        // spelling. If the inner value has no spelling, name the wrapper — that
301        // is the type the caller wrote, and the only one they can act on.
302        value
303            .serialize(self)
304            .map_err(|inner| WalkError::Custom(format!("inside newtype struct `{name}`: {inner}")))
305    }
306
307    fn serialize_newtype_variant<T>(
308        self,
309        name: &'static str,
310        variant_index: u32,
311        variant: &'static str,
312        value: &T,
313    ) -> Result<String, WalkError>
314    where
315        T: Serialize + ?Sized,
316    {
317        // Two entries keyed by the same variant but carrying different payloads
318        // are distinct keys, so the payload belongs in the spelling. Dropping it
319        // (as this did) collapsed them onto one path segment.
320        let inner = value.serialize(KeyRenderer)?;
321        Ok(format!(
322            "{}({inner})",
323            Self::variant_key(name, variant_index, variant)
324        ))
325    }
326
327    fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq, WalkError> {
328        Err(Self::not_a_scalar(match len {
329            Some(len) => format!("a sequence of {len} elements"),
330            None => "a sequence of unannounced length".to_string(),
331        }))
332    }
333
334    fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple, WalkError> {
335        Err(Self::not_a_scalar(format_args!("a {len}-tuple")))
336    }
337
338    fn serialize_tuple_struct(
339        self,
340        name: &'static str,
341        len: usize,
342    ) -> Result<Self::SerializeTupleStruct, WalkError> {
343        Err(Self::not_a_scalar(format_args!(
344            "tuple struct `{name}` with {len} fields"
345        )))
346    }
347
348    fn serialize_tuple_variant(
349        self,
350        name: &'static str,
351        variant_index: u32,
352        variant: &'static str,
353        len: usize,
354    ) -> Result<Self::SerializeTupleVariant, WalkError> {
355        Err(Self::not_a_scalar(format_args!(
356            "tuple variant `{name}::{variant}` (variant #{variant_index}) with {len} fields"
357        )))
358    }
359
360    fn serialize_map(self, len: Option<usize>) -> Result<Self::SerializeMap, WalkError> {
361        Err(Self::not_a_scalar(match len {
362            Some(len) => format!("a map of {len} entries"),
363            None => "a map of unannounced length".to_string(),
364        }))
365    }
366
367    fn serialize_struct(
368        self,
369        name: &'static str,
370        len: usize,
371    ) -> Result<Self::SerializeStruct, WalkError> {
372        Err(Self::not_a_scalar(format_args!(
373            "struct `{name}` with {len} fields"
374        )))
375    }
376
377    fn serialize_struct_variant(
378        self,
379        name: &'static str,
380        variant_index: u32,
381        variant: &'static str,
382        len: usize,
383    ) -> Result<Self::SerializeStructVariant, WalkError> {
384        Err(Self::not_a_scalar(format_args!(
385            "struct variant `{name}::{variant}` (variant #{variant_index}) with {len} fields"
386        )))
387    }
388}
389
390impl<'a> Serializer for &'a mut FloatWalker {
391    type Ok = ();
392    type Error = WalkError;
393    type SerializeSeq = SeqWalker<'a>;
394    type SerializeTuple = SeqWalker<'a>;
395    type SerializeTupleStruct = SeqWalker<'a>;
396    type SerializeTupleVariant = VariantSeqWalker<'a>;
397    type SerializeMap = MapWalker<'a>;
398    type SerializeStruct = StructWalker<'a>;
399    type SerializeStructVariant = StructWalker<'a>;
400
401    fn serialize_f64(self, value: f64) -> Result<(), WalkError> {
402        self.check(value)
403    }
404
405    fn serialize_f32(self, value: f32) -> Result<(), WalkError> {
406        // Widen for the verdict AND the message: `f32::NAN as f64` is still
407        // NaN and `f32::INFINITY as f64` is still infinite, so the class is
408        // preserved exactly.
409        self.check(f64::from(value))
410    }
411
412    // The integer widths and `char` carry no finiteness verdict, and saying so
413    // once per width states that decision fourteen times over. The narrow widths
414    // widen losslessly into the widest one of their signedness — the shape
415    // `KeyRenderer` already uses above — so "an integer is not a float" is
416    // decided in one place per signedness, and a future verdict (a range check,
417    // say) has one place to live.
418
419    fn serialize_bool(self, _: bool) -> Result<(), WalkError> {
420        Ok(())
421    }
422
423    fn serialize_i8(self, value: i8) -> Result<(), WalkError> {
424        self.serialize_i64(i64::from(value))
425    }
426
427    fn serialize_i16(self, value: i16) -> Result<(), WalkError> {
428        self.serialize_i64(i64::from(value))
429    }
430
431    fn serialize_i32(self, value: i32) -> Result<(), WalkError> {
432        self.serialize_i64(i64::from(value))
433    }
434
435    fn serialize_i64(self, value: i64) -> Result<(), WalkError> {
436        self.serialize_i128(i128::from(value))
437    }
438
439    fn serialize_i128(self, _: i128) -> Result<(), WalkError> {
440        Ok(())
441    }
442
443    fn serialize_u8(self, value: u8) -> Result<(), WalkError> {
444        self.serialize_u64(u64::from(value))
445    }
446
447    fn serialize_u16(self, value: u16) -> Result<(), WalkError> {
448        self.serialize_u64(u64::from(value))
449    }
450
451    fn serialize_u32(self, value: u32) -> Result<(), WalkError> {
452        self.serialize_u64(u64::from(value))
453    }
454
455    fn serialize_u64(self, value: u64) -> Result<(), WalkError> {
456        self.serialize_u128(u128::from(value))
457    }
458
459    fn serialize_u128(self, _: u128) -> Result<(), WalkError> {
460        Ok(())
461    }
462
463    fn serialize_char(self, value: char) -> Result<(), WalkError> {
464        // JSON writes a `char` as the one-character string it encodes to.
465        self.serialize_str(value.encode_utf8(&mut [0u8; 4]))
466    }
467
468    fn serialize_str(self, _: &str) -> Result<(), WalkError> {
469        Ok(())
470    }
471
472    fn serialize_bytes(self, _: &[u8]) -> Result<(), WalkError> {
473        Ok(())
474    }
475
476    fn serialize_none(self) -> Result<(), WalkError> {
477        Ok(())
478    }
479
480    fn serialize_some<T>(self, value: &T) -> Result<(), WalkError>
481    where
482        T: Serialize + ?Sized,
483    {
484        value.serialize(self)
485    }
486
487    fn serialize_unit(self) -> Result<(), WalkError> {
488        Ok(())
489    }
490
491    fn serialize_unit_struct(self, _: &'static str) -> Result<(), WalkError> {
492        Ok(())
493    }
494
495    fn serialize_unit_variant(
496        self,
497        _: &'static str,
498        _: u32,
499        _: &'static str,
500    ) -> Result<(), WalkError> {
501        Ok(())
502    }
503
504    fn serialize_newtype_struct<T>(self, _: &'static str, value: &T) -> Result<(), WalkError>
505    where
506        T: Serialize + ?Sized,
507    {
508        value.serialize(self)
509    }
510
511    fn serialize_newtype_variant<T>(
512        self,
513        name: &'static str,
514        variant_index: u32,
515        variant: &'static str,
516        value: &T,
517    ) -> Result<(), WalkError>
518    where
519        T: Serialize + ?Sized,
520    {
521        // Externally tagged enums serialize as `{"Variant": payload}`, so the
522        // variant name IS a path segment in the emitted JSON — and an empty one
523        // would splice an invisible segment into the reported path.
524        assert!(
525            !variant.is_empty(),
526            "`{name}` variant #{variant_index} has an empty name; \
527             its path segment would be invisible"
528        );
529        let restore = self.push_field(variant);
530        let outcome = value.serialize(&mut *self);
531        self.pop_to(restore);
532        outcome
533    }
534
535    fn serialize_seq(self, len: Option<usize>) -> Result<SeqWalker<'a>, WalkError> {
536        Ok(SeqWalker::new(self, len, Origin::plain("a sequence")))
537    }
538
539    fn serialize_tuple(self, len: usize) -> Result<SeqWalker<'a>, WalkError> {
540        Ok(SeqWalker::new(self, Some(len), Origin::plain("a tuple")))
541    }
542
543    fn serialize_tuple_struct(
544        self,
545        name: &'static str,
546        len: usize,
547    ) -> Result<SeqWalker<'a>, WalkError> {
548        Ok(SeqWalker::new(self, Some(len), Origin::plain(name)))
549    }
550
551    fn serialize_tuple_variant(
552        self,
553        name: &'static str,
554        variant_index: u32,
555        variant: &'static str,
556        len: usize,
557    ) -> Result<VariantSeqWalker<'a>, WalkError> {
558        let origin = Origin::variant(name, variant_index, variant);
559        let restore = self.push_field(variant);
560        Ok(VariantSeqWalker {
561            seq: SeqWalker::new(self, Some(len), origin),
562            restore,
563        })
564    }
565
566    fn serialize_map(self, len: Option<usize>) -> Result<MapWalker<'a>, WalkError> {
567        Ok(MapWalker {
568            walker: self,
569            restore: None,
570            announced: len,
571            entries: 0,
572            origin: Origin::plain("a map"),
573        })
574    }
575
576    fn serialize_struct(
577        self,
578        name: &'static str,
579        len: usize,
580    ) -> Result<StructWalker<'a>, WalkError> {
581        Ok(StructWalker {
582            walker: self,
583            restore: None,
584            announced: len,
585            fields: 0,
586            origin: Origin::plain(name),
587        })
588    }
589
590    fn serialize_struct_variant(
591        self,
592        name: &'static str,
593        variant_index: u32,
594        variant: &'static str,
595        len: usize,
596    ) -> Result<StructWalker<'a>, WalkError> {
597        let origin = Origin::variant(name, variant_index, variant);
598        let restore = self.push_field(variant);
599        Ok(StructWalker {
600            walker: self,
601            restore: Some(restore),
602            announced: len,
603            fields: 0,
604            origin,
605        })
606    }
607
608    fn collect_str<T>(self, _: &T) -> Result<(), WalkError>
609    where
610        T: Display + ?Sized,
611    {
612        Ok(())
613    }
614
615    fn is_human_readable(&self) -> bool {
616        // The format this guard protects is JSON. Types whose `Serialize` impl
617        // branches on this (e.g. compact binary encodings) must be walked in the
618        // same shape the JSON writer will use, or the guard would inspect a
619        // different set of floats than the one persisted.
620        true
621    }
622}
623
624/// What opened a compound, named for the length-coverage assertion below.
625#[derive(Clone, Copy)]
626struct Origin {
627    /// The type name serde supplied, or a literal for the anonymous compounds
628    /// (a bare sequence or map has no name in the data model).
629    type_name: &'static str,
630    /// `Some((variant, variant_index))` when the compound is an enum variant.
631    variant: Option<(&'static str, u32)>,
632}
633
634impl Origin {
635    fn plain(type_name: &'static str) -> Self {
636        Origin {
637            type_name,
638            variant: None,
639        }
640    }
641
642    fn variant(type_name: &'static str, variant_index: u32, variant: &'static str) -> Self {
643        Origin {
644            type_name,
645            variant: Some((variant, variant_index)),
646        }
647    }
648}
649
650impl Display for Origin {
651    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
652        match self.variant {
653            Some((variant, variant_index)) => write!(
654                f,
655                "`{}::{variant}` (variant #{variant_index})",
656                self.type_name
657            ),
658            None => f.write_str(self.type_name),
659        }
660    }
661}
662
663/// serde's contract: a compound that announces a length emits exactly that many
664/// elements. The guard's whole claim — *every* float the writer emits is
665/// checked — rests on the walk seeing the same elements the writer will, so a
666/// compound that emits a different number than it announced has subtrees the
667/// walk never visited. That is precisely the silent coverage loss this module
668/// exists to prevent (#2601), so announced lengths are checked, not ignored.
669fn assert_announced_len(origin: &Origin, announced: Option<usize>, emitted: usize) {
670    if let Some(announced) = announced {
671        assert_eq!(
672            emitted, announced,
673            "{origin} announced {announced} elements but emitted {emitted}"
674        );
675    }
676}
677
678/// Sequence / tuple cursor: elements are addressed by position.
679struct SeqWalker<'a> {
680    walker: &'a mut FloatWalker,
681    index: usize,
682    announced: Option<usize>,
683    origin: Origin,
684}
685
686impl<'a> SeqWalker<'a> {
687    fn new(walker: &'a mut FloatWalker, announced: Option<usize>, origin: Origin) -> Self {
688        SeqWalker {
689            walker,
690            index: 0,
691            announced,
692            origin,
693        }
694    }
695
696    fn finish(&self) {
697        assert_announced_len(&self.origin, self.announced, self.index);
698    }
699}
700
701impl SerializeSeq for SeqWalker<'_> {
702    type Ok = ();
703    type Error = WalkError;
704
705    fn serialize_element<T>(&mut self, value: &T) -> Result<(), WalkError>
706    where
707        T: Serialize + ?Sized,
708    {
709        let restore = self.walker.push_index(self.index);
710        let outcome = value.serialize(&mut *self.walker);
711        self.walker.pop_to(restore);
712        self.index += 1;
713        outcome
714    }
715
716    fn end(self) -> Result<(), WalkError> {
717        self.finish();
718        Ok(())
719    }
720}
721
722impl SerializeTuple for SeqWalker<'_> {
723    type Ok = ();
724    type Error = WalkError;
725
726    fn serialize_element<T>(&mut self, value: &T) -> Result<(), WalkError>
727    where
728        T: Serialize + ?Sized,
729    {
730        SerializeSeq::serialize_element(self, value)
731    }
732
733    fn end(self) -> Result<(), WalkError> {
734        SerializeSeq::end(self)
735    }
736}
737
738impl SerializeTupleStruct for SeqWalker<'_> {
739    type Ok = ();
740    type Error = WalkError;
741
742    fn serialize_field<T>(&mut self, value: &T) -> Result<(), WalkError>
743    where
744        T: Serialize + ?Sized,
745    {
746        SerializeSeq::serialize_element(self, value)
747    }
748
749    fn end(self) -> Result<(), WalkError> {
750        SerializeSeq::end(self)
751    }
752}
753
754/// A tuple variant additionally owns the pushed variant-name segment.
755struct VariantSeqWalker<'a> {
756    seq: SeqWalker<'a>,
757    restore: usize,
758}
759
760impl SerializeTupleVariant for VariantSeqWalker<'_> {
761    type Ok = ();
762    type Error = WalkError;
763
764    fn serialize_field<T>(&mut self, value: &T) -> Result<(), WalkError>
765    where
766        T: Serialize + ?Sized,
767    {
768        SerializeSeq::serialize_element(&mut self.seq, value)
769    }
770
771    fn end(self) -> Result<(), WalkError> {
772        self.seq.finish();
773        self.seq.walker.pop_to(self.restore);
774        Ok(())
775    }
776}
777
778/// Map cursor: the key is rendered into the path, then the value is walked.
779struct MapWalker<'a> {
780    walker: &'a mut FloatWalker,
781    /// Set while a key has been consumed and its value not yet walked.
782    restore: Option<usize>,
783    announced: Option<usize>,
784    entries: usize,
785    origin: Origin,
786}
787
788impl SerializeMap for MapWalker<'_> {
789    type Ok = ();
790    type Error = WalkError;
791
792    fn serialize_key<T>(&mut self, key: &T) -> Result<(), WalkError>
793    where
794        T: Serialize + ?Sized,
795    {
796        // A key that cannot be rendered as a scalar is not JSON-encodable at
797        // all; fall back to a positional marker so the walk (and its float
798        // verdict) still completes.
799        let rendered = key
800            .serialize(KeyRenderer)
801            .unwrap_or_else(|err| format!("<unrenderable key: {err}>"));
802        self.restore = Some(self.walker.push_field(&rendered));
803        self.entries += 1;
804        Ok(())
805    }
806
807    fn serialize_value<T>(&mut self, value: &T) -> Result<(), WalkError>
808    where
809        T: Serialize + ?Sized,
810    {
811        let outcome = value.serialize(&mut *self.walker);
812        if let Some(restore) = self.restore.take() {
813            self.walker.pop_to(restore);
814        }
815        outcome
816    }
817
818    fn end(self) -> Result<(), WalkError> {
819        assert_announced_len(&self.origin, self.announced, self.entries);
820        Ok(())
821    }
822}
823
824/// Struct cursor: fields are addressed by name.
825struct StructWalker<'a> {
826    walker: &'a mut FloatWalker,
827    /// `Some` for a struct *variant*, whose variant-name segment must be popped
828    /// when the compound ends.
829    restore: Option<usize>,
830    announced: usize,
831    fields: usize,
832    origin: Origin,
833}
834
835impl SerializeStruct for StructWalker<'_> {
836    type Ok = ();
837    type Error = WalkError;
838
839    fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), WalkError>
840    where
841        T: Serialize + ?Sized,
842    {
843        let restore = self.walker.push_field(key);
844        let outcome = value.serialize(&mut *self.walker);
845        self.walker.pop_to(restore);
846        self.fields += 1;
847        outcome
848    }
849
850    fn end(self) -> Result<(), WalkError> {
851        assert_announced_len(&self.origin, Some(self.announced), self.fields);
852        if let Some(restore) = self.restore {
853            self.walker.pop_to(restore);
854        }
855        Ok(())
856    }
857}
858
859impl SerializeStructVariant for StructWalker<'_> {
860    type Ok = ();
861    type Error = WalkError;
862
863    fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), WalkError>
864    where
865        T: Serialize + ?Sized,
866    {
867        SerializeStruct::serialize_field(self, key, value)
868    }
869
870    fn end(self) -> Result<(), WalkError> {
871        SerializeStruct::end(self)
872    }
873}
874
875#[cfg(test)]
876mod tests {
877    use super::*;
878    use serde::Serialize;
879    use std::collections::BTreeMap;
880
881    #[derive(Serialize)]
882    struct Leaf {
883        edf: f64,
884        name: String,
885    }
886
887    #[derive(Serialize)]
888    struct Root {
889        blocks: Vec<Leaf>,
890        scale: Option<f64>,
891        counts: Vec<u32>,
892        by_term: BTreeMap<String, f64>,
893    }
894
895    fn root() -> Root {
896        Root {
897            blocks: vec![
898                Leaf {
899                    edf: 1.0,
900                    name: "a".to_string(),
901                },
902                Leaf {
903                    edf: 2.0,
904                    name: "b".to_string(),
905                },
906            ],
907            scale: Some(0.5),
908            counts: vec![1, 2, 3],
909            by_term: BTreeMap::from([("s(x)".to_string(), 3.25)]),
910        }
911    }
912
913    #[test]
914    fn all_finite_payload_passes() {
915        assert!(ensure_serialized_floats_are_finite(&root()).is_ok());
916    }
917
918    #[test]
919    fn nested_sequence_element_reports_indexed_path() {
920        let mut value = root();
921        value.blocks[1].edf = f64::NAN;
922        let err = ensure_serialized_floats_are_finite(&value).unwrap_err();
923        assert_eq!(err.path, "blocks[1].edf");
924        assert!(err.value.is_nan());
925        assert!(
926            err.to_string().contains("blocks[1].edf must be finite"),
927            "message should name the path: {err}"
928        );
929    }
930
931    #[test]
932    fn optional_scalar_reports_its_field() {
933        let mut value = root();
934        value.scale = Some(f64::INFINITY);
935        let err = ensure_serialized_floats_are_finite(&value).unwrap_err();
936        assert_eq!(err.path, "scale");
937        assert_eq!(err.value, f64::INFINITY);
938    }
939
940    #[test]
941    fn map_value_reports_its_key() {
942        let mut value = root();
943        value
944            .by_term
945            .insert("s(z)".to_string(), f64::NEG_INFINITY);
946        let err = ensure_serialized_floats_are_finite(&value).unwrap_err();
947        assert_eq!(err.path, "by_term.s(z)");
948    }
949
950    #[test]
951    fn none_is_not_a_non_finite_float() {
952        let mut value = root();
953        value.scale = None;
954        assert!(ensure_serialized_floats_are_finite(&value).is_ok());
955    }
956
957    #[test]
958    fn bare_scalar_has_empty_path() {
959        let err = ensure_serialized_floats_are_finite(&f64::NAN).unwrap_err();
960        assert!(err.path.is_empty());
961        assert!(err.to_string().starts_with("value must be finite"));
962    }
963
964    #[test]
965    fn f32_non_finite_is_caught_and_widened() {
966        #[derive(Serialize)]
967        struct Small {
968            w: f32,
969        }
970        let err = ensure_serialized_floats_are_finite(&Small { w: f32::NAN }).unwrap_err();
971        assert_eq!(err.path, "w");
972        assert!(err.value.is_nan());
973    }
974
975    #[test]
976    fn struct_variant_and_newtype_variant_paths_are_reported() {
977        #[derive(Serialize)]
978        enum Node {
979            Scale { phi: f64 },
980            Raw(f64),
981        }
982        #[derive(Serialize)]
983        struct Holder {
984            node: Node,
985        }
986        let err =
987            ensure_serialized_floats_are_finite(&Holder { node: Node::Scale { phi: f64::NAN } })
988                .unwrap_err();
989        assert_eq!(err.path, "node.Scale.phi");
990        let err = ensure_serialized_floats_are_finite(&Holder {
991            node: Node::Raw(f64::INFINITY),
992        })
993        .unwrap_err();
994        assert_eq!(err.path, "node.Raw");
995    }
996
997    #[test]
998    fn path_state_is_restored_after_each_branch() {
999        // A finite branch visited BEFORE the offending one must not leave its
1000        // segments on the path (the truncate-on-exit contract).
1001        #[derive(Serialize)]
1002        struct Two {
1003            first: Vec<Leaf>,
1004            second: f64,
1005        }
1006        let err = ensure_serialized_floats_are_finite(&Two {
1007            first: vec![Leaf {
1008                edf: 1.0,
1009                name: "ok".to_string(),
1010            }],
1011            second: f64::NAN,
1012        })
1013        .unwrap_err();
1014        assert_eq!(err.path, "second");
1015    }
1016
1017    #[test]
1018    fn enum_keyed_map_reports_the_variant_as_its_key() {
1019        // A unit-variant key is written as the bare variant name, so that is the
1020        // segment the path must carry.
1021        #[derive(Serialize, PartialEq, Eq, PartialOrd, Ord)]
1022        enum Term {
1023            Linear,
1024            Smooth,
1025        }
1026        #[derive(Serialize)]
1027        struct Holder {
1028            by_term: BTreeMap<Term, f64>,
1029        }
1030        let err = ensure_serialized_floats_are_finite(&Holder {
1031            by_term: BTreeMap::from([(Term::Linear, 1.0), (Term::Smooth, f64::NAN)]),
1032        })
1033        .unwrap_err();
1034        assert_eq!(err.path, "by_term.Smooth");
1035    }
1036
1037    #[cfg(debug_assertions)]
1038    #[test]
1039    #[should_panic(expected = "announced 2 elements but emitted 1")]
1040    fn a_compound_that_under_emits_its_announced_length_is_caught() {
1041        // The coverage claim is only as good as serde's length contract: a
1042        // compound that emits fewer elements than it announced has subtrees the
1043        // walk never visited, and would otherwise pass by silence.
1044        struct UnderEmitting;
1045        impl Serialize for UnderEmitting {
1046            fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1047                let mut seq = serializer.serialize_seq(Some(2))?;
1048                seq.serialize_element(&1.0f64)?;
1049                seq.end()
1050            }
1051        }
1052        // `assert_announced_len` panics before this returns, which is what
1053        // `#[should_panic]` catches. Asserting on the result anyway keeps the
1054        // test honest: were that assertion ever removed, the under-emitting
1055        // walk would return `Ok` and this would fail rather than pass silently.
1056        assert!(
1057            ensure_serialized_floats_are_finite(&UnderEmitting).is_err(),
1058            "a compound emitting fewer elements than it announced must not pass"
1059        );
1060    }
1061
1062    #[test]
1063    fn walk_agrees_with_what_serde_json_would_write() {
1064        // The contract: the guard rejects exactly the payloads whose JSON
1065        // rendering contains a `null` that came from a float. Verify on a value
1066        // that serde_json silently lossy-renders.
1067        let mut value = root();
1068        value.blocks[0].edf = f64::NAN;
1069        let json = serde_json::to_string(&value).expect("serde_json renders NaN as null");
1070        assert!(
1071            json.contains("\"edf\":null"),
1072            "precondition: serde_json writes NaN as null, got {json}"
1073        );
1074        assert!(ensure_serialized_floats_are_finite(&value).is_err());
1075    }
1076}