Skip to content

Commit a049850

Browse files
committed
Yeast: add type-checking errors in AST dump
1 parent 49f1909 commit a049850

4 files changed

Lines changed: 459 additions & 51 deletions

File tree

shared/yeast/src/dump.rs

Lines changed: 194 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::fmt::Write;
22

3-
use crate::{Ast, Node, NodeContent, CHILD_FIELD};
3+
use crate::{schema::Schema, Ast, Node, NodeContent, CHILD_FIELD};
44

55
/// Options for controlling AST dump output.
66
pub struct DumpOptions {
@@ -45,16 +45,143 @@ pub fn dump_ast_with_options(
4545
options: &DumpOptions,
4646
) -> String {
4747
let mut out = String::new();
48-
dump_node(ast, root, source, options, 0, &mut out);
48+
dump_node(ast, root, source, options, 0, None, &mut out);
4949
out
5050
}
5151

52+
/// Dump an AST and annotate type mismatches against a schema inline.
53+
///
54+
/// Any node that does not match the expected type set for its parent field is
55+
/// rendered with a trailing `" <-- ERROR: ..."` annotation on the same line.
56+
pub fn dump_ast_with_type_errors(
57+
ast: &Ast,
58+
root: usize,
59+
source: &str,
60+
schema: &Schema,
61+
) -> String {
62+
dump_ast_with_type_errors_and_options(ast, root, source, schema, &DumpOptions::default())
63+
}
64+
65+
/// Dump an AST and annotate type mismatches against a schema inline.
66+
///
67+
/// Any node that does not match the expected type set for its parent field is
68+
/// rendered with a trailing `" <-- ERROR: ..."` annotation on the same line.
69+
pub fn dump_ast_with_type_errors_and_options(
70+
ast: &Ast,
71+
root: usize,
72+
source: &str,
73+
schema: &Schema,
74+
options: &DumpOptions,
75+
) -> String {
76+
let mut out = String::new();
77+
dump_node(ast, root, source, options, 0, Some((schema, None, None)), &mut out);
78+
out
79+
}
80+
81+
fn format_node_types(node_types: &[crate::schema::NodeType]) -> String {
82+
node_types
83+
.iter()
84+
.map(|t| {
85+
if t.named {
86+
t.kind.clone()
87+
} else {
88+
format!("\"{}\"", t.kind)
89+
}
90+
})
91+
.collect::<Vec<_>>()
92+
.join(" | ")
93+
}
94+
95+
const EMPTY_NODE_TYPES: &[crate::schema::NodeType] = &[];
96+
97+
/// Generate a type-checking error message for a node if it doesn't match expected types.
98+
///
99+
/// # Arguments
100+
/// - `schema`: The AST schema to validate against.
101+
/// - `node`: The node being checked.
102+
/// - `expected`: The set of allowed types for this node, or `None` if type-checking is disabled.
103+
/// - `parent_field`: Optional tuple of (parent_kind, field_name) for context in error messages.
104+
///
105+
/// # Returns
106+
/// `Some(error_message)` if the node violates the schema (e.g., wrong kind, missing field declaration).
107+
/// `None` if the node matches the expected types or if type-checking is disabled.
108+
fn type_error_for_node(
109+
schema: &Schema,
110+
node: &Node,
111+
expected: Option<&[crate::schema::NodeType]>,
112+
parent_field: Option<(&str, &str)>,
113+
) -> Option<String> {
114+
if schema.id_for_node_kind(node.kind_name()).is_none()
115+
&& schema.id_for_unnamed_node_kind(node.kind_name()).is_none()
116+
{
117+
return Some(format!("node kind '{}' not in schema", node.kind_name()));
118+
}
119+
120+
let expected = expected?;
121+
if expected.is_empty() {
122+
if let Some((kind, field)) = parent_field {
123+
return Some(format!("the node '{kind}' has no field '{field}'"));
124+
}
125+
return Some("field not declared in schema for this parent node".to_string());
126+
}
127+
if schema.node_matches_types(node.kind_name(), node.is_named(), expected) {
128+
None
129+
} else {
130+
let actual = if node.is_named() {
131+
node.kind_name().to_string()
132+
} else {
133+
format!("\"{}\"", node.kind_name())
134+
};
135+
136+
if let Some((kind, field)) = parent_field {
137+
Some(format!(
138+
"The field {}.{} should contain {}, but got {}",
139+
kind,
140+
field,
141+
format_node_types(expected),
142+
actual
143+
))
144+
} else {
145+
Some(format!(
146+
"expected {}, got {}",
147+
format_node_types(expected),
148+
actual
149+
))
150+
}
151+
}
152+
}
153+
154+
/// Look up the allowed types for a field in the schema.
155+
///
156+
/// # Arguments
157+
/// - `schema`: The AST schema to query.
158+
/// - `parent_kind`: The node kind of the parent that contains this field.
159+
/// - `field_id`: The field ID within that parent node.
160+
///
161+
/// # Returns
162+
/// `Some(&[NodeType])` if the field is declared in the schema and has type constraints.
163+
/// `None` if the field is not declared or has no constraints (undeclared field).
164+
fn expected_for_field<'a>(
165+
schema: &'a Schema,
166+
parent_kind: &str,
167+
field_id: u16,
168+
) -> Option<&'a [crate::schema::NodeType]> {
169+
schema
170+
.field_types(parent_kind, field_id)
171+
.map(|v| v.as_slice())
172+
}
173+
52174
fn dump_node(
53175
ast: &Ast,
54176
id: usize,
55177
source: &str,
56178
options: &DumpOptions,
57179
indent: usize,
180+
type_check: Option<(
181+
&Schema,
182+
Option<&[crate::schema::NodeType]>,
183+
Option<(&str, &str)>,
184+
)>,
58185
out: &mut String,
59186
) {
60187
let node = match ast.get_node(id) {
@@ -90,6 +217,12 @@ fn dump_node(
90217
}
91218
}
92219

220+
if let Some((schema, expected, parent_field)) = type_check {
221+
if let Some(err) = type_error_for_node(schema, node, expected, parent_field) {
222+
write!(out, " <-- ERROR: {err}").unwrap();
223+
}
224+
}
225+
93226
writeln!(out).unwrap();
94227

95228
// Named fields first
@@ -98,39 +231,87 @@ fn dump_node(
98231
continue; // Handle unnamed children last
99232
}
100233
let field_name = ast.field_name_for_id(field_id).unwrap_or("?");
234+
let child_type_check = type_check.map(|(schema, _, _)| {
235+
let expected = expected_for_field(schema, node.kind_name(), field_id)
236+
.or(Some(EMPTY_NODE_TYPES));
237+
let parent_field = Some((node.kind_name(), field_name));
238+
(schema, expected, parent_field)
239+
});
240+
101241
if children.len() == 1 {
102242
write!(out, "{prefix} {field_name}:").unwrap();
103243
// Inline single child
104244
let child = ast.get_node(children[0]);
105245
if child.is_some_and(is_leaf) {
106246
write!(out, " ").unwrap();
107-
dump_node_inline(ast, children[0], source, options, out);
247+
dump_node_inline(ast, children[0], source, options, child_type_check, out);
108248
} else {
109249
writeln!(out).unwrap();
110-
dump_node(ast, children[0], source, options, indent + 2, out);
250+
dump_node(
251+
ast,
252+
children[0],
253+
source,
254+
options,
255+
indent + 2,
256+
child_type_check,
257+
out,
258+
);
111259
}
112260
} else {
113261
writeln!(out, "{prefix} {field_name}:").unwrap();
114262
for &child_id in children {
115-
dump_node(ast, child_id, source, options, indent + 2, out);
263+
dump_node(
264+
ast,
265+
child_id,
266+
source,
267+
options,
268+
indent + 2,
269+
child_type_check,
270+
out,
271+
);
116272
}
117273
}
118274
}
119275

