Skip to content

Commit c0a00a9

Browse files
committed
yeast: Get rid of postfix ? operator
This will be superseded by the next commit, which generalised the mechanism and also makes the implementation simpler.
1 parent 052d686 commit c0a00a9

7 files changed

Lines changed: 24 additions & 395 deletions

File tree

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,15 +121,13 @@ pub(crate) enum Content {
121121

122122
pub(crate) enum Field {
123123
/// A named field such as `argument: {args}` or
124-
/// `label: (identifier #{name})?`.
124+
/// `label: (identifier #{name})`.
125125
Named {
126126
/// The field name before `:`, such as `argument`.
127127
name: Ident,
128128
/// The node or Rust splice after `:`, such as `(identifier "x")` or
129129
/// `{args}`.
130130
value: Template,
131-
/// Whether the field value has a trailing `?`.
132-
optional: bool,
133131
},
134132
/// Source-location metadata such as `location: {source}`.
135133
Location {

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

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -63,37 +63,10 @@ pub fn query(input: TokenStream) -> TokenStream {
6363
/// `Option<OutputId>`, iterator chains) splice
6464
/// their elements
6565
/// field: {expr} - extend a named field with `{expr}`'s ids
66-
/// field: (kind ...)? - set the field only if every `#{expr}`
67-
/// beneath it has a value (see below)
6866
/// location: {expr} - override this node's source range with the
6967
/// span of one or more nodes; may be repeated
7068
/// ```
7169
///
72-
/// # Optional fields
73-
///
74-
/// A `?` on a field's value makes that field fallible: if a `#{expr}`
75-
/// anywhere beneath it interpolates an absent value — an `Option` that is
76-
/// `None` — the subtree is abandoned and the field is left unset. This
77-
/// replaces the surrounding `Option::map` that would otherwise be needed:
78-
///
79-
/// ```text
80-
/// (break_expr label: {lbl.map(|l| tree!((identifier #{l})))}) // before
81-
/// (break_expr label: (identifier #{lbl})?) // after
82-
/// ```
83-
///
84-
/// The value may be nested arbitrarily deeply, and a nested `?` catches
85-
/// first, so an inner absent value need not discard the outer node:
86-
///
87-
/// ```text
88-
/// (parameter pattern: (name_pattern identifier: (identifier #{name}))? type: {ty})
89-
/// ```
90-
///
91-
/// Only `#{expr}` propagates absence. A `{expr}` splice is unaffected, since
92-
/// yielding no ids already leaves a field unset, and `?` is rejected on one.
93-
/// Outside a `?`, interpolating an `Option` with `#{expr}` remains a compile
94-
/// error, so the choice between "unset the field" and "unwrap it" stays
95-
/// explicit.
96-
///
9770
/// Uses the `BuildCtx` binding named `ctx` from the surrounding scope:
9871
///
9972
/// ```text

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

Lines changed: 19 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,8 @@
11
use proc_macro2::{Ident, Span, TokenStream};
22
use quote::quote;
3-
use std::sync::atomic::{AtomicUsize, Ordering};
4-
use syn::Lifetime;
53

64
use crate::ast::{Cardinality, Content, Field, Node, Pattern, Replacement, Rule, Template};
75

8-
struct LoweringContext<'a> {
9-
/// Enclosing optional-field label to exit when a computed value is absent.
10-
fallible_scope: Option<&'a Lifetime>,
11-
}
12-
13-
fn fresh_fallible_label() -> Lifetime {
14-
static COUNTER: AtomicUsize = AtomicUsize::new(0);
15-
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
16-
Lifetime::new(&format!("'__yeast_field_{n}"), Span::call_site())
17-
}
18-
196
#[derive(Clone)]
207
struct CaptureBinding {
218
name: Ident,
@@ -286,20 +273,15 @@ fn capture_bindings<'a>(
286273

287274
impl Template {
288275
pub(crate) fn lower_root(&self) -> TokenStream {
289-
self.lower(&LoweringContext {
290-
fallible_scope: None,
291-
})
276+
self.lower()
292277
}
293278

294279
pub(crate) fn lower_list(templates: &[Self]) -> TokenStream {
295-
let context = LoweringContext {
296-
fallible_scope: None,
297-
};
298280
let items = templates
299281
.iter()
300282
.map(|template| match template {
301283
Template::Node(node) => {
302-
let node = node.lower(&context);
284+
let node = node.lower();
303285
quote! { __nodes.push(#node); }
304286
}
305287
Template::Splice(block) => {
@@ -322,9 +304,9 @@ impl Template {
322304
}
323305
}
324306

325-
fn lower(&self, context: &LoweringContext<'_>) -> TokenStream {
307+
fn lower(&self) -> TokenStream {
326308
match self {
327-
Template::Node(node) => node.lower(context),
309+
Template::Node(node) => node.lower(),
328310
Template::Splice(block) => {
329311
let block = rust_block(block);
330312
quote! { ::std::convert::Into::<yeast::OutputId>::into(#block) }
@@ -334,7 +316,7 @@ impl Template {
334316
}
335317

336318
impl Node {
337-
fn lower(&self, context: &LoweringContext<'_>) -> TokenStream {
319+
fn lower(&self) -> TokenStream {
338320
let ctx = Ident::new("ctx", Span::call_site());
339321
let kind_str = self.kind.to_string();
340322

@@ -343,40 +325,18 @@ impl Node {
343325
Content::Static(lit) => quote! { #ctx.literal(#kind_str, #lit) },
344326
Content::Computed(block) => {
345327
let block = rust_block(block);
346-
if let Some(label) = context.fallible_scope {
347-
quote! {
348-
{
349-
let __expr = #block;
350-
let ::std::option::Option::Some(__value_ref) =
351-
yeast::MaybeYeastValue::maybe_yeast_value(&__expr)
352-
else {
353-
break #label ::std::option::Option::None;
354-
};
355-
let __value =
356-
yeast::YeastDisplay::yeast_to_string(__value_ref, &#ctx);
357-
let __source_range =
358-
yeast::YeastSourceRange::yeast_source_range(__value_ref, &#ctx);
359-
#ctx.literal_with_source_range(
360-
#kind_str,
361-
&__value,
362-
__source_range,
363-
)
364-
}
365-
}
366-
} else {
367-
quote! {
368-
{
369-
let __expr = #block;
370-
let __value =
371-
yeast::YeastDisplay::yeast_to_string(&__expr, &#ctx);
372-
let __source_range =
373-
yeast::YeastSourceRange::yeast_source_range(&__expr, &#ctx);
374-
#ctx.literal_with_source_range(
375-
#kind_str,
376-
&__value,
377-
__source_range,
378-
)
379-
}
328+
quote! {
329+
{
330+
let __expr = #block;
331+
let __value =
332+
yeast::YeastDisplay::yeast_to_string(&__expr, &#ctx);
333+
let __source_range =
334+
yeast::YeastSourceRange::yeast_source_range(&__expr, &#ctx);
335+
#ctx.literal_with_source_range(
336+
#kind_str,
337+
&__value,
338+
__source_range,
339+
)
380340
}
381341
}
382342
}
@@ -399,11 +359,7 @@ impl Node {
399359
#ctx.extend_location_ranges(__value, &mut __location_ranges);
400360
});
401361
}
402-
Field::Named {
403-
name,
404-
value,
405-
optional,
406-
} => {
362+
Field::Named { name, value } => {
407363
let field_name = name.to_string();
408364
let field_str = field_name
409365
.strip_prefix("r#")
@@ -413,10 +369,8 @@ impl Node {
413369
&format!("__field_{field_str}_{field_counter}"),
414370
Span::call_site(),
415371
);
416-
417372
match value {
418373
Template::Splice(block) => {
419-
debug_assert!(!optional);
420374
let block = rust_block(block);
421375
stmts.push(quote! {
422376
let mut #temp: Vec<yeast::OutputId> = Vec::new();
@@ -430,25 +384,8 @@ impl Node {
430384
}
431385
});
432386
}
433-
Template::Node(child) if *optional => {
434-
let label = fresh_fallible_label();
435-
let value = child.lower(&LoweringContext {
436-
fallible_scope: Some(&label),
437-
});
438-
stmts.push(quote! {
439-
#[allow(unused_labels)]
440-
let #temp: ::std::option::Option<yeast::OutputId> = #label: {
441-
::std::option::Option::Some(#value)
442-
};
443-
});
444-
field_args.push(quote! {
445-
if let ::std::option::Option::Some(__id) = #temp {
446-
__fields.push((#field_str, vec![__id]));
447-
}
448-
});
449-
}
450387
Template::Node(child) => {
451-
let value = child.lower(context);
388+
let value = child.lower();
452389
stmts.push(quote! { let #temp: yeast::OutputId = #value; });
453390
field_args.push(quote! {
454391
__fields.push((#field_str, vec![#temp]));

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

Lines changed: 2 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ type Result<T> = std::result::Result<T, syn::Error>;
99
pub(crate) fn parse_template(input: TokenStream) -> Result<Template> {
1010
let mut tokens = input.into_iter().peekable();
1111
let template = parse_one(&mut tokens)?;
12-
reject_stray_optional(&mut tokens, false)?;
1312
if let Some(token) = tokens.next() {
1413
return Err(syn::Error::new_spanned(
1514
token,
@@ -93,33 +92,12 @@ fn parse_node(group: proc_macro2::Group) -> Result<Node> {
9392
return Err(error);
9493
}
9594
let block = parse_block(expect_group(&mut tokens, Delimiter::Brace)?)?;
96-
if let Some(TokenTree::Punct(punctuation)) = tokens.peek() {
97-
if punctuation.as_char() == '?' {
98-
return Err(syn::Error::new_spanned(
99-
punctuation.clone(),
100-
"`?` is not valid on a `location` field",
101-
));
102-
}
103-
}
10495
fields.push(Field::Location { block });
10596
continue;
10697
}
10798

10899
let value = parse_one(&mut tokens)?;
109-
let optional = if matches!(value, Template::Splice(_)) {
110-
reject_stray_optional(&mut tokens, true)?;
111-
false
112-
} else if peek_is_punct(&mut tokens, '?') {
113-
tokens.next();
114-
true
115-
} else {
116-
false
117-
};
118-
fields.push(Field::Named {
119-
name,
120-
value,
121-
optional,
122-
});
100+
fields.push(Field::Named { name, value });
123101
}
124102

125103
if let Some(token) = tokens.next() {
@@ -146,7 +124,6 @@ fn parse_list(tokens: &mut Tokens) -> Result<Vec<Template>> {
146124
continue;
147125
}
148126
templates.push(Template::Node(parse_node(group)?));
149-
reject_stray_optional(tokens, false)?;
150127
continue;
151128
}
152129

@@ -155,7 +132,6 @@ fn parse_list(tokens: &mut Tokens) -> Result<Vec<Template>> {
155132
tokens,
156133
Delimiter::Brace,
157134
)?)?));
158-
reject_stray_optional(tokens, true)?;
159135
continue;
160136
}
161137

@@ -164,23 +140,6 @@ fn parse_list(tokens: &mut Tokens) -> Result<Vec<Template>> {
164140
Ok(templates)
165141
}
166142

167-
fn reject_stray_optional(tokens: &mut Tokens, after_splice: bool) -> Result<()> {
168-
let Some(TokenTree::Punct(punctuation)) = tokens.peek() else {
169-
return Ok(());
170-
};
171-
if punctuation.as_char() != '?' {
172-
return Ok(());
173-
}
174-
let message = if after_splice {
175-
"`?` is not valid on a `{...}` splice; a splice that yields no value \
176-
already leaves its field unset"
177-
} else {
178-
"`?` is only valid on the value of a named field, as in \
179-
`label: (identifier #{lbl})?`"
180-
};
181-
Err(syn::Error::new_spanned(punctuation.clone(), message))
182-
}
183-
184143
fn parse_block(group: proc_macro2::Group) -> Result<syn::Block> {
185144
syn::parse2(TokenStream::from(TokenTree::Group(group)))
186145
}
@@ -201,10 +160,6 @@ fn peek_is_group(tokens: &mut Tokens, delimiter: Delimiter) -> bool {
201160
matches!(tokens.peek(), Some(TokenTree::Group(group)) if group.delimiter() == delimiter)
202161
}
203162

204-
fn peek_is_punct(tokens: &mut Tokens, expected: char) -> bool {
205-
matches!(tokens.peek(), Some(TokenTree::Punct(punctuation)) if punctuation.as_char() == expected)
206-
}
207-
208163
fn expect_ident(tokens: &mut Tokens, message: &str) -> Result<Ident> {
209164
match tokens.next() {
210165
Some(TokenTree::Ident(identifier)) => Ok(identifier),
@@ -256,7 +211,7 @@ mod tests {
256211
let template = parse_template(quote! {
257212
(call
258213
argument: {args}
259-
method: (identifier #{name})?)
214+
method: (identifier #{name}))
260215
})
261216
.unwrap();
262217

@@ -297,17 +252,6 @@ mod tests {
297252
);
298253
}
299254

300-
#[test]
301-
fn rejects_optional_location_fields() {
302-
let result = parse_template(quote! {
303-
(identifier "name" location: {source}?)
304-
});
305-
let Err(error) = result else {
306-
panic!("expected an optional location field to be rejected");
307-
};
308-
assert_eq!(error.to_string(), "`?` is not valid on a `location` field");
309-
}
310-
311255
#[test]
312256
fn parses_lists_and_skips_empty_nodes() {
313257
let templates = parse_templates(quote! {

‎shared/yeast/doc/yeast.md‎

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -331,48 +331,6 @@ accessor_declaration source="⟨accessor_kind⟩"
331331
Node and child ranges are still validated against the source text and UTF-8
332332
boundaries.
333333

334-
### Optional fields (`?`)
335-
336-
A `?` on a field's value makes that field fallible. If a `#{expr}` anywhere
337-
beneath it interpolates an absent value — an `Option` that is `None` — the
338-
subtree is abandoned and the field is left unset:
339-
340-
```rust
341-
rule!((breakStmt label: _? @@lbl) => (break_expr label: (identifier #{lbl})?))
342-
```
343-
344-
Here an optional capture is being wrapped in a leaf, which without `?` needs
345-
an `Option::map` to build the leaf only in the `Some` case:
346-
347-
```rust
348-
// Equivalent, but the intent is buried in the closure:
349-
(break_expr label: {lbl.map(|l| tree!((identifier #{l})))})
350-
```
351-
352-
The marker mirrors the query language, where a quantifier likewise follows the
353-
value it applies to (`label: _? @@lbl`). Note that the schema puts it on the
354-
other side of the colon — `external_name?: identifier` — because it is
355-
*declaring* a field's cardinality rather than supplying a value for it.
356-
357-
Absence propagates outwards through as many levels as necessary, and a nested
358-
`?` catches first, so an inner absent value need not discard the outer node:
359-
360-
```rust
361-
// If `name` is absent, `pattern` is left unset — but `type` is still set.
362-
(parameter pattern: (name_pattern identifier: (identifier #{name}))? type: {ty})
363-
```
364-
365-
Only `#{expr}` propagates absence, because it supplies a node's *content*: with
366-
no value there is no leaf to build. A `{expr}` splice supplies *children*, where
367-
yielding nothing already leaves the field unset (see below), so `?` is rejected
368-
on one. Repetition has no marker at all, in templates or elsewhere: how many
369-
children a `{expr}` contributes is a property of the expression rather than of
370-
the syntax.
371-
372-
Outside a `?`, interpolating an `Option` with `#{expr}` remains a compile error.
373-
That is deliberate: it keeps the choice between "leave the field unset" and
374-
"unwrap it" explicit at every interpolation.
375-
376334
### Embedded Rust expressions
377335

378336
`{expr}` embeds a Rust expression whose value is appended to the

0 commit comments

Comments
 (0)