use serde::Serialize;
use crate::conlang::phonology::validator;
use crate::conlang::types::Phonology;
use crate::conlang::types::morphology::Morphology;
use crate::language_entry::DictionaryEntry;
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct Finding {
pub level: &'static str,
pub message: String,
}
#[derive(Debug, Clone, Default, Serialize, PartialEq)]
pub struct OracleReport {
pub word: String,
pub findings: Vec<Finding>,
}
impl OracleReport {
pub fn ok(&self) -> bool {
self.findings.is_empty()
}
}
pub fn check_word(
phon: &Phonology,
morph: &Morphology,
entries: &[DictionaryEntry],
word: &str,
) -> OracleReport {
let mut r = OracleReport { word: word.to_string(), ..Default::default() };
let seq = phon.segment(&word.to_lowercase());
if seq.is_empty() {
r.findings.push(Finding { level: "phonotactics", message: "empty or unsegmentable".into() });
return r;
}
let unknown: Vec<&str> =
seq.iter().filter(|s| phon.phoneme(s).is_none()).map(|s| s.as_str()).collect();
if !unknown.is_empty() {
r.findings.push(Finding {
level: "phonotactics",
message: format!("segment(s) not in the inventory: {}", unknown.join(" ")),
});
return r;
}
for v in validator::violations(phon, &seq) {
r.findings.push(Finding { level: "phonotactics", message: format!("contains {v}") });
}
let is_listed = entries.iter().any(|e| e.word.eq_ignore_ascii_case(word));
if !is_listed {
let parse = crate::conlang::parse::parse(phon, morph, entries, word);
if parse.parses.is_empty() {
r.findings.push(Finding {
level: "morphology",
message: "no analysis: not a listed root, and no affix stripping reaches one".into(),
});
}
}
r
}
#[cfg(test)]
mod tests {
use super::*;
fn phon() -> Phonology {
let body = r#"{
phonemes: [
{ ipa: "k", kind: "consonant" }, { ipa: "t", kind: "consonant" },
{ ipa: "n", kind: "consonant" }, { ipa: "a", kind: "vowel" }, { ipa: "i", kind: "vowel" }
],
constraints: [ { kind: "no_geminate" } ]
}"#;
Phonology::from_hjson(body).unwrap().unwrap()
}
fn morph() -> Morphology {
Morphology::from_hjson(r#"{ morphemes: [ { id: "pl", gloss: "PL", form: "i", position: "suffix" } ] }"#)
.unwrap()
.unwrap()
}
fn entry(w: &str) -> DictionaryEntry {
DictionaryEntry { word: w.into(), pos: "noun".into(), translation: "x".into(), ..Default::default() }
}
#[test]
fn a_well_formed_listed_word_passes() {
let r = check_word(&phon(), &morph(), &[entry("kata")], "kata");
assert!(r.ok(), "findings: {:?}", r.findings);
}
#[test]
fn a_foreign_segment_is_flagged() {
let r = check_word(&phon(), &morph(), &[entry("kata")], "kabu");
assert!(r.findings.iter().any(|f| f.level == "phonotactics" && f.message.contains("not in the inventory")));
}
#[test]
fn a_constraint_violation_is_flagged() {
let r = check_word(&phon(), &morph(), &[entry("kata")], "katta");
assert!(r.findings.iter().any(|f| f.level == "phonotactics" && f.message.contains("geminate")));
}
#[test]
fn an_unanalyzable_but_legal_word_is_flagged_at_morphology() {
let r = check_word(&phon(), &morph(), &[entry("kata")], "nika");
assert!(r.findings.iter().any(|f| f.level == "morphology"));
}
#[test]
fn a_suffixed_form_of_a_root_passes() {
let r = check_word(&phon(), &morph(), &[entry("kata")], "katai");
assert!(!r.findings.iter().any(|f| f.level == "morphology"), "findings: {:?}", r.findings);
}
}