skullbrain-parser 0.1.0

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

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

#[derive(Default)]
struct Context<'input> {
    missing_open_delimiter: Option<DiagnosticDetails<'input>>,
    missing_close_delimiter: Option<(DiagnosticDetails<'input>, bool)>,
}

impl<'input, D: Diagnostics, C: ConcreteSyntaxTreeBuilder> ParserState<'input, D, C> {
    pub(crate) fn parse_brace_aggressive(&mut self, parse_content: impl FnOnce(&mut Self)) {
        self.parse_delimited_aggressive(
            SyntaxKind::DelimitedRange,
            (
                TokenKind::Punctuation(PunctuationKind::OpenBrace),
                SyntaxKind::OpenBrace,
            ),
            (
                TokenKind::Punctuation(PunctuationKind::CloseBrace),
                SyntaxKind::CloseBrace,
            ),
            parse_content,
        )
    }

    pub(crate) fn parse_parenthesis_aggressive(&mut self, parse_content: impl FnOnce(&mut Self)) {
        self.parse_delimited_aggressive(
            SyntaxKind::DelimitedRange,
            (
                TokenKind::Punctuation(PunctuationKind::OpenParen),
                SyntaxKind::OpenParen,
            ),
            (
                TokenKind::Punctuation(PunctuationKind::CloseParen),
                SyntaxKind::CloseParen,
            ),
            parse_content,
        )
    }
    /// For cases where you want to be even more aggressive - will parse delimited content
    /// if there's ANY hint it might be delimited (including just having content that could be wrapped)
    pub(crate) fn parse_delimited_aggressive(
        &mut self,
        range_kind: SyntaxKind,
        open: (TokenKind, SyntaxKind),
        close: (TokenKind, SyntaxKind),
        parse_content: impl FnOnce(&mut Self),
    ) {
        self.start_node(range_kind);

        let mut context = Context::default();
        self.skip_trivia();

        self.has_opening_delimiter(&mut context, &open);

        self.push_delimited(close.0.clone());

        if self.try_possibilities(&mut context, &close) {
            self.skip_trivia();

            let close_tok = &((&close).0);
            if self.peek_raw().is_some_and(|i| !i.kind().is(close_tok)) {
                parse_content(self);
            }

            if self.try_possibilities(&mut context, &close) {
                let details = self.diagnostic_details();
                context.missing_close_delimiter = Some((details, true));
            }
        }

        self.resume_normal_tokenstream();
        self.finish_node();

        self.try_emit_diagnostics_with_context(open.0, close.0, context)
    }

    fn try_emit_diagnostics_with_context(
        &mut self,
        open: TokenKind,
        close: TokenKind,
        context: Context<'_>,
    ) {
        match (
            context.missing_open_delimiter,
            context.missing_close_delimiter,
        ) {
            (None, None) => (),
            (Some(diagnostic_details), None) => {
                let error = ParseError::expected_token_before(open);
                self.emit_diagnostic(error, diagnostic_details);
            }
            (None, Some((diagnostic_details, is_before))) => {
                let error = if is_before {
                    ParseError::expected_token_before(close)
                } else {
                    ParseError::expected_token_after(close)
                };

                self.emit_diagnostic(error, diagnostic_details);
            }

            (Some(d1), Some((d2, _))) => {
                let diagnostic_details = DiagnosticDetails {
                    span: d1.span.start..d2.span.end,
                    ..d1
                };

                let error = ParseError::wrap_with(open, close);
                self.emit_diagnostic(error, diagnostic_details);
            }
        }
    }

    fn has_opening_delimiter(
        &mut self,
        context: &mut Context<'input>,
        open: &(TokenKind, SyntaxKind),
    ) {
        let open_token = &open.0;
        match self.peek_raw() {
            Some(item) if item.kind().is(&open_token) => {
                self.parse_raw_token(open.1);
            }
            Some(_) => {
                self.recover();

                match self.peek_raw() {
                    Some(item) if item.kind().is(&open_token) => {
                        self.parse_raw_token(open.1);
                    }
                    _ => {
                        let details = self.diagnostic_details();
                        context.missing_open_delimiter = Some(details);
                    }
                }
            }
            _ => {
                let details = self.diagnostic_details();
                context.missing_open_delimiter = Some(details);
            }
        }
    }

    fn try_possibilities(
        &mut self,
        context: &mut Context<'input>,
        (close_tok, close_kind): &(TokenKind, SyntaxKind),
    ) -> bool {
        self.skip_trivia();

        match self.peek_raw() {
            // Case 1: End of input - missing content and closing token
            None => {
                let details = self.diagnostic_details();
                context.missing_close_delimiter = Some((details, false));
                false
            }

            // Case 2: Immediate closing token - empty delimiters
            Some(i) if i.kind().is(&close_tok) => {
                let Some(i) = self.next_raw() else {
                    unreachable!()
                };
                self.token(close_kind.clone(), i);
                false
            }

            Some(_) => true,
        }
    }
}

impl<'input, D: Diagnostics, C: ConcreteSyntaxTreeBuilder> ParserState<'input, D, C> {
    pub(crate) fn peek_sequence_for_brace<'a>(
        peekable: &mut PeekSequence,
        should_parse_content: impl FnOnce(&mut PeekSequence) -> bool,
    ) -> bool {
        Self::peek_sequence_for_delimited(
            (PunctuationKind::OpenBrace, PunctuationKind::CloseBrace),
            peekable,
            should_parse_content,
        )
    }

    pub(crate) fn peek_sequence_for_parenthesis(
        peekable: &mut PeekSequence,
        should_parse_content: impl FnOnce(&mut PeekSequence) -> bool,
    ) -> bool {
        Self::peek_sequence_for_delimited(
            (PunctuationKind::OpenParen, PunctuationKind::CloseParen),
            peekable,
            should_parse_content,
        )
    }

    fn peek_sequence_for_delimited(
        (open, close): (PunctuationKind, PunctuationKind),
        peekable: &mut PeekSequence,
        should_parse_content: impl FnOnce(&mut PeekSequence) -> bool,
    ) -> bool {
        let contains_open_delimiter = peekable.peek_next_is(&TokenKind::Punctuation(open));

        let content = should_parse_content(peekable);

        let contains_close_delimiter = peekable.peek_next_is(&TokenKind::Punctuation(close));
        (contains_open_delimiter || contains_close_delimiter) && content
    }
}

#[cfg(test)]
mod test {
    use crate::{
        primitive::is_trivia, state::ConcreteSyntaxTreeBuilder, test::create_parsing_test,
        Diagnostics, ParserState, SyntaxKind,
    };

    #[test]
    fn test_delimited_parser() {
        create_parsing_test("delimited/*.skull", |state| {
            state.parse_brace_aggressive(parse_content);
        });
    }

    fn parse_content(state: &mut ParserState<impl Diagnostics, impl ConcreteSyntaxTreeBuilder>) {
        loop {
            match state.peek() {
                None => break,
                Some(item) if is_trivia(item.kind()) => {
                    // Handle trivia first
                    let skipped_trivia = state.skip_trivia();
                    debug_assert!(skipped_trivia);
                }
                Some(item) if item.kind().is_unknown() => {
                    state.recover();
                }
                Some(_) => {
                    let Some(item) = state.next() else {
                        unreachable!()
                    };

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