skullbrain-parser 0.1.0

Parses skullbrain tokens into a concrete syntax tree (CST).
Documentation
use itertools::PeekingNext;
use skullbrain_diagnostics::{EmitDiagnostics, ParseError};
use skullbrain_lexer::{OperatorKind, TokenKind};

use crate::{state::ConcreteSyntaxTreeBuilder, Diagnostics, ParserState, SyntaxKind};

impl<'input, D: Diagnostics, C: ConcreteSyntaxTreeBuilder> ParserState<'input, D, C> {
    pub(crate) fn parse_with_binding_power(&mut self, min_bp: u8) {
        let checkpoint = self.checkpoint();

        // FIRST: Handle prefix/unary operators and primary expressions
        self.parse_optional_unary_prefix();

        while let Some(item) = self.peek() {
            let Some(op_kind) = item.kind().operator_ref() else {
                break;
            };

            let Some((left_bp, right_bp)) = binding_power(op_kind) else {
                break;
            };

            if left_bp < min_bp {
                break;
            }

            let op_syntax = operator_to_syntax(op_kind);

            self.start_node_at(checkpoint.clone(), SyntaxKind::Operator);

            self.parse_token(op_syntax);
            self.skip_trivia();

            self.start_node(SyntaxKind::Rhs);
            self.parse_with_binding_power(right_bp);
            self.finish_node();

            self.finish_node();
        }
    }

    fn parse_optional_unary_prefix(&mut self) {
        let item = self.peeking_next(
            |i| matches!(i.kind(),TokenKind::Operator(op_kind) if is_unary_prefix(op_kind) ),
        );

        let start_checkpoint = self.checkpoint();

        match item {
            Some(item) => {
                let Some(op_kind) = item.kind().operator_ref() else {
                    unreachable!()
                };

                self.start_node(SyntaxKind::Operator);

                // For unary prefix, use the prefix binding power, not min_bp
                let prefix_bp = prefix_binding_power(op_kind).unwrap_or(15);

                self.token(operator_to_syntax(op_kind), item);

                self.skip_trivia();

                let before_expr_checkpoint = self.checkpoint();
                self.parse_with_binding_power(prefix_bp);
                self.skip_trivia();

                // This ensures that for standalone unary expressions like "!true", the
                // actual parses expression is under a node so that disambiguating unary-minus vs binary-minus is easier
                if self.peek().is_none() {
                    self.start_node_at(before_expr_checkpoint, SyntaxKind::Rhs);
                    self.finish_node();
                }

                self.finish_node();

                // Wrap the unary operator in an LHS node if there are more tokens to parse.
                // This ensures that for standalone unary expressions like "!true", the root
                // node is an Operator, but when part of a larger expression like "x + !true",
                // the unary operator gets properly wrapped as the LHS of the binary operation.
                if self.peek().is_some() {
                    self.start_node_at(start_checkpoint, SyntaxKind::Lhs);
                    self.finish_node();
                }
            }
            None => {
                // Check if we're about to encounter a binary operator without a left operand
                // (e.g., "&& literal" or "+ 5" at start of expression)
                // This catches malformed expressions
                match self.peek() {
                    Some(item) if matches!(item.kind(),TokenKind::Operator(op) if op.is_not_assignment()) =>
                    {
                        let diagnostic_details = self.diagnostic_details();
                        let error = ParseError::expected_expression_before();
                        self.emit_diagnostic(error, diagnostic_details);
                    }
                    _ => self.__parse_actual_expression(),
                }

                self.skip_trivia();

                // Only wrap the expression in an LHS node if it will be the left operand
                // of a binary operation. For simple expressions like "ident", no LHS wrapper
                // is needed, but for "ident + something", the "ident" needs LHS wrapping.
                if let Some(_) = self.peek().filter(
                    |item| matches!(item.kind(),TokenKind::Operator(op) if op.is_not_assignment()),
                ) {
                    self.start_node_at(start_checkpoint, SyntaxKind::Lhs);
                    self.finish_node();
                }
            }
        }
    }
}

