llmcc_core/
printer.rs

1use crate::context::CompileUnit;
2use crate::graph_builder::{BasicBlock, BlockId};
3use crate::ir::{HirId, HirNode};
4use tree_sitter::Node;
5
6const SNIPPET_COL: usize = 60;
7const TRUNCATE_COL: usize = 60;
8
9#[derive(Debug, Clone)]
10struct RenderNode {
11    label: String,
12    line_info: Option<String>,
13    snippet: Option<String>,
14    children: Vec<RenderNode>,
15}
16
17impl RenderNode {
18    fn new(
19        label: String,
20        line_info: Option<String>,
21        snippet: Option<String>,
22        children: Vec<RenderNode>,
23    ) -> Self {
24        Self {
25            label,
26            line_info,
27            snippet,
28            children,
29        }
30    }
31}
32
33pub fn render_llmcc_ir<'tcx>(root: HirId, unit: CompileUnit<'tcx>) -> (String, String) {
34    let hir_root = unit.hir_node(root);
35    let ast_render = build_ast_render(hir_root.inner_ts_node(), unit);
36    let hir_render = build_hir_render(&hir_root, unit);
37    let ast = render_lines(&ast_render);
38    let hir = render_lines(&hir_render);
39    (ast, hir)
40}
41
42pub fn print_llmcc_ir<'tcx>(unit: CompileUnit<'tcx>) {
43    let root = unit.file_start_hir_id().unwrap();
44    let (ast, hir) = render_llmcc_ir(root, unit);
45    println!("{}\n", ast);
46    println!("{}\n", hir);
47}
48
49pub fn render_llmcc_graph<'tcx>(root: BlockId, unit: CompileUnit<'tcx>) -> String {
50    let block = unit.bb(root);
51    let render = build_block_render(&block, unit);
52    render_lines(&render)
53}
54
55pub fn print_llmcc_graph<'tcx>(root: BlockId, unit: CompileUnit<'tcx>) {
56    let graph = render_llmcc_graph(root, unit);
57    println!("{}\n", graph);
58}
59
60fn build_ast_render<'tcx>(node: Node<'tcx>, unit: CompileUnit<'tcx>) -> RenderNode {
61    let kind = node.kind();
62    let kind_id = node.kind_id();
63    let label = match field_info(node) {
64        Some((name, field_id)) => format!("({name}_{field_id}):{kind} [{kind_id}]"),
65        None => format!("{kind} [{kind_id}]"),
66    };
67
68    // Get line information from byte positions
69    let start_line = get_line_from_byte(&unit, node.start_byte());
70    let end_line = get_line_from_byte(&unit, node.end_byte());
71    let line_info = Some(format!("[{}-{}]", start_line, end_line));
72
73    let snippet = snippet_from_ctx(&unit, node.start_byte(), node.end_byte());
74
75    let mut cursor = node.walk();
76    let children = node
77        .children(&mut cursor)
78        .map(|child| build_ast_render(child, unit))
79        .collect();
80
81    RenderNode::new(label, line_info, snippet, children)
82}
83
84fn build_hir_render<'tcx>(node: &HirNode<'tcx>, unit: CompileUnit<'tcx>) -> RenderNode {
85    let label = node.format_node(unit);
86
87    // Get line information from byte positions
88    let start_line = get_line_from_byte(&unit, node.start_byte());
89    let end_line = get_line_from_byte(&unit, node.end_byte());
90    let line_info = Some(format!("[{}-{}]", start_line, end_line));
91
92    let snippet = snippet_from_ctx(&unit, node.start_byte(), node.end_byte());
93    let children = node
94        .children()
95        .iter()
96        .map(|id| {
97            let child = unit.hir_node(*id);
98            build_hir_render(&child, unit)
99        })
100        .collect();
101    RenderNode::new(label, line_info, snippet, children)
102}
103
104fn build_block_render<'tcx>(block: &BasicBlock<'tcx>, unit: CompileUnit<'tcx>) -> RenderNode {
105    let label = block.format_block(unit);
106
107    // Get line information from the block's node
108    let line_info = block.opt_node().map(|node| {
109        let start_line = get_line_from_byte(&unit, node.start_byte());
110        let end_line = get_line_from_byte(&unit, node.end_byte());
111        format!("[{}-{}]", start_line, end_line)
112    });
113
114    let snippet = block
115        .opt_node()
116        .and_then(|n| snippet_from_ctx(&unit, n.start_byte(), n.end_byte()));
117    let children = block
118        .children()
119        .iter()
120        .map(|id| {
121            let child = unit.bb(*id);
122            build_block_render(&child, unit)
123        })
124        .collect();
125    RenderNode::new(label, line_info, snippet, children)
126}
127
128fn render_lines(node: &RenderNode) -> String {
129    let mut lines = Vec::new();
130    render_node(node, 0, &mut lines);
131    lines.join("\n")
132}
133
134fn render_node(node: &RenderNode, depth: usize, out: &mut Vec<String>) {
135    let indent = "  ".repeat(depth);
136    let mut line = format!("{}({}", indent, node.label);
137
138    // Add line information if available
139    if let Some(line_info) = &node.line_info {
140        line.push_str(&format!(" {}", line_info));
141    }
142
143    if let Some(snippet) = &node.snippet {
144        let padded = pad_snippet(&line, snippet);
145        line.push_str(&padded);
146    }
147
148    if node.children.is_empty() {
149        line.push(')');
150        out.push(line);
151    } else {
152        out.push(line);
153        for child in &node.children {
154            render_node(child, depth + 1, out);
155        }
156        out.push(format!("{})", indent));
157    }
158}
159
160fn safe_truncate(s: &mut String, max_len: usize) {
161    if s.len() > max_len {
162        let mut new_len = max_len;
163        while !s.is_char_boundary(new_len) {
164            new_len -= 1;
165        }
166        s.truncate(new_len);
167    }
168}
169
170fn pad_snippet(line: &str, snippet: &str) -> String {
171    let mut snippet = snippet.trim().replace('\n', " ");
172    if snippet.len() > TRUNCATE_COL {
173        safe_truncate(&mut snippet, TRUNCATE_COL);
174        snippet.push_str("...");
175    }
176
177    if snippet.is_empty() {
178        return String::new();
179    }
180
181    let padding = SNIPPET_COL.saturating_sub(line.len());
182    format!("{}|{}|", " ".repeat(padding), snippet)
183}
184
185fn snippet_from_ctx(unit: &CompileUnit<'_>, start: usize, end: usize) -> Option<String> {
186    unit.file()
187        .opt_get_text(start, end)
188        .map(|text| text.split_whitespace().collect::<Vec<_>>().join(" "))
189        .filter(|s| !s.is_empty())
190}
191
192/// Get line number from byte position
193fn get_line_from_byte(unit: &CompileUnit<'_>, byte_pos: usize) -> usize {
194    let content = unit.file().content();
195    let text = String::from_utf8_lossy(&content[..byte_pos.min(content.len())]);
196    text.lines().count()
197}
198
199fn field_info(node: Node<'_>) -> Option<(String, u16)> {
200    let parent = node.parent()?;
201    let mut cursor = parent.walk();
202    if !cursor.goto_first_child() {
203        return None;
204    }
205    loop {
206        if cursor.node().id() == node.id() {
207            let name = cursor.field_name()?.to_string();
208            let id = cursor.field_id()?.get();
209            return Some((name, id));
210        }
211        if !cursor.goto_next_sibling() {
212            break;
213        }
214    }
215    None
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use pretty_assertions::assert_eq;
222
223    fn node(label: &str, snippet: Option<&str>, children: Vec<RenderNode>) -> RenderNode {
224        RenderNode::new(
225            label.to_string(),
226            None,
227            snippet.map(ToOwned::to_owned),
228            children,
229        )
230    }
231
232    #[test]
233    fn render_tree_formats_nested_structure() {
234        let tree = node(
235            "root",
236            Some("snippet text"),
237            vec![
238                node("child1", None, vec![]),
239                node(
240                    "child2",
241                    Some("long snippet for child two"),
242                    vec![node("grandchild", None, vec![])],
243                ),
244            ],
245        );
246
247        let rendered = render_lines(&tree);
248        let lines: Vec<&str> = rendered.lines().collect();
249
250        assert!(lines[0].starts_with("(root"));
251        assert!(lines[0].contains("|snippet text|"));
252        assert_eq!(lines[1].trim_start(), "(child1)");
253        assert!(lines[2].trim_start().starts_with("(child2"));
254        assert!(lines[2].contains("|long snippet for child two|"));
255        assert_eq!(lines[3].trim_start(), "(grandchild)");
256        assert_eq!(lines[4].trim(), ")");
257        assert_eq!(lines[5], ")");
258    }
259
260    #[test]
261    fn render_tree_truncates_long_snippets() {
262        let long_snippet = "a very long snippet that should be truncated for readability because it exceeds the maximum column width specified by the printer logic";
263        let tree = node("root", Some(long_snippet), vec![]);
264        let rendered = render_lines(&tree);
265        assert!(rendered.contains("a very long snippet"));
266        assert!(rendered.contains("...|"));
267        assert!(rendered.ends_with(")"));
268    }
269}