beans 6.6.0

A parser generator library based on the Earley parser
Documentation
//! # Error
//!
//! This module contains error and warning related primitives.
//! The main type is [`WResult`], a wrapper to [`Result`][std::result::Result] which includes warnings.
//! As a matter of fact, `WResult<T>` is isomorphic to `Result<(T, WarningSet), Error>` where
//! [`WarningSet`] is a set of warnings and [`Error`] is the Beans' error type.

use crate::case::Case;
use crate::span::Span;
use fragile::Fragile;
use std::collections::{linked_list, LinkedList};
use std::fmt;
use std::rc::Rc;
use itertools::Itertools;

/// [`EMPTY_WARNING_SET`] is a warning set without any warnings.
/// It is useful to use this set as an empty warning set
/// instead of creating a new one each time.
const EMPTY_WARNING_SET: WarningSet = WarningSet::Empty;

/// `Result` is a shorthand for the usual `Result` type.
/// It includes the crate's `Error` by default, and is inside `WithWarnings`
/// monad.
pub type Result<T> = std::result::Result<WithWarnings<T>, Error>;

/// `ErrorType` is an enum that contains all the possible errors
/// that `beans` might encounter when parsing.
///
/// # Error types
///
/// `LexerGrammarSyntax`: error that arises when there is a syntax error within the grammar file.
/// `LexingError`: error that arises when lexing.
#[derive(thiserror::Error, Debug)]
pub enum Error {
    /// `InternalError(message: String)`: error in the Beans implementation. This should not happen.
    #[error(
        "Error in the Beans implementation: {message}. This should not happen."
    )]
    InternalError {
        /// The message giving details about the error.
        message: String,
    },
    /// `SerializationError(message: String)`: error serializing.
    #[error("Failed to serialize or deserialize: {0}")]
    SerializationError(#[from] bincode::Error),
    /// `LexerGrammarSyntax(message: String)`: error in the syntax of the lexer grammar.
    #[error(
        "Error in the syntax of the lexer grammar: {message}, {location}."
    )]
    LexerGrammarSyntax {
        /// The message giving details about the error.
        message: String,
        /// The `Location` that made the error occur. It's a hint a what should
        /// be patched.
        location: Fragile<Span>,
    },
    /// `LexerGrammarDuplicateDefinition(token: String)`: duplicate terminal definition.
    #[error("Duplicate definition of the token {token}, {location}.")]
    LexerGrammarDuplicateDefinition {
        token: String,
        location: Fragile<Span>,
    },
    #[error("The token {token} is unwanted, therefore it must have a description, {span}.")]
    LexerGrammarUnwantedNoDescription { token: String, span: Fragile<Span> },
    #[error("Reached EOF while trying to read a string")]
    LexerGrammarEofString,
    /// `LexingError(message: String)`: error while transforming a string stream into a token stream.
    #[error("Lexing error: cannot recognize a token {location}")]
    LexingError {
        /// The `Location` that made the error occur. It's a hint a what should
        /// be patched.
        location: Fragile<Span>,
    },
    #[error("Lexing error: {message}, {span}")]
    UnwantedToken {
        span: Fragile<Span>,
        message: String,
    },
    /// `GrammarDuplicateDefinition(message: String, location: Location)`: duplicate definition at `location`.
    #[error("Found duplicate definition of terminal in grammar: {message}, {location}.")]
    GrammarDuplicateDefinition {
        /// The message giving details about the error.
        message: String,
        /// The `Location` where the second, offending, definition has been
        /// found.
        location: Fragile<Span>,
        /// The `Location` where the first definition has been found.
        old_location: Fragile<Span>,
    },
    #[error("Found two references to {macro_name}, with arities {first_arity} and {second_arity}, {location}.")]
    GrammarArityMismatch {
        macro_name: String,
        first_arity: usize,
        second_arity: usize,
        location: Fragile<Span>,
    },
    /// `GrammarNonTerminalDuplicate(message: String)`: duplicate non-terminal in the grammar.
    #[error("Found duplicate definition of nonterminal in grammar: {message}, {location}.")]
    GrammarNonTerminalDuplicate {
        /// The message giving details about the error.
        message: String,
        /// The `Location` that made the error occur. It's a hint a what should
        /// be patched.
        location: Fragile<Span>,
    },
    #[error("Tried to invoke terminal {terminal} as if it was a macro, {location}.")]
    GrammarTerminalInvocation {
        terminal: String,
        location: Fragile<Span>,
    },
    /// `GrammarSyntaxError(message: String)`: syntax error in the grammar.
    #[error("Syntax error in grammar: {message}, {location}.")]
    GrammarSyntaxError {
        /// The message giving details about the error.
        message: String,
        /// The `Location` that made the error occur. It's a hint a what should
        /// be patched.
        location: Fragile<Span>,
    },
    /// `GrammarVariantKey(message: String)`: have a variant key in proxy is bad.
    #[error("The `variant` key is reserved in proxies, {location}.")]
    GrammarVariantKey { location: Fragile<Span> },
    /// `SyntaxError`: syntax error in the input.
    #[error(
	"Syntax error: {name} doesn't make sense {location}.\n{}",
	match &.alternatives[..] {
	    [] => "".to_string(),
	    [x] => format!("  -> try inserting {} instead", x.to_string()),
	    [rest@.., last] => format!(
		"  -> try inserting {} or {} instead",
		rest
		    .into_iter()
		    .map(|x| &**x)
		    .intersperse(", ")
		    .collect::<String>(),
		last
	    ),
	}
    )]
    SyntaxError {
        name: String,
        alternatives: Vec<String>,
        /// The `Location` that made the error occur. It's a hint a what should
        /// be patched.
        location: Fragile<Span>,
    },
    #[error("Syntax error: EOF but parsing isn't done {location}.")]
    SyntaxErrorValidPrefix { location: Fragile<Span> },
    /// `IOError`: any io error.
    #[error("IO error: {0}")]
    IOError(#[from] std::io::Error),
    /// `RegexError`: any regex error
    #[error("Regex error: {message}, {location}")]
    RegexError {
        /// The `Location` that made the error occur. It's a hint a what should
        /// be patched.
        location: Fragile<Span>,
        /// The message giving details about the error.
        message: String,
    },
    /// `SameOutputAndInput`: writing to the output file would overwrite the
    /// input file, which Beans refuses to do.
    #[error("The file to be written is the same as the source file.")]
    SameOutputAndInput,
}

