Skip to content

Commit c3a9218

Browse files
committed
Yeast: Add one-shot phase kind
1 parent a049850 commit c3a9218

4 files changed

Lines changed: 323 additions & 29 deletions

File tree

shared/yeast/doc/yeast.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,8 +349,8 @@ to enable rewriting:
349349

350350
```rust
351351
let desugar = yeast::DesugaringConfig::new()
352-
.add_phase("cleanup", cleanup_rules())
353-
.add_phase("desugar", desugar_rules())
352+
.add_phase("cleanup", yeast::PhaseKind::Repeating, cleanup_rules())
353+
.add_phase("translate", yeast::PhaseKind::OneShot, translate_rules())
354354
.with_output_node_types_yaml(include_str!("output-node-types.yml"));
355355

356356
let lang = simple::LanguageSpec {
@@ -365,6 +365,15 @@ let lang = simple::LanguageSpec {
365365
A single-phase config is just `.add_phase(...)` called once. Phase names
366366
appear in error messages so you can tell which phase failed.
367367

368+
There are two kinds of phases:
369+
- **Repeating**:
370+
Each node is re-processed until none of the rules in the phase matches.
371+
When a node no longer matches any rules, its children are recursively processed. In practice this is used to desugar or simplify an AST, while staying mostly within the same schema.
372+
- **One-shot**:
373+
Each node is processed by the first matching rule, and the engine panics if no rule matches.
374+
Rules are then recursively applied to every captured node.
375+
In practice this is used when translating from one AST schema to another, where an exhaustive match is required.
376+
368377
The same YAML node-types is used for both the runtime yeast `Schema` (so
369378
rules can refer to output-only kinds and fields) and TRAP validation (it
370379
is converted to JSON internally).

shared/yeast/src/captures.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,21 @@ impl Captures {
6161
}
6262
}
6363
}
64+
65+
/// Apply a fallible function to every captured id (across all keys),
66+
/// replacing each id with the result. Stops and returns the error on
67+
/// the first failure.
68+
pub fn try_map_all_captures<E>(
69+
&mut self,
70+
mut f: impl FnMut(Id) -> Result<Id, E>,
71+
) -> Result<(), E> {
72+
for ids in self.captures.values_mut() {
73+
for id in ids {
74+
*id = f(*id)?;
75+
}
76+
}
77+
Ok(())
78+
}
6479
pub fn map_captures_to(&mut self, from: &str, to: &'static str, f: &mut impl FnMut(Id) -> Id) {
6580
if let Some(from_ids) = self.captures.get(from) {
6681
let new_values = from_ids.iter().copied().map(f).collect();

shared/yeast/src/lib.rs

Lines changed: 119 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -526,18 +526,39 @@ impl Rule {
526526
node: Id,
527527
fresh: &tree_builder::FreshScope,
528528
) -> Result<Option<Vec<Id>>, String> {
529+
match self.try_match(ast, node)? {
530+
Some(captures) => Ok(Some(self.run_transform(ast, captures, node, fresh))),
531+
None => Ok(None),
532+
}
533+
}
534+
535+
/// Attempt to match this rule's query against `node`, returning the
536+
/// resulting captures on success. Does not invoke the transform.
537+
fn try_match(&self, ast: &Ast, node: Id) -> Result<Option<Captures>, String> {
529538
let mut captures = Captures::new();
530539
if self.query.do_match(ast, node, &mut captures)? {
531-
fresh.next_scope();
532-
let source_range = ast.get_node(node).and_then(|n| match n.content {
533-
NodeContent::Range(r) => Some(r),
534-
_ => n.source_range,
535-
});
536-
Ok(Some((self.transform)(ast, captures, fresh, source_range)))
540+
Ok(Some(captures))
537541
} else {
538542
Ok(None)
539543
}
540544
}
545+
546+
/// Run this rule's transform with the given captures, using `node`'s
547+
/// source range as the source range of the produced nodes.
548+
fn run_transform(
549+
&self,
550+
ast: &mut Ast,
551+
captures: Captures,
552+
node: Id,
553+
fresh: &tree_builder::FreshScope,
554+
) -> Vec<Id> {
555+
fresh.next_scope();
556+
let source_range = ast.get_node(node).and_then(|n| match n.content {
557+
NodeContent::Range(r) => Some(r),
558+
_ => n.source_range,
559+
});
560+
(self.transform)(ast, captures, fresh, source_range)
561+
}
541562
}
542563

543564
const MAX_REWRITE_DEPTH: usize = 100;
@@ -572,17 +593,17 @@ impl<'a> RuleIndex<'a> {
572593
}
573594
}
574595

575-
fn apply_rules(
596+
fn apply_repeating_rules(
576597
rules: &[Rule],
577598
ast: &mut Ast,
578599
id: Id,
579600
fresh: &tree_builder::FreshScope,
580601
) -> Result<Vec<Id>, String> {
581602
let index = RuleIndex::new(rules);
582-
apply_rules_inner(&index, ast, id, fresh, 0, None)
603+
apply_repeating_rules_inner(&index, ast, id, fresh, 0, None)
583604
}
584605

585-
fn apply_rules_inner(
606+
fn apply_repeating_rules_inner(
586607
index: &RuleIndex,
587608
ast: &mut Ast,
588609
id: Id,
@@ -611,7 +632,7 @@ fn apply_rules_inner(
611632
let next_skip = if rule.repeated { None } else { Some(rule_ptr) };
612633
let mut results = Vec::new();
613634
for node in result_node {
614-
results.extend(apply_rules_inner(
635+
results.extend(apply_repeating_rules_inner(
615636
index,
616637
ast,
617638
node,
@@ -636,7 +657,7 @@ fn apply_rules_inner(
636657
for children in fields.values_mut() {
637658
let mut new_children: Option<Vec<Id>> = None;
638659
for (i, &child_id) in children.iter().enumerate() {
639-
let result = apply_rules_inner(index, ast, child_id, fresh, rewrite_depth, None)?;
660+
let result = apply_repeating_rules_inner(index, ast, child_id, fresh, rewrite_depth, None)?;
640661
let unchanged = result.len() == 1 && result[0] == child_id;
641662
match (&mut new_children, unchanged) {
642663
(None, true) => {} // unchanged so far, no allocation needed
@@ -661,6 +682,75 @@ fn apply_rules_inner(
661682
Ok(vec![id])
662683
}
663684

685+
/// Apply rules using `OneShot` semantics: the first matching rule fires on
686+
/// each visited node, recursion proceeds only through captured nodes (not
687+
/// through the input node's children directly), and an error is returned if
688+
/// no rule matches a visited node.
689+
fn apply_one_shot_rules(
690+
rules: &[Rule],
691+
ast: &mut Ast,
692+
id: Id,
693+
fresh: &tree_builder::FreshScope,
694+
) -> Result<Vec<Id>, String> {
695+
let index = RuleIndex::new(rules);
696+
apply_one_shot_rules_inner(&index, ast, id, fresh, 0)
697+
}
698+
699+
fn apply_one_shot_rules_inner(
700+
index: &RuleIndex,
701+
ast: &mut Ast,
702+
id: Id,
703+
fresh: &tree_builder::FreshScope,
704+
rewrite_depth: usize,
705+
) -> Result<Vec<Id>, String> {
706+
if rewrite_depth > MAX_REWRITE_DEPTH {
707+
return Err(format!(
708+
"Desugaring exceeded maximum rewrite depth ({MAX_REWRITE_DEPTH}). \
709+
This likely indicates a non-terminating rule cycle."
710+
));
711+
}
712+
713+
let node_kind = ast.get_node(id).map(|n| n.kind()).unwrap_or("");
714+
for rule in index.rules_for_kind(node_kind) {
715+
if let Some(mut captures) = rule.try_match(ast, id)? {
716+
// Recursively translate every captured node before invoking the
717+
// transform. The transform's output uses output-schema kinds, so
718+
// we must translate captured input-schema nodes to their
719+
// output-schema equivalents first.
720+
captures.try_map_all_captures(|captured_id| {
721+
let result =
722+
apply_one_shot_rules_inner(index, ast, captured_id, fresh, rewrite_depth + 1)?;
723+
if result.len() != 1 {
724+
return Err(format!(
725+
"OneShot: recursion on captured node produced {} results, expected exactly 1",
726+
result.len()
727+
));
728+
}
729+
Ok(result[0])
730+
})?;
731+
return Ok(rule.run_transform(ast, captures, id, fresh));
732+
}
733+
}
734+
735+
Err(format!(
736+
"OneShot: no rule matched node of kind '{node_kind}'"
737+
))
738+
}
739+
740+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
741+
pub enum PhaseKind {
742+
/// A node is re-processed until none of the rules in the phase matches,
743+
/// albeit a single rule cannot be applied twice in a row unless that rule is also marked as repeating.
744+
/// When a node no longer matches any rules, its children are recursively processed (top down).
745+
Repeating,
746+
747+
/// A node is processed by the first matching rule, and the engine panics if no rule matches.
748+
/// Rules are then recursively applied to every captured node.
749+
/// In practice this is used when translating from one AST schema to another, where every node must be rewritten,
750+
/// and it would be a type error to match the rule patterns (based on the input schema) against the output nodes (which conform to the output schema).
751+
OneShot,
752+
}
753+
664754
/// One phase of a desugaring pass: a named bundle of rules that runs to
665755
/// completion (a full traversal applying its rules) before the next phase
666756
/// starts. Rules within a phase compete for matches as usual; rules in
@@ -670,13 +760,15 @@ pub struct Phase {
670760
/// Name used in error messages.
671761
pub name: String,
672762
pub rules: Vec<Rule>,
763+
pub kind: PhaseKind,
673764
}
674765

675766
impl Phase {
676-
pub fn new(name: impl Into<String>, rules: Vec<Rule>) -> Self {
767+
pub fn new(name: impl Into<String>, kind: PhaseKind, rules: Vec<Rule>) -> Self {
677768
Self {
678769
name: name.into(),
679770
rules,
771+
kind,
680772
}
681773
}
682774
}
@@ -694,8 +786,8 @@ impl Phase {
694786
///
695787
/// ```ignore
696788
/// let config = yeast::DesugaringConfig::new()
697-
/// .add_phase("cleanup", cleanup_rules)
698-
/// .add_phase("desugar", desugar_rules)
789+
/// .add_phase("cleanup", PhaseKind::Repeating, cleanup_rules)
790+
/// .add_phase("desugar", PhaseKind::Repeating, desugar_rules)
699791
/// .with_output_node_types_yaml(yaml);
700792
/// ```
701793
#[derive(Default)]
@@ -715,9 +807,14 @@ impl DesugaringConfig {
715807
Self::default()
716808
}
717809

