Skip to content

Commit 865fc07

Browse files
committed
yeast: Clarify semantics surrounding captured sequences
Copilot's second review sent me down a bit of a rabbit hole. The fundamental question is the following: what (if anything) does it mean to capture a query sequence? That is, consider the following query: ``` (foo ((bar) (baz)) @quux) ``` Here, `@quux` is attached to a subquery that matches a sequence of nodes, `bar` followerd by `baz`. So, what should this actually capture? The current implementation treats this in a somewhat surprising way, equivalent to if we had written ``` (foo ((bar) @quux (baz) @quux)) ``` that is, both kinds of child nodes get pushed into the `quux` capture. To me, this behaviour is very surprising and unintuitive (and since there is an explicit way to express this anyway, there doesn't seem to be much reason to prefer the "shorthand" version). For this reason, the present commit just disallows adding a capture to a query sequence entirely. A capture is only attached to a single query node, never a sequence of nodes. Note that this does not mean that a _capture_ can't contain a sequence of nodes. Both ``` (foo (bar)* @bars) ``` and ``` (foo ((bar) @bars)*) ``` (which are equivalent) capture all of the `bar` children and put them in `bars` as a `Vec<Id>`. In the new AST, we represent the syntax in the order it appears, so `(bar)* @bars` is a capture of a repeated node match. However, when we lower this into an executable query, we push the capture in, so it becomes more like the second version. We're still only capturing a single node at a time, but we may do so several times due to the repetition. (One might consider getting rid of the first syntax entirely, then, but I think it's more readable to write this as "capture of repeat" rather than "repeat of capture".) Finally, in addition to no longer allowing captures to apply to sequences, we now also do not allow sequences to be empty (i.e. `()`) or contain a single element. The first would have no effect (an empty sequence always succeeds), and the second is redundant (a singleton sequence is equivalent to its single element). All of these checks are enforced at compile-time.
1 parent cafada6 commit 865fc07

4 files changed

Lines changed: 179 additions & 64 deletions

File tree

‎shared/yeast-macros/src/ast.rs‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ impl Cardinality {
5454
multiple: true,
5555
required: true,
5656
};
57+
58+
pub(crate) fn combine(self, other: Self) -> Self {
59+
Self {
60+
multiple: self.multiple || other.multiple,
61+
required: self.required && other.required,
62+
}
63+
}
5764
}
5865

5966
/// An input pattern such as `(call method: (identifier) @name)`.
@@ -81,6 +88,27 @@ pub(crate) enum Pattern {
8188
},
8289
}
8390