/// # Summary
///
/// Possible type of a warning.
#[derive(Debug, PartialEq, Eq)]
pub enum WarningType {
    /// `CaseConvention(message: String, expected_case: Case, found_case: Case)`
    /// Code does not respect case convention, which is bad.
    CaseConvention(Rc<str>, Case, Case),
    /// `UndefinedNonTerminal(definition: String, non_terminal: String)`
    ///
    /// Rule defined in `definition` refers to the undefined non-terminal `non_terminal`,
    /// which means it will never be constructed.
    /// This is not an error to allow users to add rules that don't work yes,
    /// as a WIP feature without preventing the compilation of the rest of the code.
    UndefinedNonTerminal(Rc<str>, Rc<str>),
    /// NullWarning
    /// Empty warning that is used only in example. It does not mean anything besides
    /// there is a warning. Please consider using a more excplicit warning type in real code.
    NullWarning,
}

/// # Summary
///
/// `Warning` is a non-fatal error.
#[derive(Debug, PartialEq, Eq)]
pub struct Warning {
    location: Option<Span>,
    warn_type: WarningType,
}

impl Warning {
    /// Build a new warning of type `warn_type`.
    pub fn new(warn_type: WarningType) -> Self {
        Self {
            warn_type,
            location: None,
        }
    }
    /// Build a new warning of type `warn_type` with `location`.
    pub fn with_location(warn_type: WarningType, location: Span) -> Self {
        Self {
            warn_type,
            location: Some(location),
        }
    }

    /// Get the type of the warning.
    pub fn warn_type(&self) -> &WarningType {
        &self.warn_type
    }

    /// Get the optional location of the warning.
    pub fn location(&self) -> Option<&Span> {
        self.location.as_ref()
    }
}

impl fmt::Display for Warning {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use WarningType::*;
        let r#type;
        let message: Box<dyn AsRef<str>>;
        match &self.warn_type {
            CaseConvention(msg, _case_found, _case_expected) => {
                r#type = "Case convention warning";
                message = Box::new(msg.as_ref());
            }
            UndefinedNonTerminal(origin, non_terminal) => {
                r#type = "Undefined non-terminal warning";
                message = Box::new(format!(
                    "In definition of non-terminal `{}', `{}' has been used but not defined",
                    origin, non_terminal
                ));
            }
            NullWarning => {
                r#type = "Warning";
                message = Box::new("Empty warning");
            }
        };
        if let Some(location) = self.location.as_ref() {
            write!(
                f,
                "{}\n @{}, from {}:{} to {}:{}\n{}",
                r#type,
                location.file().display(),
                location.start().0,
                location.start().1,
                location.end().0,
                location.end().1,
                (*message).as_ref()
            )
        } else {
            write!(f, "{}\n{}", r#type, (*message).as_ref())
        }
    }
}

/// # Summary
///
/// Iterator over a warning set.
#[derive(Debug)]
pub struct WarningIterator<'iter> {
    warnings: Option<linked_list::Iter<'iter, Warning>>,
}

impl<'iter> WarningIterator<'iter> {
    fn new(warnings: Option<linked_list::Iter<'iter, Warning>>) -> Self {
        Self { warnings }
    }
}

impl<'iter> Iterator for WarningIterator<'iter> {
    type Item = &'iter Warning;
    fn next(&mut self) -> Option<Self::Item> {
        self.warnings.as_mut().and_then(|iterator| iterator.next())
    }
}

/// # Summary
///
/// [`WarningSet`] is an abstract type for a [`Warning`] collection.
/// It should optimize empty creation (because most of the code will build a warning set without using it).
/// It should also allow constant time union (or at least fast union) of two sets.
#[derive(Debug, PartialEq, Eq)]
pub enum WarningSet {
    /// A set of warnings that actually contains some warnings.
    Set(LinkedList<Warning>),
    /// An empty set of warnings.
    Empty,
}

