Skip to main content

perl_dap/variables/
renderer.rs

1//! Variable renderer for DAP protocol.
2//!
3//! This module provides the [`VariableRenderer`] trait and [`PerlVariableRenderer`]
4//! implementation for converting Perl values into DAP-compatible variable representations.
5
6use crate::value::PerlValue;
7use serde::{Deserialize, Serialize};
8
9/// A rendered variable for the DAP protocol.
10///
11/// This struct represents a variable in a format suitable for the Debug Adapter Protocol,
12/// supporting lazy expansion of complex data structures.
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct RenderedVariable {
16    /// The name of the variable (e.g., "$foo", "@bar", "%hash")
17    pub name: String,
18
19    /// The string representation of the value
20    pub value: String,
21
22    /// The type of the variable (e.g., "SCALAR", "ARRAY", "HASH")
23    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
24    pub type_name: Option<String>,
25
26    /// Reference ID for lazy expansion (0 = not expandable)
27    pub variables_reference: i64,
28
29    /// Number of named children (for objects/hashes)
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub named_variables: Option<i64>,
32
33    /// Number of indexed children (for arrays)
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub indexed_variables: Option<i64>,
36
37    /// Presentation hint for the UI
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub presentation_hint: Option<VariablePresentationHint>,
40
41    /// Memory address (if available)
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub memory_reference: Option<String>,
44}
45
46/// Presentation hints for variable display in the DAP UI.
47#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48#[serde(rename_all = "camelCase")]
49pub struct VariablePresentationHint {
50    /// The kind of variable (e.g., "property", "method", "class")
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub kind: Option<String>,
53
54    /// Attributes (e.g., "static", "constant", "readOnly")
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub attributes: Option<Vec<String>>,
57
58    /// Visibility (e.g., "public", "private", "protected")
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub visibility: Option<String>,
61}
62
63impl RenderedVariable {
64    /// Creates a new rendered variable with the given name and value.
65    #[must_use]
66    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
67        Self {
68            name: name.into(),
69            value: value.into(),
70            type_name: None,
71            variables_reference: 0,
72            named_variables: None,
73            indexed_variables: None,
74            presentation_hint: None,
75            memory_reference: None,
76        }
77    }
78
79    /// Sets the type name for this variable.
80    #[must_use]
81    pub fn with_type(mut self, type_name: impl Into<String>) -> Self {
82        self.type_name = Some(type_name.into());
83        self
84    }
85
86    /// Sets the variables reference for lazy expansion.
87    #[must_use]
88    pub fn with_reference(mut self, reference: i64) -> Self {
89        self.variables_reference = reference;
90        self
91    }
92
93    /// Sets the indexed variables count (for arrays).
94    #[must_use]
95    pub fn with_indexed_variables(mut self, count: i64) -> Self {
96        self.indexed_variables = Some(count);
97        self
98    }
99
100    /// Sets the named variables count (for hashes/objects).
101    #[must_use]
102    pub fn with_named_variables(mut self, count: i64) -> Self {
103        self.named_variables = Some(count);
104        self
105    }
106
107    /// Returns true if this variable can be expanded.
108    #[must_use]
109    pub fn is_expandable(&self) -> bool {
110        self.variables_reference != 0
111    }
112}
113
114/// Trait for rendering Perl values into DAP variables.
115///
116/// Implementations of this trait convert [`PerlValue`] instances into
117/// [`RenderedVariable`] structures suitable for the DAP protocol.
118pub trait VariableRenderer {
119    /// Render a Perl value into a DAP variable.
120    ///
121    /// # Arguments
122    ///
123    /// * `name` - The variable name (e.g., "$foo")
124    /// * `value` - The Perl value to render
125    ///
126    /// # Returns
127    ///
128    /// A [`RenderedVariable`] suitable for the DAP protocol.
129    fn render(&self, name: &str, value: &PerlValue) -> RenderedVariable;
130
131    /// Render a Perl value with a specific variables reference ID.
132    ///
133    /// This is used when the value is expandable and needs a reference ID
134    /// for lazy loading of children.
135    ///
136    /// # Arguments
137    ///
138    /// * `name` - The variable name
139    /// * `value` - The Perl value to render
140    /// * `reference_id` - The variables reference ID for expansion
141    fn render_with_reference(
142        &self,
143        name: &str,
144        value: &PerlValue,
145        reference_id: i64,
146    ) -> RenderedVariable;
147
148    /// Render the children of an expandable value.
149    ///
150    /// # Arguments
151    ///
152    /// * `value` - The parent value to expand
153    /// * `start` - The starting index for pagination (0-based)
154    /// * `count` - The maximum number of children to return
155    ///
156    /// # Returns
157    ///
158    /// A vector of rendered child variables.
159    fn render_children(
160        &self,
161        value: &PerlValue,
162        start: usize,
163        count: usize,
164    ) -> Vec<RenderedVariable>;
165}
166
167/// Default Perl variable renderer implementation.
168///
169/// This renderer follows Perl conventions for variable display:
170/// - Strings are quoted
171/// - Arrays show element count
172/// - Hashes show key count
173/// - References show the referent type
174/// - Objects show class name
175#[derive(Debug, Default)]
176pub struct PerlVariableRenderer {
177    /// Maximum string length before truncation
178    max_string_length: usize,
179    /// Maximum array elements to show in preview
180    max_array_preview: usize,
181    /// Maximum hash pairs to show in preview
182    max_hash_preview: usize,
183}
184
185impl PerlVariableRenderer {
186    /// Creates a new Perl variable renderer with default settings.
187    #[must_use]
188    pub fn new() -> Self {
189        Self { max_string_length: 100, max_array_preview: 3, max_hash_preview: 3 }
190    }
191
192    /// Sets the maximum string length before truncation.
193    #[must_use]
194    pub fn with_max_string_length(mut self, length: usize) -> Self {
195        self.max_string_length = length;
196        self
197    }
198
199    /// Sets the maximum array elements in preview.
200    #[must_use]
201    pub fn with_max_array_preview(mut self, count: usize) -> Self {
202        self.max_array_preview = count;
203        self
204    }
205
206    /// Sets the maximum hash pairs in preview.
207    #[must_use]
208    pub fn with_max_hash_preview(mut self, count: usize) -> Self {
209        self.max_hash_preview = count;
210        self
211    }
212
213    /// Formats a scalar string value with quoting and truncation.
214    fn format_string(&self, s: &str) -> String {
215        let truncated = if s.len() > self.max_string_length {
216            // Find a char boundary at or before max_string_length to avoid
217            // panicking on multi-byte UTF-8 sequences.
218            let mut end = self.max_string_length;
219            while end > 0 && !s.is_char_boundary(end) {
220                end -= 1;
221            }
222            format!("{}...", &s[..end])
223        } else {
224            s.to_string()
225        };
226
227        // Escape special characters and quote
228        let escaped = truncated
229            .replace('\\', "\\\\")
230            .replace('"', "\\\"")
231            .replace('\n', "\\n")
232            .replace('\r', "\\r")
233            .replace('\t', "\\t");
234
235        format!("\"{}\"", escaped)
236    }
237
238    /// Formats an array value for preview.
239    fn format_array_preview(&self, elements: &[PerlValue]) -> String {
240        if elements.is_empty() {
241            return "[]".to_string();
242        }
243
244        let preview: Vec<String> = elements
245            .iter()
246            .take(self.max_array_preview)
247            .map(|v| self.format_value_brief(v))
248            .collect();
249
250        let suffix = if elements.len() > self.max_array_preview {
251            format!(", ... ({} total)", elements.len())
252        } else {
253            String::new()
254        };
255
256        format!("[{}{}]", preview.join(", "), suffix)
257    }
258
259    /// Formats a hash value for preview.
260    fn format_hash_preview(&self, pairs: &[(String, PerlValue)]) -> String {
261        if pairs.is_empty() {
262            return "{}".to_string();
263        }
264
265        let preview: Vec<String> = pairs
266            .iter()
267            .take(self.max_hash_preview)
268            .map(|(k, v)| format!("{} => {}", k, self.format_value_brief(v)))
269            .collect();
270
271        let suffix = if pairs.len() > self.max_hash_preview {
272            format!(", ... ({} keys)", pairs.len())
273        } else {
274            String::new()
275        };
276
277        format!("{{{}{}}}", preview.join(", "), suffix)
278    }
279
280    /// Returns the chain of backslash prefixes for nested references.
281    ///
282    /// For `\\\42` (ref to ref to ref to scalar) this returns `"\\\\"` (three backslashes).
283    /// Stops counting after 10 levels to avoid runaway recursion on cyclic-like structures.
284    fn ref_prefix_chain(&self, value: &PerlValue) -> String {
285        let mut depth = 0u32;
286        let mut current = value;
287        while let PerlValue::Reference(inner) = current {
288            depth += 1;
289            current = inner;
290            if depth >= 10 {
291                break;
292            }
293        }
294        "\\".repeat(depth as usize)
295    }
296
297    /// Formats the ultimate target of a reference chain briefly.
298    ///
299    /// Skips intermediate `Reference` wrappers to show the leaf value.
300    fn format_deref_target_brief(&self, value: &PerlValue) -> String {
301        let mut current = value;
302        let mut safety = 0u32;
303        while let PerlValue::Reference(inner) = current {
304            current = inner;
305            safety += 1;
306            if safety >= 10 {
307                break;
308            }
309        }
310        match current {
311            PerlValue::Reference(_) => "REF(...)".to_string(),
312            other => self.format_non_ref_brief(other),
313        }
314    }
315
316    /// Formats the ultimate target of a reference chain fully.
317    fn format_deref_target(&self, value: &PerlValue) -> String {
318        let mut current = value;
319        let mut safety = 0u32;
320        while let PerlValue::Reference(inner) = current {
321            current = inner;
322            safety += 1;
323            if safety >= 10 {
324                break;
325            }
326        }
327        match current {
328            PerlValue::Reference(_) => "REF(...)".to_string(),
329            other => self.format_value(other),
330        }
331    }
332
333    /// Formats a non-reference value briefly (used by deref target formatting).
334    fn format_non_ref_brief(&self, value: &PerlValue) -> String {
335        match value {
336            PerlValue::Reference(_) => "REF".to_string(),
337            other => self.format_value_brief(other),
338        }
339    }
340
341    /// Formats a value briefly (for use in previews).
342    fn format_value_brief(&self, value: &PerlValue) -> String {
343        match value {
344            PerlValue::Undef => "undef".to_string(),
345            PerlValue::Scalar(s) => self.format_string(s),
346            PerlValue::Number(n) => n.to_string(),
347            PerlValue::Integer(i) => i.to_string(),
348            PerlValue::Array(elements) => format!("ARRAY({})", elements.len()),
349            PerlValue::Hash(pairs) => format!("HASH({})", pairs.len()),
350            PerlValue::Reference(inner) => {
351                let prefix = self.ref_prefix_chain(value);
352                format!("{}{}", prefix, self.format_deref_target_brief(inner))
353            }
354            PerlValue::Object { class, value } => {
355                let backing = match value.as_ref() {
356                    PerlValue::Hash(_) => "HASH",
357                    PerlValue::Array(_) => "ARRAY",
358                    PerlValue::Scalar(_) | PerlValue::Number(_) | PerlValue::Integer(_) => "SCALAR",
359                    _ => "REF",
360                };
361                format!("{} = {}(...)", class, backing)
362            }
363            PerlValue::Code { name } => {
364                name.as_ref().map_or_else(|| "CODE(...)".to_string(), |n| format!("\\&{}", n))
365            }
366            PerlValue::Glob(name) => format!("*{}", name),
367            PerlValue::Regex(pattern) => format!("qr/{}/", pattern),
368            PerlValue::Tied { class, .. } => format!("TIED({})", class),
369            PerlValue::Truncated { summary, .. } => summary.clone(),
370            PerlValue::Error(msg) => format!("<error: {}>", msg),
371        }
372    }
373
374    /// Formats a full value (for the value field).
375    fn format_value(&self, value: &PerlValue) -> String {
376        match value {
377            PerlValue::Undef => "undef".to_string(),
378            PerlValue::Scalar(s) => self.format_string(s),
379            PerlValue::Number(n) => n.to_string(),
380            PerlValue::Integer(i) => i.to_string(),
381            PerlValue::Array(elements) => self.format_array_preview(elements),
382            PerlValue::Hash(pairs) => self.format_hash_preview(pairs),
383            PerlValue::Reference(inner) => {
384                let prefix = self.ref_prefix_chain(value);
385                format!("{}{}", prefix, self.format_deref_target(inner))
386            }
387            PerlValue::Object { class, value } => {
388                let backing = match value.as_ref() {
389                    PerlValue::Hash(_) => "HASH",
390                    PerlValue::Array(_) => "ARRAY",
391                    PerlValue::Scalar(_) | PerlValue::Number(_) | PerlValue::Integer(_) => "SCALAR",
392                    _ => "REF",
393                };
394                format!("{} = {}(...)", class, backing)
395            }
396            PerlValue::Code { name } => {
397                name.as_ref().map_or_else(|| "sub { ... }".to_string(), |n| format!("\\&{}", n))
398            }
399            PerlValue::Glob(name) => format!("*{}", name),
400            PerlValue::Regex(pattern) => format!("qr/{}/", pattern),
401            PerlValue::Tied { class, value } => {
402                if let Some(v) = value {
403                    format!("TIED({}) = {}", class, self.format_value_brief(v))
404                } else {
405                    format!("TIED({})", class)
406                }
407            }
408            PerlValue::Truncated { summary, total_count } => {
409                if let Some(count) = total_count {
410                    format!("{} ({} total)", summary, count)
411                } else {
412                    summary.clone()
413                }
414            }
415            PerlValue::Error(msg) => format!("<error: {}>", msg),
416        }
417    }
418}
419
420impl VariableRenderer for PerlVariableRenderer {
421    fn render(&self, name: &str, value: &PerlValue) -> RenderedVariable {
422        let formatted_value = self.format_value(value);
423        let type_name = value.type_name().to_string();
424
425        let mut rendered = RenderedVariable::new(name, formatted_value).with_type(type_name);
426
427        // Set child counts for expandable types
428        match value {
429            PerlValue::Array(elements) => {
430                rendered.indexed_variables = Some(elements.len() as i64);
431            }
432            PerlValue::Hash(pairs) => {
433                rendered.named_variables = Some(pairs.len() as i64);
434            }
435            PerlValue::Object { class, value: inner } => {
436                rendered.type_name = Some(class.clone());
437                rendered.presentation_hint = Some(VariablePresentationHint {
438                    kind: Some("class".to_string()),
439                    attributes: None,
440                    visibility: None,
441                });
442                match inner.as_ref() {
443                    PerlValue::Hash(pairs) => {
444                        rendered.named_variables = Some(pairs.len() as i64);
445                    }
446                    PerlValue::Array(elements) => {
447                        rendered.indexed_variables = Some(elements.len() as i64);
448                    }
449                    _ => {}
450                }
451            }
452            _ => {}
453        }
454
455        rendered
456    }
457
458    fn render_with_reference(
459        &self,
460        name: &str,
461        value: &PerlValue,
462        reference_id: i64,
463    ) -> RenderedVariable {
464        let mut rendered = self.render(name, value);
465
466        if value.is_expandable() {
467            rendered.variables_reference = reference_id;
468        }
469
470        rendered
471    }
472
473    fn render_children(
474        &self,
475        value: &PerlValue,
476        start: usize,
477        count: usize,
478    ) -> Vec<RenderedVariable> {
479        match value {
480            PerlValue::Array(elements) => elements
481                .iter()
482                .enumerate()
483                .skip(start)
484                .take(count)
485                .map(|(i, v)| self.render(&format!("[{}]", i), v))
486                .collect(),
487            PerlValue::Hash(pairs) => {
488                pairs.iter().skip(start).take(count).map(|(k, v)| self.render(k, v)).collect()
489            }
490            PerlValue::Reference(inner) => {
491                vec![self.render("$_", inner)]
492            }
493            PerlValue::Object { value: inner, .. } => self.render_children(inner, start, count),
494            PerlValue::Tied { value: Some(inner), .. } => self.render_children(inner, start, count),
495            _ => vec![],
496        }
497    }
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503
504    #[test]
505    fn test_render_scalar() {
506        let renderer = PerlVariableRenderer::new();
507        let value = PerlValue::Scalar("hello".to_string());
508        let rendered = renderer.render("$x", &value);
509
510        assert_eq!(rendered.name, "$x");
511        assert_eq!(rendered.value, "\"hello\"");
512        assert_eq!(rendered.type_name, Some("SCALAR".to_string()));
513        assert_eq!(rendered.variables_reference, 0);
514    }
515
516    #[test]
517    fn test_render_integer() {
518        let renderer = PerlVariableRenderer::new();
519        let value = PerlValue::Integer(42);
520        let rendered = renderer.render("$n", &value);
521
522        assert_eq!(rendered.name, "$n");
523        assert_eq!(rendered.value, "42");
524        assert_eq!(rendered.type_name, Some("SCALAR".to_string()));
525    }
526
527    #[test]
528    fn test_render_array() {
529        let renderer = PerlVariableRenderer::new();
530        let value = PerlValue::Array(vec![
531            PerlValue::Integer(1),
532            PerlValue::Integer(2),
533            PerlValue::Integer(3),
534        ]);
535        let rendered = renderer.render("@arr", &value);
536
537        assert_eq!(rendered.name, "@arr");
538        assert!(rendered.value.starts_with('['));
539        assert_eq!(rendered.type_name, Some("ARRAY".to_string()));
540        assert_eq!(rendered.indexed_variables, Some(3));
541    }
542
543    #[test]
544    fn test_render_hash() {
545        let renderer = PerlVariableRenderer::new();
546        let value = PerlValue::Hash(vec![
547            ("key1".to_string(), PerlValue::Scalar("value1".to_string())),
548            ("key2".to_string(), PerlValue::Integer(42)),
549        ]);
550        let rendered = renderer.render("%hash", &value);
551
552        assert_eq!(rendered.name, "%hash");
553        assert!(rendered.value.starts_with('{'));
554        assert_eq!(rendered.type_name, Some("HASH".to_string()));
555        assert_eq!(rendered.named_variables, Some(2));
556    }
557
558    #[test]
559    fn test_render_with_reference() {
560        let renderer = PerlVariableRenderer::new();
561        let value = PerlValue::Array(vec![PerlValue::Integer(1)]);
562        let rendered = renderer.render_with_reference("@arr", &value, 42);
563
564        assert_eq!(rendered.variables_reference, 42);
565        assert!(rendered.is_expandable());
566    }
567
568    #[test]
569    fn test_render_children_array() {
570        let renderer = PerlVariableRenderer::new();
571        let value = PerlValue::Array(vec![
572            PerlValue::Integer(10),
573            PerlValue::Integer(20),
574            PerlValue::Integer(30),
575        ]);
576        let children = renderer.render_children(&value, 0, 10);
577
578        assert_eq!(children.len(), 3);
579        assert_eq!(children[0].name, "[0]");
580        assert_eq!(children[0].value, "10");
581        assert_eq!(children[1].name, "[1]");
582        assert_eq!(children[2].name, "[2]");
583    }
584
585    #[test]
586    fn test_render_children_hash() {
587        let renderer = PerlVariableRenderer::new();
588        let value = PerlValue::Hash(vec![
589            ("foo".to_string(), PerlValue::Integer(1)),
590            ("bar".to_string(), PerlValue::Integer(2)),
591        ]);
592        let children = renderer.render_children(&value, 0, 10);
593
594        assert_eq!(children.len(), 2);
595        assert_eq!(children[0].name, "foo");
596        assert_eq!(children[1].name, "bar");
597    }
598
599    #[test]
600    fn test_render_object() {
601        let renderer = PerlVariableRenderer::new();
602        let value = PerlValue::Object {
603            class: "My::Class".to_string(),
604            value: Box::new(PerlValue::Hash(vec![(
605                "attr".to_string(),
606                PerlValue::Scalar("value".to_string()),
607            )])),
608        };
609        let rendered = renderer.render("$obj", &value);
610
611        assert_eq!(rendered.name, "$obj");
612        assert!(rendered.value.contains("My::Class"));
613        assert_eq!(rendered.type_name, Some("My::Class".to_string()));
614        assert_eq!(rendered.named_variables, Some(1));
615    }
616
617    #[test]
618    fn test_string_truncation() {
619        let renderer = PerlVariableRenderer::new().with_max_string_length(10);
620        let value = PerlValue::Scalar("this is a very long string".to_string());
621        let rendered = renderer.render("$s", &value);
622
623        assert!(rendered.value.contains("..."));
624        assert!(rendered.value.len() < 30);
625    }
626
627    #[test]
628    fn test_string_truncation_zero_max_length() {
629        let renderer = PerlVariableRenderer::new().with_max_string_length(0);
630        let value = PerlValue::Scalar("non-empty".to_string());
631        let rendered = renderer.render("$s", &value);
632
633        assert_eq!(rendered.value, "\"...\"");
634    }
635
636    #[test]
637    fn test_string_truncation_utf8_boundary_safety() {
638        let renderer = PerlVariableRenderer::new().with_max_string_length(1);
639        let value = PerlValue::Scalar("éclair".to_string());
640        let rendered = renderer.render("$s", &value);
641
642        assert_eq!(rendered.value, "\"...\"");
643    }
644
645    #[test]
646    fn test_string_escaping() {
647        let renderer = PerlVariableRenderer::new();
648        let value = PerlValue::Scalar("line1\nline2\ttab".to_string());
649        let rendered = renderer.render("$s", &value);
650
651        assert!(rendered.value.contains("\\n"));
652        assert!(rendered.value.contains("\\t"));
653    }
654
655    #[test]
656    fn test_render_undef() {
657        let renderer = PerlVariableRenderer::new();
658        let value = PerlValue::Undef;
659        let rendered = renderer.render("$x", &value);
660
661        assert_eq!(rendered.value, "undef");
662        assert_eq!(rendered.type_name, Some("undef".to_string()));
663    }
664
665    #[test]
666    fn test_render_reference() {
667        let renderer = PerlVariableRenderer::new();
668        let value = PerlValue::Reference(Box::new(PerlValue::Integer(42)));
669        let rendered = renderer.render("$ref", &value);
670
671        assert_eq!(rendered.name, "$ref");
672        assert!(rendered.value.contains("42"));
673        assert_eq!(rendered.type_name, Some("REF".to_string()));
674    }
675
676    #[test]
677    fn test_render_code() {
678        let renderer = PerlVariableRenderer::new();
679        let value = PerlValue::Code { name: Some("my_sub".to_string()) };
680        let rendered = renderer.render("$code", &value);
681
682        assert!(rendered.value.contains("my_sub"));
683        assert_eq!(rendered.type_name, Some("CODE".to_string()));
684    }
685
686    // ---------------------------------------------------------------
687    // Circular reference detection and large structure handling tests
688    // ---------------------------------------------------------------
689
690    /// Simulates a self-referential hash like `{ a => \$self }`.
691    ///
692    /// Since `PerlValue` uses `Box` (no `Rc`), true cycles cannot exist at
693    /// the type level.  The Perl debugger would itself truncate such cycles,
694    /// so we model the leaf as a `Truncated` variant representing the
695    /// back-reference the debugger would emit.
696    #[test]
697    fn test_render_self_referential_hash() {
698        let renderer = PerlVariableRenderer::new();
699
700        // Simulate: my $self = { a => \$self }
701        // The debugger would show the back-reference as a truncated/circular marker.
702        let circular_marker =
703            PerlValue::Truncated { summary: "HASH(0x...circular)".to_string(), total_count: None };
704        let value = PerlValue::Hash(vec![(
705            "a".to_string(),
706            PerlValue::Reference(Box::new(circular_marker)),
707        )]);
708
709        let rendered = renderer.render("$self", &value);
710
711        assert_eq!(rendered.name, "$self");
712        assert_eq!(rendered.type_name, Some("HASH".to_string()));
713        assert_eq!(rendered.named_variables, Some(1));
714        // The value preview should contain the circular marker, not panic or hang
715        assert!(rendered.value.contains("circular"));
716
717        // Children should also render safely
718        let children = renderer.render_children(&value, 0, 10);
719        assert_eq!(children.len(), 1);
720        assert_eq!(children[0].name, "a");
721        assert!(children[0].value.contains("circular"));
722    }
723
724    /// Deep nesting of >100 reference levels should produce bounded output.
725    ///
726    /// The renderer caps reference chain traversal at 10 levels, emitting
727    /// `REF(...)` for anything deeper.
728    #[test]
729    fn test_render_deep_nesting_over_100_levels_bounded() {
730        let renderer = PerlVariableRenderer::new();
731
732        // Build a tower of 150 nested references: \\\...\42
733        let mut value = PerlValue::Integer(42);
734        for _ in 0..150 {
735            value = PerlValue::Reference(Box::new(value));
736        }
737
738        let rendered = renderer.render("$deep", &value);
739
740        assert_eq!(rendered.name, "$deep");
741        assert_eq!(rendered.type_name, Some("REF".to_string()));
742
743        // The output must be bounded — the renderer stops at 10 backslash
744        // prefixes and then emits REF(...) for the remainder.
745        // Count backslash prefixes in the value string.
746        let backslash_prefix_count = rendered.value.chars().take_while(|&c| c == '\\').count();
747        assert!(
748            backslash_prefix_count <= 10,
749            "backslash prefix count {} should be <= 10",
750            backslash_prefix_count,
751        );
752        // The value should contain the REF(...) truncation marker
753        assert!(
754            rendered.value.contains("REF(...)"),
755            "deeply nested ref should contain REF(...), got: {}",
756            rendered.value,
757        );
758
759        // Total output length should be reasonable (not exponential)
760        assert!(
761            rendered.value.len() < 200,
762            "rendered value length {} should be < 200",
763            rendered.value.len(),
764        );
765    }
766
767    /// A reference chain of exactly 10 levels should still reach the leaf.
768    #[test]
769    fn test_render_reference_chain_at_depth_limit() {
770        let renderer = PerlVariableRenderer::new();
771
772        let mut value = PerlValue::Scalar("leaf".to_string());
773        for _ in 0..10 {
774            value = PerlValue::Reference(Box::new(value));
775        }
776
777        let rendered = renderer.render("$ref10", &value);
778        // At exactly 10, the chain traversal stops and formats the leaf.
779        // The output should contain 10 backslashes and then the leaf value.
780        assert!(rendered.value.contains("leaf") || rendered.value.contains("REF(...)"));
781        assert!(rendered.value.len() < 200);
782    }
783
784    /// Large array with >10K elements should truncate in preview.
785    #[test]
786    fn test_render_large_array_over_10k_elements_truncates() {
787        let renderer = PerlVariableRenderer::new();
788
789        let elements: Vec<PerlValue> = (0..10_001).map(PerlValue::Integer).collect();
790        let value = PerlValue::Array(elements);
791
792        let rendered = renderer.render("@big", &value);
793
794        assert_eq!(rendered.name, "@big");
795        assert_eq!(rendered.type_name, Some("ARRAY".to_string()));
796        assert_eq!(rendered.indexed_variables, Some(10_001));
797
798        // Preview should show only max_array_preview (default 3) elements
799        // plus a "... (N total)" suffix
800        assert!(
801            rendered.value.contains("10001 total"),
802            "should show total count, got: {}",
803            rendered.value,
804        );
805        assert!(rendered.value.starts_with('['));
806        assert!(rendered.value.ends_with(']'));
807
808        // Preview string should be bounded — not contain all 10K values
809        assert!(
810            rendered.value.len() < 500,
811            "preview length {} should be < 500",
812            rendered.value.len(),
813        );
814    }
815
816    /// `render_children` paginates large arrays correctly.
817    #[test]
818    fn test_render_children_large_array_pagination() {
819        let renderer = PerlVariableRenderer::new();
820
821        let elements: Vec<PerlValue> = (0..10_001).map(PerlValue::Integer).collect();
822        let value = PerlValue::Array(elements);
823
824        // Request a window of 100 starting at index 5000
825        let children = renderer.render_children(&value, 5000, 100);
826        assert_eq!(children.len(), 100);
827        assert_eq!(children[0].name, "[5000]");
828        assert_eq!(children[0].value, "5000");
829        assert_eq!(children[99].name, "[5099]");
830        assert_eq!(children[99].value, "5099");
831
832        // Request past the end — should return only what's available
833        let tail = renderer.render_children(&value, 10_000, 100);
834        assert_eq!(tail.len(), 1);
835        assert_eq!(tail[0].name, "[10000]");
836    }
837
838    /// Large hash with >5K pairs should truncate in preview.
839    #[test]
840    fn test_render_large_hash_over_5k_pairs_truncates() {
841        let renderer = PerlVariableRenderer::new();
842
843        let pairs: Vec<(String, PerlValue)> =
844            (0..5_001).map(|i| (format!("key_{}", i), PerlValue::Integer(i))).collect();
845        let value = PerlValue::Hash(pairs);
846
847        let rendered = renderer.render("%big", &value);
848
849        assert_eq!(rendered.name, "%big");
850        assert_eq!(rendered.type_name, Some("HASH".to_string()));
851        assert_eq!(rendered.named_variables, Some(5_001));
852
853        // Preview should show only max_hash_preview (default 3) pairs
854        // plus a "... (N keys)" suffix
855        assert!(
856            rendered.value.contains("5001 keys"),
857            "should show key count, got: {}",
858            rendered.value,
859        );
860        assert!(rendered.value.starts_with('{'));
861        assert!(rendered.value.ends_with('}'));
862
863        // Preview string should be bounded
864        assert!(
865            rendered.value.len() < 500,
866            "preview length {} should be < 500",
867            rendered.value.len(),
868        );
869    }
870
871    /// `render_children` paginates large hashes correctly.
872    #[test]
873    fn test_render_children_large_hash_pagination() {
874        let renderer = PerlVariableRenderer::new();
875
876        let pairs: Vec<(String, PerlValue)> =
877            (0..5_001).map(|i| (format!("key_{}", i), PerlValue::Integer(i))).collect();
878        let value = PerlValue::Hash(pairs);
879
880        // Request a window of 50 starting at index 2500
881        let children = renderer.render_children(&value, 2500, 50);
882        assert_eq!(children.len(), 50);
883        assert_eq!(children[0].name, "key_2500");
884        assert_eq!(children[0].value, "2500");
885
886        // Request past the end
887        let tail = renderer.render_children(&value, 5000, 100);
888        assert_eq!(tail.len(), 1);
889        assert_eq!(tail[0].name, "key_5000");
890    }
891
892    /// Blessed object backed by a hash — the standard Perl OO pattern.
893    #[test]
894    fn test_render_blessed_object_hash_based() {
895        let renderer = PerlVariableRenderer::new();
896
897        let value = PerlValue::Object {
898            class: "HTTP::Response".to_string(),
899            value: Box::new(PerlValue::Hash(vec![
900                ("_rc".to_string(), PerlValue::Integer(200)),
901                ("_content".to_string(), PerlValue::Scalar("OK".to_string())),
902                (
903                    "_headers".to_string(),
904                    PerlValue::Hash(vec![(
905                        "Content-Type".to_string(),
906                        PerlValue::Scalar("text/html".to_string()),
907                    )]),
908                ),
909            ])),
910        };
911
912        let rendered = renderer.render("$resp", &value);
913
914        assert_eq!(rendered.name, "$resp");
915        assert_eq!(rendered.type_name, Some("HTTP::Response".to_string()));
916        assert_eq!(rendered.named_variables, Some(3));
917        assert!(rendered.value.contains("HTTP::Response"));
918
919        // Children should be the hash keys
920        let children = renderer.render_children(&value, 0, 10);
921        assert_eq!(children.len(), 3);
922        assert_eq!(children[0].name, "_rc");
923        assert_eq!(children[0].value, "200");
924        assert_eq!(children[1].name, "_content");
925        assert!(children[1].value.contains("OK"));
926    }
927
928    /// Blessed object backed by an array (inside-out objects).
929    #[test]
930    fn test_render_blessed_object_array_based() {
931        let renderer = PerlVariableRenderer::new();
932
933        let value = PerlValue::Object {
934            class: "My::InsideOut".to_string(),
935            value: Box::new(PerlValue::Array(vec![
936                PerlValue::Scalar("field_a".to_string()),
937                PerlValue::Integer(99),
938            ])),
939        };
940
941        let rendered = renderer.render("$io", &value);
942
943        assert_eq!(rendered.type_name, Some("My::InsideOut".to_string()));
944        assert_eq!(rendered.indexed_variables, Some(2));
945        assert!(rendered.value.contains("My::InsideOut"));
946
947        let children = renderer.render_children(&value, 0, 10);
948        assert_eq!(children.len(), 2);
949        assert_eq!(children[0].name, "[0]");
950        assert_eq!(children[1].name, "[1]");
951    }
952
953    /// Blessed object backed by a scalar (e.g., URI, overloaded stringification).
954    #[test]
955    fn test_render_blessed_object_scalar_based() {
956        let renderer = PerlVariableRenderer::new();
957
958        // URI objects are blessed scalar refs: bless \("https://example.com"), "URI"
959        let value = PerlValue::Object {
960            class: "URI".to_string(),
961            value: Box::new(PerlValue::Scalar("https://example.com".to_string())),
962        };
963
964        let rendered = renderer.render("$uri", &value);
965
966        assert_eq!(rendered.type_name, Some("URI".to_string()));
967        // Scalar-backed objects don't expose named or indexed children
968        assert_eq!(rendered.named_variables, None);
969        assert_eq!(rendered.indexed_variables, None);
970        assert_eq!(rendered.value, "URI = SCALAR(...)");
971    }
972
973    /// Blessed object with deeply nested class name (Perl namespaces).
974    #[test]
975    fn test_render_blessed_object_deep_namespace() {
976        let renderer = PerlVariableRenderer::new();
977
978        let value = PerlValue::Object {
979            class: "Very::Deep::Nested::Package::Name".to_string(),
980            value: Box::new(PerlValue::Hash(vec![])),
981        };
982
983        let rendered = renderer.render("$obj", &value);
984
985        assert_eq!(rendered.type_name, Some("Very::Deep::Nested::Package::Name".to_string()));
986        assert!(rendered.value.contains("Very::Deep::Nested::Package::Name"));
987        assert_eq!(rendered.named_variables, Some(0));
988    }
989
990    /// Blessed object with `render_with_reference` gets a reference ID
991    /// and is expandable.
992    #[test]
993    fn test_render_blessed_object_with_reference() {
994        let renderer = PerlVariableRenderer::new();
995
996        let value = PerlValue::Object {
997            class: "DBI::db".to_string(),
998            value: Box::new(PerlValue::Hash(vec![(
999                "Driver".to_string(),
1000                PerlValue::Scalar("SQLite".to_string()),
1001            )])),
1002        };
1003
1004        let rendered = renderer.render_with_reference("$dbh", &value, 99);
1005
1006        assert_eq!(rendered.variables_reference, 99);
1007        assert!(rendered.is_expandable());
1008        assert_eq!(rendered.type_name, Some("DBI::db".to_string()));
1009    }
1010
1011    /// Simulates a hash whose value is a reference back to a parent —
1012    /// the pattern `{ parent => \%grandparent, child => \%self }` where
1013    /// the debugger truncates the cycle.
1014    #[test]
1015    fn test_render_hash_with_multiple_back_references() {
1016        let renderer = PerlVariableRenderer::new();
1017
1018        let grandparent_marker = PerlValue::Truncated {
1019            summary: "HASH(0xaaa...circular)".to_string(),
1020            total_count: Some(5),
1021        };
1022        let self_marker = PerlValue::Truncated {
1023            summary: "HASH(0xbbb...circular)".to_string(),
1024            total_count: Some(3),
1025        };
1026
1027        let value = PerlValue::Hash(vec![
1028            ("parent".to_string(), PerlValue::Reference(Box::new(grandparent_marker))),
1029            ("child".to_string(), PerlValue::Reference(Box::new(self_marker))),
1030            ("name".to_string(), PerlValue::Scalar("node".to_string())),
1031        ]);
1032
1033        let rendered = renderer.render("$node", &value);
1034
1035        assert_eq!(rendered.named_variables, Some(3));
1036        // Should not panic or produce unbounded output
1037        assert!(rendered.value.len() < 500);
1038
1039        let children = renderer.render_children(&value, 0, 10);
1040        assert_eq!(children.len(), 3);
1041        // The circular references render as REF type with truncated target
1042        assert!(children[0].value.contains("circular"));
1043        assert!(children[1].value.contains("circular"));
1044        assert!(children[2].value.contains("node"));
1045    }
1046
1047    /// Empty array and hash render as `[]` and `{}` respectively.
1048    #[test]
1049    fn test_render_empty_collections() {
1050        let renderer = PerlVariableRenderer::new();
1051
1052        let empty_arr = PerlValue::Array(vec![]);
1053        let rendered = renderer.render("@empty", &empty_arr);
1054        assert_eq!(rendered.value, "[]");
1055        assert_eq!(rendered.indexed_variables, Some(0));
1056
1057        let empty_hash = PerlValue::Hash(vec![]);
1058        let rendered = renderer.render("%empty", &empty_hash);
1059        assert_eq!(rendered.value, "{}");
1060        assert_eq!(rendered.named_variables, Some(0));
1061    }
1062
1063    /// Verifies that configuring lower preview limits still works for
1064    /// large structures.
1065    #[test]
1066    fn test_render_large_array_with_custom_preview_limit() {
1067        let renderer =
1068            PerlVariableRenderer::new().with_max_array_preview(1).with_max_hash_preview(1);
1069
1070        let elements: Vec<PerlValue> = (0..100).map(PerlValue::Integer).collect();
1071        let value = PerlValue::Array(elements);
1072
1073        let rendered = renderer.render("@arr", &value);
1074        // With preview=1, should show one element then "... (100 total)"
1075        assert!(rendered.value.contains("100 total"));
1076        // Only the first element should appear before the ellipsis
1077        assert!(rendered.value.starts_with("[0"));
1078    }
1079
1080    /// Verifies that configuring lower preview limits works for large hashes.
1081    #[test]
1082    fn test_render_large_hash_with_custom_preview_limit() {
1083        let renderer = PerlVariableRenderer::new().with_max_hash_preview(1);
1084
1085        let pairs: Vec<(String, PerlValue)> =
1086            (0..100).map(|i| (format!("k{}", i), PerlValue::Integer(i))).collect();
1087        let value = PerlValue::Hash(pairs);
1088
1089        let rendered = renderer.render("%h", &value);
1090        assert!(rendered.value.contains("100 keys"));
1091        assert!(rendered.value.starts_with('{'));
1092    }
1093}