lib.rs raw

use ::neotoma::{
    Source,
    cache::BasicCache,
    grammar::{Grammar, GrammarParser, GrammarResult},
    parse,
    parser::Parser,
};
use pyo3::prelude::*;
use pyo3::types::{PyList, PyTuple};
use std::io::Cursor;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum PyNeotomaError {
    #[error("Parse error: {0}")]
    ParseError(String),
    #[error("IO error: {0}")]
    IoError(String),
}

impl From<PyNeotomaError> for PyErr {
    fn from(err: PyNeotomaError) -> PyErr {
        match err {
            PyNeotomaError::ParseError(msg) => pyo3::exceptions::PyValueError::new_err(msg),
            PyNeotomaError::IoError(msg) => pyo3::exceptions::PyIOError::new_err(msg),
        }
    }
}

/// Flatten nested sequences into a single tuple
fn flatten_sequence(py: Python, results: &[GrammarResult]) -> PyResult<Vec<PyObject>> {
    let mut flattened = Vec::new();
    for item in results {
        match item {
            GrammarResult::Sequence(nested_results) => {
                // Recursively flatten nested sequences
                flattened.extend(flatten_sequence(py, nested_results)?);
            }
            _ => {
                // Convert non-sequence items normally
                flattened.push(grammar_result_to_python(py, item)?);
            }
        }
    }
    Ok(flattened)
}

/// Flatten nested repetitions into a single list
fn flatten_repetition(py: Python, results: &[GrammarResult]) -> PyResult<Vec<PyObject>> {
    let mut flattened = Vec::new();
    for item in results {
        match item {
            GrammarResult::Repetition(nested_results) => {
                // Recursively flatten nested repetitions
                flattened.extend(flatten_repetition(py, nested_results)?);
            }
            _ => {
                // Convert non-repetition items normally
                flattened.push(grammar_result_to_python(py, item)?);
            }
        }
    }
    Ok(flattened)
}

/// Convert GrammarResult to Python objects
fn grammar_result_to_python(py: Python, result: &GrammarResult) -> PyResult<PyObject> {
    match result {
        GrammarResult::Literal(bytes) => {
            // Return literal as Python bytes
            Ok(pyo3::types::PyBytes::new(py, bytes).into_any().unbind())
        }
        GrammarResult::Unicode(string) => {
            // Return unicode string directly
            Ok(pyo3::types::PyString::new(py, string).into_any().unbind())
        }
        GrammarResult::Bytes(bytes) => {
            // Return as Python bytes
            Ok(pyo3::types::PyBytes::new(py, bytes).into_any().unbind())
        }
        GrammarResult::Sequence(results) => {
            // Return flattened sequence as Python tuple
            let py_items = flatten_sequence(py, results)?;
            Ok(PyTuple::new(py, py_items)?.into_any().unbind())
        }
        GrammarResult::Alternative(result) => {
            // Return the alternative result directly (unwrap the box)
            grammar_result_to_python(py, result)
        }
        GrammarResult::Repetition(results) => {
            // Return flattened repetition as Python list
            let py_items = flatten_repetition(py, results)?;
            Ok(PyList::new(py, py_items)?.into_any().unbind())
        }
        GrammarResult::Optional(Some(result)) => {
            // Return the optional result directly (unwrap the box)
            grammar_result_to_python(py, result)
        }
        GrammarResult::Optional(None) | GrammarResult::Empty => {
            // Return None for empty/missing results
            Ok(py.None())
        }
    }
}

/// A compiled grammar that can parse input text according to defined rules.
///
/// Grammar objects are created from grammar text using Grammar()
/// Once created, they can be used repeatedly to parse different input texts.
#[pyclass(name = "Grammar")]
pub struct PyGrammar {
    grammar: Grammar,
}

#[pymethods]
impl PyGrammar {
    /// Create a new grammar from grammar text.
    ///
    /// Args:
    ///     grammar_text: A string containing the grammar definition
    ///
    /// Returns:
    ///     A compiled Grammar object
    ///
    /// Raises:
    ///     ValueError: If the grammar text contains syntax errors
    ///
    /// Example:
    ///     >>> grammar = Grammar('digits')
    ///     >>> grammar = Grammar('(+ alpha / whitespace)')
    #[new]
    fn new(grammar_text: &str) -> PyResult<Self> {
        let parser = GrammarParser::new();
        let mut source = Source::new(Cursor::new(grammar_text.as_bytes()));

        let grammar = parse(parser, &mut source)
            .map_err(|e| PyNeotomaError::ParseError(format!("{:?}", e)))?;

        Ok(Self { grammar })
    }

