use std::collections::BTreeMap;
use crate::conlang::morphology::paradigm;
use crate::conlang::types::morphology::Morphology;
use crate::conlang::Phonology;
#[derive(Debug, Clone)]
pub struct Word {
pub root: String,
pub gloss: String,
}
#[derive(Debug, Clone)]
pub struct NounPhrase {
pub head: Word,
pub number: String,
pub adjective: Option<Word>,
}
#[derive(Debug, Clone, Default)]
pub struct Clause {
pub subject: Option<NounPhrase>,
pub verb: Option<Word>,
pub verb_person: String,
pub object: Option<NounPhrase>,
pub noun_paradigm: String,
pub verb_paradigm: String,
pub negated: bool,
pub negator: Option<Word>,
pub question: bool,
pub question_particle: Option<Word>,
}
#[derive(Debug, Clone)]
pub struct RenderedClause {
pub words: Vec<(String, String)>,
pub surface: String,
pub literal: String,
}
#[derive(Clone, Copy, PartialEq)]
enum Role {
Subject,
Verb,
Object,
}
pub fn assemble(
phon: &Phonology,
morph: &Morphology,
typology: &BTreeMap<String, String>,
clause: &Clause,
) -> RenderedClause {
let transitive = clause.object.is_some();
let alignment = typology.get("alignment").map(String::as_str).unwrap_or("nominative_accusative");
let order = typology.get("word_order").map(String::as_str).unwrap_or("svo");
let adj_before = typology
.get("adjective_order")
.map(|v| !v.to_lowercase().contains("noun_adjective"))
.unwrap_or(true);
let (subj_case, obj_case) = case_roles(alignment, transitive);
let render_np = |np: &NounPhrase, case: Option<&str>| -> Vec<(String, String)> {
let mut feats: BTreeMap<String, String> = BTreeMap::new();
if !np.number.is_empty() {
feats.insert("number".into(), np.number.clone());
}
if let Some(c) = case {
feats.insert("case".into(), c.to_string());
}
let noun = inflect(phon, morph, &clause.noun_paradigm, &np.head, &feats);
let mut out = Vec::new();
if let Some(adj) = &np.adjective {
let adj_paradigm = morph
.agreement_for("adjective")
.map(|r| r.paradigm.clone())
.unwrap_or_else(|| clause.noun_paradigm.clone());
let a = inflect(phon, morph, &adj_paradigm, adj, &feats);
if adj_before {
out.push(a);
out.push(noun);
} else {
out.push(noun);
out.push(a);
}
} else {
out.push(noun);
}
out
};
let subject = clause.subject.as_ref().map(|np| render_np(np, subj_case.as_deref()));
let object = clause.object.as_ref().map(|np| render_np(np, obj_case.as_deref()));
let neg_strategy = typology.get("negation").map(String::as_str).unwrap_or("particle");
let q_strategy = typology.get("question").map(String::as_str).unwrap_or("particle");
let verb = clause.verb.as_ref().map(|v| {
let mut vw = if let (Some(rule), Some(subj)) = (morph.agreement_for("verb"), &clause.subject)
{
let mut head: BTreeMap<String, String> = BTreeMap::new();
head.insert("person".into(), clause.verb_person.clone());
head.insert("number".into(), subj.number.clone());
crate::conlang::morphology::agreement::agree(phon, morph, rule, &v.root, &v.gloss, &head)
.map(|a| vec![(a.form, a.gloss)])
.unwrap_or_else(|| vec![inflect(phon, morph, &clause.verb_paradigm, v, &BTreeMap::new())])
} else {
vec![inflect(phon, morph, &clause.verb_paradigm, v, &BTreeMap::new())]
};
if clause.negated {
apply_negation(&mut vw, neg_strategy, clause.negator.as_ref());
}
if clause.question && q_strategy == "morphology" {
if let Some(first) = vw.first_mut() {
first.1 = format!("{}.Q", first.1);
}
}
vw
});
let invert = clause.question && q_strategy == "word_order";
let order_roles = if invert { front_verb(order) } else { word_order(order) };
let mut words: Vec<(String, String)> = Vec::new();
for role in order_roles {
let part = match role {
Role::Subject => &subject,
Role::Verb => &verb,
Role::Object => &object,
};
if let Some(ws) = part {
words.extend(ws.iter().cloned());
}
}
if clause.question && q_strategy == "particle" {
if let Some(qp) = &clause.question_particle {
words.push((qp.root.clone(), "Q".into()));
}
}
let mut surface = words.iter().map(|(w, _)| w.as_str()).collect::<Vec<_>>().join(" ");
if clause.question {
surface.push('?');
}
let literal = literal_english(clause);
RenderedClause { words, surface, literal }
}
#[derive(Debug, Clone)]
pub struct RelativeClause {
pub head_is_subject: bool,
pub verb: Word,
pub other: Option<Word>,
pub relativizer: Option<Word>,
pub noun_paradigm: String,
pub verb_paradigm: String,
}
pub fn relative_np(
phon: &Phonology,
morph: &Morphology,
typology: &BTreeMap<String, String>,
head: &Word,
rc: &RelativeClause,
) -> RenderedClause {
let other_np = rc.other.as_ref().map(|w| NounPhrase {
head: w.clone(),
number: "sg".into(),
adjective: None,
});
let clause = if rc.head_is_subject {
Clause {
subject: None, verb: Some(rc.verb.clone()),
verb_person: "3".into(),
object: other_np,
noun_paradigm: rc.noun_paradigm.clone(),
verb_paradigm: rc.verb_paradigm.clone(),
..Default::default()
}
} else {
Clause {
subject: other_np,
verb: Some(rc.verb.clone()),
verb_person: "3".into(),
object: None, noun_paradigm: rc.noun_paradigm.clone(),
verb_paradigm: rc.verb_paradigm.clone(),
..Default::default()
}
};
let embedded = assemble(phon, morph, typology, &clause);
let head_word = (head.root.clone(), head.gloss.clone());
let rel_word = rc
.relativizer
.as_ref()
.map(|w| (w.root.clone(), "REL".to_string()));
let prenominal = typology
.get("relative_clause")
.map(|s| s.eq_ignore_ascii_case("prenominal"))
.unwrap_or(false);
let mut words: Vec<(String, String)> = Vec::new();
if prenominal {
words.extend(embedded.words.iter().cloned());
if let Some(r) = &rel_word {
words.push(r.clone());
}
words.push(head_word);
} else {
words.push(head_word);
if let Some(r) = &rel_word {
words.push(r.clone());
}
words.extend(embedded.words.iter().cloned());
}
let surface = words.iter().map(|(w, _)| w.as_str()).collect::<Vec<_>>().join(" ");
let literal = format!("the {} that {}", head.gloss, embedded.literal);
RenderedClause {
words,
surface,
literal,
}
}
pub fn coordinate(conjuncts: &[RenderedClause], conjunction: Option<&Word>) -> RenderedClause {
let conj_gloss = conjunction.map(|w| {
if w.gloss.trim().is_empty() {
"CONJ".to_string()
} else {
w.gloss.clone()
}
});
let mut words: Vec<(String, String)> = Vec::new();
let mut literals: Vec<String> = Vec::new();
for (i, c) in conjuncts.iter().enumerate() {
if i > 0 {
if let (Some(conj), Some(g)) = (conjunction, &conj_gloss) {
words.push((conj.root.clone(), g.clone()));
}
}
words.extend(c.words.iter().cloned());
literals.push(c.literal.clone());
}
let surface = words.iter().map(|(w, _)| w.as_str()).collect::<Vec<_>>().join(" ");
let connector = conj_gloss.as_deref().unwrap_or("and");
let literal = literals.join(&format!(" {connector} "));
RenderedClause {
words,
surface,
literal,
}
}
pub fn bare_np(word: &Word) -> RenderedClause {
RenderedClause {
words: vec![(word.root.clone(), word.gloss.clone())],
surface: word.root.clone(),
literal: word.gloss.clone(),
}
}
fn apply_negation(verb: &mut Vec<(String, String)>, strategy: &str, negator: Option<&Word>) {
match strategy {
"affix" => match (verb.first_mut(), negator) {
(Some(first), Some(neg)) => {
first.0 = format!("{}{}", neg.root, first.0);
first.1 = format!("NEG-{}", first.1);
}
(Some(first), None) => first.1 = format!("{}.NEG", first.1),
(None, _) => {}
},
_ => match negator {
Some(neg) => verb.insert(0, (neg.root.clone(), "NEG".into())),
None => {
if let Some(first) = verb.first_mut() {
first.1 = format!("NEG {}", first.1);
}
}
},
}
}
fn front_verb(code: &str) -> Vec<Role> {
let mut out = vec![Role::Verb];
for r in word_order(code) {
if r != Role::Verb {
out.push(r);
}
}
out
}
fn inflect(
phon: &Phonology,
morph: &Morphology,
paradigm_name: &str,
word: &Word,
wanted: &BTreeMap<String, String>,
) -> (String, String) {
let bare = || (word.root.clone(), word.gloss.clone());
let Some(template) = morph.paradigm(paradigm_name) else {
return bare();
};
let attempts = relax(wanted);
for w in &attempts {
if let Some(row) = paradigm::realize_features(phon, morph, template, &word.root, &word.gloss, w) {
return (row.form, row.gloss);
}
}
bare()
}
fn relax(wanted: &BTreeMap<String, String>) -> Vec<BTreeMap<String, String>> {
let mut out = Vec::new();
let number = wanted.get("number");
let case_spellings: Vec<&str> = wanted
.get("case")
.map(|c| case_spellings(c))
.unwrap_or_default();
for sp in &case_spellings {
let mut w = BTreeMap::new();
if let Some(n) = number {
w.insert("number".to_string(), n.clone());
}
w.insert("case".to_string(), sp.to_string());
out.push(w);
}
for sp in &case_spellings {
out.push([("case".to_string(), sp.to_string())].into_iter().collect());
}
if let Some(n) = number {
out.push([("number".to_string(), n.clone())].into_iter().collect());
}
out.push(wanted.clone());
out
}
fn case_spellings(case: &str) -> Vec<&'static str> {
match case.to_lowercase().as_str() {
"nom" | "nominative" => vec!["nom", "nominative"],
"acc" | "accusative" => vec!["acc", "accusative"],
"erg" | "ergative" => vec!["erg", "ergative"],
"abs" | "absolutive" => vec!["abs", "absolutive"],
"dat" | "dative" => vec!["dat", "dative"],
"gen" | "genitive" => vec!["gen", "genitive"],
_ => vec![],
}
}
fn case_roles(alignment: &str, transitive: bool) -> (Option<String>, Option<String>) {
if alignment.to_lowercase().contains("ergative") {
if transitive {
(Some("erg".into()), Some("abs".into()))
} else {
(Some("abs".into()), None)
}
} else {
(Some("nom".into()), transitive.then(|| "acc".into()))
}
}
fn word_order(code: &str) -> Vec<Role> {
code.to_lowercase()
.chars()
.filter_map(|c| match c {
's' => Some(Role::Subject),
'v' => Some(Role::Verb),
'o' => Some(Role::Object),
_ => None,
})
.collect::<Vec<_>>()
.into_iter()
.fold(Vec::new(), |mut acc, r| {
if !acc.contains(&r) {
acc.push(r);
}
acc
})
}
fn literal_english(clause: &Clause) -> String {
let np = |np: &NounPhrase| -> String {
match &np.adjective {
Some(a) => format!("{} {}", a.gloss, np.head.gloss),
None => np.head.gloss.clone(),
}
};
let mut parts = Vec::new();
if let Some(s) = &clause.subject {
parts.push(np(s));
}
if let Some(v) = &clause.verb {
if clause.negated {
parts.push(format!("does not {}", v.gloss));
} else {
parts.push(v.gloss.clone());
}
}
if let Some(o) = &clause.object {
parts.push(np(o));
}
let mut s = parts.join(" ");
if clause.question {
s.push('?');
}
s
}
#[cfg(test)]
mod tests {
use super::*;
fn phon() -> Phonology {
let body = r#"{ phonemes: [
{ ipa: "k", kind: "consonant" }, { ipa: "t", kind: "consonant" },
{ ipa: "m", kind: "consonant" }, { ipa: "n", kind: "consonant" },
{ ipa: "r", kind: "consonant" }, { ipa: "s", kind: "consonant" },
{ ipa: "p", kind: "consonant" }, { ipa: "l", kind: "consonant" },
{ ipa: "a", kind: "vowel" }, { ipa: "i", kind: "vowel" }, { ipa: "u", kind: "vowel" }
] }"#;
Phonology::from_hjson(body).unwrap().unwrap()
}
fn morph() -> Morphology {
let body = r#"{
morphemes: [
{ id: "acc", gloss: "ACC", form: "n", position: "suffix", category: "case", value: "accusative" }
]
paradigms: [ { name: "noun", cells: [
{ features: { case: "nom" }, morphemes: [] }
{ features: { case: "acc" }, morphemes: ["acc"] }
] } ]
agreement: [
{ dependent: "adjective", head: "noun", features: ["case"], paradigm: "noun" }
]
}"#;
Morphology::from_hjson(body).unwrap().unwrap()
}
fn clause() -> Clause {
Clause {
subject: Some(NounPhrase { head: Word { root: "kira".into(), gloss: "bird".into() }, number: "sg".into(), adjective: None }),
verb: Some(Word { root: "nami".into(), gloss: "see".into() }),
verb_person: "3".into(),
object: Some(NounPhrase { head: Word { root: "pata".into(), gloss: "stone".into() }, number: "sg".into(), adjective: None }),
noun_paradigm: "noun".into(),
verb_paradigm: "verb".into(),
..Default::default()
}
}
#[test]
fn sov_clause_orders_and_case_marks() {
let mut t = BTreeMap::new();
t.insert("word_order".to_string(), "sov".to_string());
t.insert("alignment".to_string(), "nominative_accusative".to_string());
let r = assemble(&phon(), &morph(), &t, &clause());
assert_eq!(r.surface, "kira patan nami");
assert_eq!(r.words[1].1, "stone-ACC");
assert_eq!(r.literal, "bird see stone");
}
#[test]
fn svo_reorders_the_verb() {
let mut t = BTreeMap::new();
t.insert("word_order".to_string(), "svo".to_string());
let r = assemble(&phon(), &morph(), &t, &clause());
assert_eq!(r.surface, "kira nami patan"); }
#[test]
fn adjective_agrees_in_case() {
let mut c = clause();
c.object.as_mut().unwrap().adjective = Some(Word { root: "mira".into(), gloss: "bright".into() });
let mut t = BTreeMap::new();
t.insert("word_order".to_string(), "svo".to_string());
let r = assemble(&phon(), &morph(), &t, &c);
assert!(r.surface.contains("miran patan"), "got: {}", r.surface);
}
#[test]
fn negation_particle_sits_before_the_verb() {
let mut c = clause();
c.negated = true;
c.negator = Some(Word { root: "na".into(), gloss: "not".into() });
let mut t = BTreeMap::new();
t.insert("word_order".to_string(), "svo".to_string());
t.insert("negation".to_string(), "particle".to_string());
let r = assemble(&phon(), &morph(), &t, &c);
assert_eq!(r.surface, "kira na nami patan");
assert!(r.words.iter().any(|(_, g)| g == "NEG"));
assert!(r.literal.contains("does not see"));
}
#[test]
fn negation_affix_fuses_onto_the_verb() {
let mut c = clause();
c.negated = true;
c.negator = Some(Word { root: "na".into(), gloss: "not".into() });
let mut t = BTreeMap::new();
t.insert("word_order".to_string(), "svo".to_string());
t.insert("negation".to_string(), "affix".to_string());
let r = assemble(&phon(), &morph(), &t, &c);
assert_eq!(r.surface, "kira nanami patan");
assert!(r.words.iter().any(|(f, g)| f == "nanami" && g == "NEG-see"));
}
#[test]
fn negation_without_a_form_marks_only_the_gloss() {
let mut c = clause();
c.negated = true; let mut t = BTreeMap::new();
t.insert("word_order".to_string(), "svo".to_string());
let r = assemble(&phon(), &morph(), &t, &c);
assert_eq!(r.surface, "kira nami patan");
assert!(r.words.iter().any(|(_, g)| g.contains("NEG")));
}
#[test]
fn question_particle_lands_at_the_clause_edge() {
let mut c = clause();
c.question = true;
c.question_particle = Some(Word { root: "ka".into(), gloss: "Q".into() });
let mut t = BTreeMap::new();
t.insert("word_order".to_string(), "sov".to_string());
t.insert("question".to_string(), "particle".to_string());
let r = assemble(&phon(), &morph(), &t, &c);
assert_eq!(r.surface, "kira patan nami ka?");
assert_eq!(r.words.last().unwrap().1, "Q");
}
#[test]
fn question_word_order_fronts_the_verb() {
let mut c = clause();
c.question = true;
let mut t = BTreeMap::new();
t.insert("word_order".to_string(), "svo".to_string());
t.insert("question".to_string(), "word_order".to_string());
let r = assemble(&phon(), &morph(), &t, &c);
assert_eq!(r.surface, "nami kira patan?");
}
#[test]
fn relative_clause_subject_gap_postnominal() {
let mut t = BTreeMap::new();
t.insert("word_order".to_string(), "svo".to_string());
t.insert("relative_clause".to_string(), "postnominal".to_string());
let rc = RelativeClause {
head_is_subject: true,
verb: Word { root: "nami".into(), gloss: "see".into() },
other: Some(Word { root: "pata".into(), gloss: "stone".into() }),
relativizer: Some(Word { root: "ya".into(), gloss: "that".into() }),
noun_paradigm: "noun".into(),
verb_paradigm: "verb".into(),
};
let head = Word { root: "kira".into(), gloss: "bird".into() };
let r = relative_np(&phon(), &morph(), &t, &head, &rc);
assert_eq!(r.surface, "kira ya nami patan");
assert!(r.words.iter().any(|(_, g)| g == "REL"));
assert!(r.words.iter().any(|(_, g)| g == "stone-ACC"));
assert_eq!(r.literal, "the bird that see stone");
}
#[test]
fn relative_clause_object_gap_prenominal() {
let mut t = BTreeMap::new();
t.insert("word_order".to_string(), "svo".to_string());
t.insert("relative_clause".to_string(), "prenominal".to_string());
let rc = RelativeClause {
head_is_subject: false,
verb: Word { root: "nami".into(), gloss: "see".into() },
other: Some(Word { root: "kira".into(), gloss: "bird".into() }),
relativizer: None,
noun_paradigm: "noun".into(),
verb_paradigm: "verb".into(),
};
let head = Word { root: "pata".into(), gloss: "stone".into() };
let r = relative_np(&phon(), &morph(), &t, &head, &rc);
assert_eq!(r.surface, "kira nami pata");
assert_eq!(r.words.last().unwrap().0, "pata"); assert_eq!(r.literal, "the stone that bird see");
}
#[test]
fn coordinate_two_clauses_with_a_conjunction() {
let mut t = BTreeMap::new();
t.insert("word_order".to_string(), "svo".to_string());
let c1 = assemble(&phon(), &morph(), &t, &clause()); let mut c2c = clause();
c2c.subject = Some(NounPhrase {
head: Word { root: "muru".into(), gloss: "river".into() },
number: "sg".into(),
adjective: None,
});
c2c.object = None; c2c.verb = Some(Word { root: "tasa".into(), gloss: "fall".into() });
let c2 = assemble(&phon(), &morph(), &t, &c2c); let conj = Word { root: "na".into(), gloss: "and".into() };
let r = coordinate(&[c1, c2], Some(&conj));
assert_eq!(r.surface, "kira nami patan na muru tasa");
assert!(r.words.iter().any(|(f, g)| f == "na" && g == "and"));
assert_eq!(r.literal, "bird see stone and river fall");
}
#[test]
fn coordinate_noun_phrases() {
let bird = bare_np(&Word { root: "kira".into(), gloss: "bird".into() });
let stone = bare_np(&Word { root: "pata".into(), gloss: "stone".into() });
let conj = Word { root: "na".into(), gloss: "and".into() };
let r = coordinate(&[bird, stone], Some(&conj));
assert_eq!(r.surface, "kira na pata");
assert_eq!(r.literal, "bird and stone");
}
#[test]
fn ergative_alignment_marks_the_subject() {
let body = r#"{
morphemes: [ { id: "erg", gloss: "ERG", form: "k", position: "suffix", category: "case" } ]
paradigms: [ { name: "noun", cells: [
{ features: { case: "abs" }, morphemes: [] }
{ features: { case: "erg" }, morphemes: ["erg"] }
] } ]
}"#;
let m = Morphology::from_hjson(body).unwrap().unwrap();
let mut t = BTreeMap::new();
t.insert("alignment".to_string(), "ergative_absolutive".to_string());
t.insert("word_order".to_string(), "sov".to_string());
let r = assemble(&phon(), &m, &t, &clause());
assert_eq!(r.words[0].1, "bird-ERG");
assert_eq!(r.words[1].0, "pata"); }
}