Skip to main content

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