luau_lexer/token/
literal.rs

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