455 lines
19 KiB
EmacsLisp
455 lines
19 KiB
EmacsLisp
// morphology-sga.el - Old Irish morphology for the NLG engine.
|
|
//
|
|
// Implements Old Irish verb conjugation, noun declension, and initial consonant
|
|
// lenition for the ca. 600-900 CE period. Designed as a companion to
|
|
// morphology.el and called by the engine when the language profile code is "sga".
|
|
//
|
|
// Language profile: code=sga, name=Old Irish, morph_type=fusional, word_order=VSO,
|
|
// question_strategy=particle, script=latin, family=celtic.
|
|
//
|
|
// Verb conjugation covered:
|
|
// Tenses: present, past
|
|
// Persons: first/second/third x singular/plural (slots 0-5)
|
|
// Forms: absolute (verb-initial position only; conjunct not implemented)
|
|
// Irregulars: bith (to be, substantive), téit (to go), gaibid (to take/hold),
|
|
// ad·cí (to see), as·beir (to say)
|
|
// Copula: "is" — invariant in 3sg; used as the default copular form
|
|
// Canonical map: "be" -> copula "is" (3sg) / "bith" (substantive be)
|
|
//
|
|
// Noun declension covered:
|
|
// o-stem masculine (like "fer" — man)
|
|
// ā-stem feminine (like "ben" — woman)
|
|
// Cases: nominative, vocative, accusative, genitive, dative
|
|
// Numbers: singular, plural
|
|
// Definite article: simplified "in" prefix (article + noun)
|
|
//
|
|
// Lenition (initial mutation):
|
|
// b->bh, c->ch, d->dh, f->fh, g->gh, m->mh, p->ph, s->sh, t->th
|
|
// Other initial consonants and vowels are returned unchanged.
|
|
//
|
|
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with)
|
|
|
|
// ── String helpers ─────────────────────────────────────────────────────────────
|
|
|
|
fn sga_drop(s: String, n: Int) -> String {
|
|
let len: Int = str_len(s)
|
|
if n >= len { return "" }
|
|
return str_slice(s, 0, len - n)
|
|
}
|
|
|
|
fn sga_first(s: String) -> String {
|
|
if str_len(s) == 0 { return "" }
|
|
return str_slice(s, 0, 1)
|
|
}
|
|
|
|
fn sga_rest(s: String) -> String {
|
|
let n: Int = str_len(s)
|
|
if n <= 1 { return "" }
|
|
return str_slice(s, 1, n)
|
|
}
|
|
|
|
// ── Person/number slot ─────────────────────────────────────────────────────────
|
|
//
|
|
// Maps person x number to a 0-based paradigm slot.
|
|
// 0 = 1st singular (mé)
|
|
// 1 = 2nd singular (tú)
|
|
// 2 = 3rd singular (é/sí/ed)
|
|
// 3 = 1st plural (sní)
|
|
// 4 = 2nd plural (sí)
|
|
// 5 = 3rd plural (é/sí/ed)
|
|
|
|
fn sga_slot(person: String, number: String) -> Int {
|
|
if str_eq(person, "first") {
|
|
if str_eq(number, "singular") { return 0 }
|
|
return 3
|
|
}
|
|
if str_eq(person, "second") {
|
|
if str_eq(number, "singular") { return 1 }
|
|
return 4
|
|
}
|
|
// third
|
|
if str_eq(number, "singular") { return 2 }
|
|
return 5
|
|
}
|
|
|
|
// ── Lenition ───────────────────────────────────────────────────────────────────
|
|
//
|
|
// Initial consonant lenition (séimhiú) — the most common mutation in Old Irish.
|
|
// Applies after certain prepositions, the genitive of feminine nouns, and many
|
|
// other grammatical environments. Simplified: only the first consonant is mutated.
|
|
//
|
|
// Lenition table (b c d f g m p s t):
|
|
// b -> bh c -> ch d -> dh f -> fh g -> gh
|
|
// m -> mh p -> ph s -> sh t -> th
|
|
// Vowels and other consonants: unchanged.
|
|
|
|
fn sga_lenite(word: String) -> String {
|
|
let init: String = sga_first(word)
|
|
let tail: String = sga_rest(word)
|
|
if str_eq(init, "b") { return "bh" + tail }
|
|
if str_eq(init, "c") { return "ch" + tail }
|
|
if str_eq(init, "d") { return "dh" + tail }
|
|
if str_eq(init, "f") { return "fh" + tail }
|
|
if str_eq(init, "g") { return "gh" + tail }
|
|
if str_eq(init, "m") { return "mh" + tail }
|
|
if str_eq(init, "p") { return "ph" + tail }
|
|
if str_eq(init, "s") { return "sh" + tail }
|
|
if str_eq(init, "t") { return "th" + tail }
|
|
// Vowels and unlenitable consonants (l, n, r, …): unchanged
|
|
return word
|
|
}
|
|
|
|
// ── Copula paradigm ────────────────────────────────────────────────────────────
|
|
//
|
|
// The Old Irish copula "is" is largely invariant in the present tense in its
|
|
// most common (3rd singular assertive) use. The full paradigm has variants
|
|
// (am, at, is in sg; ammi, adib, it in pl) but for NLG purposes we return "is"
|
|
// for 3sg and the simplified forms elsewhere.
|
|
|
|
fn sga_copula_present(slot: Int) -> String {
|
|
if slot == 0 { return "am" }
|
|
if slot == 1 { return "at" }
|
|
if slot == 2 { return "is" }
|
|
if slot == 3 { return "am" }
|
|
if slot == 4 { return "adib" }
|
|
return "it"
|
|
}
|
|
|
|
// ── Substantive "bith" (to be) ─────────────────────────────────────────────────
|
|
//
|
|
// "Bith" is the substantive/existential verb for being. It differs from the
|
|
// copula "is" in that it can carry tense inflection and express existence rather
|
|
// than predication.
|
|
//
|
|
// Present: am, at, is, am, adib, at
|
|
// Past: ba, ba, ba, bámmar, bádaid, batar
|
|
|
|
fn sga_bith_present(slot: Int) -> String {
|
|
if slot == 0 { return "am" }
|
|
if slot == 1 { return "at" }
|
|
if slot == 2 { return "is" }
|
|
if slot == 3 { return "am" }
|
|
if slot == 4 { return "adib" }
|
|
return "at"
|
|
}
|
|
|
|
fn sga_bith_past(slot: Int) -> String {
|
|
if slot == 0 { return "ba" }
|
|
if slot == 1 { return "ba" }
|
|
if slot == 2 { return "ba" }
|
|
if slot == 3 { return "bámmar" }
|
|
if slot == 4 { return "bádaid" }
|
|
return "batar"
|
|
}
|
|
|
|
// ── Irregular verb tables ──────────────────────────────────────────────────────
|
|
|
|
// téit (to go) — strong verb, highly suppletive in the past
|
|
// Present: tíagu, téit, téit, tíagmai, tíagid, tíagat
|
|
// Past: lod, lod, luid, lodmar, lodaid, lotar
|
|
|
|
fn sga_teit_present(slot: Int) -> String {
|
|
if slot == 0 { return "tíagu" }
|
|
if slot == 1 { return "téit" }
|
|
if slot == 2 { return "téit" }
|
|
if slot == 3 { return "tíagmai" }
|
|
if slot == 4 { return "tíagid" }
|
|
return "tíagat"
|
|
}
|
|
|
|
fn sga_teit_past(slot: Int) -> String {
|
|
if slot == 0 { return "lod" }
|
|
if slot == 1 { return "lod" }
|
|
if slot == 2 { return "luid" }
|
|
if slot == 3 { return "lodmar" }
|
|
if slot == 4 { return "lodaid" }
|
|
return "lotar"
|
|
}
|
|
|
|
// gaibid (to take/hold) — AI class strong verb
|
|
// Present: gaibim, gaibi, gaibid, gaibmi, gaibthe, gaibid
|
|
|
|
fn sga_gaibid_present(slot: Int) -> String {
|
|
if slot == 0 { return "gaibim" }
|
|
if slot == 1 { return "gaibi" }
|
|
if slot == 2 { return "gaibid" }
|
|
if slot == 3 { return "gaibmi" }
|
|
if slot == 4 { return "gaibthe" }
|
|
return "gaibid"
|
|
}
|
|
|
|
// ad·cí (to see) — compound verb with deuterotonic stress
|
|
// Present: ad·ciu, ad·cí, ad·cí, ad·cími, ad·cíthe, ad·ciat
|
|
|
|
fn sga_adci_present(slot: Int) -> String {
|
|
if slot == 0 { return "ad·ciu" }
|
|
if slot == 1 { return "ad·cí" }
|
|
if slot == 2 { return "ad·cí" }
|
|
if slot == 3 { return "ad·cími" }
|
|
if slot == 4 { return "ad·cíthe" }
|
|
return "ad·ciat"
|
|
}
|
|
|
|
// as·beir (to say) — compound verb
|
|
// Present: as·biur, as·beir, as·beir, as·beram, as·berid, as·berat
|
|
|
|
fn sga_asbeir_present(slot: Int) -> String {
|
|
if slot == 0 { return "as·biur" }
|
|
if slot == 1 { return "as·beir" }
|
|
if slot == 2 { return "as·beir" }
|
|
if slot == 3 { return "as·beram" }
|
|
if slot == 4 { return "as·berid" }
|
|
return "as·berat"
|
|
}
|
|
|
|
// ── Canonical verb mapping ─────────────────────────────────────────────────────
|
|
//
|
|
// Maps English semantic labels to Old Irish infinitives / citation forms.
|
|
// Old Irish verbs are cited by 3sg present absolute (the most stable form).
|
|
|
|
fn sga_map_canonical(verb: String) -> String {
|
|
if str_eq(verb, "be") { return "is" }
|
|
if str_eq(verb, "go") { return "téit" }
|
|
if str_eq(verb, "take") { return "gaibid" }
|
|
if str_eq(verb, "hold") { return "gaibid" }
|
|
if str_eq(verb, "see") { return "ad·cí" }
|
|
if str_eq(verb, "say") { return "as·beir" }
|
|
return verb
|
|
}
|
|
|
|
// ── Regular AI-class verb conjugation ─────────────────────────────────────────
|
|
//
|
|
// The AI (first) conjugation is the most productive class in Old Irish.
|
|
// Stems ending in a broad consonant take broad endings; slender take slender
|
|
// endings. For simplicity this module uses a single generic set.
|
|
//
|
|
// Present absolute (verb-initial position):
|
|
// Sg: 1st -aim, 2nd -ai, 3rd -aid
|
|
// Pl: 1st -am, 2nd -aid, 3rd -at
|
|
//
|
|
// There is no regular past tense for AI verbs in the simple sense — the engine
|
|
// falls back to the citation form when past is requested for unknown verbs.
|
|
|
|
fn sga_ai_present(stem: String, slot: Int) -> String {
|
|
if slot == 0 { return stem + "aim" }
|
|
if slot == 1 { return stem + "ai" }
|
|
if slot == 2 { return stem + "aid" }
|
|
if slot == 3 { return stem + "am" }
|
|
if slot == 4 { return stem + "aid" }
|
|
return stem + "at"
|
|
}
|
|
|
|
// ── sga_conjugate: main conjugation entry point ───────────────────────────────
|
|
//
|
|
// verb: Old Irish citation form or English canonical label
|
|
// tense: "present" | "past"
|
|
// person: "first" | "second" | "third"
|
|
// number: "singular" | "plural"
|
|
//
|
|
// Returns the inflected absolute form. Unknown tenses fall back to the citation
|
|
// form. Conjunct forms (after particles) are not implemented; only absolute
|
|
// forms are produced.
|
|
|
|
fn sga_conjugate(verb: String, tense: String, person: String, number: String) -> String {
|
|
let v: String = sga_map_canonical(verb)
|
|
let slot: Int = sga_slot(person, number)
|
|
|
|
// ── Copula "is" ───────────────────────────────────────────────────────────
|
|
if str_eq(v, "is") {
|
|
if str_eq(tense, "present") { return sga_copula_present(slot) }
|
|
// Past copula: "ba" is the standard past (invariant simplification)
|
|
return "ba"
|
|
}
|
|
|
|
// ── Substantive bith ──────────────────────────────────────────────────────
|
|
if str_eq(v, "bith") {
|
|
if str_eq(tense, "present") { return sga_bith_present(slot) }
|
|
if str_eq(tense, "past") { return sga_bith_past(slot) }
|
|
return v
|
|
}
|
|
|
|
// ── téit (to go) ──────────────────────────────────────────────────────────
|
|
if str_eq(v, "téit") {
|
|
if str_eq(tense, "present") { return sga_teit_present(slot) }
|
|
if str_eq(tense, "past") { return sga_teit_past(slot) }
|
|
return v
|
|
}
|
|
|
|
// ── gaibid (to take/hold) ─────────────────────────────────────────────────
|
|
if str_eq(v, "gaibid") {
|
|
if str_eq(tense, "present") { return sga_gaibid_present(slot) }
|
|
// Past gaibid: gab- preterite (simplified to slot 2 form for all)
|
|
return "gab"
|
|
}
|
|
|
|
// ── ad·cí (to see) ────────────────────────────────────────────────────────
|
|
if str_eq(v, "ad·cí") {
|
|
if str_eq(tense, "present") { return sga_adci_present(slot) }
|
|
// Past: ro·acc forms exist but are complex; return citation form
|
|
return v
|
|
}
|
|
|
|
// ── as·beir (to say) ──────────────────────────────────────────────────────
|
|
if str_eq(v, "as·beir") {
|
|
if str_eq(tense, "present") { return sga_asbeir_present(slot) }
|
|
return v
|
|
}
|
|
|
|
// ── Regular AI-class verb ─────────────────────────────────────────────────
|
|
//
|
|
// Citation form for AI verbs ends in -id (3sg pres abs). Strip -id to get
|
|
// the stem, then apply AI endings. If the verb doesn't end in -id, use the
|
|
// whole form as the stem (best-effort).
|
|
if str_ends_with(v, "id") {
|
|
let stem: String = sga_drop(v, 2)
|
|
if str_eq(tense, "present") { return sga_ai_present(stem, slot) }
|
|
// No regular past implemented: return citation form
|
|
return v
|
|
}
|
|
|
|
// Unknown verb: return citation form unchanged
|
|
return v
|
|
}
|
|
|
|
// ── o-stem masculine declension ("fer" — man) ─────────────────────────────────
|
|
//
|
|
// The o-stem is the most common masculine declension class.
|
|
//
|
|
// Singular: nom fer voc a fhir acc fer gen fir dat fiur
|
|
// Plural: nom fir voc a firu acc firu gen fer dat feraib
|
|
|
|
fn sga_decline_ostem(noun: String, gram_case: String, number: String) -> String {
|
|
if str_eq(noun, "fer") {
|
|
if str_eq(number, "singular") {
|
|
if str_eq(gram_case, "nominative") { return "fer" }
|
|
if str_eq(gram_case, "vocative") { return "fhir" }
|
|
if str_eq(gram_case, "accusative") { return "fer" }
|
|
if str_eq(gram_case, "genitive") { return "fir" }
|
|
if str_eq(gram_case, "dative") { return "fiur" }
|
|
return "fer"
|
|
}
|
|
if str_eq(gram_case, "nominative") { return "fir" }
|
|
if str_eq(gram_case, "vocative") { return "firu" }
|
|
if str_eq(gram_case, "accusative") { return "firu" }
|
|
if str_eq(gram_case, "genitive") { return "fer" }
|
|
if str_eq(gram_case, "dative") { return "feraib" }
|
|
return "fir"
|
|
}
|
|
|
|
// Generic o-stem masculine: append case endings to noun base.
|
|
// Nom/Acc sg take no ending; Gen sg syncopates (approximated by +a removed);
|
|
// the engine uses safe suffix-only approximations here.
|
|
if str_eq(number, "singular") {
|
|
if str_eq(gram_case, "nominative") { return noun }
|
|
if str_eq(gram_case, "vocative") { return sga_lenite(noun) }
|
|
if str_eq(gram_case, "accusative") { return noun }
|
|
if str_eq(gram_case, "genitive") { return noun + "a" }
|
|
if str_eq(gram_case, "dative") { return noun + "u" }
|
|
return noun
|
|
}
|
|
if str_eq(gram_case, "nominative") { return noun + "i" }
|
|
if str_eq(gram_case, "vocative") { return noun + "u" }
|
|
if str_eq(gram_case, "accusative") { return noun + "u" }
|
|
if str_eq(gram_case, "genitive") { return noun }
|
|
if str_eq(gram_case, "dative") { return noun + "aib" }
|
|
return noun + "i"
|
|
}
|
|
|
|
// ── ā-stem feminine declension ("ben" — woman) ────────────────────────────────
|
|
//
|
|
// The ā-stem is the most common feminine declension class. It shows heavy
|
|
// syncope and internal change (ben -> mná in oblique forms).
|
|
//
|
|
// Singular: nom ben voc a ben acc bein gen mná dat mnáib
|
|
// Plural: nom mná voc a mná acc mná gen ban dat mnáib
|
|
|
|
fn sga_decline_astem(noun: String, gram_case: String, number: String) -> String {
|
|
if str_eq(noun, "ben") {
|
|
if str_eq(number, "singular") {
|
|
if str_eq(gram_case, "nominative") { return "ben" }
|
|
if str_eq(gram_case, "vocative") { return "ben" }
|
|
if str_eq(gram_case, "accusative") { return "bein" }
|
|
if str_eq(gram_case, "genitive") { return "mná" }
|
|
if str_eq(gram_case, "dative") { return "mnáib" }
|
|
return "ben"
|
|
}
|
|
if str_eq(gram_case, "nominative") { return "mná" }
|
|
if str_eq(gram_case, "vocative") { return "mná" }
|
|
if str_eq(gram_case, "accusative") { return "mná" }
|
|
if str_eq(gram_case, "genitive") { return "ban" }
|
|
if str_eq(gram_case, "dative") { return "mnáib" }
|
|
return "mná"
|
|
}
|
|
|
|
// Generic ā-stem feminine: suffix-based approximation.
|
|
if str_eq(number, "singular") {
|
|
if str_eq(gram_case, "nominative") { return noun }
|
|
if str_eq(gram_case, "vocative") { return noun }
|
|
if str_eq(gram_case, "accusative") { return noun + "i" }
|
|
if str_eq(gram_case, "genitive") { return noun + "e" }
|
|
if str_eq(gram_case, "dative") { return noun + "aib" }
|
|
return noun
|
|
}
|
|
if str_eq(gram_case, "nominative") { return noun + "a" }
|
|
if str_eq(gram_case, "vocative") { return noun + "a" }
|
|
if str_eq(gram_case, "accusative") { return noun + "a" }
|
|
if str_eq(gram_case, "genitive") { return noun }
|
|
if str_eq(gram_case, "dative") { return noun + "aib" }
|
|
return noun + "a"
|
|
}
|
|
|
|
// ── Gender detection heuristic ─────────────────────────────────────────────────
|
|
//
|
|
// Ideally gender is supplied by the lexicon. Known exemplars are routed
|
|
// explicitly; everything else defaults to o-stem masculine.
|
|
|
|
fn sga_detect_gender(noun: String) -> String {
|
|
if str_eq(noun, "ben") { return "feminine" }
|
|
if str_eq(noun, "mná") { return "feminine" }
|
|
// Default: masculine o-stem
|
|
return "masculine"
|
|
}
|
|
|
|
// ── sga_decline: main declension entry point ──────────────────────────────────
|
|
//
|
|
// noun: nominative singular Old Irish noun (e.g. "fer", "ben")
|
|
// gram_case: "nominative" | "vocative" | "accusative" | "genitive" | "dative"
|
|
// number: "singular" | "plural"
|
|
//
|
|
// Returns the inflected form. Falls back to nominative singular on unknown input.
|
|
|
|
fn sga_decline(noun: String, gram_case: String, number: String) -> String {
|
|
let gender: String = sga_detect_gender(noun)
|
|
|
|
if str_eq(gender, "masculine") { return sga_decline_ostem(noun, gram_case, number) }
|
|
if str_eq(gender, "feminine") { return sga_decline_astem(noun, gram_case, number) }
|
|
|
|
// Fallback
|
|
return noun
|
|
}
|
|
|
|
// ── sga_noun_phrase: noun phrase builder ──────────────────────────────────────
|
|
//
|
|
// Old Irish has a definite article that agrees in case, number, and gender.
|
|
// The article has many allomorphs (int, in, ind, na, …). For NLG purposes this
|
|
// module uses the simplified invariant form "in" for all definite contexts,
|
|
// placed before the declined noun.
|
|
//
|
|
// Indefinite nouns carry no article (like Latin).
|
|
//
|
|
// noun: nominative singular Old Irish noun
|
|
// gram_case: "nominative" | "vocative" | "accusative" | "genitive" | "dative"
|
|
// number: "singular" | "plural"
|
|
// definite: "true" | "false"
|
|
//
|
|
// Returns the complete noun phrase string.
|
|
|
|
fn sga_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
|
|
let base: String = sga_decline(noun, gram_case, number)
|
|
|
|
if !str_eq(definite, "true") { return base }
|
|
|
|
// Definite: prepend simplified article "in"
|
|
return "in " + base
|
|
}
|