pub mod recognize;
pub mod state;
pub use recognize::{RecognitionResult, Recognizer};
pub use state::{EarleyItem, StateSet};
use crate::grammar::{
Grammar,
compile::{CompiledGrammar, compile},
validate::{ValidationReport, validate},
};
use crate::lexer::Token;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EngineError {
InvalidGrammar(ValidationReport),
NotAccepted {
last_consumed: usize,
},
}
#[derive(Debug, Clone)]
pub struct Engine<'g> {
grammar: &'g Grammar,
validation: ValidationReport,
compiled: CompiledGrammar,
}
impl<'g> Engine<'g> {
#[must_use]
pub fn new(grammar: &'g Grammar) -> Self {
let validation = validate(grammar);
let compiled = compile(grammar);
Self {
grammar,
validation,
compiled,
}
}
#[must_use]
pub fn compiled_grammar(&self) -> &CompiledGrammar {
&self.compiled
}
#[must_use]
pub fn validation_report(&self) -> &ValidationReport {
&self.validation
}
#[must_use]
pub fn grammar(&self) -> &'g Grammar {
self.grammar
}
pub fn recognize(&self, tokens: &[Token<'_>]) -> Result<RecognitionResult, EngineError> {
if self.validation.has_errors() {
return Err(EngineError::InvalidGrammar(self.validation.clone()));
}
let result = Recognizer::new(&self.compiled).recognize(tokens);
if result.accepted {
Ok(result)
} else {
Err(EngineError::NotAccepted {
last_consumed: tokens.len(),
})
}
}
}
pub fn parse(grammar: &Grammar, tokens: &[Token<'_>]) -> Result<RecognitionResult, EngineError> {
Engine::new(grammar).recognize(tokens)
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic)]
use super::*;
use crate::grammar::{
Alternative, IdlVersion, Production, ProductionId, SpecRef, Symbol, TokenKind,
};
fn t(kind: TokenKind) -> Token<'static> {
Token::synthetic(kind)
}
const TS: SpecRef = SpecRef {
doc: "TEST",
section: "0.0",
};
const G_OK: Grammar = Grammar {
name: "ok",
version: IdlVersion::V4_2,
productions: &[Production {
id: ProductionId(0),
name: "a",
spec_ref: TS,
alternatives: &[Alternative {
name: None,
symbols: &[Symbol::Terminal(TokenKind::Keyword("x"))],
note: None,
}],
ast_hint: None,
}],
start: ProductionId(0),
token_rules: &[],
};
const G_INVALID_START: Grammar = Grammar {
name: "invalid_start",
version: IdlVersion::V4_2,
productions: &[],
start: ProductionId(99),
token_rules: &[],
};
const G_DANGLING: Grammar = Grammar {
name: "dangling",
version: IdlVersion::V4_2,
productions: &[Production {
id: ProductionId(0),
name: "a",
spec_ref: TS,
alternatives: &[Alternative {
name: None,
symbols: &[Symbol::Nonterminal(ProductionId(99))],
note: None,
}],
ast_hint: None,
}],
start: ProductionId(0),
token_rules: &[],
};
#[test]
fn engine_new_runs_validation_eagerly() {
let engine = Engine::new(&G_OK);
assert!(engine.validation_report().is_empty());
}
#[test]
fn engine_new_captures_errors_on_broken_grammar() {
let engine = Engine::new(&G_INVALID_START);
assert!(engine.validation_report().has_errors());
}
#[test]
fn engine_grammar_accessor_returns_same_reference() {
let engine = Engine::new(&G_OK);
assert!(std::ptr::eq(engine.grammar(), &G_OK));
}
#[test]
fn engine_recognize_succeeds_on_valid_grammar_and_input() {
let engine = Engine::new(&G_OK);
let result = engine.recognize(&[t(TokenKind::Keyword("x"))]);
assert!(matches!(result, Ok(r) if r.accepted));
}
#[test]
fn engine_recognize_returns_invalid_grammar_for_invalid_start() {
let engine = Engine::new(&G_INVALID_START);
let result = engine.recognize(&[]);
assert!(matches!(result, Err(EngineError::InvalidGrammar(_))));
}
#[test]
fn engine_recognize_returns_invalid_grammar_for_dangling_reference() {
let engine = Engine::new(&G_DANGLING);
let result = engine.recognize(&[]);
assert!(matches!(result, Err(EngineError::InvalidGrammar(_))));
}
#[test]
fn engine_recognize_returns_not_accepted_for_wrong_input() {
let engine = Engine::new(&G_OK);
let result = engine.recognize(&[t(TokenKind::Keyword("y"))]);
assert!(matches!(
result,
Err(EngineError::NotAccepted { last_consumed: 1 })
));
}
#[test]
fn engine_recognize_returns_not_accepted_for_empty_input_when_grammar_requires_terminal() {
let engine = Engine::new(&G_OK);
let result = engine.recognize(&[]);
assert!(matches!(
result,
Err(EngineError::NotAccepted { last_consumed: 0 })
));
}
#[test]
fn parse_convenience_function_succeeds_on_valid_input() {
let result = parse(&G_OK, &[t(TokenKind::Keyword("x"))]);
assert!(matches!(result, Ok(r) if r.accepted));
}
#[test]
fn parse_convenience_function_propagates_invalid_grammar_error() {
let result = parse(&G_INVALID_START, &[]);
assert!(matches!(result, Err(EngineError::InvalidGrammar(_))));
}
#[test]
fn engine_validation_report_persists_across_recognize_calls() {
let engine = Engine::new(&G_OK);
let _first = engine.recognize(&[t(TokenKind::Keyword("x"))]);
let _second = engine.recognize(&[t(TokenKind::Keyword("x"))]);
assert!(engine.validation_report().is_empty());
}
}