Yeast: Fix three issues from second review round

1. Depth guard now only counts rule rewrites, not tree traversal.
   Deep ASTs (>100 levels) no longer trigger false "non-terminating
   cycle" errors. Only actual rule-rewrite chains are depth-limited.

2. Repeated matcher detects zero-width matches and breaks the loop.
   Patterns like ((_)?)* no longer infinite-loop — if the iterator
   does not advance after a successful match, the repetition stops.

3. Shorthand rule! syntax now propagates source_range to synthetic
   nodes via create_node_with_range, matching the full template path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Taus
2026-05-01 15:48:45 +00:00
parent 15f45dd43e
commit 112b38f78e
3 changed files with 16 additions and 6 deletions

View File

@@ -630,11 +630,12 @@ pub fn parse_rule_top(input: TokenStream) -> Result<TokenStream> {
.unwrap_or_else(|| panic!("node kind '{}' not found", #output_kind_str));
let mut __fields = std::collections::BTreeMap::new();
#(#field_stmts)*
let __id = #ctx_ident.ast.create_node(
let __id = #ctx_ident.ast.create_node_with_range(
__kind,
yeast::NodeContent::DynamicString(String::new()),
__fields,
true,
__source_range,
);
vec![__id]
}

View File

@@ -556,16 +556,16 @@ impl Rule {
}
}
const MAX_RULE_DEPTH: usize = 100;
const MAX_REWRITE_DEPTH: usize = 100;
fn apply_rules(rules: &Vec<Rule>, ast: &mut Ast, id: Id, fresh: &tree_builder::FreshScope) -> Result<Vec<Id>, String> {
apply_rules_inner(rules, ast, id, fresh, 0)
}
fn apply_rules_inner(rules: &Vec<Rule>, ast: &mut Ast, id: Id, fresh: &tree_builder::FreshScope, depth: usize) -> Result<Vec<Id>, String> {
if depth > MAX_RULE_DEPTH {
fn apply_rules_inner(rules: &Vec<Rule>, ast: &mut Ast, id: Id, fresh: &tree_builder::FreshScope, rewrite_depth: usize) -> Result<Vec<Id>, String> {
if rewrite_depth > MAX_REWRITE_DEPTH {
return Err(format!(
"Desugaring exceeded maximum depth ({MAX_RULE_DEPTH}). \
"Desugaring exceeded maximum rewrite depth ({MAX_REWRITE_DEPTH}). \
This likely indicates a non-terminating rule cycle."
));
}
@@ -574,7 +574,8 @@ fn apply_rules_inner(rules: &Vec<Rule>, ast: &mut Ast, id: Id, fresh: &tree_buil
if let Some(result_node) = rule.try_rule(ast, id, fresh)? {
let mut results = Vec::new();
for node in result_node {
results.extend(apply_rules_inner(rules, ast, node, fresh, depth + 1)?);
// Increment depth only when re-processing rule output
results.extend(apply_rules_inner(rules, ast, node, fresh, rewrite_depth + 1)?);
}
return Ok(results);
}
@@ -583,6 +584,7 @@ fn apply_rules_inner(rules: &Vec<Rule>, ast: &mut Ast, id: Id, fresh: &tree_buil
let mut node = ast.nodes[id].clone();
// recursively descend into all the fields
// Child traversal does not increment rewrite depth
for vec in node.fields.values_mut() {
let old = std::mem::take(vec);
let mut new = Vec::new();

View File

@@ -125,11 +125,18 @@ impl QueryListElem {
loop {
let matches_initial = matches.clone();
let start = remaining_children.clone();
let start_next = start.clone().next();
if !match_children(children.iter(), ast, remaining_children, matches)? {
*remaining_children = start;
*matches = matches_initial;
break;
}
// Guard against zero-width matches: if the iterator
// didn't advance, break to avoid infinite looping.
let current_next = remaining_children.clone().next();
if start_next == current_next {
break;
}
iters += 1;
if *rep == Rep::ZeroOrOne {
break;