Yeast: Propagate errors via Result instead of panicking

Runner::run and run_from_tree now return Result<Ast, String> instead
of panicking on errors. Errors from query matching (unknown node kinds
or field names), parser setup, and unexpected result counts are all
propagated.

The extractor's extract_and_desugar gracefully falls back to the
un-desugared AST on error, logging the failure via tracing::error.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Taus
2026-05-01 15:22:58 +00:00
parent 23011671ff
commit a3e7e4cfd2
4 changed files with 43 additions and 39 deletions

View File

@@ -837,7 +837,12 @@ pub fn extract_and_desugar(
schema,
);
let runner = yeast::Runner::new(language.clone(), rules);
let ast = runner.run_from_tree(&tree);
let ast = runner.run_from_tree(&tree)
.unwrap_or_else(|e| {
tracing::error!("Desugaring failed: {e}");
// Fall back to the un-desugared AST
yeast::Ast::from_tree(language.clone(), &tree)
});
traverse_yeast(&ast, &mut visitor);

View File

@@ -21,6 +21,6 @@ fn main() {
let language = get_language(&args.language);
let source = std::fs::read_to_string(&args.file).unwrap();
let runner = yeast::Runner::new(language, vec![]);
let ast = runner.run(&source);
let ast = runner.run(&source).unwrap();
println!("{}", ast.print(&source, ast.get_root()));
}

View File

@@ -539,52 +539,49 @@ impl Rule {
Self { query, transform }
}
fn try_rule(&self, ast: &mut Ast, node: Id, fresh: &tree_builder::FreshScope) -> Option<Vec<Id>> {
fn try_rule(&self, ast: &mut Ast, node: Id, fresh: &tree_builder::FreshScope) -> Result<Option<Vec<Id>>, String> {
let mut captures = Captures::new();
if self.query.do_match(ast, node, &mut captures).unwrap() {
if self.query.do_match(ast, node, &mut captures)? {
fresh.next_scope();
// Get the source range of the matched node for location info
let source_range = ast.get_node(node).and_then(|n| {
match n.content {
NodeContent::Range(r) => Some(r),
_ => n.source_range,
}
});
Some((self.transform)(ast, captures, fresh, source_range))
Ok(Some((self.transform)(ast, captures, fresh, source_range)))
} else {
None
Ok(None)
}
}
}
fn apply_rules(rules: &Vec<Rule>, ast: &mut Ast, id: Id, fresh: &tree_builder::FreshScope) -> Vec<Id> {
// apply the transformation rules on this node
fn apply_rules(rules: &Vec<Rule>, ast: &mut Ast, id: Id, fresh: &tree_builder::FreshScope) -> Result<Vec<Id>, String> {
for rule in rules {
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, fresh))
.collect();
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(rules, ast, node, fresh)?);
}
return Ok(results);
}
}
// copy the current node
let mut node = ast.nodes[id].clone();
// recursively descend into all the fields
for vec in node.fields.values_mut() {
let mut old = Vec::new();
mem::swap(vec, &mut old);
*vec = old
.iter()
.flat_map(|node| apply_rules(rules, ast, *node, fresh))
.collect();
let old = std::mem::take(vec);
let mut new = Vec::new();
for child_id in old {
new.extend(apply_rules(rules, ast, child_id, fresh)?);
}
*vec = new;
}
node.id = ast.nodes.len();
ast.nodes.push(node);
vec![ast.nodes.len() - 1]
Ok(vec![ast.nodes.len() - 1])
}
pub struct Runner {
@@ -597,28 +594,30 @@ impl Runner {
Self { language, rules }
}
pub fn run_from_tree(&self, tree: &tree_sitter::Tree) -> Ast {
pub fn run_from_tree(&self, tree: &tree_sitter::Tree) -> Result<Ast, String> {
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, &fresh);
let res = apply_rules(&self.rules, &mut ast, 0, &fresh)?;
if res.len() != 1 {
panic!("Expected at exactly one result node, got {}", res.len());
return Err(format!("Expected exactly one result node, got {}", res.len()));
}
ast.set_root(res[0]);
ast
Ok(ast)
}
pub fn run(&self, input: &str) -> Ast {
pub fn run(&self, input: &str) -> Result<Ast, String> {
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();
parser.set_language(&self.language)
.map_err(|e| format!("Failed to set language: {e}"))?;
let tree = parser.parse(input, None)
.ok_or_else(|| "Failed to parse input".to_string())?;
let mut ast = Ast::from_tree(self.language.clone(), &tree);
let res = apply_rules(&self.rules, &mut ast, 0, &fresh);
let res = apply_rules(&self.rules, &mut ast, 0, &fresh)?;
if res.len() != 1 {
panic!("Expected at exactly one result node, got {}", res.len());
return Err(format!("Expected exactly one result node, got {}", res.len()));
}
ast.set_root(res[0]);
ast
Ok(ast)
}
}

View File

@@ -6,7 +6,7 @@ use yeast::*;
/// Helper: parse Ruby source, apply rules, return dump of result.
fn run_and_dump(input: &str, rules: Vec<Rule>) -> String {
let runner = Runner::new(tree_sitter_ruby::LANGUAGE.into(), rules);
let ast = runner.run(input);
let ast = runner.run(input).unwrap();
dump_ast(&ast, ast.get_root(), input)
}
@@ -64,7 +64,7 @@ program
#[test]
fn test_query_match() {
let runner = Runner::new(tree_sitter_ruby::LANGUAGE.into(), vec![]);
let ast = runner.run("x = 1");
let ast = runner.run("x = 1").unwrap();
let query = yeast::query!(
(program
@@ -85,7 +85,7 @@ fn test_query_match() {
#[test]
fn test_query_no_match() {
let runner = Runner::new(tree_sitter_ruby::LANGUAGE.into(), vec![]);
let ast = runner.run("x = 1");
let ast = runner.run("x = 1").unwrap();
let query = yeast::query!(
(program
@@ -103,7 +103,7 @@ fn test_query_no_match() {
#[test]
fn test_query_repeated_capture() {
let runner = Runner::new(tree_sitter_ruby::LANGUAGE.into(), vec![]);
let ast = runner.run("x, y, z = 1");
let ast = runner.run("x, y, z = 1").unwrap();
let query = yeast::query!(
(assignment
@@ -129,7 +129,7 @@ fn test_query_repeated_capture() {
#[test]
fn test_tree_builder() {
let runner = Runner::new(tree_sitter_ruby::LANGUAGE.into(), vec![]);
let mut ast = runner.run("x = 1");
let mut ast = runner.run("x = 1").unwrap();
let input = "x = 1";
let query = yeast::query!(
@@ -286,7 +286,7 @@ program
#[test]
fn test_cursor_navigation() {
let runner = Runner::new(tree_sitter_ruby::LANGUAGE.into(), vec![]);
let ast = runner.run("x = 1");
let ast = runner.run("x = 1").unwrap();
let mut cursor = AstCursor::new(&ast);
// Start at root