Skip to main content

cairo_lang_syntax/node/
ast_ext.rs

1//! Custom methods for AST node types.
2//!
3//! The impls here are visible through [`super`] module.
4
5use cairo_lang_filesystem::ids::SmolStrId;
6use cairo_lang_utils::require;
7use num_bigint::{BigInt, Sign};
8use num_traits::Num;
9use salsa::Database;
10use unescaper::unescape;
11
12use super::{
13    TerminalFalse, TerminalLiteralNumber, TerminalShortString, TerminalString, TerminalTrue,
14};
15use crate::node::Terminal;
16
17impl<'a> TerminalTrue<'a> {
18    #[inline(always)]
19    pub fn boolean_value(&self) -> bool {
20        true
21    }
22}
23
24impl<'a> TerminalFalse<'a> {
25    #[inline(always)]
26    pub fn boolean_value(&self) -> bool {
27        false
28    }
29}
30
31impl<'a> TerminalLiteralNumber<'a> {
32    /// Interpret this terminal as a [`BigInt`] number and get the suffix if this literal has one.
33    ///
34    /// Splits at `_` boundaries from right to left, returning the longest prefix that parses as a
35    /// valid number in the appropriate radix, with the remainder (after the `_`) as the suffix.
36    /// This handles ambiguity for radices where suffix chars are valid digits — e.g., `0x1_f32`
37    /// parses as hex `0x1F32` (no suffix) because `"1_f32"` is a valid hex number, while
38    /// `1_f32` in decimal returns value `1` with suffix `"f32"`. Also handles compound suffixes
39    /// like `1_u32_u8`: returns value `1` with suffix `"u32_u8"`.
40    ///
41    /// Returns `(BigInt::ZERO, Some(full_token_text))` as a sentinel for malformed literals where
42    /// no prefix parses (e.g., `0x_u32` where the hex body is empty). Callers treat `Some` suffix
43    /// as an error indicator.
44    pub fn numeric_value_and_suffix(
45        &self,
46        db: &'a dyn Database,
47    ) -> (BigInt, Option<SmolStrId<'a>>) {
48        let text = self.text(db).long(db).as_str();
49
50        let (text, radix) = if let Some(num_no_prefix) = text.strip_prefix("0x") {
51            (num_no_prefix, 16)
52        } else if let Some(num_no_prefix) = text.strip_prefix("0o") {
53            (num_no_prefix, 8)
54        } else if let Some(num_no_prefix) = text.strip_prefix("0b") {
55            (num_no_prefix, 2)
56        } else {
57            (text, 10)
58        };
59
60        let mut index = text.len();
61        while index > 0 {
62            if let Ok(value) = BigInt::from_str_radix(&text[..index], radix) {
63                return (value, text.get((index + 1)..).map(|s| SmolStrId::from(db, s)));
64            }
65            index = text[..index].rfind('_').unwrap_or(0);
66        }
67        // Malformed token (e.g., `0x_u32`): no numeric prefix found; return full text as suffix
68        // so callers that check `suffix.is_some()` correctly treat this as an error.
69        (BigInt::ZERO, Some(self.text(db)))
70    }
71}
72
73impl<'a> TerminalShortString<'a> {
74    /// Interpret this token/terminal as a string.
75    pub fn string_value(&self, db: &'a dyn Database) -> Option<String> {
76        let text = self.text(db).long(db);
77
78        let (text, _suffix) = string_value(text, '\'')?;
79
80        Some(text)
81    }
82
83    /// Interpret this terminal as a [`BigInt`] number.
84    pub fn numeric_value(&self, db: &'a dyn Database) -> Option<BigInt> {
85        self.string_value(db).map(|string| BigInt::from_bytes_be(Sign::Plus, string.as_bytes()))
86    }
87
88    /// Get suffix from this literal if it has one.
89    pub fn suffix(&self, db: &'a dyn Database) -> Option<&'a str> {
90        let text = self.text(db).long(db);
91        let (_literal, mut suffix) = text[1..].rsplit_once('\'')?;
92        require(!suffix.is_empty())?;
93        if suffix.starts_with('_') {
94            suffix = &suffix[1..];
95        }
96        Some(suffix)
97    }
98}
99
100impl<'a> TerminalString<'a> {
101    /// Interpret this token/terminal as a string.
102    pub fn string_value(&self, db: &'a dyn Database) -> Option<String> {
103        let text = self.text(db).long(db);
104        let (text, suffix) = string_value(text, '"')?;
105        if !suffix.is_empty() {
106            unreachable!();
107        }
108
109        Some(text)
110    }
111}
112
113/// Interpret the given text as a string with the given delimiter. Returns the text and the suffix.
114fn string_value(text: &str, delimiter: char) -> Option<(String, &str)> {
115    let (prefix, text) = text.split_once(delimiter)?;
116    if !prefix.is_empty() {
117        unreachable!();
118    }
119
120    let (text, suffix) = text.rsplit_once(delimiter)?;
121
122    let text = unescape(text).ok()?;
123
124    require(text.is_ascii())?;
125
126    Some((text, suffix))
127}