ecma-syntax-cat 0.1.0

ECMAScript abstract syntax tree as comp-cat-rs-idiomatic Rust types. ESTree-shaped, ES2024-complete, no panics, no Rc, no interior mutability. Foundation crate for boa-cat and related downstream tooling.
Documentation
//! Static ECMAScript literals.
//!
//! Template literals are *not* part of this enum because they contain
//! embedded expressions; see [`crate::expression::ExpressionKind::Template`].

use crate::error::Error;

/// A static literal value.
#[derive(Debug, Clone, PartialEq)]
pub enum Literal {
    /// A numeric literal.  Per ECMA-262 these are double-precision floats;
    /// integer literals widen losslessly while in their integer range.
    Number(f64),
    /// A `BigInt` literal stored as its decimal text (without the trailing
    /// `n` suffix).  Downstream tools can parse this into a real arbitrary-
    /// precision integer if needed; the AST stays dependency-free.
    BigInt(String),
    /// A string literal, with escapes already resolved by the lexer.
    String(String),
    /// `true` or `false`.
    Boolean(bool),
    /// `null`.
    Null,
    /// A regular expression literal: pattern between the slashes and any
    /// flag characters that follow.
    RegExp {
        /// The pattern between the slashes (no surrounding slashes).
        pattern: String,
        /// Concatenated flag characters (e.g. `"gi"`).
        flags: String,
    },
}

/// Eq impl for [`Literal`] follows IEEE-754 equality for `Number`, which
/// makes `NaN != NaN`.  This matches ECMA-262 strict-equality semantics.
impl Eq for Literal {}

impl Literal {
    /// Build a numeric literal.
    #[must_use]
    pub fn number(value: f64) -> Self {
        Self::Number(value)
    }

    /// Build a `BigInt` literal from a decimal string.  No validation is
    /// performed; the lexer is responsible for ensuring only digit
    /// characters reach this constructor.
    #[must_use]
    pub fn bigint(digits: impl Into<String>) -> Self {
        Self::BigInt(digits.into())
    }

    /// Build a string literal with the given (already-unescaped) content.
    #[must_use]
    pub fn string(content: impl Into<String>) -> Self {
        Self::String(content.into())
    }

    /// Build a boolean literal.
    #[must_use]
    pub fn boolean(value: bool) -> Self {
        Self::Boolean(value)
    }

    /// Build a `null` literal.
    #[must_use]
    pub fn null() -> Self {
        Self::Null
    }

    /// Build a regex literal.
    ///
    /// # Errors
    ///
    /// Returns [`Error::EmptyRegexPattern`] when the pattern is empty, or
    /// [`Error::InvalidRegexFlag`] when a flag character is not one of the
    /// ECMA-262 flags `g i m s u y d v`.
    pub fn regex(pattern: impl Into<String>, flags: impl Into<String>) -> Result<Self, Error> {
        let pattern = pattern.into();
        let flags = flags.into();
        if pattern.is_empty() {
            Err(Error::EmptyRegexPattern)
        } else {
            flags
                .chars()
                .find(|c| !is_valid_regex_flag(*c))
                .map_or(Ok(Self::RegExp { pattern, flags }), |flag| {
                    Err(Error::InvalidRegexFlag { flag })
                })
        }
    }
}

impl std::fmt::Display for Literal {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Number(value) => write!(f, "{value}"),
            Self::BigInt(digits) => write!(f, "{digits}n"),
            Self::String(content) => write!(f, "{content:?}"),
            Self::Boolean(value) => write!(f, "{value}"),
            Self::Null => f.write_str("null"),
            Self::RegExp { pattern, flags } => write!(f, "/{pattern}/{flags}"),
        }
    }
}

fn is_valid_regex_flag(flag: char) -> bool {
    matches!(flag, 'g' | 'i' | 'm' | 's' | 'u' | 'y' | 'd' | 'v')
}