Skip to content

Commit dbfddab

Browse files
tausbnCopilot
andcommitted
yeast: Annotate children outside parent source ranges
Keep external child locations visible in AST dumps without treating non-containment as a fatal source-skeleton error. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 110d16f commit dbfddab

2 files changed

Lines changed: 59 additions & 15 deletions

File tree

‎shared/yeast/doc/yeast.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,17 @@ return_expr source="return ⟨value⟩"
292292
call_expr source="⟨callee⟩(⟨argument⟩)"
293293
```
294294

295+
Children outside the node's source range retain their own locations and are
296+
annotated where they are printed rather than being treated as errors:
297+
298+
```text
299+
accessor_declaration source="⟨accessor_kind⟩"
300+
name_node: identifier "value" source="value" (external)
301+
```
302+
303+
Node and child ranges are still validated against the source text and UTF-8
304+
boundaries.
305+
295306
### Optional fields (`?`)
296307

297308
A `?` on a field's value makes that field fallible. If a `#{expr}` anywhere

‎shared/yeast/src/dump.rs‎

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ pub fn dump_ast(ast: &Ast, root: Id, source: &str) -> String {
5151

5252
pub fn dump_ast_with_options(ast: &Ast, root: Id, source: &str, options: &DumpOptions) -> String {
5353
let mut out = String::new();
54-
dump_node(ast, root, source, options, 0, None, &mut out);
54+
dump_node(ast, root, source, options, 0, None, false, &mut out);
5555
out
5656
}
5757

@@ -86,6 +86,7 @@ pub fn dump_ast_with_type_errors_and_options(
8686
expected: None,
8787
parent_field: None,
8888
}),
89+
false,
8990
&mut out,
9091
);
9192
out
@@ -195,6 +196,7 @@ fn dump_node(
195196
options: &DumpOptions,
196197
indent: usize,
197198
type_check: Option<TypeCheckContext<'_>>,
199+
external_to_parent: bool,
198200
out: &mut String,
199201
) {
200202
let node = match ast.get_node(id) {
@@ -232,6 +234,9 @@ fn dump_node(
232234

233235
if options.show_abridged_source {
234236
write_source_skeleton(ast, node, source, out);
237+
if external_to_parent {
238+
write!(out, " (external)").unwrap();
239+
}
235240
}
236241

237242
if let Some(context) = type_check {
@@ -300,9 +305,18 @@ fn dump_node(
300305
write!(out, "{prefix} {field_name}:").unwrap();
301306
// Inline single child
302307
let child = ast.get_node(children[0]);
308+
let external = child.is_some_and(|child| is_external_child(node, child));
303309
if child.is_some_and(is_leaf) {
304310
write!(out, " ").unwrap();
305-
dump_node_inline(ast, children[0], source, options, child_type_check, out);
311+
dump_node_inline(
312+
ast,
313+
children[0],
314+
source,
315+
options,
316+
child_type_check,
317+
external,
318+
out,
319+
);
306320
} else {
307321
writeln!(out).unwrap();
308322
dump_node(
@@ -312,19 +326,24 @@ fn dump_node(
312326
options,
313327
indent + 2,
314328
child_type_check,
329+
external,
315330
out,
316331
);
317332
}
318333
} else {
319334
writeln!(out, "{prefix} {field_name}:").unwrap();
320335
for &child_id in children {
336+
let external = ast
337+
.get_node(child_id)
338+
.is_some_and(|child| is_external_child(node, child));
321339
dump_node(
322340
ast,
323341
child_id,
324342
source,
325343
options,
326344
indent + 2,
327345
child_type_check,
346+
external,
328347
out,
329348
);
330349
}
@@ -365,13 +384,15 @@ fn dump_node(
365384
for &child_id in children {
366385
if let Some(child) = ast.get_node(child_id) {
367386
if child.is_named() {
387+
let external = is_external_child(node, child);
368388
dump_node(
369389
ast,
370390
child_id,
371391
source,
372392
options,
373393
indent + 1,
374394
child_type_check,
395+
external,
375396
out,
376397
);
377398
}
@@ -387,6 +408,7 @@ fn dump_node_inline(
387408
source: &str,
388409
options: &DumpOptions,
389410
type_check: Option<TypeCheckContext<'_>>,
411+
external_to_parent: bool,
390412
out: &mut String,
391413
) {
392414
let node = match ast.get_node(id) {
@@ -419,6 +441,9 @@ fn dump_node_inline(
419441

420442
if options.show_abridged_source {
421443
write_source_skeleton(ast, node, source, out);
444+
if external_to_parent {
445+
write!(out, " (external)").unwrap();
446+
}
422447
}
423448

424449
if let Some(context) = type_check {
@@ -457,6 +482,13 @@ fn node_source_range(node: &Node) -> Option<crate::Range> {
457482
}
458483
}
459484

485+
fn is_external_child(parent: &Node, child: &Node) -> bool {
486+
let (Some(parent), Some(child)) = (node_source_range(parent), node_source_range(child)) else {
487+
return false;
488+
};
489+
child.start_byte < parent.start_byte || child.end_byte > parent.end_byte
490+
}
491+
460492
fn source_skeleton(ast: &Ast, node: &Node, source: &str) -> SourceSkeleton {
461493
let Some(parent) = node_source_range(node) else {
462494
return SourceSkeleton::Missing;
@@ -494,10 +526,7 @@ fn source_skeleton(ast: &Ast, node: &Node, source: &str) -> SourceSkeleton {
494526
));
495527
}
496528
if child.start < parent.start || child.end > parent.end {
497-
return SourceSkeleton::Invalid(format!(
498-
"child range {}..{} is outside node range {}..{}",
499-
child.start, child.end, parent.start, parent.end
500-
));
529+
continue;
501530
}
502531
if child.start == child.end {
503532
continue;
@@ -613,12 +642,6 @@ mod tests {
613642
#[test]
614643
fn source_skeleton_validates_empty_child_ranges() {
615644
let cases = [
616-
(
617-
"abcdef",
618-
range(0, 3),
619-
range(4, 4),
620-
"child range 4..4 is outside node range 0..3",
621-
),
622645
(
623646
"abcdef",
624647
range(0, 6),
@@ -640,6 +663,13 @@ mod tests {
640663
"unexpected dump: {dump}"
641664
);
642665
}
666+
667+
let dump = dump_with_children("abcdef", range(0, 3), &[("marker", range(4, 4))]);
668+
assert!(dump.starts_with("parent source=\"abc\"\n"));
669+
assert!(
670+
dump.contains("marker: child source=\"\" (external)\n"),
671+
"unexpected dump: {dump}"
672+
);
643673
}
644674

645675
#[test]
@@ -667,12 +697,15 @@ mod tests {
667697
}
668698

669699
#[test]
670-
fn source_skeleton_reports_children_outside_the_parent() {
700+
fn source_skeleton_marks_children_outside_the_parent() {
671701
let source = "abcdefghi";
672702
let dump = dump_with_children(source, range(0, 6), &[("child", range(7, 9))]);
673703

674-
assert!(dump
675-
.starts_with("parent source=<invalid: child range 7..9 is outside node range 0..6>\n"));
704+
assert!(dump.starts_with("parent source=\"abcdef\"\n"));
705+
assert!(
706+
dump.contains(" child source=\"hi\" (external)\n"),
707+
"unexpected dump: {dump}"
708+
);
676709
}
677710
}
678711

0 commit comments

Comments
 (0)