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
+667
View File
@@ -0,0 +1,667 @@
// morphology-fro.el - Old French morphology for the NLG engine.
//
// Implements Old French verb conjugation, noun declension, and the definite
// article. Designed as a companion to morphology.el and called by the engine
// when the language profile code is "fro".
//
// Language profile: code=fro, name=Old French, morph_type=fusional,
// word_order=V2, question_strategy=inversion, script=latin,
// family=romance.
//
// Historical note: Old French (ca. 9001400 CE) is the ancestor of Modern
// French. It diverged from Vulgar Latin and retained a two-case system
// nominative (cas sujet) and oblique (cas régime) inherited ultimately from
// Latin. By around 1300 CE the case distinction had largely collapsed in
// spoken usage, surviving mainly in formal written registers until it
// disappeared altogether. This file targets the core Old French period
// (ca. 10001300).
//
// Two-case system (masculine nouns):
// Singular: nominative stem + -s (li murs = the wall [subject])
// oblique stem (le mur = the wall [object])
// Plural: nominative stem (li mur = the walls [subject])
// oblique stem + -s (les murs = the walls [object])
// Feminine nouns show no case distinction throughout.
//
// Verb conjugation covered:
// Tenses: present indicative, passé simple (past), future
// Persons: first/second/third × singular/plural (slots 0-5)
// Conjugations:
// 1st (-er): present -e/-es/-e/-ons/-ez/-ent
// passé simple -ai/-as/-a/-ames/-astes/-erent
// future stem+rai/ras/ra/rons/rez/ront
// 2nd (-ir): present -is/-is/-it/-issons/-issiez/-issent
// passé simple -is/-is/-it/-imes/-istes/-irent
// future stem+rai series
// 3rd (-re): present stem/s/t/-ons/-ez/-ent
// passé simple -is series (like 2nd)
// future stem+rai series
// Irregulars: estre (be), avoir (have), aler (go), venir (come),
// faire (do/make)
// Canonical map: "be" -> "estre"
//
// Noun declension covered:
// Masculine: two-case (nom/obl) × sg/pl as above
// Feminine: case-neutral, sg base / pl base + -s
// Gender detection: -e ending -> feminine (heuristic), else masculine
//
// Article:
// Definite masculine nom sg: li; obl sg: le; nom pl: li; obl pl: les
// Definite feminine sg: la; pl: les
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with)
// String helpers
fn fro_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn fro_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 (je)
// 1 = 2nd singular (tu)
// 2 = 3rd singular (il/ele)
// 3 = 1st plural (nos)
// 4 = 2nd plural (vos)
// 5 = 3rd plural (il/eles)
fn fro_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 Old French dictionary infinitives
// before conjugation.
fn fro_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "estre" }
if str_eq(verb, "have") { return "avoir" }
if str_eq(verb, "go") { return "aler" }
if str_eq(verb, "come") { return "venir" }
if str_eq(verb, "do") { return "faire" }
if str_eq(verb, "make") { return "faire" }
if str_eq(verb, "say") { return "dire" }
if str_eq(verb, "see") { return "veoir" }
if str_eq(verb, "want") { return "vouloir" }
if str_eq(verb, "can") { return "pooir" }
return verb
}
// Irregular verb: estre (to be)
//
// Suppletive paradigm one of the most irregular verbs in Old French.
//
// Present indicative:
// 1sg sui 2sg es 3sg est
// 1pl somes 2pl estes 3pl sont
//
// Passé simple (past):
// 1sg fui 2sg fus 3sg fu
// 1pl fumes 2pl fustes 3pl furent
//
// Future (periphrastic, based on ester- stem):
// 1sg esterai 2sg esteras 3sg estera
// 1pl esterons 2pl esterez 3pl esteront
fn fro_estre_present(slot: Int) -> String {
if slot == 0 { return "sui" }
if slot == 1 { return "es" }
if slot == 2 { return "est" }
if slot == 3 { return "somes" }
if slot == 4 { return "estes" }
return "sont"
}
fn fro_estre_past(slot: Int) -> String {
if slot == 0 { return "fui" }
if slot == 1 { return "fus" }
if slot == 2 { return "fu" }
if slot == 3 { return "fumes" }
if slot == 4 { return "fustes" }
return "furent"
}
fn fro_estre_future(slot: Int) -> String {
if slot == 0 { return "esterai" }
if slot == 1 { return "esteras" }
if slot == 2 { return "estera" }
if slot == 3 { return "esterons" }
if slot == 4 { return "esterez" }
return "esteront"
}
// Irregular verb: avoir (to have)
//
// Present indicative:
// 1sg ai 2sg as 3sg a
// 1pl avons 2pl avez 3pl ont
//
// Passé simple:
// 1sg oi 2sg os 3sg ot
// 1pl eumes 2pl eustes 3pl orent
//
// Future:
// 1sg avrai 2sg avras 3sg avra
// 1pl avrons 2pl avrez 3pl avront
fn fro_avoir_present(slot: Int) -> String {
if slot == 0 { return "ai" }
if slot == 1 { return "as" }
if slot == 2 { return "a" }
if slot == 3 { return "avons" }
if slot == 4 { return "avez" }
return "ont"
}
fn fro_avoir_past(slot: Int) -> String {
if slot == 0 { return "oi" }
if slot == 1 { return "os" }
if slot == 2 { return "ot" }
if slot == 3 { return "eumes" }
if slot == 4 { return "eustes" }
return "orent"
}
fn fro_avoir_future(slot: Int) -> String {
if slot == 0 { return "avrai" }
if slot == 1 { return "avras" }
if slot == 2 { return "avra" }
if slot == 3 { return "avrons" }
if slot == 4 { return "avrez" }
return "avront"
}
// Irregular verb: aler (to go)
//
// Highly suppletive present draws on Latin *vadere (vois- stem).
//
// Present indicative:
// 1sg vois 2sg vas 3sg va
// 1pl alons 2pl alez 3pl vont
//
// Passé simple (regular -er pattern on al-):
// 1sg alai 2sg alas 3sg ala
// 1pl alames 2pl alastes 3pl alerent
//
// Future (ir- stem, archaic):
// 1sg irai 2sg iras 3sg ira
// 1pl irons 2pl irez 3pl iront
fn fro_aler_present(slot: Int) -> String {
if slot == 0 { return "vois" }
if slot == 1 { return "vas" }
if slot == 2 { return "va" }
if slot == 3 { return "alons" }
if slot == 4 { return "alez" }
return "vont"
}
fn fro_aler_past(slot: Int) -> String {
if slot == 0 { return "alai" }
if slot == 1 { return "alas" }
if slot == 2 { return "ala" }
if slot == 3 { return "alames" }
if slot == 4 { return "alastes" }
return "alerent"
}
fn fro_aler_future(slot: Int) -> String {
if slot == 0 { return "irai" }
if slot == 1 { return "iras" }
if slot == 2 { return "ira" }
if slot == 3 { return "irons" }
if slot == 4 { return "irez" }
return "iront"
}
// Irregular verb: venir (to come)
//
// Present indicative (vien-/ven- alternation):
// 1sg vieng 2sg viens 3sg vient
// 1pl venons 2pl venez 3pl vienent
//
// Passé simple:
// 1sg ving 2sg vins 3sg vint
// 1pl vinsmes 2pl vinstes 3pl vindrent
//
// Future (venr- stem):
// 1sg venrai 2sg venras 3sg venra
// 1pl venrons 2pl venrez 3pl venront
fn fro_venir_present(slot: Int) -> String {
if slot == 0 { return "vieng" }
if slot == 1 { return "viens" }
if slot == 2 { return "vient" }
if slot == 3 { return "venons" }
if slot == 4 { return "venez" }
return "vienent"
}
fn fro_venir_past(slot: Int) -> String {
if slot == 0 { return "ving" }
if slot == 1 { return "vins" }
if slot == 2 { return "vint" }
if slot == 3 { return "vinsmes" }
if slot == 4 { return "vinstes" }
return "vindrent"
}
fn fro_venir_future(slot: Int) -> String {
if slot == 0 { return "venrai" }
if slot == 1 { return "venras" }
if slot == 2 { return "venra" }
if slot == 3 { return "venrons" }
if slot == 4 { return "venrez" }
return "venront"
}
// Irregular verb: faire (to do/make)
//
// Present indicative (faz/fais- alternation):
// 1sg faz 2sg fais 3sg fait
// 1pl faisons 2pl faites 3pl font
//
// Passé simple:
// 1sg fis 2sg fis 3sg fist
// 1pl fimes 2pl fistes 3pl firent
//
// Future (fer- stem):
// 1sg ferai 2sg feras 3sg fera
// 1pl ferons 2pl ferez 3pl feront
fn fro_faire_present(slot: Int) -> String {
if slot == 0 { return "faz" }
if slot == 1 { return "fais" }
if slot == 2 { return "fait" }
if slot == 3 { return "faisons" }
if slot == 4 { return "faites" }
return "font"
}
fn fro_faire_past(slot: Int) -> String {
if slot == 0 { return "fis" }
if slot == 1 { return "fis" }
if slot == 2 { return "fist" }
if slot == 3 { return "fimes" }
if slot == 4 { return "fistes" }
return "firent"
}
fn fro_faire_future(slot: Int) -> String {
if slot == 0 { return "ferai" }
if slot == 1 { return "feras" }
if slot == 2 { return "fera" }
if slot == 3 { return "ferons" }
if slot == 4 { return "ferez" }
return "feront"
}
// Conjugation class detection
//
// Old French verbs fall into three broad conjugation classes:
// 1st conjugation: infinitive ends in -er (chanter, donner)
// 2nd conjugation: infinitive ends in -ir (finir, choisir)
// 3rd conjugation: infinitive ends in -re (vendre, rendre)
//
// Returns "1", "2", or "3".
fn fro_verb_class(verb: String) -> String {
if fro_str_ends(verb, "er") { return "1" }
if fro_str_ends(verb, "ir") { return "2" }
if fro_str_ends(verb, "re") { return "3" }
return "1"
}
// fro_verb_stem: strip the infinitive suffix to expose the productive stem.
// 1st (-er): drop 2 bytes
// 2nd (-ir): drop 2 bytes
// 3rd (-re): drop 2 bytes
fn fro_verb_stem(verb: String, vclass: String) -> String {
return fro_drop(verb, 2)
}
// 1st conjugation (-er): regular endings
//
// Present indicative (stem + ending):
// 1sg -e 2sg -es 3sg -e
// 1pl -ons 2pl -ez 3pl -ent
//
// Passé simple:
// 1sg -ai 2sg -as 3sg -a
// 1pl -ames 2pl -astes 3pl -erent
//
// Future (infinitive is the base drop -r then add endings):
// Actually the future stem = infinitive minus final -r (chanter- -> chanterai)
// 1sg -ai 2sg -as 3sg -a
// 1pl -ons 2pl -ez 3pl -ont
// Combined with chanterr-: chant+er+ai = chanterai; stem for future = infinitive + "a"...
// Simpler: future base = fro_drop(verb, 1) i.e. drop final -r to keep the -e:
// chanterai, chanteras, chantera, chanterons, chanterez, chanteront
fn fro_conj1_present(stem: String, slot: Int) -> String {
if slot == 0 { return stem + "e" }
if slot == 1 { return stem + "es" }
if slot == 2 { return stem + "e" }
if slot == 3 { return stem + "ons" }
if slot == 4 { return stem + "ez" }
return stem + "ent"
}
fn fro_conj1_past(stem: String, slot: Int) -> String {
if slot == 0 { return stem + "ai" }
if slot == 1 { return stem + "as" }
if slot == 2 { return stem + "a" }
if slot == 3 { return stem + "ames" }
if slot == 4 { return stem + "astes" }
return stem + "erent"
}
fn fro_conj1_future(verb: String, slot: Int) -> String {
// Future base = infinitive minus final -r (retains the -e): chanter -> chante-
let base: String = fro_drop(verb, 1)
if slot == 0 { return base + "rai" }
if slot == 1 { return base + "ras" }
if slot == 2 { return base + "ra" }
if slot == 3 { return base + "rons" }
if slot == 4 { return base + "rez" }
return base + "ront"
}
// 2nd conjugation (-ir): regular endings
//
// Present indicative uses an infix -iss- in pl forms (inchoative):
// 1sg stem + -is 2sg stem + -is 3sg stem + -it
// 1pl stem + -issons 2pl stem + -issiez 3pl stem + -issent
//
// Passé simple:
// 1sg stem + -is 2sg stem + -is 3sg stem + -it
// 1pl stem + -imes 2pl stem + -istes 3pl stem + -irent
//
// Future (infinitive minus final -r):
// finir -> fini- -> finirai ...
fn fro_conj2_present(stem: String, slot: Int) -> String {
if slot == 0 { return stem + "is" }
if slot == 1 { return stem + "is" }
if slot == 2 { return stem + "it" }
if slot == 3 { return stem + "issons" }
if slot == 4 { return stem + "issiez" }
return stem + "issent"
}
fn fro_conj2_past(stem: String, slot: Int) -> String {
if slot == 0 { return stem + "is" }
if slot == 1 { return stem + "is" }
if slot == 2 { return stem + "it" }
if slot == 3 { return stem + "imes" }
if slot == 4 { return stem + "istes" }
return stem + "irent"
}
fn fro_conj2_future(verb: String, slot: Int) -> String {
let base: String = fro_drop(verb, 1)
if slot == 0 { return base + "rai" }
if slot == 1 { return base + "ras" }
if slot == 2 { return base + "ra" }
if slot == 3 { return base + "rons" }
if slot == 4 { return base + "rez" }
return base + "ront"
}
// 3rd conjugation (-re): regular endings
//
// Present indicative:
// 1sg stem (no ending) 2sg stem + -s 3sg stem + -t
// 1pl stem + -ons 2pl stem + -ez 3pl stem + -ent
//
// Passé simple (same endings as 2nd conj):
// 1sg stem + -is 2sg stem + -is 3sg stem + -it
// 1pl stem + -imes 2pl stem + -istes 3pl stem + -irent
//
// Future (-re verbs drop -e before adding endings):
// vendre -> vendr- -> vendrai ...
fn fro_conj3_present(stem: String, slot: Int) -> String {
if slot == 0 { return stem }
if slot == 1 { return stem + "s" }
if slot == 2 { return stem + "t" }
if slot == 3 { return stem + "ons" }
if slot == 4 { return stem + "ez" }
return stem + "ent"
}
fn fro_conj3_past(stem: String, slot: Int) -> String {
if slot == 0 { return stem + "is" }
if slot == 1 { return stem + "is" }
if slot == 2 { return stem + "it" }
if slot == 3 { return stem + "imes" }
if slot == 4 { return stem + "istes" }
return stem + "irent"
}
fn fro_conj3_future(verb: String, slot: Int) -> String {
// Drop -re (2 bytes) to get consonant-final stem, then add -rai etc.
let base: String = fro_drop(verb, 2)
if slot == 0 { return base + "rai" }
if slot == 1 { return base + "ras" }
if slot == 2 { return base + "ra" }
if slot == 3 { return base + "rons" }
if slot == 4 { return base + "rez" }
return base + "ront"
}
// fro_conjugate: main conjugation entry point
//
// verb: Old French infinitive (e.g. "chanter", "finir", "vendre")
// or English canonical label ("be", "go", "have", ...)
// tense: "present" | "past" | "future"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
//
// Returns the inflected form. Unknown tenses fall back to the infinitive.
fn fro_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let v: String = fro_map_canonical(verb)
let slot: Int = fro_slot(person, number)
// Irregular: estre (to be)
if str_eq(v, "estre") {
if str_eq(tense, "present") { return fro_estre_present(slot) }
if str_eq(tense, "past") { return fro_estre_past(slot) }
if str_eq(tense, "future") { return fro_estre_future(slot) }
return v
}
// Irregular: avoir (to have)
if str_eq(v, "avoir") {
if str_eq(tense, "present") { return fro_avoir_present(slot) }
if str_eq(tense, "past") { return fro_avoir_past(slot) }
if str_eq(tense, "future") { return fro_avoir_future(slot) }
return v
}
// Irregular: aler (to go)
if str_eq(v, "aler") {
if str_eq(tense, "present") { return fro_aler_present(slot) }
if str_eq(tense, "past") { return fro_aler_past(slot) }
if str_eq(tense, "future") { return fro_aler_future(slot) }
return v
}
// Irregular: venir (to come)
if str_eq(v, "venir") {
if str_eq(tense, "present") { return fro_venir_present(slot) }
if str_eq(tense, "past") { return fro_venir_past(slot) }
if str_eq(tense, "future") { return fro_venir_future(slot) }
return v
}
// Irregular: faire (to do/make)
if str_eq(v, "faire") {
if str_eq(tense, "present") { return fro_faire_present(slot) }
if str_eq(tense, "past") { return fro_faire_past(slot) }
if str_eq(tense, "future") { return fro_faire_future(slot) }
return v
}
// Regular conjugations
let vclass: String = fro_verb_class(v)
let stem: String = fro_verb_stem(v, vclass)
if str_eq(vclass, "1") {
if str_eq(tense, "present") { return fro_conj1_present(stem, slot) }
if str_eq(tense, "past") { return fro_conj1_past(stem, slot) }
if str_eq(tense, "future") { return fro_conj1_future(v, slot) }
return v
}
if str_eq(vclass, "2") {
if str_eq(tense, "present") { return fro_conj2_present(stem, slot) }
if str_eq(tense, "past") { return fro_conj2_past(stem, slot) }
if str_eq(tense, "future") { return fro_conj2_future(v, slot) }
return v
}
if str_eq(vclass, "3") {
if str_eq(tense, "present") { return fro_conj3_present(stem, slot) }
if str_eq(tense, "past") { return fro_conj3_past(stem, slot) }
if str_eq(tense, "future") { return fro_conj3_future(v, slot) }
return v
}
// Final fallback: return the infinitive
return v
}
// Gender detection
//
// Heuristic gender detection from the citation form (nominative singular).
// Old French gender was inherited from Latin with only two genders surviving:
// masculine and feminine (neuter collapsed into masculine/feminine by Vulgar Latin).
//
// Heuristic: citation form ending in -e -> feminine; otherwise -> masculine.
// This is imperfect but covers the majority of common nouns.
fn fro_gender(noun: String) -> String {
if fro_str_ends(noun, "e") { return "fem" }
return "masc"
}
// Noun declension
//
// Old French two-case system:
//
// Masculine (e.g. mur wall, stem = mur):
// Singular nominative (cas sujet): murs (stem + -s)
// Singular oblique (cas régime): mur (stem)
// Plural nominative: mur (stem)
// Plural oblique: murs (stem + -s)
//
// This pattern means nominative markers invert relative to Latin:
// the nominative takes -s in singular but loses it in plural,
// while the oblique works the other way round.
//
// Feminine (e.g. dame lady):
// No case distinction throughout.
// Singular: dame (citation form)
// Plural: dames (citation + -s)
//
// gram_case: "nominative" | "oblique"
// number: "singular" | "plural"
fn fro_decline_masc(noun: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return noun + "s" }
// oblique singular: bare stem
return noun
}
// plural
if str_eq(gram_case, "nominative") { return noun }
return noun + "s"
}
fn fro_decline_fem(noun: String, number: String) -> String {
if str_eq(number, "singular") { return noun }
return noun + "s"
}
// fro_decline: main declension entry point.
//
// noun: citation form (nominative/oblique singular typically the bare stem)
// gram_case: "nominative" | "oblique"
// number: "singular" | "plural"
fn fro_decline(noun: String, gram_case: String, number: String) -> String {
let gender: String = fro_gender(noun)
if str_eq(gender, "masc") {
return fro_decline_masc(noun, gram_case, number)
}
return fro_decline_fem(noun, number)
}
// Definite article
//
// Old French definite articles are case- and gender-sensitive:
//
// Masculine:
// nom sg: li obl sg: le
// nom pl: li obl pl: les
//
// Feminine:
// sg: la pl: les
//
// gender: "masc" | "fem"
// gram_case: "nominative" | "oblique"
// number: "singular" | "plural"
fn fro_article(gender: String, gram_case: String, number: String) -> String {
if str_eq(gender, "masc") {
if str_eq(number, "plural") { return "les" }
if str_eq(gram_case, "nominative") { return "li" }
return "le"
}
// feminine
if str_eq(number, "plural") { return "les" }
return "la"
}
// fro_noun_phrase: noun phrase builder
//
// Assembles a declined noun with an optional definite article.
//
// noun: citation form (nominative/oblique singular stem)
// gram_case: "nominative" | "oblique"
// number: "singular" | "plural"
// definite: "true" to prepend the definite article; any other value omits it
fn fro_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
let gender: String = fro_gender(noun)
let declined: String = fro_decline(noun, gram_case, number)
if str_eq(definite, "true") {
let art: String = fro_article(gender, gram_case, number)
return art + " " + declined
}
return declined
}