Skip to content
5 changes: 5 additions & 0 deletions shared/yeast-macros/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ pub(crate) enum Pattern {
pattern: Box<Pattern>,
cardinality: Cardinality,
},
/// A choice that tries its alternatives from left to right.
Choice { alternatives: Vec<Pattern> },
/// An ordered sequence of patterns within a field.
Sequence(Vec<Pattern>),
/// A repeated or optional pattern.
Expand All @@ -92,6 +94,9 @@ impl Pattern {
match self {
Pattern::Any { .. } | Pattern::Node { .. } | Pattern::Unnamed(_) => true,
Pattern::Capture { cardinality, .. } => *cardinality == Cardinality::SINGLE,
Pattern::Choice { alternatives } => {
alternatives.iter().all(Pattern::matches_single_node)
}
Pattern::Sequence(_) | Pattern::Repeated { .. } => false,
}
}
Expand Down
4 changes: 4 additions & 0 deletions shared/yeast-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ mod template_parse;
/// (kind)* @capture - capture each repeated named-node match
/// "literal"* @capture - capture each repeated unnamed-token match
/// (kind)? - zero or one named node
/// [(pattern) (pattern)] - query choice: try alternatives in order
/// ```
///
/// Named fields and bare child patterns may be intermixed in any order.
Expand Down Expand Up @@ -161,6 +162,9 @@ pub fn trees(input: TokenStream) -> TokenStream {
/// - `@name` (no quantifier) → `name: OutputId`
/// - `@name` (after `*`/`+`) → `name: Vec<OutputId>`
/// - `@name` (after `?`) → `name: Option<OutputId>`
/// - captures in a query choice are unified by name: a capture in every branch
/// remains required, a missing/optional capture becomes optional, and any
/// repeated occurrence makes the binding a vector
///
/// Raw `@@name` captures use the corresponding input `Id` types instead.
///
Expand Down
64 changes: 64 additions & 0 deletions shared/yeast-macros/src/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,14 @@ impl Pattern {
}
}
}
Pattern::Choice { alternatives } => {
let alternatives = alternatives.iter().map(Pattern::lower);
quote! {
yeast::query::QueryNode::Choice {
alternatives: vec![#(#alternatives),*],
}
}
}
Pattern::Sequence(_) | Pattern::Repeated { .. } => {
unreachable!("sequence and repeated patterns cannot match a single node")
}
Expand All @@ -168,6 +176,9 @@ impl Pattern {
fn lower_list(&self) -> Vec<TokenStream> {
match self {
Pattern::Sequence(patterns) => patterns.iter().flat_map(Pattern::lower_list).collect(),
Pattern::Choice { .. } if !self.matches_single_node() => {
vec![self.lower_list_choice()]
}
Pattern::Capture {
name,
pattern,
Expand Down Expand Up @@ -226,6 +237,21 @@ impl Pattern {
}
}

fn lower_list_choice(&self) -> TokenStream {
let Pattern::Choice { alternatives } = self else {
unreachable!("expected query choice pattern");
};
let alternatives = alternatives.iter().map(|alternative| {
let children = alternative.lower_list();
quote! { vec![#(#children),*] }
});
quote! {
yeast::query::QueryListElem::Choice {
alternatives: vec![#(#alternatives),*],
}
}
}

fn captures(&self) -> Vec<CaptureBinding> {
let mut captures = Vec::new();
self.collect_captures(Cardinality::SINGLE, &mut captures);
Expand Down Expand Up @@ -254,6 +280,9 @@ impl Pattern {
cardinality,
});
}
Pattern::Choice { alternatives } => {
captures.extend(unify_choice_captures(alternatives, cardinality));
}
Pattern::Sequence(patterns) => {
for pattern in patterns {
pattern.collect_captures(cardinality, captures);
Expand All @@ -269,6 +298,41 @@ impl Pattern {
}
}

fn unify_choice_captures(
alternatives: &[Pattern],
parent: Cardinality,
) -> Vec<CaptureBinding> {
let alternative_count = alternatives.len();
let mut order = Vec::new();
let mut by_name = std::collections::BTreeMap::<String, Vec<CaptureBinding>>::new();

for alternative in alternatives {
for capture in alternative.captures() {
let name = capture.name.to_string();
if !by_name.contains_key(&name) {
order.push(name.clone());
}
by_name.entry(name).or_default().push(capture);
}
}

order
.into_iter()
.map(|name| {
let captures = by_name.remove(&name).unwrap();
let present_in_every_alternative = captures.len() == alternative_count;
let mut capture = captures[0].clone();
for other in captures.iter().skip(1) {
capture.cardinality.multiple |= other.cardinality.multiple;
capture.cardinality.required &= other.cardinality.required;
}
capture.cardinality.required &= present_in_every_alternative;
capture.cardinality = combine_cardinality(parent, capture.cardinality);
capture
})
.collect()
}

fn combine_cardinality(parent: Cardinality, child: Cardinality) -> Cardinality {
Cardinality {
multiple: parent.multiple || child.multiple,
Expand Down
Loading
Loading