/home/noah/src/ruchy/src/runtime/inspect.rs
Line | Count | Source |
1 | | //! Object inspection protocol for REPL display |
2 | | //! |
3 | | //! [OBJ-INSPECT-002] Implement consistent object introspection API |
4 | | |
5 | | use std::collections::{HashMap, HashSet}; |
6 | | use std::fmt::{self, Write}; |
7 | | |
8 | | /// Object inspection trait for human-readable display in REPL |
9 | | pub trait Inspect { |
10 | | /// Inspect the object, writing to the inspector |
11 | | fn inspect(&self, inspector: &mut Inspector) -> fmt::Result; |
12 | | |
13 | | /// Maximum recursion depth for this type |
14 | 0 | fn inspect_depth(&self) -> usize { |
15 | 0 | 1 |
16 | 0 | } |
17 | | } |
18 | | |
19 | | /// Inspector manages inspection state and formatting |
20 | | pub struct Inspector { |
21 | | /// Current recursion depth |
22 | | pub depth: usize, |
23 | | /// Maximum allowed depth |
24 | | pub max_depth: usize, |
25 | | /// Set of visited object addresses (cycle detection) |
26 | | visited: VisitSet, |
27 | | /// Complexity budget remaining |
28 | | pub budget: usize, |
29 | | /// Display style configuration |
30 | | pub style: InspectStyle, |
31 | | /// Output buffer |
32 | | pub output: String, |
33 | | } |
34 | | |
35 | | /// Optimized set for tracking visited objects |
36 | | struct VisitSet { |
37 | | /// Inline storage for common case (<8 elements) |
38 | | inline: [usize; 8], |
39 | | /// Current count |
40 | | count: usize, |
41 | | /// Overflow storage for larger sets |
42 | | overflow: Option<HashSet<usize>>, |
43 | | } |
44 | | |
45 | | impl VisitSet { |
46 | 0 | fn new() -> Self { |
47 | 0 | Self { |
48 | 0 | inline: [0; 8], |
49 | 0 | count: 0, |
50 | 0 | overflow: None, |
51 | 0 | } |
52 | 0 | } |
53 | | |
54 | 0 | fn insert(&mut self, addr: usize) -> bool { |
55 | | // Check inline storage first |
56 | 0 | for i in 0..self.count.min(8) { |
57 | 0 | if self.inline[i] == addr { |
58 | 0 | return false; // Already visited |
59 | 0 | } |
60 | | } |
61 | | |
62 | | // Check overflow if present |
63 | 0 | if let Some(ref mut overflow) = self.overflow { |
64 | 0 | return overflow.insert(addr); |
65 | 0 | } |
66 | | |
67 | | // Add to inline if space available |
68 | 0 | if self.count < 8 { |
69 | 0 | self.inline[self.count] = addr; |
70 | 0 | self.count += 1; |
71 | 0 | true |
72 | | } else { |
73 | | // Migrate to overflow storage |
74 | 0 | let mut overflow = HashSet::new(); |
75 | 0 | for &addr in &self.inline { |
76 | 0 | overflow.insert(addr); |
77 | 0 | } |
78 | 0 | overflow.insert(addr); |
79 | 0 | self.overflow = Some(overflow); |
80 | 0 | self.count += 1; |
81 | 0 | true |
82 | | } |
83 | 0 | } |
84 | | |
85 | 0 | fn contains(&self, addr: usize) -> bool { |
86 | | // Check inline |
87 | 0 | for i in 0..self.count.min(8) { |
88 | 0 | if self.inline[i] == addr { |
89 | 0 | return true; |
90 | 0 | } |
91 | | } |
92 | | |
93 | | // Check overflow |
94 | 0 | if let Some(ref overflow) = self.overflow { |
95 | 0 | overflow.contains(&addr) |
96 | | } else { |
97 | 0 | false |
98 | | } |
99 | 0 | } |
100 | | } |
101 | | |
102 | | /// Display style configuration |
103 | | #[derive(Debug, Clone)] |
104 | | pub struct InspectStyle { |
105 | | /// Maximum elements to display in collections |
106 | | pub max_elements: usize, |
107 | | /// Maximum string length before truncation |
108 | | pub max_string_len: usize, |
109 | | /// Use colors in output |
110 | | pub use_colors: bool, |
111 | | /// Indentation string |
112 | | pub indent: String, |
113 | | } |
114 | | |
115 | | impl Default for InspectStyle { |
116 | 0 | fn default() -> Self { |
117 | 0 | Self { |
118 | 0 | max_elements: 10, |
119 | 0 | max_string_len: 100, |
120 | 0 | use_colors: false, |
121 | 0 | indent: " ".to_string(), |
122 | 0 | } |
123 | 0 | } |
124 | | } |
125 | | |
126 | | /// Canonical display forms for values |
127 | | #[derive(Debug, Clone)] |
128 | | pub enum DisplayForm { |
129 | | /// Atomic values (42, true, "hello") |
130 | | Atomic(String), |
131 | | /// Composite values (`[1,2,3]`, `{x: 10}`) |
132 | | Composite(CompositeForm), |
133 | | /// Reference values (&value@0x7fff) |
134 | | Reference(usize, Box<DisplayForm>), |
135 | | /// Opaque values (`<fn>`, `<thread#42>`) |
136 | | Opaque(OpaqueHandle), |
137 | | } |
138 | | |
139 | | /// Composite value display structure |
140 | | #[derive(Debug, Clone)] |
141 | | pub struct CompositeForm { |
142 | | /// Opening delimiter |
143 | | pub opener: &'static str, |
144 | | /// Elements with optional labels |
145 | | pub elements: Vec<(Option<String>, DisplayForm)>, |
146 | | /// Closing delimiter |
147 | | pub closer: &'static str, |
148 | | /// Number of elided elements |
149 | | pub elided: Option<usize>, |
150 | | } |
151 | | |
152 | | /// Handle for opaque values |
153 | | #[derive(Debug, Clone)] |
154 | | pub struct OpaqueHandle { |
155 | | /// Type name |
156 | | pub type_name: String, |
157 | | /// Optional identifier |
158 | | pub id: Option<String>, |
159 | | } |
160 | | |
161 | | impl Default for Inspector { |
162 | 0 | fn default() -> Self { |
163 | 0 | Self::new() |
164 | 0 | } |
165 | | } |
166 | | |
167 | | impl Inspector { |
168 | | /// Create a new inspector with default settings |
169 | 0 | pub fn new() -> Self { |
170 | 0 | Self::with_style(InspectStyle::default()) |
171 | 0 | } |
172 | | |
173 | | /// Create an inspector with custom style |
174 | 0 | pub fn with_style(style: InspectStyle) -> Self { |
175 | 0 | Self { |
176 | 0 | depth: 0, |
177 | 0 | max_depth: 10, |
178 | 0 | visited: VisitSet::new(), |
179 | 0 | budget: 10000, |
180 | 0 | style, |
181 | 0 | output: String::new(), |
182 | 0 | } |
183 | 0 | } |
184 | | |
185 | | /// Enter a new inspection context (cycle detection) |
186 | 0 | pub fn enter<T>(&mut self, val: &T) -> bool { |
187 | 0 | let addr = std::ptr::from_ref::<T>(val) as usize; |
188 | 0 | if self.visited.contains(addr) { |
189 | 0 | false // Cycle detected |
190 | | } else { |
191 | 0 | self.visited.insert(addr); |
192 | 0 | self.depth += 1; |
193 | 0 | true |
194 | | } |
195 | 0 | } |
196 | | |
197 | | /// Exit an inspection context |
198 | 0 | pub fn exit(&mut self) { |
199 | 0 | self.depth = self.depth.saturating_sub(1); |
200 | 0 | } |
201 | | |
202 | | /// Check if budget allows continued inspection |
203 | 0 | pub fn has_budget(&self) -> bool { |
204 | 0 | self.budget > 0 |
205 | 0 | } |
206 | | |
207 | | /// Consume inspection budget |
208 | 0 | pub fn consume_budget(&mut self, amount: usize) { |
209 | 0 | self.budget = self.budget.saturating_sub(amount); |
210 | 0 | } |
211 | | |
212 | | /// Get current depth |
213 | 0 | pub fn depth(&self) -> usize { |
214 | 0 | self.depth |
215 | 0 | } |
216 | | |
217 | | /// Check if at maximum depth |
218 | 0 | pub fn at_max_depth(&self) -> bool { |
219 | 0 | self.depth >= self.max_depth |
220 | 0 | } |
221 | | } |
222 | | |
223 | | impl fmt::Write for Inspector { |
224 | 0 | fn write_str(&mut self, s: &str) -> fmt::Result { |
225 | 0 | self.consume_budget(s.len()); |
226 | 0 | self.output.push_str(s); |
227 | 0 | Ok(()) |
228 | 0 | } |
229 | | } |
230 | | |
231 | | // === Primitive Implementations === |
232 | | |
233 | | impl Inspect for i32 { |
234 | 0 | fn inspect(&self, inspector: &mut Inspector) -> fmt::Result { |
235 | 0 | write!(inspector, "{self}") |
236 | 0 | } |
237 | | } |
238 | | |
239 | | impl Inspect for i64 { |
240 | 0 | fn inspect(&self, inspector: &mut Inspector) -> fmt::Result { |
241 | 0 | write!(inspector, "{self}") |
242 | 0 | } |
243 | | } |
244 | | |
245 | | impl Inspect for f64 { |
246 | 0 | fn inspect(&self, inspector: &mut Inspector) -> fmt::Result { |
247 | 0 | write!(inspector, "{self}") |
248 | 0 | } |
249 | | } |
250 | | |
251 | | impl Inspect for bool { |
252 | 0 | fn inspect(&self, inspector: &mut Inspector) -> fmt::Result { |
253 | 0 | write!(inspector, "{self}") |
254 | 0 | } |
255 | | } |
256 | | |
257 | | impl Inspect for String { |
258 | 0 | fn inspect(&self, inspector: &mut Inspector) -> fmt::Result { |
259 | 0 | if self.len() <= inspector.style.max_string_len { |
260 | 0 | write!(inspector, "\"{self}\"") |
261 | | } else { |
262 | 0 | write!(inspector, "\"{}...\" ({} chars)", |
263 | 0 | &self[..inspector.style.max_string_len], |
264 | 0 | self.len()) |
265 | | } |
266 | 0 | } |
267 | | } |
268 | | |
269 | | impl Inspect for &str { |
270 | 0 | fn inspect(&self, inspector: &mut Inspector) -> fmt::Result { |
271 | 0 | if self.len() <= inspector.style.max_string_len { |
272 | 0 | write!(inspector, "\"{self}\"") |
273 | | } else { |
274 | 0 | write!(inspector, "\"{}...\" ({} chars)", |
275 | 0 | &self[..inspector.style.max_string_len], |
276 | 0 | self.len()) |
277 | | } |
278 | 0 | } |
279 | | } |
280 | | |
281 | | impl<T: Inspect> Inspect for Vec<T> { |
282 | 0 | fn inspect(&self, inspector: &mut Inspector) -> fmt::Result { |
283 | 0 | if inspector.at_max_depth() { |
284 | 0 | return write!(inspector, "[{} elements]", self.len()); |
285 | 0 | } |
286 | | |
287 | 0 | if !inspector.enter(self) { |
288 | 0 | return write!(inspector, "[<circular>]"); |
289 | 0 | } |
290 | | |
291 | 0 | write!(inspector, "[")?; |
292 | | |
293 | 0 | let display_count = self.len().min(inspector.style.max_elements); |
294 | 0 | for (i, item) in self.iter().take(display_count).enumerate() { |
295 | 0 | if i > 0 { |
296 | 0 | write!(inspector, ", ")?; |
297 | 0 | } |
298 | 0 | item.inspect(inspector)?; |
299 | | |
300 | 0 | if !inspector.has_budget() { |
301 | 0 | write!(inspector, ", ...")?; |
302 | 0 | break; |
303 | 0 | } |
304 | | } |
305 | | |
306 | 0 | if self.len() > display_count { |
307 | 0 | write!(inspector, ", ...{} more", self.len() - display_count)?; |
308 | 0 | } |
309 | | |
310 | 0 | inspector.exit(); |
311 | 0 | write!(inspector, "]") |
312 | 0 | } |
313 | | } |
314 | | |
315 | | impl<K: Inspect, V: Inspect> Inspect for HashMap<K, V> { |
316 | 0 | fn inspect(&self, inspector: &mut Inspector) -> fmt::Result { |
317 | 0 | if inspector.at_max_depth() { |
318 | 0 | return write!(inspector, "{{{} entries}}", self.len()); |
319 | 0 | } |
320 | | |
321 | 0 | if !inspector.enter(self) { |
322 | 0 | return write!(inspector, "{{<circular>}}"); |
323 | 0 | } |
324 | | |
325 | 0 | write!(inspector, "{{")?; |
326 | | |
327 | 0 | let display_count = self.len().min(inspector.style.max_elements); |
328 | 0 | for (i, (key, value)) in self.iter().take(display_count).enumerate() { |
329 | 0 | if i > 0 { |
330 | 0 | write!(inspector, ", ")?; |
331 | 0 | } |
332 | 0 | key.inspect(inspector)?; |
333 | 0 | write!(inspector, ": ")?; |
334 | 0 | value.inspect(inspector)?; |
335 | | |
336 | 0 | if !inspector.has_budget() { |
337 | 0 | write!(inspector, ", ...")?; |
338 | 0 | break; |
339 | 0 | } |
340 | | } |
341 | | |
342 | 0 | if self.len() > display_count { |
343 | 0 | write!(inspector, ", ...{} more", self.len() - display_count)?; |
344 | 0 | } |
345 | | |
346 | 0 | inspector.exit(); |
347 | 0 | write!(inspector, "}}") |
348 | 0 | } |
349 | | } |
350 | | |
351 | | impl<T: Inspect> Inspect for Option<T> { |
352 | 0 | fn inspect(&self, inspector: &mut Inspector) -> fmt::Result { |
353 | 0 | match self { |
354 | 0 | Some(val) => { |
355 | 0 | write!(inspector, "Some(")?; |
356 | 0 | val.inspect(inspector)?; |
357 | 0 | write!(inspector, ")") |
358 | | } |
359 | 0 | None => write!(inspector, "None"), |
360 | | } |
361 | 0 | } |
362 | | } |
363 | | |
364 | | impl<T: Inspect, E: Inspect> Inspect for Result<T, E> { |
365 | 0 | fn inspect(&self, inspector: &mut Inspector) -> fmt::Result { |
366 | 0 | match self { |
367 | 0 | Ok(val) => { |
368 | 0 | write!(inspector, "Ok(")?; |
369 | 0 | val.inspect(inspector)?; |
370 | 0 | write!(inspector, ")") |
371 | | } |
372 | 0 | Err(err) => { |
373 | 0 | write!(inspector, "Err(")?; |
374 | 0 | err.inspect(inspector)?; |
375 | 0 | write!(inspector, ")") |
376 | | } |
377 | | } |
378 | 0 | } |
379 | | } |
380 | | |
381 | | #[cfg(test)] |
382 | | mod tests { |
383 | | use super::*; |
384 | | |
385 | | #[test] |
386 | | fn test_primitive_inspection() { |
387 | | let mut inspector = Inspector::new(); |
388 | | |
389 | | 42i32.inspect(&mut inspector).unwrap(); |
390 | | assert!(inspector.output.contains("42")); |
391 | | } |
392 | | |
393 | | #[test] |
394 | | fn test_vector_inspection() { |
395 | | let vec = vec![1, 2, 3, 4, 5]; |
396 | | let mut inspector = Inspector::new(); |
397 | | |
398 | | vec.inspect(&mut inspector).unwrap(); |
399 | | assert!(inspector.output.contains('[')); |
400 | | assert!(inspector.output.contains('1')); |
401 | | assert!(inspector.output.contains('5')); |
402 | | } |
403 | | |
404 | | #[test] |
405 | | fn test_cycle_detection() { |
406 | | // Can't easily test with standard types, but VisitSet works |
407 | | let mut visited = VisitSet::new(); |
408 | | |
409 | | assert!(visited.insert(0x1000)); |
410 | | assert!(!visited.insert(0x1000)); // Already visited |
411 | | assert!(visited.insert(0x2000)); |
412 | | assert!(visited.contains(0x1000)); |
413 | | assert!(visited.contains(0x2000)); |
414 | | assert!(!visited.contains(0x3000)); |
415 | | } |
416 | | |
417 | | #[test] |
418 | | fn test_overflow_visit_set() { |
419 | | let mut visited = VisitSet::new(); |
420 | | |
421 | | // Fill inline storage |
422 | | for i in 0..10 { |
423 | | visited.insert(i * 0x1000); |
424 | | } |
425 | | |
426 | | // Should have migrated to overflow |
427 | | assert!(visited.overflow.is_some()); |
428 | | assert!(visited.contains(0x0000)); |
429 | | assert!(visited.contains(0x9000)); |
430 | | } |
431 | | } |