rexact-syntax 0.2.0

Regex parsers for ECMAScript, Python, Go, Rust, PCRE2 and Java dialects, with an error catalogue
Documentation
//! Syntax tree produced by the dialect parsers.
//!
//! Invariants: spans are half-open ranges of Unicode scalar offsets into the
//! original pattern text. Code points are `u32` (not `char`) because ECMAScript
//! without the Unicode flag can name lone surrogates such as `\uD83D`.

use std::fmt;

/// Half-open range `[start, end)` in scalar-value offsets.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Span {
    /// First offset covered.
    pub start: usize,
    /// One past the last offset covered.
    pub end: usize,
}

/// `\d`, `\w`, `\s` and negations. Their meaning is per dialect and alphabet.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Shorthand {
    /// `\d`
    Digit,
    /// `\D`
    NotDigit,
    /// `\w`
    Word,
    /// `\W`
    NotWord,
    /// `\s`
    Space,
    /// `\S`
    NotSpace,
}

impl Shorthand {
    fn letter(self) -> char {
        match self {
            Shorthand::Digit => 'd',
            Shorthand::NotDigit => 'D',
            Shorthand::Word => 'w',
            Shorthand::NotWord => 'W',
            Shorthand::Space => 's',
            Shorthand::NotSpace => 'S',
        }
    }
}

/// Which Unicode property family a `\p{...}` refers to.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PropKind {
    /// General category (name is the canonical short name, e.g. `Lu`, or a group such as `L`).
    GeneralCategory,
    /// Script (canonical long name).
    Script,
    /// Script extensions (canonical long name).
    ScriptExtensions,
    /// Binary property (canonical long name).
    Binary,
}

/// A resolved `\p{...}` / `\P{...}` reference: the name is already validated
/// against the pinned Unicode data.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PropertyRef {
    /// `\P{...}` or `\p{^...}`.
    pub negated: bool,
    /// Property family.
    pub kind: PropKind,
    /// Canonical name within the family.
    pub name: &'static str,
    /// Derived from a POSIX class: exempt from caseless widening (PCRE2).
    pub exact: bool,
}

/// One member of a character class.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClassItem {
    /// A Unicode property.
    Property(PropertyRef),
    /// Inclusive range of code points (a single character has `lo == hi`).
    Range(u32, u32),
    /// A shorthand such as `\d`.
    Shorthand(Shorthand),
}

/// A bracketed class, or a lone shorthand outside brackets.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Class {
    /// `[^...]`.
    pub negated: bool,
    /// Members, in source order.
    pub items: Vec<ClassItem>,
}

/// Zero-width assertions.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Assertion {
    /// `^`
    LineStart,
    /// `$`
    LineEnd,
    /// `\A`
    TextStart,
    /// `\Z` (Python: end of text only)
    TextEnd,
    /// `\b`
    WordBoundary,
    /// `\B`
    NotWordBoundary,
    /// Rust `\<` or `\b{start}`: a word boundary with a word character next.
    WordStart,
    /// Rust `\>` or `\b{end}`: a word boundary with a word character before.
    WordEnd,
    /// PCRE2 `\Z`: end of text or just before a final line break.
    TextEndOptionalNewline,
}

/// Set operators of Rust character classes.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SetOp {
    /// `&&`
    Intersect,
    /// `--`
    Difference,
    /// `~~`
    SymmetricDifference,
}

/// A Rust bracketed class with nesting and set operations.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ClassExpr {
    /// Plain members.
    Items(Vec<ClassItem>),
    /// Union of operands.
    Union(Vec<ClassExpr>),
    /// `[...]` with optional negation applied after evaluating the inside.
    Bracket {
        /// `[^...]`.
        negated: bool,
        /// Contents.
        inner: Box<ClassExpr>,
    },
    /// `lhs op rhs`, left-associative.
    Op(SetOp, Box<ClassExpr>, Box<ClassExpr>),
}

/// Scoped inline flag changes, `(?i-m:...)`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct FlagDelta {
    /// Letters switched on.
    pub on: crate::flags::Flags,
    /// Letters switched off.
    pub off: crate::flags::Flags,
}

