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}