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
+651
View File
@@ -0,0 +1,651 @@
// morphology-sa.el - Sanskrit morphology for the NLG engine.
//
// Implements Sanskrit verb conjugation and noun declension using IAST
// transliteration as the primary form. Designed as a companion to
// morphology.el and called by the engine when the language profile code
// is "sa".
//
// Language profile: code=sa, name=Sanskrit, morph_type=fusional,
// word_order=SOV, question_strategy=intonation, script=devanagari,
// family=indo-aryan.
//
// Verb conjugation covered:
// Tenses: present (laṭ), past (imperfect laṅ), future (lṛṭ)
// Persons: first/second/third × singular/plural
// (dual is treated as plural throughout see sa_slot)
// Classes: Class 1 (bhū-adi, stem + a + endings) as the regular path
// Irregulars: as, bhū, gam, dṛś, vad, kṛ (the core NLG vocabulary)
// Canonical map: "be" -> "as"
//
// Noun declension covered:
// Cases: nominative, accusative, instrumental, dative, ablative,
// genitive, locative, vocative (all 8 Sanskrit cases)
// Stem types: a-stem masculine (paradigm: deva),
// ā-stem feminine (paradigm: devī)
// Numbers: singular, plural (dual collapsed to plural)
//
// Sanskrit has no articles. sa_noun_phrase returns the declined noun
// directly.
//
// Notes on IAST diacritics used throughout this file:
// ā ī ū long vowels
// vocalic r
// anusvāra (nasalisation / homorganic nasal)
// visarga (final breath)
// ś palatal / retroflex sibilants
// retroflex stops and nasal
// ñ palatal nasal
// Sandhi is intentionally suppressed forms are returned in their
// isolated (pausa) shapes so the NLG realizer can apply sandhi later.
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with)
// String helpers
fn sa_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn sa_str_drop_last(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len {
return ""
}
return str_slice(s, 0, len - n)
}
// Person/number slot
//
// Maps person × number to a 0-based index into paradigm arrays.
//
// 0 = 1st singular (uttama eka)
// 1 = 2nd singular (madhyama eka)
// 2 = 3rd singular (prathama eka)
// 3 = 1st plural (uttama bahu)
// 4 = 2nd plural (madhyama bahu)
// 5 = 3rd plural (prathama bahu)
//
// Sanskrit has a dual number but for NLG simplicity the dual is collapsed:
// "dual" inputs return the same slot as "plural". Forms in this file
// therefore carry plural endings even when the dual was requested.
fn sa_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
//
// Semantic-layer canonical English labels are mapped to IAST dictionary
// entries before conjugation. The dictionary entry is then looked up in
// the irregular table; unknown entries fall through to the Class-1 path.
fn sa_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "as" }
if str_eq(verb, "become") { return "bhu" }
if str_eq(verb, "go") { return "gam" }
if str_eq(verb, "see") { return "drs" }
if str_eq(verb, "speak") { return "vad" }
if str_eq(verb, "say") { return "vad" }
if str_eq(verb, "do") { return "kr" }
if str_eq(verb, "make") { return "kr" }
return verb
}
// Irregular verb: as (to be)
//
// Present (laṭ) parasmaipada:
// 1sg asmi 2sg asi 3sg asti
// 1pl smaḥ 2pl stha 3pl santi
//
// Imperfect (laṅ) parasmaipada:
// 1sg āsam 2sg āsīḥ 3sg āsīt
// 1pl āsma 2pl āsta 3pl āsan
fn sa_as_present(slot: Int) -> String {
if slot == 0 { return "asmi" }
if slot == 1 { return "asi" }
if slot == 2 { return "asti" }
if slot == 3 { return "smaḥ" }
if slot == 4 { return "stha" }
return "santi"
}
fn sa_as_past(slot: Int) -> String {
if slot == 0 { return "āsam" }
if slot == 1 { return "āsīḥ" }
if slot == 2 { return "āsīt" }
if slot == 3 { return "āsma" }
if slot == 4 { return "āsta" }
return "āsan"
}
// Future (lṛṭ) of as: bhaviṣyāmi series (uses bhū as suppletive stem)
fn sa_as_future(slot: Int) -> String {
if slot == 0 { return "bhaviṣyāmi" }
if slot == 1 { return "bhaviṣyasi" }
if slot == 2 { return "bhaviṣyati" }
if slot == 3 { return "bhaviṣyāmaḥ" }
if slot == 4 { return "bhaviṣyatha" }
return "bhaviṣyanti"
}
// Irregular verb: bhū (to be, to become)
//
// Class 1; present stem bho → bhava (guṇa of u before -a-).
//
// Present (laṭ):
// 1sg bhavāmi 2sg bhavasi 3sg bhavati
// 1pl bhavāmaḥ 2pl bhavatha 3pl bhavanti
//
// Imperfect (laṅ):
// 1sg abhavam 2sg abhavaḥ 3sg abhavat
// 1pl abhavāma 2pl abhavata 3pl abhavan
//
// Future (lṛṭ): regular from bhaviṣya-
fn sa_bhu_present(slot: Int) -> String {
if slot == 0 { return "bhavāmi" }
if slot == 1 { return "bhavasi" }
if slot == 2 { return "bhavati" }
if slot == 3 { return "bhavāmaḥ" }
if slot == 4 { return "bhavatha" }
return "bhavanti"
}
fn sa_bhu_past(slot: Int) -> String {
if slot == 0 { return "abhavam" }
if slot == 1 { return "abhavaḥ" }
if slot == 2 { return "abhavat" }
if slot == 3 { return "abhavāma" }
if slot == 4 { return "abhavata" }
return "abhavan"
}
fn sa_bhu_future(slot: Int) -> String {
if slot == 0 { return "bhaviṣyāmi" }
if slot == 1 { return "bhaviṣyasi" }
if slot == 2 { return "bhaviṣyati" }
if slot == 3 { return "bhaviṣyāmaḥ" }
if slot == 4 { return "bhaviṣyatha" }
return "bhaviṣyanti"
}
// Irregular verb: gam (to go)
//
// Historically Class 1 with the present stem gaccha- (inserted -ccha-).
//
// Present (laṭ):
// gacchāmi gacchasi gacchati
// gacchāmaḥ gacchatha gacchanti
//
// Imperfect (laṅ): augmented agaccha-
//
// Future: gamiṣyati series
fn sa_gam_present(slot: Int) -> String {
if slot == 0 { return "gacchāmi" }
if slot == 1 { return "gacchasi" }
if slot == 2 { return "gacchati" }
if slot == 3 { return "gacchāmaḥ" }
if slot == 4 { return "gacchatha" }
return "gacchanti"
}
fn sa_gam_past(slot: Int) -> String {
if slot == 0 { return "agaccham" }
if slot == 1 { return "agacchaḥ" }
if slot == 2 { return "agacchat" }
if slot == 3 { return "agacchāma" }
if slot == 4 { return "agacchata" }
return "agacchan"
}
fn sa_gam_future(slot: Int) -> String {
if slot == 0 { return "gamiṣyāmi" }
if slot == 1 { return "gamiṣyasi" }
if slot == 2 { return "gamiṣyati" }
if slot == 3 { return "gamiṣyāmaḥ" }
if slot == 4 { return "gamiṣyatha" }
return "gamiṣyanti"
}
// Irregular verb: dṛś (to see)
//
// Suppletive present stem paśya- (Class 4 / ātmanepada suppletive).
// Used in the active (parasmaipada) sense throughout for NLG simplicity.
//
// Present: paśyāmi paśyasi paśyati paśyāmaḥ paśyatha paśyanti
// Imperfect: apaśyam series
// Future: drakṣyati series
fn sa_drs_present(slot: Int) -> String {
if slot == 0 { return "paśyāmi" }
if slot == 1 { return "paśyasi" }
if slot == 2 { return "paśyati" }
if slot == 3 { return "paśyāmaḥ" }
if slot == 4 { return "paśyatha" }
return "paśyanti"
}
fn sa_drs_past(slot: Int) -> String {
if slot == 0 { return "apaśyam" }
if slot == 1 { return "apaśyaḥ" }
if slot == 2 { return "apaśyat" }
if slot == 3 { return "apaśyāma" }
if slot == 4 { return "apaśyata" }
return "apaśyan"
}
fn sa_drs_future(slot: Int) -> String {
if slot == 0 { return "drakṣyāmi" }
if slot == 1 { return "drakṣyasi" }
if slot == 2 { return "drakṣyati" }
if slot == 3 { return "drakṣyāmaḥ" }
if slot == 4 { return "drakṣyatha" }
return "drakṣyanti"
}
// Irregular verb: vad (to speak, to say)
//
// Class 1; present stem vada-.
//
// Present: vadāmi vadasi vadati vadāmaḥ vadatha vadanti
// Imperfect: avadam series
// Future: vadiṣyati series
fn sa_vad_present(slot: Int) -> String {
if slot == 0 { return "vadāmi" }
if slot == 1 { return "vadasi" }
if slot == 2 { return "vadati" }
if slot == 3 { return "vadāmaḥ" }
if slot == 4 { return "vadatha" }
return "vadanti"
}
fn sa_vad_past(slot: Int) -> String {
if slot == 0 { return "avadam" }
if slot == 1 { return "avadaḥ" }
if slot == 2 { return "avadat" }
if slot == 3 { return "avadāma" }
if slot == 4 { return "avadata" }
return "avadan"
}
fn sa_vad_future(slot: Int) -> String {
if slot == 0 { return "vadiṣyāmi" }
if slot == 1 { return "vadiṣyasi" }
if slot == 2 { return "vadiṣyati" }
if slot == 3 { return "vadiṣyāmaḥ" }
if slot == 4 { return "vadiṣyatha" }
return "vadiṣyanti"
}
// Irregular verb: kṛ (to do, to make)
//
// Class 8 (tanādi); highly irregular. Present stem karo- (sg) / kuru- (pl).
//
// Present:
// 1sg karomi 2sg karoṣi 3sg karoti
// 1pl kurmaḥ 2pl kurutha 3pl kurvanti
//
// Imperfect: akaro- / akuru-
// Future: kariṣyati series
fn sa_kr_present(slot: Int) -> String {
if slot == 0 { return "karomi" }
if slot == 1 { return "karoṣi" }
if slot == 2 { return "karoti" }
if slot == 3 { return "kurmaḥ" }
if slot == 4 { return "kurutha" }
return "kurvanti"
}
fn sa_kr_past(slot: Int) -> String {
if slot == 0 { return "akaravam" }
if slot == 1 { return "akarodaḥ" }
if slot == 2 { return "akarot" }
if slot == 3 { return "akurma" }
if slot == 4 { return "akuruta" }
return "akurvan"
}
fn sa_kr_future(slot: Int) -> String {
if slot == 0 { return "kariṣyāmi" }
if slot == 1 { return "kariṣyasi" }
if slot == 2 { return "kariṣyati" }
if slot == 3 { return "kariṣyāmaḥ" }
if slot == 4 { return "kariṣyatha" }
return "kariṣyanti"
}
// Class-1 regular conjugation (bhū-adi)
//
// The thematic class: root guṇa-strengthened root + a + personal ending.
// Present endings (parasmaipada):
// 1sg -āmi 2sg -asi 3sg -ati
// 1pl -āmaḥ 2pl -atha 3pl -anti
//
// Imperfect (laṅ) = augment a- + stem + imperfect endings:
// 1sg -am 2sg -aḥ 3sg -at
// 1pl -āma 2pl -ata 3pl -an
//
// Future (lṛṭ) = stem + iṣya + present personal endings
//
// The caller supplies the present-tense verbal stem (e.g. "bodha" for
// "to know/wake"). We do not derive stems automatically from roots for
// arbitrary input only the known irregular verbs above have rootstem
// derivation. Unknown verbs are conjugated as if their input IS the stem.
fn sa_class1_present_ending(slot: Int) -> String {
if slot == 0 { return "āmi" }
if slot == 1 { return "asi" }
if slot == 2 { return "ati" }
if slot == 3 { return "āmaḥ" }
if slot == 4 { return "atha" }
return "anti"
}
fn sa_class1_past_ending(slot: Int) -> String {
if slot == 0 { return "am" }
if slot == 1 { return "aḥ" }
if slot == 2 { return "at" }
if slot == 3 { return "āma" }
if slot == 4 { return "ata" }
return "an"
}
fn sa_class1_future_ending(slot: Int) -> String {
if slot == 0 { return "iṣyāmi" }
if slot == 1 { return "iṣyasi" }
if slot == 2 { return "iṣyati" }
if slot == 3 { return "iṣyāmaḥ" }
if slot == 4 { return "iṣyatha" }
return "iṣyanti"
}
fn sa_class1_conjugate(stem: String, tense: String, slot: Int) -> String {
if str_eq(tense, "present") {
return stem + sa_class1_present_ending(slot)
}
if str_eq(tense, "past") {
return "a" + stem + sa_class1_past_ending(slot)
}
if str_eq(tense, "future") {
return stem + sa_class1_future_ending(slot)
}
return stem
}
// sa_conjugate: main conjugation entry point
//
// verb: IAST form (e.g. "gam", "as") or English canonical ("be", "go")
// tense: "present" | "past" | "future"
// person: "first" | "second" | "third"
// number: "singular" | "plural" (or "dual" treated as plural)
//
// Returns the inflected form in IAST. Falls back to the verb stem for any
// unknown input rather than crashing.
fn sa_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let v: String = sa_map_canonical(verb)
let slot: Int = sa_slot(person, number)
// Irregular: as (to be)
if str_eq(v, "as") {
if str_eq(tense, "present") { return sa_as_present(slot) }
if str_eq(tense, "past") { return sa_as_past(slot) }
if str_eq(tense, "future") { return sa_as_future(slot) }
return v
}
// Irregular: bhū (to be/become)
if str_eq(v, "bhu") {
if str_eq(tense, "present") { return sa_bhu_present(slot) }
if str_eq(tense, "past") { return sa_bhu_past(slot) }
if str_eq(tense, "future") { return sa_bhu_future(slot) }
return v
}
// Irregular: gam (to go)
if str_eq(v, "gam") {
if str_eq(tense, "present") { return sa_gam_present(slot) }
if str_eq(tense, "past") { return sa_gam_past(slot) }
if str_eq(tense, "future") { return sa_gam_future(slot) }
return v
}
// Irregular: dṛś / drs (to see)
if str_eq(v, "drs") {
if str_eq(tense, "present") { return sa_drs_present(slot) }
if str_eq(tense, "past") { return sa_drs_past(slot) }
if str_eq(tense, "future") { return sa_drs_future(slot) }
return v
}
// Irregular: vad (to speak/say)
if str_eq(v, "vad") {
if str_eq(tense, "present") { return sa_vad_present(slot) }
if str_eq(tense, "past") { return sa_vad_past(slot) }
if str_eq(tense, "future") { return sa_vad_future(slot) }
return v
}
// Irregular: kṛ / kr (to do/make)
if str_eq(v, "kr") {
if str_eq(tense, "present") { return sa_kr_present(slot) }
if str_eq(tense, "past") { return sa_kr_past(slot) }
if str_eq(tense, "future") { return sa_kr_future(slot) }
return v
}
// Regular Class-1 fallback
// Treat the supplied string as a present-tense verbal stem and apply the
// standard thematic endings. This handles any verb the caller passes in
// the form "<stem>" without a recognised root tag.
return sa_class1_conjugate(v, tense, slot)
}
// a-stem masculine paradigm (deva)
//
// Stems of the deva- type are the most numerous Sanskrit noun class.
// All eight cases × singular and plural are encoded below.
//
// Singular:
// nom deva-ḥ "devaḥ" (visarga in citation; use bare form here)
// acc deva-m "devam"
// ins deva-na "devena" (guṇa: a+nena)
// dat deva-āya "devāya"
// abl deva-āt "devāt"
// gen deva-sya "devasya"
// loc deva-e "deve"
// voc deva "deva" (bare stem)
//
// Plural:
// nom deva-āḥ "devāḥ"
// acc deva-ān "devān"
// ins deva-aiḥ "devaiḥ"
// dat deva-bhyaḥ "devebhyaḥ" (with connecting -e-)
// abl deva-bhyaḥ "devebhyaḥ" (dat=abl in plural for a-stems)
// gen deva-ānām "devānām"
// loc deva-eṣu "deveṣu"
// voc deva-āḥ "devāḥ"
fn sa_decline_a_stem_sg(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "" }
if str_eq(gram_case, "accusative") { return stem + "m" }
if str_eq(gram_case, "instrumental") { return stem + "ena" }
if str_eq(gram_case, "dative") { return stem + "āya" }
if str_eq(gram_case, "ablative") { return stem + "āt" }
if str_eq(gram_case, "genitive") { return stem + "sya" }
if str_eq(gram_case, "locative") { return stem + "e" }
if str_eq(gram_case, "vocative") { return stem }
return stem
}
fn sa_decline_a_stem_pl(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "āḥ" }
if str_eq(gram_case, "accusative") { return stem + "ān" }
if str_eq(gram_case, "instrumental") { return stem + "aiḥ" }
if str_eq(gram_case, "dative") { return stem + "ebhyaḥ" }
if str_eq(gram_case, "ablative") { return stem + "ebhyaḥ" }
if str_eq(gram_case, "genitive") { return stem + "ānām" }
if str_eq(gram_case, "locative") { return stem + "eṣu" }
if str_eq(gram_case, "vocative") { return stem + "āḥ" }
return stem + "āḥ"
}
// ā-stem feminine paradigm (devī / nārī type)
//
// ā-stems are the primary feminine class. Paradigm for nārī (woman):
//
// Singular:
// nom nārī acc nārīm ins nāryā
// dat nāryai abl nāryāḥ gen nāryāḥ
// loc nāryām voc nāri
//
// Plural:
// nom nāryaḥ acc nārīḥ ins nārībhiḥ
// dat nārībhyaḥ abl nārībhyaḥ gen nārīṇām
// loc nārīṣu voc nāryaḥ
//
// For input the caller passes the nominative singular form (e.g. "nārī").
// We strip the final ī to obtain the stem for oblique formation.
fn sa_decline_aa_stem_sg(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "ī" }
if str_eq(gram_case, "accusative") { return stem + "īm" }
if str_eq(gram_case, "instrumental") { return stem + "" }
if str_eq(gram_case, "dative") { return stem + "yai" }
if str_eq(gram_case, "ablative") { return stem + "yāḥ" }
if str_eq(gram_case, "genitive") { return stem + "yāḥ" }
if str_eq(gram_case, "locative") { return stem + "yām" }
if str_eq(gram_case, "vocative") { return stem + "i" }
return stem + "ī"
}
fn sa_decline_aa_stem_pl(stem: String, gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return stem + "yaḥ" }
if str_eq(gram_case, "accusative") { return stem + "īḥ" }
if str_eq(gram_case, "instrumental") { return stem + "ībhiḥ" }
if str_eq(gram_case, "dative") { return stem + "ībhyaḥ" }
if str_eq(gram_case, "ablative") { return stem + "ībhyaḥ" }
if str_eq(gram_case, "genitive") { return stem + "īṇām" }
if str_eq(gram_case, "locative") { return stem + "īṣu" }
if str_eq(gram_case, "vocative") { return stem + "yaḥ" }
return stem + "yaḥ"
}
// Stem-type detection
//
// Infers the stem class from the nominative singular form supplied by the
// caller. Sanskrit stems are conventionally cited in their nom-sg form.
//
// Heuristics (sufficient for the NLG working vocabulary):
// ends in ā -> ā-stem feminine
// ends in ī -> ā-stem feminine (long ī subtype)
// ends in aḥ -> a-stem masculine (visarga ending from -as)
// ends in a -> treat as a-stem masculine (bare stem supplied)
// otherwise -> return base form as-is (unknown/consonant stem fallback)
fn sa_stem_type(noun: String) -> String {
if sa_str_ends(noun, "ā") { return "aa" }
if sa_str_ends(noun, "ī") { return "aa" }
if sa_str_ends(noun, "aḥ") { return "a" }
if sa_str_ends(noun, "a") { return "a" }
return "unknown"
}
// sa_extract_stem: strip the nominative-singular suffix to get the bare stem.
//
// a-stem "deva" -> "dev" (if ends in bare -a; strip final char)
// "devaḥ" -> "dev" (strip -aḥ = 2 Unicode code-points but
// since is multi-byte we treat "aḥ" as suffix)
// ā-stem "nārī" -> "nār" (strip )
// "nārā" -> "nār" (strip )
//
// Because IAST diacritics are multi-byte UTF-8 the raw byte lengths do not
// equal character counts. The engine's str_len / str_slice operate on bytes.
// Rather than counting UTF-8 bytes for each diacritic here we take a simpler
// path: we look for a known suffix and drop a fixed number of characters.
// For the characters used:
// ā = 2 bytes (U+0101)
// ī = 2 bytes (U+012B)
// = 3 bytes (U+1E25)
// So "aḥ" = 1 + 3 = 4 bytes; bare "a" = 1 byte; "ā" = 2 bytes; "ī" = 2 bytes.
//
// The function uses str_ends_with for detection, then str_slice to strip.
fn sa_extract_stem(noun: String, stype: String) -> String {
let n: Int = str_len(noun)
if str_eq(stype, "a") {
// Check whether it ends in "aḥ" (visarga form): 4 bytes to strip
if sa_str_ends(noun, "aḥ") {
return str_slice(noun, 0, n - 4)
}
// Otherwise bare -a: 1 byte
return str_slice(noun, 0, n - 1)
}
if str_eq(stype, "aa") {
// ī or ā: both 2 bytes
return str_slice(noun, 0, n - 2)
}
return noun
}
// sa_decline: main declension entry point
//
// noun: nominative singular IAST form (e.g. "deva", "devaḥ", "nārī")
// gram_case: "nominative" | "accusative" | "instrumental" | "dative" |
// "ablative" | "genitive" | "locative" | "vocative"
// number: "singular" | "plural" (dual plural)
//
// Returns the inflected form. Unknown stems return the noun unchanged.
fn sa_decline(noun: String, gram_case: String, number: String) -> String {
let stype: String = sa_stem_type(noun)
if str_eq(stype, "a") {
let stem: String = sa_extract_stem(noun, "a")
if str_eq(number, "singular") { return sa_decline_a_stem_sg(stem, gram_case) }
return sa_decline_a_stem_pl(stem, gram_case)
}
if str_eq(stype, "aa") {
let stem: String = sa_extract_stem(noun, "aa")
if str_eq(number, "singular") { return sa_decline_aa_stem_sg(stem, gram_case) }
return sa_decline_aa_stem_pl(stem, gram_case)
}
// Unknown stem class: return noun unchanged rather than producing garbage
return noun
}
// sa_noun_phrase: noun phrase builder
//
// Sanskrit has no articles neither definite nor indefinite. The definite
// parameter is accepted for interface compatibility with other language modules
// but has no effect on the output.
//
// Sanskrit expresses definiteness and referential status through word order,
// demonstratives (etad / tad), and discourse context none of which is the
// responsibility of the morphology module.
//
// noun: nominative singular IAST form
// gram_case: "nominative" | "accusative" | "instrumental" | "dative" |
// "ablative" | "genitive" | "locative" | "vocative"
// number: "singular" | "plural"
// definite: ignored
fn sa_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
return sa_decline(noun, gram_case, number)
}