skullbrain-parser 0.1.0

Parses skullbrain tokens into a concrete syntax tree (CST).
Documentation
mod look_aheadable;
mod position;
mod recovery;

use itertools::PeekingNext;
pub(crate) use look_aheadable::PeekSequence;
pub(crate) use position::PositionedToken;
use skullbrain_lexer::TokenKind;

use crate::syntax_kind::SyntaxKind;
use look_aheadable::Sequence;
use skullbrain_diagnostics::{DiagnosticDetails, EmitDiagnostics, ParseError};
use std::path::Path;

pub(crate) trait Diagnostics: EmitDiagnostics<DiagnosticType = ParseError> {}

impl<T: EmitDiagnostics<DiagnosticType = ParseError>> Diagnostics for T {}

/// Represents the state of the parser during the parsing process.
///
/// It holds the input source, diagnostic emitter, the lexer for tokenizing the input,
/// and the builder responsible for constructing the Concrete Syntax Tree (CST).
pub(crate) struct ParserState<'input, D: Diagnostics, C: ConcreteSyntaxTreeBuilder> {
    /// The path to the source file being parsed. Used for diagnostic messages.
    source: &'input Path,
    /// The diagnostic emitter responsible for reporting errors and warnings.
    diagnostic: D,
    /// The lexer that produces tokens from the input character stream.
    lexer: Sequence<'input>,
    /// The builder that constructs the Concrete Syntax Tree (CST).
    builder: C,
    /// The end index of the last processed token, useful for pinpointing
    /// the location of unexpected EOF errors for diagnostics.
    input_len: usize,

    /// Used to disallow certain statements outside their intended scope of use
    scope_contexts: Vec<ScopeContext>,
}

#[derive(PartialEq)]
pub(crate) enum ScopeContext {
    Loop,
    Function,
    Class,
    Enum,
    TraitDelc,
}

/// Trait for constructing a Concrete Syntax Tree (CST).
pub(crate) trait ConcreteSyntaxTreeBuilder {
    /// The type of the final tree that will be produced by this builder.
    type Output;

    /// A checkpoint type
    type Checkpoint: Clone;

    /// Starts a new node with the specified syntax kind.
    ///
    /// This method is called when entering a new syntactic construct.
    /// The builder should remember the current position to correctly
    /// determine the range of the node when `finish_node` is called.
    ///
    /// # Parameters
    /// * `kind` - The syntactic kind of the node being started
    fn start_node(&mut self, kind: SyntaxKind);

    /// Start a node at the given checkpoint wrapping all nodes
    /// and tokens created since the checkpoint was taken.
    fn start_node_at(&mut self, checkpoint: Self::Checkpoint, kind: SyntaxKind);

    /// Finishes the current node.
    ///
    /// This method is called when exiting a syntactic construct.
    /// The builder should finalize the current node, associating all tokens
    /// and child nodes since the matching `start_node` call with this node.
    fn finish_node(&mut self);

    /// Adds a token to the current node.
    ///
    /// # Parameters
    /// * `kind` - The kind of the token
    /// * `item` - The text and range of the token contentn
    fn token(&mut self, kind: SyntaxKind, item: PositionedToken);

    /// Take a snapshot of the builder’s state, which can be used to retroactively insert surrounding nodes
    /// by calling `start_node_at`, or for backtracking by allowing to `revert_to` the returned checkpoint.
    /// Checkpoints are invalidated and can no longer be used if nodes older than (started before) the checkpoint are finished,
    /// and also if the builder is reverted past them to an even earlier checkpoint.
    fn checkpoint(&self) -> Self::Checkpoint;

    /// Builds and returns the final constructed tree.
    ///
    /// This method should be called after all tokens and nodes have been processed.
    ///
    /// # Returns
    /// The final constructed tree of type `Self::Output`
    fn finish(self) -> Self::Output;
}

impl<'input, D: Diagnostics, C: ConcreteSyntaxTreeBuilder> ParserState<'input, D, C> {
    pub(crate) fn new(source: &'input Path, input: &'input str, diagnostic: D, builder: C) -> Self {
        Self {
            source,
            diagnostic,
            lexer: Sequence::new(input),
            builder,
            input_len: input.len(),
            scope_contexts: Vec::new(),
        }
    }

    /// Peeks at the next token in the input stream without consuming it.
    ///
    /// # Returns
    ///
    /// An `Option` containing a reference to the next `Token` if there is one,
    /// or `None` if the end of the input has been reached.
    pub(crate) fn peek(&mut self) -> Option<&PositionedToken> {
        self.lexer.peek()
    }

    pub(crate) fn peek_raw(&mut self) -> Option<&PositionedToken> {
        self.lexer.peek_raw()
    }

    pub(super) fn next_raw(&mut self) -> Option<<Self as Iterator>::Item> {
        self.lexer.next_raw()
    }

    pub(crate) fn diagnostic_details_with_item(
        &self,
        item: &PositionedToken,
    ) -> DiagnosticDetails<'input> {
        DiagnosticDetails::new(self.source, item.range.clone())
    }

    pub(crate) fn diagnostic_details(&mut self) -> DiagnosticDetails<'input> {
        let source = self.source;
        let range = self
            .peek_raw()
            .map(|i| i.range.clone())
            .unwrap_or(self.input_len..self.input_len);

        DiagnosticDetails::new(source, range)
    }

    pub(crate) fn peek_sequence<T>(
        &mut self,
        transform: impl FnOnce(&mut PeekSequence<'_, 'input>) -> T,
    ) -> T {
        transform(&mut PeekSequence::new(&mut self.lexer))
    }
}

