From 9a5cc3c5e3ad1bfefb414b29767993e47dcd0603 Mon Sep 17 00:00:00 2001 From: Taus Date: Fri, 26 Jun 2026 12:39:47 +0000 Subject: [PATCH 01/10] yeast: Make `Id` a newtype, delete `NodeRef` Previously, the `Id` type was a bare usize alias. The `NodeRef` newtype existed solely to carry the AST-aware `YeastDisplay` / `YeastSourceRange` impls (so that `#{captured_node}` rendered source text rather than the numeric id) without colliding with the impls for raw integer types. This commit promotes `Id` itself to a (transparent) newtype struct and moves the AST-aware trait impls directly onto it. With `Id` and `usize` now being different types, the integer-display impl (for `usize`) and the source-text impl (for `Id`) coexist without conflict, and `NodeRef` becomes redundant (and so we remove it). --- shared/yeast-macros/src/parse.rs | 47 ++++++------ shared/yeast/doc/yeast.md | 2 +- shared/yeast/src/build.rs | 5 +- shared/yeast/src/dump.rs | 19 ++--- shared/yeast/src/lib.rs | 72 +++++++++---------- shared/yeast/src/visitor.rs | 8 +-- shared/yeast/tests/test.rs | 16 ++--- .../extractor/src/languages/swift/swift.rs | 36 +++++----- 8 files changed, 92 insertions(+), 113 deletions(-) diff --git a/shared/yeast-macros/src/parse.rs b/shared/yeast-macros/src/parse.rs index d02556b5cdfe..7b5e783e4ed7 100644 --- a/shared/yeast-macros/src/parse.rs +++ b/shared/yeast-macros/src/parse.rs @@ -342,7 +342,7 @@ pub fn parse_trees_top(input: TokenStream) -> Result { } Ok(quote! { { - let mut __nodes: Vec = Vec::new(); + let mut __nodes: Vec = Vec::new(); #(#items)* __nodes } @@ -356,7 +356,7 @@ fn parse_direct_node(tokens: &mut Tokens, ctx: &Ident) -> Result { Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace => { let group = expect_group(tokens, Delimiter::Brace)?; let expr = group.stream(); - Ok(quote! { ::std::convert::Into::::into({ #expr }) }) + Ok(quote! { ::std::convert::Into::::into({ #expr }) }) } Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Parenthesis => { let group = expect_group(tokens, Delimiter::Parenthesis)?; @@ -450,7 +450,7 @@ fn parse_direct_node_inner(tokens: &mut Tokens, ctx: &Ident) -> Result::into) + { #expr }.into_iter().map(::std::convert::Into::::into) } } else { let expr = group.stream(); @@ -458,7 +458,7 @@ fn parse_direct_node_inner(tokens: &mut Tokens, ctx: &Ident) -> Result = #chained.collect(); + let #temp: Vec = #chained.collect(); }); // An empty splice means the field is absent — skip it // entirely rather than emitting an empty named field. @@ -471,7 +471,7 @@ fn parse_direct_node_inner(tokens: &mut Tokens, ctx: &Ident) -> Result Result)> = Vec::new(); + let mut __fields: Vec<(&str, Vec)> = Vec::new(); #(#field_args)* #ctx.node(#kind_str, __fields) } @@ -499,7 +499,7 @@ fn parse_direct_node_inner(tokens: &mut Tokens, ctx: &Ident) -> Result template) -- iterator map: produces Vec +/// .map(param -> template) -- iterator map: produces Vec /// ``` /// /// The chain may be empty (returns `base` unchanged). Multiple chained calls @@ -558,10 +558,10 @@ fn parse_chain_suffix(tokens: &mut Tokens, ctx: &Ident, base: TokenStream) -> Re current = quote! { { let mut __iter = #current; - let __result: Option = if let Some(#init_param) = __iter.next() { - let mut __acc: usize = #init_body; + let __result: Option = if let Some(#init_param) = __iter.next() { + let mut __acc: yeast::Id = #init_body; for #elem_param in __iter { - let #acc_param: usize = __acc; + let #acc_param: yeast::Id = __acc; __acc = #fold_body; } Some(__acc) @@ -616,7 +616,7 @@ fn parse_direct_list(tokens: &mut Tokens, ctx: &Ident) -> Result::into) + { #expr }.into_iter().map(::std::convert::Into::::into) } } else { let expr = group.stream(); @@ -629,7 +629,7 @@ fn parse_direct_list(tokens: &mut Tokens, ctx: &Ident) -> Result::into({ #expr })); + __nodes.push(::std::convert::Into::::into({ #expr })); }); } continue; @@ -649,7 +649,7 @@ struct CaptureInfo { name: String, multiplicity: CaptureMultiplicity, /// `true` for `@@name` captures: the auto-translate prefix skips them, - /// so the bound `NodeRef` refers to the raw (input-schema) node. + /// so the bound `Id` refers to the raw (input-schema) node. raw: bool, } @@ -804,22 +804,17 @@ pub fn parse_rule_top(input: TokenStream) -> Result { match cap.multiplicity { CaptureMultiplicity::Repeated => { quote! { - let #name: Vec = __captures.get_all(#name_str) - .into_iter() - .map(yeast::NodeRef) - .collect(); + let #name: Vec = __captures.get_all(#name_str); } } CaptureMultiplicity::Optional => { quote! { - let #name: Option = - __captures.get_opt(#name_str).map(yeast::NodeRef); + let #name: Option = __captures.get_opt(#name_str); } } CaptureMultiplicity::Single => { quote! { - let #name: yeast::NodeRef = - yeast::NodeRef(__captures.get_var(#name_str).unwrap()); + let #name: yeast::Id = __captures.get_var(#name_str).unwrap(); } } } @@ -850,7 +845,7 @@ pub fn parse_rule_top(input: TokenStream) -> Result { __fields.insert( __field_id, #name.into_iter() - .map(::std::convert::Into::::into) + .map(::std::convert::Into::::into) .collect(), ); }, @@ -859,14 +854,14 @@ pub fn parse_rule_top(input: TokenStream) -> Result { .unwrap_or_else(|| panic!("field '{}' not found", #name_str)); if let Some(__id) = #name { __fields.entry(__field_id).or_insert_with(Vec::new) - .push(::std::convert::Into::::into(__id)); + .push(::std::convert::Into::::into(__id)); } }, CaptureMultiplicity::Single => quote! { let __field_id = #ctx_ident.ast.field_id_for_name(#name_str) .unwrap_or_else(|| panic!("field '{}' not found", #name_str)); __fields.entry(__field_id).or_insert_with(Vec::new) - .push(::std::convert::Into::::into(#name)); + .push(::std::convert::Into::::into(#name)); }, } }) @@ -898,7 +893,7 @@ pub fn parse_rule_top(input: TokenStream) -> Result { } quote! { - let mut __nodes: Vec = Vec::new(); + let mut __nodes: Vec = Vec::new(); #(#transform_items)* __nodes } @@ -919,7 +914,7 @@ pub fn parse_rule_top(input: TokenStream) -> Result { __translator.auto_translate_captures(&mut __captures, __ast, __user_ctx, __skip)?; #(#bindings)* let mut #ctx_ident = yeast::build::BuildCtx::with_translator(__ast, &__captures, __fresh, __source_range, __user_ctx, __translator); - let __result: Vec = { #transform_body }; + let __result: Vec = { #transform_body }; Ok(__result) })) } diff --git a/shared/yeast/doc/yeast.md b/shared/yeast/doc/yeast.md index 3c122e7ebf9c..dad36bb0edb4 100644 --- a/shared/yeast/doc/yeast.md +++ b/shared/yeast/doc/yeast.md @@ -302,7 +302,7 @@ already conforms to the output schema. For rules that need the raw (input-schema) capture — typically to read its source text or to translate it explicitly with mutable context state between calls — use `@@name` instead. The body sees the original -input-schema `NodeRef`: +input-schema `Id`: ```rust yeast::rule!( diff --git a/shared/yeast/src/build.rs b/shared/yeast/src/build.rs index c7c605305994..7875942f9c15 100644 --- a/shared/yeast/src/build.rs +++ b/shared/yeast/src/build.rs @@ -176,9 +176,6 @@ impl BuildCtx<'_, C> { /// (translation is not meaningful when input and output share a /// schema). /// - /// Accepts any value convertible to [`Id`] (including [`crate::NodeRef`]), - /// so manual rules can pass capture bindings directly without unwrapping. - /// /// Errors if this `BuildCtx` was constructed by hand (without a /// translator handle) — for example, in unit tests that don't go /// through the rule driver. @@ -191,7 +188,7 @@ impl BuildCtx<'_, C> { } /// Translate an optional capture, returning the first translated id or - /// `None`. Convenience for `?`-quantifier captures (`Option`). + /// `None`. Convenience for `?`-quantifier captures (`Option`). /// /// If the underlying translation produces multiple ids for a single /// input, only the first is returned. For most use cases (e.g. diff --git a/shared/yeast/src/dump.rs b/shared/yeast/src/dump.rs index be496d40bd5b..34b614323600 100644 --- a/shared/yeast/src/dump.rs +++ b/shared/yeast/src/dump.rs @@ -1,6 +1,6 @@ use std::fmt::Write; -use crate::{schema::Schema, Ast, Node, NodeContent, CHILD_FIELD}; +use crate::{schema::Schema, Ast, Id, Node, NodeContent, CHILD_FIELD}; /// Options for controlling AST dump output. pub struct DumpOptions { @@ -34,16 +34,11 @@ impl Default for DumpOptions { /// method: /// identifier "foo" /// ``` -pub fn dump_ast(ast: &Ast, root: usize, source: &str) -> String { +pub fn dump_ast(ast: &Ast, root: Id, source: &str) -> String { dump_ast_with_options(ast, root, source, &DumpOptions::default()) } -pub fn dump_ast_with_options( - ast: &Ast, - root: usize, - source: &str, - options: &DumpOptions, -) -> String { +pub fn dump_ast_with_options(ast: &Ast, root: Id, source: &str, options: &DumpOptions) -> String { let mut out = String::new(); dump_node(ast, root, source, options, 0, None, &mut out); out @@ -53,7 +48,7 @@ pub fn dump_ast_with_options( /// /// Any node that does not match the expected type set for its parent field is /// rendered with a trailing `" <-- ERROR: ..."` annotation on the same line. -pub fn dump_ast_with_type_errors(ast: &Ast, root: usize, source: &str, schema: &Schema) -> String { +pub fn dump_ast_with_type_errors(ast: &Ast, root: Id, source: &str, schema: &Schema) -> String { dump_ast_with_type_errors_and_options(ast, root, source, schema, &DumpOptions::default()) } @@ -63,7 +58,7 @@ pub fn dump_ast_with_type_errors(ast: &Ast, root: usize, source: &str, schema: & /// rendered with a trailing `" <-- ERROR: ..."` annotation on the same line. pub fn dump_ast_with_type_errors_and_options( ast: &Ast, - root: usize, + root: Id, source: &str, schema: &Schema, options: &DumpOptions, @@ -176,7 +171,7 @@ fn expected_for_field<'a>( fn dump_node( ast: &Ast, - id: usize, + id: Id, source: &str, options: &DumpOptions, indent: usize, @@ -315,7 +310,7 @@ fn dump_node( /// Dump a leaf node inline (no newline prefix, caller provides context). fn dump_node_inline( ast: &Ast, - id: usize, + id: Id, source: &str, options: &DumpOptions, type_check: Option<( diff --git a/shared/yeast/src/lib.rs b/shared/yeast/src/lib.rs index 004a8408cb67..bfcaface53a0 100644 --- a/shared/yeast/src/lib.rs +++ b/shared/yeast/src/lib.rs @@ -22,37 +22,31 @@ use captures::Captures; pub use cursor::Cursor; use query::QueryNode; -/// Node ids are indexes into the arena -pub type Id = usize; - -/// Field and Kind ids are provided by tree-sitter -type FieldId = u16; -type KindId = u16; - -/// A typed reference to a node in an [`Ast`] arena. Wraps an [`Id`] but -/// deliberately does not implement [`std::fmt::Display`]: rendering a node -/// requires the [`Ast`] it lives in (to resolve [`NodeContent::Range`] back -/// to source text). Use [`YeastDisplay::yeast_to_string`] to format it. -#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)] -pub struct NodeRef(pub Id); +/// Node id: an index into the [`Ast`] arena. A newtype around `usize` +/// rather than a bare alias so that it can carry its own +/// [`YeastDisplay`] / [`YeastSourceRange`] / [`IntoFieldIds`] impls +/// without colliding with the impls for plain integers. +/// +/// Use `id.0` (or `id.into()`) to obtain the raw arena index. +#[repr(transparent)] +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Hash, Serialize)] +pub struct Id(pub usize); -impl NodeRef { - pub fn id(self) -> Id { - self.0 +impl From for Id { + fn from(value: usize) -> Self { + Id(value) } } -impl From for Id { - fn from(value: NodeRef) -> Self { +impl From for usize { + fn from(value: Id) -> Self { value.0 } } -impl From for NodeRef { - fn from(value: Id) -> Self { - NodeRef(value) - } -} +/// Field and Kind ids are provided by tree-sitter +type FieldId = u16; +type KindId = u16; /// Like [`std::fmt::Display`], but the formatting routine is given access to /// the [`Ast`] so that node references can resolve to their source text. @@ -67,21 +61,21 @@ pub trait YeastDisplay { /// Optional source range for values used in `#{expr}` interpolations. /// /// By default this returns `None`, so synthesized leaves inherit the matched -/// rule's source range. `NodeRef` returns the referenced node's range, letting +/// rule's source range. `Id` returns the referenced node's range, letting /// `(kind #{capture})` carry the captured node's location. pub trait YeastSourceRange { fn yeast_source_range(&self, ast: &Ast) -> Option; } -impl YeastDisplay for NodeRef { +impl YeastDisplay for Id { fn yeast_to_string(&self, ast: &Ast) -> String { - ast.source_text(self.0) + ast.source_text(*self) } } -impl YeastSourceRange for NodeRef { +impl YeastSourceRange for Id { fn yeast_source_range(&self, ast: &Ast) -> Option { - ast.get_node(self.0).and_then(|n| match &n.content { + ast.get_node(*self).and_then(|n| match &n.content { NodeContent::Range(r) => Some(r.clone()), _ => n.source_range, }) @@ -172,7 +166,7 @@ impl<'a> AstCursor<'a> { } impl<'a> Cursor<'a, Ast, Node, FieldId> for AstCursor<'a> { fn node(&self) -> &'a Node { - &self.ast.nodes[self.node_id] + &self.ast.nodes[self.node_id.0] } fn field_id(&self) -> Option { @@ -347,16 +341,16 @@ impl Ast { /// /// This reflects the effective AST after desugaring and excludes orphaned /// arena nodes left behind by rewrite operations. - pub fn reachable_node_ids(&self) -> Vec { + pub fn reachable_node_ids(&self) -> Vec { let mut reachable = Vec::new(); let mut stack = vec![self.root]; let mut seen = vec![false; self.nodes.len()]; while let Some(id) = stack.pop() { - if id >= self.nodes.len() || seen[id] { + if id.0 >= self.nodes.len() || seen[id.0] { continue; } - seen[id] = true; + seen[id.0] = true; reachable.push(id); if let Some(node) = self.get_node(id) { @@ -380,11 +374,11 @@ impl Ast { } pub fn get_node(&self, id: Id) -> Option<&Node> { - self.nodes.get(id) + self.nodes.get(id.0) } pub fn print(&self, source: &str, root_id: Id) -> Value { - let root = &self.nodes()[root_id]; + let root = &self.nodes()[root_id.0]; self.print_node(root, source) } @@ -427,7 +421,7 @@ impl Ast { is_named, source_range, }); - id + Id(id) } fn union_source_range_of_children( @@ -498,7 +492,7 @@ impl Ast { pub fn prepend_field_child(&mut self, node_id: Id, field_id: FieldId, value_id: Id) { let node = self .nodes - .get_mut(node_id) + .get_mut(node_id.0) .expect("prepend_field_child: invalid node id"); node.fields.entry(field_id).or_default().insert(0, value_id); } @@ -524,7 +518,7 @@ impl Ast { fields: BTreeMap::new(), content: NodeContent::DynamicString(content), }); - id + Id(id) } pub fn field_name_for_id(&self, id: FieldId) -> Option<&'static str> { @@ -1008,7 +1002,7 @@ fn apply_repeating_rules_inner( // // Child traversal does not increment rewrite depth and starts fresh // (no rule is skipped on child subtrees). - let mut fields = std::mem::take(&mut ast.nodes[id].fields); + let mut fields = std::mem::take(&mut ast.nodes[id.0].fields); for children in fields.values_mut() { let mut new_children: Option> = None; for (i, &child_id) in children.iter().enumerate() { @@ -1041,7 +1035,7 @@ fn apply_repeating_rules_inner( *children = new; } } - ast.nodes[id].fields = fields; + ast.nodes[id.0].fields = fields; Ok(vec![id]) } diff --git a/shared/yeast/src/visitor.rs b/shared/yeast/src/visitor.rs index 4bd2606c958b..bbf4308f1334 100644 --- a/shared/yeast/src/visitor.rs +++ b/shared/yeast/src/visitor.rs @@ -49,7 +49,7 @@ impl Visitor { pub fn build_with_schema(self, schema: crate::schema::Schema) -> Ast { Ast { - root: 0, + root: Id(0), schema, nodes: self.nodes.into_iter().map(|n| n.inner).collect(), source: Vec::new(), @@ -72,7 +72,7 @@ impl Visitor { }, parent: self.current, }); - id + Id(id) } fn enter_node(&mut self, node: tree_sitter::Node<'_>) -> bool { @@ -83,10 +83,10 @@ impl Visitor { fn leave_node(&mut self, field_name: Option<&'static str>, _node: tree_sitter::Node<'_>) { let node_id = self.current.unwrap(); - let node_parent = self.nodes[node_id].parent; + let node_parent = self.nodes[node_id.0].parent; if let Some(parent_id) = node_parent { - let parent = self.nodes.get_mut(parent_id).unwrap(); + let parent = self.nodes.get_mut(parent_id.0).unwrap(); if let Some(field) = field_name { let field_id = self.language.field_id_for_name(field).unwrap().get(); parent diff --git a/shared/yeast/tests/test.rs b/shared/yeast/tests/test.rs index 73243a09ab76..0399316b3058 100644 --- a/shared/yeast/tests/test.rs +++ b/shared/yeast/tests/test.rs @@ -1059,7 +1059,7 @@ fn test_one_shot_does_not_recurse_into_wrapper_output() { } /// Verify that `@@name` capture markers skip the auto-translate prefix: -/// the body sees the *raw* (input-schema) NodeRef and can read its +/// the body sees the *raw* (input-schema) `Id` and can read its /// source text or call `ctx.translate(...)` explicitly. Compare with /// the bare `@name` form, where the auto-translate prefix runs the /// same translation up front and the body sees the post-translate id. @@ -1081,7 +1081,7 @@ fn test_raw_capture_marker() { (assignment left: (_) @@raw_lhs right: (_) @rhs) => { - let text = ctx.ast.source_text(raw_lhs.into()); + let text = ctx.ast.source_text(raw_lhs); tree!((call method: (identifier #{text.as_str()}) receiver: {rhs})) @@ -1116,7 +1116,7 @@ fn test_raw_capture_marker() { } /// Companion to `test_raw_capture_marker`: confirms that calling -/// `ctx.translate(raw)` on a `@@`-captured NodeRef from the rule body +/// `ctx.translate(raw)` on a `@@`-captured `Id` from the rule body /// produces the correctly-translated output-schema node. With `@`, the /// translation has already happened, so `ctx.translate(...)` inside the /// body would attempt to re-translate an output node (which has no @@ -1235,10 +1235,8 @@ fn test_desugar_for_with_multiple_assignment() { } /// Regression test: `#{capture}` in a template must render the *source text* -/// of the captured node, not its arena `Id`. Previously, captures were bound -/// as `usize`, so `#{cap}` printed the integer id (e.g. `"3"`) via `Display`. -/// Captures are now bound as `NodeRef`, which has no `Display` impl and -/// resolves to the captured node's source text via `YeastDisplay`. +/// of the captured node, not its arena `Id`. Captures are bound as `Id`, +/// whose `YeastDisplay` impl resolves to the captured node's source text. #[test] fn test_hash_brace_renders_capture_source_text() { let rule: Rule = rule!( @@ -1266,7 +1264,7 @@ fn test_hash_brace_renders_capture_source_text() { ); } -/// Regression test: non-`NodeRef` values in `#{expr}` still render via their +/// Regression test: non-`Id` values in `#{expr}` still render via their /// `Display` impl (covered by `YeastDisplay`'s blanket impls for primitives). #[test] fn test_hash_brace_renders_integer_expression() { @@ -1304,7 +1302,7 @@ fn test_hash_brace_uses_capture_location_for_leaf() { let ast = run_and_ast("foo.bar()", vec![rule]); - let mut bar_ids: Vec = Vec::new(); + let mut bar_ids: Vec = Vec::new(); for id in ast.reachable_node_ids() { let Some(node) = ast.get_node(id) else { continue; diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index 7ca3bda6a1e9..53ecd397514e 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -15,26 +15,26 @@ struct SwiftContext { /// (`computed_getter`/`computed_setter`/`computed_modify`/ /// `willset_clause`/`didset_clause`/`getter_specifier`/ /// `setter_specifier`). - property_name: Option, + property_name: Option, /// Translated type node for the property type. Set by the outer /// `property_binding` rule (computed accessors variant) and /// `protocol_property_declaration` when present; read by the /// accessor inner rules. - property_type: Option, + property_type: Option, /// Default-value expression for the next translated `parameter`. Set /// by the outer `function_parameter` rule; read by the `parameter` /// rules. - default_value: Option, + default_value: Option, /// Translated outer modifiers (e.g. visibility, attributes) to /// attach to each child of a flattening outer rule. Set by /// `property_declaration`, `enum_entry`, and /// `protocol_property_declaration`. - outer_modifiers: Vec, + outer_modifiers: Vec, /// The `let`/`var` binding modifier for a `property_declaration`. /// Set by `property_declaration`; read by the inner declaration /// rules (`property_binding` variants, accessor rules) so they /// emit it as part of the output node's `modifier:` field. - binding_modifier: Option, + binding_modifier: Option, /// True when the current child of a flattening outer rule is not /// the first one — its inner rule should emit a /// `chained_declaration` modifier so the original grouping can be @@ -45,10 +45,10 @@ struct SwiftContext { /// Build a freshly-created `chained_declaration` modifier node if /// `ctx.is_chained`, else `None`. Used by inner declaration rules to /// emit the chained tag for non-first children of a flattening outer -/// rule. Returns `Option` so it splices via `{..…}` to 0 or 1 ids. -fn chained_modifier(ctx: &mut yeast::build::BuildCtx<'_, SwiftContext>) -> Option { +/// rule. Returns `Option` so it splices via `{..…}` to 0 or 1 ids. +fn chained_modifier(ctx: &mut yeast::build::BuildCtx<'_, SwiftContext>) -> Option { if ctx.is_chained { - Some(ctx.literal("modifier", "chained_declaration").into()) + Some(ctx.literal("modifier", "chained_declaration")) } else { None } @@ -63,10 +63,10 @@ fn chained_modifier(ctx: &mut yeast::build::BuildCtx<'_, SwiftContext>) -> Optio /// condition. fn and_chain( ctx: &mut yeast::build::BuildCtx<'_, SwiftContext>, - conds: Vec, + conds: Vec, ) -> yeast::Id { - conds.into_iter() - .map(yeast::Id::from) + conds + .into_iter() .reduce(|acc, elem| { tree!((binary_expr operator: (infix_operator "&&") left: {acc} right: {elem})) }) @@ -79,7 +79,7 @@ fn and_chain( /// guarantees at least one part. fn member_chain( ctx: &mut yeast::build::BuildCtx<'_, SwiftContext>, - parts: Vec, + parts: Vec, ) -> yeast::Id { let mut iter = parts.into_iter(); let first = iter @@ -199,7 +199,7 @@ fn translation_rules() -> Vec> { computed_value: (computed_property accessor: _+ @@accessors)) => {..{ - ctx.property_name = Some(tree!((identifier #{pattern})).into()); + ctx.property_name = Some(tree!((identifier #{pattern}))); ctx.property_type = ty; let mut result = Vec::new(); @@ -261,7 +261,7 @@ fn translation_rules() -> Vec> { ); // Publish the property name for the observer rules. - ctx.property_name = Some(tree!((identifier #{name})).into()); + ctx.property_name = Some(tree!((identifier #{name}))); // Observers are subsequent outputs of this flattening // rule, so they always get `chained_declaration`. ctx.is_chained = true; @@ -306,8 +306,8 @@ fn translation_rules() -> Vec> { (modifiers)* @mods) => {..{ - let binding_text = ctx.ast.source_text(binding_kind.into()); - ctx.binding_modifier = Some(ctx.literal("modifier", &binding_text).into()); + let binding_text = ctx.ast.source_text(binding_kind); + ctx.binding_modifier = Some(ctx.literal("modifier", &binding_text)); ctx.outer_modifiers = mods; let mut result = Vec::new(); @@ -723,7 +723,7 @@ fn translation_rules() -> Vec> { ), // Labeled statement (e.g. `outer: for ...`). Strip the trailing ':' from the label token. rule!((labeled_statement label: (statement_label) @lbl statement: @stmt) => { - let text = ctx.ast.source_text(lbl.into()); + let text = ctx.ast.source_text(lbl); let name = &text[..text.len() - 1]; tree!((labeled_stmt label: (identifier #{name}) stmt: {stmt})) }), @@ -1019,7 +1019,7 @@ fn translation_rules() -> Vec> { (modifiers)* @mods) => {..{ - ctx.property_name = Some(tree!((identifier #{name})).into()); + ctx.property_name = Some(tree!((identifier #{name}))); ctx.property_type = ty; ctx.outer_modifiers = mods; From cc3c23263149d0f03305ef38bf9554caacea1bbb Mon Sep 17 00:00:00 2001 From: Taus Date: Fri, 26 Jun 2026 12:45:35 +0000 Subject: [PATCH 02/10] yeast: Replace `{..expr}` splice syntax with trait-dispatched `{expr}` In the initial implementation of yeast, the splice syntax was needed do distinguish between splicing multiple nodes or just a single node. However, this was always an ugly "wart" in the syntax, since the user shouldn't have to worry about these things. To fix this, we add an `IntoFieldIds` trait that dispatches on the value's type: `Id` pushes a single id, and a blanket impl for `IntoIterator>` handles `Vec`, `Option`, and arbitrary iterator chains. With this, we no longer need to use the special splice syntax, and hence we can get rid of it. --- shared/yeast-macros/src/lib.rs | 13 +- shared/yeast-macros/src/parse.rs | 102 +++---- shared/yeast/doc/yeast.md | 38 ++- shared/yeast/src/lib.rs | 30 ++ shared/yeast/tests/test.rs | 16 +- .../extractor/src/languages/swift/swift.rs | 274 +++++++++--------- 6 files changed, 247 insertions(+), 226 deletions(-) diff --git a/shared/yeast-macros/src/lib.rs b/shared/yeast-macros/src/lib.rs index 07077be51f04..420f9fc70c63 100644 --- a/shared/yeast-macros/src/lib.rs +++ b/shared/yeast-macros/src/lib.rs @@ -41,15 +41,18 @@ pub fn query(input: TokenStream) -> TokenStream { /// (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 -/// {..expr} - splice an iterable of Id (in child/field position) -/// field: {..expr} - splice into a named field +/// {expr} - embed a Rust expression, dispatched via +/// the `IntoFieldIds` trait: `Id` pushes a +/// single id; iterables (`Vec`, +/// `Option`, iterator chains) splice +/// their elements +/// field: {expr} - extend a named field with `{expr}`'s ids /// {expr}.map(p -> tpl) - apply tpl to each element; splice result /// {expr}.reduce_left(f -> init, acc, e -> fold) /// - fold with per-element init; splice 0 or 1 result /// ``` /// -/// Chain syntax after `{expr}` or `{..expr}`: +/// Chain syntax after `{expr}`: /// - `.map(param -> template)` — one output node per input element. /// - `.reduce_left(first -> init, acc, elem -> fold)` — fold left; the first /// element is converted by `init`, subsequent elements are folded by `fold` @@ -100,7 +103,7 @@ pub fn trees(input: TokenStream) -> TokenStream { /// rule!( /// (query_pattern field: (_) @name (kind)* @repeated (_)? @optional) /// => -/// (output_template field: {name} {..repeated}) +/// (output_template field: {name} {repeated}) /// ) /// /// // Shorthand: captures become fields on the output node diff --git a/shared/yeast-macros/src/parse.rs b/shared/yeast-macros/src/parse.rs index 7b5e783e4ed7..2422d0a8a5cd 100644 --- a/shared/yeast-macros/src/parse.rs +++ b/shared/yeast-macros/src/parse.rs @@ -429,45 +429,41 @@ fn parse_direct_node_inner(tokens: &mut Tokens, ctx: &Ident) -> Result into the field + // Check for field: {expr}.chain (chain pipeline) or plain field: {expr} + // (trait-dispatched: handles single ids and iterables uniformly). if peek_is_group(tokens, Delimiter::Brace) { - let group_clone = tokens.clone().next().unwrap(); - if let TokenTree::Group(g) = &group_clone { - let mut inner_check = g.stream().into_iter(); - let is_splice = matches!(inner_check.next(), Some(TokenTree::Punct(p)) if p.as_char() == '.') - && matches!(inner_check.next(), Some(TokenTree::Punct(p)) if p.as_char() == '.'); - // Determine if a chain (.map(..)) follows the `{}` group. - let mut after = tokens.clone(); - after.next(); // skip the brace group - let has_chain = - matches!(after.peek(), Some(TokenTree::Punct(p)) if p.as_char() == '.'); - - if is_splice || has_chain { - let group = expect_group(tokens, Delimiter::Brace)?; - let base: TokenStream = if is_splice { - let mut inner = group.stream().into_iter().peekable(); - inner.next(); // consume first . - inner.next(); // consume second . - let expr: TokenStream = inner.collect(); - quote! { - { #expr }.into_iter().map(::std::convert::Into::::into) - } - } else { - let expr = group.stream(); - quote! { { #expr }.into_iter() } - }; - let chained = parse_chain_suffix(tokens, ctx, base)?; - stmts.push(quote! { - let #temp: Vec = #chained.collect(); - }); - // An empty splice means the field is absent — skip it - // entirely rather than emitting an empty named field. - field_args.push(quote! { - if !#temp.is_empty() { __fields.push((#field_str, #temp)); } - }); - continue; - } + // Determine if a chain (.map(..)) follows the `{}` group. + let mut after = tokens.clone(); + after.next(); // skip the brace group + let has_chain = matches!(after.peek(), Some(TokenTree::Punct(p)) if p.as_char() == '.'); + + if has_chain { + let group = expect_group(tokens, Delimiter::Brace)?; + let expr = group.stream(); + let base = quote! { { #expr }.into_iter() }; + let chained = parse_chain_suffix(tokens, ctx, base)?; + stmts.push(quote! { + let #temp: Vec = #chained.collect(); + }); + // An empty pipeline means the field is absent — skip it + // entirely rather than emitting an empty named field. + field_args.push(quote! { + if !#temp.is_empty() { __fields.push((#field_str, #temp)); } + }); + continue; } + + // Plain `{expr}` — trait-dispatched extend. + let group = expect_group(tokens, Delimiter::Brace)?; + let expr = group.stream(); + stmts.push(quote! { + let mut #temp: Vec = Vec::new(); + yeast::IntoFieldIds::extend_into({ #expr }, &mut #temp); + }); + field_args.push(quote! { + if !#temp.is_empty() { __fields.push((#field_str, #temp)); } + }); + continue; } let value = parse_direct_node(tokens, ctx)?; @@ -495,8 +491,7 @@ fn parse_direct_node_inner(tokens: &mut Tokens, ctx: &Ident) -> Result template) -- iterator map: produces Vec @@ -603,25 +598,15 @@ fn parse_direct_list(tokens: &mut Tokens, ctx: &Ident) -> Result::into) - } - } else { - let expr = group.stream(); - quote! { { #expr }.into_iter() } - }; + if has_chain { + let expr = group.stream(); + let base = quote! { { #expr }.into_iter() }; let chained = parse_chain_suffix(tokens, ctx, base)?; items.push(quote! { __nodes.extend(#chained); @@ -629,7 +614,7 @@ fn parse_direct_list(tokens: &mut Tokens, ctx: &Ident) -> Result::into({ #expr })); + yeast::IntoFieldIds::extend_into({ #expr }, &mut __nodes); }); } continue; @@ -951,13 +936,6 @@ fn peek_is_hash(tokens: &mut Tokens) -> bool { matches!(tokens.peek(), Some(TokenTree::Punct(p)) if p.as_char() == '#') } -/// Check for `..` (two consecutive dot punctuation tokens). -fn peek_is_dotdot(tokens: &Tokens) -> bool { - let mut lookahead = tokens.clone(); - matches!(lookahead.next(), Some(TokenTree::Punct(p)) if p.as_char() == '.') - && matches!(lookahead.next(), Some(TokenTree::Punct(p)) if p.as_char() == '.') -} - fn peek_is_underscore(tokens: &mut Tokens) -> bool { matches!(tokens.peek(), Some(TokenTree::Ident(id)) if *id == "_") } diff --git a/shared/yeast/doc/yeast.md b/shared/yeast/doc/yeast.md index dad36bb0edb4..8ea2e67b2dec 100644 --- a/shared/yeast/doc/yeast.md +++ b/shared/yeast/doc/yeast.md @@ -214,7 +214,7 @@ yeast::tree!(ctx, ```rust yeast::trees!(ctx, (assignment left: {tmp} right: {right}) - {..body} + {body} ) ``` @@ -256,12 +256,26 @@ occurrences of the same `$name` within one `BuildCtx` share the same value: ### Embedded Rust expressions -`{expr}` embeds a Rust expression that returns a single node `Id`: +`{expr}` embeds a Rust expression whose value is appended to the +enclosing field (or to the rule body's id list). Dispatch happens via +the [`IntoFieldIds`] trait, which is implemented for: + +- `Id` — pushes the single id. +- Any `IntoIterator>` — extends with all yielded ids + (covers `Vec`, `Option`, iterator chains, etc.). + +So the same `{expr}` syntax handles single ids, splices, and zero-or-many +options uniformly: ```rust (assignment - left: {some_node_id} // insert a pre-built node - right: {rhs} // insert a captured value (inside rule!) + left: {some_node_id} // a single Id + right: {rhs} // a captured value (inside rule!) +) + +yeast::trees!(ctx, + (assignment left: {tmp} right: {right}) + {extra_nodes} // splices a Vec ) ``` @@ -277,21 +291,17 @@ expressions (with `let` bindings) work too: }) ``` -`{..expr}` splices a `Vec` (or any iterable of `Id`); the contents -are likewise a Rust block, so the splice can be the result of arbitrary -computation: +Inside `rule!`, captures are Rust variables — `{name}` works for +single, optional, and repeated captures alike: ```rust -yeast::trees!(ctx, - (assignment left: {tmp} right: {right}) - {..extra_nodes} // splice a Vec +rule!( + (assignment left: @lhs right: _* @parts) + => + (assignment left: {lhs} right: (block stmt: {parts})) ) ``` -Inside `rule!`, captures are Rust variables, so `{name}` inserts a -single capture (`Id`) and `{..name}` splices a repeated capture -(`Vec`). - ### Raw captures (`@@name`) The default `@name` capture marker is *auto-translated*: in OneShot diff --git a/shared/yeast/src/lib.rs b/shared/yeast/src/lib.rs index bfcaface53a0..2c53f756fdff 100644 --- a/shared/yeast/src/lib.rs +++ b/shared/yeast/src/lib.rs @@ -48,6 +48,36 @@ impl From for usize { type FieldId = u16; type KindId = u16; +/// Trait for values that can be appended to a field's id list inside a +/// `tree!`/`trees!`/`rule!` template (in `{expr}` placeholders). +/// +/// `Id` pushes a single id; the blanket impl for +/// `IntoIterator>` handles `Vec`, `Option`, +/// arbitrary iterators yielding `Id`, etc. +/// +/// This lets `{expr}` interpolate any of these shapes without a +/// dedicated splice syntax — the macro emits the same trait-dispatched +/// call regardless of the value's type. +pub trait IntoFieldIds { + fn extend_into(self, out: &mut Vec); +} + +impl IntoFieldIds for Id { + fn extend_into(self, out: &mut Vec) { + out.push(self); + } +} + +impl IntoFieldIds for I +where + I: IntoIterator, + T: Into, +{ + fn extend_into(self, out: &mut Vec) { + out.extend(self.into_iter().map(Into::into)); + } +} + /// Like [`std::fmt::Display`], but the formatting routine is given access to /// the [`Ast`] so that node references can resolve to their source text. /// diff --git a/shared/yeast/tests/test.rs b/shared/yeast/tests/test.rs index 0399316b3058..3cc02838fadf 100644 --- a/shared/yeast/tests/test.rs +++ b/shared/yeast/tests/test.rs @@ -635,7 +635,7 @@ fn ruby_rules() -> Vec { left: (identifier $tmp) right: {right} ) - {..left.iter().enumerate().map(|(i, &lhs)| + {left.iter().enumerate().map(|(i, &lhs)| yeast::tree!( (assignment left: {lhs} @@ -667,7 +667,7 @@ fn ruby_rules() -> Vec { left: {pat} right: (identifier $tmp) ) - stmt: {..body} + stmt: {body} ) ) ) @@ -903,7 +903,7 @@ fn one_shot_xeq1_rules() -> Vec { yeast::rule!( (program (_)* @stmts) => - (program stmt: {..stmts}) + (program stmt: {stmts}) ), yeast::rule!( (assignment left: (_) @left right: (_) @right) @@ -979,7 +979,7 @@ fn test_one_shot_recurses_into_returned_capture() { yeast::rule!( (program (_)* @stmts) => - (program stmt: {..stmts}) + (program stmt: {stmts}) ), // Returns the captured `left` verbatim, discarding `right`. yeast::rule!( @@ -1021,7 +1021,7 @@ fn test_one_shot_does_not_recurse_into_wrapper_output() { yeast::rule!( (program (_)* @stmts) => - (program stmt: {..stmts}) + (program stmt: {stmts}) ), // Wraps `left` in nested `first_node`/`second_node` output kinds. // Neither wrapper kind has a matching rule, so a buggy implementation @@ -1072,7 +1072,7 @@ fn test_raw_capture_marker() { yeast::rule!( (program (_)* @stmts) => - (program stmt: {..stmts}) + (program stmt: {stmts}) ), // `@@raw_lhs` is untranslated: the body reads its source text // ("x") and embeds it directly as the identifier content. `@rhs` @@ -1130,7 +1130,7 @@ fn test_raw_capture_marker_explicit_translate() { yeast::rule!( (program (_)* @stmts) => - (program stmt: {..stmts}) + (program stmt: {stmts}) ), yeast::rule!( (assignment left: (_) @@raw_lhs right: (_) @rhs) @@ -1138,7 +1138,7 @@ fn test_raw_capture_marker_explicit_translate() { { let translated_lhs = ctx.translate(raw_lhs)?; tree!((call - method: {..translated_lhs} + method: {translated_lhs} receiver: {rhs})) } ), diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index 53ecd397514e..5689d930bff3 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -45,7 +45,7 @@ struct SwiftContext { /// Build a freshly-created `chained_declaration` modifier node if /// `ctx.is_chained`, else `None`. Used by inner declaration rules to /// emit the chained tag for non-first children of a flattening outer -/// rule. Returns `Option` so it splices via `{..…}` to 0 or 1 ids. +/// rule. Returns `Option` so it splices via `{…}` to 0 or 1 ids. fn chained_modifier(ctx: &mut yeast::build::BuildCtx<'_, SwiftContext>) -> Option { if ctx.is_chained { Some(ctx.literal("modifier", "chained_declaration")) @@ -100,7 +100,7 @@ fn translation_rules() -> Vec> { (source_file statement: _* @children) => (top_level - body: (block stmt: {..children}) + body: (block stmt: {children}) ) ), // Declarations may be wrapped in local/global wrapper nodes. @@ -144,12 +144,12 @@ fn translation_rules() -> Vec> { rule!( (operator_declaration "prefix" (referenceable_operator _ @op) (simple_identifier)? @prec) => - (operator_syntax_declaration name: (identifier #{op}) fixity: (fixity "prefix") precedence: {..prec}) + (operator_syntax_declaration name: (identifier #{op}) fixity: (fixity "prefix") precedence: {prec}) ), rule!( (operator_declaration "postfix" (referenceable_operator _ @op) (simple_identifier)? @prec) => - (operator_syntax_declaration name: (identifier #{op}) fixity: (fixity "postfix") precedence: {..prec}) + (operator_syntax_declaration name: (identifier #{op}) fixity: (fixity "postfix") precedence: {prec}) ), rule!( (operator_declaration "infix" (referenceable_operator _ @op) (simple_identifier)? @prec) @@ -157,7 +157,7 @@ fn translation_rules() -> Vec> { (operator_syntax_declaration name: (identifier #{op}) fixity: (fixity "infix") - precedence: {..prec}) + precedence: {prec}) ), rule!((bitwise_operation lhs: @l op: @op rhs: @r) => (binary_expr left: {l} operator: (infix_operator #{op}) right: {r})), rule!((nil_coalescing_expression value: @l if_nil: @r) => (binary_expr left: {l} operator: (infix_operator "??") right: {r})), @@ -170,9 +170,9 @@ fn translation_rules() -> Vec> { rule!((postfix_expression operation: @op target: @operand) => (unary_expr operator: (postfix_operator #{op}) operand: {operand})), // TODO: Parenthesised single-value tuple is a grouping expression and should pass through. // Multi-value tuples become tuple_expr. - rule!((tuple_expression value: _* @v) => (tuple_expr element: {..v})), + rule!((tuple_expression value: _* @v) => (tuple_expr element: {v})), // Blocks contain statement* directly. - rule!((block statement: _+ @stmts) => (block stmt: {..stmts})), + rule!((block statement: _+ @stmts) => (block stmt: {stmts})), rule!((block) => (block)), // ---- Variables ---- // property_binding rules — these produce variable_declaration and/or accessor_declaration @@ -198,7 +198,7 @@ fn translation_rules() -> Vec> { type: _? @ty computed_value: (computed_property accessor: _+ @@accessors)) => - {..{ + {{ ctx.property_name = Some(tree!((identifier #{pattern}))); ctx.property_type = ty; @@ -223,13 +223,13 @@ fn translation_rules() -> Vec> { computed_value: (computed_property statement: _* @body)) => (accessor_declaration - modifier: {..ctx.binding_modifier} - modifier: {..ctx.outer_modifiers.clone()} - modifier: {..chained_modifier(&mut ctx)} + modifier: {ctx.binding_modifier} + modifier: {ctx.outer_modifiers.clone()} + modifier: {chained_modifier(&mut ctx)} name: (identifier #{name}) - type: {..ty} + type: {ty} accessor_kind: (accessor_kind "get") - body: (block stmt: {..body})) + body: (block stmt: {body})) ), // Stored property with willSet/didSet observers (initializer // optional) → a `variable_declaration` followed by one @@ -249,15 +249,15 @@ fn translation_rules() -> Vec> { value: _? @val observers: (willset_didset_block willset: _? @@ws didset: _? @@ds)) => - {..{ + {{ let var_decl = tree!( (variable_declaration - modifier: {..ctx.binding_modifier} - modifier: {..ctx.outer_modifiers.clone()} - modifier: {..chained_modifier(&mut ctx)} + modifier: {ctx.binding_modifier} + modifier: {ctx.outer_modifiers.clone()} + modifier: {chained_modifier(&mut ctx)} pattern: (name_pattern identifier: (identifier #{name})) - type: {..ty} - value: {..val}) + type: {ty} + value: {val}) ); // Publish the property name for the observer rules. @@ -282,12 +282,12 @@ fn translation_rules() -> Vec> { value: _? @val) => (variable_declaration - modifier: {..ctx.binding_modifier} - modifier: {..ctx.outer_modifiers.clone()} - modifier: {..chained_modifier(&mut ctx)} + modifier: {ctx.binding_modifier} + modifier: {ctx.outer_modifiers.clone()} + modifier: {chained_modifier(&mut ctx)} pattern: {pattern} - type: {..ty} - value: {..val}) + type: {ty} + value: {val}) ), // property_declaration: flatten declarators (each may translate // to multiple nodes — variable_declaration and/or @@ -305,7 +305,7 @@ fn translation_rules() -> Vec> { declarator: _* @@decls (modifiers)* @mods) => - {..{ + {{ let binding_text = ctx.ast.source_text(binding_kind); ctx.binding_modifier = Some(ctx.literal("modifier", &binding_text)); ctx.outer_modifiers = mods; @@ -342,19 +342,19 @@ fn translation_rules() -> Vec> { data_contents: (enum_type_parameters parameter: _* @params)) => (class_like_declaration - modifier: {..ctx.outer_modifiers.clone()} - modifier: {..chained_modifier(&mut ctx)} + modifier: {ctx.outer_modifiers.clone()} + modifier: {chained_modifier(&mut ctx)} modifier: (modifier "enum_case") name: (identifier #{name}) - member: (constructor_declaration parameter: {..params} body: (block))) + member: (constructor_declaration parameter: {params} body: (block))) ), // enum_case_entry with explicit raw value → variable_declaration with that value. rule!( (enum_case_entry name: @name raw_value: @val) => (variable_declaration - modifier: {..ctx.outer_modifiers.clone()} - modifier: {..chained_modifier(&mut ctx)} + modifier: {ctx.outer_modifiers.clone()} + modifier: {chained_modifier(&mut ctx)} modifier: (modifier "enum_case") pattern: (name_pattern identifier: (identifier #{name})) value: {val}) @@ -364,8 +364,8 @@ fn translation_rules() -> Vec> { (enum_case_entry name: @name) => (variable_declaration - modifier: {..ctx.outer_modifiers.clone()} - modifier: {..chained_modifier(&mut ctx)} + modifier: {ctx.outer_modifiers.clone()} + modifier: {chained_modifier(&mut ctx)} modifier: (modifier "enum_case") pattern: (name_pattern identifier: (identifier #{name}))) ), @@ -376,7 +376,7 @@ fn translation_rules() -> Vec> { rule!( (enum_entry case: _+ @@cases (modifiers)* @mods) => - {..{ + {{ ctx.outer_modifiers = mods; let mut result = Vec::new(); @@ -418,7 +418,7 @@ fn translation_rules() -> Vec> { => (constructor_pattern constructor: (member_access_expr base: {typ} member: (identifier #{name})) - element: {..items}) + element: {items}) ), // case .foo(x,y) pattern rule!( @@ -426,10 +426,10 @@ fn translation_rules() -> Vec> { => (constructor_pattern constructor: (member_access_expr base: (inferred_type_expr #{dot}) member: (identifier #{name})) - element: {..items}) + element: {items}) ), // Tuple pattern and its (optionally named) items - rule!((pattern kind: (tuple_pattern item: _* @elems)) => (tuple_pattern element: {..elems})), + rule!((pattern kind: (tuple_pattern item: _* @elems)) => (tuple_pattern element: {elems})), rule!((tuple_pattern_item name: @key pattern: @pat) => (pattern_element key: (identifier #{key}) pattern: {pat})), rule!((tuple_pattern_item pattern: @pat) => (pattern_element pattern: {pat})), // Type casting pattern (TODO) @@ -452,9 +452,9 @@ fn translation_rules() -> Vec> { => (function_declaration name: (identifier #{name}) - parameter: {..params} - return_type: {..ret} - body: (block stmt: {..body_stmts})) + parameter: {params} + return_type: {ret} + body: (block stmt: {body_stmts})) ), // Parameters are wrapped in function_parameter, which also carries // optional default values. Publishes the default value into `ctx` @@ -463,7 +463,7 @@ fn translation_rules() -> Vec> { rule!( (function_parameter parameter: @@p default_value: _? @def) => - {..{ + {{ ctx.default_value = def; ctx.translate(p)? }} @@ -475,7 +475,7 @@ fn translation_rules() -> Vec> { (parameter external_name: (identifier #{ext}) pattern: (name_pattern identifier: (identifier #{name})) - default: {..ctx.default_value}) + default: {ctx.default_value}) ), rule!( (parameter external_name: @ext name: @name type: @ty) @@ -484,7 +484,7 @@ fn translation_rules() -> Vec> { external_name: (identifier #{ext}) pattern: (name_pattern identifier: (identifier #{name})) type: {ty} - default: {..ctx.default_value}) + default: {ctx.default_value}) ), // Parameter with just name and type (no external name) rule!( @@ -492,7 +492,7 @@ fn translation_rules() -> Vec> { => (parameter pattern: (name_pattern identifier: (identifier #{name})) - default: {..ctx.default_value}) + default: {ctx.default_value}) ), rule!( (parameter name: @name type: @ty) @@ -500,7 +500,7 @@ fn translation_rules() -> Vec> { (parameter pattern: (name_pattern identifier: (identifier #{name})) type: {ty} - default: {..ctx.default_value}) + default: {ctx.default_value}) ), // Reference to a function, f(x:y:z:). This is parsed as a call with a single argument with multiple reference_specifier labels. // We don't want downstream QL to try to handle this as a call_expr with a weird argument, so explicitly mark it as unsupported for now. @@ -514,7 +514,7 @@ fn translation_rules() -> Vec> { rule!( (call_expression function: @func suffix: (call_suffix arguments: (value_arguments argument: (value_argument)* @args))) => - (call_expr callee: {func} argument: {..args}) + (call_expr callee: {func} argument: {args}) ), // Value argument with label (value: _ matches both named nodes and anonymous tokens like nil) rule!( @@ -537,7 +537,7 @@ fn translation_rules() -> Vec> { // Return / break / continue, one rule per keyword. // The anonymous "return"/"break"/"continue" keywords are matched as // string literals. - rule!((control_transfer_statement kind: "return" result: _? @val) => (return_expr value: {..val})), + rule!((control_transfer_statement kind: "return" result: _? @val) => (return_expr value: {val})), rule!((control_transfer_statement kind: "break" result: @lbl) => (break_expr label: (identifier #{lbl}))), rule!((control_transfer_statement kind: "break") => (break_expr)), rule!((control_transfer_statement kind: "continue" result: @lbl) => (continue_expr label: (identifier #{lbl}))), @@ -556,20 +556,20 @@ fn translation_rules() -> Vec> { statement: _* @body) => (function_expr - modifier: {..attrs} - capture_declaration: {..captures} - parameter: {..params} - return_type: {..ret} - body: (block stmt: {..body})) + modifier: {attrs} + capture_declaration: {captures} + parameter: {params} + return_type: {ret} + body: (block stmt: {body})) ), // capture_list_item with ownership modifier (e.g. [weak self], [unowned x]) rule!( (capture_list_item ownership: _? @ownership name: @name value: _? @val) => (variable_declaration - modifier: {..ownership} + modifier: {ownership} pattern: (name_pattern identifier: (identifier #{name})) - value: {..val}) + value: {val}) ), // Lambda parameter with type and optional external name rule!( @@ -615,7 +615,7 @@ fn translation_rules() -> Vec> { (if_expr condition: {and_chain(&mut ctx, cond)} then: {then_body} - else: {..else_stmts}) + else: {else_stmts}) ), // Guard statement rule!( @@ -623,7 +623,7 @@ fn translation_rules() -> Vec> { => (guard_if_stmt condition: {and_chain(&mut ctx, cond)} - else: (block stmt: {..else_stmts})) + else: (block stmt: {else_stmts})) ), // Ternary expression → if_expr rule!( @@ -635,7 +635,7 @@ fn translation_rules() -> Vec> { rule!( (switch_statement expr: @val entry: (switch_entry)* @cases) => - (switch_expr value: {val} case: {..cases}) + (switch_expr value: {val} case: {cases}) ), // Switch entry with multiple patterns and body rule!( @@ -644,19 +644,19 @@ fn translation_rules() -> Vec> { pattern: (switch_pattern pattern: @rest)+ statement: _* @body) => - (switch_case pattern: (or_pattern pattern: {first} pattern: {..rest}) body: (block stmt: {..body})) + (switch_case pattern: (or_pattern pattern: {first} pattern: {rest}) body: (block stmt: {body})) ), // Switch entry with exactly one pattern and body rule!( (switch_entry pattern: (switch_pattern pattern: @pat) statement: _* @body) => - (switch_case pattern: {pat} body: (block stmt: {..body})) + (switch_case pattern: {pat} body: (block stmt: {body})) ), // Switch entry: default case (no patterns) rule!( (switch_entry default: (default_keyword) statement: _* @body) => - (switch_case body: (block stmt: {..body})) + (switch_case body: (block stmt: {body})) ), // if case PATTERN = expr — preserve the pattern directly (no Optional wrapping) rule!( @@ -702,8 +702,8 @@ fn translation_rules() -> Vec> { (for_each_stmt pattern: {pat} iterable: {iter} - guard: {..guard} - body: (block stmt: {..body})) + guard: {guard} + body: (block stmt: {body})) ), // While loop rule!( @@ -711,7 +711,7 @@ fn translation_rules() -> Vec> { => (while_stmt condition: {and_chain(&mut ctx, cond)} - body: (block stmt: {..body})) + body: (block stmt: {body})) ), // Repeat-while loop rule!( @@ -719,7 +719,7 @@ fn translation_rules() -> Vec> { => (do_while_stmt condition: {and_chain(&mut ctx, cond)} - body: (block stmt: {..body})) + body: (block stmt: {body})) ), // Labeled statement (e.g. `outer: for ...`). Strip the trailing ':' from the label token. rule!((labeled_statement label: (statement_label) @lbl statement: @stmt) => { @@ -729,18 +729,18 @@ fn translation_rules() -> Vec> { }), // ---- Collections ---- // Array literal - rule!((array_literal element: _* @elems) => (array_literal element: {..elems})), + rule!((array_literal element: _* @elems) => (array_literal element: {elems})), // Empty array literal rule!((array_literal) => (array_literal)), // Dictionary literal — zip keys and values into key_value_pairs rule!( (dictionary_literal key: _* @keys value: _* @vals) => - (map_literal element: {..keys.into_iter().zip(vals).map(|(k, v)| + (map_literal element: {keys.into_iter().zip(vals).map(|(k, v)| tree!((key_value_pair key: {k} value: {v})) )}) ), - rule!((dictionary_literal element: _* @elems) => (map_literal element: {..elems})), + rule!((dictionary_literal element: _* @elems) => (map_literal element: {elems})), rule!((dictionary_literal_item key: @k value: @v) => (key_value_pair key: {k} value: {v})), // ---- Optionals and errors ---- // Optional chaining — unwrap the marker @@ -753,8 +753,8 @@ fn translation_rules() -> Vec> { (do_statement body: (block statement: _* @body) catch: (catch_block)* @catches) => (try_expr - body: (block stmt: {..body}) - catch_clause: {..catches}) + body: (block stmt: {body}) + catch_clause: {catches}) ), // Catch block with bound identifier; optional where-clause guard. rule!( @@ -766,14 +766,14 @@ fn translation_rules() -> Vec> { => (catch_clause pattern: {pattern} - guard: {..guard} - body: (block stmt: {..body})) + guard: {guard} + body: (block stmt: {body})) ), // Catch block without error binding rule!( (catch_block keyword: (catch_keyword) body: (block statement: _* @body)) => - (catch_clause body: (block stmt: {..body})) + (catch_clause body: (block stmt: {body})) ), // Empty catch block: catch {} rule!( @@ -787,7 +787,7 @@ fn translation_rules() -> Vec> { => (catch_clause pattern: {pat} - body: (block stmt: {..body})) + body: (block stmt: {body})) ), // As expression (type cast) — as?, as! rule!((as_expression (as_operator) @op expr: @val type: @ty) => (type_cast_expr expr: {val} operator: (infix_operator #{op}) type: {ty})), @@ -812,7 +812,7 @@ fn translation_rules() -> Vec> { pattern: (name_pattern identifier: (identifier #{parts.last().unwrap()})) imported_expr: {name} modifier: (modifier #{kind}) - modifier: {..mods}) + modifier: {mods}) ), // Non-scoped import declaration (for example `import Foundation`): // flatten the identifier parts into a member_access_expr and use a @@ -823,7 +823,7 @@ fn translation_rules() -> Vec> { (import_declaration pattern: (bulk_importing_pattern) imported_expr: {name} - modifier: {..mods}) + modifier: {mods}) ), // ---- Types and classes ---- // Self expression → name_expr @@ -831,7 +831,7 @@ fn translation_rules() -> Vec> { // Super expression → super_expr rule!((super_expression) => (super_expr)), // Modifiers — unwrap to individual modifier children - rule!((modifiers _* @mods) => {..mods}), + rule!((modifiers _* @mods) => {mods}), rule!((attribute) @m => (modifier #{m})), rule!((visibility_modifier) @m => (modifier #{m})), rule!((function_modifier) @m => (modifier #{m})), @@ -848,7 +848,7 @@ fn translation_rules() -> Vec> { // Keep a conservative textual fallback to avoid dropping type information. rule!((user_type) @ty => (named_type_expr name: (identifier #{ty}))), // Tuple type → tuple_type_expr - rule!((tuple_type element: _* @elems) => (tuple_type_expr element: {..elems})), + rule!((tuple_type element: _* @elems) => (tuple_type_expr element: {elems})), rule!((tuple_type_item name: @name type: @ty) => (tuple_type_element name: (identifier #{name}) type: {ty})), rule!((tuple_type_item type: @ty) => (tuple_type_element type: {ty})), // Array type `[T]` → generic_type_expr with Array base @@ -865,7 +865,7 @@ fn translation_rules() -> Vec> { base: (named_type_expr name: (identifier "Optional")) type_argument: {w})), // Function type `(Params) -> Ret` → function_type_expr. - rule!((function_type parameter: _* @ps return_type: @ret) => (function_type_expr parameter: {..ps} return_type: {ret})), + rule!((function_type parameter: _* @ps return_type: @ret) => (function_type_expr parameter: {ps} return_type: {ret})), rule!((function_type_parameter name: @name type: @ty) => (parameter external_name: (identifier #{name}) type: {ty})), rule!((function_type_parameter type: @ty) => (parameter type: {ty})), // Selector expression: `#selector(inner)` -- not yet supported @@ -889,10 +889,10 @@ fn translation_rules() -> Vec> { => (class_like_declaration modifier: (modifier #{kind}) - modifier: {..mods} + modifier: {mods} name: (identifier #{name}) - base_type: {..bases} - member: {..members}) + base_type: {bases} + member: {members}) ), // Enum class declaration: same as a regular class but with an enum body. rule!( @@ -905,10 +905,10 @@ fn translation_rules() -> Vec> { => (class_like_declaration modifier: (modifier #{kind}) - modifier: {..mods} + modifier: {mods} name: (identifier #{name}) - base_type: {..bases} - member: {..members}) + base_type: {bases} + member: {members}) ), // Class declaration with empty body rule!( @@ -921,9 +921,9 @@ fn translation_rules() -> Vec> { => (class_like_declaration modifier: (modifier #{kind}) - modifier: {..mods} + modifier: {mods} name: (identifier #{name}) - base_type: {..bases}) + base_type: {bases}) ), // Protocol declaration rule!( @@ -935,10 +935,10 @@ fn translation_rules() -> Vec> { => (class_like_declaration modifier: (modifier "protocol") - modifier: {..mods} + modifier: {mods} name: (identifier #{name}) - base_type: {..bases} - member: {..members}) + base_type: {bases} + member: {members}) ), // Protocol function — return type and body statements both optional. rule!( @@ -950,11 +950,11 @@ fn translation_rules() -> Vec> { (modifiers)* @mods) => (function_declaration - modifier: {..mods} + modifier: {mods} name: (identifier #{name}) - parameter: {..params} - return_type: {..ret} - body: (block stmt: {..body_stmts})) + parameter: {params} + return_type: {ret} + body: (block stmt: {body_stmts})) ), // Init declaration → constructor_declaration. Body statements optional; // body itself is also optional (protocol requirement). @@ -965,9 +965,9 @@ fn translation_rules() -> Vec> { (modifiers)* @mods) => (constructor_declaration - modifier: {..mods} - parameter: {..params} - body: (block stmt: {..body_stmts})) + modifier: {mods} + parameter: {params} + body: (block stmt: {body_stmts})) ), // Deinit declaration → destructor_declaration. Body statements optional. rule!( @@ -976,15 +976,15 @@ fn translation_rules() -> Vec> { (modifiers)* @mods) => (destructor_declaration - modifier: {..mods} - body: (block stmt: {..body_stmts})) + modifier: {mods} + body: (block stmt: {body_stmts})) ), // Typealias declaration rule!( (typealias_declaration name: @name value: @val (modifiers)* @mods) => (type_alias_declaration - modifier: {..mods} + modifier: {mods} name: (identifier #{name}) r#type: {val}) ), @@ -999,9 +999,9 @@ fn translation_rules() -> Vec> { (associatedtype_declaration name: @name inherits_from: _? @bound (modifiers)* @mods) => (associated_type_declaration - modifier: {..mods} + modifier: {mods} name: (identifier #{name}) - bound: {..bound}) + bound: {bound}) ), // Protocol property declaration: translate each accessor // requirement to an `accessor_declaration` carrying the property @@ -1018,7 +1018,7 @@ fn translation_rules() -> Vec> { type: _? @ty (modifiers)* @mods) => - {..{ + {{ ctx.property_name = Some(tree!((identifier #{name}))); ctx.property_type = ty; ctx.outer_modifiers = mods; @@ -1040,23 +1040,23 @@ fn translation_rules() -> Vec> { => (accessor_declaration name: {ctx.property_name.ok_or("getter_specifier outside protocol_property_declaration context")?} - type: {..ctx.property_type} + type: {ctx.property_type} accessor_kind: (accessor_kind "get") - modifier: {..ctx.outer_modifiers.clone()} - modifier: {..chained_modifier(&mut ctx)}) + modifier: {ctx.outer_modifiers.clone()} + modifier: {chained_modifier(&mut ctx)}) ), rule!( (setter_specifier) => (accessor_declaration name: {ctx.property_name.ok_or("setter_specifier outside protocol_property_declaration context")?} - type: {..ctx.property_type} + type: {ctx.property_type} accessor_kind: (accessor_kind "set") - modifier: {..ctx.outer_modifiers.clone()} - modifier: {..chained_modifier(&mut ctx)}) + modifier: {ctx.outer_modifiers.clone()} + modifier: {chained_modifier(&mut ctx)}) ), // protocol_property_requirements wrapper — should be consumed by above; fallback - rule!((protocol_property_requirements accessor: _* @accs) => {..accs}), + rule!((protocol_property_requirements accessor: _* @accs) => {accs}), // Computed getter → accessor_declaration (body optional). // Reads property name/type from the outer property_binding rule // and binding/outer modifiers + chained tag from the outer @@ -1065,58 +1065,58 @@ fn translation_rules() -> Vec> { (computed_getter body: (block statement: _* @body)?) => (accessor_declaration - modifier: {..ctx.binding_modifier} - modifier: {..ctx.outer_modifiers.clone()} - modifier: {..chained_modifier(&mut ctx)} + modifier: {ctx.binding_modifier} + modifier: {ctx.outer_modifiers.clone()} + modifier: {chained_modifier(&mut ctx)} name: {ctx.property_name.ok_or("computed_getter outside property_binding context")?} - type: {..ctx.property_type} + type: {ctx.property_type} accessor_kind: (accessor_kind "get") - body: (block stmt: {..body})) + body: (block stmt: {body})) ), // Computed setter with explicit parameter name. rule!( (computed_setter parameter: @param body: (block statement: _* @body)) => (accessor_declaration - modifier: {..ctx.binding_modifier} - modifier: {..ctx.outer_modifiers.clone()} - modifier: {..chained_modifier(&mut ctx)} + modifier: {ctx.binding_modifier} + modifier: {ctx.outer_modifiers.clone()} + modifier: {chained_modifier(&mut ctx)} name: {ctx.property_name.ok_or("computed_setter outside property_binding context")?} - type: {..ctx.property_type} + type: {ctx.property_type} accessor_kind: (accessor_kind "set") parameter: (parameter pattern: (name_pattern identifier: (identifier #{param}))) - body: (block stmt: {..body})) + body: (block stmt: {body})) ), // Computed setter without explicit parameter name; body optional. rule!( (computed_setter body: (block statement: _* @body)?) => (accessor_declaration - modifier: {..ctx.binding_modifier} - modifier: {..ctx.outer_modifiers.clone()} - modifier: {..chained_modifier(&mut ctx)} + modifier: {ctx.binding_modifier} + modifier: {ctx.outer_modifiers.clone()} + modifier: {chained_modifier(&mut ctx)} name: {ctx.property_name.ok_or("computed_setter outside property_binding context")?} - type: {..ctx.property_type} + type: {ctx.property_type} accessor_kind: (accessor_kind "set") - body: (block stmt: {..body})) + body: (block stmt: {body})) ), // Computed modify → accessor_declaration rule!( (computed_modify body: (block statement: _* @body)) => (accessor_declaration - modifier: {..ctx.binding_modifier} - modifier: {..ctx.outer_modifiers.clone()} - modifier: {..chained_modifier(&mut ctx)} + modifier: {ctx.binding_modifier} + modifier: {ctx.outer_modifiers.clone()} + modifier: {chained_modifier(&mut ctx)} name: {ctx.property_name.ok_or("computed_modify outside property_binding context")?} - type: {..ctx.property_type} + type: {ctx.property_type} accessor_kind: (accessor_kind "modify") - body: (block stmt: {..body})) + body: (block stmt: {body})) ), // willset/didset block — spread to children (only reachable as a // fallback; the outer property_binding manual rule normally // captures the willset/didset clauses directly). - rule!((willset_didset_block _* @clauses) => {..clauses}), + rule!((willset_didset_block _* @clauses) => {clauses}), // willset clause → accessor_declaration (body optional). Reads // `ctx.property_name` set by the outer property_binding rule and // binding/outer modifiers + chained tag from the outer @@ -1125,24 +1125,24 @@ fn translation_rules() -> Vec> { (willset_clause body: (block statement: _* @body)?) => (accessor_declaration - modifier: {..ctx.binding_modifier} - modifier: {..ctx.outer_modifiers.clone()} - modifier: {..chained_modifier(&mut ctx)} + modifier: {ctx.binding_modifier} + modifier: {ctx.outer_modifiers.clone()} + modifier: {chained_modifier(&mut ctx)} name: {ctx.property_name.ok_or("willset_clause outside property_binding context")?} accessor_kind: (accessor_kind "willSet") - body: (block stmt: {..body})) + body: (block stmt: {body})) ), // didset clause → accessor_declaration (body optional). rule!( (didset_clause body: (block statement: _* @body)?) => (accessor_declaration - modifier: {..ctx.binding_modifier} - modifier: {..ctx.outer_modifiers.clone()} - modifier: {..chained_modifier(&mut ctx)} + modifier: {ctx.binding_modifier} + modifier: {ctx.outer_modifiers.clone()} + modifier: {chained_modifier(&mut ctx)} name: {ctx.property_name.ok_or("didset_clause outside property_binding context")?} accessor_kind: (accessor_kind "didSet") - body: (block stmt: {..body})) + body: (block stmt: {body})) ), // Preprocessor conditionals — unsupported rule!((diagnostic) => (unsupported_node)), From e59f6468709e8d971474b960ae75ee3c70dde20a Mon Sep 17 00:00:00 2001 From: Taus Date: Fri, 26 Jun 2026 14:02:25 +0000 Subject: [PATCH 03/10] yeast: Remove dead Captures methods `Captures::map_captures`, `Captures::map_captures_to`, and `Captures::try_map_all_captures` had no callers. The last one was subsumed by `try_map_captures_except` (which takes a skip list and degenerates to the old behaviour when the list is empty). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- shared/yeast/src/captures.rs | 46 +++++++----------------------------- 1 file changed, 9 insertions(+), 37 deletions(-) diff --git a/shared/yeast/src/captures.rs b/shared/yeast/src/captures.rs index 101ab329220d..93141ef0084e 100644 --- a/shared/yeast/src/captures.rs +++ b/shared/yeast/src/captures.rs @@ -54,37 +54,15 @@ impl Captures { self.captures.entry(key).or_default().push(id); } - pub fn map_captures(&mut self, kind: &str, f: &mut impl FnMut(Id) -> Id) { - if let Some(ids) = self.captures.get_mut(kind) { - for id in ids { - *id = f(*id); - } - } - } - - /// Apply a fallible function to every captured id (across all keys), - /// replacing each id with the results. A function returning an empty - /// vector removes the capture; returning multiple ids splices them - /// into the capture's value list (suitable for `*`/`+` captures). - /// Stops and returns the error on the first failure. - pub fn try_map_all_captures( - &mut self, - mut f: impl FnMut(Id) -> Result, E>, - ) -> Result<(), E> { - for ids in self.captures.values_mut() { - let mut new_ids = Vec::with_capacity(ids.len()); - for &id in ids.iter() { - new_ids.extend(f(id)?); - } - *ids = new_ids; - } - Ok(()) - } - - /// Like [`try_map_all_captures`] but leaves captures whose name appears - /// in `skip` untouched. Used by the `rule!` macro to support `@@name` - /// (raw) captures alongside the default auto-translated `@name` - /// captures. + /// Apply a fallible function to every captured id, replacing each id + /// with the results. A function returning an empty vector removes + /// the capture; returning multiple ids splices them into the + /// capture's value list (suitable for `*`/`+` captures). Captures + /// whose name appears in `skip` are left untouched. Stops and + /// returns the error on the first failure. + /// + /// Used by the `rule!` macro's auto-translate prefix to translate + /// every capture except those marked `@@name` (raw). pub fn try_map_captures_except( &mut self, skip: &[&str], @@ -102,12 +80,6 @@ impl Captures { } Ok(()) } - pub fn map_captures_to(&mut self, from: &str, to: &'static str, f: &mut impl FnMut(Id) -> Id) { - if let Some(from_ids) = self.captures.get(from) { - let new_values = from_ids.iter().copied().map(f).collect(); - self.captures.insert(to, new_values); - } - } pub fn merge(&mut self, other: &Captures) { for (key, ids) in &other.captures { From b3dc7009a4705ff5f00b252fe9d517adcd3869fe Mon Sep 17 00:00:00 2001 From: Taus Date: Fri, 26 Jun 2026 14:03:11 +0000 Subject: [PATCH 04/10] yeast: Remove dead `BuildCtx::translate_opt` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `translate_opt` was a convenience for the manual_rule! body code, collapsing `Option` to `Option` via `translate`. Since the `@@` raw-capture migration replaced manual_rule! with rule!, no callers remain — the auto-translate prefix handles `Option` captures directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- shared/yeast/src/build.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/shared/yeast/src/build.rs b/shared/yeast/src/build.rs index 7875942f9c15..cda78e69793f 100644 --- a/shared/yeast/src/build.rs +++ b/shared/yeast/src/build.rs @@ -186,20 +186,6 @@ impl BuildCtx<'_, C> { None => Err("translate() called on a BuildCtx without a translator handle".into()), } } - - /// Translate an optional capture, returning the first translated id or - /// `None`. Convenience for `?`-quantifier captures (`Option`). - /// - /// If the underlying translation produces multiple ids for a single - /// input, only the first is returned. For most use cases (e.g. - /// translating a single type annotation) this is what you want; if - /// you need all ids, use [`translate`] directly. - pub fn translate_opt>(&mut self, id: Option) -> Result, String> { - match id { - Some(id) => Ok(self.translate(id)?.into_iter().next()), - None => Ok(None), - } - } } impl std::ops::Deref for BuildCtx<'_, C> { From b6abfe6e5cfa0d8af77ec076cf05ec26ea425f37 Mon Sep 17 00:00:00 2001 From: Taus Date: Fri, 26 Jun 2026 14:04:02 +0000 Subject: [PATCH 05/10] yeast: Remove dead `prepend_field` / `prepend_field_child` `BuildCtx::prepend_field` and the underlying `Ast::prepend_field_child` existed to support the create-then-mutate pattern in swift.rs (build an output node, then prepend modifiers to its `modifier:` field). The SwiftContext-based refactor on the previous branches eliminated all such call sites: every emitted declaration now carries its modifiers from birth, so the in-place prepend operation has no users. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- shared/yeast/src/build.rs | 9 --------- shared/yeast/src/lib.rs | 9 --------- 2 files changed, 18 deletions(-) diff --git a/shared/yeast/src/build.rs b/shared/yeast/src/build.rs index cda78e69793f..a5f454a5f544 100644 --- a/shared/yeast/src/build.rs +++ b/shared/yeast/src/build.rs @@ -158,15 +158,6 @@ impl<'a, C> BuildCtx<'a, C> { self.ast .create_named_token_with_range(kind, generated, self.source_range) } - - /// Prepend a value to a field of an existing node. - pub fn prepend_field(&mut self, node_id: Id, field_name: &str, value_id: Id) { - let field_id = self - .ast - .field_id_for_name(field_name) - .unwrap_or_else(|| panic!("build: field '{field_name}' not found")); - self.ast.prepend_field_child(node_id, field_id, value_id); - } } impl BuildCtx<'_, C> { diff --git a/shared/yeast/src/lib.rs b/shared/yeast/src/lib.rs index 2c53f756fdff..8a880cef7a24 100644 --- a/shared/yeast/src/lib.rs +++ b/shared/yeast/src/lib.rs @@ -518,15 +518,6 @@ impl Ast { self.create_named_token_with_range(kind, content, None) } - /// Prepend a child id to the given field of the given node. - pub fn prepend_field_child(&mut self, node_id: Id, field_id: FieldId, value_id: Id) { - let node = self - .nodes - .get_mut(node_id.0) - .expect("prepend_field_child: invalid node id"); - node.fields.entry(field_id).or_default().insert(0, value_id); - } - pub fn create_named_token_with_range( &mut self, kind: &'static str, From 807bb51df78ae8508bc8945548d3b59a13c7393d Mon Sep 17 00:00:00 2001 From: Taus Date: Fri, 26 Jun 2026 14:05:19 +0000 Subject: [PATCH 06/10] yeast: Unify `Node::kind()` and `Node::kind_name()` Both accessors returned the same private `kind_name: &'static str` field; `kind_name()` is widely used (mainly by dump.rs and schema diagnostics) and `kind()` had only 2 internal callers in lib.rs and a handful in tests. Pick the more descriptive name and update the callers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/extractor/mod.rs | 2 +- shared/yeast/src/lib.rs | 8 +--- shared/yeast/tests/test.rs | 38 ++++++++++--------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/shared/tree-sitter-extractor/src/extractor/mod.rs b/shared/tree-sitter-extractor/src/extractor/mod.rs index b066fbc85b30..a704be9dd950 100644 --- a/shared/tree-sitter-extractor/src/extractor/mod.rs +++ b/shared/tree-sitter-extractor/src/extractor/mod.rs @@ -66,7 +66,7 @@ impl<'a> AstNode for Node<'a> { impl AstNode for yeast::Node { fn kind(&self) -> &str { - yeast::Node::kind(self) + yeast::Node::kind_name(self) } fn is_named(&self) -> bool { yeast::Node::is_named(self) diff --git a/shared/yeast/src/lib.rs b/shared/yeast/src/lib.rs index 8a880cef7a24..90dea060758a 100644 --- a/shared/yeast/src/lib.rs +++ b/shared/yeast/src/lib.rs @@ -623,10 +623,6 @@ pub struct Node { } impl Node { - pub fn kind(&self) -> &'static str { - self.kind_name - } - pub fn kind_name(&self) -> &'static str { self.kind_name } @@ -971,7 +967,7 @@ fn apply_repeating_rules_inner( )); } - let node_kind = ast.get_node(id).map(|n| n.kind()).unwrap_or(""); + let node_kind = ast.get_node(id).map(|n| n.kind_name()).unwrap_or(""); for rule in index.rules_for_kind(node_kind) { let rule_ptr = *rule as *const Rule; if Some(rule_ptr) == skip_rule { @@ -1090,7 +1086,7 @@ fn apply_one_shot_rules_inner( )); } - let node_kind = ast.get_node(id).map(|n| n.kind()).unwrap_or(""); + let node_kind = ast.get_node(id).map(|n| n.kind_name()).unwrap_or(""); for rule in index.rules_for_kind(node_kind) { if let Some(captures) = rule.try_match(ast, id)? { diff --git a/shared/yeast/tests/test.rs b/shared/yeast/tests/test.rs index 3cc02838fadf..57a9e17dbd4c 100644 --- a/shared/yeast/tests/test.rs +++ b/shared/yeast/tests/test.rs @@ -300,7 +300,7 @@ fn test_query_skips_extras_in_positional_match() { let mut cursor = AstCursor::new(&ast); cursor.goto_first_child(); let array_id = cursor.node_id(); - assert_eq!(ast.get_node(array_id).unwrap().kind(), "array"); + assert_eq!(ast.get_node(array_id).unwrap().kind_name(), "array"); // Two positional wildcards should bind to the two integers, skipping // the comment that sits between them. @@ -309,11 +309,15 @@ fn test_query_skips_extras_in_positional_match() { let matched = query.do_match(&ast, array_id, &mut captures).unwrap(); assert!(matched); assert_eq!( - ast.get_node(captures.get_var("a").unwrap()).unwrap().kind(), + ast.get_node(captures.get_var("a").unwrap()) + .unwrap() + .kind_name(), "integer" ); assert_eq!( - ast.get_node(captures.get_var("b").unwrap()).unwrap().kind(), + ast.get_node(captures.get_var("b").unwrap()) + .unwrap() + .kind_name(), "integer" ); } @@ -391,7 +395,7 @@ fn test_capture_unnamed_node_parenthesized() { assert!(matched); let op_id = captures.get_var("op").unwrap(); let op_node = ast.get_node(op_id).unwrap(); - assert_eq!(op_node.kind(), "="); + assert_eq!(op_node.kind_name(), "="); assert!(!op_node.is_named()); } @@ -414,7 +418,7 @@ fn test_capture_bare_underscore_repeated() { let all = captures.get_all("all"); assert_eq!(all.len(), 1); - assert_eq!(ast.get_node(all[0]).unwrap().kind(), "="); + assert_eq!(ast.get_node(all[0]).unwrap().kind_name(), "="); assert!(!ast.get_node(all[0]).unwrap().is_named()); } @@ -441,7 +445,7 @@ fn test_capture_unnamed_node_bare_literal() { assert!(matched); let op_id = captures.get_var("op").unwrap(); let op_node = ast.get_node(op_id).unwrap(); - assert_eq!(op_node.kind(), "="); + assert_eq!(op_node.kind_name(), "="); assert!(!op_node.is_named()); } @@ -479,7 +483,7 @@ fn test_bare_underscore_matches_unnamed() { .unwrap(); assert!(matched, "_ should match the unnamed `=`"); let any_node = ast.get_node(captures.get_var("any").unwrap()).unwrap(); - assert_eq!(any_node.kind(), "="); + assert_eq!(any_node.kind_name(), "="); assert!(!any_node.is_named()); } @@ -506,7 +510,7 @@ fn test_bare_forms_in_field_position() { assert_eq!( ast.get_node(captures.get_var("lhs").unwrap()) .unwrap() - .kind(), + .kind_name(), "identifier" ); @@ -516,7 +520,7 @@ fn test_bare_forms_in_field_position() { let matched = query.do_match(&ast, assignment_id, &mut captures).unwrap(); assert!(matched); let op = ast.get_node(captures.get_var("op").unwrap()).unwrap(); - assert_eq!(op.kind(), "="); + assert_eq!(op.kind_name(), "="); assert!(!op.is_named()); } @@ -535,7 +539,7 @@ fn test_forward_scan_finds_unnamed_token_late() { let mut cursor = AstCursor::new(&ast); cursor.goto_first_child(); // for cursor.goto_first_child(); // do (the body) - while cursor.node().kind() != "do" || !cursor.node().is_named() { + while cursor.node().kind_name() != "do" || !cursor.node().is_named() { assert!(cursor.goto_next_sibling(), "expected to find named `do`"); } let do_id = cursor.node_id(); @@ -545,7 +549,7 @@ fn test_forward_scan_finds_unnamed_token_late() { let matched = query.do_match(&ast, do_id, &mut captures).unwrap(); assert!(matched, "forward-scan should find the `end` keyword"); let kw = ast.get_node(captures.get_var("kw").unwrap()).unwrap(); - assert_eq!(kw.kind(), "end"); + assert_eq!(kw.kind_name(), "end"); assert!(!kw.is_named()); } @@ -561,7 +565,7 @@ fn test_forward_scan_preserves_order() { let mut cursor = AstCursor::new(&ast); cursor.goto_first_child(); cursor.goto_first_child(); - while cursor.node().kind() != "do" || !cursor.node().is_named() { + while cursor.node().kind_name() != "do" || !cursor.node().is_named() { assert!(cursor.goto_next_sibling(), "expected to find named `do`"); } let do_id = cursor.node_id(); @@ -1172,11 +1176,11 @@ fn test_cursor_navigation() { let mut cursor = AstCursor::new(&ast); // Start at root - assert_eq!(cursor.node().kind(), "program"); + assert_eq!(cursor.node().kind_name(), "program"); // Go to first child (assignment) assert!(cursor.goto_first_child()); - assert_eq!(cursor.node().kind(), "assignment"); + assert_eq!(cursor.node().kind_name(), "assignment"); // No sibling assert!(!cursor.goto_next_sibling()); @@ -1187,10 +1191,10 @@ fn test_cursor_navigation() { // Go back up assert!(cursor.goto_parent()); - assert_eq!(cursor.node().kind(), "assignment"); + assert_eq!(cursor.node().kind_name(), "assignment"); assert!(cursor.goto_parent()); - assert_eq!(cursor.node().kind(), "program"); + assert_eq!(cursor.node().kind_name(), "program"); // Can't go further up assert!(!cursor.goto_parent()); @@ -1307,7 +1311,7 @@ fn test_hash_brace_uses_capture_location_for_leaf() { let Some(node) = ast.get_node(id) else { continue; }; - if node.kind() == "identifier" && ast.source_text(id) == "bar" { + if node.kind_name() == "identifier" && ast.source_text(id) == "bar" { bar_ids.push(id); } } From 37c8111c18cb551faa9bb7a099b35f49847e5f60 Mon Sep 17 00:00:00 2001 From: Taus Date: Fri, 26 Jun 2026 14:06:12 +0000 Subject: [PATCH 07/10] yeast-macros: Add error message to defensive `expect_ident` in `parse_ctx_or_implicit` The empty error string passed to `expect_ident` was dead code (the preceding lookahead has already confirmed the token is an ident), but it would have been a confusing message if it ever fired. Replace with an explicit "unreachable" string that makes the intent clearer to readers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- shared/yeast-macros/src/parse.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shared/yeast-macros/src/parse.rs b/shared/yeast-macros/src/parse.rs index 2422d0a8a5cd..6e4de57a1419 100644 --- a/shared/yeast-macros/src/parse.rs +++ b/shared/yeast-macros/src/parse.rs @@ -304,7 +304,8 @@ fn parse_ctx_or_implicit(tokens: &mut Tokens) -> Ident { && matches!(lookahead.next(), Some(TokenTree::Punct(p)) if p.as_char() == ','); if is_explicit { - let ctx = expect_ident(tokens, "").unwrap(); + let ctx = expect_ident(tokens, "unreachable: ident was just peeked") + .expect("unreachable: ident was just peeked"); let _ = tokens.next(); // consume comma ctx } else { From bda8e7dae172de5934a5068ec0ebfc2864249434 Mon Sep 17 00:00:00 2001 From: Taus Date: Fri, 26 Jun 2026 15:09:44 +0000 Subject: [PATCH 08/10] yeast-macros: Remove unused `.map` and `.reduce_left` chain syntax The `{expr}.map(p -> tpl)` and `{expr}.reduce_left(first -> init, acc, elem -> fold)` post-fix chains on `{expr}` placeholders had no remaining users in the codebase: `.map` was never used, and the 4 `.reduce_left` sites in `swift.rs` were rewritten to plain `Iterator::reduce` via an `and_chain` helper in an earlier commit. Removes the entire `parse_chain_suffix` function (~90 lines) and the `has_chain` detection / dispatch branches at the two call sites (field-position in `parse_direct_node_inner` and body-position in `parse_direct_list`). The remaining `{expr}` path is the trait-dispatched one introduced by the splice-syntax cleanup, which handles single ids and iterables uniformly via `IntoFieldIds`. Also strips the chain syntax from the `tree!` macro doc comment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- shared/yeast-macros/src/lib.rs | 11 --- shared/yeast-macros/src/parse.rs | 137 ++----------------------------- 2 files changed, 9 insertions(+), 139 deletions(-) diff --git a/shared/yeast-macros/src/lib.rs b/shared/yeast-macros/src/lib.rs index 420f9fc70c63..7db97f9fb709 100644 --- a/shared/yeast-macros/src/lib.rs +++ b/shared/yeast-macros/src/lib.rs @@ -47,19 +47,8 @@ pub fn query(input: TokenStream) -> TokenStream { /// `Option`, iterator chains) splice /// their elements /// field: {expr} - extend a named field with `{expr}`'s ids -/// {expr}.map(p -> tpl) - apply tpl to each element; splice result -/// {expr}.reduce_left(f -> init, acc, e -> fold) -/// - fold with per-element init; splice 0 or 1 result /// ``` /// -/// Chain syntax after `{expr}`: -/// - `.map(param -> template)` — one output node per input element. -/// - `.reduce_left(first -> init, acc, elem -> fold)` — fold left; the first -/// element is converted by `init`, subsequent elements are folded by `fold` -/// with the accumulator bound to `acc`. An empty iterable yields nothing. -/// - Chains always splice (the result is iterable). -/// - Multiple chains can be chained, e.g. `.map(...).reduce_left(...)`. -/// /// Can be called with an explicit context or using the implicit context /// from an enclosing `rule!`: /// diff --git a/shared/yeast-macros/src/parse.rs b/shared/yeast-macros/src/parse.rs index 6e4de57a1419..2ab6236fdac9 100644 --- a/shared/yeast-macros/src/parse.rs +++ b/shared/yeast-macros/src/parse.rs @@ -430,37 +430,16 @@ fn parse_direct_node_inner(tokens: &mut Tokens, ctx: &Ident) -> Result = #chained.collect(); - }); - // An empty pipeline means the field is absent — skip it - // entirely rather than emitting an empty named field. - field_args.push(quote! { - if !#temp.is_empty() { __fields.push((#field_str, #temp)); } - }); - continue; - } - - // Plain `{expr}` — trait-dispatched extend. let group = expect_group(tokens, Delimiter::Brace)?; let expr = group.stream(); stmts.push(quote! { let mut #temp: Vec = Vec::new(); yeast::IntoFieldIds::extend_into({ #expr }, &mut #temp); }); + // An empty `{expr}` means the field is absent — skip it + // entirely rather than emitting an empty named field. field_args.push(quote! { if !#temp.is_empty() { __fields.push((#field_str, #temp)); } }); @@ -492,93 +471,6 @@ fn parse_direct_node_inner(tokens: &mut Tokens, ctx: &Ident) -> Result template) -- iterator map: produces Vec -/// ``` -/// -/// The chain may be empty (returns `base` unchanged). Multiple chained calls -/// are supported, e.g. `.map(p -> ...).map(q -> ...)`. -/// -/// Each call expects the receiver to be an iterator. The `base` argument -/// should therefore already be an iterator (use `.into_iter()` on it before -/// calling this function). -fn parse_chain_suffix(tokens: &mut Tokens, ctx: &Ident, base: TokenStream) -> Result { - let mut current = base; - while matches!(tokens.peek(), Some(TokenTree::Punct(p)) if p.as_char() == '.') { - tokens.next(); // consume . - let method = expect_ident(tokens, "expected method name after `.`")?; - let method_str = method.to_string(); - let args_group = expect_group(tokens, Delimiter::Parenthesis)?; - match method_str.as_str() { - "map" => { - let mut inner = args_group.stream().into_iter().peekable(); - let param = expect_ident(&mut inner, "expected lambda parameter name")?; - expect_punct(&mut inner, '-', "expected `->` after lambda parameter")?; - expect_punct(&mut inner, '>', "expected `->` after lambda parameter")?; - let body = parse_direct_node(&mut inner, ctx)?; - if let Some(tok) = inner.next() { - return Err(syn::Error::new_spanned( - tok, - "unexpected token after lambda body", - )); - } - current = quote! { - #current.map(|#param| #body) - }; - } - "reduce_left" => { - // Syntax: reduce_left(first -> init_tpl, acc, elem -> fold_tpl) - // - first -> init_tpl : converts the first element to the initial accumulator - // - acc, elem -> fold_tpl : fold step (acc = current accumulator, elem = next element) - // Empty iterator produces an empty iterator; non-empty produces a single-element iterator. - let mut inner = args_group.stream().into_iter().peekable(); - let init_param = expect_ident(&mut inner, "expected initial lambda parameter")?; - expect_punct(&mut inner, '-', "expected `->` after init parameter")?; - expect_punct(&mut inner, '>', "expected `->` after init parameter")?; - let init_body = parse_direct_node(&mut inner, ctx)?; - expect_punct(&mut inner, ',', "expected `,` after init template")?; - let acc_param = expect_ident(&mut inner, "expected accumulator parameter")?; - expect_punct(&mut inner, ',', "expected `,` after accumulator parameter")?; - let elem_param = expect_ident(&mut inner, "expected element parameter")?; - expect_punct(&mut inner, '-', "expected `->` after element parameter")?; - expect_punct(&mut inner, '>', "expected `->` after element parameter")?; - let fold_body = parse_direct_node(&mut inner, ctx)?; - if let Some(tok) = inner.next() { - return Err(syn::Error::new_spanned( - tok, - "unexpected token after fold template", - )); - } - current = quote! { - { - let mut __iter = #current; - let __result: Option = if let Some(#init_param) = __iter.next() { - let mut __acc: yeast::Id = #init_body; - for #elem_param in __iter { - let #acc_param: yeast::Id = __acc; - __acc = #fold_body; - } - Some(__acc) - } else { - None - }; - __result.into_iter() - } - }; - } - _ => { - return Err(syn::Error::new_spanned( - method, - format!("unknown builtin method `.{method_str}()`"), - )); - } - } - } - Ok(current) -} - /// Parse the top-level list of a `trees!` template. /// Each item is a node template or `{expr}` splice. fn parse_direct_list(tokens: &mut Tokens, ctx: &Ident) -> Result> { @@ -599,25 +491,14 @@ fn parse_direct_list(tokens: &mut Tokens, ctx: &Ident) -> Result Date: Fri, 26 Jun 2026 15:18:45 +0000 Subject: [PATCH 09/10] yeast: Delete the `Cursor` trait, inline its methods on `AstCursor` The trait had a single implementor (`AstCursor`), three type parameters of which one (`T`) was never used in any method signature, and one external consumer that needed `use yeast::Cursor;` in scope just to call methods on the cursor. The abstraction was overhead without a second implementor to justify it. Move the six trait methods to an inherent `impl AstCursor` block; delete `shared/yeast/src/cursor.rs`, the `pub mod cursor;` and `pub use cursor::Cursor;` lines in `lib.rs`, and the `use yeast::Cursor;` in `tree-sitter-extractor`'s `traverse_yeast`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/extractor/mod.rs | 1 - shared/yeast/src/cursor.rs | 8 --- shared/yeast/src/lib.rs | 55 +++++++++---------- 3 files changed, 26 insertions(+), 38 deletions(-) delete mode 100644 shared/yeast/src/cursor.rs diff --git a/shared/tree-sitter-extractor/src/extractor/mod.rs b/shared/tree-sitter-extractor/src/extractor/mod.rs index a704be9dd950..54b01ba5146e 100644 --- a/shared/tree-sitter-extractor/src/extractor/mod.rs +++ b/shared/tree-sitter-extractor/src/extractor/mod.rs @@ -882,7 +882,6 @@ fn emit_extras_in(visitor: &mut Visitor, node: Node<'_>) { } fn traverse_yeast(tree: &yeast::Ast, visitor: &mut Visitor) { - use yeast::Cursor; let mut cursor = tree.walk(); visitor.enter_node(cursor.node()); let mut recurse = true; diff --git a/shared/yeast/src/cursor.rs b/shared/yeast/src/cursor.rs deleted file mode 100644 index ef5f6d94f259..000000000000 --- a/shared/yeast/src/cursor.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub trait Cursor<'a, T, N, F> { - fn node(&self) -> &'a N; - fn field_id(&self) -> Option; - fn field_name(&self) -> Option<&'static str>; - fn goto_first_child(&mut self) -> bool; - fn goto_next_sibling(&mut self) -> bool; - fn goto_parent(&mut self) -> bool; -} diff --git a/shared/yeast/src/lib.rs b/shared/yeast/src/lib.rs index 90dea060758a..fdfe4dd0fb01 100644 --- a/shared/yeast/src/lib.rs +++ b/shared/yeast/src/lib.rs @@ -7,7 +7,6 @@ use serde_json::{json, Value}; pub mod build; pub mod captures; -pub mod cursor; pub mod dump; pub mod node_types_yaml; pub mod query; @@ -19,7 +18,6 @@ mod visitor; pub use yeast_macros::{query, rule, tree, trees}; use captures::Captures; -pub use cursor::Cursor; use query::QueryNode; /// Node id: an index into the [`Ast`] arena. A newtype around `usize` @@ -174,37 +172,16 @@ impl<'a> AstCursor<'a> { self.node_id } - fn goto_next_sibling_opt(&mut self) -> Option<()> { - self.node_id = self.parents.last_mut()?.1.next()?; - Some(()) - } - - fn goto_first_child_opt(&mut self) -> Option<()> { - let parent_id = self.node_id; - let parent = self.ast.get_node(parent_id)?; - let mut children = ChildrenIter::new(parent); - let first_child = children.next()?; - self.node_id = first_child; - self.parents.push((parent_id, children)); - Some(()) - } - - fn goto_parent_opt(&mut self) -> Option<()> { - self.node_id = self.parents.pop()?.0; - Some(()) - } -} -impl<'a> Cursor<'a, Ast, Node, FieldId> for AstCursor<'a> { - fn node(&self) -> &'a Node { + pub fn node(&self) -> &'a Node { &self.ast.nodes[self.node_id.0] } - fn field_id(&self) -> Option { + pub fn field_id(&self) -> Option { let (_, children) = self.parents.last()?; children.current_field() } - fn field_name(&self) -> Option<&'static str> { + pub fn field_name(&self) -> Option<&'static str> { if self.field_id() == Some(CHILD_FIELD) { None } else { @@ -213,17 +190,37 @@ impl<'a> Cursor<'a, Ast, Node, FieldId> for AstCursor<'a> { } } - fn goto_first_child(&mut self) -> bool { + pub fn goto_first_child(&mut self) -> bool { self.goto_first_child_opt().is_some() } - fn goto_next_sibling(&mut self) -> bool { + pub fn goto_next_sibling(&mut self) -> bool { self.goto_next_sibling_opt().is_some() } - fn goto_parent(&mut self) -> bool { + pub fn goto_parent(&mut self) -> bool { self.goto_parent_opt().is_some() } + + fn goto_next_sibling_opt(&mut self) -> Option<()> { + self.node_id = self.parents.last_mut()?.1.next()?; + Some(()) + } + + fn goto_first_child_opt(&mut self) -> Option<()> { + let parent_id = self.node_id; + let parent = self.ast.get_node(parent_id)?; + let mut children = ChildrenIter::new(parent); + let first_child = children.next()?; + self.node_id = first_child; + self.parents.push((parent_id, children)); + Some(()) + } + + fn goto_parent_opt(&mut self) -> Option<()> { + self.node_id = self.parents.pop()?.0; + Some(()) + } } /// An iterator over the child Ids of a node. From 041a8e6adce77242560fd791ea00ecca5c8c8290 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:26:07 +0000 Subject: [PATCH 10/10] Fix source_text call in @@raw_lhs documentation example --- shared/yeast/doc/yeast.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/yeast/doc/yeast.md b/shared/yeast/doc/yeast.md index 8ea2e67b2dec..8aa050592f6b 100644 --- a/shared/yeast/doc/yeast.md +++ b/shared/yeast/doc/yeast.md @@ -320,7 +320,7 @@ yeast::rule!( => { // raw_lhs is untranslated: read its original source text. - let text = ctx.ast.source_text(raw_lhs.into()); + let text = ctx.ast.source_text(raw_lhs); // rhs is already translated by the auto-translate prefix. tree!((call method: (identifier #{text.as_str()})