Skip to main content

mago_type_syntax/lexer/
mod.rs

1mod keyword;
2
3use mago_database::file::FileId;
4use mago_database::file::HasFileId;
5use mago_span::Position;
6use mago_syntax_core::float_exponent;
7use mago_syntax_core::float_separator;
8use mago_syntax_core::input::Input;
9use mago_syntax_core::number_sign;
10use mago_syntax_core::part_of_identifier;
11use mago_syntax_core::start_of_binary_number;
12use mago_syntax_core::start_of_float_number;
13use mago_syntax_core::start_of_hexadecimal_number;
14use mago_syntax_core::start_of_identifier;
15use mago_syntax_core::start_of_octal_number;
16use mago_syntax_core::start_of_octal_or_float_number;
17use mago_syntax_core::utils::read_digits_of_base;
18
19use crate::error::SyntaxError;
20use crate::token::TypeToken;
21use crate::token::TypeTokenKind;
22
23#[derive(Debug)]
24pub struct TypeLexer<'arena> {
25    input: Input<'arena>,
26}
27
28impl<'arena> TypeLexer<'arena> {
29    #[inline]
30    #[must_use]
31    pub fn new(input: Input<'arena>) -> TypeLexer<'arena> {
32        TypeLexer { input }
33    }
34
35    #[inline]
36    #[must_use]
37    pub fn has_reached_eof(&self) -> bool {
38        self.input.has_reached_eof()
39    }
40
41    #[inline]
42    #[must_use]
43    pub fn current_position(&self) -> Position {
44        self.input.current_position()
45    }
46
47    #[inline]
48    #[must_use]
49    pub fn slice_in_range(&self, from: u32, to: u32) -> &'arena [u8] {
50        self.input.slice_in_range(from, to)
51    }
52
53    #[inline]
54    pub fn advance(&mut self) -> Option<Result<TypeToken<'arena>, SyntaxError>> {
55        if self.input.has_reached_eof() {
56            return None;
57        }
58
59        let start = self.input.current_position();
60        let whitespaces = self.input.consume_whitespaces();
61        if !whitespaces.is_empty() {
62            let end = self.input.current_position();
63            return Some(Ok(self.token(TypeTokenKind::Whitespace, whitespaces, start, end)));
64        }
65
66        let remaining = self.input.read_remaining();
67        // SAFETY: has_reached_eof() was checked at the top; remaining is non-empty.
68        let first = unsafe { *remaining.get_unchecked(0) };
69        let second = remaining.get(1).copied();
70
71        let (kind, length) = match first {
72            b'*' => (TypeTokenKind::Asterisk, 1),
73            b':' => {
74                if second == Some(b':') {
75                    (TypeTokenKind::ColonColon, 2)
76                } else {
77                    (TypeTokenKind::Colon, 1)
78                }
79            }
80            b'=' => (TypeTokenKind::Equals, 1),
81            b'?' => (TypeTokenKind::Question, 1),
82            b'!' => (TypeTokenKind::Exclamation, 1),
83            b'&' => (TypeTokenKind::Ampersand, 1),
84            b'|' => (TypeTokenKind::Pipe, 1),
85            b'>' => (TypeTokenKind::GreaterThan, 1),
86            b'<' => (TypeTokenKind::LessThan, 1),
87            b'(' => (TypeTokenKind::LeftParenthesis, 1),
88            b')' => (TypeTokenKind::RightParenthesis, 1),
89            b'[' => (TypeTokenKind::LeftBracket, 1),
90            b']' => (TypeTokenKind::RightBracket, 1),
91            b'{' => (TypeTokenKind::LeftBrace, 1),
92            b'}' => (TypeTokenKind::RightBrace, 1),
93            b',' => (TypeTokenKind::Comma, 1),
94            b'+' => (TypeTokenKind::Plus, 1),
95            b'-' => (TypeTokenKind::Minus, 1),
96            b'.' => match remaining.get(..3) {
97                Some([b'.', b'.', b'.']) => (TypeTokenKind::Ellipsis, 3),
98                _ if matches!(second, Some(b'0'..=b'9')) => self.read_decimal(),
99                _ => {
100                    return Some(Err(SyntaxError::UnrecognizedToken(
101                        self.file_id(),
102                        first,
103                        self.input.current_position(),
104                    )));
105                }
106            },
107            b'/' if second == Some(b'/') => self.read_single_line_comment(),
108            b'\'' | b'"' => self.read_literal_string(first),
109            b'\\' if second.is_some_and(|b| b.is_ascii_alphabetic() || b == b'_' || b >= 0x80) => {
110                self.read_fully_qualified_identifier()
111            }
112            b'$' if second.is_some_and(|b| b.is_ascii_alphabetic() || b == b'_' || b >= 0x80) => self.read_variable(),
113            b'0'..=b'9' => self.read_number(),
114            b if b.is_ascii_alphabetic() || b == b'_' || b >= 0x80 => self.read_identifier_or_keyword(),
115            _ => {
116                return Some(Err(SyntaxError::UnrecognizedToken(self.file_id(), first, self.input.current_position())));
117            }
118        };
119
120        let buffer = self.input.consume(length);
121        let end = self.input.current_position();
122
123        Some(Ok(self.token(kind, buffer, start, end)))
124    }
125
126    #[inline]
127    fn read_variable(&self) -> (TypeTokenKind, usize) {
128        let mut length = 2;
129        while let [part_of_identifier!(), ..] = self.input.peek(length, 1) {
130            length += 1;
131        }
132        (TypeTokenKind::Variable, length)
133    }
134
135    #[inline]
136    fn read_single_line_comment(&self) -> (TypeTokenKind, usize) {
137        let mut length = 2;
138        loop {
139            match self.input.peek(length, 1) {
140                [b'\n', ..] | [] => break,
141                [_, ..] => length += 1,
142            }
143        }
144        (TypeTokenKind::SingleLineComment, length)
145    }
146
147    #[inline]
148    fn read_decimal(&self) -> (TypeTokenKind, usize) {
149        let mut length = read_digits_of_base(&self.input, 2, 10);
150        if let float_exponent!() = self.input.peek(length, 1) {
151            let mut exp_length = length + 1;
152            if let number_sign!() = self.input.peek(exp_length, 1) {
153                exp_length += 1;
154            }
155
156            let after_exp = read_digits_of_base(&self.input, exp_length, 10);
157            if after_exp > exp_length {
158                length = after_exp;
159            }
160        }
161        (TypeTokenKind::LiteralFloat, length)
162    }
163
164    #[inline]
165    fn read_number(&self) -> (TypeTokenKind, usize) {
166        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
167        enum NumberKind {
168            Integer,
169            Float,
170            OctalOrFloat,
171            IntegerOrFloat,
172        }
173
174        let mut length = 1;
175        let (base, kind): (u8, NumberKind) = match self.input.read(3) {
176            start_of_binary_number!() => {
177                length += 1;
178                (2, NumberKind::Integer)
179            }
180            start_of_octal_number!() => {
181                length += 1;
182                (8, NumberKind::Integer)
183            }
184            start_of_hexadecimal_number!() => {
185                length += 1;
186                (16, NumberKind::Integer)
187            }
188            start_of_octal_or_float_number!() => (10, NumberKind::OctalOrFloat),
189            start_of_float_number!() => (10, NumberKind::Float),
190            _ => (10, NumberKind::IntegerOrFloat),
191        };
192
193        if kind != NumberKind::Float {
194            length = read_digits_of_base(&self.input, length, base);
195            if kind == NumberKind::Integer {
196                return (TypeTokenKind::LiteralInteger, length);
197            }
198        }
199
200        let is_float = matches!(self.input.peek(length, 3), float_separator!());
201        if !is_float {
202            return (TypeTokenKind::LiteralInteger, length);
203        }
204
205        if let [b'.'] = self.input.peek(length, 1) {
206            length += 1;
207            length = read_digits_of_base(&self.input, length, 10);
208        }
209
210        if let float_exponent!() = self.input.peek(length, 1) {
211            let mut exp_length = length + 1;
212            if let number_sign!() = self.input.peek(exp_length, 1) {
213                exp_length += 1;
214            }
215
216            let after_exp = read_digits_of_base(&self.input, exp_length, 10);
217            if after_exp > exp_length {
218                length = after_exp;
219            }
220        }
221
222        (TypeTokenKind::LiteralFloat, length)
223    }
224
225    #[inline]
226    fn read_literal_string(&self, quote: u8) -> (TypeTokenKind, usize) {
227        let total = self.input.len();
228        let start = self.input.current_offset();
229        let mut length = 1;
230        let mut last_was_backslash = false;
231        let mut partial = false;
232
233        loop {
234            let pos = start + length;
235            if pos >= total {
236                partial = true;
237                break;
238            }
239
240            let byte = self.input.read_at(pos);
241            if *byte == b'\\' {
242                last_was_backslash = !last_was_backslash;
243                length += 1;
244            } else {
245                if byte == &quote && !last_was_backslash {
246                    length += 1;
247                    break;
248                }
249                length += 1;
250                last_was_backslash = false;
251            }
252        }
253
254        if partial { (TypeTokenKind::PartialLiteralString, length) } else { (TypeTokenKind::LiteralString, length) }
255    }
256
257    #[inline]
258    fn read_fully_qualified_identifier(&self) -> (TypeTokenKind, usize) {
259        let mut length = 2;
260        let mut last_was_slash = false;
261        loop {
262            match self.input.peek(length, 1) {
263                [start_of_identifier!(), ..] if last_was_slash => {
264                    length += 1;
265                    last_was_slash = false;
266                }
267                [part_of_identifier!(), ..] if !last_was_slash => {
268                    length += 1;
269                }
270                [b'\\', ..] => {
271                    if last_was_slash {
272                        length -= 1;
273                        break;
274                    }
275                    length += 1;
276                    last_was_slash = true;
277                }
278                _ => break,
279            }
280        }
281        (TypeTokenKind::FullyQualifiedIdentifier, length)
282    }
283
284    /// Read an identifier or keyword (including compound keywords with hyphens).
285    /// This is the hot path - optimized for common case (simple identifiers).
286    #[inline]
287    fn read_identifier_or_keyword(&self) -> (TypeTokenKind, usize) {
288        let remaining = self.input.read_remaining();
289        let total = remaining.len();
290        let mut length = 1;
291        let mut next_is_hyphen = false;
292        let mut next_is_backslash = false;
293
294        // Scan identifier bytes greedily. Break on `-` or `\\` if the next
295        // byte after them could extend the identifier (hyphen-joined keyword
296        // or namespace separator).
297        while length < total {
298            // SAFETY: length < total guarantees the index is in bounds.
299            let b = unsafe { *remaining.get_unchecked(length) };
300            if mago_syntax_core::utils::is_part_of_identifier(&b) {
301                length += 1;
302                continue;
303            }
304
305            if b == b'-' && length + 1 < total {
306                // SAFETY: `length + 1 < total` was just checked.
307                let b2 = unsafe { *remaining.get_unchecked(length + 1) };
308                if mago_syntax_core::utils::is_part_of_identifier(&b2) {
309                    next_is_hyphen = true;
310                }
311            } else if b == b'\\' && length + 1 < total {
312                // SAFETY: `length + 1 < total` was just checked.
313                let b2 = unsafe { *remaining.get_unchecked(length + 1) };
314                if mago_syntax_core::utils::is_start_of_identifier(&b2) {
315                    next_is_backslash = true;
316                }
317            } else {
318                // Any other byte ends the identifier scan; nothing to record.
319            }
320
321            break;
322        }
323
324        if next_is_backslash {
325            return self.finish_qualified_identifier(length);
326        }
327
328        if !next_is_hyphen {
329            // SAFETY: length <= total (identifier was scanned in bounds).
330            let bytes = unsafe { remaining.get_unchecked(..length) };
331            if let Some(kind) = keyword::lookup_keyword(bytes) {
332                return (kind, length);
333            }
334            return (TypeTokenKind::Identifier, length);
335        }
336
337        let base_len = length;
338        while length < total {
339            // SAFETY: `length < total` was just checked.
340            let b = unsafe { *remaining.get_unchecked(length) };
341            if mago_syntax_core::utils::is_part_of_identifier(&b) {
342                length += 1;
343                continue;
344            }
345
346            if b == b'-' && length + 1 < total {
347                // SAFETY: `length + 1 < total` was just checked.
348                let b2 = unsafe { *remaining.get_unchecked(length + 1) };
349                if mago_syntax_core::utils::is_part_of_identifier(&b2) {
350                    length += 1;
351                    continue;
352                }
353            }
354
355            break;
356        }
357
358        // SAFETY: `length` was only ever advanced while `length < total`, so it is in bounds.
359        let bytes = unsafe { remaining.get_unchecked(..length) };
360        if let Some(kind) = keyword::lookup_keyword(bytes) {
361            return (kind, length);
362        }
363
364        // SAFETY: `base_len <= length <= total`.
365        let base_bytes = unsafe { remaining.get_unchecked(..base_len) };
366        if let Some(kind) = keyword::lookup_keyword(base_bytes) {
367            return (kind, base_len);
368        }
369
370        (TypeTokenKind::Identifier, base_len)
371    }
372
373    /// Continue reading a qualified identifier (with backslashes).
374    #[inline]
375    fn finish_qualified_identifier(&self, start_len: usize) -> (TypeTokenKind, usize) {
376        let mut length = start_len;
377        let mut slashes = 0;
378        let mut last_was_slash = false;
379
380        loop {
381            match self.input.peek(length, 1) {
382                [start_of_identifier!(), ..] if last_was_slash => {
383                    length += 1;
384                    last_was_slash = false;
385                }
386                [part_of_identifier!(), ..] if !last_was_slash => {
387                    length += 1;
388                }
389                [b'\\', ..] => {
390                    if last_was_slash {
391                        length -= 1;
392                        slashes -= 1;
393                        break;
394                    }
395                    length += 1;
396                    slashes += 1;
397                    last_was_slash = true;
398                }
399                _ => break,
400            }
401        }
402
403        if last_was_slash {
404            length -= 1;
405            slashes -= 1;
406        }
407
408        if slashes > 0 { (TypeTokenKind::QualifiedIdentifier, length) } else { (TypeTokenKind::Identifier, length) }
409    }
410
411    #[inline]
412    fn token(&self, kind: TypeTokenKind, value: &'arena [u8], start: Position, _end: Position) -> TypeToken<'arena> {
413        TypeToken { kind, start, value }
414    }
415}
416
417impl HasFileId for TypeLexer<'_> {
418    #[inline]
419    fn file_id(&self) -> FileId {
420        self.input.file_id()
421    }
422}