Skip to main content

modppl/modeling/
dyntrie.rs

1use crate::{Trace, Trie};
2use std::any::Any;
3use std::fmt::{self, Debug, Display, Write};
4use std::sync::Arc;
5
6///
7pub type DynTrie = Trie<Arc<dyn Any + Send + Sync>>;
8
9///
10pub type DynTrace<Args, Ret> = Trace<Args, DynTrie, Ret>;
11
12/// Conversion support for [`DynTrie::auto`].
13pub trait DynAutoCast: Clone + Sized + 'static {
14    /// Try to convert a dynamically stored value into `Self`.
15    fn autocast(value: &(dyn Any + Send + Sync)) -> Option<Self>;
16}
17
18impl DynTrie {
19    /// Safely casCastt the inner `dyn Any` at `addr` into type `V` at runtime.
20    pub fn read<V: 'static + Clone>(&self, addr: &str) -> V {
21        match self.search(addr) {
22            Some(v) => {
23                let v_typed = v.ref_inner().unwrap().downcast_ref::<V>();
24                match v_typed {
25                    Some(v) => v.clone(),
26                    None => {
27                        panic!("read: failed when downcasting type at address \"{}\"", addr);
28                    }
29                }
30            }
31            None => {
32                panic!("read: failed when searching empty address \"{}\"", addr);
33            }
34        }
35    }
36
37    /// Cast or convert the inner `dyn Any` at `addr` into type `V` at runtime.
38    ///
39    /// This first tries the same exact downcast as [`DynTrie::read`]. If that
40    /// fails, it tries [`DynAutoCast`] conversions for supported literal types.
41    ///
42    /// # Safety
43    ///
44    /// Autocasts may be lossy or surprising. Numeric conversions use Rust's
45    /// `as` casts, so callers must be willing to accept truncation, wrapping, or
46    /// precision loss where those casts allow it.
47    pub unsafe fn auto<V: DynAutoCast>(&self, addr: &str) -> V {
48        match self.search(addr) {
49            Some(v) => {
50                let value = v.ref_inner().unwrap().as_ref();
51                if let Some(value) = value.downcast_ref::<V>() {
52                    return value.clone();
53                }
54                V::autocast(value).unwrap_or_else(|| {
55                    panic!("auto: failed when autocasting type at address \"{addr}\"")
56                })
57            }
58            None => {
59                panic!("auto: failed when searching empty address \"{addr}\"");
60            }
61        }
62    }
63}
64
65macro_rules! impl_exact_autocast {
66    ($($ty:ty),* $(,)?) => {
67        $(
68            impl DynAutoCast for $ty {
69                fn autocast(_: &(dyn Any + Send + Sync)) -> Option<Self> {
70                    None
71                }
72            }
73        )*
74    };
75}
76
77macro_rules! impl_numeric_autocast {
78    ($target:ty; $($source:ty),* $(,)?) => {
79        impl DynAutoCast for $target {
80            fn autocast(value: &(dyn Any + Send + Sync)) -> Option<Self> {
81                $(
82                    if let Some(value) = value.downcast_ref::<$source>() {
83                        return Some(*value as $target);
84                    }
85                )*
86                None
87            }
88        }
89    };
90}
91
92macro_rules! impl_tuple2_autocast {
93    (($target:ty, $target2:ty); $(($source:ty, $source2:ty)),* $(,)?) => {
94        impl DynAutoCast for ($target, $target2) {
95            fn autocast(value: &(dyn Any + Send + Sync)) -> Option<Self> {
96                $(
97                    if let Some(value) = value.downcast_ref::<($source, $source2)>() {
98                        return Some((value.0 as $target, value.1 as $target2));
99                    }
100                )*
101                None
102            }
103        }
104    };
105}
106
107macro_rules! impl_tuple3_autocast {
108    (($target:ty, $target2:ty, $target3:ty); $(($source:ty, $source2:ty, $source3:ty)),* $(,)?) => {
109        impl DynAutoCast for ($target, $target2, $target3) {
110            fn autocast(value: &(dyn Any + Send + Sync)) -> Option<Self> {
111                $(
112                    if let Some(value) = value.downcast_ref::<($source, $source2, $source3)>() {
113                        return Some((
114                            value.0 as $target,
115                            value.1 as $target2,
116                            value.2 as $target3,
117                        ));
118                    }
119                )*
120                None
121            }
122        }
123    };
124}
125
126macro_rules! impl_tuple4_autocast {
127    (($target:ty, $target2:ty, $target3:ty, $target4:ty); $(($source:ty, $source2:ty, $source3:ty, $source4:ty)),* $(,)?) => {
128        impl DynAutoCast for ($target, $target2, $target3, $target4) {
129            fn autocast(value: &(dyn Any + Send + Sync)) -> Option<Self> {
130                $(
131                    if let Some(value) = value.downcast_ref::<($source, $source2, $source3, $source4)>() {
132                        return Some((
133                            value.0 as $target,
134                            value.1 as $target2,
135                            value.2 as $target3,
136                            value.3 as $target4,
137                        ));
138                    }
139                )*
140                None
141            }
142        }
143    };
144}
145
146impl_exact_autocast!(bool, char, String, &'static str);
147
148impl_numeric_autocast!(i8; i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64);
149impl_numeric_autocast!(i16; i8, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64);
150impl_numeric_autocast!(i32; i8, i16, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64);
151impl_numeric_autocast!(i64; i8, i16, i32, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64);
152impl_numeric_autocast!(i128; i8, i16, i32, i64, isize, u8, u16, u32, u64, u128, usize, f32, f64);
153impl_numeric_autocast!(isize; i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, usize, f32, f64);
154impl_numeric_autocast!(u8; i8, i16, i32, i64, i128, isize, u16, u32, u64, u128, usize, f32, f64);
155impl_numeric_autocast!(u16; i8, i16, i32, i64, i128, isize, u8, u32, u64, u128, usize, f32, f64);
156impl_numeric_autocast!(u32; i8, i16, i32, i64, i128, isize, u8, u16, u64, u128, usize, f32, f64);
157impl_numeric_autocast!(u64; i8, i16, i32, i64, i128, isize, u8, u16, u32, u128, usize, f32, f64);
158impl_numeric_autocast!(u128; i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, usize, f32, f64);
159impl_numeric_autocast!(usize; i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, f32, f64);
160impl_numeric_autocast!(f32; i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f64);
161impl_numeric_autocast!(f64; i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32);
162
163impl_tuple2_autocast!((f32, f32); (f64, f64));
164impl_tuple2_autocast!((f64, f64); (f32, f32));
165impl_tuple3_autocast!((f32, f32, f32); (f64, f64, f64));
166impl_tuple3_autocast!((f64, f64, f64); (f32, f32, f32));
167impl_tuple4_autocast!((f32, f32, f32, f32); (f64, f64, f64, f64));
168impl_tuple4_autocast!((f64, f64, f64, f64); (f32, f32, f32, f32));
169
170macro_rules! impl_array_autocast {
171    ($target:ty; $($source:ty),* $(,)?) => {
172        impl<const N: usize> DynAutoCast for [$target; N] {
173            fn autocast(value: &(dyn Any + Send + Sync)) -> Option<Self> {
174                $(
175                    if let Some(value) = value.downcast_ref::<[$source; N]>() {
176                        return Some(value.map(|x| x as $target));
177                    }
178                )*
179                None
180            }
181        }
182    };
183}
184
185impl_array_autocast!(f32; f64);
186impl_array_autocast!(f64; f32);
187
188fn format_tuple2<T: fmt::Display>(value: &(T, T)) -> String {
189    format!("({}, {})", value.0, value.1)
190}
191
192fn format_tuple3<T: fmt::Display>(value: &(T, T, T)) -> String {
193    format!("({}, {}, {})", value.0, value.1, value.2)
194}
195
196fn format_tuple4<T: fmt::Display>(value: &(T, T, T, T)) -> String {
197    format!("({}, {}, {}, {})", value.0, value.1, value.2, value.3)
198}
199
200fn format_array<T: fmt::Display, const N: usize>(value: &[T; N]) -> String {
201    let mut out = String::from("[");
202    for (i, x) in value.iter().enumerate() {
203        if i > 0 {
204            out.push_str(", ");
205        }
206        write!(out, "{x}").unwrap();
207    }
208    out.push(']');
209    out
210}
211
212/// Convert a dynamically stored trace leaf into a readable string.
213///
214/// This recognizes common literal-like values. Custom structs are intentionally
215/// reported as `<unknown>` because [`DynTrie`] stores leaves as [`Any`], which
216/// preserves type identity for downcasting but not a general `Display`/`Debug`
217/// implementation.
218pub fn dyn_value_to_string(value: &(dyn Any + Send + Sync)) -> String {
219    dyn_value_to_string_with(value, &[])
220}
221
222/// A printer-side formatter for a dynamically stored trace leaf.
223pub type DynValueFormatter = fn(&(dyn Any + Send + Sync)) -> Option<String>;
224
225/// Options for rendering dynamic traces as strings.
226#[derive(Clone, Copy, Debug, Eq, PartialEq)]
227pub struct DynTracePrintOptions {
228    /// Include the trace arguments.
229    pub args: bool,
230
231    /// Include the trace data choices.
232    pub data: bool,
233
234    /// Include the trace return value.
235    pub retv: bool,
236
237    /// Include the trace log joint probability.
238    pub logjp: bool,
239
240    /// Include node and leaf weights in the printed trace data.
241    pub weights: bool,
242}
243
244impl DynTracePrintOptions {
245    /// Set whether trace arguments are printed.
246    pub const fn set_args(mut self, args: bool) -> Self {
247        self.args = args;
248        self
249    }
250
251    /// Set whether trace data choices are printed.
252    pub const fn set_data(mut self, data: bool) -> Self {
253        self.data = data;
254        self
255    }
256
257    /// Set whether trace return values are printed.
258    pub const fn set_retv(mut self, retv: bool) -> Self {
259        self.retv = retv;
260        self
261    }
262
263    /// Set whether trace log joint probabilities are printed.
264    pub const fn set_logjp(mut self, logjp: bool) -> Self {
265        self.logjp = logjp;
266        self
267    }
268
269    /// Set whether data node and leaf weights are printed.
270    pub const fn set_weights(mut self, weights: bool) -> Self {
271        self.weights = weights;
272        self
273    }
274
275    /// Set maximum or minimum trace printing verbosity.
276    ///
277    /// Maximum verbosity prints every trace field and data weights. Minimum
278    /// verbosity prints only data choices without weights.
279    pub const fn verbose(verbose: bool) -> Self {
280        Self {
281            args: verbose,
282            data: true,
283            retv: verbose,
284            logjp: verbose,
285            weights: verbose,
286        }
287    }
288}
289
290impl Default for DynTracePrintOptions {
291    fn default() -> Self {
292        Self {
293            args: true,
294            data: true,
295            retv: true,
296            logjp: true,
297            weights: false,
298        }
299    }
300}
301
302/// Build a printer-side formatter for a custom type that implements [`Display`].
303pub fn dyn_display_formatter<T: Display + 'static>(
304    value: &(dyn Any + Send + Sync),
305) -> Option<String> {
306    value.downcast_ref::<T>().map(ToString::to_string)
307}
308
309/// Build a printer-side formatter for a custom type that implements [`Debug`].
310pub fn dyn_debug_formatter<T: Debug + 'static>(value: &(dyn Any + Send + Sync)) -> Option<String> {
311    value
312        .downcast_ref::<T>()
313        .map(|value| format!("{:?}", value))
314}
315
316/// Convert a dynamically stored trace leaf into a readable string, using
317/// custom printer-side formatters before falling back to built-in literals.
318pub fn dyn_value_to_string_with(
319    value: &(dyn Any + Send + Sync),
320    formatters: &[DynValueFormatter],
321) -> String {
322    for formatter in formatters {
323        if let Some(value) = formatter(value) {
324            return value;
325        }
326    }
327
328    macro_rules! downcast_display {
329        ($($ty:ty),* $(,)?) => {
330            $(
331                if let Some(value) = value.downcast_ref::<$ty>() {
332                    return value.to_string();
333                }
334            )*
335        };
336    }
337
338    macro_rules! downcast_tuple {
339        ($formatter:ident; $($ty:ty),* $(,)?) => {
340            $(
341                if let Some(value) = value.downcast_ref::<$ty>() {
342                    return $formatter(value);
343                }
344            )*
345        };
346    }
347
348    downcast_display!(
349        bool,
350        char,
351        String,
352        &'static str,
353        i8,
354        i16,
355        i32,
356        i64,
357        i128,
358        isize,
359        u8,
360        u16,
361        u32,
362        u64,
363        u128,
364        usize,
365        f32,
366        f64,
367    );
368
369    downcast_tuple!(format_tuple2; (f32, f32), (f64, f64));
370    downcast_tuple!(format_tuple3; (f32, f32, f32), (f64, f64, f64));
371    downcast_tuple!(format_tuple4; (f32, f32, f32, f32), (f64, f64, f64, f64));
372
373    macro_rules! downcast_array {
374        ($($ty:ty; $n:literal),* $(,)?) => {
375            $(
376                if let Some(value) = value.downcast_ref::<[$ty; $n]>() {
377                    return format_array(value);
378                }
379            )*
380        };
381    }
382
383    downcast_array!(
384        f32; 2, f32; 3, f32; 4,
385        f64; 2, f64; 3, f64; 4,
386    );
387
388    "<unknown>".to_string()
389}
390
391fn write_dyntrie(
392    out: &mut String,
393    trie: &DynTrie,
394    indent: usize,
395    formatters: &[DynValueFormatter],
396    options: DynTracePrintOptions,
397) {
398    let mut entries = trie.iter().collect::<Vec<_>>();
399    entries.sort_by(|(left, _), (right, _)| left.cmp(right));
400
401    for (addr, subtrie) in entries {
402        let padding = " ".repeat(indent);
403        if subtrie.is_leaf() {
404            let value = subtrie.ref_inner().unwrap();
405            write!(
406                out,
407                "{padding}\"{addr}\" => {}",
408                dyn_value_to_string_with(value.as_ref(), formatters)
409            )
410            .unwrap();
411            if options.weights {
412                write!(out, "  [weight = {}]", subtrie.weight()).unwrap();
413            }
414            writeln!(out).unwrap();
415        } else {
416            write!(out, "{padding}\"{addr}\" => {{").unwrap();
417            if options.weights {
418                write!(out, "  [weight = {}]", subtrie.weight()).unwrap();
419            }
420            writeln!(out).unwrap();
421            write_dyntrie(out, subtrie, indent + 2, formatters, options);
422            writeln!(out, "{padding}}}").unwrap();
423        }
424    }
425}
426
427/// Return a readable tree representation of a dynamic trace's data choices.
428pub fn dyntrie_to_string(trie: &DynTrie) -> String {
429    dyntrie_to_string_with(trie, &[])
430}
431
432/// Return a readable tree representation of a dynamic trace's data choices,
433/// using custom printer-side formatters before falling back to built-in literals.
434pub fn dyntrie_to_string_with(trie: &DynTrie, formatters: &[DynValueFormatter]) -> String {
435    dyntrie_to_string_with_options(trie, formatters, DynTracePrintOptions::default())
436}
437
438/// Return a readable tree representation of a dynamic trace's data choices,
439/// using custom printer-side formatters and rendering options.
440pub fn dyntrie_to_string_with_options(
441    trie: &DynTrie,
442    formatters: &[DynValueFormatter],
443    options: DynTracePrintOptions,
444) -> String {
445    let mut out = String::from("data: {\n");
446    write_dyntrie(&mut out, trie, 2, formatters, options);
447    out.push('}');
448    out
449}
450
451/// Return a readable representation of a dynamic trace.
452pub fn dyntrace_to_string<Args: Debug, Ret: Debug>(trace: &DynTrace<Args, Ret>) -> String {
453    dyntrace_to_string_with(trace, &[])
454}
455
456/// Return a readable representation of a dynamic trace, using custom
457/// printer-side formatters before falling back to built-in literals.
458pub fn dyntrace_to_string_with<Args: Debug, Ret: Debug>(
459    trace: &DynTrace<Args, Ret>,
460    formatters: &[DynValueFormatter],
461) -> String {
462    dyntrace_to_string_with_options(trace, formatters, DynTracePrintOptions::default())
463}
464
465/// Return a readable representation of a dynamic trace, using custom
466/// printer-side formatters and rendering options.
467pub fn dyntrace_to_string_with_options<Args: Debug, Ret: Debug>(
468    trace: &DynTrace<Args, Ret>,
469    formatters: &[DynValueFormatter],
470    options: DynTracePrintOptions,
471) -> String {
472    let mut out = String::new();
473    if options.args {
474        writeln!(out, "args: {:?}", trace.args).unwrap();
475    }
476    if options.data {
477        writeln!(
478            out,
479            "{}",
480            dyntrie_to_string_with_options(&trace.data, formatters, options)
481        )
482        .unwrap();
483    }
484    if options.retv {
485        match trace.retv.as_ref() {
486            Some(retv) => writeln!(out, "retv: {:?}", retv).unwrap(),
487            None => writeln!(out, "retv: None").unwrap(),
488        }
489    }
490    if options.logjp {
491        writeln!(out, "logjp: {}", trace.logjp).unwrap();
492    }
493    out
494}
495
496/// Print a readable representation of a dynamic trace.
497pub fn print_dyntrace<Args: Debug, Ret: Debug>(trace: &DynTrace<Args, Ret>) {
498    print!("{}", dyntrace_to_string(trace));
499}
500
501/// Print a readable representation of a dynamic trace, using custom
502/// printer-side formatters before falling back to built-in literals.
503pub fn print_dyntrace_with<Args: Debug, Ret: Debug>(
504    trace: &DynTrace<Args, Ret>,
505    formatters: &[DynValueFormatter],
506) {
507    print!("{}", dyntrace_to_string_with(trace, formatters));
508}
509
510/// Print a readable representation of a dynamic trace, using custom
511/// printer-side formatters and rendering options.
512pub fn print_dyntrace_with_options<Args: Debug, Ret: Debug>(
513    trace: &DynTrace<Args, Ret>,
514    formatters: &[DynValueFormatter],
515    options: DynTracePrintOptions,
516) {
517    print!(
518        "{}",
519        dyntrace_to_string_with_options(trace, formatters, options)
520    );
521}
522
523#[cfg(test)]
524mod tests {
525    use super::{
526        dyn_debug_formatter, dyn_display_formatter, dyn_value_to_string, dyn_value_to_string_with,
527        dyntrace_to_string_with_options, dyntrie_to_string, dyntrie_to_string_with_options,
528        DynTracePrintOptions,
529    };
530    use crate::{Trace, Trie};
531    use std::any::Any;
532    use std::fmt;
533    use std::sync::Arc;
534
535    #[derive(Debug)]
536    struct CustomStruct {
537        value: i64,
538    }
539
540    impl fmt::Display for CustomStruct {
541        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
542            write!(f, "CustomStruct(value = {})", self.value)
543        }
544    }
545
546    #[test]
547    fn dyn_value_to_string_prints_literals_and_unknown_custom_values() {
548        assert_eq!(dyn_value_to_string(&true), "true");
549        assert_eq!(dyn_value_to_string(&42_i64), "42");
550        assert_eq!(dyn_value_to_string(&"label"), "label");
551        assert_eq!(dyn_value_to_string(&(0.1_f64, 0.2_f64)), "(0.1, 0.2)");
552        assert_eq!(
553            dyn_value_to_string(&(0.3_f64, 0.5_f64, 0.2_f64)),
554            "(0.3, 0.5, 0.2)"
555        );
556        let custom = CustomStruct { value: 3 };
557        assert_eq!(custom.value, 3);
558        assert_eq!(dyn_value_to_string(&custom), "<unknown>");
559        assert_eq!(
560            dyn_value_to_string_with(&custom, &[dyn_display_formatter::<CustomStruct>]),
561            "CustomStruct(value = 3)"
562        );
563        assert_eq!(
564            dyn_value_to_string_with(&custom, &[dyn_debug_formatter::<CustomStruct>]),
565            "CustomStruct { value: 3 }"
566        );
567    }
568
569    #[test]
570    fn dyntrie_to_string_can_hide_weights() {
571        let mut trie = Trie::new();
572        trie.w_observe("x", Arc::new(1.0_f64) as Arc<dyn Any + Send + Sync>, -2.0);
573
574        assert!(!dyntrie_to_string(&trie).contains("[weight ="));
575        assert!(dyntrie_to_string_with_options(
576            &trie,
577            &[],
578            DynTracePrintOptions::default().set_weights(true)
579        )
580        .contains("[weight = -2]"));
581        assert!(!dyntrie_to_string_with_options(
582            &trie,
583            &[],
584            DynTracePrintOptions::default().set_weights(false)
585        )
586        .contains("[weight ="));
587    }
588
589    #[test]
590    fn dyntrace_print_options_control_fields_and_unwrap_retv() {
591        let mut trie = Trie::new();
592        trie.observe("x", Arc::new(1.0_f64) as Arc<dyn Any + Send + Sync>);
593        let trace = Trace::new("args", trie, "retv", -1.5);
594
595        let all_fields =
596            dyntrace_to_string_with_options(&trace, &[], DynTracePrintOptions::default());
597        assert!(all_fields.contains("args: \"args\""));
598        assert!(all_fields.contains("data: {"));
599        assert!(all_fields.contains("retv: \"retv\""));
600        assert!(!all_fields.contains("retv: Some"));
601        assert!(all_fields.contains("logjp: -1.5"));
602
603        let data_only = dyntrace_to_string_with_options(
604            &trace,
605            &[],
606            DynTracePrintOptions::default()
607                .set_args(false)
608                .set_retv(false)
609                .set_logjp(false),
610        );
611        assert!(!data_only.contains("args:"));
612        assert!(data_only.contains("data: {"));
613        assert!(!data_only.contains("retv:"));
614        assert!(!data_only.contains("logjp:"));
615    }
616
617    #[test]
618    fn dyntrace_print_options_verbose_sets_max_and_min() {
619        let mut trie = Trie::new();
620        trie.w_observe("x", Arc::new(1.0_f64) as Arc<dyn Any + Send + Sync>, -2.0);
621        let trace = Trace::new("args", trie, "retv", -1.5);
622
623        let max = dyntrace_to_string_with_options(&trace, &[], DynTracePrintOptions::verbose(true));
624        assert!(max.contains("args:"));
625        assert!(max.contains("data: {"));
626        assert!(max.contains("retv:"));
627        assert!(max.contains("logjp:"));
628        assert!(max.contains("[weight = -2]"));
629
630        let min =
631            dyntrace_to_string_with_options(&trace, &[], DynTracePrintOptions::verbose(false));
632        assert!(!min.contains("args:"));
633        assert!(min.contains("data: {"));
634        assert!(!min.contains("retv:"));
635        assert!(!min.contains("logjp:"));
636        assert!(!min.contains("[weight ="));
637    }
638
639    #[test]
640    fn dyntrie_auto_converts_supported_literals() {
641        let mut trie = Trie::new();
642        trie.observe("f64", Arc::new(0.25_f64) as Arc<dyn Any + Send + Sync>);
643        trie.observe(
644            "rgb",
645            Arc::new((0.3_f64, 0.5_f64, 0.2_f64)) as Arc<dyn Any + Send + Sync>,
646        );
647        trie.observe("n", Arc::new(3_u8) as Arc<dyn Any + Send + Sync>);
648
649        unsafe {
650            assert_eq!(trie.auto::<f32>("f64"), 0.25_f32);
651            assert_eq!(
652                trie.auto::<(f32, f32, f32)>("rgb"),
653                (0.3_f32, 0.5_f32, 0.2_f32)
654            );
655            assert_eq!(trie.auto::<i64>("n"), 3_i64);
656        }
657    }
658
659    #[test]
660    #[should_panic(expected = "read: failed when downcasting type at address \"f64\"")]
661    fn dyntrie_read_stays_strict_when_autocast_would_succeed() {
662        let mut trie = Trie::new();
663        trie.observe("f64", Arc::new(0.25_f64) as Arc<dyn Any + Send + Sync>);
664
665        let _ = trie.read::<f32>("f64");
666    }
667}