c2cd5e01e1
El SDK CI - dev / build-and-test (pull_request) Successful in 3m34s
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
873 lines
34 KiB
EmacsLisp
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)
|
|
}
|