Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 37 additions & 9 deletions shared/yeast-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,10 @@ pub fn query(input: TokenStream) -> TokenStream {
/// error, so the choice between "unset the field" and "unwrap it" stays
/// explicit.
///
/// Can be called with an explicit context or using the implicit context
/// from an enclosing `rule!`:
/// Uses the `BuildCtx` binding named `ctx` from the surrounding scope:
///
/// ```text
/// tree!(ctx, (kind ...)) // explicit BuildCtx
/// tree!((kind ...)) // implicit context from rule!
/// tree!((kind ...))
/// ```
#[proc_macro]
pub fn tree(input: TokenStream) -> TokenStream {
Expand All @@ -96,12 +94,10 @@ pub fn tree(input: TokenStream) -> TokenStream {
/// Like `tree!` but returns `Vec<Id>` and supports multiple top-level
/// elements. All syntax from `tree!` is available.
///
/// Can be called with an explicit context or using the implicit context
/// from an enclosing `rule!`:
/// Uses the `BuildCtx` binding named `ctx` from the surrounding scope:
///
/// ```text
/// trees!(ctx, (node1 ...) (node2 ...)) // explicit BuildCtx
/// trees!((node1 ...) (node2 ...)) // implicit context from rule!
/// trees!((node1 ...) (node2 ...))
/// ```
#[proc_macro]
pub fn trees(input: TokenStream) -> TokenStream {
Expand All @@ -112,6 +108,38 @@ pub fn trees(input: TokenStream) -> TokenStream {
}
}

/// Build one AST node whose root uses another node's source range.
///
/// Uses the `BuildCtx` binding named `ctx` from the surrounding scope:
///
/// ```text
/// tree_at!(source, (kind ...))
/// ```
#[proc_macro]
pub fn tree_at(input: TokenStream) -> TokenStream {
let input2: TokenStream2 = input.into();
match parse::parse_tree_at_top(input2) {
Ok(output) => output.into(),
Err(err) => err.to_compile_error().into(),
}
}

/// Build one AST node whose root spans a collection of nodes.
///
/// Uses the `BuildCtx` binding named `ctx` from the surrounding scope:
///
/// ```text
/// tree_spanning!(sources, (kind ...))
/// ```
#[proc_macro]
pub fn tree_spanning(input: TokenStream) -> TokenStream {
let input2: TokenStream2 = input.into();
match parse::parse_tree_spanning_top(input2) {
Ok(output) => output.into(),
Err(err) => err.to_compile_error().into(),
}
}

/// Define a desugaring rule with query and transform in one declaration.
///
/// ```text
Expand Down Expand Up @@ -146,7 +174,7 @@ pub fn trees(input: TokenStream) -> TokenStream {
/// Mutations to `ctx` are visible to the transform when the guard succeeds.
/// Omitting the guard is equivalent to writing `where true`.
///
/// `tree!` and `trees!` can be used without explicit context inside `{...}`.
/// `tree!` and `trees!` use the rule transform's `ctx` binding inside `{...}`.
#[proc_macro]
pub fn rule(input: TokenStream) -> TokenStream {
let input2: TokenStream2 = input.into();
Expand Down
101 changes: 78 additions & 23 deletions shared/yeast-macros/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ use proc_macro2::{Delimiter, Ident, Literal, Span, TokenStream, TokenTree};
use quote::quote;
use std::iter::Peekable;
use std::sync::atomic::{AtomicUsize, Ordering};
use syn::Lifetime;
use syn::{
Expr, Lifetime, Token,
parse::{Parse, ParseStream},
};

type Tokens = Peekable<proc_macro2::token_stream::IntoIter>;
type Result<T> = std::result::Result<T, syn::Error>;
Expand Down Expand Up @@ -332,28 +335,10 @@ fn parse_query_list(tokens: &mut Tokens) -> Result<Vec<TokenStream>> {

const IMPLICIT_CTX: &str = "ctx";

/// Determine the context identifier: either explicit `ctx,` or the implicit
/// `ctx` from an enclosing `rule!`.
fn parse_ctx_or_implicit(tokens: &mut Tokens) -> Ident {
// Check if first token is an ident followed by a comma
let mut lookahead = tokens.clone();
let is_explicit = matches!(lookahead.next(), Some(TokenTree::Ident(_)))
&& matches!(lookahead.next(), Some(TokenTree::Punct(p)) if p.as_char() == ',');

if is_explicit {
let ctx = expect_ident(tokens, "unreachable: ident was just peeked")
.expect("unreachable: ident was just peeked");
let _ = tokens.next(); // consume comma
ctx
} else {
Ident::new(IMPLICIT_CTX, Span::call_site())
}
}

/// Parse `tree!(ctx, (template))` or `tree!((template))` — returns single `Id`.
/// Parse `tree!((template))` — returns single `Id`.
pub fn parse_tree_top(input: TokenStream) -> Result<TokenStream> {
let mut tokens = input.into_iter().peekable();
let ctx = parse_ctx_or_implicit(&mut tokens);
let ctx = Ident::new(IMPLICIT_CTX, Span::call_site());

let first = parse_direct_node(&mut tokens, &ctx, None)?;

Expand All @@ -368,10 +353,10 @@ pub fn parse_tree_top(input: TokenStream) -> Result<TokenStream> {
Ok(quote! { { #first } })
}

/// Parse `trees!(ctx, ...)` or `trees!(...)` — returns `Vec<Id>`.
/// Parse `trees!(...)` — returns `Vec<Id>`.
pub fn parse_trees_top(input: TokenStream) -> Result<TokenStream> {
let mut tokens = input.into_iter().peekable();
let ctx = parse_ctx_or_implicit(&mut tokens);
let ctx = Ident::new(IMPLICIT_CTX, Span::call_site());
let items = parse_direct_list(&mut tokens, &ctx)?;
if let Some(tok) = tokens.next() {
return Err(syn::Error::new_spanned(
Expand All @@ -388,6 +373,76 @@ pub fn parse_trees_top(input: TokenStream) -> Result<TokenStream> {
})
}

pub fn parse_tree_at_top(input: TokenStream) -> Result<TokenStream> {
let LocatedTreeInput {
source,
template,
} = syn::parse2(input)?;
let mut tokens = template.into_iter().peekable();
let ctx = Ident::new(IMPLICIT_CTX, Span::call_site());
let node = parse_direct_node(&mut tokens, &ctx, None)?;
if let Some(tok) = tokens.next() {
return Err(syn::Error::new_spanned(
tok,
"unexpected token after tree_at! template",
));
}

Ok(quote! {
{
let __yeast_source: yeast::Id = { #source };
let __yeast_source_range = #ctx
.ast
.get_node(__yeast_source)
.and_then(|node| node.source_range());
let __yeast_node: yeast::Id = #node;
#ctx.set_node_source_range(__yeast_node, __yeast_source_range)
}
})
}

pub fn parse_tree_spanning_top(input: TokenStream) -> Result<TokenStream> {
let LocatedTreeInput {
source: sources,
template,
} = syn::parse2(input)?;
let mut tokens = template.into_iter().peekable();
let ctx = Ident::new(IMPLICIT_CTX, Span::call_site());
let node = parse_direct_node(&mut tokens, &ctx, None)?;
if let Some(tok) = tokens.next() {
return Err(syn::Error::new_spanned(
tok,
"unexpected token after tree_spanning! template",
));
}

Ok(quote! {
{
let __yeast_source_range = ::std::iter::IntoIterator::into_iter({ #sources })
.filter_map(|source: yeast::Id| {
#ctx.ast.get_node(source).and_then(|node| node.source_range())
})
.reduce(yeast::Range::union);
let __yeast_node: yeast::Id = #node;
#ctx.set_node_source_range(__yeast_node, __yeast_source_range)
}
})
}

struct LocatedTreeInput {
source: Expr,
template: TokenStream,
}

impl Parse for LocatedTreeInput {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let source = input.parse()?;
input.parse::<Token![,]>()?;
let template = input.parse()?;
Ok(Self { source, template })
}
}

/// Parse a single node template and generate code that returns an `Id`.
/// Handles: `(kind fields... children...)` and `{expr}`.
///
Expand Down
19 changes: 8 additions & 11 deletions shared/yeast/doc/yeast.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,22 +160,21 @@ Templates construct new AST nodes using the `tree!` and `trees!` macros.
All children in a template must be in named fields — output AST nodes are
always fully fielded.

When used inside a `rule!` macro, the context is implicit — no explicit
`BuildCtx` argument is needed. When used standalone, they take a `BuildCtx`
as the first argument:
The macros use a `BuildCtx` binding named `ctx` from the surrounding scope.
`rule!` provides this binding automatically; standalone uses must create it:

```rust
// Inside rule! — implicit context, captures are Rust variables
// Inside rule! — ctx is provided automatically, captures are Rust variables
yeast::rule!(
(assignment left: (_) @left right: (_) @right)
=>
(assignment left: {right} right: {left})
);

// Standalone — explicit context
// Standalone — create a binding named ctx
let mut user_ctx = ();
let mut ctx = BuildCtx::new(ast, &captures, &mut user_ctx);
let id = yeast::tree!(ctx,
let id = yeast::tree!(
(assignment
left: {ctx.capture("lhs")}
right: {ctx.capture("rhs")}
Expand All @@ -188,7 +187,7 @@ let id = yeast::tree!(ctx,
`tree!(...)` returns a single node `Id`:

```rust
yeast::tree!(ctx,
yeast::tree!(
(assignment
left: {ctx.capture("lhs")}
right: {ctx.capture("rhs")}
Expand All @@ -201,7 +200,7 @@ yeast::tree!(ctx,
`trees!(...)` returns `Vec<Id>`:

```rust
yeast::trees!(ctx,
yeast::trees!(
(assignment left: {tmp} right: {right})
{body}
)
Expand Down Expand Up @@ -262,7 +261,6 @@ rule!(
=>
synthetic_node {
tree_at!(
ctx,
source_node,
(synthetic_node child: (nested value: {child}))
)
Expand All @@ -282,7 +280,6 @@ rule!(
=>
synthetic_node {
tree_spanning!(
ctx,
[first, second],
(synthetic_node child: {child})
)
Expand Down Expand Up @@ -382,7 +379,7 @@ options uniformly:
right: {rhs} // a captured value (inside rule!)
)

yeast::trees!(ctx,
yeast::trees!(
(assignment left: {tmp} right: {right})
{extra_nodes} // splices a Vec<Id>
)
Expand Down
36 changes: 1 addition & 35 deletions shared/yeast/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,41 +15,7 @@ pub mod schema;
mod visitor;

pub use range::{Point, Range};
pub use yeast_macros::{query, rule, rules, tree, trees};

/// Build a single AST node whose root uses another node's source range.
///
/// Nested nodes in the template are built normally and derive their locations
/// from their own children.
#[macro_export]
macro_rules! tree_at {
($ctx:ident, $source:expr, ($($tree:tt)*)) => {{
let __yeast_source: $crate::Id = $source;
let __yeast_source_range = $ctx
.ast
.get_node(__yeast_source)
.and_then(|node| node.source_range());
let __yeast_node: $crate::Id = $crate::tree!($ctx, ($($tree)*));
$ctx.set_node_source_range(__yeast_node, __yeast_source_range)
}};
}

/// Build a single AST node whose root spans a collection of nodes.
///
/// Nested nodes in the template are built normally and derive their locations
/// from their own children.
#[macro_export]
macro_rules! tree_spanning {
($ctx:ident, $sources:expr, ($($tree:tt)*)) => {{
let __yeast_source_range = ::std::iter::IntoIterator::into_iter($sources)
.filter_map(|source: $crate::Id| {
$ctx.ast.get_node(source).and_then(|node| node.source_range())
})
.reduce($crate::Range::union);
let __yeast_node: $crate::Id = $crate::tree!($ctx, ($($tree)*));
$ctx.set_node_source_range(__yeast_node, __yeast_source_range)
}};
}
pub use yeast_macros::{query, rule, rules, tree, tree_at, tree_spanning, trees};

use captures::Captures;
use query::QueryNode;
Expand Down
15 changes: 7 additions & 8 deletions shared/yeast/tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -695,7 +695,7 @@ fn test_tree_builder() {
// Swap left and right
let mut user_ctx = ();
let mut ctx = yeast::build::BuildCtx::new(&mut ast, &captures, &mut user_ctx);
let new_id = yeast::tree!(ctx,
let new_id = yeast::tree!(
(program
child: (assignment
left: {ctx.capture("right")}
Expand Down Expand Up @@ -723,8 +723,8 @@ fn build_optional_right(ast: &mut Ast, value: Option<yeast::Id>) -> (yeast::Id,
let captures = yeast::captures::Captures::new();
let mut user_ctx = ();
let mut ctx = yeast::build::BuildCtx::new(ast, &captures, &mut user_ctx);
let left = yeast::tree!(ctx, (identifier "x"));
let root = yeast::tree!(ctx,
let left = yeast::tree!((identifier "x"));
let root = yeast::tree!(
(assignment
left: {left}
right: (integer #{value})?
Expand Down Expand Up @@ -781,8 +781,8 @@ fn test_optional_field_propagates_through_nested_nodes() {
// The absent value sits two levels below the `?`, so the whole
// `left_assignment_list` subtree is abandoned along with it.
let absent: Option<yeast::Id> = None;
let right = yeast::tree!(ctx, (integer "1"));
let root = yeast::tree!(ctx,
let right = yeast::tree!((integer "1"));
let root = yeast::tree!(
(assignment
left: (left_assignment_list child: (identifier #{absent}))?
right: {right}
Expand All @@ -809,7 +809,7 @@ fn test_innermost_optional_field_catches_first() {

// The inner `?` catches, so only `child` is dropped; `left` survives.
let absent: Option<yeast::Id> = None;
let root = yeast::tree!(ctx,
let root = yeast::tree!(
(assignment
left: (left_assignment_list child: (identifier #{absent})?)?
)
Expand Down Expand Up @@ -1782,7 +1782,7 @@ fn test_tree_at_assigns_capture_range_to_root_only() {
) @@source
=>
call {
let arguments = tree_at!(ctx, source, (argument_list argument: (integer "0")));
let arguments = tree_at!(source, (argument_list argument: (integer "0")));
tree!((call method: {name} receiver: {recv} arguments: {arguments}))
}
);
Expand Down Expand Up @@ -1817,7 +1817,6 @@ fn test_tree_spanning_assigns_union_to_root_only() {
=>
call {
let arguments = tree_spanning!(
ctx,
[recv, name],
(argument_list argument: (integer "0"))
);
Expand Down
Loading
Loading