regast 0.1.0

Regular expressions with first-class syntax and parse trees
Documentation
//! Regular expressions with first-class pattern ASTs and match parse trees.

use std::sync::Mutex;

pub use regast_core::{
    Backend, Disambiguation, MatchError, NoMatch, ParseTree, ParseTreeWalk, PtKind, PtNode,
    TraceStep,
};
pub use regast_syntax::{
    AnchorKind, AstKind, ClassItem, ClassRange, ClassSet, Cursor, Flow, GroupInfo, GroupKind,
    JsonShape, NodeId, NodeMap, ParseError, ParseErrorKind, Pattern, PerlClass, RepKind, Span,
    Visitor, visit,
};

use regast_core::Matcher;

const DEFAULT_SIZE_LIMIT: usize = 100_000;

pub struct Regast {
    ast: Pattern,
    matcher: Mutex<Matcher>,
}

impl Regast {
    /// Compile a pattern using POSIX disambiguation.
    ///
    /// # Errors
    ///
    /// Returns [`ParseError`] when the pattern is malformed or unsupported.
    pub fn new(pattern: &str) -> Result<Self, ParseError> {
        Self::builder(pattern).build()
    }

    #[must_use]
    pub fn builder(pattern: &str) -> Builder {
        Builder {
            pattern: pattern.to_owned(),
            disambiguation: Disambiguation::Posix,
            size_limit: DEFAULT_SIZE_LIMIT,
            backend: Backend::Derivative,
        }
    }

    #[must_use]
    pub const fn ast(&self) -> &Pattern {
        &self.ast
    }

    /// Parse an entire input into its complete derivation tree.
    ///
    /// # Errors
    ///
    /// Returns [`MatchError::NoMatch`] when the entire input does not match, and
    /// [`MatchError::SizeLimitExceeded`] on resource exhaustion.
    pub fn parse(&self, input: &str) -> Result<ParseTree, MatchError> {
        self.matcher
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .parse(input)
    }

    /// Find and parse the leftmost-longest matching substring.
    ///
    /// # Errors
    ///
    /// Returns [`MatchError::SizeLimitExceeded`] if the configured matching-state limit is
    /// exceeded.
    pub fn find_parse(&self, input: &str) -> Result<Option<ParseTree>, MatchError> {
        self.matcher
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .find_parse(input)
    }

    #[must_use]
    pub fn is_match(&self, input: &str) -> bool {
        self.matcher
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .is_match(input)
    }

    /// Check an entire input while preserving resource-limit failures.
    ///
    /// # Errors
    ///
    /// Returns [`MatchError::SizeLimitExceeded`] if the configured matching-state limit is
    /// exceeded.
    pub fn try_is_match(&self, input: &str) -> Result<bool, MatchError> {
        self.matcher
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .try_is_match(input)
    }

    #[must_use]
    pub fn trace(&self, input: &str) -> Vec<TraceStep> {
        self.matcher
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .trace(input)
    }
}

#[derive(Clone, Debug)]
pub struct Builder {
    pattern: String,
    disambiguation: Disambiguation,
    size_limit: usize,
    backend: Backend,
}

impl Builder {
    #[must_use]
    pub const fn disambiguation(mut self, disambiguation: Disambiguation) -> Self {
        self.disambiguation = disambiguation;
        self
    }

    #[must_use]
    pub const fn posix(self) -> Self {
        self.disambiguation(Disambiguation::Posix)
    }

    #[must_use]
    pub const fn greedy(self) -> Self {
        self.disambiguation(Disambiguation::Greedy)
    }

    #[must_use]
    pub const fn size_limit(mut self, limit: usize) -> Self {
        self.size_limit = limit;
        self
    }

    #[must_use]
    pub const fn backend(mut self, backend: Backend) -> Self {
        self.backend = backend;
        self
    }

    /// Compile the configured pattern.
    ///
    /// # Errors
    ///
    /// Returns [`ParseError`] when the pattern is malformed or unsupported.
    pub fn build(self) -> Result<Regast, ParseError> {
        let ast = Pattern::parse(&self.pattern)?;
        let matcher = Matcher::new_with_backend(
            ast.clone(),
            self.disambiguation,
            self.size_limit,
            self.backend,
        );
        Ok(Regast {
            ast,
            matcher: Mutex::new(matcher),
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn facade_exposes_ast_match_and_parse_tree() {
        let regex = Regast::new("(?<letter>a|b)+").unwrap();
        assert_eq!(regex.ast().source(), "(?<letter>a|b)+");
        assert!(regex.is_match("aba"));
        let tree = regex.parse("aba").unwrap();
        assert_eq!(tree.group_by_name_all("letter").len(), 3);
    }

    #[test]
    fn compiled_pattern_is_send_and_sync() {
        const fn assert_send_sync<T: Send + Sync>() {}
        assert_send_sync::<Regast>();
    }
}