    /// Parse input text using this grammar.
    ///
    /// Args:
    ///     input_text: The text to parse
    ///
    /// Returns:
    ///     A structured representation of the parsing result
    ///
    /// Raises:
    ///     ValueError: If the input doesn't match the grammar
    ///
    /// Example:
    ///     >>> grammar = Grammar('digits')
    ///     >>> result = grammar.parse("12345")
    fn parse(&self, py: Python, input_text: &str) -> PyResult<PyObject> {
        let mut source = Source::new(Cursor::new(input_text.as_bytes()));
        let mut cache = BasicCache::new();
        let mut context = ();

        let result = self
            .grammar
            .parse(&mut source, &mut cache, &mut context)
            .map_err(|e| PyNeotomaError::ParseError(format!("{:?}", e)))?;

        grammar_result_to_python(py, &result)
    }
}

/// Parse input with a grammar in one step (convenience function).
///
/// This creates a grammar and immediately uses it to parse input.
/// Use this for one-off parsing; for repeated parsing, create a Grammar object.
///
/// Args:
///     grammar_text: A string containing the grammar definition
///     input_text: The text to parse
///
/// Returns:
///     A structured representation of the parsing result
///
/// Raises:
///     ValueError: If the grammar text contains syntax errors or input doesn't match
///
/// Example:
///     >>> result = parse_with_grammar('digits', "12345")
#[pyfunction]
fn parse_with_grammar(py: Python, grammar_text: &str, input_text: &str) -> PyResult<PyObject> {
    let grammar = PyGrammar::new(grammar_text)?;
    grammar.parse(py, input_text)
}

/// neotoma: Python bindings for the Neotoma parsing library
///
/// This module provides Python access to Neotoma's powerful grammar-based parsing capabilities.
///
/// ## Quick Start
///
/// ```python
/// import neotoma
///
/// # Parse simple patterns
/// result = neotoma.parse_with_grammar('digits', "12345")
///
/// # Create reusable grammars
/// grammar = neotoma.Grammar('(+ alpha / whitespace)')
/// result = grammar.parse("hello world")
///
/// # Ensure complete input consumption with eof
/// strict_grammar = neotoma.Grammar('(alpha eof)')
/// strict_grammar.parse("hello")     # succeeds
/// strict_grammar.parse("hello123")  # fails - has trailing content
/// ```
///
/// ## Grammar Syntax Summary
///
/// - Literals: `"text"`
/// - Built-ins: `digits`, `alpha`, `alphanumeric`, `whitespace`, `hexdigits`, `eof`
/// - Unicode: `udigits`, `ualpha`, `ualphanumeric`, `uwhitespace`
/// - Character classes: `[abc]`, `[^abc]`
/// - Sequences: `(A B C)`
/// - Alternatives: `(| A B C)`
/// - Repetition: `(* A)`, `(+ A)`, `(? A)`
/// - Separated: `(+ A / B)`, `(* A / B)`
/// - Special: `(< A)`, `(< A B)`
/// - Named rules: `name = rule`, `name`, `@start name`
///
/// ## Built-in Parsers
///
/// - `digits`: Matches one or more decimal digits (0-9)
/// - `alpha`: Matches one or more alphabetic characters (ASCII a-z, A-Z)
/// - `alphanumeric`: Matches one or more alphanumeric characters (ASCII)
/// - `whitespace`: Matches one or more whitespace characters (ASCII)
/// - `hexdigits`: Matches one or more hexadecimal digits (0-9, a-f, A-F)
/// - `eof`: Matches end of file - ensures complete input consumption
/// - `udigits`, `ualpha`, `ualphanumeric`, `uwhitespace`: Unicode versions
///
/// ## End-of-File (`eof`) Usage
///
/// The `eof` parser is essential for creating strict parsers that require
/// complete input consumption. Without `eof`, parsers may succeed on partial
/// matches, leaving trailing content unprocessed.
///
/// ```python
/// # Without eof - partial matches succeed
/// loose_grammar = neotoma.Grammar('alpha')
/// loose_grammar.parse("hello123")  # succeeds, matches "hello", ignores "123"
///
/// # With eof - requires complete consumption
/// strict_grammar = neotoma.Grammar('(alpha eof)')
/// strict_grammar.parse("hello")     # succeeds
/// strict_grammar.parse("hello123")  # fails - "123" remains after "hello"
///
/// # Common pattern for complete expressions
/// expr_grammar = neotoma.Grammar('@start complete_expr\ncomplete_expr = (expression eof)')
/// ```
///
/// See the README for complete documentation.
#[pymodule]
fn neotoma(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_class::<PyGrammar>()?;
    m.add_function(wrap_pyfunction!(parse_with_grammar, m)?)?;
    Ok(())
}