impl<'input, D: Diagnostics, C: ConcreteSyntaxTreeBuilder> ParserState<'input, D, C> {
    /// Push a new delimited context
    ///
    /// # Parameters
    /// * `closing` - The expected closing token for this delimited region (e.g., `}` or `)`).
    pub(crate) fn push_delimited(&mut self, closing: TokenKind) {
        self.lexer.push_delimited(closing)
    }

    pub(crate) fn resume_normal_tokenstream(&mut self) {
        self.lexer.resume_normal_tokenstream();
    }
}

impl<'input, D: Diagnostics, C: ConcreteSyntaxTreeBuilder> ParserState<'input, D, C> {
    pub(crate) fn parse_token_with(
        &mut self,
        kind: SyntaxKind,
        block: impl FnOnce(&mut Self, &PositionedToken),
    ) {
        let Some(item) = self.next() else {
            unreachable!()
        };
        block(self, &item);
        self.token(kind, item);
    }

    pub(crate) fn parse_token(&mut self, kind: SyntaxKind) {
        let Some(item) = self.next() else {
            unreachable!()
        };

        self.token(kind, item);
    }

    pub(crate) fn parse_raw_token(&mut self, kind: SyntaxKind) {
        let Some(item) = self.next_raw() else {
            unreachable!()
        };

        self.token(kind, item);
    }
}

impl<'input, D: Diagnostics, C: ConcreteSyntaxTreeBuilder> ParserState<'input, D, C> {
    pub(crate) fn is_directly_in_ctx(&self, context: ScopeContext) -> bool {
        self.scope_contexts
            .last()
            .is_some_and(|ctx| ctx == &context)
    }

    /// Check if we're currently in a class context with valid nesting rules
    /// Returns true if:
    /// - We're in a class context
    /// - Functions are not nested within other functions
    pub(crate) fn is_in_valid_class_context(&self) -> bool {
        let mut in_valid_ctx = false;

        let mut found_function = false;

        for ctx in self.scope_contexts.iter().rev() {
            match ctx {
                ScopeContext::Loop => {
                    // Skip loops - they don't affect validity
                    continue;
                }
                ScopeContext::Function => {
                    if found_function {
                        return false; // Found nested function - invalid
                    }

                    found_function = true;
                }
                ScopeContext::Class => {
                    in_valid_ctx = true;
                    break;
                }
                ScopeContext::Enum | ScopeContext::TraitDelc => break,
            }
        }

        in_valid_ctx
    }

    pub(crate) fn in_valid_trait_delc_context(&self) -> bool {
        let mut iter = self.scope_contexts.iter().rev();
        iter.next()
            .is_some_and(|i| matches!(i, ScopeContext::Function))
            && iter
                .next()
                .is_some_and(|i| matches!(i, ScopeContext::TraitDelc))
    }

    pub(crate) fn parse_in_context<T>(
        &mut self,
        context: ScopeContext,
        parse: impl FnOnce(&mut Self) -> T,
    ) -> T {
        self.enter_context(context);
        let out = parse(self);
        self.exit_context();
        out
    }

    fn enter_context(&mut self, context: ScopeContext) {
        self.scope_contexts.push(context);
    }

    fn exit_context(&mut self) {
        self.scope_contexts
            .pop()
            .expect("failed to pop scope context as it was empty");
    }
}
impl<'input, D: Diagnostics, C: ConcreteSyntaxTreeBuilder> EmitDiagnostics
    for ParserState<'input, D, C>
{
    type DiagnosticType = <D as EmitDiagnostics>::DiagnosticType;
    fn emit_diagnostic(
        &mut self,
        kind: Self::DiagnosticType,
        diagnostic_details: DiagnosticDetails,
    ) {
        self.diagnostic.emit_diagnostic(kind, diagnostic_details);
    }
}

impl<'input, D: Diagnostics, C: ConcreteSyntaxTreeBuilder> Iterator for ParserState<'input, D, C> {
    type Item = PositionedToken;

    fn next(&mut self) -> Option<Self::Item> {
        self.lexer.next()
    }
}

impl<'input, D: Diagnostics, C: ConcreteSyntaxTreeBuilder> ConcreteSyntaxTreeBuilder
    for ParserState<'input, D, C>
{
    type Output = C::Output;
    type Checkpoint = C::Checkpoint;
    fn start_node(&mut self, kind: SyntaxKind) {
        self.builder.start_node(kind);
    }

    fn finish_node(&mut self) {
        self.builder.finish_node();
    }

    fn token(&mut self, kind: SyntaxKind, item: PositionedToken) {
        self.builder.token(kind, item);
    }

    fn finish(self) -> Self::Output {
        self.builder.finish()
    }

    fn checkpoint(&self) -> Self::Checkpoint {
        self.builder.checkpoint()
    }

    fn start_node_at(&mut self, checkpoint: Self::Checkpoint, kind: SyntaxKind) {
        self.builder.start_node_at(checkpoint, kind);
    }
}

impl<'input, D: Diagnostics, C: ConcreteSyntaxTreeBuilder> PeekingNext
    for ParserState<'input, D, C>
{
    fn peeking_next<F>(&mut self, accept: F) -> Option<Self::Item>
    where
        Self: Sized,
        F: FnOnce(&Self::Item) -> bool,
    {
        match self.peek() {
            Some(item) if accept(item) => {
                let Some(item) = self.next() else {
                    unreachable!()
                };

                Some(item)
            }
            _ => None,
        }
    }
}