Skip to main content

whitaker_common/rstest/
paragraph_fingerprint.rs

1//! Pure paragraph fingerprints for repeated `rstest` setup evidence.
2
3use std::collections::BTreeMap;
4
5/// A deterministic local-variable slot assigned by first appearance order.
6#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub struct LocalSlot(u32);
8
9impl LocalSlot {
10    /// Builds a local slot from its stable ordinal.
11    ///
12    /// # Examples
13    ///
14    /// ```
15    /// use whitaker_common::rstest::LocalSlot;
16    ///
17    /// let slot = LocalSlot::new(0);
18    /// assert_eq!(slot.index(), 0);
19    /// ```
20    #[must_use]
21    pub const fn new(index: u32) -> Self {
22        Self(index)
23    }
24
25    /// Returns the stable slot ordinal.
26    #[must_use]
27    pub const fn index(self) -> u32 {
28        self.0
29    }
30}
31
32/// Assigns deterministic local slots by first appearance order.
33#[derive(Clone, Debug, Default)]
34pub struct ParagraphNormalizer {
35    slots: BTreeMap<String, LocalSlot>,
36    next_slot: u32,
37}
38
39impl ParagraphNormalizer {
40    /// Builds an empty paragraph normalizer.
41    ///
42    /// # Examples
43    ///
44    /// ```
45    /// use whitaker_common::rstest::ParagraphNormalizer;
46    ///
47    /// let mut normalizer = ParagraphNormalizer::new();
48    ///
49    /// assert_eq!(normalizer.local_slot("user").index(), 0);
50    /// assert_eq!(normalizer.local_slot("cache").index(), 1);
51    /// assert_eq!(normalizer.local_slot("user").index(), 0);
52    /// ```
53    #[must_use]
54    pub const fn new() -> Self {
55        Self {
56            slots: BTreeMap::new(),
57            next_slot: 0,
58        }
59    }
60
61    /// Returns the deterministic slot for a local name.
62    ///
63    /// First appearance controls numbering. Later uses of the same local name
64    /// reuse the original slot.
65    #[must_use]
66    pub fn local_slot(&mut self, local_name: impl Into<String>) -> LocalSlot {
67        let local_name = local_name.into();
68        if let Some(slot) = self.slots.get(&local_name) {
69            return *slot;
70        }
71
72        let slot = LocalSlot::new(self.next_slot);
73        self.next_slot += 1;
74        self.slots.insert(local_name, slot);
75        slot
76    }
77}
78
79/// A normalized callee identity used inside paragraph statements.
80#[derive(Clone, Debug, PartialEq, Eq, Hash)]
81pub enum CalleeShape {
82    /// A known canonical definition path.
83    DefPath(String),
84    /// A present callee whose identity is not known to the shared model.
85    Unknown,
86}
87
88impl CalleeShape {
89    /// Builds a known definition-path callee shape.
90    ///
91    /// # Examples
92    ///
93    /// ```
94    /// use whitaker_common::rstest::CalleeShape;
95    ///
96    /// let callee = CalleeShape::def_path("crate::make_user");
97    /// assert_eq!(callee, CalleeShape::DefPath("crate::make_user".to_string()));
98    /// ```
99    #[must_use]
100    pub fn def_path(def_path: impl Into<String>) -> Self {
101        Self::DefPath(def_path.into())
102    }
103
104    /// Builds an unknown callee shape.
105    ///
106    /// # Examples
107    ///
108    /// ```
109    /// use whitaker_common::rstest::CalleeShape;
110    ///
111    /// assert_eq!(CalleeShape::unknown(), CalleeShape::Unknown);
112    /// ```
113    #[must_use]
114    pub const fn unknown() -> Self {
115        Self::Unknown
116    }
117}
118
119/// A normalized expression shape used by paragraph statements.
120#[derive(Clone, Debug, PartialEq, Eq, Hash)]
121pub enum ExprShape {
122    /// A function call with known or unknown callee identity and arity.
123    Call { callee: CalleeShape, argc: usize },
124    /// A method call with method name and arity.
125    MethodCall { method: String, argc: usize },
126    /// A stable path expression.
127    Path,
128    /// A stable literal expression.
129    Lit,
130    /// A present expression shape outside the supported model.
131    Other,
132}
133
134impl ExprShape {
135    /// Builds a function-call expression shape.
136    ///
137    /// # Examples
138    ///
139    /// ```
140    /// use whitaker_common::rstest::{CalleeShape, ExprShape};
141    ///
142    /// let shape = ExprShape::call(CalleeShape::def_path("crate::make_user"), 2);
143    /// assert_eq!(
144    ///     shape,
145    ///     ExprShape::Call {
146    ///         callee: CalleeShape::def_path("crate::make_user"),
147    ///         argc: 2,
148    ///     },
149    /// );
150    /// ```
151    #[must_use]
152    pub const fn call(callee: CalleeShape, argc: usize) -> Self {
153        Self::Call { callee, argc }
154    }
155
156    /// Builds a method-call expression shape.
157    #[must_use]
158    pub fn method_call(method: impl Into<String>, argc: usize) -> Self {
159        Self::MethodCall {
160            method: method.into(),
161            argc,
162        }
163    }
164
165    /// Builds a path expression shape.
166    #[must_use]
167    pub const fn path() -> Self {
168        Self::Path
169    }
170
171    /// Builds a literal expression shape.
172    #[must_use]
173    pub const fn lit() -> Self {
174        Self::Lit
175    }
176
177    /// Builds an explicit unsupported expression shape.
178    #[must_use]
179    pub const fn other() -> Self {
180        Self::Other
181    }
182}
183
184/// A normalized statement shape used for paragraph grouping.
185#[derive(Clone, Debug, PartialEq, Eq, Hash)]
186pub enum StmtShape {
187    /// A `let` statement represented by its initializer shape.
188    Let { init: ExprShape },
189    /// A mutating call, optionally tied to a normalized local receiver slot.
190    MutCall {
191        receiver: Option<LocalSlot>,
192        callee: CalleeShape,
193    },
194}
195
196impl StmtShape {
197    /// Builds a `let` statement shape from its initializer.
198    ///
199    /// # Examples
200    ///
201    /// ```
202    /// use whitaker_common::rstest::{ExprShape, StmtShape};
203    ///
204    /// assert_eq!(
205    ///     StmtShape::let_binding(ExprShape::lit()),
206    ///     StmtShape::Let { init: ExprShape::Lit },
207    /// );
208    /// ```
209    #[must_use]
210    pub const fn let_binding(init: ExprShape) -> Self {
211        Self::Let { init }
212    }
213
214    /// Builds a mutating-call statement shape.
215    #[must_use]
216    pub const fn mutable_call(receiver: Option<LocalSlot>, callee: CalleeShape) -> Self {
217        Self::MutCall { receiver, callee }
218    }
219}
220
221/// A normalized fingerprint for one assertion-free setup paragraph.
222#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
223pub struct ParagraphFingerprint {
224    shapes: Vec<StmtShape>,
225}
226
227impl ParagraphFingerprint {
228    /// Builds a paragraph fingerprint from normalized statement shapes.
229    ///
230    /// # Examples
231    ///
232    /// ```
233    /// use whitaker_common::rstest::{
234    ///     CalleeShape, ExprShape, ParagraphFingerprint, ParagraphNormalizer,
235    ///     StmtShape,
236    /// };
237    ///
238    /// let mut normalizer = ParagraphNormalizer::new();
239    /// let first = ParagraphFingerprint::new([
240    ///     StmtShape::let_binding(ExprShape::call(CalleeShape::def_path("crate::build"), 0)),
241    ///     StmtShape::mutable_call(Some(normalizer.local_slot("user")), CalleeShape::unknown()),
242    /// ]);
243    ///
244    /// let mut renamed = ParagraphNormalizer::new();
245    /// let second = ParagraphFingerprint::new([
246    ///     StmtShape::let_binding(ExprShape::call(CalleeShape::def_path("crate::build"), 0)),
247    ///     StmtShape::mutable_call(Some(renamed.local_slot("account")), CalleeShape::unknown()),
248    /// ]);
249    ///
250    /// assert_eq!(first, second);
251    /// ```
252    #[must_use]
253    pub fn new<I>(shapes: I) -> Self
254    where
255        I: IntoIterator<Item = StmtShape>,
256    {
257        Self {
258            shapes: shapes.into_iter().collect(),
259        }
260    }
261
262    /// Returns the stored statement shapes in paragraph order.
263    #[must_use]
264    pub fn shapes(&self) -> &[StmtShape] {
265        &self.shapes
266    }
267
268    /// Consumes the fingerprint and returns the stored statement shapes.
269    #[must_use]
270    pub fn into_shapes(self) -> Vec<StmtShape> {
271        self.shapes
272    }
273}