Files
el/elp/src/morphology-goh.el
T
will.anderson c2cd5e01e1
El SDK CI - dev / build-and-test (pull_request) Successful in 3m34s
fix: elb macOS OpenSSL + C master declarations header; add ELP missing imports
elb.el:
- Auto-detect Homebrew OpenSSL (-L$(brew --prefix openssl)/lib) so -lssl
  resolves on macOS without manual flags; no-op on Linux
- Add -include elp-c-decls.h when present in out_dir: resolves undeclared
  cross-module calls in packages like ELP that lack explicit imports

ELP source:
- Add import "morphology.el" to all 29 language morphology modules
- Add language module imports to morphology.el (all langs it dispatches to)
  These were missing since ELP was originally built as a monolithic unit
2026-05-08 19:44:31 -05:00

638 lines
25 KiB
EmacsLisp
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// morphology-goh.el - Old High German morphology for the NLG engine.
//
// Implements Old High German verb conjugation, noun declension, and the
// demonstrative determiner. Designed as a companion to morphology.el and
// called by the engine when the language profile code is "goh".
//
// Language profile: code=goh, name=Old High German, morph_type=fusional,
// word_order=V2, question_strategy=inversion, script=latin,
// family=germanic.
//
// Historical note: Old High German (ca. 7501050 CE) is the earliest
// substantially attested stage of High German, preserved in texts such as the
// Hildebrandslied, the Muspilli, and Notker's translations. It is the
// ancestor of Middle High German and thence Modern German. The "High" refers
// to the geographical highlands of southern Germany, Austria, and Switzerland
// distinct from the Low German / Old Saxon dialects spoken to the north.
//
// Defining feature the Second Germanic Consonant Shift (Hochdeutsche
// Lautverschiebung), which distinguishes OHG from Gothic and Old English:
// p ff/pf (Gothic "skip" OHG "skif"; Goth "apan" → OHG "affo")
// t ss/z (Goth "watan" OHG "wazzer"; OE "tid" → OHG "zit")
// k ch (Goth "mikan" OHG "mihil")
// This shift only applies to inherited consonants and not to recent loanwords.
//
// Three genders (masculine, feminine, neuter), four cases (nominative,
// accusative, genitive, dative), and two numbers (singular, plural).
//
// Verb conjugation covered:
// Tenses: present indicative, past indicative
// Persons: first/second/third × singular/plural (slots 0-5)
// Classes: weak verbs (dental -ta past suffix the most productive class)
// Irregulars: wesan/sīn (be), habēn (have), gān (go), sehan (see),
// quethan (say), tuon (do)
// Canonical map: "be" -> "wesan"
//
// Noun declension covered:
// Strong masc a-stem (tag day): 4 cases × sg/pl
// Strong fem ō-stem (geba gift): 4 cases × sg/pl
// Strong neut a-stem (wort word): 4 cases × sg/pl
// Weak masc n-stem (boto messenger): 4 cases × sg/pl
//
// Demonstrative / definite article:
// OHG uses the demonstrative dër/diu/daz (the) which doubles as a definite
// determiner. This implementation selects the correct nominative form by
// inferred gender and passes it as a separate word before the noun.
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with)
// String helpers
import "morphology.el"
fn goh_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn goh_drop(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
//
// Maps person × number to a 0-based paradigm index.
// 0 = 1st singular (ih)
// 1 = 2nd singular ()
// 2 = 3rd singular (ër/siu/ez)
// 3 = 1st plural (wir)
// 4 = 2nd plural (ir)
// 5 = 3rd plural (sie)
fn goh_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 resolved to OHG dictionary infinitives
// before conjugation.
fn goh_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "wesan" }
if str_eq(verb, "have") { return "haben" }
if str_eq(verb, "go") { return "gan" }
if str_eq(verb, "see") { return "sehan" }
if str_eq(verb, "say") { return "quethan" }
if str_eq(verb, "do") { return "tuon" }
if str_eq(verb, "make") { return "tuon" }
if str_eq(verb, "come") { return "queman" }
if str_eq(verb, "give") { return "geban" }
if str_eq(verb, "know") { return "wizzan" }
if str_eq(verb, "want") { return "wellan" }
return verb
}
// Irregular verb: wesan/sīn (to be)
//
// wesan is one of the most irregular verbs in OHG. The present uses the
// bim/bin- forms (from Proto-Germanic *biju-); the past uses the was-/wāri-
// forms (from the strong past of wesan).
//
// Present indicative:
// 1sg bim/bin 2sg bist 3sg ist
// 1pl birum 2pl biruț 3pl sint
//
// Note: 2pl "biruț" the ț represents an old dental fricative; rendered here
// as "birut" (common orthographic variant in OHG manuscripts).
//
// Past indicative:
// 1sg was 2sg wāri 3sg was
// 1pl wārum 2pl wāruț 3pl wārun
fn goh_wesan_present(slot: Int) -> String {
if slot == 0 { return "bim" }
if slot == 1 { return "bist" }
if slot == 2 { return "ist" }
if slot == 3 { return "birum" }
if slot == 4 { return "birut" }
return "sint"
}
fn goh_wesan_past(slot: Int) -> String {
if slot == 0 { return "was" }
if slot == 1 { return "wari" }
if slot == 2 { return "was" }
if slot == 3 { return "warum" }
if slot == 4 { return "warut" }
return "warun"
}
// Irregular verb: habēn (to have)
//
// habēn is an athematic preterite-present verb.
//
// Present indicative:
// 1sg habem 2sg habest 3sg habet
// 1pl habemes 2pl habet 3pl habent
//
// Past (weak past -ta on stem hab-):
// 1sg habeta 2sg habetos 3sg habeta
// 1pl habetom 2pl habetot 3pl habeton
fn goh_haben_present(slot: Int) -> String {
if slot == 0 { return "habem" }
if slot == 1 { return "habest" }
if slot == 2 { return "habet" }
if slot == 3 { return "habemes" }
if slot == 4 { return "habet" }
return "habent"
}
fn goh_haben_past(slot: Int) -> String {
if slot == 0 { return "habeta" }
if slot == 1 { return "habetos" }
if slot == 2 { return "habeta" }
if slot == 3 { return "habetom" }
if slot == 4 { return "habetot" }
return "habeton"
}
// Irregular verb: gān (to go)
//
// gān is an anomalous contracted verb.
//
// Present indicative:
// 1sg gan 2sg gest 3sg get
// 1pl games 2pl gat 3pl gant
//
// Past (reduplicating/suppletive giang- forms):
// 1sg giang 2sg giangi 3sg giang
// 1pl giangum 2pl giangun 3pl giangun
fn goh_gan_present(slot: Int) -> String {
if slot == 0 { return "gan" }
if slot == 1 { return "gest" }
if slot == 2 { return "get" }
if slot == 3 { return "games" }
if slot == 4 { return "gat" }
return "gant"
}
fn goh_gan_past(slot: Int) -> String {
if slot == 0 { return "giang" }
if slot == 1 { return "giangi" }
if slot == 2 { return "giang" }
if slot == 3 { return "giangum" }
if slot == 4 { return "giangun" }
return "giangun"
}
// Irregular verb: sehan (to see)
//
// Strong class 5 (ablaut e/a). Present has i-mutation in sg forms.
//
// Present indicative:
// 1sg sihu 2sg sihist 3sg sihit
// 1pl sehemes 2pl sehet 3pl sehent
//
// Past (strong ablaut: e a):
// 1sg sah 2sg sahi 3sg sah
// 1pl sahum 2pl sahut 3pl sahun
fn goh_sehan_present(slot: Int) -> String {
if slot == 0 { return "sihu" }
if slot == 1 { return "sihist" }
if slot == 2 { return "sihit" }
if slot == 3 { return "sehemes" }
if slot == 4 { return "sehet" }
return "sehent"
}
fn goh_sehan_past(slot: Int) -> String {
if slot == 0 { return "sah" }
if slot == 1 { return "sahi" }
if slot == 2 { return "sah" }
if slot == 3 { return "sahum" }
if slot == 4 { return "sahut" }
return "sahun"
}
// Irregular verb: quethan (to say)
//
// Strong class 5 (ablaut e/a). Present sg has i-mutation (quid-).
//
// Present indicative:
// 1sg quidu 2sg quidist 3sg quidit
// 1pl quethumes 2pl quethet 3pl quethent
//
// Past (strong ablaut: e a):
// 1sg quad 2sg quadi 3sg quad
// 1pl quadum 2pl quadut 3pl quadun
fn goh_quethan_present(slot: Int) -> String {
if slot == 0 { return "quidu" }
if slot == 1 { return "quidist" }
if slot == 2 { return "quidit" }
if slot == 3 { return "quethumes" }
if slot == 4 { return "quethet" }
return "quethent"
}
fn goh_quethan_past(slot: Int) -> String {
if slot == 0 { return "quad" }
if slot == 1 { return "quadi" }
if slot == 2 { return "quad" }
if slot == 3 { return "quadum" }
if slot == 4 { return "quadut" }
return "quadun"
}
// Irregular verb: tuon (to do/make)
//
// tuon is a contracted athematic verb.
//
// Present indicative:
// 1sg tuom 2sg tuost 3sg tuot
// 1pl tuomes 2pl tuot 3pl tuont
//
// Past (weak past -ta on stem tā-):
// 1sg teta 2sg tetos 3sg teta
// 1pl tetom 2pl tetot 3pl teton
fn goh_tuon_present(slot: Int) -> String {
if slot == 0 { return "tuom" }
if slot == 1 { return "tuost" }
if slot == 2 { return "tuot" }
if slot == 3 { return "tuomes" }
if slot == 4 { return "tuot" }
return "tuont"
}
fn goh_tuon_past(slot: Int) -> String {
if slot == 0 { return "teta" }
if slot == 1 { return "tetos" }
if slot == 2 { return "teta" }
if slot == 3 { return "tetom" }
if slot == 4 { return "tetot" }
return "teton"
}
// Weak verb regular paradigm
//
// Weak verbs are the productive OHG class new verbs are regularly formed on
// this pattern. The infinitive ends in -en or -on; the weak past uses the
// dental suffix -ta/-to (a reflex of Proto-Germanic *-dō-).
//
// Class 1 weak (most common; infinitive -en, stem ends in consonant):
//
// Present indicative (stem + ending):
// 1sg -u 2sg -ist 3sg -it
// 1pl -emēs 2pl -et 3pl -ent
//
// Past indicative (stem + dental suffix):
// 1sg -ta 2sg -tōs 3sg -ta
// 1pl -tōm 2pl -tōt 3pl -tōn
//
// The stem for a regular weak verb is obtained by dropping -en (2 bytes).
fn goh_weak_present(stem: String, slot: Int) -> String {
if slot == 0 { return stem + "u" }
if slot == 1 { return stem + "ist" }
if slot == 2 { return stem + "it" }
if slot == 3 { return stem + "emes" }
if slot == 4 { return stem + "et" }
return stem + "ent"
}
fn goh_weak_past(stem: String, slot: Int) -> String {
if slot == 0 { return stem + "ta" }
if slot == 1 { return stem + "tos" }
if slot == 2 { return stem + "ta" }
if slot == 3 { return stem + "tom" }
if slot == 4 { return stem + "tot" }
return stem + "ton"
}
// goh_verb_stem: strip the infinitive ending to expose the productive stem.
// -en endings: drop 2 bytes (most weak verbs: sagēn sag-)
// -on endings: drop 2 bytes (class 2 weak: lobōn lob-)
// -an endings: drop 2 bytes (strong verbs handled as irregular; fallback)
fn goh_verb_stem(verb: String) -> String {
return goh_drop(verb, 2)
}
// goh_conjugate: main conjugation entry point
//
// verb: OHG infinitive (e.g. "sagēn", "lobōn") or English canonical label
// tense: "present" | "past"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
//
// Returns the inflected form. Unknown tenses fall back to the infinitive.
fn goh_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let v: String = goh_map_canonical(verb)
let slot: Int = goh_slot(person, number)
// Irregular: wesan (to be)
if str_eq(v, "wesan") {
if str_eq(tense, "present") { return goh_wesan_present(slot) }
if str_eq(tense, "past") { return goh_wesan_past(slot) }
return v
}
// Irregular: habēn / haben (to have)
if str_eq(v, "haben") {
if str_eq(tense, "present") { return goh_haben_present(slot) }
if str_eq(tense, "past") { return goh_haben_past(slot) }
return v
}
// Also match the macron-spelled infinitive if passed directly
if str_eq(v, "haben") {
if str_eq(tense, "present") { return goh_haben_present(slot) }
if str_eq(tense, "past") { return goh_haben_past(slot) }
return v
}
// Irregular: gān / gan (to go)
if str_eq(v, "gan") {
if str_eq(tense, "present") { return goh_gan_present(slot) }
if str_eq(tense, "past") { return goh_gan_past(slot) }
return v
}
// Irregular: sehan (to see)
if str_eq(v, "sehan") {
if str_eq(tense, "present") { return goh_sehan_present(slot) }
if str_eq(tense, "past") { return goh_sehan_past(slot) }
return v
}
// Irregular: quethan (to say)
if str_eq(v, "quethan") {
if str_eq(tense, "present") { return goh_quethan_present(slot) }
if str_eq(tense, "past") { return goh_quethan_past(slot) }
return v
}
// Irregular: tuon (to do/make)
if str_eq(v, "tuon") {
if str_eq(tense, "present") { return goh_tuon_present(slot) }
if str_eq(tense, "past") { return goh_tuon_past(slot) }
return v
}
// Regular weak conjugation
let stem: String = goh_verb_stem(v)
if str_eq(tense, "present") { return goh_weak_present(stem, slot) }
if str_eq(tense, "past") { return goh_weak_past(stem, slot) }
// Final fallback: return the infinitive
return v
}
// Noun stem type detection
//
// OHG nouns are grouped by historical stem class. This engine supports four:
//
// "masc_a" strong masculine a-stem (tag, fisc, arm)
// "fem_o" strong feminine ō-stem (geba, zala, burg)
// "neut_a" strong neuter a-stem (wort, kind, tier)
// "masc_n" weak masculine n-stem (boto, hazo, namo)
//
// Detection heuristic from the citation form (nominative singular):
// ends in -o masc_n (weak masc nouns: boto, hazo)
// ends in -a fem_o (strong fem: geba, zala)
// ends in -t, -d, -n (common neuter endings) neut_a
// otherwise masc_a (default strong masculine)
//
// Callers may override by passing the stem type directly when the heuristic
// would produce the wrong class (e.g. for monosyllables with ambiguous endings).
fn goh_stem_type(noun: String) -> String {
if goh_str_ends(noun, "o") { return "masc_n" }
if goh_str_ends(noun, "a") { return "fem_o" }
if goh_str_ends(noun, "t") { return "neut_a" }
if goh_str_ends(noun, "d") { return "neut_a" }
if goh_str_ends(noun, "nd") { return "neut_a" }
return "masc_a"
}
// goh_extract_stem: derive the bare stem used as the base for all case endings.
//
// masc_a: citation form is nom sg without ending (tag = tag, fisc = fisc)
// citation IS the stem; no stripping needed
// fem_o: strip final -a (geba geb-)
// neut_a: citation form is nom/acc sg without ending (wort = wort)
// citation IS the stem; no stripping needed
// masc_n: strip final -o (boto bot-)
fn goh_extract_stem(noun: String, stype: String) -> String {
if str_eq(stype, "fem_o") { return goh_drop(noun, 1) }
if str_eq(stype, "masc_n") { return goh_drop(noun, 1) }
// masc_a and neut_a: citation IS the stem
return noun
}
// Strong masculine a-stem declension (tag day)
//
// The strong masculine a-stem is the most common OHG masculine class.
// It corresponds to the Gothic a-stem and the Latin 2nd-declension masculine.
//
// Paradigm (stem = tag-):
// Singular: nom tag acc tag gen tages dat tage
// Plural: nom taga acc taga gen tago dat tagum
fn goh_decline_masc_a_sg(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem }
if str_eq(gram_case, "accusative") { return stem }
if str_eq(gram_case, "genitive") { return stem + "es" }
if str_eq(gram_case, "dative") { return stem + "e" }
return stem
}
fn goh_decline_masc_a_pl(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "a" }
if str_eq(gram_case, "accusative") { return stem + "a" }
if str_eq(gram_case, "genitive") { return stem + "o" }
if str_eq(gram_case, "dative") { return stem + "um" }
return stem + "a"
}
// Strong feminine ō-stem declension (geba gift)
//
// The ō-stem feminines are the standard OHG feminine class.
// They correspond to the Gothic o-stem and Latin 1st-declension nouns.
//
// Paradigm (stem = geb-):
// Singular: nom geba acc geba gen gebā dat gebu
// Plural: nom gebā acc gebā gen gebōno dat gebōm
fn goh_decline_fem_o_sg(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "a" }
if str_eq(gram_case, "accusative") { return stem + "a" }
if str_eq(gram_case, "genitive") { return stem + "a" }
if str_eq(gram_case, "dative") { return stem + "u" }
return stem + "a"
}
fn goh_decline_fem_o_pl(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "a" }
if str_eq(gram_case, "accusative") { return stem + "a" }
if str_eq(gram_case, "genitive") { return stem + "ono" }
if str_eq(gram_case, "dative") { return stem + "om" }
return stem + "a"
}
// Strong neuter a-stem declension (wort word)
//
// Strong neuter nouns share the a-stem pattern but have identical nom/acc
// throughout (a pan-Germanic neuter feature). The plural differs from the
// masculine in the nom/acc: neuters use stem alone rather than stem + -a.
//
// Paradigm (stem = wort):
// Singular: nom wort acc wort gen wortes dat worte
// Plural: nom wort acc wort gen worto dat wortum
fn goh_decline_neut_a_sg(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem }
if str_eq(gram_case, "accusative") { return stem }
if str_eq(gram_case, "genitive") { return stem + "es" }
if str_eq(gram_case, "dative") { return stem + "e" }
return stem
}
fn goh_decline_neut_a_pl(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem }
if str_eq(gram_case, "accusative") { return stem }
if str_eq(gram_case, "genitive") { return stem + "o" }
if str_eq(gram_case, "dative") { return stem + "um" }
return stem
}
// Weak masculine n-stem declension (boto messenger)
//
// Weak nouns (n-stems) are characterised by the nasal -n- appearing in all
// forms except the nominative singular. They correspond to the Gothic n-stem
// and the Old English weak noun class.
//
// Paradigm (stem = bot-):
// Singular: nom boto acc boton gen boton dat boton
// Plural: nom boton acc boton gen botōno dat botōm
fn goh_decline_masc_n_sg(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "o" }
if str_eq(gram_case, "accusative") { return stem + "on" }
if str_eq(gram_case, "genitive") { return stem + "on" }
if str_eq(gram_case, "dative") { return stem + "on" }
return stem + "o"
}
fn goh_decline_masc_n_pl(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "on" }
if str_eq(gram_case, "accusative") { return stem + "on" }
if str_eq(gram_case, "genitive") { return stem + "ono" }
if str_eq(gram_case, "dative") { return stem + "om" }
return stem + "on"
}
// goh_decline: main declension entry point
//
// noun: OHG nominative singular form (e.g. "tag", "geba", "wort", "boto")
// gram_case: "nominative" | "accusative" | "genitive" | "dative"
// number: "singular" | "plural"
//
// Returns the inflected form. Unknown stem types return the citation form
// unchanged as a safe fallback.
fn goh_decline(noun: String, gram_case: String, number: String) -> String {
let stype: String = goh_stem_type(noun)
let stem: String = goh_extract_stem(noun, stype)
if str_eq(stype, "masc_a") {
if str_eq(number, "singular") { return goh_decline_masc_a_sg(stem, gram_case) }
return goh_decline_masc_a_pl(stem, gram_case)
}
if str_eq(stype, "fem_o") {
if str_eq(number, "singular") { return goh_decline_fem_o_sg(stem, gram_case) }
return goh_decline_fem_o_pl(stem, gram_case)
}
if str_eq(stype, "neut_a") {
if str_eq(number, "singular") { return goh_decline_neut_a_sg(stem, gram_case) }
return goh_decline_neut_a_pl(stem, gram_case)
}
if str_eq(stype, "masc_n") {
if str_eq(number, "singular") { return goh_decline_masc_n_sg(stem, gram_case) }
return goh_decline_masc_n_pl(stem, gram_case)
}
// Unknown: return citation form unchanged
return noun
}
// Demonstrative article
//
// OHG uses the demonstrative pronoun dër/diu/daz as a definite determiner.
// Full declension of this pronoun is complex; this implementation provides the
// nominative forms used as determiners before nouns.
//
// Nominative forms (the most common slot for a determiner):
// Masculine sg: der Feminine sg: diu Neuter sg: daz
// Plural (all genders): die
//
// Gender is inferred from the stem type:
// masc_a masculine "der"
// fem_o feminine "diu"
// neut_a neuter "daz"
// masc_n masculine "der"
fn goh_demo_article(stype: String, number: String) -> String {
if str_eq(number, "plural") { return "die" }
if str_eq(stype, "fem_o") { return "diu" }
if str_eq(stype, "neut_a") { return "daz" }
return "der"
}
// goh_noun_phrase: noun phrase builder
//
// Assembles a declined noun with an optional OHG demonstrative article.
//
// noun: OHG nominative singular (e.g. "tag", "geba", "wort", "boto")
// gram_case: "nominative" | "accusative" | "genitive" | "dative"
// number: "singular" | "plural"
// definite: "true" to prepend the demonstrative determiner; any other value omits it
//
// Note: the demonstrative is given in its nominative singular form for
// simplicity. Full agreement would require a separate declined demonstrative
// paradigm; the NLG layer should implement that when case-agreement on the
// determiner is required.
fn goh_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
let stype: String = goh_stem_type(noun)
let declined: String = goh_decline(noun, gram_case, number)
if str_eq(definite, "true") {
let art: String = goh_demo_article(stype, number)
return art + " " + declined
}
return declined
}