luau-syntax 0.732.0

Luau lexer, parser, AST, CST, and source utilities
Documentation
use crate::ast::{BinaryOp, Expression, ExpressionKind, Statement, StatementTag};
use crate::ast_names::AstName;
use crate::confusables::find_confusable;
use crate::lexer::{ReservedWord as R, Token};
use crate::location::Position;
use std::fmt;

#[derive(Debug, Clone, Copy)]
enum Associativity {
    Left,
    Right,
}

pub(super) trait TokenAnalysis {
    fn binary_priority(&self) -> Option<BinaryPriority>;
    fn compound_assignment_op(&self) -> Option<BinaryOp>;
    fn is_keyword(&self, expected: &str) -> bool;
    fn description(&self) -> Option<TokenDescription>;
}

pub(super) enum TokenDescription {
    BrokenUnicode { codepoint: u32 },
}

#[derive(Debug, Clone, Copy)]
pub(super) struct BinaryPriority {
    pub op: BinaryOp,
    pub left: u8,
    pub right: u8,
    pub op_position: Option<Position>,
}

impl fmt::Display for TokenDescription {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match *self {
            Self::BrokenUnicode { codepoint: 0 } => formatter.write_str("invalid UTF-8 sequence"),
            Self::BrokenUnicode { codepoint } => {
                if let Some(suggestion) = find_confusable(codepoint) {
                    write!(
                        formatter,
                        "Unicode character U+{codepoint:x} (did you mean '{suggestion}'?)"
                    )
                } else {
                    write!(formatter, "Unicode character U+{codepoint:x}")
                }
            }
        }
    }
}

impl TokenAnalysis for Token<'_, '_> {
    fn binary_priority(&self) -> Option<BinaryPriority> {
        let (op, precedence, associativity) = match self {
            Token::Reserved(R::Or) => (BinaryOp::Or, 1, Associativity::Left),
            Token::Reserved(R::And) => (BinaryOp::And, 2, Associativity::Left),
            Token::EqualEqual => (BinaryOp::Equal, 3, Associativity::Left),
            Token::TildeEqual => (BinaryOp::NotEqual, 3, Associativity::Left),
            Token::Less => (BinaryOp::Less, 3, Associativity::Left),
            Token::LessEqual => (BinaryOp::LessEqual, 3, Associativity::Left),
            Token::Greater => (BinaryOp::Greater, 3, Associativity::Left),
            Token::GreaterEqual => (BinaryOp::GreaterEqual, 3, Associativity::Left),
            Token::DotDot => (BinaryOp::Concat, 5, Associativity::Right),
            Token::Plus => (BinaryOp::Add, 6, Associativity::Left),
            Token::Minus => (BinaryOp::Subtract, 6, Associativity::Left),
            Token::Star => (BinaryOp::Multiply, 7, Associativity::Left),
            Token::Slash => (BinaryOp::Divide, 7, Associativity::Left),
            Token::SlashSlash => (BinaryOp::FloorDivide, 7, Associativity::Left),
            Token::Percent => (BinaryOp::Modulo, 7, Associativity::Left),
            Token::Caret => (BinaryOp::Power, 10, Associativity::Right),
            _ => return None,
        };

        let right_limit = match associativity {
            Associativity::Left => precedence,
            Associativity::Right => precedence - 1,
        };

        Some(BinaryPriority {
            op,
            left: precedence,
            right: right_limit,
            op_position: None,
        })
    }

    fn compound_assignment_op(&self) -> Option<BinaryOp> {
        match self {
            Token::PlusEqual => Some(BinaryOp::Add),
            Token::MinusEqual => Some(BinaryOp::Subtract),
            Token::StarEqual => Some(BinaryOp::Multiply),
            Token::SlashEqual => Some(BinaryOp::Divide),
            Token::SlashSlashEqual => Some(BinaryOp::FloorDivide),
            Token::PercentEqual => Some(BinaryOp::Modulo),
            Token::CaretEqual => Some(BinaryOp::Power),
            Token::DotDotEqual => Some(BinaryOp::Concat),
            _ => None,
        }
    }

    fn is_keyword(&self, expected: &str) -> bool {
        match self {
            Token::Ident(value) => value == expected,
            Token::Reserved(word) => word.as_str() == expected,
            _ => false,
        }
    }

    fn description(&self) -> Option<TokenDescription> {
        match self {
            Token::BrokenUnicode { codepoint } => Some(TokenDescription::BrokenUnicode {
                codepoint: *codepoint,
            }),
            _ => None,
        }
    }
}

pub(super) trait LuauKeyword {
    fn is_reserved_word(&self) -> bool;
}

impl LuauKeyword for str {
    fn is_reserved_word(&self) -> bool {
        matches!(
            self,
            "and"
                | "break"
                | "do"
                | "else"
                | "elseif"
                | "end"
                | "false"
                | "for"
                | "function"
                | "if"
                | "in"
                | "local"
                | "nil"
                | "not"
                | "or"
                | "repeat"
                | "return"
                | "then"
                | "true"
                | "until"
                | "while"
        )
    }
}

impl LuauKeyword for AstName<'_> {
    fn is_reserved_word(&self) -> bool {
        matches!(
            self.bytes(),
            b"and"
                | b"break"
                | b"do"
                | b"else"
                | b"elseif"
                | b"end"
                | b"false"
                | b"for"
                | b"function"
                | b"if"
                | b"in"
                | b"local"
                | b"nil"
                | b"not"
                | b"or"
                | b"repeat"
                | b"return"
                | b"then"
                | b"true"
                | b"until"
                | b"while"
        )
    }
}

pub(super) trait ExpressionAnalysis {
    fn accepts_call(&self) -> bool;
    fn returns_multiple(&self) -> bool;
    fn is_call_statement(&self) -> bool;
}

impl ExpressionAnalysis for Expression<'_> {
    fn accepts_call(&self) -> bool {
        matches!(
            self.kind(),
            ExpressionKind::Call { .. }
                | ExpressionKind::Error { .. }
                | ExpressionKind::FunctionLiteral(_)
                | ExpressionKind::Grouped(_)
                | ExpressionKind::IndexExpr { .. }
                | ExpressionKind::IndexName { .. }
                | ExpressionKind::Instantiate { .. }
                | ExpressionKind::Local { .. }
                | ExpressionKind::Global(_)
        )
    }

    fn returns_multiple(&self) -> bool {
        matches!(
            self.kind(),
            ExpressionKind::Call { .. } | ExpressionKind::Varargs
        )
    }

    fn is_call_statement(&self) -> bool {
        matches!(self.kind(), ExpressionKind::Call { .. })
    }
}

pub(super) trait ExpressionSliceAnalysis {
    fn has_enough_values(&self, binding_count: usize) -> bool;
}

impl ExpressionSliceAnalysis for [Expression<'_>] {
    fn has_enough_values(&self, binding_count: usize) -> bool {
        self.len() >= binding_count
            || self
                .last()
                .is_some_and(|expression| expression.returns_multiple())
    }
}

pub(super) trait StatementAnalysis {
    fn is_terminal(&self) -> bool;
}

impl StatementAnalysis for Statement<'_> {
    fn is_terminal(&self) -> bool {
        matches!(
            self.tag,
            StatementTag::Break | StatementTag::Continue | StatementTag::Return
        )
    }
}