Skip to content

Commit 3119f07

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 197f5b7 commit 3119f07

1 file changed

Lines changed: 159 additions & 0 deletions

File tree

shared/yeast/src/dump.rs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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(ast: &Ast, root: usize, source: &str, options: &DumpOptions) -> String {
42+
let mut out = String::new();
43+
dump_node(ast, root, source, options, 0, &mut out);
44+
out
45+
}
46+
47+
fn dump_node(ast: &Ast, id: usize, source: &str, options: &DumpOptions, indent: usize, out: &mut String) {
48+
let node = match ast.get_node(id) {
49+
Some(n) => n,
50+
None => return,
51+
};
52+
53+
let prefix = " ".repeat(indent);
54+
55+
// Node kind
56+
write!(out, "{}{}", prefix, node.kind_name()).unwrap();
57+
58+
// Location
59+
if options.show_locations {
60+
let start = node.start_position();
61+
let end = node.end_position();
62+
write!(out, " [{},{}]-[{},{}]",
63+
start.row + 1, start.column + 1,
64+
end.row + 1, end.column + 1
65+
).unwrap();
66+
}
67+
68+
// Content for leaf nodes
69+
if options.show_content && node.is_named() && is_leaf(node) {
70+
let content = node_content(node, source);
71+
if !content.is_empty() {
72+
write!(out, " {:?}", content).unwrap();
73+
}
74+
}
75+
76+
writeln!(out).unwrap();
77+
78+
// Named fields first
79+
for (&field_id, children) in &node.fields {
80+
if field_id == CHILD_FIELD {
81+
continue; // Handle unnamed children last
82+
}
83+
let field_name = ast.field_name_for_id(field_id).unwrap_or("?");
84+
if children.len() == 1 {
85+
write!(out, "{} {}:", prefix, field_name).unwrap();
86+
// Inline single child
87+
let child = ast.get_node(children[0]);
88+
if child.map_or(false, |c| is_leaf(c)) {
89+
write!(out, " ").unwrap();
90+
dump_node_inline(ast, children[0], source, options, out);
91+
} else {
92+
writeln!(out).unwrap();
93+
dump_node(ast, children[0], source, options, indent + 2, out);
94+
}
95+
} else {
96+
writeln!(out, "{} {}:", prefix, field_name).unwrap();
97+
for &child_id in children {
98+
dump_node(ast, child_id, source, options, indent + 2, out);
99+
}
100+
}
101+
}
102+
103+
// Unnamed children — skip unnamed tokens (keywords, punctuation)
104+
if let Some(children) = node.fields.get(&CHILD_FIELD) {
105+
for &child_id in children {
106+
if let Some(child) = ast.get_node(child_id) {
107+
if child.is_named() {
108+
dump_node(ast, child_id, source, options, indent + 1, out);
109+
}
110+
}
111+
}
112+
}
113+
}
114+
115+
/// Dump a leaf node inline (no newline prefix, caller provides context).
116+
fn dump_node_inline(ast: &Ast, id: usize, source: &str, options: &DumpOptions, out: &mut String) {
117+
let node = match ast.get_node(id) {
118+
Some(n) => n,
119+
None => return,
120+
};
121+
122+
write!(out, "{}", node.kind_name()).unwrap();
123+
124+
if options.show_locations {
125+
let start = node.start_position();
126+
let end = node.end_position();
127+
write!(out, " [{},{}]-[{},{}]",
128+
start.row + 1, start.column + 1,
129+
end.row + 1, end.column + 1
130+
).unwrap();
131+
}
132+
133+
if options.show_content && node.is_named() {
134+
let content = node_content(node, source);
135+
if !content.is_empty() {
136+
write!(out, " {:?}", content).unwrap();
137+
}
138+
}
139+
140+
writeln!(out).unwrap();
141+
}
142+
143+
fn is_leaf(node: &Node) -> bool {
144+
node.fields.is_empty()
145+
}
146+
147+
fn node_content(node: &Node, source: &str) -> String {
148+
match &node.content {
149+
NodeContent::DynamicString(s) if !s.is_empty() => s.clone(),
150+
_ => {
151+
let range = node.byte_range();
152+
if range.start < source.len() && range.end <= source.len() {
153+
source[range.start..range.end].to_string()
154+
} else {
155+
String::new()
156+
}
157+
}
158+
}
159+
}

0 commit comments

Comments
 (0)