skullbrain-parser 0.1.0

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

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

impl<'input, D: Diagnostics, C: ConcreteSyntaxTreeBuilder> ParserState<'input, D, C> {
    /// Parses a sequence of elements separated by commas.
    ///
    /// # Parameters
    /// * `should_parse_content` - A function that determines if `parse_content` should be called at the current position
    /// * `parse_content` - A function that parses a single content element
    pub(crate) fn parse_seperated_by_comma(
        &mut self,
        should_parse_content: impl for<'a> FnOnce(&mut PeekSequence<'a, 'input>) -> bool + Clone,
        parse_content: impl Fn(&mut Self),
    ) {
        self.parse_seperated_by(
            PunctuationKind::Comma,
            SyntaxKind::Comma,
            should_parse_content,
            parse_content,
        );
    }

    /// Creates a predicate function for use with comma-separated sequence parsing.
    ///
    /// This helper method wraps a content detection predicate to be used specifically
    ///
    /// # Parameters
    /// * `should_parse_content` - The original predicate function to determine if content should be parsed
    ///
    /// # Returns
    /// A predicate function compatible with sequence parsing methods
    pub(crate) fn peek_sequence_for_seperated_by_comma(
        should_parse_content: impl FnOnce(&mut PeekSequence) -> bool,
    ) -> impl FnOnce(&mut PeekSequence) -> bool {
        should_parse_content
    }
}

struct Context {
    /// Tracks the state of separator encounters during parsing.
    /// - `None`: Indicates the first iteration of parsing.
    /// - `Some(true)`: Last encountered token was a separator.
    /// - `Some(false)`: Last encountered token was not a separator.
    encountered_separator_last: Option<bool>,

    /// Prevents duplicate error nodes when the same separator error persists.
    /// Set to `true` when an expected separator error has been added in the previous iteration,
    /// preventing redundant errors in the current iteration.
    expected_separator_error_added_last_iteration: bool,

    /// Indicates a recovery state after encountering an error in expression parsing.
    /// When `true`, the parser has called `ParserState::recover()` after encountering an error
    /// in an expected expression. This flag helps manage error reporting in sequences like
    /// "brain n ? n eating" where an expression is needed,
    /// but we've already recovered from a malformed expression.
    recovered_context: bool,
}

impl Default for Context {
    fn default() -> Self {
        Self {
            encountered_separator_last: None,
            expected_separator_error_added_last_iteration: false,
            recovered_context: false,
        }
    }
}

impl<'input, D: Diagnostics, C: ConcreteSyntaxTreeBuilder> ParserState<'input, D, C> {
    fn parse_seperated_by(
        &mut self,
        sep_token: PunctuationKind,
        sep_kind: SyntaxKind,
        should_parse_content: impl FnOnce(&mut PeekSequence<'_, 'input>) -> bool + Clone,
        parse_content: impl Fn(&mut Self),
    ) {
        let mut context = Context::default();

        loop {
            self.skip_trivia();

            match self.peek() {
                // Case 1 : None Left
                None => break,
                Some(item) => {
                    // Case 2: If seperator handle it like so
                    if TokenKind::Punctuation(sep_token).is(item.kind()) {
                        self.found_seperator_as_next_tok(&mut context, sep_kind.clone());
                        continue;
                    }

                    // case 3: try to parse the content_parser
                    if self.peek_sequence(should_parse_content.clone()) {
                        self.found_content_parser_as_following_sequence(
                            &mut context,
                            sep_token,
                            &parse_content,
                        );

                        continue;
                    }

                    // case 4 : Fallback
                    let should_break: bool = self.try_to_recover_from_this_unexpected_sequence(
                        sep_token,
                        &mut context,
                        should_parse_content.clone(),
                    );

                    if should_break {
                        break;
                    }
                }
            }
        }
    }

    fn found_seperator_as_next_tok(&mut self, context: &mut Context, sep_kind: SyntaxKind) {
        context.expected_separator_error_added_last_iteration = false;

        // Even if its the first iteration we still need a element before the seperator
        if context.encountered_separator_last.is_none_or(|b| b) && !context.recovered_context {
            let diagnostic_details = self.diagnostic_details();
            let error = ParseError::expected_expression_between_seperators();
            self.emit_diagnostic(error, diagnostic_details);
        }

        context.encountered_separator_last = Some(true);
        context.recovered_context = false;
        // Consume the separator
        self.parse_token(sep_kind);
    }

    fn found_content_parser_as_following_sequence(
        &mut self,
        context: &mut Context,
        sep_token: PunctuationKind,
        parse_content: impl Fn(&mut Self),
    ) {
        if !context.expected_separator_error_added_last_iteration
            && context.encountered_separator_last.is_some_and(|b| !b)
        {
            let diagnostic_details = self.diagnostic_details();
            let error = ParseError::expected_token_before(TokenKind::Punctuation(sep_token));
            self.emit_diagnostic(error, diagnostic_details);

            context.expected_separator_error_added_last_iteration = true;
        }

        context.expected_separator_error_added_last_iteration = false;
        context.encountered_separator_last = Some(false);
        context.recovered_context = false;

        parse_content(self);
    }

    fn try_to_recover_from_this_unexpected_sequence(
        &mut self,
        sep_token: PunctuationKind,
        context: &mut Context,
        should_parse_content: impl FnOnce(&mut PeekSequence<'_, 'input>) -> bool,
    ) -> bool {
        self.recover();
        context.recovered_context = true;
        let should_break = match self.peek() {
            Some(item) => {
                if item.kind().is(&TokenKind::Punctuation(sep_token))
                    || self.peek_sequence(should_parse_content)
                {
                    false
                } else {
                    true
                }
            }
            None => false,
        };

        should_break
    }
}

#[cfg(test)]
mod tests {
    use skullbrain_lexer::TokenKind;

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

    #[test]
    fn seperated_by_comma() {
        create_parsing_test("seperated_by_comma/*.skull", |state| {
            state.start_node(SyntaxKind::Unknown);
            state.parse_seperated_by_comma(should_parse_content, parse_content);
            state.finish_node();
        });
    }

    const REJECT_KINDS: &[TokenKind] = &[TokenKind::Unknown];

    fn parse_content(state: &mut ParserState<impl Diagnostics, impl ConcreteSyntaxTreeBuilder>) {
        let Some(item) = state.peek() else {
            unreachable!()
        };

        let kind = item.kind().clone();

        if REJECT_KINDS.iter().any(|v| kind.eq(v)) {
            state.recover();
        } else {
            let Some(item) = state.next() else {
                unreachable!()
            };

            assert!(kind.is_ident());

            state.token(SyntaxKind::Ident, item);
        }
    }

    fn should_parse_content(state: &mut PeekSequence) -> bool {
        const ACCEPT: &[TokenKind] = &[TokenKind::Ident];

        state.peek_next().map_or(false, |k| {
            let kind = k.kind();
            ACCEPT.iter().any(|k| k.is(kind))
        })
    }
}