ruchy/runtime/
inspect.rs

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