diff --git a/shared/yeast-macros/src/lib.rs b/shared/yeast-macros/src/lib.rs index ddace36e7b1..15f61a377c7 100644 --- a/shared/yeast-macros/src/lib.rs +++ b/shared/yeast-macros/src/lib.rs @@ -8,16 +8,14 @@ mod parse; /// # Syntax /// /// ```text -/// (_) - match any node +/// (_) - match any named node (skips unnamed tokens) /// (kind) - match a named node of the given kind -/// ("literal") - match an unnamed node (e.g. operators, keywords) +/// ("literal") - match an unnamed token by its text /// (kind field: (pattern)) - match with named field -/// (kind child*: (patterns...)) - match unnamed children (yeast-specific) +/// (kind (pat) (pat)...) - match unnamed children (after all fields) /// (pattern) @capture - capture the matched node -/// @capture - capture any node (shorthand for (_) @capture) -/// (pat1 pat2)* - zero or more repetitions -/// (pat1 pat2)+ - one or more repetitions -/// (pat1 pat2)? - zero or one repetitions +/// (pattern)* @capture - capture each repeated match +/// (pattern)? - zero or one /// ``` #[proc_macro] pub fn query(input: TokenStream) -> TokenStream { @@ -30,6 +28,16 @@ pub fn query(input: TokenStream) -> TokenStream { /// Build a single AST node from a template, returning its `Id`. /// +/// # Template syntax +/// +/// ```text +/// (kind field: @capture) - node with captured child +/// (kind "literal") - leaf with static content +/// (kind #{expr}) - leaf with computed content (expr.to_string()) +/// (kind $fresh) - leaf with auto-generated unique name +/// {expr} - embed a Rust expression returning Id +/// ``` +/// /// Can be called with an explicit context or using the implicit context /// from an enclosing `rule!`: /// @@ -48,6 +56,13 @@ pub fn tree(input: TokenStream) -> TokenStream { /// Build a list of AST nodes from a template, returning `Vec`. /// +/// Supports all syntax from `tree!`, plus: +/// +/// ```text +/// {..expr} - splice an iterable of Id +/// (@capture)* - splice a repeated capture +/// ``` +/// /// Can be called with an explicit context or using the implicit context /// from an enclosing `rule!`: /// @@ -68,21 +83,21 @@ pub fn trees(input: TokenStream) -> TokenStream { /// /// ```text /// rule!( -/// (query_pattern -/// field: (_) @capture_name -/// (kind)* @repeated -/// ) +/// (query_pattern field: (_) @name (kind)* @repeated (_)? @optional) /// => -/// (output_template -/// field: {capture_name} -/// {..repeated} -/// ) +/// (output_template field: {name} {..repeated}) /// ) +/// +/// // Shorthand: captures become fields on the output node +/// rule!((query ...) => output_kind) /// ``` /// -/// Captures become Rust variables: `@name` binds `name: Id` (single) -/// or `name: Vec` (after `*`/`+`). The transform can use `tree!` -/// and `trees!` without an explicit context. +/// Captures become Rust variables automatically: +/// - `@name` (no quantifier) → `name: Id` +/// - `@name` (after `*`/`+`) → `name: Vec` +/// - `@name` (after `?`) → `name: Option` +/// +/// `tree!` and `trees!` can be used without explicit context inside `{...}`. #[proc_macro] pub fn rule(input: TokenStream) -> TokenStream { let input2: TokenStream2 = input.into(); diff --git a/shared/yeast-macros/src/parse.rs b/shared/yeast-macros/src/parse.rs index 6987ee79e82..62e176fb43c 100644 --- a/shared/yeast-macros/src/parse.rs +++ b/shared/yeast-macros/src/parse.rs @@ -602,9 +602,6 @@ pub fn parse_rule_top(input: TokenStream) -> Result { let name = Ident::new(&cap.name, Span::call_site()); let name_str = &cap.name; match cap.multiplicity { - CaptureMultiplicity::Repeated => quote! { - let __field_id = #ctx_ident.ast.field_id_for_name(#name_str) - .unwrap_or_else(|| panic!("field '{}' not found", #name_str)); CaptureMultiplicity::Repeated => quote! { let __field_id = #ctx_ident.ast.field_id_for_name(#name_str) .unwrap_or_else(|| panic!("field '{}' not found", #name_str)); diff --git a/shared/yeast/doc/yeast.md b/shared/yeast/doc/yeast.md index 7b4dbab53d5..f2d088be0a1 100644 --- a/shared/yeast/doc/yeast.md +++ b/shared/yeast/doc/yeast.md @@ -130,35 +130,47 @@ children. Named node patterns like `(_)` automatically skip unnamed tokens ## Template language Templates construct new AST nodes using the `tree!` and `trees!` macros. -Both take a `BuildCtx` as their first argument, which holds the AST, -captures from the query match, and a fresh identifier scope. + +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: ```rust -let mut ctx = BuildCtx::new(ast, &captures); +// Inside rule! — implicit context +yeast::rule!( + (assignment left: (_) @left right: (_) @right) + => + (assignment left: {right} right: {left}) +); + +// Standalone — explicit context +let fresh = yeast::tree_builder::FreshScope::new(); +let mut ctx = BuildCtx::new(ast, &captures, &fresh); +let id = yeast::tree!(ctx, (assignment left: @lhs right: @rhs)); ``` ### `tree!` — build a single node -`tree!(ctx, ...)` returns a single node `Id`: +`tree!(...)` returns a single node `Id`: ```rust -let id = yeast::tree!(ctx, +yeast::tree!(ctx, (assignment left: @lhs right: @rhs ) -); +) ``` ### `trees!` — build multiple nodes -`trees!(ctx, ...)` returns `Vec`: +`trees!(...)` returns `Vec`: ```rust -let ids = yeast::trees!(ctx, +yeast::trees!(ctx, (assignment left: @tmp right: @right) (@body)* -); +) ``` ### Capture references @@ -245,39 +257,59 @@ This rule rewrites Ruby's `for pat in val do body end` into `val.each { |tmp| pat = tmp; body }`: ```rust -let query = yeast::query!( +let for_rule = yeast::rule!( (for pattern: (_) @pat value: (in (_) @val) body: (do (_)* @body) ) -); - -let transform = |ast: &mut Ast, match_: Captures| { - let mut ctx = BuildCtx::new(ast, &match_); - vec![yeast::tree!(ctx, - (call - receiver: @val - method: (identifier "each") - block: (block - parameters: (block_parameters - (identifier $tmp) - ) - body: (block_body - (assignment - left: @pat - right: (identifier $tmp) - ) - (@body)* + => + (call + receiver: {val} + method: (identifier "each") + block: (block + parameters: (block_parameters + (identifier $tmp) + ) + body: (block_body + (assignment + left: {pat} + right: (identifier $tmp) ) + {..body} ) ) - )] -}; - -let rule = Rule::new(query, Box::new(transform)); + ) +); ``` +Captures from the query (`@pat`, `@val`, `@body`) become Rust variables +automatically: single captures bind as `Id`, repeated captures (after +`*` or `+`) as `Vec`, and optional captures (after `?`) as +`Option`. + +## The `rule!` macro + +`rule!` combines a query and a transform into a single declaration: + +```rust +// Full template form +yeast::rule!( + (query_pattern field: (_) @capture) + => + (output_template field: {capture}) +) + +// Shorthand form — captures become fields on the output node +yeast::rule!( + (query_pattern field: (_) @capture) + => output_kind +) +``` + +The shorthand `=> kind` form auto-generates the template, mapping each +capture name to a field of the same name on the output node. + ## Integration with the extractor YEAST integrates with the shared tree-sitter extractor via two mechanisms: diff --git a/shared/yeast/src/lib.rs b/shared/yeast/src/lib.rs index 5d909a9f078..04b5e6ad327 100644 --- a/shared/yeast/src/lib.rs +++ b/shared/yeast/src/lib.rs @@ -206,14 +206,14 @@ impl Ast { fields: BTreeMap>, is_named: bool, ) -> Id { - self.create_node_with_range(kind, content, children, is_named, None) + self.create_node_with_range(kind, content, fields, is_named, None) } pub fn create_node_with_range( &mut self, kind: KindId, content: NodeContent, - children: Vec<(FieldId, Id)>, + fields: BTreeMap>, is_named: bool, source_range: Option, ) -> Id {