skullbrain-lexer 0.1.0

Lexer for the skullbrain programming language, responsible for tokenizing source code.
Documentation
//! Sourced (and usually modified) from rustc-lexer - <https://doc.rust-lang.org/nightly/nightly-rustc/src/rustc_lexer/lib.rs.html>

use crate::{Base, LiteralKind};

use super::CursorIter;

/// True if `c` is valid as a first character of an identifier.
pub fn rustc_is_id_start(c: char) -> bool {
    // This is XID_Start OR '_' (which formally is not a XID_Start).
    c == '_' || unicode_xid::UnicodeXID::is_xid_start(c)
}

/// True if `c` is valid as a non-first character of an identifier.
pub fn rustc_is_id_continue(c: char) -> bool {
    unicode_xid::UnicodeXID::is_xid_continue(c)
}

impl<I> CursorIter<I>
where
    I: Iterator<Item = char>,
{
    pub(super) fn number(&mut self, first_digit: char) -> LiteralKind {
        use LiteralKind::*;

        let mut base = Base::Decimal;
        match first_digit {
            '0' => {
                // Attempt to parse encoding base.
                match self.peek_nth(0) {
                    Some(ch) if ch == &'b' => {
                        self.next();
                        if !self.eat_decimal_digits() {
                            return Int {
                                base: Base::Decimal,
                                is_empty: true,
                            };
                        }
                    }
                    Some(ch) if ch == &'o' => {
                        base = Base::Octal;
                        self.next();
                        if !self.eat_decimal_digits() {
                            return Int {
                                base,
                                is_empty: true,
                            };
                        }
                    }
                    Some(ch) if ch == &'x' => {
                        base = Base::Hexadecimal;
                        self.next();
                        if !self.eat_hexadecimal_digits() {
                            return Int {
                                base,
                                is_empty: true,
                            };
                        }
                    }

                    // Not a base prefix; consume additional digits.
                    Some(ch) if ch.is_ascii_digit() || ch == &'_' => {
                        self.eat_decimal_digits();
                    }

                    // Also not a base prefix; nothing more to do here.
                    Some(ch) if ['.', 'e', 'E'].contains(ch) => {}

                    // Just a 0.
                    _ => {
                        return Int {
                            base: Base::Decimal,
                            is_empty: false,
                        }
                    }
                }
            }
            // No base prefix, parse number in the usual way.
            _ => {
                self.eat_decimal_digits();
            }
        };

        match self.peek_nth(0).cloned() {
            Some(ch) if ['e', 'E'].contains(&ch) => {
                self.next();
                let empty_exponent = !self.eat_float_exponent();
                Double {
                    base,
                    empty_exponent,
                }
            }
            _ => Int {
                base,
                is_empty: false,
            },
        }
    }

    fn eat_decimal_digits(&mut self) -> bool {
        let mut has_digits = false;

        while self
            .peeking_next(|ch| match ch {
                &'_' => true,
                '0'..='9' => {
                    has_digits = true;
                    true
                }
                _ => false,
            })
            .is_some()
        {}

        has_digits
    }

    fn eat_hexadecimal_digits(&mut self) -> bool {
        let mut has_digits = false;

        while self
            .peeking_next(|ch| match ch {
                &'_' => true,
                '0'..='9' | 'a'..='f' | 'A'..='F' => {
                    has_digits = true;
                    true
                }
                _ => false,
            })
            .is_some()
        {}

        has_digits
    }

    /// Eats the float exponent. Returns true if at least one digit was met,
    /// and returns false otherwise.
    fn eat_float_exponent(&mut self) -> bool {
        let _ = self.peeking_next(|ch| ['-', '+'].contains(ch));

        self.eat_decimal_digits()
    }
}

impl<I> CursorIter<I>
where
    I: Iterator<Item = char>,
{
    pub(super) fn parse_double_quoted_string(&mut self) -> LiteralKind {
        let is_terminated = self.double_quoted_string();
        LiteralKind::Str { is_terminated }
    }

    /// Eats double-quoted string and returns true
    /// if string is terminated.
    fn double_quoted_string(&mut self) -> bool {
        while let Some(c) = self.next() {
            match c {
                '"' => {
                    // Found a closing quote, meaning the string is properly terminated.
                    return true;
                }
                '\\' if self
                    .peek_nth(0)
                    .map(|next_ch| ['\\', '"'].contains(next_ch))
                    .unwrap_or(false) =>
                {
                    // If we encounter a backslash, check if the next character is:
                    // - A backslash (`\\`) → meaning the user wrote `\\` to represent a single `\`
                    // - A double quote (`"`) → meaning the user wrote `\"` to include a quote in the string
                    //
                    // If so, consume the next character as well to skip the escape sequence.
                    self.next();
                }
                _ => (),
            }
        }

        // If we reach here, the iterator is exhausted before finding a closing quote.
        // This means the string was left open (unterminated string).
        false
    }
}

impl<I> CursorIter<I>
where
    I: Iterator<Item = char>,
{
    pub(super) fn parse_single_quote_string(&mut self) -> LiteralKind {
        let is_terminated = self.single_quoted_string();
        LiteralKind::Str { is_terminated }
    }

    fn single_quoted_string(&mut self) -> bool {
        while let Some(ch) = self.peek_nth(0).cloned() {
            match ch {
                // Quotes are terminated, finish parsing.
                '\'' => {
                    self.next();
                    return true;
                }

                // Newline without following '\'' means unclosed quote, stop parsing.
                // And single quote does not allow multiple lines
                '\n' if self.peek_nth(1).is_some_and(|next_ch| next_ch != &'\'') => break,

                // Escaped slash is considered one character, so bump twice.
                '\\' => {
                    self.next();
                    self.next();
                }
                // Skip the character.
                _ => {
                    self.next();
                }
            }
        }

        false
    }
}