use std::collections::HashMap;
use std::marker::PhantomData;
#[cfg(feature = "serialization")]
use serde::{Deserialize, Serialize};
use super::ast::{ContextAST, ContextExpr, Expression, LLevFile, RuleDefinition, SymbolDef};
use super::error::{LLevError, LLevErrorKind, LLevResult, Position};
use crate::phonetic::common::PhoneticUnit;
use crate::phonetic::types::{Context, Phone, RewriteRule};
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[cfg_attr(
feature = "serialization",
serde(bound = "U: Serialize + for<'a> Deserialize<'a>")
)]
pub struct RuleSetGeneric<U: PhoneticUnit> {
pub rules: Vec<RewriteRule<U>>,
pub name: Option<String>,
pub version: Option<String>,
}
impl<U: PhoneticUnit> RuleSetGeneric<U> {
pub fn new() -> Self {
Self {
rules: Vec::new(),
name: None,
version: None,
}
}
pub fn len(&self) -> usize {
self.rules.len()
}
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
}
pub fn merge(&mut self, other: RuleSetGeneric<U>) {
self.rules.extend(other.rules);
}
}
impl<U: PhoneticUnit> Default for RuleSetGeneric<U> {
fn default() -> Self {
Self::new()
}
}
pub type RuleSet = RuleSetGeneric<u8>;
impl RuleSet {
pub fn from_llev(file: &LLevFile) -> LLevResult<Self> {
let converter = RuleConverter::<u8>::new(&file.symbols);
let mut rules = Vec::with_capacity(file.rules.len());
for (index, rule_def) in file.rules.iter().enumerate() {
if !rule_def.metadata.enabled {
continue;
}
let rule = converter.convert_rule(rule_def, index)?;
rules.push(rule);
}
Ok(Self {
rules,
name: file.metadata.name.clone(),
version: file.metadata.version.clone(),
})
}
}
pub type RuleSetChar = RuleSetGeneric<char>;
impl RuleSetChar {
pub fn from_llev(file: &LLevFile) -> LLevResult<Self> {
let converter = RuleConverter::<char>::new(&file.symbols);
let mut rules = Vec::with_capacity(file.rules.len());
for (index, rule_def) in file.rules.iter().enumerate() {
if !rule_def.metadata.enabled {
continue;
}
let rule = converter.convert_rule(rule_def, index)?;
rules.push(rule);
}
Ok(Self {
rules,
name: file.metadata.name.clone(),
version: file.metadata.version.clone(),
})
}
pub fn apply(&self, input: &str) -> String {
let phones: Vec<Phone<char>> = input
.chars()
.map(|c| {
let c_lower = c.to_lowercase().next().unwrap_or(c);
if is_vowel_char(c_lower) {
Phone::Vowel(c_lower)
} else {
Phone::Consonant(c_lower)
}
})
.collect();
let fuel = input.len() * 10 + 100;
if let Some(result) = crate::phonetic::apply_rules_seq(&self.rules, &phones, fuel) {
result
.iter()
.filter_map(|p| match p {
Phone::Vowel(c) | Phone::Consonant(c) => Some(*c),
Phone::Digraph(_c1, _c2) => {
None }
Phone::Trigraph(_c1, _c2, _c3) => {
None
}
Phone::Tetragraph(_c1, _c2, _c3, _c4) => {
None
}
Phone::Pentagraph(_c1, _c2, _c3, _c4, _c5) => {
None
}
Phone::Hexagraph(_c1, _c2, _c3, _c4, _c5, _c6) => {
None
}
Phone::Heptagraph(_c1, _c2, _c3, _c4, _c5, _c6, _c7) => {
None
}
Phone::Sequence(_) => {
None
}
Phone::Silent => None,
})
.collect()
} else {
input.to_string()
}
}
pub fn apply_full(&self, input: &str) -> String {
let phones: Vec<Phone<char>> = input
.chars()
.map(|c| {
let c_lower = c.to_lowercase().next().unwrap_or(c);
if is_vowel_char(c_lower) {
Phone::Vowel(c_lower)
} else {
Phone::Consonant(c_lower)
}
})
.collect();
let fuel = input.len() * 10 + 100;
if let Some(result) = crate::phonetic::apply_rules_seq(&self.rules, &phones, fuel) {
let mut output = String::with_capacity(result.len() * 4);
for p in result.iter() {
match p {
Phone::Vowel(c) | Phone::Consonant(c) => output.push(*c),
Phone::Digraph(c1, c2) => {
output.push(*c1);
output.push(*c2);
}
Phone::Trigraph(c1, c2, c3) => {
output.push(*c1);
output.push(*c2);
output.push(*c3);
}
Phone::Tetragraph(c1, c2, c3, c4) => {
output.push(*c1);
output.push(*c2);
output.push(*c3);
output.push(*c4);
}
Phone::Pentagraph(c1, c2, c3, c4, c5) => {
output.push(*c1);
output.push(*c2);
output.push(*c3);
output.push(*c4);
output.push(*c5);
}
Phone::Hexagraph(c1, c2, c3, c4, c5, c6) => {
output.push(*c1);
output.push(*c2);
output.push(*c3);
output.push(*c4);
output.push(*c5);
output.push(*c6);
}
Phone::Heptagraph(c1, c2, c3, c4, c5, c6, c7) => {
output.push(*c1);
output.push(*c2);
output.push(*c3);
output.push(*c4);
output.push(*c5);
output.push(*c6);
output.push(*c7);
}
Phone::Sequence(s) => {
for c in s {
output.push(*c);
}
}
Phone::Silent => {}
}
}
output
} else {
input.to_string()
}
}
}
struct RuleConverter<'a, U: PhoneticUnit> {
symbols: HashMap<&'a str, &'a Expression>,
symbol_names: Vec<&'a str>,
_phantom: PhantomData<U>,
}
impl<'a, U: PhoneticUnit> RuleConverter<'a, U> {
fn new(symbols: &'a [SymbolDef]) -> Self {
let symbol_map: HashMap<_, _> = symbols
.iter()
.map(|s| (s.name.as_str(), &s.value))
.collect();
let symbol_names: Vec<_> = symbols.iter().map(|s| s.name.as_str()).collect();
Self {
symbols: symbol_map,
symbol_names,
_phantom: PhantomData,
}
}
fn convert_rule(&self, rule_def: &RuleDefinition, index: usize) -> LLevResult<RewriteRule<U>> {
let pos = rule_def.position;
let pattern = self.convert_pattern(&rule_def.rule.pattern, pos, true)?;
if pattern.is_empty() {
return Err(LLevError::with_position(
LLevErrorKind::InvalidRule("Pattern cannot be empty".into()),
pos,
));
}
let replacement = self.convert_pattern(&rule_def.rule.replacement, pos, false)?;
let context = self.convert_context(&rule_def.rule.context, pos)?;
let syllable_condition = rule_def
.rule
.context
.as_ref()
.and_then(|ctx| ctx.syllable.clone());
let weight = rule_def
.rule
.weight
.or(rule_def.metadata.weight)
.unwrap_or(1.0);
let rule_id = rule_def.metadata.id.unwrap_or(index);
let rule_name = rule_def
.metadata
.name
.clone()
.unwrap_or_else(|| format!("rule_{}", rule_id));
Ok(RewriteRule {
rule_id,
rule_name,
pattern,
replacement,
context,
weight,
syllable_condition,
})
}
fn convert_pattern(
&self,
expr: &Expression,
pos: Position,
case_fold: bool,
) -> LLevResult<Vec<Phone<U>>> {
match expr {
Expression::Empty => Ok(Vec::new()),
Expression::Char(c) => {
let phone = self.char_to_phone(*c, pos, case_fold)?;
Ok(vec![phone])
}
Expression::Concat(a, b) => {
let mut result = self.convert_pattern(a, pos, case_fold)?;
result.extend(self.convert_pattern(b, pos, case_fold)?);
Ok(result)
}
Expression::Group(inner) => self.convert_pattern(inner, pos, case_fold),
Expression::ScopedFlags { flags, inner } => {
let new_case_fold = match flags.case_insensitive {
Some(false) => false, Some(true) => true, None => case_fold, };
self.convert_pattern(inner, pos, new_case_fold)
}
Expression::SymbolRef(name) => {
if let Some(symbol_expr) = self.symbols.get(name.as_str()) {
self.convert_pattern(symbol_expr, pos, case_fold)
} else {
Err(LLevError::undefined_symbol_with_suggestion(
name.clone(),
&self.symbol_names,
pos,
))
}
}
Expression::CharClass { .. } => Err(LLevError::with_position(
LLevErrorKind::UnsupportedPattern(
"Character classes in patterns require NFA-based matching".into(),
),
pos,
)),
Expression::CharRange { .. } => Err(LLevError::with_position(
LLevErrorKind::UnsupportedPattern(
"Character ranges in patterns require NFA-based matching".into(),
),
pos,
)),
Expression::Any => Err(LLevError::with_position(
LLevErrorKind::UnsupportedPattern(
"Wildcard (.) in patterns requires NFA-based matching".into(),
),
pos,
)),
Expression::Alt(_, _) => Err(LLevError::with_position(
LLevErrorKind::UnsupportedPattern(
"Alternation (|) in patterns requires NFA-based matching".into(),
),
pos,
)),
Expression::Star(_) => Err(LLevError::with_position(
LLevErrorKind::UnsupportedPattern(
"Kleene star (*) in patterns requires NFA-based matching".into(),
),
pos,
)),
Expression::Plus(_) => Err(LLevError::with_position(
LLevErrorKind::UnsupportedPattern(
"Kleene plus (+) in patterns requires NFA-based matching".into(),
),
pos,
)),
Expression::Optional(_) => Err(LLevError::with_position(
LLevErrorKind::UnsupportedPattern(
"Optional (?) in patterns requires NFA-based matching".into(),
),
pos,
)),
Expression::RepeatExact(_, _) => Err(LLevError::with_position(
LLevErrorKind::UnsupportedPattern(
"Repetition {n} in patterns requires NFA-based matching".into(),
),
pos,
)),
Expression::RepeatRange { .. } => Err(LLevError::with_position(
LLevErrorKind::UnsupportedPattern(
"Repetition {n,m} in patterns requires NFA-based matching".into(),
),
pos,
)),
Expression::WordBoundary => Err(LLevError::with_position(
LLevErrorKind::UnsupportedPattern(
"Word boundary (#) is only valid in contexts, not patterns".into(),
),
pos,
)),
}
}
fn char_to_phone(&self, c: char, pos: Position, case_fold: bool) -> LLevResult<Phone<U>> {
let c_folded = if case_fold {
U::to_lowercase(
U::from_char(c).ok_or_else(|| LLevError::non_ascii_in_byte_level(c, None, pos))?,
)
} else {
U::from_char(c).ok_or_else(|| LLevError::non_ascii_in_byte_level(c, None, pos))?
};
if U::is_vowel(c_folded) {
Ok(Phone::Vowel(c_folded))
} else {
Ok(Phone::Consonant(c_folded))
}
}
fn convert_context(
&self,
ctx_opt: &Option<ContextAST>,
pos: Position,
) -> LLevResult<Context<U>> {
let ctx = match ctx_opt {
None => return Ok(Context::Anywhere),
Some(ctx) => ctx,
};
let left_ctx = match &ctx.left {
None => None,
Some(expr) => Some(self.convert_context_expr(expr, pos, true)?),
};
let right_ctx = match &ctx.right {
None => None,
Some(expr) => Some(self.convert_context_expr(expr, pos, false)?),
};
match (left_ctx, right_ctx) {
(None, None) => Ok(Context::Anywhere),
(Some(left), None) => Ok(left),
(None, Some(right)) => Ok(right),
(Some(left), Some(right)) => {
Ok(Context::And(Box::new(left), Box::new(right)))
}
}
}
fn convert_context_expr(
&self,
ctx_expr: &ContextExpr,
pos: Position,
is_left_context: bool,
) -> LLevResult<Context<U>> {
match ctx_expr {
ContextExpr::WordBoundary => {
if is_left_context {
Ok(Context::Initial)
} else {
Ok(Context::Final)
}
}
ContextExpr::Pattern(expr) => {
if let Some(chars) = self.extract_char_class_from_expr(expr, pos)? {
if is_left_context {
if chars.iter().all(|&c| U::is_vowel(c)) {
Ok(Context::AfterVowel(chars))
} else if chars.iter().all(|&c| !U::is_vowel(c)) {
Ok(Context::AfterConsonant(chars))
} else {
Ok(Context::AfterVowel(chars))
}
} else {
if chars.iter().all(|&c| U::is_vowel(c)) {
Ok(Context::BeforeVowel(chars))
} else if chars.iter().all(|&c| !U::is_vowel(c)) {
Ok(Context::BeforeConsonant(chars))
} else {
Ok(Context::BeforeVowel(chars))
}
}
} else {
Err(LLevError::with_position(
LLevErrorKind::UnsupportedPattern(
"Complex pattern in context requires NFA-based matching".into(),
),
pos,
))
}
}
ContextExpr::And(a, b) => {
let left = self.convert_context_expr(a, pos, is_left_context)?;
let right = self.convert_context_expr(b, pos, is_left_context)?;
Ok(Context::And(Box::new(left), Box::new(right)))
}
ContextExpr::Or(a, b) => {
let left = self.convert_context_expr(a, pos, is_left_context)?;
let right = self.convert_context_expr(b, pos, is_left_context)?;
Ok(Context::Or(Box::new(left), Box::new(right)))
}
ContextExpr::Not(inner) => {
let converted = self.convert_context_expr(inner, pos, is_left_context)?;
Ok(Context::Not(Box::new(converted)))
}
}
}
fn extract_char_class_from_expr(
&self,
expr: &Expression,
pos: Position,
) -> LLevResult<Option<Vec<U>>> {
match expr {
Expression::CharClass { chars, negated } => {
if *negated {
return Ok(None); }
let mut units = Vec::with_capacity(chars.len());
for &c in chars {
let unit = U::from_char(c)
.ok_or_else(|| LLevError::non_ascii_in_byte_level(c, None, pos))?;
units.push(U::to_lowercase(unit));
}
Ok(Some(units))
}
Expression::CharRange { start, end } => {
let mut units = Vec::new();
for c in *start..=*end {
let unit = U::from_char(c)
.ok_or_else(|| LLevError::non_ascii_in_byte_level(c, None, pos))?;
units.push(U::to_lowercase(unit));
}
Ok(Some(units))
}
Expression::SymbolRef(name) => {
if let Some(symbol_expr) = self.symbols.get(name.as_str()) {
self.extract_char_class_from_expr(symbol_expr, pos)
} else {
Err(LLevError::undefined_symbol_with_suggestion(
name.clone(),
&self.symbol_names,
pos,
))
}
}
_ => Ok(None),
}
}
}
#[inline]
fn is_vowel_char(c: char) -> bool {
matches!(c, 'a' | 'e' | 'i' | 'o' | 'u' | 'A' | 'E' | 'I' | 'O' | 'U')
}
#[cfg(test)]
mod tests {
use super::*;
use crate::phonetic::llev::parser::parse_str;
#[test]
fn test_ruleset_empty() {
let ruleset = RuleSet::new();
assert!(ruleset.is_empty());
assert_eq!(ruleset.len(), 0);
}
#[test]
fn test_ruleset_char_empty() {
let ruleset = RuleSetChar::new();
assert!(ruleset.is_empty());
assert_eq!(ruleset.len(), 0);
}
#[test]
fn test_simple_rule_conversion() {
let file = parse_str("ph -> f;").expect("parse failed");
let ruleset = RuleSetChar::from_llev(&file).expect("conversion failed");
assert_eq!(ruleset.len(), 1);
let rule = &ruleset.rules[0];
assert_eq!(rule.pattern.len(), 2);
assert_eq!(rule.replacement.len(), 1);
assert_eq!(rule.context, Context::Anywhere);
}
#[test]
fn test_deletion_rule_conversion() {
let file = parse_str("gh -> ;").expect("parse failed");
let ruleset = RuleSetChar::from_llev(&file).expect("conversion failed");
assert_eq!(ruleset.len(), 1);
let rule = &ruleset.rules[0];
assert_eq!(rule.pattern.len(), 2);
assert!(rule.replacement.is_empty());
}
#[test]
fn test_context_final_conversion() {
let file = parse_str("e -> / _#;").expect("parse failed");
let ruleset = RuleSetChar::from_llev(&file).expect("conversion failed");
assert_eq!(ruleset.len(), 1);
let rule = &ruleset.rules[0];
assert_eq!(rule.context, Context::Final);
}
#[test]
fn test_context_initial_conversion() {
let file = parse_str("k -> c / #_;").expect("parse failed");
let ruleset = RuleSetChar::from_llev(&file).expect("conversion failed");
assert_eq!(ruleset.len(), 1);
let rule = &ruleset.rules[0];
assert_eq!(rule.context, Context::Initial);
}
#[test]
fn test_context_before_vowel_conversion() {
let file = parse_str("c -> s / _[ei];").expect("parse failed");
let ruleset = RuleSetChar::from_llev(&file).expect("conversion failed");
assert_eq!(ruleset.len(), 1);
let rule = &ruleset.rules[0];
match &rule.context {
Context::BeforeVowel(chars) => {
assert_eq!(chars.len(), 2);
assert!(chars.contains(&'e'));
assert!(chars.contains(&'i'));
}
_ => panic!("Expected BeforeVowel context"),
}
}
#[test]
fn test_context_after_vowel_conversion() {
let file = parse_str("s -> z / [aeiou]_;").expect("parse failed");
let ruleset = RuleSetChar::from_llev(&file).expect("conversion failed");
assert_eq!(ruleset.len(), 1);
let rule = &ruleset.rules[0];
match &rule.context {
Context::AfterVowel(chars) => {
assert_eq!(chars.len(), 5);
}
_ => panic!("Expected AfterVowel context"),
}
}
#[test]
fn test_rule_with_metadata() {
let file = parse_str(
r#"
[id: 42, name: "soft c", weight: 0.5]
c -> s / _[ei];
"#,
)
.expect("parse failed");
let ruleset = RuleSetChar::from_llev(&file).expect("conversion failed");
assert_eq!(ruleset.len(), 1);
let rule = &ruleset.rules[0];
assert_eq!(rule.rule_id, 42);
assert_eq!(rule.rule_name, "soft c");
assert_eq!(rule.weight, 0.5);
}
#[test]
fn test_rule_with_metadata_weight() {
let file = parse_str(
r#"
[weight: 0.3]
c -> s;
"#,
)
.expect("parse failed");
let ruleset = RuleSetChar::from_llev(&file).expect("conversion failed");
assert_eq!(ruleset.len(), 1);
let rule = &ruleset.rules[0];
assert!((rule.weight - 0.3).abs() < 1e-10);
}
#[test]
fn test_default_weight() {
let file = parse_str("c -> s;").expect("parse failed");
let ruleset = RuleSetChar::from_llev(&file).expect("conversion failed");
assert_eq!(ruleset.len(), 1);
assert_eq!(ruleset.rules[0].weight, 1.0);
}
#[test]
fn test_disabled_rule_skipped() {
let file = parse_str(
r#"
[enabled: false]
ph -> f;
gh -> f;
"#,
)
.expect("parse failed");
let ruleset = RuleSetChar::from_llev(&file).expect("conversion failed");
assert_eq!(ruleset.len(), 1);
}
#[test]
fn test_symbol_expansion() {
let file = parse_str(
r#"
@define FRONT = [ei]
c -> s / _$FRONT;
"#,
)
.expect("parse failed");
let ruleset = RuleSetChar::from_llev(&file).expect("conversion failed");
assert_eq!(ruleset.len(), 1);
let rule = &ruleset.rules[0];
match &rule.context {
Context::BeforeVowel(chars) => {
assert_eq!(chars.len(), 2);
}
_ => panic!("Expected BeforeVowel context from symbol expansion"),
}
}
#[test]
fn test_undefined_symbol_error() {
let result = parse_str("c -> s / _$UNDEFINED;");
assert!(result.is_err());
match result.unwrap_err().kind {
LLevErrorKind::UndefinedSymbol(name) => {
assert_eq!(name, "UNDEFINED");
}
_ => panic!("Expected UndefinedSymbol error"),
}
}
#[test]
fn test_undefined_symbol_with_suggestion() {
let result = parse_str(
r#"
@define FRONT_VOWEL = [ei]
c -> s / _$FRONTVOWEL;
"#,
);
assert!(result.is_err());
let err = result.unwrap_err();
let err_string = err.to_string();
assert!(err_string.contains("undefined symbol: FRONTVOWEL"));
assert!(err_string.contains("did you mean 'FRONT_VOWEL'?"));
}
#[test]
fn test_undefined_symbol_typo_suggestion() {
let result = parse_str(
r#"
@define CONSONANT = [bcdfghjklmnpqrstvwxyz]
x -> y / _$CONSNANT;
"#,
);
assert!(result.is_err());
let err = result.unwrap_err();
let err_string = err.to_string();
assert!(err_string.contains("undefined symbol: CONSNANT"));
assert!(err_string.contains("did you mean 'CONSONANT'?"));
}
#[test]
fn test_multiple_rules() {
let file = parse_str(
r#"
ph -> f;
gh -> ;
c -> s / _[ei];
"#,
)
.expect("parse failed");
let ruleset = RuleSetChar::from_llev(&file).expect("conversion failed");
assert_eq!(ruleset.len(), 3);
}
#[test]
fn test_auto_id_assignment() {
let file = parse_str(
r#"
ph -> f;
gh -> ;
"#,
)
.expect("parse failed");
let ruleset = RuleSetChar::from_llev(&file).expect("conversion failed");
assert_eq!(ruleset.rules[0].rule_id, 0);
assert_eq!(ruleset.rules[1].rule_id, 1);
}
#[test]
fn test_byte_level_conversion() {
let file = parse_str("ph -> f;").expect("parse failed");
let ruleset = RuleSet::from_llev(&file).expect("conversion failed");
assert_eq!(ruleset.len(), 1);
let rule = &ruleset.rules[0];
assert_eq!(rule.pattern.len(), 2);
assert_eq!(rule.pattern[0], Phone::Consonant(b'p'));
assert_eq!(rule.pattern[1], Phone::Consonant(b'h'));
assert_eq!(rule.replacement.len(), 1);
assert_eq!(rule.replacement[0], Phone::Consonant(b'f'));
}
#[test]
fn test_byte_level_non_ascii_error() {
let file = parse_str("ü -> u;").expect("parse failed");
let result = RuleSet::from_llev(&file);
assert!(result.is_err());
let err = result.unwrap_err();
match &err.kind {
LLevErrorKind::NonAsciiInByteLevel { character, .. } => {
assert_eq!(*character, 'ü');
}
_ => panic!("Expected NonAsciiInByteLevel error, got {:?}", err.kind),
}
let err_string = err.to_string();
assert!(err_string.contains("RuleSetChar"));
assert!(err_string.contains("ü"));
}
#[test]
fn test_char_level_unicode() {
let file = parse_str("ü -> u;").expect("parse failed");
let ruleset = RuleSetChar::from_llev(&file).expect("conversion failed");
assert_eq!(ruleset.len(), 1);
let rule = &ruleset.rules[0];
assert_eq!(rule.pattern.len(), 1);
assert_eq!(rule.pattern[0], Phone::Consonant('ü'));
}
#[test]
fn test_byte_level_non_ascii_in_context() {
let file = parse_str("a -> b / _[aäo];").expect("parse failed");
let result = RuleSet::from_llev(&file);
assert!(result.is_err());
let err = result.unwrap_err();
match &err.kind {
LLevErrorKind::NonAsciiInByteLevel { character, .. } => {
assert_eq!(*character, 'ä');
}
_ => panic!("Expected NonAsciiInByteLevel error, got {:?}", err.kind),
}
}
#[test]
fn test_unsupported_pattern_star() {
let file = parse_str("a* -> b;").expect("parse failed");
let result = RuleSetChar::from_llev(&file);
assert!(result.is_err());
match &result.unwrap_err().kind {
LLevErrorKind::UnsupportedPattern(msg) => {
assert!(msg.contains("Kleene star"));
}
_ => panic!("Expected UnsupportedPattern error"),
}
}
#[test]
fn test_unsupported_pattern_alternation() {
let file = parse_str("(a|b) -> c;").expect("parse failed");
let result = RuleSetChar::from_llev(&file);
assert!(result.is_err());
match &result.unwrap_err().kind {
LLevErrorKind::UnsupportedPattern(msg) => {
assert!(msg.contains("Alternation"));
}
_ => panic!("Expected UnsupportedPattern error"),
}
}
#[test]
fn test_ruleset_merge() {
let file1 = parse_str("ph -> f;").expect("parse failed");
let file2 = parse_str("gh -> ;").expect("parse failed");
let mut ruleset1 = RuleSetChar::from_llev(&file1).expect("conversion failed");
let ruleset2 = RuleSetChar::from_llev(&file2).expect("conversion failed");
ruleset1.merge(ruleset2);
assert_eq!(ruleset1.len(), 2);
}
#[test]
fn test_metadata_preserved() {
let file = parse_str(
r#"
@name "English Rules"
@version "1.0"
ph -> f;
"#,
)
.expect("parse failed");
let ruleset = RuleSetChar::from_llev(&file).expect("conversion failed");
assert_eq!(ruleset.name, Some("English Rules".to_string()));
assert_eq!(ruleset.version, Some("1.0".to_string()));
}
#[test]
fn test_vowel_classification() {
assert!(is_vowel_char('a'));
assert!(is_vowel_char('E'));
assert!(!is_vowel_char('z'));
}
#[test]
fn test_empty_pattern_error() {
let file = parse_str(" -> f;").expect("parse failed");
let result = RuleSetChar::from_llev(&file);
assert!(result.is_err());
match &result.unwrap_err().kind {
LLevErrorKind::InvalidRule(msg) => {
assert!(msg.contains("empty"));
}
_ => panic!("Expected InvalidRule error"),
}
}
#[test]
fn test_context_both_left_and_right() {
let file = parse_str("x -> gz / [aeiou]_[aeiou];").expect("parse failed");
let ruleset = RuleSetChar::from_llev(&file).expect("conversion failed");
assert_eq!(ruleset.len(), 1);
let rule = &ruleset.rules[0];
match &rule.context {
Context::And(left, right) => {
match left.as_ref() {
Context::AfterVowel(chars) => {
assert_eq!(chars.len(), 5); }
_ => panic!("Expected AfterVowel on left, got {:?}", left),
}
match right.as_ref() {
Context::BeforeVowel(chars) => {
assert_eq!(chars.len(), 5); }
_ => panic!("Expected BeforeVowel on right, got {:?}", right),
}
}
_ => panic!("Expected And context, got {:?}", rule.context),
}
}
#[test]
fn test_context_word_boundary_both() {
let file = parse_str("the -> ðə / #_#;").expect("parse failed");
let ruleset = RuleSetChar::from_llev(&file).expect("conversion failed");
assert_eq!(ruleset.len(), 1);
let rule = &ruleset.rules[0];
match &rule.context {
Context::And(left, right) => {
assert_eq!(left.as_ref(), &Context::Initial);
assert_eq!(right.as_ref(), &Context::Final);
}
_ => panic!(
"Expected And(Initial, Final) context, got {:?}",
rule.context
),
}
}
#[test]
fn test_context_initial_with_char_class() {
let file = parse_str("c -> s / #_[ei];").expect("parse failed");
let ruleset = RuleSetChar::from_llev(&file).expect("conversion failed");
assert_eq!(ruleset.len(), 1);
let rule = &ruleset.rules[0];
match &rule.context {
Context::And(left, right) => {
assert_eq!(left.as_ref(), &Context::Initial);
match right.as_ref() {
Context::BeforeVowel(chars) => {
assert!(chars.contains(&'e'));
assert!(chars.contains(&'i'));
}
_ => panic!("Expected BeforeVowel on right, got {:?}", right),
}
}
_ => panic!("Expected And context, got {:?}", rule.context),
}
}
#[test]
fn test_context_char_class_before_final() {
let file = parse_str("s -> z / [aeiou]_#;").expect("parse failed");
let ruleset = RuleSetChar::from_llev(&file).expect("conversion failed");
assert_eq!(ruleset.len(), 1);
let rule = &ruleset.rules[0];
match &rule.context {
Context::And(left, right) => {
match left.as_ref() {
Context::AfterVowel(chars) => {
assert_eq!(chars.len(), 5);
}
_ => panic!("Expected AfterVowel on left, got {:?}", left),
}
assert_eq!(right.as_ref(), &Context::Final);
}
_ => panic!("Expected And context, got {:?}", rule.context),
}
}
#[test]
fn test_byte_level_compound_context() {
let file = parse_str("x -> gz / [aeiou]_[aeiou];").expect("parse failed");
let ruleset = RuleSet::from_llev(&file).expect("conversion failed");
assert_eq!(ruleset.len(), 1);
let rule = &ruleset.rules[0];
match &rule.context {
Context::And(left, right) => {
match left.as_ref() {
Context::AfterVowel(chars) => {
assert_eq!(chars.len(), 5);
}
_ => panic!("Expected AfterVowel on left, got {:?}", left),
}
match right.as_ref() {
Context::BeforeVowel(chars) => {
assert_eq!(chars.len(), 5);
}
_ => panic!("Expected BeforeVowel on right, got {:?}", right),
}
}
_ => panic!("Expected And context, got {:?}", rule.context),
}
}
#[test]
fn test_byte_level_word_boundary_both() {
let file = parse_str("a -> b / #_#;").expect("parse failed");
let ruleset = RuleSet::from_llev(&file).expect("conversion failed");
assert_eq!(ruleset.len(), 1);
let rule = &ruleset.rules[0];
match &rule.context {
Context::And(left, right) => {
assert_eq!(left.as_ref(), &Context::Initial);
assert_eq!(right.as_ref(), &Context::Final);
}
_ => panic!(
"Expected And(Initial, Final) context, got {:?}",
rule.context
),
}
}
}