Skip to content

Commit 934177f

Browse files
tausbnCopilot
andcommitted
yeast: Add AST dumper for human-readable tree output
Produces indented text showing node kinds, named fields, and leaf content. Unnamed tokens are hidden unless inside a named field. Used by tests for readable assertions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2cf7188 commit 934177f

1 file changed

Lines changed: 181 additions & 0 deletions

File tree

shared/yeast/src/dump.rs

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
use std::fmt::Write;
2+
3+
use crate::{Ast, Node, NodeContent, CHILD_FIELD};
4+
5+
/// Options for controlling AST dump output.
6+
pub struct DumpOptions {
7+
/// Whether to include source locations in the output.
8+
pub show_locations: bool,
9+
/// Whether to include source text for leaf nodes.
10+
pub show_content: bool,
11+
}
12+
13+
impl Default for DumpOptions {
14+
fn default() -> Self {
15+
Self {
16+
show_locations: false,
17+
show_content: true,
18+
}
19+
}
20+
}
21+
22+
/// Dump a yeast AST as a human-readable indented text format.
23+
///
24+
/// Output format:
25+
/// ```text
26+
/// program
27+
/// assignment
28+
/// left:
29+
/// left_assignment_list
30+
/// identifier "x"
31+
/// identifier "y"
32+
/// right:
33+
/// call
34+
/// method:
35+
/// identifier "foo"
36+
/// ```
37+
pub fn dump_ast(ast: &Ast, root: usize, source: &str) -> String {
38+
dump_ast_with_options(ast, root, source, &DumpOptions::default())
39+
}
40+
41+
pub fn dump_ast_with_options(
42+
ast: &Ast,
43+
root: usize,
44+
source: &str,
45+
options: &DumpOptions,
46+
) -> String {
47+
let mut out = String::new();
48+
dump_node(ast, root, source, options, 0, &mut out);
49+
out
50+
}
51+
52+
fn dump_node(
53+
ast: &Ast,
54+
id: usize,
55+
source: &str,
56+
options: &DumpOptions,
57+
indent: usize,
58+
out: &mut String,
59+
) {
60+
let node = match ast.get_node(id) {
61+
Some(n) => n,
62+
None => return,
63+
};
64+
65+
let prefix = " ".repeat(indent);
66+
67+
// Node kind
68+
write!(out, "{}{}", prefix, node.kind_name()).unwrap();
69+
70+
// Location
71+
if options.show_locations {
72+
let start = node.start_position();
73+
let end = node.end_position();
74+
write!(
75+
out,
76+
" [{},{}]-[{},{}]",
77+
start.row + 1,
78+
start.column + 1,
79+
end.row + 1,
80+
end.column + 1
81+
)
82+
.unwrap();
83+
}
84+
85+
// Content for leaf nodes
86+
if options.show_content && node.is_named() && is_leaf(node) {
87+
let content = node_content(node, source);
88+
if !content.is_empty() {
89+
write!(out, " {content:?}").unwrap();
90+
}
91+
}
92+
93+
writeln!(out).unwrap();
94+
95+
// Named fields first
96+
for (&field_id, children) in &node.fields {
97+
if field_id == CHILD_FIELD {
98+
continue; // Handle unnamed children last
99+
}
100+
let field_name = ast.field_name_for_id(field_id).unwrap_or("?");
101+
if children.len() == 1 {
102+
write!(out, "{prefix} {field_name}:").unwrap();
103+
// Inline single child
104+
let child = ast.get_node(children[0]);
105+
if child.is_some_and(is_leaf) {
106+
write!(out, " ").unwrap();
107+
dump_node_inline(ast, children[0], source, options, out);
108+
} else {
109+
writeln!(out).unwrap();
110+
dump_node(ast, children[0], source, options, indent + 2, out);
111+
}
112+
} else {
113+
writeln!(out, "{prefix} {field_name}:").unwrap();
114+
for &child_id in children {
115+
dump_node(ast, child_id, source, options, indent + 2, out);
116+
}
117+
}
118+
}
119+
120+
// Unnamed children — skip unnamed tokens (keywords, punctuation)
121+
if let Some(children) = node.fields.get(&CHILD_FIELD) {
122+
for &child_id in children {
123+
if let Some(child) = ast.get_node(child_id) {
124+
if child.is_named() {
125+
dump_node(ast, child_id, source, options, indent + 1, out);
126+
}
127+
}
128+
}
129+
}
130+
}
131+
132+
/// Dump a leaf node inline (no newline prefix, caller provides context).
133+
fn dump_node_inline(ast: &Ast, id: usize, source: &str, options: &DumpOptions, out: &mut String) {
134+
let node = match ast.get_node(id) {
135+
Some(n) => n,
136+
None => return,
137+
};
138+
139+
write!(out, "{}", node.kind_name()).unwrap();
140+
141+
if options.show_locations {
142+
let start = node.start_position();
143+
let end = node.end_position();
144+
write!(
145+
out,
146+
" [{},{}]-[{},{}]",
147+
start.row + 1,
148+
start.column + 1,
149+
end.row + 1,
150+
end.column + 1
151+
)
152+
.unwrap();
153+
}
154+
155+
if options.show_content && node.is_named() {
156+
let content = node_content(node, source);
157+
if !content.is_empty() {
158+
write!(out, " {content:?}").unwrap();
159+
}
160+
}
161+
162+
writeln!(out).unwrap();
163+
}
164+
165+
fn is_leaf(node: &Node) -> bool {
166+
node.fields.is_empty()
167+
}
168+
169+
fn node_content(node: &Node, source: &str) -> String {
170+
match &node.content {
171+
NodeContent::DynamicString(s) if !s.is_empty() => s.clone(),
172+
_ => {
173+
let range = node.byte_range();
174+
if range.start < source.len() && range.end <= source.len() {
175+
source[range.start..range.end].to_string()
176+
} else {
177+
String::new()
178+
}
179+
}
180+
}
181+
}

0 commit comments

Comments
 (0)