use alloc::string::String;
use alloc::vec::Vec;
use svara::phoneme::Phoneme;
use varna::phoneme::PhonemeInventory;
use crate::engine::Language;
#[must_use]
pub fn phoneme_to_ipa(phoneme: Phoneme) -> Option<&'static str> {
match phoneme {
Phoneme::VowelA => Some("ɑː"),
Phoneme::VowelE => Some("iː"),
Phoneme::VowelI => Some("ɪ"),
Phoneme::VowelO => Some("oʊ"),
Phoneme::VowelU => Some("uː"),
Phoneme::VowelSchwa => Some("ə"),
Phoneme::VowelOpenO => Some("ɔː"),
Phoneme::VowelAsh => Some("æ"),
Phoneme::VowelNearI => Some("ɪ"),
Phoneme::VowelNearU => Some("ʊ"),
Phoneme::VowelOpenA => Some("ɑː"),
Phoneme::VowelOpenE => Some("ɛ"),
Phoneme::VowelCupV => Some("ʌ"),
Phoneme::VowelLongI => Some("iː"),
Phoneme::VowelBird => None,
Phoneme::DiphthongEI => Some("eɪ"),
Phoneme::DiphthongOU => Some("oʊ"),
Phoneme::DiphthongAI => None,
Phoneme::DiphthongAU => None,
Phoneme::DiphthongOI => None,
Phoneme::PlosiveP => Some("p"),
Phoneme::PlosiveB => Some("b"),
Phoneme::PlosiveT => Some("t"),
Phoneme::PlosiveD => Some("d"),
Phoneme::PlosiveK => Some("k"),
Phoneme::PlosiveG => Some("ɡ"),
Phoneme::FricativeF => Some("f"),
Phoneme::FricativeV => Some("v"),
Phoneme::FricativeS => Some("s"),
Phoneme::FricativeZ => Some("z"),
Phoneme::FricativeSh => Some("ʃ"),
Phoneme::FricativeZh => Some("ʒ"),
Phoneme::FricativeTh => Some("θ"),
Phoneme::FricativeDh => Some("ð"),
Phoneme::FricativeH => Some("h"),
Phoneme::NasalM => Some("m"),
Phoneme::NasalN => Some("n"),
Phoneme::NasalNg => Some("ŋ"),
Phoneme::AffricateCh => Some("t͡ʃ"),
Phoneme::AffricateJ => Some("d͡ʒ"),
Phoneme::ApproximantR => Some("ɹ"),
Phoneme::ApproximantW => Some("w"),
Phoneme::ApproximantJ => Some("j"),
Phoneme::LateralL => Some("l"),
Phoneme::GlottalStop => Some("ʔ"),
Phoneme::TapFlap => Some("ɾ"),
Phoneme::Silence => None,
_ => None,
}
}
pub const VARNA_INVENTORY_GAPS: &[Phoneme] = &[
Phoneme::DiphthongAI, Phoneme::DiphthongAU, Phoneme::DiphthongOI, Phoneme::VowelBird, ];
#[must_use]
pub fn phoneme_to_ipa_for(phoneme: Phoneme, language: Language) -> Option<&'static str> {
match language {
Language::English => phoneme_to_ipa(phoneme),
Language::Spanish => spanish_phoneme_to_ipa(phoneme),
Language::German => german_phoneme_to_ipa(phoneme),
Language::Hindi => hindi_phoneme_to_ipa(phoneme),
Language::Arabic => arabic_phoneme_to_ipa(phoneme),
Language::Sanskrit => sanskrit_phoneme_to_ipa(phoneme),
}
}
fn spanish_phoneme_to_ipa(phoneme: Phoneme) -> Option<&'static str> {
match phoneme {
Phoneme::VowelOpenA => Some("a"),
Phoneme::VowelOpenE => Some("e"),
Phoneme::VowelNearI => Some("i"),
Phoneme::VowelO => Some("o"),
Phoneme::VowelCupV => Some("u"),
Phoneme::PlosiveP => Some("p"),
Phoneme::PlosiveB => Some("b"),
Phoneme::PlosiveT => Some("t"),
Phoneme::PlosiveD => Some("d"),
Phoneme::PlosiveK => Some("k"),
Phoneme::PlosiveG => Some("ɡ"),
Phoneme::FricativeF => Some("f"),
Phoneme::FricativeTh => Some("θ"),
Phoneme::FricativeS => Some("s"),
Phoneme::FricativeH => Some("x"), Phoneme::NasalM => Some("m"),
Phoneme::NasalN => Some("n"),
Phoneme::NasalNg => Some("ɲ"), Phoneme::LateralL => Some("l"),
Phoneme::TapFlap => Some("r"), Phoneme::ApproximantR => Some("rr"), Phoneme::ApproximantJ => Some("j"),
Phoneme::ApproximantW => Some("w"),
Phoneme::AffricateCh => Some("t͡ʃ"),
Phoneme::Silence => None,
_ => None,
}
}
fn german_phoneme_to_ipa(phoneme: Phoneme) -> Option<&'static str> {
match phoneme {
Phoneme::VowelNearI => Some("ɪ"),
Phoneme::VowelOpenE => Some("ɛ"),
Phoneme::VowelOpenA => Some("a"),
Phoneme::VowelSchwa => Some("ə"),
Phoneme::VowelE => Some("iː"),
Phoneme::VowelO => Some("oː"),
Phoneme::VowelU => Some("uː"),
Phoneme::VowelAsh => Some("aː"),
Phoneme::PlosiveP => Some("p"),
Phoneme::PlosiveB => Some("b"),
Phoneme::PlosiveT => Some("t"),
Phoneme::PlosiveD => Some("d"),
Phoneme::PlosiveK => Some("k"),
Phoneme::PlosiveG => Some("ɡ"),
Phoneme::FricativeF => Some("f"),
Phoneme::FricativeV => Some("v"),
Phoneme::FricativeS => Some("s"),
Phoneme::FricativeZ => Some("z"),
Phoneme::FricativeSh => Some("ʃ"),
Phoneme::FricativeH => Some("h"),
Phoneme::NasalM => Some("m"),
Phoneme::NasalN => Some("n"),
Phoneme::NasalNg => Some("ŋ"),
Phoneme::AffricateCh => Some("t͡ʃ"),
Phoneme::ApproximantJ => Some("j"),
Phoneme::LateralL => Some("l"),
Phoneme::ApproximantR => Some("ʁ"),
Phoneme::Silence => None,
_ => None,
}
}
fn hindi_phoneme_to_ipa(phoneme: Phoneme) -> Option<&'static str> {
match phoneme {
Phoneme::VowelSchwa => Some("ə"),
Phoneme::VowelNearI => Some("i"),
Phoneme::VowelE => Some("iː"),
Phoneme::VowelCupV => Some("u"),
Phoneme::VowelU => Some("uː"),
Phoneme::VowelOpenE => Some("e"),
Phoneme::VowelO => Some("o"),
Phoneme::VowelOpenA => Some("ɛː"),
Phoneme::VowelOpenO => Some("ɔː"),
Phoneme::PlosiveP => Some("p"),
Phoneme::PlosiveB => Some("b"),
Phoneme::PlosiveT => Some("t̪"),
Phoneme::PlosiveD => Some("d̪"),
Phoneme::PlosiveK => Some("k"),
Phoneme::PlosiveG => Some("ɡ"),
Phoneme::FricativeS => Some("s"),
Phoneme::FricativeH => Some("ɦ"),
Phoneme::FricativeF => Some("f"),
Phoneme::FricativeSh => Some("ɕ"),
Phoneme::NasalM => Some("m"),
Phoneme::NasalN => Some("n"),
Phoneme::NasalNg => Some("ŋ"),
Phoneme::ApproximantJ => Some("j"),
Phoneme::LateralL => Some("l"),
Phoneme::TapFlap => Some("r"),
Phoneme::FricativeV => Some("ʋ"),
Phoneme::AffricateCh => Some("t͡ɕ"),
Phoneme::AffricateJ => Some("d͡ʑ"),
Phoneme::Silence => None,
_ => None,
}
}
fn arabic_phoneme_to_ipa(phoneme: Phoneme) -> Option<&'static str> {
match phoneme {
Phoneme::VowelOpenA => Some("a"),
Phoneme::VowelNearI => Some("i"),
Phoneme::VowelCupV => Some("u"),
Phoneme::PlosiveB => Some("b"),
Phoneme::PlosiveT => Some("t"),
Phoneme::PlosiveD => Some("d"),
Phoneme::PlosiveK => Some("k"),
Phoneme::PlosiveG => Some("ɣ"),
Phoneme::FricativeF => Some("f"),
Phoneme::FricativeTh => Some("θ"),
Phoneme::FricativeDh => Some("ð"),
Phoneme::FricativeS => Some("s"),
Phoneme::FricativeZ => Some("z"),
Phoneme::FricativeSh => Some("ʃ"),
Phoneme::FricativeH => Some("h"),
Phoneme::NasalM => Some("m"),
Phoneme::NasalN => Some("n"),
Phoneme::LateralL => Some("l"),
Phoneme::TapFlap => Some("r"),
Phoneme::ApproximantW => Some("w"),
Phoneme::ApproximantJ => Some("j"),
Phoneme::AffricateJ => Some("d͡ʒ"),
Phoneme::GlottalStop => Some("ʔ"),
Phoneme::Silence => None,
_ => None,
}
}
fn sanskrit_phoneme_to_ipa(phoneme: Phoneme) -> Option<&'static str> {
match phoneme {
Phoneme::VowelSchwa => Some("ɐ"),
Phoneme::VowelOpenA => Some("ɐː"),
Phoneme::VowelNearI => Some("i"),
Phoneme::VowelE => Some("iː"),
Phoneme::VowelCupV => Some("u"),
Phoneme::VowelU => Some("uː"),
Phoneme::VowelOpenE => Some("eː"),
Phoneme::VowelO => Some("oː"),
other => hindi_phoneme_to_ipa(other),
}
}
#[must_use]
pub fn inventory_for(language: Language) -> PhonemeInventory {
match language {
Language::English => varna::phoneme::english(),
Language::Spanish => varna::phoneme::inventories::spanish(),
Language::German => varna::phoneme::inventories::german(),
Language::Hindi => varna::phoneme::inventories::hindi(),
Language::Arabic => varna::phoneme::inventories::classical_arabic(),
Language::Sanskrit => varna::phoneme::sanskrit(),
}
}
#[must_use]
pub fn phonotactics_for(language: Language) -> Option<varna::phoneme::syllable::Phonotactics> {
match language {
Language::English => Some(varna::phoneme::syllable::english_phonotactics()),
Language::Spanish
| Language::German
| Language::Hindi
| Language::Arabic
| Language::Sanskrit => None, }
}
#[must_use]
pub fn validate_phonotactics(phonemes: &[Phoneme], language: Language) -> Vec<String> {
let Some(constraints) = phonotactics_for(language) else {
return Vec::new(); };
let mut violations = Vec::new();
let mut consonant_run = Vec::new();
for &ph in phonemes {
if let Some(ipa) = phoneme_to_ipa(ph) {
let is_vowel = matches!(
ph.class(),
svara::phoneme::PhonemeClass::Vowel | svara::phoneme::PhonemeClass::Diphthong
) || ph == Phoneme::Silence;
if is_vowel {
if consonant_run.len() > constraints.syllable.max_onset as usize {
violations.push(alloc::format!(
"consonant cluster too long for onset: {}",
consonant_run.join("")
));
}
consonant_run.clear();
} else {
consonant_run.push(String::from(ipa));
}
}
}
if consonant_run.len() > constraints.syllable.max_coda as usize {
violations.push(alloc::format!(
"consonant cluster too long for coda: {}",
consonant_run.join("")
));
}
violations
}
#[must_use]
pub fn validate_phonemes_for(
phonemes: &[Phoneme],
inventory: &PhonemeInventory,
language: Language,
) -> Vec<String> {
let mut invalid = Vec::new();
for &ph in phonemes {
if let Some(ipa) = phoneme_to_ipa_for(ph, language)
&& !inventory.has(ipa)
{
invalid.push(String::from(ipa));
}
}
invalid
}
#[must_use]
pub fn validate_phonemes(phonemes: &[Phoneme], inventory: &PhonemeInventory) -> Vec<String> {
validate_phonemes_for(phonemes, inventory, Language::English)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn english_rules_produce_valid_phonemes() {
let inventory = inventory_for(Language::English);
let test_words = [
"cat", "dog", "hello", "world", "knight", "write", "nation", "she", "the", "church",
"think", "sing", "make", "time", "home", "bird", "car", "unhappy", "walked", "wanted",
];
for word in &test_words {
let phonemes = crate::rules::english_rules(word);
let invalid = validate_phonemes(&phonemes, &inventory);
assert!(
invalid.is_empty(),
"word {word:?} produced invalid phonemes: {invalid:?}"
);
}
}
#[test]
fn mapped_phonemes_exist_in_inventory() {
let inventory = inventory_for(Language::English);
let rule_phonemes = [
Phoneme::VowelAsh,
Phoneme::VowelOpenE,
Phoneme::VowelNearI,
Phoneme::VowelO,
Phoneme::VowelCupV,
Phoneme::VowelSchwa,
Phoneme::VowelE,
Phoneme::VowelU,
Phoneme::VowelOpenA,
Phoneme::VowelOpenO,
Phoneme::DiphthongEI,
Phoneme::DiphthongOU,
Phoneme::PlosiveP,
Phoneme::PlosiveB,
Phoneme::PlosiveT,
Phoneme::PlosiveD,
Phoneme::PlosiveK,
Phoneme::PlosiveG,
Phoneme::FricativeF,
Phoneme::FricativeV,
Phoneme::FricativeS,
Phoneme::FricativeZ,
Phoneme::FricativeSh,
Phoneme::FricativeZh,
Phoneme::FricativeTh,
Phoneme::FricativeDh,
Phoneme::FricativeH,
Phoneme::NasalM,
Phoneme::NasalN,
Phoneme::NasalNg,
Phoneme::AffricateCh,
Phoneme::AffricateJ,
Phoneme::ApproximantR,
Phoneme::ApproximantW,
Phoneme::ApproximantJ,
Phoneme::LateralL,
];
for ph in &rule_phonemes {
let ipa = phoneme_to_ipa(*ph).expect("phoneme should have IPA mapping");
assert!(
inventory.has(ipa),
"phoneme {ph:?} (IPA: {ipa:?}) not in varna English inventory"
);
}
}
#[test]
fn inventory_gaps_are_documented() {
for ph in VARNA_INVENTORY_GAPS {
assert!(
phoneme_to_ipa(*ph).is_none(),
"phoneme {ph:?} is listed as a gap but has an IPA mapping"
);
}
}
#[test]
fn silence_is_skipped_in_validation() {
let inventory = inventory_for(Language::English);
let phonemes = [Phoneme::Silence];
let invalid = validate_phonemes(&phonemes, &inventory);
assert!(invalid.is_empty());
}
#[test]
fn serde_roundtrip_language() {
let lang: Language = serde_json::from_str("\"English\"").unwrap();
let inv = inventory_for(lang);
assert!(inv.has("p"));
}
}