cairo_lang_syntax_codegen/
generator.rs

1use std::fs;
2use std::path::PathBuf;
3
4use genco::prelude::*;
5use xshell::Shell;
6
7use crate::cairo_spec::get_spec;
8use crate::spec::{Member, Node, NodeKind, Variant, Variants};
9
10pub fn project_root() -> PathBuf {
11    // This is the directory of Cargo.toml of the syntax_codegen crate.
12    let dir = env!("CARGO_MANIFEST_DIR");
13    // Pop the "/crates/cairo-lang-syntax-codegen" suffix.
14    let res = PathBuf::from(dir).parent().unwrap().parent().unwrap().to_owned();
15    assert!(res.join("Cargo.toml").exists(), "Could not find project root directory.");
16    res
17}
18
19pub fn ensure_file_content(filename: PathBuf, content: String) {
20    if let Ok(old_contents) = fs::read_to_string(&filename)
21        && old_contents == content
22    {
23        return;
24    }
25
26    fs::write(&filename, content).unwrap();
27}
28
29pub fn get_codes() -> Vec<(String, String)> {
30    vec![
31        (
32            "crates/cairo-lang-syntax/src/node/ast.rs".into(),
33            reformat_rust_code(generate_ast_code().to_string().unwrap()),
34        ),
35        (
36            "crates/cairo-lang-syntax/src/node/kind.rs".into(),
37            reformat_rust_code(generate_kinds_code().to_string().unwrap()),
38        ),
39        (
40            "crates/cairo-lang-syntax/src/node/key_fields.rs".into(),
41            reformat_rust_code(generate_key_fields_code().to_string().unwrap()),
42        ),
43    ]
44}
45
46pub fn reformat_rust_code(text: String) -> String {
47    // Since rustfmt is used with nightly features, it takes 2 runs to reach a fixed point.
48    reformat_rust_code_inner(reformat_rust_code_inner(text))
49}
50pub fn reformat_rust_code_inner(text: String) -> String {
51    let sh = Shell::new().unwrap();
52    let cmd = sh.cmd("rustfmt").env("RUSTUP_TOOLCHAIN", "nightly-2025-09-28");
53    let cmd_with_args = cmd.arg("--config-path").arg(project_root().join("rustfmt.toml"));
54    let mut stdout = cmd_with_args.stdin(text).read().unwrap();
55    if !stdout.ends_with('\n') {
56        stdout.push('\n');
57    }
58    stdout
59}
60
61fn generate_kinds_code() -> rust::Tokens {
62    let spec = get_spec();
63    let mut tokens = quote! {
64        $("// Autogenerated file. To regenerate, please run `cargo run --bin generate-syntax`.")
65        use core::fmt;
66        use serde::{Deserialize, Serialize};
67    };
68
69    // SyntaxKind.
70    let kinds = name_tokens(&spec, |k| !matches!(k, NodeKind::Enum { .. }));
71    let token_kinds = name_tokens(&spec, |k| matches!(k, NodeKind::Token { .. }));
72    let keyword_token_kinds =
73        name_tokens(&spec, |k| matches!(k, NodeKind::Token { is_keyword } if *is_keyword));
74    let terminal_kinds = name_tokens(&spec, |k| matches!(k, NodeKind::Terminal { .. }));
75    let keyword_terminal_kinds =
76        name_tokens(&spec, |k| matches!(k, NodeKind::Terminal { is_keyword, .. } if *is_keyword));
77
78    tokens.extend(quote! {
79        #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize, salsa::Update)]
80        pub enum SyntaxKind {
81            $(for t in kinds => $t,)
82        }
83        impl SyntaxKind {
84            pub fn is_token(&self) -> bool {
85                matches!(
86                    *self,
87                    $(for t in token_kinds join ( | ) => SyntaxKind::$t)
88                )
89            }
90            pub fn is_terminal(&self) -> bool {
91                matches!(
92                    *self,
93                    $(for t in terminal_kinds join ( | ) => SyntaxKind::$t)
94                )
95            }
96            pub fn is_keyword_token(&self) -> bool {
97                matches!(
98                    *self,
99                    $(for t in keyword_token_kinds join ( | ) => SyntaxKind::$t)
100                )
101            }
102            pub fn is_keyword_terminal(&self) -> bool {
103                matches!(
104                    *self,
105                    $(for t in keyword_terminal_kinds join ( | ) => SyntaxKind::$t)
106                )
107            }
108        }
109        impl fmt::Display for SyntaxKind {
110            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111                write!(f, "{self:?}")
112            }
113        }
114    });
115    tokens
116}
117
118/// Returns an iterator to the names of the tokens matching `predicate`.
119fn name_tokens(spec: &[Node], predicate: impl Fn(&NodeKind) -> bool) -> impl Iterator<Item = &str> {
120    spec.iter().filter(move |n| predicate(&n.kind)).map(|n| n.name.as_str())
121}
122
123fn generate_key_fields_code() -> rust::Tokens {
124    let spec = get_spec();
125    let mut arms = rust::Tokens::new();
126
127    for Node { name, kind } in spec {
128        match kind {
129            NodeKind::Struct { members } | NodeKind::Terminal { members, .. } => {
130                let mut fields = rust::Tokens::new();
131                let mut key_fields_range = 0..0;
132                for (i, member) in members.into_iter().enumerate() {
133                    let field_name = member.name;
134                    if member.key {
135                        if key_fields_range.is_empty() {
136                            key_fields_range = i..(i + 1);
137                        } else {
138                            assert_eq!(key_fields_range.end, i, "Key fields must be contiguous.");
139                            key_fields_range.end = i + 1;
140                        }
141                        if !fields.is_empty() {
142                            fields.extend(quote! { $(", ") });
143                        }
144                        fields.extend(quote!($field_name));
145                    }
146                }
147                if !fields.is_empty() {
148                    arms.extend(quote! {
149                        $("\n// Key fields:") $fields.$("\n")
150                    });
151                }
152                let key_fields_range =
153                    format!("{}..{}", key_fields_range.start, key_fields_range.end);
154                arms.extend(quote! {
155                    SyntaxKind::$name => $key_fields_range,
156                });
157            }
158            NodeKind::List { .. } | NodeKind::SeparatedList { .. } | NodeKind::Token { .. } => {
159                arms.extend(quote! {
160                    SyntaxKind::$name => 0..0,
161                });
162            }
163            NodeKind::Enum { .. } => {}
164        }
165    }
166    let tokens = quote! {
167        $("// Autogenerated file. To regenerate, please run `cargo run --bin generate-syntax`.")
168        use super::kind::SyntaxKind;
169        $("/// Gets the vector of children ids that are the indexing key for this SyntaxKind.")
170        $("///")
171        $("/// Each SyntaxKind has some children that are defined in the spec to be its indexing key")
172        $("/// for its stable pointer. See [super::stable_ptr].")
173        pub fn key_fields_range(kind: SyntaxKind) -> core::ops::Range<usize> {
174            match kind {
175                $arms
176            }
177        }
178    };
179    tokens
180}
181
182fn generate_ast_code() -> rust::Tokens {
183    let spec = get_spec();
184    let mut tokens = quote! {
185        $("// Autogenerated file. To regenerate, please run `cargo run --bin generate-syntax`.")
186        #![allow(clippy::match_single_binding)]
187        #![allow(clippy::too_many_arguments)]
188        #![allow(dead_code)]
189        #![allow(unused_variables)]
190        use std::ops::Deref;
191
192        use cairo_lang_filesystem::span::TextWidth;
193        use cairo_lang_filesystem::ids::SmolStrId;
194        use cairo_lang_utils::{extract_matches, Intern};
195        use salsa::Database;
196
197        use super::element_list::ElementList;
198        use super::green::GreenNodeDetails;
199        use super::kind::SyntaxKind;
200        use super::{
201            GreenId, GreenNode, SyntaxNode, SyntaxStablePtr, SyntaxStablePtrId, Terminal, Token, TypedStablePtr,
202            TypedSyntaxNode,
203        };
204        #[path = "ast_ext.rs"]
205        mod ast_ext;
206    };
207    let spec_clone = spec.clone();
208    let all_tokens: Vec<_> =
209        spec_clone.iter().filter(|node| matches!(node.kind, NodeKind::Terminal { .. })).collect();
210    for Node { name, kind } in spec {
211        tokens.extend(match kind {
212            NodeKind::Enum { variants, missing_variant } => {
213                let variants_list = match variants {
214                    Variants::List(variants) => variants,
215                    Variants::AllTokens => all_tokens
216                        .iter()
217                        .map(|node| Variant { name: node.name.clone(), kind: node.name.clone() })
218                        .collect(),
219                };
220                gen_enum_code(name, variants_list, missing_variant)
221            }
222            NodeKind::Struct { members } => gen_struct_code(name, members, false),
223            NodeKind::Terminal { members, .. } => gen_struct_code(name, members, true),
224            NodeKind::Token { .. } => gen_token_code(name),
225            NodeKind::List { element_type } => gen_list_code(name, element_type),
226            NodeKind::SeparatedList { element_type, separator_type } => {
227                gen_separated_list_code(name, element_type, separator_type)
228            }
229        });
230    }
231    tokens
232}
233
234fn gen_list_code(name: String, element_type: String) -> rust::Tokens {
235    // TODO(spapini): Change Deref to Borrow.
236    let ptr_name = format!("{name}Ptr");
237    let green_name = format!("{name}Green");
238    let element_green_name = format!("{element_type}Green");
239    let common_code = gen_common_list_code(&name, &green_name, &ptr_name);
240    quote! {
241        #[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
242        pub struct $(&name)<'db>(ElementList<'db, $(&element_type)<'db>, 1>);
243        impl<'db> Deref for $(&name)<'db>{
244            type Target = ElementList<'db, $(&element_type)<'db>, 1>;
245            fn deref(&self) -> &Self::Target {
246                &self.0
247            }
248        }
249        impl<'db> $(&name)<'db>{
250            pub fn new_green(
251                db: &'db dyn Database, children: &[$(&element_green_name)<'db>]
252            ) -> $(&green_name)<'db> {
253                let width = children.iter().map(|id|
254                    id.0.long(db).width(db)).sum();
255                $(&green_name)(GreenNode {
256                    kind: SyntaxKind::$(&name),
257                    details: GreenNodeDetails::Node {
258                        children: children.iter().map(|x| x.0).collect(),
259                        width,
260                    },
261                }.intern(db))
262            }
263        }
264        #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, salsa::Update)]
265        pub struct $(&ptr_name)<'db>(pub SyntaxStablePtrId<'db>);
266        impl<'db> TypedStablePtr<'db> for $(&ptr_name)<'db> {
267            type SyntaxNode = $(&name)<'db>;
268            fn untyped(self) -> SyntaxStablePtrId<'db> {
269                self.0
270            }
271            fn lookup(&self, db: &'db dyn Database) -> $(&name)<'db> {
272                $(&name)::from_syntax_node(db, self.0.lookup(db))
273            }
274        }
275        impl<'db> From<$(&ptr_name)<'db>> for SyntaxStablePtrId<'db> {
276            fn from(ptr: $(&ptr_name)<'db>) -> Self {
277                ptr.untyped()
278            }
279        }
280        $common_code
281    }
282}
283
284fn gen_separated_list_code(
285    name: String,
286    element_type: String,
287    separator_type: String,
288) -> rust::Tokens {
289    // TODO(spapini): Change Deref to Borrow.
290    let ptr_name = format!("{name}Ptr");
291    let green_name = format!("{name}Green");
292    let element_or_separator_green_name = format!("{name}ElementOrSeparatorGreen");
293    let element_green_name = format!("{element_type}Green");
294    let separator_green_name = format!("{separator_type}Green");
295    let common_code = gen_common_list_code(&name, &green_name, &ptr_name);
296    quote! {
297        #[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
298        pub struct $(&name)<'db>(ElementList<'db, $(&element_type)<'db>, 2>);
299        impl<'db> Deref for $(&name)<'db>{
300            type Target = ElementList<'db, $(&element_type)<'db>, 2>;
301            fn deref(&self) -> &Self::Target {
302                &self.0
303            }
304        }
305        impl<'db> $(&name)<'db>{
306            pub fn new_green(
307                db: &'db dyn Database, children: &[$(&element_or_separator_green_name)<'db>]
308            ) -> $(&green_name)<'db> {
309                let width = children.iter().map(|id|
310                    id.id().long(db).width(db)).sum();
311                $(&green_name)(GreenNode {
312                    kind: SyntaxKind::$(&name),
313                    details: GreenNodeDetails::Node {
314                        children: children.iter().map(|x| x.id()).collect(),
315                        width,
316                    },
317                }.intern(db))
318            }
319        }
320        #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, salsa::Update)]
321        pub struct $(&ptr_name)<'db>(pub SyntaxStablePtrId<'db>);
322        impl<'db> TypedStablePtr<'db> for $(&ptr_name)<'db> {
323            type SyntaxNode = $(&name)<'db>;
324            fn untyped(self) -> SyntaxStablePtrId<'db> {
325                self.0
326            }
327            fn lookup(&self, db: &'db dyn Database) -> $(&name)<'db> {
328                $(&name)::from_syntax_node(db, self.0.lookup(db))
329            }
330        }
331        impl<'db> From<$(&ptr_name)<'db>> for SyntaxStablePtrId<'db> {
332            fn from(ptr: $(&ptr_name)<'db>) -> Self {
333                ptr.untyped()
334            }
335        }
336        #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, salsa::Update)]
337        pub enum $(&element_or_separator_green_name)<'db> {
338            Separator($(&separator_green_name)<'db>),
339            Element($(&element_green_name)<'db>),
340        }
341        impl<'db> From<$(&separator_green_name)<'db>> for $(&element_or_separator_green_name)<'db> {
342            fn from(value: $(&separator_green_name)<'db>) -> Self {
343                $(&element_or_separator_green_name)::Separator(value)
344            }
345        }
346        impl<'db> From<$(&element_green_name)<'db>> for $(&element_or_separator_green_name)<'db> {
347            fn from(value: $(&element_green_name)<'db>) -> Self {
348                $(&element_or_separator_green_name)::Element(value)
349            }
350        }
351        impl<'db> $(&element_or_separator_green_name)<'db> {
352            fn id(&self) -> GreenId<'db> {
353                match self {
354                    $(&element_or_separator_green_name)::Separator(green) => green.0,
355                    $(&element_or_separator_green_name)::Element(green) => green.0,
356                }
357            }
358        }
359        $common_code
360    }
361}
362
363fn gen_common_list_code(name: &str, green_name: &str, ptr_name: &str) -> rust::Tokens {
364    quote! {
365        #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, salsa::Update)]
366        pub struct $green_name<'db>(pub GreenId<'db>);
367        impl<'db> TypedSyntaxNode<'db> for $name<'db> {
368            const OPTIONAL_KIND: Option<SyntaxKind> = Some(SyntaxKind::$name);
369            type StablePtr = $ptr_name<'db>;
370            type Green = $green_name<'db>;
371            fn missing(db: &'db dyn Database) -> Self::Green {
372                $green_name(
373                    GreenNode {
374                        kind: SyntaxKind::$name,
375                        details: GreenNodeDetails::Node { children: [].into(), width: TextWidth::default() },
376                    }.intern(db)
377                )
378            }
379            fn from_syntax_node(db: &'db dyn Database, node: SyntaxNode<'db>) -> Self {
380                Self(ElementList::new(node))
381            }
382            fn cast(db: &'db dyn Database, node: SyntaxNode<'db>) -> Option<Self> {
383                if node.kind(db) == SyntaxKind::$name {
384                    Some(Self(ElementList::new(node)))
385                } else {
386                    None
387                }
388            }
389            fn as_syntax_node(&self) -> SyntaxNode<'db> {
390                self.node
391            }
392            fn stable_ptr(&self, db: &'db dyn Database) -> Self::StablePtr {
393                $ptr_name(self.node.stable_ptr(db))
394            }
395        }
396    }
397}
398
399#[expect(clippy::literal_string_with_formatting_args)]
400fn gen_enum_code(
401    name: String,
402    variants: Vec<Variant>,
403    missing_variant: Option<Variant>,
404) -> rust::Tokens {
405    let ptr_name = format!("{name}Ptr");
406    let green_name = format!("{name}Green");
407    let mut enum_body = quote! {};
408    let mut from_node_body = quote! {};
409    let mut cast_body = quote! {};
410    let mut ptr_conversions = quote! {};
411    let mut green_conversions = quote! {};
412    for variant in &variants {
413        let n = &variant.name;
414        let k = &variant.kind;
415
416        enum_body.extend(quote! {
417            $n($k<'db>),
418        });
419        from_node_body.extend(quote! {
420            SyntaxKind::$k => $(&name)::$n($k::from_syntax_node(db, node)),
421        });
422        cast_body.extend(quote! {
423            SyntaxKind::$k => Some($(&name)::$n($k::from_syntax_node(db, node))),
424        });
425        let variant_ptr = format!("{k}Ptr");
426        ptr_conversions.extend(quote! {
427            impl<'db> From<$(&variant_ptr)<'db>> for $(&ptr_name)<'db> {
428                fn from(value: $(&variant_ptr)<'db>) -> Self {
429                    Self(value.0)
430                }
431            }
432        });
433        let variant_green = format!("{k}Green");
434        green_conversions.extend(quote! {
435            impl<'db> From<$(&variant_green)<'db>> for $(&green_name)<'db> {
436                fn from(value: $(&variant_green)<'db>) -> Self {
437                    Self(value.0)
438                }
439            }
440        });
441    }
442    let missing_body = match missing_variant {
443        Some(missing) => quote! {
444            $(&green_name)($(missing.kind)::missing(db).0)
445        },
446        None => quote! {
447            panic!("No missing variant.");
448        },
449    };
450    quote! {
451        #[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
452        pub enum $(&name)<'db>{
453            $enum_body
454        }
455        #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, salsa::Update)]
456        pub struct $(&ptr_name)<'db>(pub SyntaxStablePtrId<'db>);
457        impl<'db> TypedStablePtr<'db> for $(&ptr_name)<'db> {
458            type SyntaxNode = $(&name)<'db>;
459            fn untyped(self) -> SyntaxStablePtrId<'db> {
460                self.0
461            }
462            fn lookup(&self, db: &'db dyn Database) -> Self::SyntaxNode {
463                $(&name)::from_syntax_node(db, self.0.lookup(db))
464            }
465        }
466        impl<'db> From<$(&ptr_name)<'db>> for SyntaxStablePtrId<'db> {
467            fn from(ptr: $(&ptr_name)<'db>) -> Self {
468                ptr.untyped()
469            }
470        }
471        $ptr_conversions
472        $green_conversions
473        #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, salsa::Update)]
474        pub struct $(&green_name)<'db>(pub GreenId<'db>);
475        impl<'db> TypedSyntaxNode<'db> for $(&name)<'db>{
476            const OPTIONAL_KIND: Option<SyntaxKind> = None;
477            type StablePtr = $(&ptr_name)<'db>;
478            type Green = $(&green_name)<'db>;
479            fn missing(db: &'db dyn Database) -> Self::Green {
480                $missing_body
481            }
482            fn from_syntax_node(db: &'db dyn Database, node: SyntaxNode<'db>) -> Self {
483                let kind = node.kind(db);
484                match kind{
485                    $from_node_body
486                    _ => panic!(
487                        "Unexpected syntax kind {:?} when constructing {}.",
488                        kind,
489                        $[str]($[const](&name))),
490                }
491            }
492            fn cast(db: &'db dyn Database, node: SyntaxNode<'db>) -> Option<Self> {
493                let kind = node.kind(db);
494                match kind {
495                    $cast_body
496                    _ => None,
497                }
498            }
499            fn as_syntax_node(&self) -> SyntaxNode<'db> {
500                match self {
501                    $(for v in &variants => $(&name)::$(&v.name)(x) => x.as_syntax_node(),)
502                }
503            }
504            fn stable_ptr(&self, db: &'db dyn Database) -> Self::StablePtr {
505                $(&ptr_name)(self.as_syntax_node().long(db).stable_ptr)
506            }
507        }
508        impl<'db> $(&name)<'db> {
509            $("/// Checks if a kind of a variant of [")$(&name)$("].")
510            pub fn is_variant(kind: SyntaxKind) -> bool {
511                matches!(kind, $(for v in &variants join (|) => SyntaxKind::$(&v.kind)))
512            }
513        }
514    }
515}
516
517#[expect(clippy::literal_string_with_formatting_args)]
518fn gen_token_code(name: String) -> rust::Tokens {
519    let green_name = format!("{name}Green");
520    let ptr_name = format!("{name}Ptr");
521
522    quote! {
523        #[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
524        pub struct $(&name)<'db> {
525            node: SyntaxNode<'db>,
526        }
527        impl<'db> Token<'db> for $(&name)<'db> {
528            fn new_green(db: &'db dyn Database, text: SmolStrId<'db>) -> Self::Green {
529                $(&green_name)(GreenNode {
530                    kind: SyntaxKind::$(&name),
531                    details: GreenNodeDetails::Token(text),
532                }.intern(db))
533            }
534            fn text(&self, db: &'db dyn Database) -> SmolStrId<'db> {
535                *extract_matches!(&self.node.long(db).green.long(db).details,
536                    GreenNodeDetails::Token)
537            }
538        }
539        #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, salsa::Update)]
540        pub struct $(&ptr_name)<'db>(pub SyntaxStablePtrId<'db>);
541        impl<'db> TypedStablePtr<'db> for $(&ptr_name)<'db> {
542            type SyntaxNode = $(&name)<'db>;
543            fn untyped(self) -> SyntaxStablePtrId<'db> {
544                self.0
545            }
546            fn lookup(&self, db: &'db dyn Database) -> $(&name)<'db> {
547                $(&name)::from_syntax_node(db, self.0.lookup(db))
548            }
549        }
550        impl<'db> From<$(&ptr_name)<'db>> for SyntaxStablePtrId<'db> {
551            fn from(ptr: $(&ptr_name)<'db>) -> Self {
552                ptr.untyped()
553            }
554        }
555        #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, salsa::Update)]
556        pub struct $(&green_name)<'db>(pub GreenId<'db>);
557        impl<'db> $(&green_name)<'db> {
558            pub fn text(&self, db: &'db dyn Database) -> SmolStrId<'db> {
559                *extract_matches!(&self.0.long(db).details, GreenNodeDetails::Token)
560            }
561        }
562        impl<'db> TypedSyntaxNode<'db> for $(&name)<'db>{
563            const OPTIONAL_KIND: Option<SyntaxKind> = Some(SyntaxKind::$(&name));
564            type StablePtr = $(&ptr_name)<'db>;
565            type Green = $(&green_name)<'db>;
566            fn missing(db: &'db dyn Database) -> Self::Green {
567                $(&green_name)(GreenNode {
568                    kind: SyntaxKind::TokenMissing,
569                    details: GreenNodeDetails::Token(SmolStrId::from(db, "")),
570                }.intern(db))
571            }
572            fn from_syntax_node(db: &'db dyn Database, node: SyntaxNode<'db>) -> Self {
573                match node.long(db).green.long(db).details {
574                    GreenNodeDetails::Token(_) => Self { node },
575                    GreenNodeDetails::Node { .. } => panic!(
576                        "Expected a token {:?}, not an internal node",
577                        SyntaxKind::$(&name)
578                    ),
579                }
580            }
581            fn cast(db: &'db dyn Database, node: SyntaxNode<'db>) -> Option<Self> {
582                match node.long(db).green.long(db).details {
583                    GreenNodeDetails::Token(_) => Some(Self { node }),
584                    GreenNodeDetails::Node { .. } => None,
585                }
586            }
587            fn as_syntax_node(&self) -> SyntaxNode<'db> {
588                self.node
589            }
590            fn stable_ptr(&self, db: &'db dyn Database) -> Self::StablePtr {
591                $(&ptr_name)(self.node.stable_ptr(db))
592            }
593        }
594    }
595}
596
597#[expect(clippy::literal_string_with_formatting_args)]
598fn gen_struct_code(name: String, members: Vec<Member>, is_terminal: bool) -> rust::Tokens {
599    let green_name = format!("{name}Green");
600    let mut body = rust::Tokens::new();
601    let mut field_indices = quote! {};
602    let mut args = quote! {};
603    let mut params = quote! {};
604    let mut args_for_missing = quote! {};
605    let mut ptr_getters = quote! {};
606    let mut key_field_index: usize = 0;
607    for (i, Member { name, kind, key }) in members.iter().enumerate() {
608        let index_name = format!("INDEX_{}", name.to_uppercase());
609        field_indices.extend(quote! {
610            pub const $index_name : usize = $i;
611        });
612        let key_name_green = format!("{name}_green");
613        args.extend(quote! {$name.0,});
614        // TODO(spapini): Validate that children SyntaxKinds are as expected.
615
616        let child_green = format!("{kind}Green");
617        params.extend(quote! {$name: $(&child_green)<'db>,});
618        body.extend(quote! {
619            pub fn $name(&self, db: &'db dyn Database) -> $kind<'db> {
620                $kind::from_syntax_node(db, self.node.get_children(db)[$i])
621            }
622        });
623        args_for_missing.extend(quote! {$kind::missing(db).0,});
624
625        if *key {
626            ptr_getters.extend(quote! {
627                pub fn $(&key_name_green)(self, db: &'db dyn Database) -> $(&child_green)<'db> {
628                    let ptr = self.0.long(db);
629                    if let SyntaxStablePtr::Child { key_fields, .. } = ptr {
630                        $(&child_green)(key_fields[$key_field_index])
631                    } else {
632                        panic!("Unexpected key field query on root.");
633                    }
634                }
635            });
636            key_field_index += 1;
637        }
638    }
639    let ptr_name = format!("{name}Ptr");
640    let new_green_impl = if is_terminal {
641        let token_name = name.replace("Terminal", "Token");
642        quote! {
643            impl<'db> Terminal<'db> for $(&name)<'db> {
644                const KIND: SyntaxKind = SyntaxKind::$(&name);
645                type TokenType = $(&token_name)<'db>;
646                fn new_green(
647                    db: &'db dyn Database,
648                    leading_trivia: TriviaGreen<'db>,
649                    token: <<$(&name)<'db> as Terminal<'db>>::TokenType as TypedSyntaxNode<'db>>::Green,
650                    trailing_trivia: TriviaGreen<'db>
651                ) -> Self::Green {
652                    let children = [$args];
653                    let width =
654                        children.into_iter().map(|id: GreenId<'_>| id.long(db).width(db)).sum();
655                    $(&green_name)(GreenNode {
656                        kind: SyntaxKind::$(&name),
657                        details: GreenNodeDetails::Node { children: children.into(), width },
658                    }.intern(db))
659                }
660                fn text(&self, db: &'db dyn Database) -> SmolStrId<'db> {
661                    let GreenNodeDetails::Node{children,..} = &self.node.long(db).green.long(db).details else {
662                        unreachable!("Expected a node, not a token");
663                    };
664                    *extract_matches!(&children[1].long(db).details, GreenNodeDetails::Token)
665                }
666            }
667        }
668    } else {
669        quote! {
670            impl<'db> $(&name)<'db> {
671                $field_indices
672                pub fn new_green(db: &'db dyn Database, $params) -> $(&green_name)<'db> {
673                    let children = [$args];
674                    let width =
675                        children.into_iter().map(|id: GreenId<'_>| id.long(db).width(db)).sum();
676                    $(&green_name)(GreenNode {
677                        kind: SyntaxKind::$(&name),
678                        details: GreenNodeDetails::Node { children: children.into(), width },
679                    }.intern(db))
680                }
681            }
682        }
683    };
684    quote! {
685        #[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
686        pub struct $(&name)<'db> {
687            node: SyntaxNode<'db>,
688        }
689        $new_green_impl
690        impl<'db> $(&name)<'db> {
691            $body
692        }
693        #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, salsa::Update)]
694        pub struct $(&ptr_name)<'db>(pub SyntaxStablePtrId<'db>);
695        impl<'db> $(&ptr_name)<'db> {
696            $ptr_getters
697        }
698        impl<'db> TypedStablePtr<'db> for $(&ptr_name)<'db> {
699            type SyntaxNode = $(&name)<'db>;
700            fn untyped(self) -> SyntaxStablePtrId<'db> {
701                self.0
702            }
703            fn lookup(&self, db: &'db dyn Database) -> $(&name)<'db> {
704                $(&name)::from_syntax_node(db, self.0.lookup(db))
705            }
706        }
707        impl<'db> From<$(&ptr_name)<'db>> for SyntaxStablePtrId<'db> {
708            fn from(ptr: $(&ptr_name)<'db>) -> Self {
709                ptr.untyped()
710            }
711        }
712        #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, salsa::Update)]
713        pub struct $(&green_name)<'db>(pub GreenId<'db>);
714        impl<'db> TypedSyntaxNode<'db> for $(&name)<'db> {
715            const OPTIONAL_KIND: Option<SyntaxKind> = Some(SyntaxKind::$(&name));
716            type StablePtr = $(&ptr_name)<'db>;
717            type Green = $(&green_name)<'db>;
718            fn missing(db: &'db dyn Database) -> Self::Green {
719                // Note: A missing syntax element should result in an internal green node
720                // of width 0, with as much structure as possible.
721                $(&green_name)(GreenNode {
722                    kind: SyntaxKind::$(&name),
723                    details: GreenNodeDetails::Node {
724                        children: [$args_for_missing].into(),
725                        width: TextWidth::default(),
726                    },
727                }.intern(db))
728            }
729            fn from_syntax_node(db: &'db dyn Database, node: SyntaxNode<'db>) -> Self {
730                let kind = node.kind(db);
731                assert_eq!(kind, SyntaxKind::$(&name), "Unexpected SyntaxKind {:?}. Expected {:?}.", kind, SyntaxKind::$(&name));
732                Self { node }
733            }
734            fn cast(db: &'db dyn Database, node: SyntaxNode<'db>) -> Option<Self> {
735                let kind = node.kind(db);
736                if kind == SyntaxKind::$(&name) {
737                    Some(Self::from_syntax_node(db, node))
738                } else {
739                    None
740                }
741            }
742            fn as_syntax_node(&self) -> SyntaxNode<'db> {
743                self.node
744            }
745            fn stable_ptr(&self, db: &'db dyn Database) -> Self::StablePtr {
746                $(&ptr_name)(self.node.stable_ptr(db))
747            }
748        }
749    }
750}