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