/// Node shape.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NodeKind {
    /// Matches the empty string.
    Empty,
    /// One code point.
    Literal(u32),
    /// `.`
    Any,
    /// Class or shorthand.
    Class(Class),
    /// Class with nesting or set operations (Rust).
    ClassSet(ClassExpr),
    /// Sequence.
    Concat(Vec<Node>),
    /// Alternation, in source order.
    Alt(Vec<Node>),
    /// Repetition. `max == None` is unbounded.
    Repeat {
        /// Repeated node.
        node: Box<Node>,
        /// Minimum count.
        min: u64,
        /// Maximum count.
        max: Option<u64>,
        /// Greedy (true) or lazy (false); same language, different spans.
        greedy: bool,
    },
    /// Group. Capture status does not affect the language but is kept.
    Group {
        /// Capture index when capturing.
        capture: Option<u32>,
        /// Group name when named.
        name: Option<String>,
        /// Body.
        node: Box<Node>,
    },
    /// Group with scoped flag changes.
    FlagGroup {
        /// The changes.
        delta: FlagDelta,
        /// Body.
        node: Box<Node>,
    },
    /// Zero-width assertion.
    Assert(Assertion),
    /// Lookahead or lookbehind.
    Look {
        /// Lookahead (true) or lookbehind (false).
        ahead: bool,
        /// Negative form.
        negate: bool,
        /// Body.
        node: Box<Node>,
    },
}

/// A node with its source span.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Node {
    /// Shape.
    pub kind: NodeKind,
    /// Source range.
    pub span: Span,
}

impl fmt::Display for Node {
    /// Compact S-expression form, used by tests and `explain`.
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.kind {
            NodeKind::Empty => write!(f, "eps"),
            NodeKind::Literal(c) => match char::from_u32(*c) {
                Some(ch) if ch.is_ascii_graphic() => write!(f, "{ch}"),
                _ => write!(f, "U+{c:04X}"),
            },
            NodeKind::Any => write!(f, "any"),
            NodeKind::Class(c) => {
                write!(f, "[{}", if c.negated { "^" } else { "" })?;
                for (i, it) in c.items.iter().enumerate() {
                    if i > 0 {
                        write!(f, " ")?;
                    }
                    match it {
                        ClassItem::Range(a, b) if a == b => write!(f, "{a:X}")?,
                        ClassItem::Range(a, b) => write!(f, "{a:X}-{b:X}")?,
                        ClassItem::Shorthand(s) => write!(f, "\\{}", s.letter())?,
                        ClassItem::Property(p) => write!(
                            f,
                            "\\{}{{{:?}:{}}}",
                            if p.negated { "P" } else { "p" },
                            p.kind,
                            p.name
                        )?,
                    }
                }
                write!(f, "]")
            }
            NodeKind::ClassSet(e) => write!(f, "[set {e:?}]"),
            NodeKind::Concat(v) | NodeKind::Alt(v) => {
                let tag = if matches!(self.kind, NodeKind::Concat(_)) {
                    "cat"
                } else {
                    "alt"
                };
                write!(f, "({tag}")?;
                for n in v {
                    write!(f, " {n}")?;
                }
                write!(f, ")")
            }
            NodeKind::Repeat {
                node,
                min,
                max,
                greedy,
            } => {
                let max = max.map_or("inf".to_string(), |m| m.to_string());
                write!(
                    f,
                    "(rep{} {min} {max} {node})",
                    if *greedy { "" } else { "?" }
                )
            }
            NodeKind::Group {
                capture,
                name,
                node,
            } => match (capture, name) {
                (Some(i), Some(n)) => write!(f, "(group#{i}:{n} {node})"),
                (Some(i), None) => write!(f, "(group#{i} {node})"),
                _ => write!(f, "(ncgroup {node})"),
            },
            NodeKind::FlagGroup { node, .. } => write!(f, "(flags {node})"),
            NodeKind::Assert(a) => write!(f, "{a:?}"),
            NodeKind::Look {
                ahead,
                negate,
                node,
            } => {
                let d = if *ahead { "ahead" } else { "behind" };
                write!(f, "(look{}{} {node})", d, if *negate { "!" } else { "" })
            }
        }
    }
}