Skip to main content

sim_lib_standard_core/
observation.rs

1//! Canonical, bounded projections of characterization observations.
2
3use sim_kernel::{Datum, Error, Result, Symbol};
4
5/// Profile-owned semantic projection for a guest value.
6pub type GuestValueProjection<T> = dyn Fn(&T) -> Result<Datum>;
7
8/// A bounded observation lane, retaining the distinction between no lane,
9/// an observed empty lane, and an observed prefix whose tail was omitted.
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub enum BoundedLane<T> {
12    /// The scenario did not select this lane.
13    Absent,
14    /// The complete ordered lane, including an observed empty lane.
15    Complete(Vec<T>),
16    /// The ordered prefix retained when the lane exceeded its bound.
17    Truncated {
18        /// Items retained in their original order.
19        items: Vec<T>,
20        /// Exact number of omitted trailing items.
21        omitted: usize,
22    },
23}
24
25impl<T> BoundedLane<T> {
26    /// Capture an explicitly selected lane under `limit`.
27    pub fn capture(items: Vec<T>, limit: usize) -> Self {
28        if items.len() <= limit {
29            Self::Complete(items)
30        } else {
31            let omitted = items.len() - limit;
32            Self::Truncated {
33                items: items.into_iter().take(limit).collect(),
34                omitted,
35            }
36        }
37    }
38}
39
40/// Stable source location attached to a failed outcome.
41#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct FailureLocation {
43    /// Stable source identity, independent of a host filesystem path.
44    pub source: Symbol,
45    /// Zero-based start byte in the identified source.
46    pub start: usize,
47    /// Zero-based exclusive end byte in the identified source.
48    pub end: usize,
49}
50
51/// Canonical failure fields used by characterization captures.
52#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct CanonicalFailure {
54    /// Stable failure class.
55    pub class: Symbol,
56    /// Semantic failure detail; display strings are not accepted here.
57    pub detail: Datum,
58    /// Optional stable source location.
59    pub location: Option<FailureLocation>,
60}
61
62/// Canonical result of a scenario.
63#[derive(Clone, Debug, PartialEq, Eq)]
64pub enum CanonicalOutcome {
65    /// Successful semantic data.
66    Success(Datum),
67    /// Stable failure data.
68    Failure(CanonicalFailure),
69}
70
71/// Every observable scenario lane after semantic projection.
72#[derive(Clone, Debug, PartialEq, Eq)]
73pub struct CanonicalObservation {
74    /// Selected value-or-failure lane, or `None` when absent.
75    pub outcome: Option<CanonicalOutcome>,
76    /// Ordered event data.
77    pub events: BoundedLane<Datum>,
78    /// Ordered receipt data.
79    pub receipts: BoundedLane<Datum>,
80    /// Ordered browse/Card data.
81    pub browse: BoundedLane<Datum>,
82}
83
84/// Project a guest value that has no intrinsic canonical data face.
85///
86/// The caller must supply a profile-owned semantic projection. Deliberately
87/// there is no `Debug` or display fallback.
88pub fn project_guest_value<T>(
89    value: &T,
90    projection: Option<&GuestValueProjection<T>>,
91) -> Result<Datum> {
92    projection.ok_or_else(|| {
93        Error::Eval("guest value has no canonical data face or profile projection".to_owned())
94    })?(value)
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    struct HostValue {
102        semantic: i64,
103        host_format: &'static str,
104    }
105
106    fn semantic_projection(value: &HostValue) -> Result<Datum> {
107        let _host_only_formatting = value.host_format;
108        Ok(Datum::String(value.semantic.to_string()))
109    }
110
111    fn observation(receipts: Vec<Datum>, location: FailureLocation) -> CanonicalObservation {
112        CanonicalObservation {
113            outcome: Some(CanonicalOutcome::Failure(CanonicalFailure {
114                class: Symbol::qualified("test", "rejected"),
115                detail: Datum::String("invalid-input".to_owned()),
116                location: Some(location),
117            })),
118            events: BoundedLane::Complete(vec![Datum::String("started".to_owned())]),
119            receipts: BoundedLane::capture(receipts, 4),
120            browse: BoundedLane::Complete(Vec::new()),
121        }
122    }
123
124    #[test]
125    fn guest_projection_ignores_host_formatting_and_is_mandatory() {
126        let terse = HostValue {
127            semantic: 7,
128            host_format: "7",
129        };
130        let verbose = HostValue {
131            semantic: 7,
132            host_format: "HostValue(7)",
133        };
134
135        assert_eq!(
136            project_guest_value(&terse, Some(&semantic_projection)).unwrap(),
137            project_guest_value(&verbose, Some(&semantic_projection)).unwrap()
138        );
139        assert!(project_guest_value(&terse, None).is_err());
140    }
141
142    #[test]
143    fn receipt_order_and_failure_location_are_semantic() {
144        let location = FailureLocation {
145            source: Symbol::qualified("fixture", "source"),
146            start: 2,
147            end: 5,
148        };
149        let first = Datum::String("first".to_owned());
150        let second = Datum::String("second".to_owned());
151        let baseline = observation(vec![first.clone(), second.clone()], location.clone());
152
153        assert_ne!(baseline, observation(vec![second, first], location.clone()));
154        assert_ne!(
155            baseline,
156            observation(
157                vec![
158                    Datum::String("first".to_owned()),
159                    Datum::String("second".to_owned())
160                ],
161                FailureLocation {
162                    start: 3,
163                    ..location
164                }
165            )
166        );
167    }
168
169    #[test]
170    fn absent_empty_and_truncated_lanes_remain_distinct() {
171        let empty = BoundedLane::<Datum>::capture(Vec::new(), 1);
172        let truncated = BoundedLane::capture(vec![Datum::Bool(true), Datum::Bool(false)], 1);
173
174        assert_ne!(BoundedLane::Absent, empty);
175        assert_ne!(empty, truncated);
176        assert_eq!(
177            truncated,
178            BoundedLane::Truncated {
179                items: vec![Datum::Bool(true)],
180                omitted: 1,
181            }
182        );
183    }
184}