llmcc_core/
printer.rs

1//! TOOD: use impl fmt::Debug
2use crate::context::CompileUnit;
3use crate::graph_builder::{BasicBlock, BlockId};
4use crate::ir::{HirId, HirNode};
5use std::fmt;
6
7/// Output format for rendering
8///
9/// Controls how the tree structure is rendered to string output.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
11pub enum PrintFormat {
12    /// Standard tree format with indentation and nested structure
13    /// ```text
14    /// (root
15    ///   (child1)
16    ///   (child2)
17    /// )
18    /// ```
19    #[default]
20    Tree,
21
22    /// Compact format with minimal whitespace
23    /// ```text
24    /// (root (child1) (child2))
25    /// ```
26    Compact,
27
28    /// One node per line, minimal formatting
29    /// ```text
30    /// root
31    /// child1
32    /// child2
33    /// ```
34    Flat,
35}
36
37impl fmt::Display for PrintFormat {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        match self {
40            PrintFormat::Tree => write!(f, "tree"),
41            PrintFormat::Compact => write!(f, "compact"),
42            PrintFormat::Flat => write!(f, "flat"),
43        }
44    }
45}
46
47impl std::str::FromStr for PrintFormat {
48    type Err = String;
49
50    fn from_str(s: &str) -> Result<Self, Self::Err> {
51        match s.to_lowercase().as_str() {
52            "tree" => Ok(PrintFormat::Tree),
53            "compact" => Ok(PrintFormat::Compact),
54            "flat" => Ok(PrintFormat::Flat),
55            other => Err(format!(
56                "Unknown format: {other}. Use 'tree', 'compact', or 'flat'"
57            )),
58        }
59    }
60}
61
62/// Production-ready configuration for rendering and printing
63///
64/// This struct controls all aspects of output formatting. Use the builder
65/// methods to customize behavior, or use preset configurations via
66/// [`PrintConfig::minimal()`] and [`PrintConfig::verbose()`].
67#[derive(Debug, Clone)]
68pub struct PrintConfig {
69    /// Output format (tree, compact, flat)
70    pub format: PrintFormat,
71
72    /// Include source code snippets in output
73    pub include_snippets: bool,
74
75    /// Include line number information [start-end]
76    pub include_line_info: bool,
77
78    /// Column width for snippet alignment (for tree format)
79    pub snippet_col_width: usize,
80
81    /// Maximum snippet length before truncation with "..."
82    pub snippet_max_length: usize,
83
84    /// Maximum nesting depth (prevents stack overflow on deeply nested input)
85    pub max_depth: usize,
86
87    /// Indentation width in spaces per nesting level
88    pub indent_width: usize,
89
90    /// Include unique node IDs in output
91    pub include_node_ids: bool,
92
93    /// Include field names (for tree-sitter nodes)
94    pub include_field_names: bool,
95
96    /// Truncate long lines to this width (0 = no truncation)
97    pub line_width_limit: usize,
98}
99
100impl Default for PrintConfig {
101    fn default() -> Self {
102        PrintConfig {
103            format: PrintFormat::Tree,
104            include_snippets: true,
105            include_line_info: true,
106            snippet_col_width: 60,
107            snippet_max_length: 60,
108            max_depth: 1000,
109            indent_width: 2,
110            include_node_ids: false,
111            include_field_names: false,
112            line_width_limit: 0,
113        }
114    }
115}
116
117impl PrintConfig {
118    /// Create a new configuration with default settings
119    pub fn new() -> Self {
120        Self::default()
121    }
122
123    // Builder methods
124    // ====================================================================
125
126    /// Set output format
127    pub fn with_format(mut self, format: PrintFormat) -> Self {
128        self.format = format;
129        self
130    }
131
132    /// Enable/disable snippets
133    pub fn with_snippets(mut self, enabled: bool) -> Self {
134        self.include_snippets = enabled;
135        self
136    }
137
138    /// Enable/disable line information
139    pub fn with_line_info(mut self, enabled: bool) -> Self {
140        self.include_line_info = enabled;
141        self
142    }
143
144    /// Set snippet display width
145    pub fn with_snippet_width(mut self, width: usize) -> Self {
146        self.snippet_col_width = width;
147        self
148    }
149
150    /// Set maximum snippet length before truncation
151    pub fn with_snippet_max_length(mut self, length: usize) -> Self {
152        self.snippet_max_length = length;
153        self
154    }
155
156    /// Set maximum nesting depth
157    pub fn with_max_depth(mut self, depth: usize) -> Self {
158        self.max_depth = depth;
159        self
160    }
161
162    /// Set indentation width
163    pub fn with_indent_width(mut self, width: usize) -> Self {
164        self.indent_width = width;
165        self
166    }
167
168    /// Enable/disable node IDs
169    pub fn with_node_ids(mut self, enabled: bool) -> Self {
170        self.include_node_ids = enabled;
171        self
172    }
173
174    /// Enable/disable field names
175    pub fn with_field_names(mut self, enabled: bool) -> Self {
176        self.include_field_names = enabled;
177        self
178    }
179
180    /// Set line width limit
181    pub fn with_line_width_limit(mut self, width: usize) -> Self {
182        self.line_width_limit = width;
183        self
184    }
185
186    // Preset configurations
187    // ====================================================================
188
189    /// Minimal configuration (fastest rendering)
190    ///
191    /// Disables snippets, line info, and node IDs for maximum speed
192    pub fn minimal() -> Self {
193        PrintConfig {
194            format: PrintFormat::Flat,
195            include_snippets: false,
196            include_line_info: false,
197            include_node_ids: false,
198            include_field_names: false,
199            ..Default::default()
200        }
201    }
202
203    /// Verbose configuration (maximum detail)
204    ///
205    /// Enables all features for comprehensive output
206    pub fn verbose() -> Self {
207        PrintConfig {
208            format: PrintFormat::Tree,
209            include_snippets: true,
210            include_line_info: true,
211            include_node_ids: true,
212            include_field_names: true,
213            snippet_col_width: 80,
214            snippet_max_length: 100,
215            ..Default::default()
216        }
217    }
218
219    /// Compact configuration (balanced)
220    ///
221    /// Good for interactive debugging
222    pub fn compact() -> Self {
223        PrintConfig {
224            format: PrintFormat::Compact,
225            include_snippets: true,
226            include_line_info: true,
227            include_node_ids: false,
228            ..Default::default()
229        }
230    }
231
232    /// Validate configuration for logical consistency
233    pub fn validate(&self) -> Result<(), String> {
234        if self.max_depth == 0 {
235            return Err("max_depth must be > 0".to_string());
236        }
237        if self.indent_width == 0 {
238            return Err("indent_width must be > 0".to_string());
239        }
240        if self.snippet_col_width == 0 {
241            return Err("snippet_col_width must be > 0".to_string());
242        }
243        Ok(())
244    }
245}
246
247/// Error type for rendering operations
248#[derive(Debug, Clone)]
249pub struct RenderError {
250    pub message: String,
251}
252
253impl fmt::Display for RenderError {
254    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255        write!(f, "Render error: {}", self.message)
256    }
257}
258
259impl std::error::Error for RenderError {}
260
261impl From<String> for RenderError {
262    fn from(message: String) -> Self {
263        RenderError { message }
264    }
265}
266
267impl From<&str> for RenderError {
268    fn from(message: &str) -> Self {
269        RenderError {
270            message: message.to_string(),
271        }
272    }
273}
274
275impl RenderError {
276    /// Create a new render error
277    pub fn new(message: impl Into<String>) -> Self {
278        RenderError {
279            message: message.into(),
280        }
281    }
282
283    /// Error for exceeding maximum depth
284    pub fn max_depth_exceeded(depth: usize, max: usize) -> Self {
285        RenderError::new(format!("Maximum depth {depth} exceeded (limit: {max})"))
286    }
287
288    /// Error for invalid configuration
289    pub fn config_invalid(reason: impl Into<String>) -> Self {
290        RenderError::new(format!("Invalid configuration: {}", reason.into()))
291    }
292}
293
294pub type RenderResult<T> = Result<T, RenderError>;
295
296// ============================================================================
297// Internal Render Node Structure
298// ============================================================================
299
300/// Internal representation of a node for rendering
301#[derive(Debug, Clone)]
302struct RenderNode {
303    label: String,
304    line_info: Option<String>,
305    snippet: Option<String>,
306    children: Vec<RenderNode>,
307    node_id: Option<String>,
308    /// Suffix to add after the closing parenthesis (e.g., "@type i32")
309    suffix: Option<String>,
310}
311
312impl RenderNode {
313    fn new(
314        label: String,
315        line_info: Option<String>,
316        snippet: Option<String>,
317        children: Vec<RenderNode>,
318        node_id: Option<String>,
319    ) -> Self {
320        Self {
321            label,
322            line_info,
323            snippet,
324            children,
325            node_id,
326            suffix: None,
327        }
328    }
329
330    fn with_suffix(mut self, suffix: Option<String>) -> Self {
331        self.suffix = suffix;
332        self
333    }
334}
335
336// ============================================================================
337// Public API Functions
338// ============================================================================
339
340/// Render HIR with default configuration
341pub fn render_llmcc_ir(root: HirId, unit: CompileUnit<'_>) -> RenderResult<(String, String)> {
342    render_llmcc_ir_with_config(root, unit, &PrintConfig::default())
343}
344
345/// Render HIR with custom configuration
346pub fn render_llmcc_ir_with_config(
347    root: HirId,
348    unit: CompileUnit<'_>,
349    config: &PrintConfig,
350) -> RenderResult<(String, String)> {
351    config.validate()?;
352
353    let hir_root = unit.hir_node(root);
354
355    // Build AST render tree from parse tree if available
356    let ast_render = if let Some(parse_tree) = unit.parse_tree() {
357        if let Some(root_node) = parse_tree.root_node() {
358            build_ast_render(&*root_node, unit, config, 0)?
359        } else {
360            RenderNode::new(
361                "No AST root node found".to_string(),
362                None,
363                None,
364                vec![],
365                None,
366            )
367        }
368    } else {
369        RenderNode::new(
370            "Parse tree not available for this compilation unit".to_string(),
371            None,
372            None,
373            vec![],
374            None,
375        )
376    };
377
378    let hir_render = build_hir_render(&hir_root, unit, config, 0)?;
379    let ast = render_lines(&ast_render, config)?;
380    let hir = render_lines(&hir_render, config)?;
381
382    Ok((ast, hir))
383}
384
385/// Print HIR to stdout
386pub fn print_llmcc_ir(unit: CompileUnit<'_>) -> RenderResult<()> {
387    print_llmcc_ir_with_config(unit, &PrintConfig::default())
388}
389
390/// Print HIR to stdout with custom configuration
391pub fn print_llmcc_ir_with_config(unit: CompileUnit<'_>, config: &PrintConfig) -> RenderResult<()> {
392    let root = unit
393        .file_root_id()
394        .ok_or_else(|| RenderError::new("No HIR root node found"))?;
395
396    let (ast, _hir) = render_llmcc_ir_with_config(root, unit, config)?;
397    println!("{ast}\n");
398    // println!("{}\n", hir);
399    Ok(())
400}
401
402/// Render control flow graph with custom configuration
403pub fn render_llmcc_graph_with_config(
404    root: BlockId,
405    unit: CompileUnit<'_>,
406    config: &PrintConfig,
407) -> RenderResult<String> {
408    config.validate()?;
409
410    let block = unit.bb(root);
411    let render = build_block_render(&block, unit, config, 0)?;
412    render_lines(&render, config)
413}
414
415/// Print control flow graph to stdout
416pub fn print_llmcc_graph(root: BlockId, unit: CompileUnit<'_>) -> RenderResult<()> {
417    print_llmcc_graph_with_config(root, unit, &PrintConfig::default())
418}
419
420/// Print control flow graph to stdout with custom configuration
421pub fn print_llmcc_graph_with_config(
422    root: BlockId,
423    unit: CompileUnit<'_>,
424    config: &PrintConfig,
425) -> RenderResult<()> {
426    let graph = render_llmcc_graph_with_config(root, unit, config)?;
427    println!("{graph}\n");
428    Ok(())
429}
430
431// ============================================================================
432// Internal Rendering Functions
433// ============================================================================
434
435/// Build render tree for AST node (from parse tree)
436fn build_ast_render(
437    node: &(dyn crate::lang_def::ParseNode + '_),
438    unit: CompileUnit<'_>,
439    config: &PrintConfig,
440    depth: usize,
441) -> RenderResult<RenderNode> {
442    build_ast_render_with(node, None, 0, unit, config, depth)
443}
444
445/// Build render tree for AST node with parent context for field names
446fn build_ast_render_with(
447    node: &(dyn crate::lang_def::ParseNode + '_),
448    parent: Option<&(dyn crate::lang_def::ParseNode + '_)>,
449    child_index: usize,
450    unit: CompileUnit<'_>,
451    config: &PrintConfig,
452    depth: usize,
453) -> RenderResult<RenderNode> {
454    // Check depth limit
455    if depth > config.max_depth {
456        return Err(RenderError::max_depth_exceeded(depth, config.max_depth));
457    }
458
459    // Get field name if available from parent
460    let field_name: Option<&str> = parent.and_then(|p| p.child_field_name(child_index));
461
462    // Use the trait method to format the label
463    let label = node.format_node_label(field_name);
464
465    // Line range info
466    let line_info = Some(format!("[{}-{}]", node.start_byte(), node.end_byte()));
467
468    // Extract snippet from source
469    let snippet = if config.include_snippets {
470        snippet_from_ctx(&unit, node.start_byte(), node.end_byte(), config)
471    } else {
472        None
473    };
474
475    // Collect children
476    let mut children = Vec::new();
477    for i in 0..node.child_count() {
478        if let Some(child) = node.child(i)
479            && let Ok(render) =
480                build_ast_render_with(&*child, Some(node), i, unit, config, depth + 1)
481        {
482            children.push(render);
483        }
484    }
485
486    Ok(RenderNode::new(label, line_info, snippet, children, None))
487}
488
489/// Build render tree for HIR node
490fn build_hir_render<'tcx>(
491    node: &HirNode<'tcx>,
492    unit: CompileUnit<'tcx>,
493    config: &PrintConfig,
494    depth: usize,
495) -> RenderResult<RenderNode> {
496    // Check depth limit
497    if depth > config.max_depth {
498        return Err(RenderError::max_depth_exceeded(depth, config.max_depth));
499    }
500
501    let mut label = node.format(unit);
502
503    // Add identifier name info for Ident nodes
504    if let crate::ir::HirNode::Ident(ident) = node {
505        label.push_str(&format!(" = \"{}\"", ident.name));
506    }
507
508    let line_info = if config.include_line_info {
509        Some(format!(
510            "[{}-{}]",
511            get_line_from_byte(&unit, node.start_byte()),
512            get_line_from_byte(&unit, node.end_byte())
513        ))
514    } else {
515        None
516    };
517
518    let snippet = if config.include_snippets {
519        snippet_from_ctx(&unit, node.start_byte(), node.end_byte(), config)
520    } else {
521        None
522    };
523
524    let children = node
525        .child_ids()
526        .iter()
527        .map(|id| {
528            let child = unit.hir_node(*id);
529            build_hir_render(&child, unit, config, depth + 1)
530        })
531        .collect::<RenderResult<Vec<_>>>()?;
532
533    Ok(RenderNode::new(label, line_info, snippet, children, None))
534}
535
536/// Build render tree for control flow block
537fn build_block_render<'tcx>(
538    block: &BasicBlock<'tcx>,
539    unit: CompileUnit<'tcx>,
540    config: &PrintConfig,
541    depth: usize,
542) -> RenderResult<RenderNode> {
543    // Check depth limit
544    if depth > config.max_depth {
545        return Err(RenderError::max_depth_exceeded(depth, config.max_depth));
546    }
547
548    let label = block.format_block(unit);
549    let suffix = block.format_suffix();
550
551    let line_info = if config.include_line_info {
552        block.opt_node().map(|node| {
553            format!(
554                "[{}-{}]",
555                get_line_from_byte(&unit, node.start_byte()),
556                get_line_from_byte(&unit, node.end_byte())
557            )
558        })
559    } else {
560        None
561    };
562
563    let snippet = if config.include_snippets {
564        block
565            .opt_node()
566            .and_then(|n| snippet_from_ctx(&unit, n.start_byte(), n.end_byte(), config))
567    } else {
568        None
569    };
570
571    let children = block
572        .children()
573        .iter()
574        .map(|id| {
575            let child = unit.bb(*id);
576            build_block_render(&child, unit, config, depth + 1)
577        })
578        .collect::<RenderResult<Vec<_>>>()?;
579
580    Ok(RenderNode::new(label, line_info, snippet, children, None).with_suffix(suffix))
581}
582
583/// Render node tree to string based on configuration
584fn render_lines(node: &RenderNode, config: &PrintConfig) -> RenderResult<String> {
585    let mut lines = Vec::new();
586    render_node_with_format(node, 0, &mut lines, config)?;
587    Ok(lines.join("\n"))
588}
589
590/// Render individual node with format-specific handling
591fn render_node_with_format(
592    node: &RenderNode,
593    depth: usize,
594    out: &mut Vec<String>,
595    config: &PrintConfig,
596) -> RenderResult<()> {
597    match config.format {
598        PrintFormat::Tree => render_node_tree(node, depth, out, config),
599        PrintFormat::Compact => render_node_compact(node, out, config),
600        PrintFormat::Flat => render_node_flat(node, out, config),
601    }
602}
603
604/// Render in tree format (indented with nesting)
605fn render_node_tree(
606    node: &RenderNode,
607    depth: usize,
608    out: &mut Vec<String>,
609    config: &PrintConfig,
610) -> RenderResult<()> {
611    let indent = " ".repeat(depth * config.indent_width);
612    let mut line = format!("{indent}(");
613
614    // Add label
615    line.push_str(&node.label);
616
617    // Add node ID if configured
618    if config.include_node_ids
619        && let Some(id) = &node.node_id
620    {
621        line.push_str(&format!(" #{id}"));
622    }
623
624    // Add line information
625    if let Some(line_info) = &node.line_info {
626        line.push_str(&format!(" {line_info}"));
627    }
628
629    // Handle children
630    if node.children.is_empty() {
631        line.push(')');
632        // Add suffix after closing paren (e.g., "@type i32")
633        if let Some(suffix) = &node.suffix {
634            line.push_str(&format!(" {suffix}"));
635        }
636        // Align snippet to column and add inline with pipes
637        if let Some(snippet) = &node.snippet {
638            // Pad to column width for alignment
639            let padding = config.snippet_col_width.saturating_sub(line.len());
640            if padding > 0 {
641                line.push_str(&" ".repeat(padding));
642            } else {
643                line.push(' ');
644            }
645            line.push_str(&format!("|{snippet}|"));
646        }
647        out.push(line);
648    } else {
649        // Align snippet to column and add inline with pipes (for nodes with children)
650        if let Some(snippet) = &node.snippet {
651            let padding = config.snippet_col_width.saturating_sub(line.len());
652            if padding > 0 {
653                line.push_str(&" ".repeat(padding));
654            } else {
655                line.push(' ');
656            }
657            line.push_str(&format!("|{snippet}|"));
658        }
659        out.push(line);
660        for child in &node.children {
661            render_node_tree(child, depth + 1, out, config)?;
662        }
663        out.push(format!("{indent})"));
664    }
665
666    Ok(())
667}
668
669/// Render in compact format
670fn render_node_compact(
671    node: &RenderNode,
672    out: &mut Vec<String>,
673    config: &PrintConfig,
674) -> RenderResult<()> {
675    let mut line = format!("({})", node.label);
676
677    if config.include_line_info
678        && let Some(info) = &node.line_info
679    {
680        line.push_str(&format!(" {info}"));
681    }
682
683    for child in &node.children {
684        let mut child_line = format!("({})", child.label);
685        if config.include_line_info && child.line_info.is_some() {
686            child_line.push_str(&format!(" {}", child.line_info.as_ref().unwrap()));
687        }
688        line.push_str(&format!(" {child_line}"));
689    }
690
691    out.push(line);
692    Ok(())
693}
694
695/// Render in flat format (one node per line)
696fn render_node_flat(
697    node: &RenderNode,
698    out: &mut Vec<String>,
699    config: &PrintConfig,
700) -> RenderResult<()> {
701    let mut line = node.label.clone();
702
703    if config.include_line_info
704        && let Some(info) = &node.line_info
705    {
706        line.push_str(&format!(" {info}"));
707    }
708
709    out.push(line);
710
711    for child in &node.children {
712        render_node_flat(child, out, config)?;
713    }
714
715    Ok(())
716}
717
718// ============================================================================
719// Utility Functions
720// ============================================================================
721
722/// Extract and format source code snippet
723fn snippet_from_ctx(
724    unit: &CompileUnit<'_>,
725    start: usize,
726    end: usize,
727    config: &PrintConfig,
728) -> Option<String> {
729    unit.file()
730        .opt_get_text(start, end)
731        .map(|text| text.split_whitespace().collect::<Vec<_>>().join(" "))
732        .filter(|s| !s.is_empty() && s.len() <= config.snippet_max_length)
733}
734
735/// Get line number from byte position
736fn get_line_from_byte(unit: &CompileUnit<'_>, byte_pos: usize) -> usize {
737    let content = unit.file().content();
738    let text = String::from_utf8_lossy(&content[..byte_pos.min(content.len())]);
739    text.lines().count()
740}
741
742#[cfg(test)]
743mod tests {
744    use super::*;
745
746    #[test]
747    fn test_print_format_display() {
748        assert_eq!(PrintFormat::Tree.to_string(), "tree");
749        assert_eq!(PrintFormat::Compact.to_string(), "compact");
750        assert_eq!(PrintFormat::Flat.to_string(), "flat");
751    }
752
753    #[test]
754    fn test_print_format_from_str() {
755        assert_eq!("tree".parse::<PrintFormat>().unwrap(), PrintFormat::Tree);
756        assert_eq!(
757            "compact".parse::<PrintFormat>().unwrap(),
758            PrintFormat::Compact
759        );
760        assert_eq!("flat".parse::<PrintFormat>().unwrap(), PrintFormat::Flat);
761        assert!("invalid".parse::<PrintFormat>().is_err());
762    }
763
764    #[test]
765    fn test_print_config_default() {
766        let config = PrintConfig::default();
767        assert_eq!(config.format, PrintFormat::Tree);
768        assert!(config.include_snippets);
769        assert!(config.include_line_info);
770        assert_eq!(config.max_depth, 1000);
771    }
772
773    #[test]
774    fn test_print_config_minimal() {
775        let config = PrintConfig::minimal();
776        assert_eq!(config.format, PrintFormat::Flat);
777        assert!(!config.include_snippets);
778        assert!(!config.include_line_info);
779        assert!(!config.include_node_ids);
780    }
781
782    #[test]
783    fn test_print_config_verbose() {
784        let config = PrintConfig::verbose();
785        assert_eq!(config.format, PrintFormat::Tree);
786        assert!(config.include_snippets);
787        assert!(config.include_line_info);
788        assert!(config.include_node_ids);
789    }
790
791    #[test]
792    fn test_print_config_builder() {
793        let config = PrintConfig::new()
794            .with_format(PrintFormat::Flat)
795            .with_snippets(false)
796            .with_max_depth(10)
797            .with_indent_width(4);
798
799        assert_eq!(config.format, PrintFormat::Flat);
800        assert!(!config.include_snippets);
801        assert_eq!(config.max_depth, 10);
802        assert_eq!(config.indent_width, 4);
803    }
804
805    #[test]
806    fn test_print_config_validation() {
807        let bad_config = PrintConfig {
808            max_depth: 0,
809            ..Default::default()
810        };
811        assert!(bad_config.validate().is_err());
812
813        let bad_config = PrintConfig {
814            indent_width: 0,
815            ..Default::default()
816        };
817        assert!(bad_config.validate().is_err());
818
819        let good_config = PrintConfig::default();
820        assert!(good_config.validate().is_ok());
821    }
822
823    #[test]
824    fn test_render_error_creation() {
825        let err = RenderError::new("test error");
826        assert_eq!(err.message, "test error");
827
828        let err = RenderError::max_depth_exceeded(100, 50);
829        assert!(err.message.contains("100"));
830        assert!(err.message.contains("50"));
831    }
832
833    #[test]
834    fn test_render_node_creation() {
835        let node = RenderNode::new("test".to_string(), None, None, vec![], None);
836        assert_eq!(node.label, "test");
837        assert_eq!(node.children.len(), 0);
838    }
839}