Files
el/elp/src/morphology-sa.el
2026-05-02 22:15:25 -05:00

652 lines
26 KiB
EmacsLisp
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// morphology-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)
}