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