use super::super::ast::{ContextExpr, Expression, SyllableCondition, SyllableExpr};
use super::super::error::LLevErrorKind;
use super::*;
#[test]
fn test_parse_simple_expression() {
let expr = parse_expression("abc").expect("test: parse_expression abc");
match expr {
Expression::Concat(left, right) => match (*left, *right) {
(Expression::Concat(ll, lr), Expression::Char('c')) => {
assert!(matches!(*ll, Expression::Char('a')));
assert!(matches!(*lr, Expression::Char('b')));
}
_ => panic!("unexpected structure"),
},
_ => panic!("expected Concat"),
}
}
#[test]
fn test_parse_alternation() {
let expr = parse_expression("a|b").expect("test: parse_expression a|b");
match expr {
Expression::Alt(left, right) => {
assert!(matches!(*left, Expression::Char('a')));
assert!(matches!(*right, Expression::Char('b')));
}
_ => panic!("expected Alt"),
}
}
#[test]
fn test_parse_char_class() {
let expr = parse_expression("[aeiou]").expect("test: parse_expression [aeiou]");
match expr {
Expression::CharClass { chars, negated } => {
assert!(!negated);
assert_eq!(chars, vec!['a', 'e', 'i', 'o', 'u']);
}
_ => panic!("expected CharClass"),
}
}
#[test]
fn test_parse_char_class_negated() {
let expr = parse_expression("[^aeiou]").expect("test: parse_expression [^aeiou]");
match expr {
Expression::CharClass { chars, negated } => {
assert!(negated);
assert_eq!(chars, vec!['a', 'e', 'i', 'o', 'u']);
}
_ => panic!("expected negated CharClass"),
}
}
#[test]
fn test_parse_char_range() {
let expr = parse_expression("[a-c]").expect("test: parse_expression [a-c]");
match expr {
Expression::CharClass { chars, negated } => {
assert!(!negated);
assert_eq!(chars, vec!['a', 'b', 'c']);
}
_ => panic!("expected CharClass with range"),
}
}
#[test]
fn test_parse_quantifiers() {
let star = parse_expression("a*").expect("test: parse_expression a*");
assert!(matches!(star, Expression::Star(_)));
let plus = parse_expression("a+").expect("test: parse_expression a+");
assert!(matches!(plus, Expression::Plus(_)));
let opt = parse_expression("a?").expect("test: parse_expression a?");
assert!(matches!(opt, Expression::Optional(_)));
}
#[test]
fn test_parse_counted_quantifier() {
let exact = parse_expression("a{3}").expect("test: parse_expression a{3}");
match exact {
Expression::RepeatExact(_, n) => {
assert_eq!(n, 3);
}
_ => panic!("expected RepeatExact, got {:?}", exact),
}
let range = parse_expression("a{2,4}").expect("test: parse_expression a{2,4}");
match range {
Expression::RepeatRange { min, max, .. } => {
assert_eq!(min, 2);
assert_eq!(max, Some(4));
}
_ => panic!("expected RepeatRange, got {:?}", range),
}
let min_only = parse_expression("a{2,}").expect("test: parse_expression a{2,}");
match min_only {
Expression::RepeatRange { min, max, .. } => {
assert_eq!(min, 2);
assert_eq!(max, None);
}
_ => panic!("expected RepeatRange, got {:?}", min_only),
}
}
#[test]
fn test_parse_group() {
let expr = parse_expression("(ab)").expect("test: parse_expression (ab)");
match expr {
Expression::Concat(left, right) => {
assert!(matches!(*left, Expression::Char('a')));
assert!(matches!(*right, Expression::Char('b')));
}
_ => panic!("expected group contents"),
}
}
#[test]
fn test_parse_any() {
let expr = parse_expression(".").expect("test: parse_expression .");
assert!(matches!(expr, Expression::Any));
}
#[test]
fn test_parse_word_boundary() {
let expr = parse_expression("#").expect("test: parse_expression #");
assert!(matches!(expr, Expression::WordBoundary));
}
#[test]
fn test_parse_symbol_ref() {
let input = r#"
@define VOWEL = [aeiou]
$VOWEL -> x
"#;
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
match &rule.pattern {
Expression::CharClass { chars, negated } => {
assert!(!negated);
assert_eq!(chars.len(), 5);
assert!(chars.contains(&'a'));
assert!(chars.contains(&'e'));
assert!(chars.contains(&'i'));
assert!(chars.contains(&'o'));
assert!(chars.contains(&'u'));
}
_ => panic!("expected CharClass, got {:?}", rule.pattern),
}
}
#[test]
fn test_parse_empty_file() {
let file = parse_str("").expect("test: parse_str empty input");
assert!(file.rules.is_empty());
assert!(file.symbols.is_empty());
assert!(file.includes.is_empty());
}
#[test]
fn test_parse_file_metadata() {
let input = r#"
@name "Test Rules"
@version "1.0"
@author "Test Author"
"#;
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.metadata.name, Some("Test Rules".to_string()));
assert_eq!(file.metadata.version, Some("1.0".to_string()));
assert_eq!(file.metadata.author, Some("Test Author".to_string()));
}
#[test]
fn test_parse_define_directive() {
let input = "@define MY_VOWEL = [aeiou]";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.symbols.len(), 1);
assert_eq!(file.symbols[0].name, "MY_VOWEL");
}
#[test]
fn test_parse_include_directive() {
let input = r#"@include "other.llev""#;
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.includes.len(), 1);
assert_eq!(file.includes[0].path, "other.llev");
}
#[test]
fn test_parse_simple_rule() {
let input = "ph -> f";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
match &rule.pattern {
Expression::Concat(left, right) => {
assert!(matches!(**left, Expression::Char('p')));
assert!(matches!(**right, Expression::Char('h')));
}
_ => panic!("expected Concat for pattern"),
}
assert!(matches!(rule.replacement, Expression::Char('f')));
}
#[test]
fn test_parse_rule_with_context() {
let input = "c -> s / _[ei]";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
assert!(matches!(rule.pattern, Expression::Char('c')));
assert!(matches!(rule.replacement, Expression::Char('s')));
assert!(rule.context.is_some());
let ctx = rule.context.as_ref().expect("should have context");
assert!(ctx.left.is_none());
assert!(ctx.right.is_some());
}
#[test]
fn test_parse_deletion_rule() {
let input = "gh -> ;";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
assert!(matches!(rule.replacement, Expression::Empty));
}
#[test]
fn test_parse_rule_with_metadata() {
let input = r#"[id: 1, name: "ph to f"]
ph -> f"#;
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let def = &file.rules[0];
assert_eq!(def.metadata.id, Some(1));
assert_eq!(def.metadata.name, Some("ph to f".to_string()));
}
#[test]
fn test_parse_multiple_rules() {
let input = r#"
ph -> f
gh ->
c -> k
"#;
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 3);
}
#[test]
fn test_parse_complex_pattern() {
let expr = parse_expression("(a|b)*c+d?").expect("test: parse_expression (a|b)*c+d?");
assert!(matches!(expr, Expression::Concat(_, _)));
}
#[test]
fn test_parse_metadata_group_as_identifier() {
let input = r#"[id: 1, name: "test", group: orthography]
ph -> f"#;
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let def = &file.rules[0];
assert_eq!(def.metadata.group, Some("orthography".to_string()));
}
#[test]
fn test_parse_metadata_group_as_string() {
let input = r#"[id: 1, name: "test", group: "orthography"]
ph -> f"#;
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let def = &file.rules[0];
assert_eq!(def.metadata.group, Some("orthography".to_string()));
}
#[test]
fn test_parse_metadata_ipa_phonemic() {
let input = r#"[id: 1, name: "sch to sh", ipa: "/ʃ/"]
sch -> sh"#;
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let def = &file.rules[0];
assert_eq!(def.metadata.ipa, Some("/ʃ/".to_string()));
}
#[test]
fn test_parse_metadata_ipa_phonetic() {
let input = r#"[id: 2, name: "ch to x", ipa: "[x]"]
ch -> X / V_"#;
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let def = &file.rules[0];
assert_eq!(def.metadata.ipa, Some("[x]".to_string()));
}
#[test]
fn test_parse_metadata_ipa_with_all_fields() {
let input = r#"[id: 100, name: "tsch affricate", weight: 0.5, group: german_consonants, ipa: "/t͡ʃ/"]
tsch -> tsh"#;
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let def = &file.rules[0];
assert_eq!(def.metadata.id, Some(100));
assert_eq!(def.metadata.name, Some("tsch affricate".to_string()));
assert_eq!(def.metadata.weight, Some(0.5));
assert_eq!(def.metadata.group, Some("german_consonants".to_string()));
assert_eq!(def.metadata.ipa, Some("/t͡ʃ/".to_string()));
}
#[test]
fn test_parse_escaped_uppercase_literal() {
let input = r#"\B -> b"#;
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
assert!(matches!(rule.pattern, Expression::Char('B')));
assert!(matches!(rule.replacement, Expression::Char('b')));
}
#[test]
fn test_parse_string_literal_for_uppercase() {
let input = r#""ABC" -> abc"#;
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
match &rule.pattern {
Expression::Concat(left, right) => {
assert!(matches!(**right, Expression::Char('C')));
match &**left {
Expression::Concat(ll, lr) => {
assert!(matches!(**ll, Expression::Char('A')));
assert!(matches!(**lr, Expression::Char('B')));
}
_ => panic!("expected nested Concat"),
}
}
_ => panic!("expected Concat for pattern"),
}
}
#[test]
fn test_parse_mixed_escaped_and_regular() {
let input = r#"\Bcd -> bcd"#;
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
match &rule.pattern {
Expression::Concat(left, right) => {
assert!(matches!(**right, Expression::Char('d')));
match &**left {
Expression::Concat(ll, lr) => {
assert!(matches!(**ll, Expression::Char('B')));
assert!(matches!(**lr, Expression::Char('c')));
}
_ => panic!("expected nested Concat"),
}
}
_ => panic!("expected Concat for pattern"),
}
}
#[test]
fn test_parse_context_word_boundary_initial() {
let input = "wr -> r / #_";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
assert!(matches!(
ctx.left.as_deref(),
Some(ContextExpr::WordBoundary)
));
assert!(ctx.right.is_none());
}
#[test]
fn test_parse_context_word_boundary_final() {
let input = "e -> / _#";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
assert!(ctx.left.is_none());
assert!(matches!(
ctx.right.as_deref(),
Some(ContextExpr::WordBoundary)
));
}
#[test]
fn test_parse_context_not() {
let input = "c -> k / _![ei]";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
assert!(ctx.left.is_none());
match ctx.right.as_deref() {
Some(ContextExpr::Not(inner)) => {
assert!(matches!(**inner, ContextExpr::Pattern(_)));
}
_ => panic!("expected Not context"),
}
}
#[test]
fn test_parse_context_and() {
let input = "x -> gz / [aeiou]&[aeiou]_";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
match ctx.left.as_deref() {
Some(ContextExpr::And(_, _)) => {}
_ => panic!("expected And context"),
}
assert!(ctx.right.is_none());
}
#[test]
fn test_parse_context_or() {
let input = "e -> / _([bcdfg]|#)";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
assert!(ctx.left.is_none());
match ctx.right.as_deref() {
Some(ContextExpr::Or(_, _)) => {}
_ => panic!("expected Or context, got {:?}", ctx.right),
}
}
#[test]
fn test_parse_context_complex_compound() {
let input = "t -> d / ![x]&[aeiou]|[ou]_";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
match ctx.left.as_deref() {
Some(ContextExpr::Or(_, _)) => {}
_ => panic!("expected Or context at top level"),
}
}
#[test]
fn test_parse_syllable_monosyllable() {
let input = "y -> i / _# if monosyllable";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
match &ctx.syllable {
Some(SyllableExpr::Cond(SyllableCondition::Monosyllable)) => {}
_ => panic!("expected monosyllable condition"),
}
}
#[test]
fn test_parse_syllable_polysyllable() {
let input = "y -> i / _# if polysyllable";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
match &ctx.syllable {
Some(SyllableExpr::Cond(SyllableCondition::Polysyllable)) => {}
_ => panic!("expected polysyllable condition"),
}
}
#[test]
fn test_parse_syllable_and() {
let input = "y -> i / _# if polysyllable & final_syllable";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
match &ctx.syllable {
Some(SyllableExpr::And(_, _)) => {}
_ => panic!("expected And syllable condition"),
}
}
#[test]
fn test_parse_syllable_or() {
let input = "a -> aa / _ if open_syllable | initial_syllable";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
match &ctx.syllable {
Some(SyllableExpr::Or(_, _)) => {}
_ => panic!("expected Or syllable condition"),
}
}
#[test]
fn test_parse_syllable_not() {
let input = "e -> i / _[bcdfg] if !final_syllable";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
match &ctx.syllable {
Some(SyllableExpr::Not(_)) => {}
_ => panic!("expected Not syllable condition"),
}
}
#[test]
fn test_parse_syllable_complex() {
let input = "t -> d / [aeiou]_[bcdfg] if !monosyllable & !final_syllable";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
match &ctx.syllable {
Some(SyllableExpr::And(left, right)) => {
assert!(matches!(**left, SyllableExpr::Not(_)));
assert!(matches!(**right, SyllableExpr::Not(_)));
}
_ => panic!("expected And of two Not conditions"),
}
}
#[test]
fn test_parse_syllable_with_parens() {
let input = "y -> i / _# if (monosyllable | polysyllable) & final_syllable";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
match &ctx.syllable {
Some(SyllableExpr::And(left, _)) => {
assert!(matches!(**left, SyllableExpr::Or(_, _)));
}
_ => panic!("expected And with Or on left"),
}
}
#[test]
fn test_parse_all_syllable_keywords() {
let keywords = [
("monosyllable", SyllableCondition::Monosyllable),
("polysyllable", SyllableCondition::Polysyllable),
("open_syllable", SyllableCondition::OpenSyllable),
("closed_syllable", SyllableCondition::ClosedSyllable),
("final_syllable", SyllableCondition::FinalSyllable),
("initial_syllable", SyllableCondition::InitialSyllable),
];
for (kw, expected_cond) in keywords {
let input = format!("a -> b / _ if {}", kw);
let file = parse_str(&input).expect(&format!("failed to parse {}", kw));
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
match &ctx.syllable {
Some(SyllableExpr::Cond(cond)) => {
assert_eq!(
std::mem::discriminant(cond),
std::mem::discriminant(&expected_cond),
"keyword {} didn't produce expected condition",
kw
);
}
_ => panic!("expected Cond for keyword {}", kw),
}
}
}
#[test]
fn test_parse_context_only_no_syllable() {
let input = "c -> s / _[ei]";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
assert!(ctx.syllable.is_none());
}
#[test]
fn test_parse_no_context_no_syllable() {
let input = "ph -> f";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
assert!(rule.context.is_none());
}
#[test]
fn test_parse_user_symbol_in_char_class() {
let input = r#"
@define MY_VOWEL = [aeiou]
x -> gz / [$abc]_[AB]
"#;
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
if let Some(ref boxed) = ctx.left {
if let ContextExpr::Pattern(ref expr) = **boxed {
if let Expression::CharClass { ref chars, negated } = expr {
assert!(!negated);
assert!(chars.contains(&'$'), "Should contain literal $");
assert!(chars.contains(&'a'), "Should contain literal a");
assert!(chars.contains(&'b'), "Should contain literal b");
assert!(chars.contains(&'c'), "Should contain literal c");
} else {
panic!("expected CharClass in left context");
}
} else {
panic!("expected Pattern in left context");
}
} else {
panic!("expected Some in left context");
}
}
#[test]
fn test_parse_negated_user_symbol_in_char_class() {
let input = r#"
c -> k / _[^\$ab]
"#;
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
if let Some(ref boxed) = ctx.right {
if let ContextExpr::Pattern(ref expr) = **boxed {
if let Expression::CharClass { ref chars, negated } = expr {
assert!(negated);
assert!(chars.contains(&'$'));
assert!(chars.contains(&'a'));
assert!(chars.contains(&'b'));
} else {
panic!("expected CharClass in right context");
}
} else {
panic!("expected Pattern in right context");
}
} else {
panic!("expected Some in right context");
}
}
#[test]
fn test_parse_mixed_symbol_and_chars_in_class() {
let input = r#"
@define M = [mn]
a -> b / _[$xyz]
"#;
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
if let Some(ref boxed) = ctx.right {
if let ContextExpr::Pattern(ref expr) = **boxed {
if let Expression::CharClass { ref chars, negated } = expr {
assert!(!negated);
assert!(chars.contains(&'$'), "Should contain literal $");
assert!(chars.contains(&'x'));
assert!(chars.contains(&'y'));
assert!(chars.contains(&'z'));
} else {
panic!("expected CharClass in right context");
}
} else {
panic!("expected Pattern in right context");
}
} else {
panic!("expected Some in right context");
}
}
#[test]
fn test_parse_dollar_as_literal_in_char_class() {
let input = "a -> b / _[$]";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
if let Some(ref boxed) = ctx.right {
if let ContextExpr::Pattern(ref expr) = **boxed {
if let Expression::CharClass { ref chars, negated } = expr {
assert!(!negated);
assert_eq!(chars.len(), 1);
assert!(chars.contains(&'$'));
} else {
panic!("expected CharClass");
}
} else {
panic!("expected Pattern");
}
} else {
panic!("expected right context");
}
}
#[test]
fn test_parse_empty_named_class_error() {
let input = "a -> b / _[[::]xyz]";
let result = parse_str(input);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err.kind, LLevErrorKind::InvalidPattern(_)));
}
#[test]
fn test_parse_symbol_reference_outside_char_class() {
let input = r#"
@define MY_VOWEL = [aeiou]
$MY_VOWEL -> X
"#;
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
if let Expression::CharClass { chars, negated } = &rule.pattern {
assert!(!negated);
assert!(chars.contains(&'a'));
assert!(chars.contains(&'e'));
assert!(chars.contains(&'i'));
assert!(chars.contains(&'o'));
assert!(chars.contains(&'u'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_parse_builtin_vowel_class() {
let input = "c -> s / _[[:vowel:]]";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
if let Some(ref boxed) = ctx.right {
if let ContextExpr::Pattern(ref expr) = **boxed {
if let Expression::CharClass { ref chars, negated } = expr {
assert!(!negated);
assert!(chars.contains(&'a'));
assert!(chars.contains(&'e'));
assert!(chars.contains(&'i'));
assert!(chars.contains(&'o'));
assert!(chars.contains(&'u'));
assert!(chars.contains(&'A'));
assert!(chars.contains(&'É™'));
} else {
panic!("expected CharClass");
}
} else {
panic!("expected Pattern");
}
} else {
panic!("expected Some");
}
}
#[test]
fn test_parse_builtin_full_word_alias() {
let input = "[[:plosive:]] -> 0";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
if let Expression::CharClass { ref chars, negated } = rule.pattern {
assert!(!negated);
assert!(chars.contains(&'p'));
assert!(chars.contains(&'t'));
assert!(chars.contains(&'k'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_parse_builtin_case_insensitive() {
let inputs = vec![
"c -> s / _[[:VOWEL:]]",
"c -> s / _[[:Vowel:]]",
"c -> s / _[[:vowel:]]",
];
for input in inputs {
let file = parse_str(input).expect(&format!("should parse: {}", input));
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
if let Some(ref boxed) = ctx.right {
if let ContextExpr::Pattern(ref expr) = **boxed {
if let Expression::CharClass { ref chars, .. } = expr {
assert!(chars.contains(&'a'));
}
}
}
}
}
#[test]
fn test_parse_builtin_consonant_class() {
let input = "[[:consonant:]] -> 0";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
if let Expression::CharClass { ref chars, negated } = rule.pattern {
assert!(!negated);
assert!(chars.contains(&'b'));
assert!(chars.contains(&'c'));
assert!(chars.contains(&'d'));
assert!(!chars.contains(&'a'));
assert!(!chars.contains(&'e'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_parse_builtin_posix_alpha() {
let input = "[[:alpha:]] -> x";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
if let Expression::CharClass { ref chars, .. } = rule.pattern {
assert!(chars.contains(&'a'));
assert!(chars.contains(&'z'));
assert!(chars.contains(&'A'));
assert!(chars.contains(&'Z'));
assert!(!chars.contains(&'0'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_parse_builtin_mixed_with_chars() {
let input = "c -> s / _[[:vowel:]y]";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
if let Some(ref boxed) = ctx.right {
if let ContextExpr::Pattern(ref expr) = **boxed {
if let Expression::CharClass { ref chars, .. } = expr {
assert!(chars.contains(&'a'));
assert!(chars.contains(&'e'));
assert!(chars.contains(&'y'));
} else {
panic!("expected CharClass");
}
}
}
}
#[test]
fn test_parse_define_lowercase_symbol_rejected() {
let input = "@define vowel = [aeiou]";
let result = parse_str(input);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(
err.kind,
LLevErrorKind::SymbolNameMustBeUppercase { .. }
));
}
#[test]
fn test_parse_define_uppercase_required() {
let invalid_inputs = vec![
"@define vowel = [aeiou]", "@define Vowel = [aeiou]", "@define alpha = [abc]", ];
for input in invalid_inputs {
let result = parse_str(input);
assert!(result.is_err(), "should error on: {}", input);
let err = result.unwrap_err();
assert!(
matches!(err.kind, LLevErrorKind::SymbolNameMustBeUppercase { .. }),
"expected SymbolNameMustBeUppercase for: {}",
input
);
}
let valid_inputs = vec![
"@define VOWEL = [aeiou]",
"@define V = [aeiou]",
"@define MY_CLASS = [abc]",
];
for input in valid_inputs {
let result = parse_str(input);
assert!(
result.is_ok(),
"should succeed for: {}, got: {:?}",
input,
result.err()
);
}
}
#[test]
fn test_parse_user_defined_non_conflicting() {
let input = r#"
@define MY_VOWELS = [aeiou]
c -> s / _[$MY_VOWELS]
"#;
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.symbols.len(), 1);
assert_eq!(file.rules.len(), 1);
}
#[test]
fn test_parse_builtin_front_vowel() {
let input = "c -> s / _[[:front_vowel:]]";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
if let Some(ref boxed) = ctx.right {
if let ContextExpr::Pattern(ref expr) = **boxed {
if let Expression::CharClass { ref chars, .. } = expr {
assert!(chars.contains(&'e'));
assert!(chars.contains(&'i'));
assert!(!chars.contains(&'o'));
assert!(!chars.contains(&'u'));
} else {
panic!("expected CharClass");
}
}
}
}
#[test]
fn test_parse_builtin_stop_consonant() {
let input = "[[:stop:]] -> 0";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
if let Expression::CharClass { ref chars, .. } = rule.pattern {
assert!(chars.contains(&'p'));
assert!(chars.contains(&'b'));
assert!(chars.contains(&'t'));
assert!(chars.contains(&'d'));
assert!(chars.contains(&'k'));
assert!(chars.contains(&'g'));
assert!(!chars.contains(&'f'));
assert!(!chars.contains(&'s'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_parse_builtin_ascii_vowel() {
let input = "[[:ascii_vowel:]] -> V";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
if let Expression::CharClass { ref chars, .. } = rule.pattern {
assert!(chars.contains(&'a'));
assert!(chars.contains(&'e'));
assert!(!chars.contains(&'É™'));
assert!(!chars.contains(&'ɪ'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_parse_builtin_ipa_vowel() {
let input = "[[:ipa_vowel:]] -> V";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
if let Expression::CharClass { ref chars, .. } = rule.pattern {
assert!(chars.contains(&'É™'));
assert!(chars.contains(&'ɪ'));
assert!(!chars.contains(&'a'));
assert!(!chars.contains(&'e'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_parse_standalone_vowel_class() {
let input = "[:vowel:] -> V";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
if let Expression::CharClass { ref chars, negated } = rule.pattern {
assert!(!negated);
assert!(chars.contains(&'a'));
assert!(chars.contains(&'e'));
assert!(chars.contains(&'É™'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_parse_standalone_vowel() {
let input = "[:vowel:] -> V";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
if let Expression::CharClass { ref chars, .. } = rule.pattern {
assert!(chars.contains(&'a'));
assert!(chars.contains(&'e'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_parse_standalone_in_context() {
let input = "c -> s / _[:front_vowel:]";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
if let Some(ref boxed) = ctx.right {
if let ContextExpr::Pattern(ref expr) = **boxed {
if let Expression::CharClass { ref chars, .. } = expr {
assert!(chars.contains(&'e'));
assert!(chars.contains(&'i'));
assert!(!chars.contains(&'o'));
} else {
panic!("expected CharClass");
}
} else {
panic!("expected Pattern");
}
} else {
panic!("expected right context");
}
}
#[test]
fn test_parse_standalone_consonant() {
let input = "[:consonant:][:vowel:] -> CV";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
match &rule.pattern {
Expression::Concat(left, right) => {
if let Expression::CharClass { chars: l_chars, .. } = &**left {
assert!(l_chars.contains(&'b'));
assert!(l_chars.contains(&'c'));
assert!(!l_chars.contains(&'a'));
} else {
panic!("expected left CharClass");
}
if let Expression::CharClass { chars: r_chars, .. } = &**right {
assert!(r_chars.contains(&'a'));
assert!(r_chars.contains(&'e'));
assert!(!r_chars.contains(&'b'));
} else {
panic!("expected right CharClass");
}
}
_ => panic!("expected Concat"),
}
}
#[test]
fn test_parse_standalone_posix_digit() {
let input = "[:digit:] -> 0";
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
if let Expression::CharClass { ref chars, .. } = rule.pattern {
assert!(chars.contains(&'0'));
assert!(chars.contains(&'5'));
assert!(chars.contains(&'9'));
assert!(!chars.contains(&'a'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_parse_standalone_user_symbol() {
let input = r#"
@define MY_CHARS = [xyz]
$MY_CHARS -> 0
"#;
let file = parse_str(input).expect("test: parse_str input");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
if let Expression::CharClass { ref chars, .. } = rule.pattern {
assert_eq!(chars.len(), 3);
assert!(chars.contains(&'x'));
assert!(chars.contains(&'y'));
assert!(chars.contains(&'z'));
} else {
panic!("expected CharClass, got {:?}", rule.pattern);
}
}
#[test]
fn test_parse_standalone_undefined_symbol_error() {
let input = "$UNDEFINED_CLASS -> x";
let result = parse_str(input);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err.kind, LLevErrorKind::UndefinedSymbol(_)));
}
#[test]
fn test_parse_standalone_empty_error() {
let input = "[::]-> x";
let result = parse_str(input);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err.kind, LLevErrorKind::InvalidPattern(_)));
}
#[test]
fn test_parse_standalone_vs_posix() {
let standalone = parse_str("[:vowel:] -> V").expect("test: parse [:vowel:] -> V");
let posix = parse_str("[[:vowel:]] -> V").expect("test: parse [[:vowel:]] -> V");
let standalone_rule = &standalone.rules[0].rule;
let posix_rule = &posix.rules[0].rule;
if let (
Expression::CharClass { chars: s_chars, .. },
Expression::CharClass { chars: p_chars, .. },
) = (&standalone_rule.pattern, &posix_rule.pattern)
{
assert_eq!(s_chars.len(), p_chars.len());
for c in s_chars {
assert!(p_chars.contains(c));
}
} else {
panic!("expected CharClass for both");
}
}
#[test]
fn test_parse_inline_named_class() {
let file = parse_str("[x:vowel:] -> X").expect("test: parse [x:vowel:] -> X");
let rule = &file.rules[0].rule;
if let Expression::CharClass { chars, negated } = &rule.pattern {
assert!(!negated);
assert!(chars.contains(&'x'));
assert!(chars.contains(&':'));
assert!(chars.contains(&'v'));
assert!(chars.contains(&'o'));
assert!(chars.contains(&'w'));
assert!(chars.contains(&'e'));
assert!(chars.contains(&'l'));
assert!(!chars.contains(&'a'));
assert!(!chars.contains(&'i'));
assert!(!chars.contains(&'u'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_parse_inline_named_class_correct_syntax() {
let file = parse_str("[x[:vowel:]] -> X").expect("test: parse [x[:vowel:]] -> X");
let rule = &file.rules[0].rule;
if let Expression::CharClass { chars, negated } = &rule.pattern {
assert!(!negated);
assert!(chars.contains(&'x'));
assert!(chars.contains(&'a'));
assert!(chars.contains(&'e'));
assert!(chars.contains(&'i'));
assert!(chars.contains(&'o'));
assert!(chars.contains(&'u'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_parse_inline_named_class_bracket_equivalence() {
let file1 = parse_str("[x[:vowel:]] -> X").expect("test: parse file1");
let file2 = parse_str("[x[[:vowel:]]] -> X").expect("test: parse file2");
let file3 = parse_str("[x[[[:vowel:]]]] -> X").expect("test: parse file3");
if let (
Expression::CharClass { chars: chars1, .. },
Expression::CharClass { chars: chars2, .. },
Expression::CharClass { chars: chars3, .. },
) = (
&file1.rules[0].rule.pattern,
&file2.rules[0].rule.pattern,
&file3.rules[0].rule.pattern,
) {
assert_eq!(chars1.len(), chars2.len());
assert_eq!(chars1.len(), chars3.len());
for c in chars1 {
assert!(chars2.contains(c));
assert!(chars3.contains(c));
}
} else {
panic!("expected CharClass for all");
}
}
#[test]
fn test_parse_inline_named_class_with_symbol() {
let input = r#"
@define ABC = [abc]
[$abc:xyz:] -> X
"#;
let file = parse_str(input).expect("test: parse_str input");
let rule = &file.rules[0].rule;
if let Expression::CharClass { chars, negated } = &rule.pattern {
assert!(!negated);
assert!(chars.contains(&'$'), "Should contain literal $");
assert!(chars.contains(&'a'));
assert!(chars.contains(&'b'));
assert!(chars.contains(&'c'));
assert!(chars.contains(&':'));
assert!(chars.contains(&'x'));
assert!(chars.contains(&'y'));
assert!(chars.contains(&'z'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_parse_chars_union_named_class() {
let file = parse_str("[xyz[:consonant:]] -> X").expect("test: parse [xyz[:consonant:]] -> X");
let rule = &file.rules[0].rule;
if let Expression::CharClass { chars, negated } = &rule.pattern {
assert!(!negated);
assert!(chars.contains(&'x'));
assert!(chars.contains(&'y'));
assert!(chars.contains(&'z'));
assert!(chars.contains(&'b'));
assert!(chars.contains(&'c'));
assert!(chars.contains(&'d'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_parse_negated_nested_class() {
let file = parse_str("[[^:vowel:]] -> C").expect("test: parse [[^:vowel:]] -> C");
let rule = &file.rules[0].rule;
if let Expression::CharClass { chars, negated } = &rule.pattern {
assert!(!negated);
assert!(!chars.contains(&'a'));
assert!(!chars.contains(&'e'));
assert!(!chars.contains(&'i'));
assert!(!chars.contains(&'o'));
assert!(!chars.contains(&'u'));
assert!(chars.contains(&'b'));
assert!(chars.contains(&'c'));
assert!(chars.contains(&'!'));
assert!(chars.contains(&' '));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_parse_nested_class_union() {
let file = parse_str("[abc[:consonant:]] -> X").expect("test: parse [abc[:consonant:]] -> X");
let rule = &file.rules[0].rule;
if let Expression::CharClass { chars, negated } = &rule.pattern {
assert!(!negated);
assert!(chars.contains(&'a'));
assert!(chars.contains(&'b'));
assert!(chars.contains(&'c'));
assert!(chars.contains(&'d'));
assert!(chars.contains(&'f'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_parse_deeply_nested_class() {
let file = parse_str("[[[[:vowel:]]]] -> V").expect("test: parse [[[[:vowel:]]]] -> V");
let rule = &file.rules[0].rule;
if let Expression::CharClass { chars, negated } = &rule.pattern {
assert!(!negated);
assert!(chars.contains(&'a'));
assert!(chars.contains(&'e'));
assert!(chars.contains(&'i'));
assert!(chars.contains(&'o'));
assert!(chars.contains(&'u'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_parse_colon_as_literal() {
let file = parse_str("[:abc] -> X").expect("test: parse [:abc] -> X");
let rule = &file.rules[0].rule;
if let Expression::CharClass { chars, negated } = &rule.pattern {
assert!(!negated);
assert!(chars.contains(&':'));
assert!(chars.contains(&'a'));
assert!(chars.contains(&'b'));
assert!(chars.contains(&'c'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_parse_escaped_brackets_in_char_class() {
let file = parse_str(r"[a\[\]b] -> X").expect("test: parse [a\\[\\]b] -> X");
let rule = &file.rules[0].rule;
if let Expression::CharClass { chars, negated } = &rule.pattern {
assert!(!negated);
assert!(chars.contains(&'a'));
assert!(chars.contains(&'['));
assert!(chars.contains(&']'));
assert!(chars.contains(&'b'));
assert_eq!(chars.len(), 4);
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_parse_escaped_vs_unescaped_bracket() {
let file = parse_str(r"[\[[[:vowel:]]] -> X").expect("test: parse [\\[[[:vowel:]]] -> X");
let rule = &file.rules[0].rule;
if let Expression::CharClass { chars, negated } = &rule.pattern {
assert!(!negated);
assert!(chars.contains(&'['));
assert!(chars.contains(&'a'));
assert!(chars.contains(&'e'));
assert!(chars.contains(&'i'));
assert!(chars.contains(&'o'));
assert!(chars.contains(&'u'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_feature_bundle_voiced_stop() {
let file = parse_str("[:voiced stop:] -> V").expect("test: parse [:voiced stop:] -> V");
let rule = &file.rules[0].rule;
if let Expression::CharClass { chars, negated } = &rule.pattern {
assert!(!negated);
assert!(chars.contains(&'b'));
assert!(chars.contains(&'d'));
assert!(chars.contains(&'g'));
assert!(!chars.contains(&'p'));
assert!(!chars.contains(&'t'));
assert!(!chars.contains(&'k'));
assert!(!chars.contains(&'v'));
assert!(!chars.contains(&'z'));
assert!(!chars.contains(&'a'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_feature_bundle_negated_nasal_stop() {
let file = parse_str("[:!nasal stop:] -> S").expect("test: parse [:!nasal stop:] -> S");
let rule = &file.rules[0].rule;
if let Expression::CharClass { chars, negated } = &rule.pattern {
assert!(!negated);
assert!(chars.contains(&'p'));
assert!(chars.contains(&'t'));
assert!(chars.contains(&'k'));
assert!(chars.contains(&'b'));
assert!(chars.contains(&'d'));
assert!(chars.contains(&'g'));
assert!(!chars.contains(&'m'));
assert!(!chars.contains(&'n'));
assert!(!chars.contains(&'f'));
assert!(!chars.contains(&'s'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_feature_bundle_single_negated() {
let file = parse_str("[:!nasal:] -> X").expect("test: parse [:!nasal:] -> X");
let rule = &file.rules[0].rule;
if let Expression::CharClass { chars, negated } = &rule.pattern {
assert!(!negated);
assert!(!chars.contains(&'m'));
assert!(!chars.contains(&'n'));
assert!(chars.contains(&'p'));
assert!(chars.contains(&'b'));
assert!(chars.contains(&'a'));
assert!(chars.contains(&'e'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_feature_bundle_three_features() {
let file = parse_str("[:high_vowel front_vowel vowel:] -> I")
.expect("test: parse multi-feature class");
let rule = &file.rules[0].rule;
if let Expression::CharClass { chars, negated } = &rule.pattern {
assert!(!negated);
assert!(chars.contains(&'i'));
assert!(chars.contains(&'I'));
assert!(!chars.contains(&'a'));
assert!(!chars.contains(&'u'));
assert!(!chars.contains(&'p'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_feature_bundle_nested_syntax() {
let file =
parse_str("[x[[:voiced stop:]]] -> X").expect("test: parse [x[[:voiced stop:]]] -> X");
let rule = &file.rules[0].rule;
if let Expression::CharClass { chars, negated } = &rule.pattern {
assert!(!negated);
assert!(chars.contains(&'x'));
assert!(chars.contains(&'b'));
assert!(chars.contains(&'d'));
assert!(chars.contains(&'g'));
assert!(!chars.contains(&'p'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_feature_bundle_backwards_compatible() {
let file = parse_str("[:stop:] -> S").expect("test: parse [:stop:] -> S");
let rule = &file.rules[0].rule;
if let Expression::CharClass { chars, negated } = &rule.pattern {
assert!(!negated);
assert!(chars.contains(&'p'));
assert!(chars.contains(&'t'));
assert!(chars.contains(&'k'));
assert!(chars.contains(&'b'));
assert!(chars.contains(&'d'));
assert!(chars.contains(&'g'));
} else {
panic!("expected CharClass");
}
}
#[test]
fn test_parse_scoped_flags_case_sensitive_c() {
let file = parse_str("(?c:ABC) -> abc").expect("test: parse (?c:ABC) -> abc");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
if let Expression::ScopedFlags { flags, inner } = &rule.pattern {
assert_eq!(flags.case_insensitive, Some(false));
if let Expression::Concat(_, _) = **inner {
} else if let Expression::Char(_) = **inner {
} else {
panic!(
"expected Concat or Char inside ScopedFlags, got {:?}",
inner
);
}
} else {
panic!("expected ScopedFlags, got {:?}", rule.pattern);
}
}
#[test]
fn test_parse_scoped_flags_case_sensitive_minus_i() {
let file = parse_str("(?-i:XYZ) -> xyz").expect("test: parse (?-i:XYZ) -> xyz");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
if let Expression::ScopedFlags { flags, inner: _ } = &rule.pattern {
assert_eq!(flags.case_insensitive, Some(false));
} else {
panic!("expected ScopedFlags, got {:?}", rule.pattern);
}
}
#[test]
fn test_parse_scoped_flags_in_context() {
let file = parse_str("a -> b / (?c:ABC)_").expect("test: parse a -> b / (?c:ABC)_");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
let ctx = rule.context.as_ref().expect("should have context");
if let Some(ref left) = ctx.left {
if let ContextExpr::Pattern(ref expr) = **left {
assert!(
matches!(expr, Expression::ScopedFlags { .. }),
"expected ScopedFlags in left context"
);
} else {
panic!("expected Pattern in left context");
}
} else {
panic!("expected left context");
}
}
#[test]
fn test_parse_scoped_flags_with_alternation() {
let file = parse_str("(?c:A|B) -> x").expect("test: parse (?c:A|B) -> x");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
if let Expression::ScopedFlags { flags, inner } = &rule.pattern {
assert_eq!(flags.case_insensitive, Some(false));
assert!(
matches!(**inner, Expression::Alt(_, _)),
"expected Alt inside ScopedFlags, got {:?}",
inner
);
} else {
panic!("expected ScopedFlags");
}
}
#[test]
fn test_parse_regular_group_not_scoped_flags() {
let file = parse_str("(abc) -> xyz").expect("test: parse (abc) -> xyz");
assert_eq!(file.rules.len(), 1);
let rule = &file.rules[0].rule;
assert!(
!matches!(rule.pattern, Expression::ScopedFlags { .. }),
"regular group should not become ScopedFlags"
);
}