120276
// Unnamed children — skip unnamed tokens (keywords, punctuation)
121277
if let Some(children) = node.fields.get(&CHILD_FIELD) {
278+
let child_type_check = type_check.map(|(schema, _, _)| {
279+
let expected = expected_for_field(schema, node.kind_name(), CHILD_FIELD)
280+
.or(Some(EMPTY_NODE_TYPES));
281+
let parent_field = Some((node.kind_name(), "children"));
282+
(schema, expected, parent_field)
283+
});
122284
for &child_id in children {
123285
if let Some(child) = ast.get_node(child_id) {
124286
if child.is_named() {
125-
dump_node(ast, child_id, source, options, indent + 1, out);
287+
dump_node(
288+
ast,
289+
child_id,
290+
source,
291+
options,
292+
indent + 1,
293+
child_type_check,
294+
out,
295+
);
126296
}
127297
}
128298
}
129299
}
130300
}
131301

132302
/// 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) {
303+
fn dump_node_inline(
304+
ast: &Ast,
305+
id: usize,
306+
source: &str,
307+
options: &DumpOptions,
308+
type_check: Option<(
309+
&Schema,
310+
Option<&[crate::schema::NodeType]>,
311+
Option<(&str, &str)>,
312+
)>,
313+
out: &mut String,
314+
) {
134315
let node = match ast.get_node(id) {
135316
Some(n) => n,
136317
None => return,
@@ -159,6 +340,12 @@ fn dump_node_inline(ast: &Ast, id: usize, source: &str, options: &DumpOptions, o
159340
}
160341
}
161342

343+
if let Some((schema, expected, parent_field)) = type_check {
344+
if let Some(err) = type_error_for_node(schema, node, expected, parent_field) {
345+
write!(out, " <-- ERROR: {err}").unwrap();
346+
}
347+
}
348+
162349
writeln!(out).unwrap();
163350
}
164351

0 commit comments

Comments
 (0)