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

631 lines
27 KiB
EmacsLisp

// morphology-hi.el - Hindi morphology for the NLG engine.
//
// Implements Hindi noun declension (direct and oblique cases), postpositional
// particles, verb stem extraction, tense conjugation, and genitive agreement.
//
// Hindi is a fusional/agglutinative hybrid, SOV, pro-drop, with Devanagari script.
//
// Key facts:
// - 2 genders: masculine (m), feminine (f)
// - 2 numbers: singular (sg), plural (pl)
// - 2 main cases: direct (nominative/accusative) and oblique (before postpositions)
// - Verbs agree with subject in gender and number
// - No articles
//
// Conventions used throughout:
// gender: "m" | "f"
// number: "sg" | "pl"
// case: "direct" | "oblique"
// tense: "present" | "past" | "future"
// person: "1" | "2" | "3"
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with,
// str_drop_last)
// String helpers
fn hi_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn hi_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 hi_str_last_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 {
return ""
}
return str_slice(s, n - 1, n)
}
// Gender inference
//
// Heuristic from the noun's final Devanagari character.
// Masculine: typically ends in (aa), or a consonant cluster
// Feminine: typically ends in (ii), ि (i-matra), or other endings
//
// This is an approximation gender is inherent to each noun and not always
// predictable. The caller should supply gender explicitly when known.
//
// Common feminine endings: "" (long ii), "ि" (short i matra in some forms),
// "" (like बहन), "" (like ), "" (like )
// Common masculine endings: "" (aa matra), consonants generally
fn hi_gender(noun: String) -> String {
// Long ii ending strongly feminine
if hi_str_ends(noun, "") { return "f" }
// aa matra ending strongly masculine
if hi_str_ends(noun, "") { return "m" }
// Known feminine words by ending
if hi_str_ends(noun, "") {
// बहन (sister), दुक (shop) feminine; but also many masc.
// Default feminine for -न common feminine nouns
return "f"
}
if hi_str_ends(noun, "") { return "f" } // त, त,
if hi_str_ends(noun, "") { return "f" } // (variant spellings)
if hi_str_ends(noun, "") { return "m" } // आक etc.
// Specific common words
if str_eq(noun, "लड़का") { return "m" }
if str_eq(noun, "लड़की") { return "f" }
if str_eq(noun, "आदमी") { return "m" }
if str_eq(noun, "औरत") { return "f" }
if str_eq(noun, "घर") { return "m" }
if str_eq(noun, "मेज़") { return "f" }
if str_eq(noun, "किताब") { return "f" }
if str_eq(noun, "पानी") { return "m" }
if str_eq(noun, "दूध") { return "m" }
if str_eq(noun, "हाथ") { return "m" }
if str_eq(noun, "आँख") { return "f" }
if str_eq(noun, "बच्चा") { return "m" }
if str_eq(noun, "बच्ची") { return "f" }
if str_eq(noun, "काम") { return "m" }
if str_eq(noun, "बात") { return "f" }
if str_eq(noun, "दिन") { return "m" }
if str_eq(noun, "रात") { return "f" }
if str_eq(noun, "देश") { return "m" }
if str_eq(noun, "भाषा") { return "f" }
if str_eq(noun, "जगह") { return "f" }
if str_eq(noun, "समय") { return "m" }
if str_eq(noun, "साल") { return "m" }
// Default to masculine
return "m"
}
// Masculine noun declension
//
// Two patterns:
// (A) Ends in -आ ( matra): लड़क, बच्च, ड़ vowel-final type
// direct sg: base (लड़क)
// direct pl: stem + (लड़के)
// oblique sg: stem + (लड़के)
// oblique pl: stem + (लड़क)
//
// (B) Consonant-final or other: आदम, घर, थ, ि invariant type
// direct sg: base
// direct pl: base (no change in direct plural for most)
// oblique sg: base (no change)
// oblique pl: base + (घरं, ं, ि)
// Exception: आदम (masc ending ): acts like (B) in direct but
// oblique pl uses drop the ī: आदमि
fn hi_masc_aa_stem(noun: String) -> String {
// Strip trailing (aa matra, 3 UTF-8 bytes in Devanagari but El strings
// are Unicode so we drop the last character which is )
return hi_str_drop_last(noun, 1)
}
fn hi_noun_direct_m(noun: String, number: String) -> String {
if hi_str_ends(noun, "") {
// Pattern A: aa-final masculine
if str_eq(number, "sg") { return noun }
// pl: replace with
return hi_masc_aa_stem(noun) + ""
}
// Pattern B: invariant direct forms
return noun
}
fn hi_noun_oblique_m(noun: String, number: String) -> String {
if hi_str_ends(noun, "") {
// Pattern A
let stem: String = hi_masc_aa_stem(noun)
if str_eq(number, "sg") { return stem + "" }
return stem + "ों"
}
// Pattern B: consonant-final or ī-final
// आदम and similar ī-final masculines: oblique pl inserts
if hi_str_ends(noun, "") {
if str_eq(number, "sg") { return noun }
// oblique pl: drop , add ि
let stem: String = hi_str_drop_last(noun, 1)
return stem + "ियों"
}
// Default consonant-final masculines
if str_eq(number, "sg") { return noun }
return noun + "ों"
}
// Feminine noun declension
//
// Two patterns:
// (A) Ends in -ई/- (long ii): लड़क, बच्च
// direct sg: base (लड़क)
// direct pl: stem + ि (लड़कि)
// oblique sg: base (लड़क)
// oblique pl: stem + ि (लड़कि)
//
// (B) Consonant-final or other: मेज़, त, िब, त, औरत,
// direct sg: base (मेज़)
// direct pl: base + ें (मेज़ें, िबें)
// oblique sg: base
// oblique pl: base + (मेज़ं, ि)
fn hi_noun_direct_f(noun: String, number: String) -> String {
if hi_str_ends(noun, "") {
// Pattern A: ii-final feminine
if str_eq(number, "sg") { return noun }
let stem: String = hi_str_drop_last(noun, 1)
return stem + "ियाँ"
}
// Pattern B: other feminine
if str_eq(number, "sg") { return noun }
// Direct plural: add ें
return noun + "ें"
}
fn hi_noun_oblique_f(noun: String, number: String) -> String {
if hi_str_ends(noun, "") {
// Pattern A
if str_eq(number, "sg") { return noun }
let stem: String = hi_str_drop_last(noun, 1)
return stem + "ियों"
}
// Pattern B
if str_eq(number, "sg") { return noun }
return noun + "ों"
}
// Unified noun declension entry points
// hi_noun_direct: direct case form (nominative/bare accusative).
fn hi_noun_direct(noun: String, gender: String, number: String) -> String {
if str_eq(gender, "m") { return hi_noun_direct_m(noun, number) }
if str_eq(gender, "f") { return hi_noun_direct_f(noun, number) }
return noun
}
// hi_noun_oblique: oblique case form (before postpositions).
fn hi_noun_oblique(noun: String, gender: String, number: String) -> String {
if str_eq(gender, "m") { return hi_noun_oblique_m(noun, number) }
if str_eq(gender, "f") { return hi_noun_oblique_f(noun, number) }
return noun
}
// Postpositional particles
//
// Hindi marks grammatical relations with postpositions that follow the oblique
// noun. The genitive postposition agrees with the possessed noun's gender and
// number use hi_agree_genitive for that case.
//
// case values:
// "nominative" no postposition (direct case subject)
// "accusative_animate" (ko) animate direct objects
// "dative" (ko)
// "genitive" /क/के use hi_agree_genitive instead
// "locative_in" में (meṃ in)
// "locative_on" पर (par on)
// "instrumental" से (se with/by)
// "ablative" से (se from)
// "comitative" के (ke saath with/together with)
// "benefactive" के ि (ke liye for)
fn hi_postposition(gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return "" }
if str_eq(gram_case, "accusative_animate") { return "को" }
if str_eq(gram_case, "dative") { return "को" }
if str_eq(gram_case, "genitive") { return "का" } // base; use hi_agree_genitive
if str_eq(gram_case, "locative_in") { return "में" }
if str_eq(gram_case, "locative_on") { return "पर" }
if str_eq(gram_case, "instrumental") { return "से" }
if str_eq(gram_case, "ablative") { return "से" }
if str_eq(gram_case, "comitative") { return "के साथ" }
if str_eq(gram_case, "benefactive") { return "के लिए" }
return ""
}
// hi_agree_genitive: return the genitive postposition (/क/के) that agrees
// with the gender and number of the POSSESSED noun (not the possessor).
//
// possessed_gender: "m" | "f"
// possessed_number: "sg" | "pl"
//
// m sg (kaa)
// f sg (kii)
// m pl के (ke)
// f pl (kii)
fn hi_agree_genitive(possessed_gender: String, possessed_number: String) -> String {
if str_eq(possessed_gender, "f") { return "की" }
if str_eq(possessed_number, "pl") { return "के" }
return "का"
}
// Verb stem extraction
//
// Most Hindi infinitives end in -न (naa). Stripping it gives the verb stem.
// Examples: , , करन कर, देखन देख
//
// Irregulars that do not follow this pattern return a hardcoded stem.
fn hi_verb_stem(infinitive: String) -> String {
// Irregular stems
if str_eq(infinitive, "होना") { return "हो" }
if str_eq(infinitive, "करना") { return "कर" }
if str_eq(infinitive, "जाना") { return "जा" }
if str_eq(infinitive, "आना") { return "" }
if str_eq(infinitive, "देना") { return "दे" }
if str_eq(infinitive, "लेना") { return "ले" }
if str_eq(infinitive, "देखना") { return "देख" }
if str_eq(infinitive, "कहना") { return "कह" }
if str_eq(infinitive, "जानना") { return "जान" }
if str_eq(infinitive, "चाहना") { return "चाह" }
if str_eq(infinitive, "खाना") { return "खा" }
if str_eq(infinitive, "पीना") { return "पी" }
if str_eq(infinitive, "सोना") { return "सो" }
if str_eq(infinitive, "लिखना") { return "लिख" }
if str_eq(infinitive, "पढ़ना") { return "पढ़" }
if str_eq(infinitive, "बोलना") { return "बोल" }
if str_eq(infinitive, "चलना") { return "चल" }
if str_eq(infinitive, "बैठना") { return "बैठ" }
if str_eq(infinitive, "उठना") { return "उठ" }
if str_eq(infinitive, "मिलना") { return "मिल" }
if str_eq(infinitive, "रहना") { return "रह" }
if str_eq(infinitive, "सुनना") { return "सुन" }
if str_eq(infinitive, "समझना") { return "समझ" }
if str_eq(infinitive, "मानना") { return "मान" }
if str_eq(infinitive, "बनाना") { return "बना" }
if str_eq(infinitive, "लाना") { return "ला" }
if str_eq(infinitive, "भेजना") { return "भेज" }
if str_eq(infinitive, "खोलना") { return "खोल" }
if str_eq(infinitive, "बंद करना") { return "बंद कर" }
// Generic: strip trailing (last character)
if hi_str_ends(infinitive, "ना") {
return hi_str_drop_last(infinitive, 1)
// Note: is two Unicode chars ( + ) but as a suffix pattern
// we want to strip the last char '' and keep 'न' however the
// actual Devanagari suffix is a single syllable grapheme cluster.
// str_drop_last drops the last Unicode codepoint.
// = (na) + (aa-matra): str_drop_last(s,1) drops , leaving at end.
// We need to drop 2 codepoints ( and = ) to get the true stem.
}
return infinitive
}
// hi_verb_stem_clean: correctly strips -न (2 Unicode codepoints: + matra)
fn hi_verb_stem_clean(infinitive: String) -> String {
// Irregulars first
if str_eq(infinitive, "होना") { return "हो" }
if str_eq(infinitive, "करना") { return "कर" }
if str_eq(infinitive, "जाना") { return "जा" }
if str_eq(infinitive, "आना") { return "" }
if str_eq(infinitive, "देना") { return "दे" }
if str_eq(infinitive, "लेना") { return "ले" }
if str_eq(infinitive, "देखना") { return "देख" }
if str_eq(infinitive, "कहना") { return "कह" }
if str_eq(infinitive, "जानना") { return "जान" }
if str_eq(infinitive, "चाहना") { return "चाह" }
if str_eq(infinitive, "खाना") { return "खा" }
if str_eq(infinitive, "पीना") { return "पी" }
if str_eq(infinitive, "सोना") { return "सो" }
if str_eq(infinitive, "लिखना") { return "लिख" }
if str_eq(infinitive, "पढ़ना") { return "पढ़" }
if str_eq(infinitive, "बोलना") { return "बोल" }
if str_eq(infinitive, "चलना") { return "चल" }
if str_eq(infinitive, "बैठना") { return "बैठ" }
if str_eq(infinitive, "उठना") { return "उठ" }
if str_eq(infinitive, "मिलना") { return "मिल" }
if str_eq(infinitive, "रहना") { return "रह" }
if str_eq(infinitive, "सुनना") { return "सुन" }
if str_eq(infinitive, "समझना") { return "समझ" }
if str_eq(infinitive, "मानना") { return "मान" }
if str_eq(infinitive, "बनाना") { return "बना" }
if str_eq(infinitive, "लाना") { return "ला" }
if str_eq(infinitive, "भेजना") { return "भेज" }
if str_eq(infinitive, "खोलना") { return "खोल" }
// Strip = 2 codepoints
if hi_str_ends(infinitive, "ना") {
return hi_str_drop_last(infinitive, 2)
}
return infinitive
}
// Present tense habitual conjugation
//
// Structure: stem + aspect-suffix + auxiliary
//
// Aspect suffixes (agree with SUBJECT gender/number):
// m sg: (taa)
// f sg: (tii)
// m pl: ते (te)
// f pl: (tii)
//
// Auxiliary /है/हैं (am/is/are) agreed with person and number:
// 1sg: हूँ (huuṃ)
// 2sg: (ho) [informal], हैं (haiṃ) [formal]
// 3sg: है (hai)
// 1pl: हैं (haiṃ)
// 2pl: (ho) [informal], हैं (haiṃ) [formal]
// 3pl: हैं (haiṃ)
fn hi_present_aspect(gender: String, number: String) -> String {
if str_eq(gender, "f") { return "ती" }
if str_eq(number, "pl") { return "ते" }
return "ता"
}
fn hi_aux_present(person: String, number: String) -> String {
if str_eq(person, "1") {
if str_eq(number, "sg") { return "हूँ" }
return "हैं"
}
if str_eq(person, "2") {
if str_eq(number, "sg") { return "हो" }
return "हो"
}
// third person
if str_eq(number, "sg") { return "है" }
return "हैं"
}
// Past tense conjugation
//
// Simple past suffix agrees with subject (when object has no ).
// stem + past-suffix:
// m sg: (aa) e.g. (khaayaa)
// f sg: (ii) e.g. (khaaii)
// m pl: (e) e.g. (khaae)
// f pl: ईं (iiṃ) e.g. ईं (khaaiṃ)
//
// Many verb stems ending in a vowel undergo contraction the irregulars table
// handles known cases; otherwise stem + suffix is concatenated.
fn hi_past_suffix(gender: String, number: String) -> String {
if str_eq(gender, "m") {
if str_eq(number, "sg") { return "" }
return ""
}
// feminine
if str_eq(number, "sg") { return "" }
return "ईं"
}
// Past tense irregulars full past form returned for known verb+gender+number.
fn hi_past_irregular(stem: String, gender: String, number: String) -> String {
// (ho): /थ/थे/थ
if str_eq(stem, "हो") {
if str_eq(gender, "m") {
if str_eq(number, "sg") { return "था" }
return "थे"
}
if str_eq(number, "sg") { return "थी" }
return "थीं"
}
// (jaa): गय/गई/गए/गईं
if str_eq(stem, "जा") {
if str_eq(gender, "m") {
if str_eq(number, "sg") { return "गया" }
return "गए"
}
if str_eq(number, "sg") { return "गई" }
return "गईं"
}
// करन (kar): ि/क/किए/क
if str_eq(stem, "कर") {
if str_eq(gender, "m") {
if str_eq(number, "sg") { return "किया" }
return "किए"
}
if str_eq(number, "sg") { return "की" }
return "कीं"
}
// देन (de): ि/द/दिए/द
if str_eq(stem, "दे") {
if str_eq(gender, "m") {
if str_eq(number, "sg") { return "दिया" }
return "दिए"
}
if str_eq(number, "sg") { return "दी" }
return "दीं"
}
// लेन (le): ि/ल/लिए/ल
if str_eq(stem, "ले") {
if str_eq(gender, "m") {
if str_eq(number, "sg") { return "लिया" }
return "लिए"
}
if str_eq(number, "sg") { return "ली" }
return "लीं"
}
// आन (aa): आय/आई/आए/आईं
if str_eq(stem, "") {
if str_eq(gender, "m") {
if str_eq(number, "sg") { return "आया" }
return "आए"
}
if str_eq(number, "sg") { return "आई" }
return "आईं"
}
// (khaa): /खई/खए/खईं
if str_eq(stem, "खा") {
if str_eq(gender, "m") {
if str_eq(number, "sg") { return "खाया" }
return "खाए"
}
if str_eq(number, "sg") { return "खाई" }
return "खाईं"
}
// (pii): ि/प/पिए/प
if str_eq(stem, "पी") {
if str_eq(gender, "m") {
if str_eq(number, "sg") { return "पिया" }
return "पिए"
}
if str_eq(number, "sg") { return "पी" }
return "पीं"
}
// No irregular match
return ""
}
// Future tense conjugation
//
// Future: stem + future-suffix (fused person+gender+number)
//
// 1sg m: ऊँग (uuṃgaa) 1sg f: ऊँग (uuṃgii)
// 2sg m: ओगे (oge) 2sg f: ओग (ogii)
// 3sg m: एग (egaa) 3sg f: एग (egii)
// 1pl m: एंगे (eṃge) 1pl f: एंग (eṃgii)
// 2pl m: ओगे (oge) 2pl f: ओग (ogii)
// 3pl m: एंगे (eṃge) 3pl f: एंग (eṃgii)
fn hi_future_suffix(person: String, number: String, gender: String) -> String {
if str_eq(person, "1") {
if str_eq(number, "sg") {
if str_eq(gender, "f") { return "ऊँगी" }
return "ऊँगा"
}
if str_eq(gender, "f") { return "एंगी" }
return "एंगे"
}
if str_eq(person, "2") {
if str_eq(gender, "f") { return "ओगी" }
return "ओगे"
}
// third person
if str_eq(number, "sg") {
if str_eq(gender, "f") { return "एगी" }
return "एगा"
}
if str_eq(gender, "f") { return "एंगी" }
return "एंगे"
}
// Tense suffix (aspect suffix only, without auxiliary)
//
// For callers that need just the aspect suffix to construct compound tenses.
fn hi_tense_suffix(tense: String, gender: String, number: String) -> String {
if str_eq(tense, "present") { return hi_present_aspect(gender, number) }
if str_eq(tense, "past") { return hi_past_suffix(gender, number) }
// future uses hi_future_suffix (includes person)
return ""
}
// (honaa to be) special handling
//
// is deeply irregular. Its forms are used as auxiliaries throughout.
//
// present: हूँ/ह/है/हैं (via hi_aux_present)
// past: /थ/थे/थ (via hi_past_irregular)
// future: ऊँग/हओगे/हएग etc. (stem + future suffix)
fn hi_hona_present(person: String, number: String) -> String {
return hi_aux_present(person, number)
}
fn hi_hona_past(gender: String, number: String) -> String {
if str_eq(gender, "m") {
if str_eq(number, "sg") { return "था" }
return "थे"
}
if str_eq(number, "sg") { return "थी" }
return "थीं"
}
// Full verb conjugation
//
// hi_conjugate: conjugate a Hindi verb.
//
// verb: Hindi infinitive in Devanagari (e.g. "खाना", "जाना", "करना")
// tense: "present" | "past" | "future"
// person: "1" | "2" | "3"
// gender: "m" | "f" (subject gender)
// number: "sg" | "pl"
//
// Returns the complete inflected verb string (including auxiliary where needed).
fn hi_conjugate(verb: String, tense: String, person: String, gender: String, number: String) -> String {
let stem: String = hi_verb_stem_clean(verb)
// fully irregular
if str_eq(verb, "होना") {
if str_eq(tense, "present") { return hi_hona_present(person, number) }
if str_eq(tense, "past") { return hi_hona_past(gender, number) }
// future: ऊँग etc.
return "हो" + hi_future_suffix(person, number, gender)
}
if str_eq(tense, "present") {
let aspect: String = hi_present_aspect(gender, number)
let aux: String = hi_aux_present(person, number)
return stem + aspect + " " + aux
}
if str_eq(tense, "past") {
let irreg: String = hi_past_irregular(stem, gender, number)
if !str_eq(irreg, "") {
return irreg
}
return stem + hi_past_suffix(gender, number)
}
if str_eq(tense, "future") {
return stem + hi_future_suffix(person, number, gender)
}
// Unknown tense: return infinitive
return verb
}
// Noun phrase construction helpers
// hi_noun_with_post: return the oblique noun form followed by its postposition.
// Use for all cases that require an oblique form (dative, locative, etc.).
//
// noun: Devanagari noun string
// gender: "m" | "f"
// number: "sg" | "pl"
// case: postposition case key (see hi_postposition)
fn hi_noun_with_post(noun: String, gender: String, number: String, gram_case: String) -> String {
let post: String = hi_postposition(gram_case)
if str_eq(post, "") {
// Nominative: use direct form
return hi_noun_direct(noun, gender, number)
}
let oblique: String = hi_noun_oblique(noun, gender, number)
return oblique + " " + post
}
// hi_genitive_phrase: "X का/की/के Y" possessive phrase.
// possessor and possessed are bare noun strings; the function computes all
// inflections and agreement automatically.
//
// possessor_gender/number: gender and number of the possessor noun
// possessed_gender/number: gender and number of the possessed noun (drives agreement)
fn hi_genitive_phrase(possessor: String, possessor_gender: String, possessor_number: String,
possessed: String, possessed_gender: String, possessed_number: String) -> String {
let obl: String = hi_noun_oblique(possessor, possessor_gender, possessor_number)
let gen: String = hi_agree_genitive(possessed_gender, possessed_number)
let poss: String = hi_noun_direct(possessed, possessed_gender, possessed_number)
return obl + " " + gen + " " + poss
}