impl WarningSet {
    /// Create an empty set.
    #[inline]
    pub fn empty() -> Self {
        EMPTY_WARNING_SET
    }

    /// Add a warning to the set.
    pub fn add(&mut self, warning: Warning) {
        match self {
            Self::Set(warnings) => warnings.push_back(warning),
            Self::Empty => {
                let mut ll = LinkedList::new();
                ll.push_back(warning);
                *self = Self::Set(ll);
            }
        }
    }

    /// Iterate over warnings in the set.
    pub fn iter(&self) -> WarningIterator<'_> {
        WarningIterator::new(match self {
            Self::Set(warnings) => Some(warnings.iter()),
            Self::Empty => None,
        })
    }

    /// Unify two warning sets. `self` gets updated, and consumes the other warning set.
    pub fn extend(&mut self, warningset: Self) {
        match self {
            Self::Set(selfwarnings) => match warningset {
                Self::Set(mut otherwarnings) => {
                    selfwarnings.append(&mut otherwarnings)
                }
                Self::Empty => {}
            },
            Self::Empty => match warningset {
                Self::Set(otherwarnings) => *self = Self::Set(otherwarnings),
                Self::Empty => {}
            },
        }
    }

    /// Return whether the warning set is empty.
    #[inline]
    pub fn is_empty(&self) -> bool {
        matches!(self, Self::Empty)
    }

    /// Utilitary function to ease the usage of regular error types.
    #[inline]
    pub fn unpack<T>(
        &mut self,
        WithWarnings { content, warnings }: WithWarnings<T>,
    ) -> T {
        self.extend(warnings);
        content
    }

    /// Constructor of the `WithWarnings` monad.
    #[inline]
    pub fn with<T>(self, content: T) -> WithWarnings<T> {
        WithWarnings {
            content,
            warnings: self,
        }
    }

    #[inline]
    pub fn with_ok<T, E>(
        self,
        content: T,
    ) -> std::result::Result<WithWarnings<T>, E> {
        Ok(self.with(content))
    }

    /// Useful constructor of the `WithWarnings` monad when you don't have any
    /// warnings.
    #[inline]
    pub fn empty_with<T>(content: T) -> WithWarnings<T> {
        Self::empty().with(content)
    }

    /// `on` provides a shorthand by conjuging `f` with `with` and `unpack`.
    #[inline]
    pub fn on<T, F, U>(
        mut self,
        content: WithWarnings<T>,
        f: F,
    ) -> WithWarnings<U>
    where
        F: FnOnce(T) -> U,
    {
        let u = f(self.unpack(content));
        self.with(u)
    }
}

impl Default for WarningSet {
    fn default() -> Self {
        Self::empty()
    }
}

/// `WithWarnings` is a handy monad to carry warnings around.
/// The idea is that instead of triggering a warning when the offeding action
/// is executed, data carries with it warnings that were thrown to produce that
/// data.
/// It simplifies tracking exactly what caused certain warnings, what warnings
/// the creation and manipulation of some data caused, and interacts nicely
/// with the `Result` monad.
#[derive(Debug)]
#[must_use]
pub struct WithWarnings<T> {
    content: T,
    warnings: WarningSet,
}

impl<T> WithWarnings<T> {
    /// `unwrap` is a brutal way to get out of the monad.
    /// It means you can ensure there are no warnings.
    /// Note that this will **not** panic if there are warnings.
    #[inline]
    pub fn unwrap(self) -> T {
        self.content
    }

    /// `chain` is to be used as the composition functor
    /// of the WithWarnings monad.
    /// It can be ended either with `unpack_into`, if you want
    /// to retrieve only the value, kept as-is, or with `with` if you want
    /// to stay in the monad (useful in return statements).
    #[inline]
    pub fn chain<F, U>(self, f: F) -> WithWarnings<U>
    where
        F: FnOnce(T) -> WithWarnings<U>,
    {
        let WithWarnings {
            content,
            mut warnings,
        } = f(self.content);
        warnings.extend(self.warnings);
        WithWarnings { content, warnings }
    }

    /// `unpack_into` is the symetrical operator of `unpack` for `WarningSet`,
    /// but applied from WithWarnings. There are no differences, it's only
    /// for practical reasons.
    #[inline]
    pub fn unpack_into(self, warnings: &mut WarningSet) -> T {
        warnings.unpack(self)
    }

    /// `with` does the same job that `WarningSet`, but this one is an external
    /// operation in the monad, whereas the other one is a constructor.
    #[inline]
    pub fn with(mut self, warnings: WarningSet) -> Self {
        self.warnings.extend(warnings);
        self
    }

    /// `with_ok` is `with` wrapped in `Ok`.
    #[inline]
    pub fn with_ok<E>(
        self,
        warnings: WarningSet,
    ) -> std::result::Result<WithWarnings<T>, E> {
        Ok(self.with(warnings))
    }
}