/// Returns the binding power for binary operators as (left_bp, right_bp).
/// Higher numbers = higher precedence (binds tighter).
///
/// Right-associative: left_bp > right_bp
/// Left-associative: left_bp < right_bp
const fn binding_power(op_kind: &OperatorKind) -> Option<(u8, u8)> {
    match op_kind {
        // Assignment is right-associative (a = b = c means a = (b = c))
        // Lowest precedence of all operators
        OperatorKind::Assignment => Some((2, 1)),
        // Logical OR - lowest precedence, left-associative
        OperatorKind::Or => Some((3, 4)),

        // Logical AND - higher than OR, left-associative
        OperatorKind::And => Some((5, 6)),

        // Equality operators - same precedence, left-associative
        OperatorKind::Eq | OperatorKind::NotEq => Some((7, 8)),

        // Comparison operators - higher than equality, left-associative
        OperatorKind::Lt | OperatorKind::Gt | OperatorKind::LtEq | OperatorKind::GtEq => {
            Some((9, 10))
        }

        // Addition and Subtraction - left-associative
        OperatorKind::Plus | OperatorKind::Minus => Some((11, 12)),

        // Multiplication, Division, Modulo - highest precedence, left-associative
        OperatorKind::Mul | OperatorKind::Divide | OperatorKind::Mod => Some((13, 14)),

        // Field access/method chaining - highest precedence, left-associative
        // This should bind tighter than unary operators
        OperatorKind::MethodChaining => Some((17, 18)),

        // Unary operators don't have binary binding power
        OperatorKind::Not => None,
    }
}

/// Returns the binding power for unary prefix operators.
/// Higher numbers = higher precedence.
const fn prefix_binding_power(op_kind: &OperatorKind) -> Option<u8> {
    match op_kind {
        // Unary minus: -x
        OperatorKind::Minus => Some(15),
        // Logical not: !x
        OperatorKind::Not => Some(15),

        // These are only binary operators
        OperatorKind::Plus
        | OperatorKind::Mul
        | OperatorKind::Divide
        | OperatorKind::Mod
        | OperatorKind::Eq
        | OperatorKind::NotEq
        | OperatorKind::Lt
        | OperatorKind::Gt
        | OperatorKind::LtEq
        | OperatorKind::GtEq
        | OperatorKind::And
        | OperatorKind::Or
        | OperatorKind::Assignment
        | OperatorKind::MethodChaining => None,
    }
}

/// Check if this operator can be used as a unary prefix operator
const fn is_unary_prefix(op_kind: &OperatorKind) -> bool {
    prefix_binding_power(op_kind).is_some()
}

const fn operator_to_syntax(op_kind: &OperatorKind) -> SyntaxKind {
    match op_kind {
        OperatorKind::Eq => SyntaxKind::OpEq,
        OperatorKind::NotEq => SyntaxKind::OpNotEq,
        OperatorKind::Lt => SyntaxKind::OpLt,
        OperatorKind::Gt => SyntaxKind::OpGt,
        OperatorKind::LtEq => SyntaxKind::OpLtEq,
        OperatorKind::GtEq => SyntaxKind::OpGtEq,
        OperatorKind::Minus => SyntaxKind::OpMinus,
        OperatorKind::And => SyntaxKind::OpAnd,
        OperatorKind::Or => SyntaxKind::OpOr,
        OperatorKind::Not => SyntaxKind::OpNot,
        OperatorKind::Plus => SyntaxKind::OpPlus,
        OperatorKind::Mul => SyntaxKind::OpMul,
        OperatorKind::Divide => SyntaxKind::OpDivide,
        OperatorKind::Mod => SyntaxKind::OpMod,
        OperatorKind::Assignment => SyntaxKind::Assignment,
        OperatorKind::MethodChaining => SyntaxKind::OpMethodChaining,
    }
}

#[cfg(test)]
mod test {
    #[test]
    fn test_arithmetic_or_boolean() {
        crate::test::create_parsing_test("arithmetic_or_boolean/*.skull", |state| {
            state.parse_expression();
        });
    }
}