luau_lexer/token/
literal.rs

1//! Luau literals
2
3use smol_str::SmolStr;
4
5use crate::{
6    prelude::{Lexable, Lexer, ParseError},
7    utils::is_numeric,
8};
9
10/// A Luau string. The stored string will include the quotes/double quotes/backticks.
11/// The only reason the different types actually exist is to allow the user to
12/// easily know which one is used without needing to check the actual string.
13#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
14#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
15pub enum LuauString {
16    ///```lua
17    /// 'single quotes'
18    /// ```
19    SingleQuotes(SmolStr),
20
21    ///```lua
22    /// "double quotes"
23    /// ```
24    DoubleQuotes(SmolStr),
25
26    ///```lua
27    /// `backticks`
28    /// ```
29    Backticks(SmolStr),
30
31    ///```lua
32    /// [[ multi line ]]
33    /// [[
34    ///     multi line
35    /// ]]
36    /// [==[
37    ///     multi line
38    /// ]==]
39    /// ```
40    MultiLine(SmolStr),
41}
42
43impl LuauString {
44    /// Counts the number of backslashes at the end of the passed characters array.
45    fn count_back_slashes(characters: &[char]) -> usize {
46        if characters.is_empty() {
47            return 0;
48        }
49
50        let mut count = 0;
51        let mut i = characters.len() - 1;
52
53        while characters[i] == '\\' {
54            count += 1;
55
56            match i.checked_sub(1) {
57                Some(new_i) => i = new_i,
58                None => break,
59            }
60        }
61
62        count
63    }
64
65    /// Whether or not the last character is escaped.
66    fn is_escaped(characters: &[char]) -> bool {
67        if characters.len() < 2 {
68            false
69        } else {
70            Self::count_back_slashes(&characters[..characters.len() - 1]) % 2 != 0
71        }
72    }
73
74    /// Whether or not the array ends with `\z`.
75    #[inline]
76    fn is_multi_line_escaped(characters: &[char]) -> bool {
77        characters[characters.len() - 1] == '\n' // Been checked before. No need to recheck.
78            || (Self::is_escaped(characters) && characters[characters.len() - 1] == 'z')
79    }
80
81    /// Parses one of the single line variants:
82    ///
83    /// * [`LuauString::SingleQuotes`]
84    /// * [`LuauString::DoubleQuotes`]
85    /// * [`LuauString::Backticks`]
86    fn parse_inner(lexer: &mut Lexer, quote_character: char) -> SmolStr {
87        let mut characters = vec![quote_character];
88        let start = lexer.lexer_position;
89        let mut is_done = false;
90
91        lexer.increment_position_by_char(quote_character);
92
93        while let Some(character) = lexer.current_char() {
94            if (character == '\n' || character == '\r') && !Self::is_multi_line_escaped(&characters)
95            {
96                lexer.errors.push(ParseError::new(
97                    start,
98                    format!(
99                        "Strings must be single line, use `\\z` here or add a {}.",
100                        quote_character
101                    ),
102                    Some(lexer.lexer_position),
103                ));
104
105                break;
106            }
107
108            characters.push(character);
109            lexer.increment_position_by_char(character);
110
111            if character == quote_character && !Self::is_escaped(&characters) {
112                is_done = true;
113
114                break;
115            }
116        }
117
118        if !is_done {
119            lexer.errors.push(ParseError::new(
120                start,
121                format!("Missing {} to close string.", quote_character),
122                Some(lexer.lexer_position),
123            ));
124        }
125
126        characters.iter().collect::<String>().into()
127    }
128
129    /// Parses [`LuauString::MultiLine`].
130    pub(crate) fn parse_multi_line(lexer: &mut Lexer) -> SmolStr {
131        let mut characters = vec!['['];
132        let start = lexer.lexer_position;
133        let mut equals_count = 0;
134        let mut is_done = false;
135
136        lexer.consume('[');
137        while lexer.consume('=') {
138            equals_count += 1;
139            characters.push('=');
140        }
141
142        if lexer.consume('[') {
143            characters.push('[');
144        } else {
145            lexer.errors.push(ParseError::new(
146                start,
147                "Missing `[`.".to_string(),
148                Some(lexer.lexer_position),
149            ));
150        }
151
152        while let Some(character) = lexer.current_char() {
153            characters.push(character);
154            lexer.increment_position_by_char(character);
155
156            if character == ']' && !Self::is_escaped(&characters) {
157                let mut matched_equals = true;
158
159                for _ in 0..equals_count {
160                    if let Some(character) = lexer.current_char() {
161                        characters.push(character);
162                        lexer.increment_position_by_char(character);
163
164                        matched_equals = character == '=';
165                    }
166                }
167
168                if matched_equals && lexer.consume(']') {
169                    characters.push(']');
170                    is_done = true;
171
172                    break;
173                }
174            }
175        }
176
177        if !is_done {
178            lexer.errors.push(ParseError::new(
179                start,
180                "Malformed multi-line string.".to_string(),
181                Some(lexer.lexer_position),
182            ));
183        }
184
185        characters.iter().collect::<String>().into()
186    }
187}
188
189impl Lexable for LuauString {
190    fn try_lex(lexer: &mut Lexer) -> Option<Self> {
191        match lexer.current_char()? {
192            '"' => Some(Self::DoubleQuotes(Self::parse_inner(lexer, '"'))),
193            '\'' => Some(Self::SingleQuotes(Self::parse_inner(lexer, '\''))),
194            '`' => Some(Self::Backticks(Self::parse_inner(lexer, '`'))),
195            '[' => Some(Self::MultiLine(Self::parse_multi_line(lexer))),
196            _ => unreachable!("Invalid quote type."),
197        }
198    }
199}
200
201/// A luau number. The stored string will include the `0b`, or `0x`. The only
202/// reason the different types actually exist is to allow the user to easily
203/// know which one is used without needing to check the actual string.
204#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
205#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
206pub enum LuauNumber {
207    ///```luau
208    /// 1
209    /// 1.1
210    /// .1
211    /// ```
212    Plain(SmolStr),
213
214    ///```luau
215    /// 0b111001101
216    /// ```
217    Binary(SmolStr),
218
219    ///```luau
220    /// 0xAB02C
221    /// ```
222    Hex(SmolStr),
223}
224
225impl LuauNumber {
226    /// Parses a [`LuauNumber::Plain`].
227    fn parse_number_inner(lexer: &mut Lexer) -> Option<Self> {
228        let start = lexer.position;
229        let mut found_decimal = false;
230
231        loop {
232            let Some(current_char) = lexer.current_char() else {
233                break;
234            };
235
236            if is_numeric(current_char) {
237                lexer.increment_position_by_char(current_char);
238            } else if current_char == '.' {
239                if found_decimal {
240                    break lexer.errors.push(ParseError::new(
241                        lexer.lexer_position,
242                        "Numbers can only have one decimal point.".to_string(),
243                        None,
244                    ));
245                }
246
247                lexer.increment_position_by_char(current_char);
248                found_decimal = true;
249            } else {
250                break;
251            }
252        }
253
254        Some(Self::Plain(SmolStr::from_iter(
255            lexer.chars[start..lexer.position].to_vec(),
256        )))
257    }
258
259    /// Parses a [`LuauNumber::Hex`].
260    fn parse_hex_number(lexer: &mut Lexer) -> Option<Self> {
261        let start = lexer.position;
262        let mut found_digit = false;
263        let mut is_faulty = false;
264
265        lexer.consume('0');
266        lexer.consume('x');
267
268        loop {
269            let Some(current_char) = lexer.current_char() else {
270                break;
271            };
272
273            if current_char.is_ascii_hexdigit() {
274                lexer.increment_position_by_char(current_char);
275                found_digit = true;
276            } else {
277                break is_faulty = !current_char.is_whitespace();
278            }
279        }
280
281        // ? Do we exit or return the faulty number?
282        if !found_digit {
283            lexer.errors.push(ParseError::new(
284                lexer.lexer_position,
285                "Hexadecimal numbers must have at least one digit after '0x'.".to_string(),
286                None,
287            ));
288        }
289        if found_digit && is_faulty {
290            lexer.errors.push(ParseError::new(
291                lexer.lexer_position,
292                "Hexadecimal numbers must only contain hexadecimal digits.".to_string(),
293                None,
294            ));
295        }
296
297        Some(Self::Hex(SmolStr::from_iter(
298            lexer.chars[start..lexer.position].to_vec(),
299        )))
300    }
301
302    /// Parses a [`LuauNumber::Binary`].
303    fn parse_binary_number(lexer: &mut Lexer) -> Option<Self> {
304        let start = lexer.position;
305        let mut found_digit = false;
306        let mut is_faulty = false;
307
308        lexer.consume('0');
309        lexer.consume('b');
310
311        loop {
312            let Some(current_char) = lexer.current_char() else {
313                break;
314            };
315
316            if current_char == '0' || current_char == '1' {
317                lexer.increment_position_by_char(current_char);
318                found_digit = true;
319            } else {
320                break is_faulty = !current_char.is_whitespace();
321            }
322        }
323
324        // ? Do we exit or return the faulty number?
325        if !found_digit {
326            lexer.errors.push(ParseError::new(
327                lexer.lexer_position,
328                "Binary number must have at least one digit after '0b'.".to_string(),
329                None,
330            ));
331        }
332        if found_digit && is_faulty {
333            lexer.errors.push(ParseError::new(
334                lexer.lexer_position,
335                "Binary number must only have 1s and 0s.".to_string(),
336                None,
337            ));
338        }
339
340        Some(Self::Binary(SmolStr::from_iter(
341            lexer.chars[start..lexer.position].to_vec(),
342        )))
343    }
344}
345
346impl Lexable for LuauNumber {
347    fn try_lex(lexer: &mut Lexer) -> Option<Self> {
348        match (lexer.current_char()?, lexer.next_char()) {
349            ('0', Some('b')) => Self::parse_binary_number(lexer),
350            ('0', Some('x')) => Self::parse_hex_number(lexer),
351            _ => Self::parse_number_inner(lexer),
352        }
353    }
354}
355
356/// A Luau literal value
357#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
358#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
359pub enum Literal {
360    /// A numeric value
361    Number(LuauNumber),
362
363    /// A string
364    String(LuauString),
365
366    /// A boolean
367    Boolean(bool),
368}
369
370impl Literal {
371    /// Parses a [`Literal::Number`].
372    #[inline]
373    pub fn parse_number(lexer: &mut Lexer) -> Option<Self> {
374        LuauNumber::try_lex(lexer).map(Self::Number)
375    }
376
377    /// Parses a [`Literal::String`].
378    #[inline]
379    pub fn parse_string(lexer: &mut Lexer) -> Option<Self> {
380        LuauString::try_lex(lexer).map(Self::String)
381    }
382}
383
384impl Lexable for Literal {
385    /// This just marks literals as lexable, refrain from using it. Use
386    /// [`Literal::parse_number`], or [`Literal::parse_string`], or the more
387    /// specific [`LuauString::try_lex`], and [`LuauNumber::try_lex`] instead.
388    fn try_lex(_: &mut Lexer) -> Option<Self> {
389        panic!(
390            "\
391            `Literal::try_lex()` should never be used. \
392            Please read the documentation for this function."
393        )
394    }
395}
396
397impl_from!(Literal <= {
398    Number(LuauNumber),
399    String(LuauString),
400    Boolean(bool),
401});