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 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 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 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 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
192fn 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}