718-
/// Append a new phase with the given name and rules.
719-
pub fn add_phase(mut self, name: impl Into<String>, rules: Vec<Rule>) -> Self {
720-
self.phases.push(Phase::new(name, rules));
810+
/// Append a new phase with the given name, kind, and rules.
811+
pub fn add_phase(
812+
mut self,
813+
name: impl Into<String>,
814+
kind: PhaseKind,
815+
rules: Vec<Rule>,
816+
) -> Self {
817+
self.phases.push(Phase::new(name, kind, rules));
721818
self
722819
}
723820

@@ -806,8 +903,11 @@ impl<'a> Runner<'a> {
806903
let fresh = tree_builder::FreshScope::new();
807904
let mut root = ast.get_root();
808905
for phase in self.phases {
809-
let res = apply_rules(&phase.rules, ast, root, &fresh)
810-
.map_err(|e| format!("Phase `{}`: {e}", phase.name))?;
906+
let res = match phase.kind {
907+
PhaseKind::Repeating => apply_repeating_rules(&phase.rules, ast, root, &fresh),
908+
PhaseKind::OneShot => apply_one_shot_rules(&phase.rules, ast, root, &fresh),
909+
}
910+
.map_err(|e| format!("Phase `{}`: {e}", phase.name))?;
811911
if res.len() != 1 {
812912
return Err(format!(
813913
"Phase `{}`: expected exactly one result node, got {}",

0 commit comments

Comments
 (0)