cainome_parser/tokens/
composite.rs

1//! A composite is a type that is composed of other types (struct or enum).
2//!
3//! A composite type can be generic, and even if in the ABI the generic types
4//! are replaced by their concrete types, the [`Composite`] token is still generic
5//! to retain the information about the generic types.
6//!
7//! A pitfall is that the ABI doesn't say which variant/field of the enum/struct
8//! is generic. This is an information that needs to be reconstructed by the parser.
9//!
10//! As an example, with the following cairo struct:
11//!
12//! ```rust,ignore
13//! struct MyStruct<A> {
14//!     field_1: A,
15//!     field_2: felt252,
16//! }
17//! ```
18//!
19//! The ABI will contains several entries for this struct, with the generic
20//! type `A` replaced by its concrete type as much as necessary.
21//!
22//! ```rust,ignore
23//! [
24//! {
25//!     "type": "struct",
26//!     "name": "MyStruct::<core::felt252>",
27//!     "members": [
28//!       {
29//!         "name": "field_1",
30//!         "type": "core::felt252"
31//!       },
32//!       {
33//!         "name": "field_2",
34//!         "type": "core::felt252"
35//!       }
36//!     ]
37//! },
38//! {
39//!     "type": "struct",
40//!     "name": "MyStruct::<core::integer::u64>",
41//!     "members": [
42//!       {
43//!         "name": "field_1",
44//!         "type": "core::integer::u64"
45//!       },
46//!       {
47//!         "name": "field_2",
48//!         "type": "core::felt252"
49//!       }
50//!     ]
51//! },
52//! ]
53//! ```
54//!
55//! As it can be seen, in this case, the ABI doesn't say which variant of the
56//! struct is generic since `field_2` is generic in the first case but not in
57//! the second one.
58//!
59//! A naive strategy would be to ensure all types are parsed a first time,
60//! and then a generic resolution is done.
61use super::constants::{CAIRO_COMPOSITE_BUILTINS, CAIRO_GENERIC_BUILTINS};
62use super::genericity;
63use super::Token;
64
65use crate::CainomeResult;
66
67#[derive(Debug, Copy, Clone, PartialEq)]
68pub enum CompositeType {
69    Struct,
70    Enum,
71    Unknown,
72}
73
74#[derive(Debug, Copy, Clone, PartialEq)]
75pub enum CompositeInnerKind {
76    Key,
77    Data,
78    Nested,
79    Flat,
80    NotUsed,
81}
82
83#[derive(Debug, Clone, PartialEq)]
84pub struct CompositeInner {
85    pub index: usize,
86    pub name: String,
87    pub kind: CompositeInnerKind,
88    pub token: Token,
89}
90
91#[derive(Debug, Clone, PartialEq)]
92pub struct Composite {
93    pub type_path: String,
94    pub inners: Vec<CompositeInner>,
95    pub generic_args: Vec<(String, Token)>,
96    pub r#type: CompositeType,
97    pub is_event: bool,
98    pub alias: Option<String>,
99}
100
101impl Composite {
102    /// Parses a composite type from a type path.
103    /// Since the composite can be named arbitrarily, by the user,
104    /// the parsing of the composite is not checking if the type path is
105    /// a core basic type, an array or something else.
106    ///
107    /// In cainome the type path is first parsed as any other token, and
108    /// [`Composite`] is the last token that is parsed (which accepts every path).
109    ///
110    /// You may use [`Composite::is_builtin`] to check if the type path is
111    /// a known Cairo builtin type.
112    ///
113    /// # Arguments
114    ///
115    /// * `type_path` - The type path to parse.
116    ///
117    /// # Returns
118    ///
119    /// Returns a [`Composite`] token if the type path is a composite.
120    /// Returns an error otherwise.
121    pub fn parse(type_path: &str) -> CainomeResult<Self> {
122        let type_path = escape_rust_keywords(type_path);
123        let generic_args = genericity::extract_generics_args(&type_path)?;
124
125        Ok(Self {
126            // We want to keep the path with generic for the generic resolution.
127            type_path: type_path.to_string(),
128            inners: vec![],
129            generic_args,
130            r#type: CompositeType::Unknown,
131            is_event: false,
132            alias: None,
133        })
134    }
135
136    pub fn type_path_no_generic(&self) -> String {
137        genericity::type_path_no_generic(&self.type_path)
138    }
139
140    pub fn is_generic(&self) -> bool {
141        !self.generic_args.is_empty()
142    }
143
144    /// Returns true if the current composite is considered as Cairo builtin.
145    /// This is useful to avoid expanding the structure if already managed by
146    /// the backend (like Option and Result for instance).
147    /// Spans and Arrays are handled by `array`.
148    pub fn is_builtin(&self) -> bool {
149        for b in CAIRO_GENERIC_BUILTINS {
150            if self.type_path.starts_with(b) {
151                return true;
152            }
153        }
154
155        for b in CAIRO_COMPOSITE_BUILTINS {
156            if self.type_path.starts_with(b) {
157                return true;
158            }
159        }
160
161        false
162    }
163
164    pub fn type_name(&self) -> String {
165        // TODO: need to opti that with regex?
166        extract_type_path_with_depth(&self.type_path_no_generic(), 0)
167    }
168
169    pub fn type_name_or_alias(&self) -> String {
170        if let Some(a) = &self.alias {
171            a.clone()
172        } else {
173            self.type_name()
174        }
175    }
176
177    pub fn apply_alias(&mut self, type_path: &str, alias: &str) {
178        if self.type_path_no_generic() == type_path {
179            self.alias = Some(alias.to_string());
180        }
181
182        for ref mut i in &mut self.inners {
183            if let Token::Composite(ref mut c) = i.token {
184                c.apply_alias(type_path, alias);
185            }
186        }
187    }
188}
189
190/// Converts a snake case string to pascal case.
191pub fn snake_to_pascal_case(s: &str) -> String {
192    s.split('_')
193        .map(|word| {
194            let mut c = word.chars();
195            match c.next() {
196                None => String::new(),
197                Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
198            }
199        })
200        .collect()
201}
202
203/// Escapes Rust keywords that may be found into cairo code.
204pub fn escape_rust_keywords(s: &str) -> String {
205    let keywords = ["move", "type", "final"];
206
207    let mut s = s.to_string();
208
209    for k in keywords {
210        let k_start = format!("{k}::");
211        let k_middle = format!("::{k}::");
212        let k_end = format!("::{k}");
213
214        if s == k {
215            return format!("r#{k}");
216        } else if s.starts_with(&k_start) {
217            s = s.replace(&k_start, &format!("r#{k}::"));
218        } else if s.ends_with(&k_end) {
219            s = s.replace(&k_end, &format!("::r#{k}"));
220        } else {
221            s = s.replace(&k_middle, &format!("::r#{k}::"));
222        }
223    }
224
225    s
226}
227
228/// Extracts the `type_path` with given module `depth`.
229/// The extraction also converts all everything to `snake_case`.
230///
231/// # Arguments
232///
233/// * `type_path` - Type path to be extracted.
234/// * `depth` - The module depth to extract.
235///
236/// # Examples
237///
238/// `module::module2::type_name` with depth 0 -> `TypeName`.
239/// `module::module2::type_name` with depth 1 -> `Module2TypeName`.
240/// `module::module2::type_name` with depth 2 -> `ModuleModule2TypeName`.
241pub fn extract_type_path_with_depth(type_path: &str, depth: usize) -> String {
242    let segments: Vec<&str> = type_path.split("::").collect();
243
244    let mut depth = depth;
245    if segments.len() < depth + 1 {
246        depth = segments.len() - 1;
247    }
248
249    let segments = &segments[segments.len() - depth - 1..segments.len()];
250    segments.iter().map(|s| snake_to_pascal_case(s)).collect()
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256    use crate::tokens::*;
257
258    fn basic_felt252() -> Token {
259        Token::CoreBasic(CoreBasic {
260            type_path: "core::felt252".to_string(),
261        })
262    }
263
264    fn basic_u64() -> Token {
265        Token::CoreBasic(CoreBasic {
266            type_path: "core::integer::u64".to_string(),
267        })
268    }
269
270    #[test]
271    fn test_snake_to_pascal_case() {
272        assert_eq!(snake_to_pascal_case("my_type"), "MyType");
273        assert_eq!(snake_to_pascal_case("my_type_long"), "MyTypeLong");
274        assert_eq!(snake_to_pascal_case("type"), "Type");
275        assert_eq!(snake_to_pascal_case("MyType"), "MyType");
276        assert_eq!(snake_to_pascal_case("MyType_hybrid"), "MyTypeHybrid");
277        assert_eq!(snake_to_pascal_case(""), "");
278    }
279
280    #[test]
281    fn test_extract_type_with_depth() {
282        assert_eq!(extract_type_path_with_depth("type_name", 0), "TypeName");
283        assert_eq!(extract_type_path_with_depth("type_name", 10), "TypeName");
284        assert_eq!(
285            extract_type_path_with_depth("module::TypeName", 1),
286            "ModuleTypeName"
287        );
288        assert_eq!(
289            extract_type_path_with_depth("module::TypeName", 8),
290            "ModuleTypeName"
291        );
292        assert_eq!(
293            extract_type_path_with_depth("module_one::module_1::TypeName", 2),
294            "ModuleOneModule1TypeName"
295        );
296    }
297
298    #[test]
299    fn test_parse() {
300        let expected = Composite {
301            type_path: "module::MyStruct".to_string(),
302            inners: vec![],
303            generic_args: vec![],
304            r#type: CompositeType::Unknown,
305            is_event: false,
306            alias: None,
307        };
308
309        assert_eq!(Composite::parse("module::MyStruct").unwrap(), expected);
310        assert!(!expected.is_generic());
311    }
312
313    #[test]
314    fn test_parse_generic_one() {
315        let expected = Composite {
316            type_path: "module::MyStruct::<core::felt252>".to_string(),
317            inners: vec![],
318            generic_args: vec![("A".to_string(), basic_felt252())],
319            r#type: CompositeType::Unknown,
320            is_event: false,
321            alias: None,
322        };
323
324        assert_eq!(
325            Composite::parse("module::MyStruct::<core::felt252>").unwrap(),
326            expected
327        );
328        assert!(expected.is_generic());
329    }
330
331    #[test]
332    fn test_parse_generic_two() {
333        let expected = Composite {
334            type_path: "module::MyStruct::<core::felt252, core::integer::u64>".to_string(),
335            inners: vec![],
336            generic_args: vec![
337                ("A".to_string(), basic_felt252()),
338                ("B".to_string(), basic_u64()),
339            ],
340            r#type: CompositeType::Unknown,
341            is_event: false,
342            alias: None,
343        };
344
345        assert_eq!(
346            Composite::parse("module::MyStruct::<core::felt252, core::integer::u64>").unwrap(),
347            expected
348        );
349        assert!(expected.is_generic());
350    }
351
352    #[test]
353    fn test_type_name() {
354        let mut c = Composite {
355            type_path: "module::MyStruct".to_string(),
356            inners: vec![],
357            generic_args: vec![],
358            r#type: CompositeType::Unknown,
359            is_event: false,
360            alias: None,
361        };
362        assert_eq!(c.type_name(), "MyStruct");
363
364        c.type_path = "module::MyStruct::<core::felt252>".to_string();
365        assert_eq!(c.type_name(), "MyStruct");
366    }
367
368    #[test]
369    fn test_escape_rust_keywords() {
370        assert_eq!(escape_rust_keywords("move"), "r#move",);
371
372        assert_eq!(escape_rust_keywords("move::salut"), "r#move::salut",);
373
374        assert_eq!(escape_rust_keywords("hey::move"), "hey::r#move",);
375
376        assert_eq!(
377            escape_rust_keywords("hey::move::salut"),
378            "hey::r#move::salut",
379        );
380
381        assert_eq!(
382            escape_rust_keywords("type::move::final"),
383            "r#type::r#move::r#final",
384        );
385    }
386}