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.byte_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(lexer.input[start..lexer.byte_position].into()))
255    }
256
257    /// Parses a [`LuauNumber::Hex`].
258    fn parse_hex_number(lexer: &mut Lexer) -> Option<Self> {
259        let start = lexer.byte_position;
260        let mut found_digit = false;
261        let mut is_faulty = false;
262
263        lexer.consume('0');
264        lexer.consume('x');
265
266        loop {
267            let Some(current_char) = lexer.current_char() else {
268                break;
269            };
270
271            if current_char.is_ascii_hexdigit() {
272                lexer.increment_position_by_char(current_char);
273                found_digit = true;
274            } else {
275                break is_faulty = !current_char.is_whitespace();
276            }
277        }
278
279        // ? Do we exit or return the faulty number?
280        if !found_digit {
281            lexer.errors.push(ParseError::new(
282                lexer.lexer_position,
283                "Hexadecimal numbers must have at least one digit after '0x'.".to_string(),
284                None,
285            ));
286        }
287        if found_digit && is_faulty {
288            lexer.errors.push(ParseError::new(
289                lexer.lexer_position,
290                "Hexadecimal numbers must only contain hexadecimal digits.".to_string(),
291                None,
292            ));
293        }
294
295        Some(Self::Hex(lexer.input[start..lexer.byte_position].into()))
296    }
297
298    /// Parses a [`LuauNumber::Binary`].
299    fn parse_binary_number(lexer: &mut Lexer) -> Option<Self> {
300        let start = lexer.byte_position;
301        let mut found_digit = false;
302        let mut is_faulty = false;
303
304        lexer.consume('0');
305        lexer.consume('b');
306
307        loop {
308            let Some(current_char) = lexer.current_char() else {
309                break;
310            };
311
312            if current_char == '0' || current_char == '1' {
313                lexer.increment_position_by_char(current_char);
314                found_digit = true;
315            } else {
316                break is_faulty = !current_char.is_whitespace();
317            }
318        }
319
320        // ? Do we exit or return the faulty number?
321        if !found_digit {
322            lexer.errors.push(ParseError::new(
323                lexer.lexer_position,
324                "Binary number must have at least one digit after '0b'.".to_string(),
325                None,
326            ));
327        }
328        if found_digit && is_faulty {
329            lexer.errors.push(ParseError::new(
330                lexer.lexer_position,
331                "Binary number must only have 1s and 0s.".to_string(),
332                None,
333            ));
334        }
335
336        Some(Self::Binary(lexer.input[start..lexer.byte_position].into()))
337    }
338}
339
340impl Lexable for LuauNumber {
341    fn try_lex(lexer: &mut Lexer) -> Option<Self> {
342        match (lexer.current_char()?, lexer.next_char()) {
343            ('0', Some('b')) => Self::parse_binary_number(lexer),
344            ('0', Some('x')) => Self::parse_hex_number(lexer),
345            _ => Self::parse_number_inner(lexer),
346        }
347    }
348}
349
350/// A Luau literal value
351#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
352#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
353pub enum Literal {
354    /// A numeric value
355    Number(LuauNumber),
356
357    /// A string
358    String(LuauString),
359
360    /// A boolean
361    Boolean(bool),
362}
363
364impl Literal {
365    /// Parses a [`Literal::Number`].
366    #[inline]
367    pub fn parse_number(lexer: &mut Lexer) -> Option<Self> {
368        LuauNumber::try_lex(lexer).map(Self::Number)
369    }
370
371    /// Parses a [`Literal::String`].
372    #[inline]
373    pub fn parse_string(lexer: &mut Lexer) -> Option<Self> {
374        LuauString::try_lex(lexer).map(Self::String)
375    }
376}
377
378impl Lexable for Literal {
379    /// This just marks literals as lexable, refrain from using it. Use
380    /// [`Literal::parse_number`], or [`Literal::parse_string`], or the more
381    /// specific [`LuauString::try_lex`], and [`LuauNumber::try_lex`] instead.
382    fn try_lex(_: &mut Lexer) -> Option<Self> {
383        panic!(
384            "\
385            `Literal::try_lex()` should never be used. \
386            Please read the documentation for this function."
387        )
388    }
389}
390
391impl_from!(Literal <= {
392    Number(LuauNumber),
393    String(LuauString),
394    Boolean(bool),
395});