91+
impl Pattern {
92+
/// The cardinality of an ordinary node capture around this pattern.
93+
///
94+
/// Sequences require explicit inner captures.
95+
pub(crate) fn capture_cardinality(&self) -> Option<Cardinality> {
96+
match self {
97+
Pattern::Any { .. } | Pattern::Node { .. } | Pattern::Unnamed(_) => {
98+
Some(Cardinality::SINGLE)
99+
}
100+
Pattern::Capture { pattern, .. } => pattern.capture_cardinality(),
101+
Pattern::Repeated {
102+
pattern,
103+
cardinality,
104+
} => pattern
105+
.capture_cardinality()
106+
.map(|inner| inner.combine(*cardinality)),
107+
Pattern::Sequence(_) => None,
108+
}
109+
}
110+
}
111+
84112
#[derive(Clone)]
85113
pub(crate) struct Capture {
86114
/// Capture identifier after `@` or `@@`.

‎shared/yeast-macros/src/lower.rs‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ impl Pattern {
128128
fn lower_list(&self) -> Vec<TokenStream> {
129129
match self {
130130
Pattern::Sequence(patterns) => patterns.iter().flat_map(Pattern::lower_list).collect(),
131+
Pattern::Capture { capture, pattern }
132+
if matches!(pattern.as_ref(), Pattern::Repeated { .. }) =>
133+
{
134+
pattern.lower_list_with_capture(&capture.name.to_string())
135+
}
131136
Pattern::Repeated {
132137
pattern,
133138
cardinality,
@@ -152,6 +157,41 @@ impl Pattern {
152157
}
153158
}
154159
}
160+
161+
fn lower_list_with_capture(&self, capture: &str) -> Vec<TokenStream> {
162+
match self {
163+
Pattern::Repeated {
164+
pattern,
165+
cardinality,
166+
} => {
167+
let children = pattern.lower_list_with_capture(capture);
168+
let repetition = match (cardinality.multiple, cardinality.required) {
169+
(true, false) => quote! { yeast::query::Rep::ZeroOrMore },
170+
(true, true) => quote! { yeast::query::Rep::OneOrMore },
171+
(false, false) => quote! { yeast::query::Rep::ZeroOrOne },
172+
(false, true) => unreachable!("single patterns are not wrapped as repeated"),
173+
};
174+
vec![quote! {
175+
yeast::query::QueryListElem::Repeated {
176+
children: vec![#(#children),*],
177+
rep: #repetition,
178+
}
179+
}]
180+
}
181+
pattern => {
182+
let pattern = pattern.lower();
183+
vec![quote! {
184+
yeast::query::QueryListElem::SingleNode(
185+
yeast::query::QueryNode::Capture {
186+
capture: #capture,
187+
node: Box::new(#pattern),
188+
}
189+
)
190+
}]
191+
}
192+
}
193+
}
194+
155195
fn captures(&self) -> Vec<BoundCapture> {
156196
let mut captures = Vec::new();
157197
self.collect_captures(Cardinality::SINGLE, &mut captures);
@@ -168,9 +208,12 @@ impl Pattern {
168208
}
169209
Pattern::Capture { capture, pattern } => {
170210
pattern.collect_captures(cardinality, captures);
211+
let captured = pattern
212+
.capture_cardinality()
213+
.expect("capture patterns are validated during parsing");
171214
captures.push(BoundCapture {
172215
capture: capture.clone(),
173-
cardinality,
216+
cardinality: combine_cardinality(cardinality, captured),
174217
});
175218
}
176219
Pattern::Sequence(patterns) => {

‎shared/yeast-macros/src/rule_parse.rs‎

Lines changed: 105 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,7 @@ pub(crate) fn parse_rule(input: TokenStream) -> Result<Rule> {
3030

3131
fn parse_pattern_with_capture(tokens: &mut Tokens) -> Result<Pattern> {
3232
let pattern = parse_pattern_atom(tokens)?;
33-
if peek_is_at(tokens) {
34-
Ok(Pattern::Capture {
35-
capture: consume_capture(tokens)?,
36-
pattern: Box::new(pattern),
37-
})
38-
} else {
39-
Ok(pattern)
40-
}
33+
maybe_capture(tokens, pattern)
4134
}
4235

4336
fn parse_pattern_atom(tokens: &mut Tokens) -> Result<Pattern> {
@@ -116,11 +109,11 @@ fn parse_pattern_fields(tokens: &mut Tokens) -> Result<Vec<(String, Pattern)>> {
116109
};
117110
let pattern = if peek_is_repetition(tokens) {
118111
let cardinality = expect_cardinality(tokens)?;
119-
let pattern = capture_repeated_pattern(tokens, atom)?;
120-
Pattern::Repeated {
121-
pattern: Box::new(pattern),
112+
let repeated = Pattern::Repeated {
113+
pattern: Box::new(atom),
122114
cardinality,
123-
}
115+
};
116+
maybe_capture(tokens, repeated)?
124117
} else {
125118
maybe_capture(tokens, atom)?
126119
};
@@ -163,13 +156,27 @@ fn parse_pattern_list(tokens: &mut Tokens) -> Result<Vec<Pattern>> {
163156
let repeated = if is_single_pattern {
164157
parse_parenthesized_pattern(&mut inner)?
165158
} else {
166-
Pattern::Sequence(parse_pattern_list(&mut inner)?)
159+
let patterns = parse_pattern_list(&mut inner)?;
160+
if patterns.is_empty() {
161+
return Err(syn::Error::new(
162+
group.span(),
163+
"empty query groups are not allowed",
164+
));
165+
}
166+
if patterns.len() == 1 {
167+
return Err(syn::Error::new(
168+
group.span(),
169+
"single-pattern query groups are not allowed; \
170+
remove the redundant parentheses",
171+
));
172+
}
173+
Pattern::Sequence(patterns)
167174
};
168-
let repeated = capture_repeated_pattern(tokens, repeated)?;
169-
patterns.push(Pattern::Repeated {
175+
let repeated = Pattern::Repeated {
170176
pattern: Box::new(repeated),
171177
cardinality,
172-
});
178+
};
179+
patterns.push(maybe_capture(tokens, repeated)?);
173180
} else {
174181
let pattern = parse_parenthesized_pattern(&mut inner)?;
175182
patterns.push(maybe_capture(tokens, pattern)?);
@@ -203,6 +210,12 @@ fn parse_pattern_list(tokens: &mut Tokens) -> Result<Vec<Pattern>> {
203210

204211
fn maybe_capture(tokens: &mut Tokens, pattern: Pattern) -> Result<Pattern> {
205212
if peek_is_at(tokens) {
213+
if pattern.capture_cardinality().is_none() {
214+
return Err(syn::Error::new_spanned(
215+
tokens.peek().unwrap().clone(),
216+
"cannot capture a query sequence; capture the desired nodes explicitly",
217+
));
218+
}
206219
Ok(Pattern::Capture {
207220
capture: consume_capture(tokens)?,
208221
pattern: Box::new(pattern),
@@ -218,46 +231,11 @@ fn maybe_repeat(tokens: &mut Tokens, pattern: Pattern) -> Result<Pattern> {
218231
}
219232

220233
let cardinality = expect_cardinality(tokens)?;
221-
let pattern = capture_repeated_pattern(tokens, pattern)?;
222-
Ok(Pattern::Repeated {
234+
let repeated = Pattern::Repeated {
223235
pattern: Box::new(pattern),
224236
cardinality,
225-
})
226-
}
227-
228-
fn capture_repeated_pattern(tokens: &mut Tokens, pattern: Pattern) -> Result<Pattern> {
229-
if !peek_is_at(tokens) {
230-
return Ok(pattern);
231-
}
232-
233-
if matches!(
234-
&pattern,
235-
Pattern::Sequence(patterns)
236-
if patterns.iter().any(|pattern| matches!(pattern, Pattern::Repeated { .. }))
237-
) {
238-
return Err(syn::Error::new_spanned(
239-
tokens.peek().unwrap().clone(),
240-
"cannot capture a repeated group containing a nested repetition; \
241-
capture the desired inner patterns explicitly",
242-
));
243-
}
244-
245-
let capture = consume_capture(tokens)?;
246-
Ok(match pattern {
247-
Pattern::Sequence(patterns) => Pattern::Sequence(
248-
patterns
249-
.into_iter()
250-
.map(|pattern| Pattern::Capture {
251-
capture: capture.clone(),
252-
pattern: Box::new(pattern),
253-
})
254-
.collect(),
255-
),
256-
pattern => Pattern::Capture {
257-
capture,
258-
pattern: Box::new(pattern),
259-
},
260-
})
237+
};
238+
maybe_capture(tokens, repeated)
261239
}
262240

263241
fn parse_replacement(input: TokenStream) -> Result<Replacement> {
@@ -517,10 +495,16 @@ mod tests {
517495
Pattern::Sequence(patterns)
518496
if matches!(
519497
patterns.as_slice(),
520-
[Pattern::Repeated {
498+
[Pattern::Capture {
521499
pattern,
522-
cardinality: Cardinality::ZERO_OR_MORE,
523-
}] if matches!(pattern.as_ref(), Pattern::Capture { .. })
500+
..
501+
}] if matches!(
502+
pattern.as_ref(),
503+
Pattern::Repeated {
504+
cardinality: Cardinality::ZERO_OR_MORE,
505+
..
506+
}
507+
)
524508
)
525509
));
526510
}
@@ -539,15 +523,74 @@ mod tests {
539523
}
540524

541525
#[test]
542-
fn rejects_capture_on_repeated_group_with_nested_repetition() {
543-
let result = parse_pattern(quote!((root ((item)* (separator))* @items)));
526+
fn repeated_capture_wraps_the_repetition_in_the_ast() {
527+
let pattern = parse_pattern(quote!((array (identifier)* @items))).unwrap();
528+
529+
let Pattern::Node { fields, .. } = pattern else {
530+
panic!("expected array node pattern");
531+
};
532+
let Pattern::Sequence(children) = &fields[0].1 else {
533+
panic!("expected child sequence");
534+
};
535+
let Pattern::Capture {
536+
capture,
537+
pattern: repeated,
538+
} = &children[0]
539+
else {
540+
panic!("expected capture around the repetition");
541+
};
542+
assert_eq!(capture.name, "items");
543+
assert!(matches!(
544+
repeated.as_ref(),
545+
Pattern::Repeated {
546+
cardinality: Cardinality::ZERO_OR_MORE,
547+
..
548+
}
549+
));
550+
}
551+
552+
#[test]
553+
fn rejects_one_element_repeated_group() {
554+
let result = parse_pattern(quote!((array ((identifier))* @items)));
555+
let Err(error) = result else {
556+
panic!("expected redundant query group to be rejected");
557+
};
558+
assert_eq!(
559+
error.to_string(),
560+
"single-pattern query groups are not allowed; remove the redundant parentheses"
561+
);
562+
}
563+
564+
#[test]
565+
fn rejects_empty_repeated_group() {
566+
let result = parse_pattern(quote!((array ()*)));
567+
let Err(error) = result else {
568+
panic!("expected empty query group to be rejected");
569+
};
570+
assert_eq!(error.to_string(), "empty query groups are not allowed");
571+
}
572+
573+
#[test]
574+
fn rejects_capture_of_query_sequence() {
575+
let result = maybe_capture(
576+
&mut quote!(@item).into_iter().peekable(),
577+
Pattern::Sequence(vec![
578+
Pattern::Node {
579+
kind: "identifier".to_string(),
580+
fields: Vec::new(),
581+
},
582+
Pattern::Node {
583+
kind: "integer".to_string(),
584+
fields: Vec::new(),
585+
},
586+
]),
587+
);
544588
let Err(error) = result else {
545-
panic!("expected nested repetition capture to be rejected");
589+
panic!("expected multi-node sequence capture to be rejected");
546590
};
547591
assert_eq!(
548592
error.to_string(),
549-
"cannot capture a repeated group containing a nested repetition; \
550-
capture the desired inner patterns explicitly"
593+
"cannot capture a query sequence; capture the desired nodes explicitly"
551594
);
552595
}
553596
}

‎shared/yeast/src/query.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ impl QueryListElem {
143143
match self {
144144
QueryListElem::Repeated { children, rep } => {
145145
if children.is_empty() {
146-
// Empty repetition always succeeds without consuming
146+
// Macro parsing rejects empty groups. Retain safe
147+
// zero-width behavior for manually constructed queries.
147148
return Ok(*rep != Rep::OneOrMore);
148149
}
149150

0 commit comments

Comments
 (0)