Files
el/elp/src/morphology-la.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

873 lines
34 KiB
EmacsLisp

// morphology-la.el - Latin morphology for the NLG engine.
//
// Implements fusional Latin verb conjugation and noun declension. Designed
// as a companion to morphology.el and called by the engine when the language
// profile code is "la".
//
// Language profile: code=la, name=Latin, morph_type=fusional, word_order=SOV,
// question_strategy=intonation, script=latin, family=italic.
//
// Verb conjugation covered:
// Tenses: present, past (perfect active), future
// Persons: first/second/third x singular/plural (slots 0-5)
// Conjugations: 1st (-are), 2nd (-ere long), 3rd (-ere short), 4th (-ire)
// Irregulars: esse (be), ire (go), velle (want), posse (can)
// Canonical map: "be" -> "esse"
//
// Noun declension covered:
// Cases: nominative, accusative, genitive, dative, ablative
// Declensions: 1st (-a fem), 2nd masc (-us), 2nd neut (-um), 3rd (-is),
// 4th (-us), 5th (-es)
//
// Latin has no articles. la_noun_phrase returns the declined noun directly.
//
// Depends on: morphology.el (str_ends_with, str_len, str_slice, str_eq)
// String helpers
import "morphology.el"
fn la_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn la_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)
}
fn la_str_last_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 {
return ""
}
return str_slice(s, n - 1, n)
}
fn la_str_last2(s: String) -> String {
let n: Int = str_len(s)
if n < 2 {
return s
}
return str_slice(s, n - 2, n)
}
fn la_str_last3(s: String) -> String {
let n: Int = str_len(s)
if n < 3 {
return s
}
return str_slice(s, n - 3, n)
}
// Person/number slot
//
// Maps person x number to a 0-based slot index used in paradigm tables.
// 0 = 1st singular (ego)
// 1 = 2nd singular (tu)
// 2 = 3rd singular (is/ea/id)
// 3 = 1st plural (nos)
// 4 = 2nd plural (vos)
// 5 = 3rd plural (ei/eae/ea)
fn la_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
}
// Conjugation class detection
//
// Latin verbs fall into four conjugation classes identified by the infinitive
// ending. The 2nd and 3rd conjugations both end in -ere; the 2nd has a long
// e (stem vowel retained before -re), the 3rd has a short e (thematic vowel).
// Heuristic: if the verb stem before -ere ends in a consonant cluster or a
// single consonant preceded by a short vowel, treat as 3rd; otherwise 2nd.
// In practice the caller passes dictionary infinitives, so we check known 2nd
// conjugation markers (stem ends in -e before -re, i.e. the penult is e) and
// known 4th (-ire) vs 3rd (-ere with consonant stem).
//
// Simplified detection strategy:
// ends in -are -> "1"
// ends in -ire -> "4"
// ends in -ere -> check penultimate vowel pattern:
// stem + "ere" where stem ends in a vowel-like syllable -> "2"
// otherwise -> "3"
//
// For the small working vocabulary, we rely on the ending length heuristic:
// -ere with 2nd conj: the infinitive without -re has a long e as last char,
// meaning the four chars before the final "re" are "e-consonant" or "ee".
// Simpler: ends in -ere and has stem length >= 3 with last stem char a vowel
// -> 2nd; else 3rd. This correctly handles monere (2nd) vs dicere (3rd).
fn la_verb_class(verb: String) -> String {
if la_str_ends(verb, "are") { return "1" }
if la_str_ends(verb, "ire") { return "4" }
if la_str_ends(verb, "ere") {
// Strip -ere, look at the last char of the stem
let stem: String = la_str_drop_last(verb, 3)
let slen: Int = str_len(stem)
if slen == 0 { return "3" }
let last: String = str_slice(stem, slen - 1, slen)
// Stem ends in a vowel -> 2nd conjugation (monere, videre, habere)
if str_eq(last, "a") { return "2" }
if str_eq(last, "e") { return "2" }
if str_eq(last, "i") { return "2" }
if str_eq(last, "o") { return "2" }
if str_eq(last, "u") { return "2" }
// Stem ends in a consonant -> 3rd conjugation (dicere, edere, ducere)
return "3"
}
// Default: treat as 3rd if ending is unknown
return "3"
}
// la_stem: strip the infinitive ending to get the present stem.
//
// 1st: -are -> stem (amāre -> am-)
// 2nd: -ere -> stem + e (monēre -> mone-) [keep the stem vowel]
// 3rd: -ere -> stem (dicere -> dic-)
// 4th: -ire -> stem + i (audire -> audi-)
fn la_stem(verb: String, vclass: String) -> String {
if str_eq(vclass, "1") { return la_str_drop_last(verb, 3) }
if str_eq(vclass, "2") { return la_str_drop_last(verb, 2) } // drop -re; keep -e
if str_eq(vclass, "3") { return la_str_drop_last(verb, 3) }
if str_eq(vclass, "4") { return la_str_drop_last(verb, 2) } // drop -re; keep -i
return la_str_drop_last(verb, 3)
}
// la_perfect_stem: derive the perfect active stem.
//
// Latin perfect stems are highly irregular in general. For regular verbs the
// most common pattern is stem + v- (1st/4th) or stem + u- (2nd/3rd), but this
// is not universal. We use the following heuristic approximations:
//
// 1st conj: present stem + av- (amavi, portavi)
// 2nd conj: present stem + u- (monui, habui) -- drops the -e from stem
// 3rd conj: present stem + - (doubled root; context varies; use stem + i)
// 4th conj: present stem + iv- (audivi)
//
// This will be wrong for many common verbs; callers can override by adding the
// verb to the la_irregular_perfect table.
fn la_perfect_stem(verb: String, vclass: String) -> String {
if str_eq(vclass, "1") {
// amāre -> am + av = amav
let pstem: String = la_str_drop_last(verb, 3)
return pstem + "av"
}
if str_eq(vclass, "2") {
// monēre -> mone -> drop e -> mon + u = monu
let pstem: String = la_str_drop_last(verb, 3)
return pstem + "u"
}
if str_eq(vclass, "3") {
// dicere -> dic + (perfect varies; approximate with stem + -i stem)
let pstem: String = la_str_drop_last(verb, 3)
return pstem
}
if str_eq(vclass, "4") {
// audire -> audi + iv = audiv
let pstem: String = la_str_drop_last(verb, 2)
return pstem + "v"
}
return la_str_drop_last(verb, 3)
}
// Perfect active endings (all conjugations share these)
//
// Slot: 0=1sg 1=2sg 2=3sg 3=1pl 4=2pl 5=3pl
// -i, -isti, -it, -imus, -istis, -erunt
fn la_perfect_ending(slot: Int) -> String {
if slot == 0 { return "i" }
if slot == 1 { return "isti" }
if slot == 2 { return "it" }
if slot == 3 { return "imus" }
if slot == 4 { return "istis" }
return "erunt"
}
// Present tense endings
//
// 1st: -o, -as, -at, -amus, -atis, -ant
// 2nd: -eo, -es, -et, -emus, -etis, -ent
// 3rd: -o, -is, -it, -imus, -itis, -unt
// 4th: -io, -is, -it, -imus, -itis, -iunt
//
// Note: for 1st conj the stem already ends in the thematic vowel (am- not ama-
// at slot 0 because the -o absorbs it). The 2nd conj stem retains its -e and
// the ending is appended directly.
fn la_present_ending(vclass: String, slot: Int) -> String {
if str_eq(vclass, "1") {
if slot == 0 { return "o" }
if slot == 1 { return "as" }
if slot == 2 { return "at" }
if slot == 3 { return "amus" }
if slot == 4 { return "atis" }
return "ant"
}
if str_eq(vclass, "2") {
if slot == 0 { return "o" }
if slot == 1 { return "s" } // stem ends -e; -es becomes mones
if slot == 2 { return "t" } // monet
if slot == 3 { return "mus" } // monemus
if slot == 4 { return "tis" } // monetis
return "nt" // monent
}
if str_eq(vclass, "3") {
if slot == 0 { return "o" }
if slot == 1 { return "is" }
if slot == 2 { return "it" }
if slot == 3 { return "imus" }
if slot == 4 { return "itis" }
return "unt"
}
// 4th (-ire)
if slot == 0 { return "o" }
if slot == 1 { return "s" } // audi + s = audis
if slot == 2 { return "t" } // audit
if slot == 3 { return "mus" } // audimus
if slot == 4 { return "tis" } // auditis
return "unt" // audiunt
}
// la_present_form: build the present tense form for a regular verb.
//
// Special slot-0 handling per class:
// 1st: am- + o = amo (strip thematic -a from stem first)
// 2nd: mone- + o = moneo
// 3rd: dic- + o = dico
// 4th: audi- + o = audio (the -i stays as part of the stem)
fn la_present_form(stem: String, vclass: String, slot: Int) -> String {
if str_eq(vclass, "1") {
if slot == 0 {
// stem ends in the thematic -a which is absorbed by -o
return la_str_drop_last(stem, 1) + "o"
}
return stem + la_present_ending(vclass, slot)
}
if str_eq(vclass, "2") {
// stem ends in -e; all endings attach directly
return stem + la_present_ending(vclass, slot)
}
if str_eq(vclass, "3") {
if slot == 0 {
return stem + "o"
}
return stem + la_present_ending(vclass, slot)
}
// 4th: stem ends in -i
if slot == 0 {
return stem + "o" // audio
}
if slot == 5 {
return stem + "unt" // audiunt (special: i + unt)
}
return stem + la_present_ending(vclass, slot)
}
// Future tense
//
// 1st/2nd conjugations: present stem + bo/bis/bit/bimus/bitis/bunt
// (1st: thematic -a dropped before -bo; 2nd: -e kept, ebo -> moneo... actually
// for 2nd the future is mone + bo = monebo; stem keeps -e)
//
// 3rd/4th conjugations: present stem + am/es/et/emus/etis/ent
// (No thematic vowel linking; -am directly on consonant stem)
fn la_future_ending_12(slot: Int) -> String {
if slot == 0 { return "bo" }
if slot == 1 { return "bis" }
if slot == 2 { return "bit" }
if slot == 3 { return "bimus" }
if slot == 4 { return "bitis" }
return "bunt"
}
fn la_future_ending_34(slot: Int) -> String {
if slot == 0 { return "am" }
if slot == 1 { return "es" }
if slot == 2 { return "et" }
if slot == 3 { return "emus" }
if slot == 4 { return "etis" }
return "ent"
}
fn la_future_form(stem: String, vclass: String, slot: Int) -> String {
if str_eq(vclass, "1") {
// Drop thematic -a then add -bo etc: am- + bo = amabo? No: ama + bo = amabo
// Actually for 1st conj the stem IS the thematic-vowel stem (ama-),
// and the future is ama + bo = amabo.
return stem + la_future_ending_12(slot)
}
if str_eq(vclass, "2") {
// mone + bo = monebo
return stem + la_future_ending_12(slot)
}
if str_eq(vclass, "3") {
// dic + am = dicam
return stem + la_future_ending_34(slot)
}
// 4th: audi + am = audiam
return stem + la_future_ending_34(slot)
}
// Irregular verb tables
//
// esse (be), ire (go), velle (want), posse (can/be able)
fn la_esse_present(slot: Int) -> String {
if slot == 0 { return "sum" }
if slot == 1 { return "es" }
if slot == 2 { return "est" }
if slot == 3 { return "sumus" }
if slot == 4 { return "estis" }
return "sunt"
}
fn la_esse_past(slot: Int) -> String {
if slot == 0 { return "fui" }
if slot == 1 { return "fuisti" }
if slot == 2 { return "fuit" }
if slot == 3 { return "fuimus" }
if slot == 4 { return "fuistis" }
return "fuerunt"
}
fn la_esse_future(slot: Int) -> String {
if slot == 0 { return "ero" }
if slot == 1 { return "eris" }
if slot == 2 { return "erit" }
if slot == 3 { return "erimus" }
if slot == 4 { return "eritis" }
return "erunt"
}
fn la_ire_present(slot: Int) -> String {
if slot == 0 { return "eo" }
if slot == 1 { return "is" }
if slot == 2 { return "it" }
if slot == 3 { return "imus" }
if slot == 4 { return "itis" }
return "eunt"
}
fn la_ire_past(slot: Int) -> String {
if slot == 0 { return "ii" }
if slot == 1 { return "isti" } // contracted: iisti -> isti
if slot == 2 { return "iit" }
if slot == 3 { return "iimus" }
if slot == 4 { return "istis" }
return "ierunt"
}
fn la_ire_future(slot: Int) -> String {
if slot == 0 { return "ibo" }
if slot == 1 { return "ibis" }
if slot == 2 { return "ibit" }
if slot == 3 { return "ibimus" }
if slot == 4 { return "ibitis" }
return "ibunt"
}
fn la_velle_present(slot: Int) -> String {
if slot == 0 { return "volo" }
if slot == 1 { return "vis" }
if slot == 2 { return "vult" }
if slot == 3 { return "volumus" }
if slot == 4 { return "vultis" }
return "volunt"
}
fn la_velle_past(slot: Int) -> String {
if slot == 0 { return "volui" }
if slot == 1 { return "voluisti" }
if slot == 2 { return "voluit" }
if slot == 3 { return "voluimus" }
if slot == 4 { return "voluistis" }
return "voluerunt"
}
fn la_velle_future(slot: Int) -> String {
if slot == 0 { return "volam" }
if slot == 1 { return "voles" }
if slot == 2 { return "volet" }
if slot == 3 { return "volemus" }
if slot == 4 { return "voletis" }
return "volent"
}
fn la_posse_present(slot: Int) -> String {
if slot == 0 { return "possum" }
if slot == 1 { return "potes" }
if slot == 2 { return "potest" }
if slot == 3 { return "possumus" }
if slot == 4 { return "potestis" }
return "possunt"
}
fn la_posse_past(slot: Int) -> String {
if slot == 0 { return "potui" }
if slot == 1 { return "potuisti" }
if slot == 2 { return "potuit" }
if slot == 3 { return "potuimus" }
if slot == 4 { return "potuistis" }
return "potuerunt"
}
fn la_posse_future(slot: Int) -> String {
if slot == 0 { return "potero" }
if slot == 1 { return "poteris" }
if slot == 2 { return "poterit" }
if slot == 3 { return "poterimus" }
if slot == 4 { return "poteritis" }
return "poterunt"
}
// Irregular perfect stems for common verbs
//
// Returns the perfect stem for common irregular verbs, or "" if not found.
// When a perfect stem is returned, la_perfect_ending() is appended directly.
//
// Verb Perfect stem Example: 3sg
// edere ed- edit
// dicere dix- dixit
// ducere dux- duxit
// facere fec- fecit
// capere cep- cepit
// venire ven- venit (same as present; context clarifies)
// videre vid- vidit
// esse -> handled separately
// ire -> handled separately
fn la_irregular_perfect_stem(verb: String) -> String {
if str_eq(verb, "edere") { return "ed" }
if str_eq(verb, "dicere") { return "dix" }
if str_eq(verb, "ducere") { return "dux" }
if str_eq(verb, "facere") { return "fec" }
if str_eq(verb, "capere") { return "cep" }
if str_eq(verb, "venire") { return "ven" }
if str_eq(verb, "videre") { return "vid" }
if str_eq(verb, "bibere") { return "bib" }
if str_eq(verb, "currere") { return "cucurr" }
if str_eq(verb, "legere") { return "leg" }
if str_eq(verb, "scribere") { return "scrips" }
if str_eq(verb, "vivere") { return "vix" }
if str_eq(verb, "cadere") { return "cecid" }
if str_eq(verb, "ponere") { return "posu" }
if str_eq(verb, "querere") { return "quaesiv" }
return ""
}
// Canonical verb mapping
//
// The semantic layer passes English canonical labels ("be", "go", "want").
// Map these to Latin infinitives before conjugation.
fn la_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "esse" }
if str_eq(verb, "go") { return "ire" }
if str_eq(verb, "want") { return "velle" }
if str_eq(verb, "can") { return "posse" }
if str_eq(verb, "eat") { return "edere" }
if str_eq(verb, "say") { return "dicere" }
if str_eq(verb, "see") { return "videre" }
if str_eq(verb, "make") { return "facere" }
if str_eq(verb, "come") { return "venire" }
if str_eq(verb, "read") { return "legere" }
if str_eq(verb, "write") { return "scribere" }
if str_eq(verb, "run") { return "currere" }
if str_eq(verb, "live") { return "vivere" }
if str_eq(verb, "love") { return "amare" }
return verb
}
// la_conjugate: main conjugation entry point
//
// verb: Latin infinitive (e.g. "amare", "esse") or English canonical
// tense: "present" | "past" | "future"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
//
// Returns the inflected form. Falls back to the base (infinitive) when a form
// is not implemented rather than crashing.
fn la_conjugate(verb: String, tense: String, person: String, number: String) -> String {
// Map canonical English labels to Latin infinitives
let v: String = la_map_canonical(verb)
let slot: Int = la_slot(person, number)
// Irregulars
if str_eq(v, "esse") {
if str_eq(tense, "present") { return la_esse_present(slot) }
if str_eq(tense, "past") { return la_esse_past(slot) }
if str_eq(tense, "future") { return la_esse_future(slot) }
return v
}
if str_eq(v, "ire") {
if str_eq(tense, "present") { return la_ire_present(slot) }
if str_eq(tense, "past") { return la_ire_past(slot) }
if str_eq(tense, "future") { return la_ire_future(slot) }
return v
}
if str_eq(v, "velle") {
if str_eq(tense, "present") { return la_velle_present(slot) }
if str_eq(tense, "past") { return la_velle_past(slot) }
if str_eq(tense, "future") { return la_velle_future(slot) }
return v
}
if str_eq(v, "posse") {
if str_eq(tense, "present") { return la_posse_present(slot) }
if str_eq(tense, "past") { return la_posse_past(slot) }
if str_eq(tense, "future") { return la_posse_future(slot) }
return v
}
// Regular conjugation
let vclass: String = la_verb_class(v)
let stem: String = la_stem(v, vclass)
if str_eq(tense, "present") {
return la_present_form(stem, vclass, slot)
}
if str_eq(tense, "past") {
// Check for a known irregular perfect stem first
let irreg_perf: String = la_irregular_perfect_stem(v)
if !str_eq(irreg_perf, "") {
return irreg_perf + la_perfect_ending(slot)
}
// Regular perfect stem derivation
let perf_stem: String = la_perfect_stem(v, vclass)
return perf_stem + la_perfect_ending(slot)
}
if str_eq(tense, "future") {
return la_future_form(stem, vclass, slot)
}
// Unknown tense: return infinitive
return v
}
// Declension detection
//
// Infer Latin declension from the nominative singular ending.
//
// ends in -a -> 1st declension (feminine)
// ends in -us -> 2nd or 4th; disambiguate: if genitive suffix pattern
// suggests 4th (-us gen) we use 4th, otherwise 2nd masc.
// Heuristic: monosyllabic -us nouns and known 4th-decl words
// use 4th; longer -us words default to 2nd.
// ends in -um -> 2nd declension neuter
// ends in -is -> 3rd declension (genitive -is is also 3rd nom for some words;
// treat nom -is as 3rd)
// ends in -es -> 5th declension
// ends in -er -> 2nd masc (puer, ager)
// otherwise -> 3rd declension (consonant stems: rex, miles, etc.)
fn la_declension(noun: String) -> String {
if la_str_ends(noun, "a") { return "1" }
if la_str_ends(noun, "um") { return "2n" }
if la_str_ends(noun, "er") { return "2m" }
if la_str_ends(noun, "us") {
// 4th declension heuristic: check known 4th decl nouns
if str_eq(noun, "manus") { return "4" }
if str_eq(noun, "usus") { return "4" }
if str_eq(noun, "fructus") { return "4" }
if str_eq(noun, "gradus") { return "4" }
if str_eq(noun, "cursus") { return "4" }
if str_eq(noun, "sensus") { return "4" }
if str_eq(noun, "spiritus") { return "4" }
if str_eq(noun, "portus") { return "4" }
if str_eq(noun, "domus") { return "4" }
if str_eq(noun, "impetus") { return "4" }
// Default: 2nd masc
return "2m"
}
if la_str_ends(noun, "es") { return "5" }
if la_str_ends(noun, "is") { return "3" }
// Consonant-stem 3rd declension (rex, canis, leo, etc.)
return "3"
}
// 1st declension: -a nouns (mostly feminine)
//
// Stem: remove final -a
// Singular: nom -a gen -ae dat -ae acc -am abl -a
// Plural: nom -ae gen -arum dat -is acc -as abl -is
fn la_decline_1(stem: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return stem + "a" }
if str_eq(gram_case, "genitive") { return stem + "ae" }
if str_eq(gram_case, "dative") { return stem + "ae" }
if str_eq(gram_case, "accusative") { return stem + "am" }
if str_eq(gram_case, "ablative") { return stem + "a" }
// vocative same as nominative for 1st decl
return stem + "a"
}
// plural
if str_eq(gram_case, "nominative") { return stem + "ae" }
if str_eq(gram_case, "genitive") { return stem + "arum" }
if str_eq(gram_case, "dative") { return stem + "is" }
if str_eq(gram_case, "accusative") { return stem + "as" }
if str_eq(gram_case, "ablative") { return stem + "is" }
return stem + "ae"
}
// 2nd declension masculine: -us nouns
//
// Stem: remove final -us
// Singular: nom -us gen -i dat -o acc -um abl -o
// Plural: nom -i gen -orum dat -is acc -os abl -is
fn la_decline_2m(stem: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return stem + "us" }
if str_eq(gram_case, "genitive") { return stem + "i" }
if str_eq(gram_case, "dative") { return stem + "o" }
if str_eq(gram_case, "accusative") { return stem + "um" }
if str_eq(gram_case, "ablative") { return stem + "o" }
return stem + "us"
}
// plural
if str_eq(gram_case, "nominative") { return stem + "i" }
if str_eq(gram_case, "genitive") { return stem + "orum" }
if str_eq(gram_case, "dative") { return stem + "is" }
if str_eq(gram_case, "accusative") { return stem + "os" }
if str_eq(gram_case, "ablative") { return stem + "is" }
return stem + "i"
}
// 2nd declension neuter: -um nouns
//
// Stem: remove final -um
// Singular: nom/acc -um gen -i dat/abl -o
// Plural: nom/acc -a gen -orum dat/abl -is
fn la_decline_2n(stem: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return stem + "um" }
if str_eq(gram_case, "genitive") { return stem + "i" }
if str_eq(gram_case, "dative") { return stem + "o" }
if str_eq(gram_case, "accusative") { return stem + "um" }
if str_eq(gram_case, "ablative") { return stem + "o" }
return stem + "um"
}
// plural
if str_eq(gram_case, "nominative") { return stem + "a" }
if str_eq(gram_case, "genitive") { return stem + "orum" }
if str_eq(gram_case, "dative") { return stem + "is" }
if str_eq(gram_case, "accusative") { return stem + "a" }
if str_eq(gram_case, "ablative") { return stem + "is" }
return stem + "a"
}
// 3rd declension: consonant and i-stem nouns
//
// The 3rd declension is highly varied; the nominative singular is usually
// irregular (the stem is seen in the genitive). The caller passes the
// nominative singular form; we use it as-is for nominative and treat it as
// the stem for other cases (approximation for productive NLG use).
//
// Singular: nom (unchanged) gen -is dat -i acc -em abl -e
// Plural: nom -es gen -um dat -ibus acc -es abl -ibus
//
// For i-stems (is ending in nom sg) we keep the full form as the stem and
// add endings directly to the base minus -is.
fn la_decline_3(noun: String, gram_case: String, number: String) -> String {
// For 3rd decl the nom sg is given; the stem for oblique cases is
// derived by stripping -is if the nom ends in -is, otherwise we use
// the noun as the stem for the oblique and append endings.
let oblique_stem: String = ""
if la_str_ends(noun, "is") {
let oblique_stem = la_str_drop_last(noun, 2)
} else {
let oblique_stem = noun
}
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return noun }
if str_eq(gram_case, "genitive") { return oblique_stem + "is" }
if str_eq(gram_case, "dative") { return oblique_stem + "i" }
if str_eq(gram_case, "accusative") { return oblique_stem + "em" }
if str_eq(gram_case, "ablative") { return oblique_stem + "e" }
return noun
}
// plural
if str_eq(gram_case, "nominative") { return oblique_stem + "es" }
if str_eq(gram_case, "genitive") { return oblique_stem + "um" }
if str_eq(gram_case, "dative") { return oblique_stem + "ibus" }
if str_eq(gram_case, "accusative") { return oblique_stem + "es" }
if str_eq(gram_case, "ablative") { return oblique_stem + "ibus" }
return oblique_stem + "es"
}
// 4th declension: -us nouns (mostly masculine)
//
// Stem: remove final -us
// Singular: nom -us gen -us dat -ui acc -um abl -u
// Plural: nom -us gen -uum dat -ibus acc -us abl -ibus
fn la_decline_4(stem: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return stem + "us" }
if str_eq(gram_case, "genitive") { return stem + "us" }
if str_eq(gram_case, "dative") { return stem + "ui" }
if str_eq(gram_case, "accusative") { return stem + "um" }
if str_eq(gram_case, "ablative") { return stem + "u" }
return stem + "us"
}
// plural
if str_eq(gram_case, "nominative") { return stem + "us" }
if str_eq(gram_case, "genitive") { return stem + "uum" }
if str_eq(gram_case, "dative") { return stem + "ibus" }
if str_eq(gram_case, "accusative") { return stem + "us" }
if str_eq(gram_case, "ablative") { return stem + "ibus" }
return stem + "us"
}
// 5th declension: -es nouns (mostly feminine)
//
// Stem: remove final -es
// Singular: nom -es gen -ei dat -ei acc -em abl -e
// Plural: nom -es gen -erum dat -ebus acc -es abl -ebus
fn la_decline_5(stem: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return stem + "es" }
if str_eq(gram_case, "genitive") { return stem + "ei" }
if str_eq(gram_case, "dative") { return stem + "ei" }
if str_eq(gram_case, "accusative") { return stem + "em" }
if str_eq(gram_case, "ablative") { return stem + "e" }
return stem + "es"
}
// plural
if str_eq(gram_case, "nominative") { return stem + "es" }
if str_eq(gram_case, "genitive") { return stem + "erum" }
if str_eq(gram_case, "dative") { return stem + "ebus" }
if str_eq(gram_case, "accusative") { return stem + "es" }
if str_eq(gram_case, "ablative") { return stem + "ebus" }
return stem + "es"
}
// 2nd declension -er nouns
//
// Nouns like "puer" retain -e throughout; nouns like "ager" drop it in oblique.
// Heuristic: if the stem (drop -er) + er would be the original, check if the
// penultimate char before -er is also r (like "puer": stem "pu" + "er" = puer)
// vs "ager": stem "agr" drops e. We default to retaining -e (puer pattern)
// for simplicity; "ager" type is less common.
//
// Singular: nom -er gen -i dat -o acc -um abl -o
// Plural: nom -i gen -orum dat -is acc -os abl -is
fn la_decline_2er(noun: String, gram_case: String, number: String) -> String {
// Oblique stem: drop -er then add "r" to get true stem? For simplicity
// use the form with -e retained (puer pattern): stem = noun minus "r".
// This correctly handles "puer" -> "pueri", "puero" etc.
// For ager-type the caller would need to pass the stem form; we approximate.
let stem: String = la_str_drop_last(noun, 1) // drop final -r -> "pue"
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return noun }
if str_eq(gram_case, "genitive") { return stem + "ri" } // pueri
if str_eq(gram_case, "dative") { return stem + "ro" } // puero
if str_eq(gram_case, "accusative") { return stem + "rum" } // puerum
if str_eq(gram_case, "ablative") { return stem + "ro" } // puero
return noun
}
// plural
if str_eq(gram_case, "nominative") { return stem + "ri" } // pueri
if str_eq(gram_case, "genitive") { return stem + "rorum" } // puerorum
if str_eq(gram_case, "dative") { return stem + "ris" } // pueris
if str_eq(gram_case, "accusative") { return stem + "ros" } // pueros
if str_eq(gram_case, "ablative") { return stem + "ris" } // pueris
return stem + "ri"
}
// la_decline: main declension entry point
//
// noun: nominative singular Latin noun (e.g. "feles", "aqua", "dominus")
// gram_case: "nominative" | "accusative" | "genitive" | "dative" | "ablative"
// number: "singular" | "plural"
//
// Returns the inflected form. Falls back to the nominative singular when a
// form is not implemented.
fn la_decline(noun: String, gram_case: String, number: String) -> String {
let decl: String = la_declension(noun)
if str_eq(decl, "1") {
let stem: String = la_str_drop_last(noun, 1)
return la_decline_1(stem, gram_case, number)
}
if str_eq(decl, "2m") {
let stem: String = la_str_drop_last(noun, 2)
return la_decline_2m(stem, gram_case, number)
}
if str_eq(decl, "2n") {
let stem: String = la_str_drop_last(noun, 2)
return la_decline_2n(stem, gram_case, number)
}
if str_eq(decl, "2er") {
return la_decline_2er(noun, gram_case, number)
}
if str_eq(decl, "3") {
return la_decline_3(noun, gram_case, number)
}
if str_eq(decl, "4") {
let stem: String = la_str_drop_last(noun, 2)
return la_decline_4(stem, gram_case, number)
}
if str_eq(decl, "5") {
let stem: String = la_str_drop_last(noun, 2)
return la_decline_5(stem, gram_case, number)
}
// Unknown declension: return nominative unchanged
return noun
}
// la_noun_phrase: noun phrase builder
//
// Latin has no definite or indefinite articles. The declined noun form is the
// complete noun phrase. The definite parameter is accepted for interface
// compatibility with other language modules but has no effect.
//
// noun: nominative singular Latin noun
// gram_case: "nominative" | "accusative" | "genitive" | "dative" | "ablative"
// number: "singular" | "plural"
// definite: ignored (Latin has no articles)
fn la_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
return la_decline(noun, gram_case, number)
}