Skip to content

Commit 73ad826

Browse files
authored
Merge pull request #22016 from asgerf/commonast-rebased5
Unified/swift: new AST spec and Swift mappings
2 parents cc83856 + 89cd677 commit 73ad826

24 files changed

Lines changed: 5632 additions & 638 deletions

File tree

shared/yeast-macros/src/lib.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,19 @@ pub fn query(input: TokenStream) -> TokenStream {
4444
/// {expr} - embed a Rust expression returning Id
4545
/// {..expr} - splice an iterable of Id (in child/field position)
4646
/// field: {..expr} - splice into a named field
47+
/// {expr}.map(p -> tpl) - apply tpl to each element; splice result
48+
/// {expr}.reduce_left(f -> init, acc, e -> fold)
49+
/// - fold with per-element init; splice 0 or 1 result
4750
/// ```
4851
///
52+
/// Chain syntax after `{expr}` or `{..expr}`:
53+
/// - `.map(param -> template)` — one output node per input element.
54+
/// - `.reduce_left(first -> init, acc, elem -> fold)` — fold left; the first
55+
/// element is converted by `init`, subsequent elements are folded by `fold`
56+
/// with the accumulator bound to `acc`. An empty iterable yields nothing.
57+
/// - Chains always splice (the result is iterable).
58+
/// - Multiple chains can be chained, e.g. `.map(...).reduce_left(...)`.
59+
///
4960
/// Can be called with an explicit context or using the implicit context
5061
/// from an enclosing `rule!`:
5162
///

shared/yeast-macros/src/parse.rs

Lines changed: 147 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,12 @@ fn parse_query_fields(tokens: &mut Tokens) -> Result<Vec<TokenStream>> {
141141
// Parse the field's pattern. To support repetition like
142142
// `field: (kind)* @cap`, parse the atom first, then check for
143143
// a quantifier, and lastly handle a trailing `@capture`.
144-
let atom = parse_query_atom(tokens)?;
144+
// `field: @cap` is sugar for `field: _ @cap`.
145+
let atom = if peek_is_at(tokens) {
146+
quote! { yeast::query::QueryNode::Any { match_unnamed: true } }
147+
} else {
148+
parse_query_atom(tokens)?
149+
};
145150
if peek_is_repetition(tokens) {
146151
let rep = expect_repetition(tokens)?;
147152
let elem = quote! {
@@ -259,6 +264,7 @@ fn parse_query_list(tokens: &mut Tokens) -> Result<Vec<TokenStream>> {
259264
yeast::query::QueryListElem::SingleNode(#node)
260265
},
261266
)?;
267+
let elem = maybe_wrap_list_capture(tokens, elem)?;
262268
elems.push(elem);
263269
continue;
264270
}
@@ -276,6 +282,7 @@ fn parse_query_list(tokens: &mut Tokens) -> Result<Vec<TokenStream>> {
276282
yeast::query::QueryListElem::SingleNode(#node)
277283
},
278284
)?;
285+
let elem = maybe_wrap_list_capture(tokens, elem)?;
279286
elems.push(elem);
280287
continue;
281288
}
@@ -389,8 +396,10 @@ fn parse_direct_node_inner(tokens: &mut Tokens, ctx: &Ident) -> Result<TokenStre
389396
let expr = group.stream();
390397
return Ok(quote! {
391398
{
392-
let __value = yeast::YeastDisplay::yeast_to_string(&(#expr), &*#ctx.ast);
393-
#ctx.literal(#kind_str, &__value)
399+
let __expr = (#expr);
400+
let __value = yeast::YeastDisplay::yeast_to_string(&__expr, &*#ctx.ast);
401+
let __source_range = yeast::YeastSourceRange::yeast_source_range(&__expr, &*#ctx.ast);
402+
#ctx.literal_with_source_range(#kind_str, &__value, __source_range)
394403
}
395404
});
396405
}
@@ -419,23 +428,35 @@ fn parse_direct_node_inner(tokens: &mut Tokens, ctx: &Ident) -> Result<TokenStre
419428
);
420429
field_counter += 1;
421430

422-
// Check for field: {..expr} — splice a Vec<Id> into the field
431+
// Check for field: {..expr}.chain or field: {expr}.chain — splice a Vec<Id> into the field
423432
if peek_is_group(tokens, Delimiter::Brace) {
424433
let group_clone = tokens.clone().next().unwrap();
425434
if let TokenTree::Group(g) = &group_clone {
426435
let mut inner_check = g.stream().into_iter();
427436
let is_splice = matches!(inner_check.next(), Some(TokenTree::Punct(p)) if p.as_char() == '.')
428437
&& matches!(inner_check.next(), Some(TokenTree::Punct(p)) if p.as_char() == '.');
429-
if is_splice {
438+
// Determine if a chain (.map(..)) follows the `{}` group.
439+
let mut after = tokens.clone();
440+
after.next(); // skip the brace group
441+
let has_chain = matches!(after.peek(), Some(TokenTree::Punct(p)) if p.as_char() == '.');
442+
443+
if is_splice || has_chain {
430444
let group = expect_group(tokens, Delimiter::Brace)?;
431-
let mut inner = group.stream().into_iter().peekable();
432-
inner.next(); // consume first .
433-
inner.next(); // consume second .
434-
let expr: proc_macro2::TokenStream = inner.collect();
445+
let base: TokenStream = if is_splice {
446+
let mut inner = group.stream().into_iter().peekable();
447+
inner.next(); // consume first .
448+
inner.next(); // consume second .
449+
let expr: TokenStream = inner.collect();
450+
quote! {
451+
(#expr).into_iter().map(::std::convert::Into::<usize>::into)
452+
}
453+
} else {
454+
let expr = group.stream();
455+
quote! { (#expr).into_iter() }
456+
};
457+
let chained = parse_chain_suffix(tokens, ctx, base)?;
435458
stmts.push(quote! {
436-
let #temp: Vec<usize> = (#expr).into_iter()
437-
.map(::std::convert::Into::<usize>::into)
438-
.collect();
459+
let #temp: Vec<usize> = #chained.collect();
439460
});
440461
// An empty splice means the field is absent — skip it
441462
// entirely rather than emitting an empty named field.
@@ -472,6 +493,98 @@ fn parse_direct_node_inner(tokens: &mut Tokens, ctx: &Ident) -> Result<TokenStre
472493
})
473494
}
474495

496+
/// Parse a chain of `.method(args)` suffixes after a `{expr}` or `{..expr}`
497+
/// placeholder in tree templates. Currently supports:
498+
///
499+
/// ```text
500+
/// .map(param -> template) -- iterator map: produces Vec<usize>
501+
/// ```
502+
///
503+
/// The chain may be empty (returns `base` unchanged). Multiple chained calls
504+
/// are supported, e.g. `.map(p -> ...).map(q -> ...)`.
505+
///
506+
/// Each call expects the receiver to be an iterator. The `base` argument
507+
/// should therefore already be an iterator (use `.into_iter()` on it before
508+
/// calling this function).
509+
fn parse_chain_suffix(
510+
tokens: &mut Tokens,
511+
ctx: &Ident,
512+
base: TokenStream,
513+
) -> Result<TokenStream> {
514+
let mut current = base;
515+
while matches!(tokens.peek(), Some(TokenTree::Punct(p)) if p.as_char() == '.') {
516+
tokens.next(); // consume .
517+
let method = expect_ident(tokens, "expected method name after `.`")?;
518+
let method_str = method.to_string();
519+
let args_group = expect_group(tokens, Delimiter::Parenthesis)?;
520+
match method_str.as_str() {
521+
"map" => {
522+
let mut inner = args_group.stream().into_iter().peekable();
523+
let param = expect_ident(&mut inner, "expected lambda parameter name")?;
524+
expect_punct(&mut inner, '-', "expected `->` after lambda parameter")?;
525+
expect_punct(&mut inner, '>', "expected `->` after lambda parameter")?;
526+
let body = parse_direct_node(&mut inner, ctx)?;
527+
if let Some(tok) = inner.next() {
528+
return Err(syn::Error::new_spanned(
529+
tok,
530+
"unexpected token after lambda body",
531+
));
532+
}
533+
current = quote! {
534+
#current.map(|#param| #body)
535+
};
536+
}
537+
"reduce_left" => {
538+
// Syntax: reduce_left(first -> init_tpl, acc, elem -> fold_tpl)
539+
// - first -> init_tpl : converts the first element to the initial accumulator
540+
// - acc, elem -> fold_tpl : fold step (acc = current accumulator, elem = next element)
541+
// Empty iterator produces an empty iterator; non-empty produces a single-element iterator.
542+
let mut inner = args_group.stream().into_iter().peekable();
543+
let init_param = expect_ident(&mut inner, "expected initial lambda parameter")?;
544+
expect_punct(&mut inner, '-', "expected `->` after init parameter")?;
545+
expect_punct(&mut inner, '>', "expected `->` after init parameter")?;
546+
let init_body = parse_direct_node(&mut inner, ctx)?;
547+
expect_punct(&mut inner, ',', "expected `,` after init template")?;
548+
let acc_param = expect_ident(&mut inner, "expected accumulator parameter")?;
549+
expect_punct(&mut inner, ',', "expected `,` after accumulator parameter")?;
550+
let elem_param = expect_ident(&mut inner, "expected element parameter")?;
551+
expect_punct(&mut inner, '-', "expected `->` after element parameter")?;
552+
expect_punct(&mut inner, '>', "expected `->` after element parameter")?;
553+
let fold_body = parse_direct_node(&mut inner, ctx)?;
554+
if let Some(tok) = inner.next() {
555+
return Err(syn::Error::new_spanned(
556+
tok,
557+
"unexpected token after fold template",
558+
));
559+
}
560+
current = quote! {
561+
{
562+
let mut __iter = #current;
563+
let __result: Option<usize> = if let Some(#init_param) = __iter.next() {
564+
let mut __acc: usize = #init_body;
565+
for #elem_param in __iter {
566+
let #acc_param: usize = __acc;
567+
__acc = #fold_body;
568+
}
569+
Some(__acc)
570+
} else {
571+
None
572+
};
573+
__result.into_iter()
574+
}
575+
};
576+
}
577+
_ => {
578+
return Err(syn::Error::new_spanned(
579+
method,
580+
format!("unknown builtin method `.{method_str}()`"),
581+
));
582+
}
583+
}
584+
}
585+
Ok(current)
586+
}
587+
475588
/// Parse the top-level list of a `trees!` template.
476589
/// Each item is a node template or `{expr}` splice.
477590
fn parse_direct_list(tokens: &mut Tokens, ctx: &Ident) -> Result<Vec<TokenStream>> {
@@ -492,18 +605,27 @@ fn parse_direct_list(tokens: &mut Tokens, ctx: &Ident) -> Result<Vec<TokenStream
492605
continue;
493606
}
494607

495-
// {expr} or {..expr} — single node or splice
608+
// {expr} or {..expr} (with optional .chain) — single node or splice
496609
if peek_is_group(tokens, Delimiter::Brace) {
497610
let group = expect_group(tokens, Delimiter::Brace)?;
611+
let has_chain = matches!(tokens.peek(), Some(TokenTree::Punct(p)) if p.as_char() == '.');
498612
let mut inner = group.stream().into_iter().peekable();
499-
if peek_is_dotdot(&inner) {
500-
inner.next(); // consume first .
501-
inner.next(); // consume second .
502-
let expr: TokenStream = inner.collect();
503-
items.push(quote! {
504-
__nodes.extend(
613+
let is_splice = peek_is_dotdot(&inner);
614+
if is_splice || has_chain {
615+
let base: TokenStream = if is_splice {
616+
inner.next(); // consume first .
617+
inner.next(); // consume second .
618+
let expr: TokenStream = inner.collect();
619+
quote! {
505620
(#expr).into_iter().map(::std::convert::Into::<usize>::into)
506-
);
621+
}
622+
} else {
623+
let expr = group.stream();
624+
quote! { (#expr).into_iter() }
625+
};
626+
let chained = parse_chain_suffix(tokens, ctx, base)?;
627+
items.push(quote! {
628+
__nodes.extend(#chained);
507629
});
508630
} else {
509631
let expr = group.stream();
@@ -604,8 +726,11 @@ fn extract_captures_inner(
604726
}
605727
last_mult = CaptureMultiplicity::Single;
606728
}
607-
TokenTree::Punct(p) if matches!(p.as_char(), '*' | '+' | '?') => {
608-
// Keep last_mult — the @capture follows
729+
TokenTree::Punct(p) if p.as_char() == '*' || p.as_char() == '+' => {
730+
last_mult = CaptureMultiplicity::Repeated;
731+
}
732+
TokenTree::Punct(p) if p.as_char() == '?' => {
733+
last_mult = CaptureMultiplicity::Optional;
609734
}
610735
_ => {
611736
last_mult = CaptureMultiplicity::Single;

shared/yeast/src/build.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,34 @@ impl<'a> BuildCtx<'a> {
8282
.create_named_token_with_range(kind, value.to_string(), self.source_range)
8383
}
8484

85+
/// Create a leaf node with fixed content and an optional preferred source range.
86+
/// If `source_range` is `None`, falls back to this context's inherited range.
87+
pub fn literal_with_source_range(
88+
&mut self,
89+
kind: &'static str,
90+
value: &str,
91+
source_range: Option<tree_sitter::Range>,
92+
) -> Id {
93+
self.ast.create_named_token_with_range(
94+
kind,
95+
value.to_string(),
96+
source_range.or(self.source_range),
97+
)
98+
}
99+
85100
/// Create a leaf node with an auto-generated unique name.
86101
pub fn fresh(&mut self, kind: &'static str, name: &str) -> Id {
87102
let generated = self.fresh.resolve(name);
88103
self.ast
89104
.create_named_token_with_range(kind, generated, self.source_range)
90105
}
106+
107+
/// Prepend a value to a field of an existing node.
108+
pub fn prepend_field(&mut self, node_id: Id, field_name: &str, value_id: Id) {
109+
let field_id = self
110+
.ast
111+
.field_id_for_name(field_name)
112+
.unwrap_or_else(|| panic!("build: field '{field_name}' not found"));
113+
self.ast.prepend_field_child(node_id, field_id, value_id);
114+
}
91115
}

0 commit comments

Comments
 (0)