Files
el/elp/src/morphology-sga.el
2026-05-02 22:15:25 -05:00

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· (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 ()
// 1 = 2nd singular ()
// 2 = 3rd singular (é/sí/ed)
// 3 = 1st plural (sní)
// 4 = 2nd plural ()
// 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· (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· (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
}