Yeast: Share FreshScope across rule applications

The fresh identifier counter is now shared across all rule applications
within a single Runner::run call. Each rule application gets a fresh
"scope" (resolved names are cleared) but the counter keeps incrementing.

This ensures that $tmp in different rules produces distinct names:
the for-rule gets $tmp-0, the assignment rule gets $tmp-1.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Taus
2026-05-01 14:04:21 +00:00
parent 147ca943b5
commit 86972fa04f
5 changed files with 30 additions and 21 deletions

View File

@@ -623,9 +623,9 @@ pub fn parse_rule_top(input: TokenStream) -> Result<TokenStream> {
Ok(quote! {
{
let __query = #query_code;
yeast::Rule::new(__query, Box::new(|__ast: &mut yeast::Ast, __captures: yeast::captures::Captures| {
yeast::Rule::new(__query, Box::new(|__ast: &mut yeast::Ast, __captures: yeast::captures::Captures, __fresh: &yeast::tree_builder::FreshScope| {
#(#bindings)*
let mut #ctx_ident = yeast::build::BuildCtx::new(__ast, &__captures);
let mut #ctx_ident = yeast::build::BuildCtx::new(__ast, &__captures, __fresh);
#transform_body
}))
}

View File

@@ -12,15 +12,15 @@ use crate::{Ast, Id, NodeContent, CHILD_FIELD};
pub struct BuildCtx<'a> {
pub ast: &'a mut Ast,
pub captures: &'a Captures,
pub fresh: FreshScope,
pub fresh: &'a FreshScope,
}
impl<'a> BuildCtx<'a> {
pub fn new(ast: &'a mut Ast, captures: &'a Captures) -> Self {
pub fn new(ast: &'a mut Ast, captures: &'a Captures, fresh: &'a FreshScope) -> Self {
Self {
ast,
captures,
fresh: FreshScope::new(),
fresh,
}
}

View File

@@ -500,32 +500,33 @@ impl From<tree_sitter::Range> for NodeContent {
pub struct Rule {
query: QueryNode,
transform: Box<dyn Fn(&mut Ast, Captures) -> Vec<Id>>,
transform: Box<dyn Fn(&mut Ast, Captures, &tree_builder::FreshScope) -> Vec<Id>>,
}
impl Rule {
pub fn new(query: QueryNode, transform: Box<dyn Fn(&mut Ast, Captures) -> Vec<Id>>) -> Self {
pub fn new(query: QueryNode, transform: Box<dyn Fn(&mut Ast, Captures, &tree_builder::FreshScope) -> Vec<Id>>) -> Self {
Self { query, transform }
}
fn try_rule(&self, ast: &mut Ast, node: Id) -> Option<Vec<Id>> {
fn try_rule(&self, ast: &mut Ast, node: Id, fresh: &tree_builder::FreshScope) -> Option<Vec<Id>> {
let mut captures = Captures::new();
if self.query.do_match(ast, node, &mut captures).unwrap() {
Some((self.transform)(ast, captures))
fresh.next_scope();
Some((self.transform)(ast, captures, fresh))
} else {
None
}
}
}
fn apply_rules(rules: &Vec<Rule>, ast: &mut Ast, id: Id) -> Vec<Id> {
fn apply_rules(rules: &Vec<Rule>, ast: &mut Ast, id: Id, fresh: &tree_builder::FreshScope) -> Vec<Id> {
// apply the transformation rules on this node
for rule in rules {
if let Some(result_node) = rule.try_rule(ast, id) {
if let Some(result_node) = rule.try_rule(ast, id, fresh) {
// We transformed it so now recurse into the result
return result_node
.iter()
.flat_map(|node| apply_rules(rules, ast, *node))
.flat_map(|node| apply_rules(rules, ast, *node, fresh))
.collect();
}
}
@@ -539,7 +540,7 @@ fn apply_rules(rules: &Vec<Rule>, ast: &mut Ast, id: Id) -> Vec<Id> {
mem::swap(vec, &mut old);
*vec = old
.iter()
.flat_map(|node| apply_rules(rules, ast, *node))
.flat_map(|node| apply_rules(rules, ast, *node, fresh))
.collect();
}
@@ -559,8 +560,9 @@ impl Runner {
}
pub fn run_from_tree(&self, tree: &tree_sitter::Tree) -> Ast {
let fresh = tree_builder::FreshScope::new();
let mut ast = Ast::from_tree(self.language.clone(), tree);
let res = apply_rules(&self.rules, &mut ast, 0);
let res = apply_rules(&self.rules, &mut ast, 0, &fresh);
if res.len() != 1 {
panic!("Expected at exactly one result node, got {}", res.len());
}
@@ -569,13 +571,12 @@ impl Runner {
}
pub fn run(&self, input: &str) -> Ast {
// Parse the input into an AST
let fresh = tree_builder::FreshScope::new();
let mut parser = tree_sitter::Parser::new();
parser.set_language(&self.language).unwrap();
let tree = parser.parse(input, None).unwrap();
let mut ast = Ast::from_tree(self.language.clone(), &tree);
let res = apply_rules(&self.rules, &mut ast, 0);
let res = apply_rules(&self.rules, &mut ast, 0, &fresh);
if res.len() != 1 {
panic!("Expected at exactly one result node, got {}", res.len());
}

View File

@@ -27,4 +27,11 @@ impl FreshScope {
})
.clone()
}
/// Clear resolved names but keep the counter. Called between rule
/// applications so that `$tmp` in different rules gets different values
/// while the counter increases monotonically.
pub fn next_scope(&self) {
self.resolved.borrow_mut().clear();
}
}

View File

@@ -145,7 +145,8 @@ fn test_tree_builder() {
query.do_match(&ast, ast.get_root(), &mut captures).unwrap();
// Swap left and right
let mut ctx = yeast::build::BuildCtx::new(&mut ast, &captures);
let fresh = yeast::tree_builder::FreshScope::new();
let mut ctx = yeast::build::BuildCtx::new(&mut ast, &captures, &fresh);
let new_id = yeast::tree!(ctx,
(program
child: (assignment
@@ -324,19 +325,19 @@ program
body:
block_body
assignment
left: identifier \"$tmp-0\"
left: identifier \"$tmp-1\"
right: identifier \"$tmp-0\"
assignment
left: identifier \"a\"
right:
element_reference
object: identifier \"$tmp-0\"
object: identifier \"$tmp-1\"
integer \"0\"
assignment
left: identifier \"b\"
right:
element_reference
object: identifier \"$tmp-0\"
object: identifier \"$tmp-1\"
integer \"1\"
identifier \"x\"
parameters: