Skip to content

Commit b29609e

Browse files
committed
yeast: Catch translation panics before they get printed
When `panic!` gets called, the default Rust panic handler prints out the error (and possibly a backtrace) to stderr. This means even if we handle the panics in `run_once`, we still get the printed output. To avoid this, we use `resume_unwind` (which foregoes the default behaviour) instead, and add a helper function called `translation_failure` that takes care of the places where translation could previously panic.
1 parent 0af4824 commit b29609e

3 files changed

Lines changed: 49 additions & 41 deletions

File tree

‎shared/yeast/src/lib.rs‎

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ pub use yeast_macros::{query, rule, rules, tree, trees};
2020
use captures::{Captures, TranslatedCaptures};
2121
use query::QueryNode;
2222

23+
#[derive(Debug)]
24+
struct TranslationFailure(String);
25+
26+
pub(crate) fn translation_failure(message: impl Into<String>) -> ! {
27+
std::panic::resume_unwind(Box::new(TranslationFailure(message.into())))
28+
}
29+
2330
impl From<tree_sitter::Point> for Point {
2431
fn from(p: tree_sitter::Point) -> Self {
2532
Point {
@@ -1166,7 +1173,7 @@ impl<'a, C: Clone> TranslatorHandle<'a, C> {
11661173
) -> TranslatedCaptures {
11671174
captures.translate_except(skip, |id| {
11681175
if id == self.matched_root {
1169-
panic!("cannot eagerly translate a capture of the matched rule root")
1176+
translation_failure("cannot eagerly translate a capture of the matched rule root")
11701177
} else {
11711178
self.translate(ast, output, user_ctx, id)
11721179
}
@@ -1327,10 +1334,10 @@ fn apply_rules_inner<C: Clone>(
13271334
rewrite_depth: usize,
13281335
) -> Vec<OutputId> {
13291336
if rewrite_depth > MAX_REWRITE_DEPTH {
1330-
panic!(
1337+
translation_failure(format!(
13311338
"Desugaring exceeded maximum rewrite depth ({MAX_REWRITE_DEPTH}). \
13321339
This likely indicates a non-terminating rule cycle."
1333-
);
1340+
));
13341341
}
13351342

13361343
let node_kind = ast.get_node(id).map(|n| n.kind_name()).unwrap_or("");
@@ -1359,7 +1366,7 @@ fn apply_rules_inner<C: Clone>(
13591366
return result;
13601367
}
13611368

1362-
panic!("no rule matched node of kind '{node_kind}'")
1369+
translation_failure(format!("no rule matched node of kind '{node_kind}'"))
13631370
}
13641371

13651372
/// A named bundle of exhaustive rules for the one-shot translation pass.
@@ -1617,37 +1624,39 @@ impl<'a, C: Clone> Runner<'a, C> {
16171624
}
16181625

16191626
fn run_once(&self, ast: Ast, user_ctx: &mut C) -> Result<OutputAst, String> {
1620-
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1627+
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
16211628
self.run_once_inner(ast, user_ctx)
1622-
}))
1623-
.map(Ok)
1624-
.unwrap_or_else(|payload| {
1625-
let message = if let Some(message) = payload.downcast_ref::<&str>() {
1626-
(*message).to_owned()
1627-
} else if let Some(message) = payload.downcast_ref::<String>() {
1628-
message.clone()
1629-
} else {
1630-
"unknown panic payload".to_owned()
1631-
};
1632-
Err(format!("YEAST translation panicked: {message}"))
1633-
})
1629+
})) {
1630+
Ok(output) => Ok(output),
1631+
Err(payload) => match payload.downcast::<TranslationFailure>() {
1632+
Ok(failure) => Err(failure.0),
1633+
Err(payload) => {
1634+
let message = if let Some(message) = payload.downcast_ref::<&str>() {
1635+
(*message).to_owned()
1636+
} else if let Some(message) = payload.downcast_ref::<String>() {
1637+
message.clone()
1638+
} else {
1639+
"unknown panic payload".to_owned()
1640+
};
1641+
Err(format!("YEAST translation panicked: {message}"))
1642+
}
1643+
},
1644+
}
16341645
}
16351646

16361647
fn run_once_inner(&self, ast: Ast, user_ctx: &mut C) -> OutputAst {
1637-
assert_eq!(
1638-
self.phases.len(),
1639-
1,
1640-
"YEAST requires exactly one translation phase"
1641-
);
1648+
if self.phases.len() != 1 {
1649+
translation_failure("YEAST requires exactly one translation phase");
1650+
}
16421651
let phase = &self.phases[0];
16431652
let mut output = OutputAst::with_schema(self.schema.clone());
16441653
let res = apply_rules(&phase.rules, &ast, &mut output, user_ctx, ast.get_root());
1645-
assert_eq!(
1646-
res.len(),
1647-
1,
1648-
"expected exactly one result node, got {}",
1649-
res.len()
1650-
);
1654+
if res.len() != 1 {
1655+
translation_failure(format!(
1656+
"expected exactly one result node, got {}",
1657+
res.len()
1658+
));
1659+
}
16511660
output.set_root(res[0]);
16521661
output.set_source(ast.into_source());
16531662
output

‎shared/yeast/src/query.rs‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,16 +79,16 @@ impl QueryNode {
7979
}
8080
QueryNode::Node { kind, children } => {
8181
let node = ast.get_node(node).unwrap();
82-
let target_kind = ast
83-
.id_for_node_kind(kind)
84-
.unwrap_or_else(|| panic!("Node kind {kind} not found in language"));
82+
let target_kind = ast.id_for_node_kind(kind).unwrap_or_else(|| {
83+
crate::translation_failure(format!("Node kind {kind} not found in language"))
84+
});
8585
if node.kind != target_kind {
8686
return false;
8787
}
8888
for (field, field_children) in children {
89-
let field_id = ast
90-
.field_id_for_name(field)
91-
.unwrap_or_else(|| panic!("Field {field} not found in language"));
89+
let field_id = ast.field_id_for_name(field).unwrap_or_else(|| {
90+
crate::translation_failure(format!("Field {field} not found in language"))
91+
});
9292
let empty = Vec::new();
9393
let mut child_iter =
9494
node.fields.get(&field_id).unwrap_or(&empty).iter().cloned();
@@ -100,9 +100,11 @@ impl QueryNode {
100100
}
101101
QueryNode::UnnamedNode { kind } => {
102102
let node = ast.get_node(node).unwrap();
103-
let target_kind = ast
104-
.id_for_unnamed_node_kind(kind)
105-
.unwrap_or_else(|| panic!("unnamed Node kind {kind} not found in language"));
103+
let target_kind = ast.id_for_unnamed_node_kind(kind).unwrap_or_else(|| {
104+
crate::translation_failure(format!(
105+
"unnamed Node kind {kind} not found in language"
106+
))
107+
});
106108
node.kind == target_kind
107109
}
108110
QueryNode::Capture {

‎shared/yeast/tests/test.rs‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1385,10 +1385,7 @@ fn test_translation_phase_errors_when_no_rule_matches() {
13851385
let err = runner
13861386
.run("x = 1")
13871387
.expect_err("expected translation to error on unmatched node");
1388-
assert!(
1389-
err.contains("no rule matched") && err.contains("integer"),
1390-
"error should describe the unmatched node kind, got: {err}"
1391-
);
1388+
assert_eq!(err, "no rule matched node of kind 'integer'");
13921389
}
13931390

13941391
#[test]

0 commit comments

Comments
 (0)