Rename: nlg → elp (Engram Language Protocol)

This commit is contained in:
Will Anderson
2026-05-02 22:15:25 -05:00
parent cbb27b8d87
commit 34725a3988
125 changed files with 129214 additions and 780 deletions
+679
View File
@@ -0,0 +1,679 @@
// morphology-got.el - Gothic morphology for the NLG engine.
//
// Implements Gothic verb conjugation and noun declension using standard
// Gothic Latin romanisation. Designed as a companion to morphology.el and
// called by the engine when the language profile code is "got".
//
// Language profile: code=got, name=Gothic, morph_type=fusional,
// word_order=SOV, question_strategy=intonation, script=gothic-latin,
// family=germanic.
//
// Historical note: Gothic is attested primarily in the 4th-century Bible
// translation by Bishop Wulfila (Ulfilas). It is the earliest substantially
// attested Germanic language and preserves many archaic features lost in later
// branches (e.g. distinct dual, mediopassive voice, four-case system).
//
// Romanisation conventions used in this file:
// þ thorn (voiced/voiceless dental fricative; like "th" in "the/thin")
// ƕ hwair (Gothic hw-, like "wh" in older English "what")
// q Gothic q (labiovelar stop, like "qu")
// ei long /iː/ (Gothic digraph)
// ai short /ɛ/ (Gothic digraph, not a diphthong)
// au short /ɔ/ (Gothic digraph, not a diphthong)
// Standard vowels: a e i u (short)
//
// Verb conjugation covered:
// Tenses: present indicative active, past indicative active
// Persons: first/second/third × singular/plural
// Classes: weak class 1 (-jan verbs), weak class 2 (-on verbs) as
// the regular productive paths
// Irregulars: wisan (be), haban (have), gaggan (go), saihwan (see),
// qiþan (say), niman (take)
// Canonical map: "be" -> "wisan"
//
// Noun declension covered:
// Cases: nominative, accusative, genitive, dative (all 4 Gothic cases)
// Stem types: a-stem masc (paradigm: dags day),
// o-stem fem (paradigm: gibo gift),
// n-stem masc (paradigm: guma man)
// Numbers: singular, plural
//
// Demonstrative / article:
// Gothic has no definite article proper. The demonstrative pronoun
// sa (masc) / so (fem) / þata (neut) functions as a near-definite
// determiner. got_noun_phrase prepends "sa" (masc) or "þo" (fem) when
// definite=true. Gender must be inferred from stem class: a-stem masc,
// o-stem fem, n-stem masc.
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with)
// String helpers
fn got_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn got_str_drop_last(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len {
return ""
}
return str_slice(s, 0, len - n)
}
// Person/number slot
//
// Gothic has singular, dual, and plural. The dual is historically attested
// for first and second person only (e.g. 1du wit, 2du jut) but is quite rare
// even in the Gothic corpus. For NLG simplicity the dual is treated as plural.
//
// 0 = 1st singular
// 1 = 2nd singular
// 2 = 3rd singular
// 3 = 1st plural
// 4 = 2nd plural
// 5 = 3rd plural
fn got_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 person
if str_eq(number, "singular") { return 2 }
return 5
}
// Canonical verb mapping
//
// English semantic-layer labels are mapped to Gothic dictionary forms before
// conjugation. Dictionary forms are the infinitive.
fn got_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "wisan" }
if str_eq(verb, "have") { return "haban" }
if str_eq(verb, "go") { return "gaggan" }
if str_eq(verb, "see") { return "saihwan" }
if str_eq(verb, "say") { return "qiþan" }
if str_eq(verb, "take") { return "niman" }
if str_eq(verb, "come") { return "qiman" }
if str_eq(verb, "give") { return "giban" }
if str_eq(verb, "know") { return "kunnan" }
if str_eq(verb, "want") { return "wiljan" }
return verb
}
// Irregular verb: wisan (to be)
//
// wisan is suppletive the present is formed from im / is / ist / sijum
// and the past from was / wast / was / wesum
//
// Present indicative active:
// 1sg im 2sg is 3sg ist
// 1pl sijum 2pl sijuþ 3pl sind
//
// Past indicative active:
// 1sg was 2sg wast 3sg was
// 1pl wesum 2pl wesuþ 3pl wesun
fn got_wisan_present(slot: Int) -> String {
if slot == 0 { return "im" }
if slot == 1 { return "is" }
if slot == 2 { return "ist" }
if slot == 3 { return "sijum" }
if slot == 4 { return "sijuþ" }
return "sind"
}
fn got_wisan_past(slot: Int) -> String {
if slot == 0 { return "was" }
if slot == 1 { return "wast" }
if slot == 2 { return "was" }
if slot == 3 { return "wesum" }
if slot == 4 { return "wesuþ" }
return "wesun"
}
// Irregular verb: haban (to have)
//
// Weak class 3 (-an preterite-present type).
//
// Present:
// 1sg haba 2sg habais 3sg habaiþ
// 1pl habam 2pl habaiþ 3pl haband
//
// Past (habida series weak past in -ida):
// 1sg habida 2sg habides 3sg habida
// 1pl habidum 2pl habideþ 3pl habidedun
fn got_haban_present(slot: Int) -> String {
if slot == 0 { return "haba" }
if slot == 1 { return "habais" }
if slot == 2 { return "habaiþ" }
if slot == 3 { return "habam" }
if slot == 4 { return "habaiþ" }
return "haband"
}
fn got_haban_past(slot: Int) -> String {
if slot == 0 { return "habida" }
if slot == 1 { return "habides" }
if slot == 2 { return "habida" }
if slot == 3 { return "habidum" }
if slot == 4 { return "habideþ" }
return "habidedun"
}
// Irregular verb: gaggan (to go)
//
// Strong class 7 (reduplicating); suppletive in past with iddja- forms.
//
// Present:
// 1sg gagga 2sg gaggis 3sg gaggiþ
// 1pl gagam 2pl gagiþ 3pl gaggand
//
// Past (iddja series suppletive):
// 1sg iddja 2sg iddjēs 3sg iddja
// 1pl iddjēdum 2pl iddjēduþ 3pl iddjēdun
fn got_gaggan_present(slot: Int) -> String {
if slot == 0 { return "gagga" }
if slot == 1 { return "gaggis" }
if slot == 2 { return "gaggiþ" }
if slot == 3 { return "gagam" }
if slot == 4 { return "gagiþ" }
return "gaggand"
}
fn got_gaggan_past(slot: Int) -> String {
if slot == 0 { return "iddja" }
if slot == 1 { return "iddjēs" }
if slot == 2 { return "iddja" }
if slot == 3 { return "iddjēdum" }
if slot == 4 { return "iddjēduþ" }
return "iddjēdun"
}
// Irregular verb: saihwan (to see)
//
// Strong class 5 (saiƕan / saihwan). Present stem saihw-; past stem sahw-.
//
// Present:
// 1sg saihwa 2sg saihwis 3sg saihwiþ
// 1pl saihwam 2pl saihwiþ 3pl saihwand
//
// Past (ablaut: aia, strong past):
// 1sg sahw 2sg sahwt 3sg sahw
// 1pl sehwum 2pl sehwuþ 3pl sehwun
fn got_saihwan_present(slot: Int) -> String {
if slot == 0 { return "saihwa" }
if slot == 1 { return "saihwis" }
if slot == 2 { return "saihwiþ" }
if slot == 3 { return "saihwam" }
if slot == 4 { return "saihwiþ" }
return "saihwand"
}
fn got_saihwan_past(slot: Int) -> String {
if slot == 0 { return "sahw" }
if slot == 1 { return "sahwt" }
if slot == 2 { return "sahw" }
if slot == 3 { return "sehwum" }
if slot == 4 { return "sehwuþ" }
return "sehwun"
}
// Irregular verb: qiþan (to say)
//
// Strong class 5 (i-ablaut). Present stem qiþ-; past stem qaþ-.
//
// Present:
// 1sg qiþa 2sg qiþis 3sg qiþiþ
// 1pl qiþam 2pl qiþiþ 3pl qiþand
//
// Past (ablaut: ia):
// 1sg qaþ 2sg qast 3sg qaþ
// 1pl qēþum 2pl qēþuþ 3pl qēþun
fn got_qithan_present(slot: Int) -> String {
if slot == 0 { return "qiþa" }
if slot == 1 { return "qiþis" }
if slot == 2 { return "qiþiþ" }
if slot == 3 { return "qiþam" }
if slot == 4 { return "qiþiþ" }
return "qiþand"
}
fn got_qithan_past(slot: Int) -> String {
if slot == 0 { return "qaþ" }
if slot == 1 { return "qast" }
if slot == 2 { return "qaþ" }
if slot == 3 { return "qēþum" }
if slot == 4 { return "qēþuþ" }
return "qēþun"
}
// Irregular verb: niman (to take)
//
// Strong class 4 (sonorant stem; i-ablaut in present, a-ablaut in past).
//
// Present:
// 1sg nima 2sg nimis 3sg nimiþ
// 1pl nimam 2pl nimiþ 3pl nimand
//
// Past (ablaut: ia):
// 1sg nam 2sg namt 3sg nam
// 1pl nēmum 2pl nēmuþ 3pl nēmun
fn got_niman_present(slot: Int) -> String {
if slot == 0 { return "nima" }
if slot == 1 { return "nimis" }
if slot == 2 { return "nimiþ" }
if slot == 3 { return "nimam" }
if slot == 4 { return "nimiþ" }
return "nimand"
}
fn got_niman_past(slot: Int) -> String {
if slot == 0 { return "nam" }
if slot == 1 { return "namt" }
if slot == 2 { return "nam" }
if slot == 3 { return "nēmum" }
if slot == 4 { return "nēmuþ" }
return "nēmun"
}
// Weak class 1 (-jan verbs): regular conjugation
//
// Class 1 weak verbs are the most productive Gothic verb class. The
// infinitive ends in -jan; the stem is obtained by stripping -jan.
//
// Present indicative active:
// 1sg stem + -a (nasjan -> nasja)
// 2sg stem + -is (nasjis)
// 3sg stem + -iþ (nasjiþ)
// 1pl stem + -jam (nasjam)
// 2pl stem + -jiþ (nasjiþ)
// 3pl stem + -jand (nasjand)
//
// Past indicative active (weak past -ida):
// 1sg stem + -ida (nasjida)
// 2sg stem + -ides (nasjides)
// 3sg stem + -ida (nasjida)
// 1pl stem + -idum (nasjidum)
// 2pl stem + -ideþ (nasjideþ)
// 3pl stem + -idedun (nasjidedun)
fn got_wk1_present_ending(slot: Int) -> String {
if slot == 0 { return "a" }
if slot == 1 { return "is" }
if slot == 2 { return "" }
if slot == 3 { return "jam" }
if slot == 4 { return "jiþ" }
return "jand"
}
fn got_wk1_past_ending(slot: Int) -> String {
if slot == 0 { return "ida" }
if slot == 1 { return "ides" }
if slot == 2 { return "ida" }
if slot == 3 { return "idum" }
if slot == 4 { return "ideþ" }
return "idedun"
}
fn got_wk1_conjugate(stem: String, tense: String, slot: Int) -> String {
if str_eq(tense, "present") {
return stem + got_wk1_present_ending(slot)
}
if str_eq(tense, "past") {
return stem + got_wk1_past_ending(slot)
}
return stem
}
// Weak class 2 (-on verbs): regular conjugation
//
// Class 2 weak verbs. Infinitive ends in -on; stem = drop -on.
// This class corresponds roughly to OE -ian / OHG -on denominatives.
//
// Present indicative active:
// 1sg stem + -o (salbon -> salbo)
// 2sg stem + -os (salbos)
// 3sg stem + -oþ (salboþ)
// 1pl stem + -om (salbom)
// 2pl stem + -oþ (salboþ)
// 3pl stem + -ond (salbond)
//
// Past indicative active (weak past -oda):
// 1sg stem + -oda (salboda)
// 2sg stem + -odes (salbodes)
// 3sg stem + -oda (salboda)
// 1pl stem + -odum (salbodum)
// 2pl stem + -odeþ (salbodeþ)
// 3pl stem + -odedun (salbodedun)
fn got_wk2_present_ending(slot: Int) -> String {
if slot == 0 { return "o" }
if slot == 1 { return "os" }
if slot == 2 { return "" }
if slot == 3 { return "om" }
if slot == 4 { return "" }
return "ond"
}
fn got_wk2_past_ending(slot: Int) -> String {
if slot == 0 { return "oda" }
if slot == 1 { return "odes" }
if slot == 2 { return "oda" }
if slot == 3 { return "odum" }
if slot == 4 { return "odeþ" }
return "odedun"
}
fn got_wk2_conjugate(stem: String, tense: String, slot: Int) -> String {
if str_eq(tense, "present") {
return stem + got_wk2_present_ending(slot)
}
if str_eq(tense, "past") {
return stem + got_wk2_past_ending(slot)
}
return stem
}
// Infinitive class detection
//
// Identifies the verb class from the infinitive ending so that the correct
// regular paradigm can be applied for verbs not in the irregular table.
//
// ends in -jan -> weak class 1 ("wk1")
// ends in -on -> weak class 2 ("wk2")
// otherwise -> default to weak class 1 (most productive class)
fn got_verb_class(verb: String) -> String {
if got_str_ends(verb, "jan") { return "wk1" }
if got_str_ends(verb, "on") { return "wk2" }
return "wk1"
}
// got_verb_stem: strip the infinitive suffix to expose the productive stem.
//
// wk1: strip -jan (3 bytes all ASCII)
// wk2: strip -on (2 bytes)
fn got_verb_stem(verb: String, vclass: String) -> String {
if str_eq(vclass, "wk1") { return got_str_drop_last(verb, 3) }
if str_eq(vclass, "wk2") { return got_str_drop_last(verb, 2) }
return got_str_drop_last(verb, 2)
}
// got_conjugate: main conjugation entry point
//
// verb: Gothic infinitive in Latin romanisation (e.g. "wisan", "nasjan")
// or English canonical label ("be", "go", "have")
// tense: "present" | "past"
// person: "first" | "second" | "third"
// number: "singular" | "plural" (or "dual" treated as plural)
//
// Returns the inflected form. Falls back to the infinitive for unknown
// tenses rather than crashing.
fn got_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let v: String = got_map_canonical(verb)
let slot: Int = got_slot(person, number)
// Irregular: wisan (to be)
if str_eq(v, "wisan") {
if str_eq(tense, "present") { return got_wisan_present(slot) }
if str_eq(tense, "past") { return got_wisan_past(slot) }
return v
}
// Irregular: haban (to have)
if str_eq(v, "haban") {
if str_eq(tense, "present") { return got_haban_present(slot) }
if str_eq(tense, "past") { return got_haban_past(slot) }
return v
}
// Irregular: gaggan (to go)
if str_eq(v, "gaggan") {
if str_eq(tense, "present") { return got_gaggan_present(slot) }
if str_eq(tense, "past") { return got_gaggan_past(slot) }
return v
}
// Irregular: saihwan (to see)
if str_eq(v, "saihwan") {
if str_eq(tense, "present") { return got_saihwan_present(slot) }
if str_eq(tense, "past") { return got_saihwan_past(slot) }
return v
}
// Irregular: qiþan (to say)
if str_eq(v, "qiþan") {
if str_eq(tense, "present") { return got_qithan_present(slot) }
if str_eq(tense, "past") { return got_qithan_past(slot) }
return v
}
// Irregular: niman (to take)
if str_eq(v, "niman") {
if str_eq(tense, "present") { return got_niman_present(slot) }
if str_eq(tense, "past") { return got_niman_past(slot) }
return v
}
// Regular weak conjugation
let vclass: String = got_verb_class(v)
let stem: String = got_verb_stem(v, vclass)
if str_eq(vclass, "wk1") {
return got_wk1_conjugate(stem, tense, slot)
}
if str_eq(vclass, "wk2") {
return got_wk2_conjugate(stem, tense, slot)
}
// Final fallback: return the infinitive
return v
}
// a-stem masculine paradigm (dags day)
//
// The a-stem masculine is the largest Gothic noun class, corresponding to the
// Proto-Germanic *-a- stems (OE -as, German -e plurals).
//
// Nominative singular: dags
// Stem: dag- (strip final -s from nom sg)
//
// Singular: nom dags acc dag gen dagis dat daga
// Plural: nom dagos acc dagans gen dage dat dagam
//
// Note: some sources list acc sg as "dag" (without -n), consistent with
// Gothic nomacc distinction in masculine a-stems.
fn got_decline_a_stem_sg(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "s" }
if str_eq(gram_case, "accusative") { return stem }
if str_eq(gram_case, "genitive") { return stem + "is" }
if str_eq(gram_case, "dative") { return stem + "a" }
return stem + "s"
}
fn got_decline_a_stem_pl(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "os" }
if str_eq(gram_case, "accusative") { return stem + "ans" }
if str_eq(gram_case, "genitive") { return stem + "e" }
if str_eq(gram_case, "dative") { return stem + "am" }
return stem + "os"
}
// o-stem feminine paradigm (gibo gift)
//
// The o-stem feminines correspond to Proto-Germanic *-ō- stems. The
// nominative singular ends in -o; nom and acc plural are identical.
//
// Singular: nom gibo acc giba gen gibos dat gibai
// Plural: nom gibos acc gibos gen gibo dat gibom
fn got_decline_o_stem_sg(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "o" }
if str_eq(gram_case, "accusative") { return stem + "a" }
if str_eq(gram_case, "genitive") { return stem + "os" }
if str_eq(gram_case, "dative") { return stem + "ai" }
return stem + "o"
}
fn got_decline_o_stem_pl(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "os" }
if str_eq(gram_case, "accusative") { return stem + "os" }
if str_eq(gram_case, "genitive") { return stem + "o" }
if str_eq(gram_case, "dative") { return stem + "om" }
return stem + "os"
}
// n-stem masculine paradigm (guma man)
//
// The n-stems (weak nouns) are characterised by -n- appearing in all cases
// except the nominative singular. They are sometimes called "weak nouns"
// by analogy with Old English grammar.
//
// Singular: nom guma acc guman gen gumins dat gumin
// Plural: nom gumans acc gumans gen gumane dat gumam
fn got_decline_n_stem_sg(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "a" }
if str_eq(gram_case, "accusative") { return stem + "an" }
if str_eq(gram_case, "genitive") { return stem + "ins" }
if str_eq(gram_case, "dative") { return stem + "in" }
return stem + "a"
}
fn got_decline_n_stem_pl(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "ans" }
if str_eq(gram_case, "accusative") { return stem + "ans" }
if str_eq(gram_case, "genitive") { return stem + "ane" }
if str_eq(gram_case, "dative") { return stem + "am" }
return stem + "ans"
}
// Stem type detection
//
// Infers the stem class from the nominative singular form.
//
// Strategy:
// ends in -s and previous char is not a vowel -> a-stem masc (dags type)
// ends in -o -> o-stem fem (gibo type)
// ends in -a -> n-stem masc (guma type)
// ends in -s and previous char is a vowel -> could be various; default a-stem
// otherwise -> default to a-stem
//
// For the common case where the caller passes the paradigm lemma directly:
// "dags" -> "a", "gibo" -> "o", "guma" -> "n"
fn got_stem_type(noun: String) -> String {
if got_str_ends(noun, "o") { return "o" }
if got_str_ends(noun, "a") { return "n" }
if got_str_ends(noun, "s") { return "a" }
// Fallback for other forms
return "a"
}
// got_extract_stem: strip the nom-sg suffix to obtain the bare stem.
//
// a-stem: strip final -s (dags -> dag)
// o-stem: strip final -o (gibo -> gib)
// n-stem: strip final -a (guma -> gum)
//
// All suffix bytes are single-byte ASCII, so str_len gives byte=char counts.
fn got_extract_stem(noun: String, stype: String) -> String {
let n: Int = str_len(noun)
// All three suffixes are 1 byte each
return str_slice(noun, 0, n - 1)
}
// Gender inference for article selection
//
// The demonstrative-article used in got_noun_phrase depends on gender.
// Gender correlates with stem class in Gothic (imperfect but useful heuristic):
// a-stem -> masculine -> sa
// o-stem -> feminine -> þo (nom sg of feminine demonstrative)
// n-stem -> masculine -> sa
//
// Neuter a-stems (like "waurd" word) exist but are not distinguished here;
// the NLG layer is expected to pass explicit gender when the distinction matters.
fn got_demo_article(stype: String) -> String {
if str_eq(stype, "o") { return "þo" }
// a-stem and n-stem both masculine
return "sa"
}
// got_decline: main declension entry point
//
// noun: nominative singular in Gothic Latin romanisation
// (e.g. "dags", "gibo", "guma")
// gram_case: "nominative" | "accusative" | "genitive" | "dative"
// number: "singular" | "plural"
//
// Returns the inflected form. Unknown stem types return the nominative
// singular unchanged rather than producing garbage output.
fn got_decline(noun: String, gram_case: String, number: String) -> String {
let stype: String = got_stem_type(noun)
let stem: String = got_extract_stem(noun, stype)
if str_eq(stype, "a") {
if str_eq(number, "singular") { return got_decline_a_stem_sg(stem, gram_case) }
return got_decline_a_stem_pl(stem, gram_case)
}
if str_eq(stype, "o") {
if str_eq(number, "singular") { return got_decline_o_stem_sg(stem, gram_case) }
return got_decline_o_stem_pl(stem, gram_case)
}
if str_eq(stype, "n") {
if str_eq(number, "singular") { return got_decline_n_stem_sg(stem, gram_case) }
return got_decline_n_stem_pl(stem, gram_case)
}
// Unknown: return the nominative singular unchanged
return noun
}
// got_noun_phrase: noun phrase builder
//
// Gothic has no definite article. The demonstrative pronouns sa (masc) /
// so (fem) / þata (neut) are used in contexts where a definite-article-like
// meaning is required (closely following Greek ὁ/ἡ/τό in Wulfila's translation).
//
// When definite=true this function prepends the gender-appropriate demonstrative
// in its nominative singular form. For non-nominative cases the demonstrative
// should ideally be declined to match; this implementation prepends the nom-sg
// form as a simplification suitable for NLG output. Callers requiring full
// demonstrative agreement should call got_decline_demonstrative separately.
//
// noun: nominative singular Gothic noun
// gram_case: "nominative" | "accusative" | "genitive" | "dative"
// number: "singular" | "plural"
// definite: "true" prepends the demonstrative; any other value omits it
fn got_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
let declined: String = got_decline(noun, gram_case, number)
if str_eq(definite, "true") {
let stype: String = got_stem_type(noun)
let article: String = got_demo_article(stype)
return article + " " + declined
}
return declined
}