Skip to content

Commit f3f66db

Browse files
committed
yeast: Tighten up parsing of parenthesised items
The previous parser was pretty lax in what it allowed to occur inside a parenthesised group. Thus, things like ``` (foo @bar) ``` or ``` (foo . bar) ``` would (because neither is valid syntax) simply ignore everything after the `foo` and behave as if the entire pattern was `(foo)`. We now expect a parenthesised item to consume its entire contents. Moreover, we now distinguish between sequences and named nodes as follows: if the first item is an identifier, then it's a named node, otherwise the parenthesised item is a sequence. In particular, this means something like `(_ ",")` is interpreted as "wildcard followed by comma". (Another valid reading would be "wildcard named node with comma inside, but this is a convoluted construction that we would likely want to express in a different way.) Hopefully this makes it clearer when a parenthesised item is one thing or the other.
1 parent fdc7026 commit f3f66db

3 files changed

Lines changed: 142 additions & 56 deletions

File tree

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,16 @@ mod template_parse;
3030
///
3131
/// Named fields and bare child patterns may be intermixed in any order.
3232
///
33-
/// Parentheses do not provide general-purpose grouping. A parenthesized query
34-
/// group contains at least two sibling patterns and may be repeated, but the
35-
/// group itself cannot be captured because it does not represent one node:
33+
/// A parenthesized query whose first element is a node kind is always parsed as
34+
/// that node pattern. Otherwise, two or more sibling patterns form a sequence.
35+
/// A sequence may be repeated, but cannot itself be captured because it does
36+
/// not represent one node:
3637
///
3738
/// ```text
3839
/// ((identifier) @items (integer) @items)* // valid: explicit node captures
3940
/// ((identifier) (integer))* @items // invalid: sequence capture
40-
/// ("+")* // invalid: redundant grouping
41-
/// "+"* // valid: repeated unnamed token
41+
/// ("+" ",")* // valid: repeated token sequence
42+
/// (_ ",")* // valid: repeated wildcard/token sequence
4243
/// ```
4344
#[proc_macro]
4445
pub fn query(input: TokenStream) -> TokenStream {

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

Lines changed: 124 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,7 @@ fn parse_pattern_atom(tokens: &mut Tokens) -> Result<Pattern> {
4242
Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Parenthesis => {
4343
let group = expect_group(tokens, Delimiter::Parenthesis)?;
4444
let mut inner = group.stream().into_iter().peekable();
45-
let pattern = parse_parenthesized_pattern(&mut inner)?;
46-
if let Some(token) = inner.next() {
47-
return Err(syn::Error::new_spanned(
48-
token,
49-
"unexpected token in query node",
50-
));
51-
}
52-
Ok(pattern)
45+
parse_parenthesized_pattern(&mut inner, group.span())
5346
}
5447
Some(TokenTree::Ident(identifier)) if *identifier == "_" => {
5548
tokens.next();
@@ -65,25 +58,51 @@ fn parse_pattern_atom(tokens: &mut Tokens) -> Result<Pattern> {
6558
}
6659
}
6760

68-
fn parse_parenthesized_pattern(tokens: &mut Tokens) -> Result<Pattern> {
61+
fn parse_parenthesized_pattern(tokens: &mut Tokens, span: Span) -> Result<Pattern> {
6962
match tokens.peek() {
70-
None => Err(syn::Error::new(
71-
Span::call_site(),
72-
"empty parenthesized group in query",
73-
)),
63+
None => Err(syn::Error::new(span, "empty query groups are not allowed")),
7464
Some(TokenTree::Ident(identifier)) if *identifier == "_" => {
7565
tokens.next();
76-
Ok(Pattern::Any {
77-
match_unnamed: false,
78-
})
66+
if tokens.peek().is_none() {
67+
Ok(Pattern::Any {
68+
match_unnamed: false,
69+
})
70+
} else {
71+
let mut patterns = vec![Pattern::Any {
72+
match_unnamed: true,
73+
}];
74+
patterns.extend(parse_pattern_list(tokens)?);
75+
finish_sequence(tokens, patterns, span)
76+
}
77+
}
78+
Some(TokenTree::Literal(_)) => {
79+
let first = Pattern::Unnamed(expect_string_literal(tokens)?);
80+
if tokens.peek().is_none() {
81+
Ok(first)
82+
} else {
83+
let mut patterns = vec![first];
84+
patterns.extend(parse_pattern_list(tokens)?);
85+
finish_sequence(tokens, patterns, span)
86+
}
7987
}
80-
Some(TokenTree::Literal(_)) => Ok(Pattern::Unnamed(expect_string_literal(tokens)?)),
8188
Some(TokenTree::Ident(_)) => {
8289
let kind = expect_ident(tokens, "expected node kind")?.to_string();
83-
Ok(Pattern::Node {
90+
let pattern = Pattern::Node {
8491
kind,
8592
fields: parse_pattern_fields(tokens)?,
86-
})
93+
};
94+
if let Some(token) = tokens.next() {
95+
Err(syn::Error::new_spanned(
96+
token,
97+
"unexpected token in query node",
98+
))
99+
} else {
100+
Ok(pattern)
101+
}
102+
}
103+
Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Parenthesis => {
104+
let patterns = parse_pattern_list(tokens)?;
105+
finish_sequence(tokens, patterns, span)
87106
}
88107
Some(token) => Err(syn::Error::new_spanned(
89108
token.clone(),
@@ -92,6 +111,23 @@ fn parse_parenthesized_pattern(tokens: &mut Tokens) -> Result<Pattern> {
92111
}
93112
}
94113

114+
fn finish_sequence(tokens: &mut Tokens, patterns: Vec<Pattern>, span: Span) -> Result<Pattern> {
115+
if let Some(token) = tokens.next() {
116+
return Err(syn::Error::new_spanned(
117+
token,
118+
"unexpected token in query sequence",
119+
));
120+
}
121+
match patterns.len() {
122+
0 => Err(syn::Error::new(span, "empty query groups are not allowed")),
123+
1 => Err(syn::Error::new(
124+
span,
125+
"single-pattern query groups are not allowed; remove the redundant parentheses",
126+
)),
127+
_ => Ok(Pattern::Sequence(patterns)),
128+
}
129+
}
130+
95131
fn parse_pattern_fields(tokens: &mut Tokens) -> Result<Vec<(String, Pattern)>> {
96132
let mut fields = Vec::new();
97133
let mut bare_children = Vec::new();
@@ -152,33 +188,14 @@ fn parse_pattern_list(tokens: &mut Tokens) -> Result<Vec<Pattern>> {
152188
let mut inner = group.stream().into_iter().peekable();
153189
if peek_is_repetition(tokens) {
154190
let cardinality = expect_cardinality(tokens)?;
155-
let is_single_pattern = matches!(inner.peek(), Some(TokenTree::Ident(_)));
156-
let repeated = if is_single_pattern {
157-
parse_parenthesized_pattern(&mut inner)?
158-
} else {
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)
174-
};
191+
let repeated = parse_parenthesized_pattern(&mut inner, group.span())?;
175192
let repeated = Pattern::Repeated {
176193
pattern: Box::new(repeated),
177194
cardinality,
178195
};
179196
patterns.push(maybe_capture(tokens, repeated)?);
180197
} else {
181-
let pattern = parse_parenthesized_pattern(&mut inner)?;
198+
let pattern = parse_parenthesized_pattern(&mut inner, group.span())?;
182199
patterns.push(maybe_capture(tokens, pattern)?);
183200
}
184201
continue;
@@ -593,4 +610,71 @@ mod tests {
593610
"cannot capture a query sequence; capture the desired nodes explicitly"
594611
);
595612
}
613+
614+
#[test]
615+
fn parses_literal_led_repeated_sequence() {
616+
let pattern = parse_pattern(quote!((root ("+" ",")*))).unwrap();
617+
let Pattern::Node { fields, .. } = pattern else {
618+
panic!("expected root node");
619+
};
620+
let Pattern::Sequence(children) = &fields[0].1 else {
621+
panic!("expected child sequence");
622+
};
623+
let Pattern::Repeated {
624+
pattern: repeated, ..
625+
} = &children[0]
626+
else {
627+
panic!("expected repetition");
628+
};
629+
assert!(matches!(
630+
repeated.as_ref(),
631+
Pattern::Sequence(patterns)
632+
if matches!(
633+
patterns.as_slice(),
634+
[Pattern::Unnamed(left), Pattern::Unnamed(right)]
635+
if left == "+" && right == ","
636+
)
637+
));
638+
}
639+
640+
#[test]
641+
fn parses_wildcard_led_repeated_sequence() {
642+
let pattern = parse_pattern(quote!((root (_ ",")*))).unwrap();
643+
let Pattern::Node { fields, .. } = pattern else {
644+
panic!("expected root node");
645+
};
646+
let Pattern::Sequence(children) = &fields[0].1 else {
647+
panic!("expected child sequence");
648+
};
649+
let Pattern::Repeated {
650+
pattern: repeated, ..
651+
} = &children[0]
652+
else {
653+
panic!("expected repetition");
654+
};
655+
assert!(matches!(
656+
repeated.as_ref(),
657+
Pattern::Sequence(patterns)
658+
if matches!(
659+
patterns.as_slice(),
660+
[
661+
Pattern::Any {
662+
match_unnamed: true
663+
},
664+
Pattern::Unnamed(comma)
665+
] if comma == ","
666+
)
667+
));
668+
}
669+
670+
#[test]
671+
fn rejects_unconsumed_node_pattern_tokens() {
672+
for query in [quote!((root (foo . (bar) @x))), quote!((root(bar @ x)))] {
673+
let result = parse_pattern(query);
674+
let Err(error) = result else {
675+
panic!("expected malformed node pattern to be rejected");
676+
};
677+
assert!(error.to_string().starts_with("unexpected token"));
678+
}
679+
}
596680
}

‎shared/yeast/doc/yeast.md‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -155,27 +155,28 @@ queries, are not supported.
155155
"+"* @operators // repeat and capture an unnamed token
156156
```
157157

158-
Parentheses are not general-purpose grouping syntax. A parenthesized query
159-
group represents a sequence of at least two sibling patterns. It may be
160-
repeated, but it cannot itself be captured because the sequence does not
161-
correspond to one AST node:
158+
When a parenthesized query starts with a node-kind identifier, it is always
159+
parsed as that node pattern, and every remaining token must belong to that
160+
node's fields or children. Otherwise, two or more sibling patterns form a
161+
sequence. A sequence may be repeated, but it cannot itself be captured because
162+
the sequence does not correspond to one AST node:
162163

163164
```rust
164165
((identifier) @items (integer) @items)* // explicit flattened captures
165166
((identifier) (integer))* @items // error: sequence capture
166167
```
167168

168-
Use the bare literal form when quantifying an unnamed token. The parenthesized
169-
form is valid as a standalone token pattern, but not as a redundant
170-
single-pattern group:
169+
Literal- and wildcard-led sequences are valid and unambiguous because neither
170+
can introduce node fields:
171171

172172
```rust
173-
"+"* // valid
174-
("+")* // error: remove the redundant parentheses
173+
("+" ",")* // repeat a two-token sequence
174+
(_ ",")* // repeat a wildcard followed by a comma
175175
```
176176

177-
Empty query groups are also rejected. This does not affect the empty output
178-
template `()`, which intentionally means “emit no replacement nodes.”
177+
Single-pattern and empty query groups are rejected as redundant or meaningless.
178+
This does not affect the empty output template `()`, which intentionally means
179+
“emit no replacement nodes.”
179180

180181
## Template language
181182

0 commit comments

Comments
 (0)