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

935 lines
38 KiB
EmacsLisp

// morphology-sw.el - Swahili morphology for the NLG engine.
//
// Implements Swahili noun class detection, subject/object agreement prefixes,
// tense markers, verb conjugation, negation, noun pluralization, and adjective
// agreement.
//
// Swahili is an agglutinative SVO language that uses a noun class system
// (15+ classes) instead of grammatical gender. Noun classes determine the
// agreement prefixes on verbs, adjectives, and other nominals.
//
// Key facts:
// - No case endings word order and prepositions handle relations
// - Verb morphology: SUBJ-TENSE-OBJ-STEM(-final vowel)
// - Latin script; tonal but tone unmarked in standard orthography
//
// Conventions used throughout:
// class: "1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9"|"10"|"11"|"15"
// person: "1sg"|"2sg"|"3sg"|"1pl"|"2pl"|"3pl" | "1"|"2"|"3" + number "sg"/"pl"
// tense: "present"|"progressive"|"past"|"future"|"perfect"
// number: "sg" | "pl"
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with,
// str_drop_last)
// String helpers
fn sw_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn sw_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 sw_str_first_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 {
return ""
}
return str_slice(s, 0, 1)
}
fn sw_str_first2(s: String) -> String {
let n: Int = str_len(s)
if n < 2 {
return s
}
return str_slice(s, 0, 2)
}
fn sw_str_first3(s: String) -> String {
let n: Int = str_len(s)
if n < 3 {
return s
}
return str_slice(s, 0, 3)
}
fn sw_str_last_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 {
return ""
}
return str_slice(s, n - 1, n)
}
// Noun class detection
//
// Returns the singular noun class number as a string.
// The noun class is inferred from the noun's prefix according to the standard
// Bantu noun class system used in Swahili.
//
// Class pairs (singular/plural):
// Class 1/2 (M-WA): m-/mw- wa- people, animates
// Class 3/4 (M-MI): m-/mw- mi- trees, plants, objects
// Class 5/6 (JI-MA): ji-/j- ma- fruits, augmentatives
// Class 7/8 (KI-VI): ki-/ch- vi-/vy- things, languages
// Class 9/10 (N): n-/m-/ same animals, loanwords
// Class 11 (U): u-/w- (10) abstract nouns
// Class 15 (KU): ku- infinitives as nouns
//
// Disambiguation between class 1 and class 3 (both m-/mw- prefix):
// Class 1 nouns refer to people/animates; class 3 to inanimate objects/plants.
// A lexical lookup for common class 1 nouns handles the ambiguous cases.
//
// Note: Class 9/10 nouns often have no visible prefix (zero-prefix or nasal
// prefix that assimilates to the following consonant).
fn sw_is_class1_noun(noun: String) -> Bool {
// Known class 1 (M-WA: human/animate) nouns
if str_eq(noun, "mtu") { return true } // person
if str_eq(noun, "mwanafunzi") { return true } // student
if str_eq(noun, "mwalimu") { return true } // teacher
if str_eq(noun, "mke") { return true } // wife/woman
if str_eq(noun, "mume") { return true } // husband/man
if str_eq(noun, "mtoto") { return true } // child
if str_eq(noun, "mgeni") { return true } // guest/stranger
if str_eq(noun, "mwana") { return true } // child/son
if str_eq(noun, "mkubwa") { return true } // elder/adult
if str_eq(noun, "mdogo") { return true } // young one
if str_eq(noun, "mgonjwa") { return true } // sick person/patient
if str_eq(noun, "mfanyakazi") { return true } // worker
if str_eq(noun, "mkulima") { return true } // farmer
if str_eq(noun, "mwimbaji") { return true } // singer
if str_eq(noun, "msomaji") { return true } // reader
if str_eq(noun, "mwandishi") { return true } // writer
if str_eq(noun, "mpiganaji") { return true } // fighter/warrior
if str_eq(noun, "msaidizi") { return true } // helper
if str_eq(noun, "mpishi") { return true } // cook
if str_eq(noun, "mwanasheria") { return true } // lawyer
if str_eq(noun, "daktari") { return true } // doctor (cl.9, but animate)
if str_eq(noun, "rafiki") { return true } // friend (cl.9)
if str_eq(noun, "ndugu") { return true } // sibling/relative (cl.9)
return false
}
fn sw_noun_class(noun: String) -> String {
// Class 15: ku- prefix infinitives used as nouns
if sw_str_ends(noun, "ku") {
if str_eq(sw_str_first2(noun), "ku") { return "15" }
}
if sw_str_first2(noun) == "ku" { return "15" }
// Explicit class 15 check
let p2: String = sw_str_first2(noun)
if str_eq(p2, "ku") { return "15" }
// Class 7/8 (KI-VI): ki- or ch-
let p3: String = sw_str_first3(noun)
if str_eq(p3, "ki-") { return "7" }
if str_eq(p2, "ki") { return "7" }
if str_eq(p2, "ch") { return "7" } // e.g. chakula (food), choo (toilet)
// Class 11 (U): u- or w- prefix abstract nouns
// Must check before class 1/3 m- prefix checks
let p1: String = sw_str_first_char(noun)
if str_eq(p1, "u") { return "11" }
if str_eq(p1, "w") { return "11" } // e.g. wimbo (song) class 11
// Class 5/6 (JI-MA): ji- prefix or zero-prefix fruits/augmentatives
if str_eq(p2, "ji") { return "5" }
// Common class 5 nouns with zero prefix (j- before vowel)
if str_eq(noun, "jicho") { return "5" } // eye
if str_eq(noun, "jino") { return "5" } // tooth
if str_eq(noun, "bega") { return "5" } // shoulder
if str_eq(noun, "tunda") { return "5" } // fruit
if str_eq(noun, "embe") { return "5" } // mango
if str_eq(noun, "gari") { return "5" } // car/vehicle
if str_eq(noun, "bei") { return "5" } // price
if str_eq(noun, "sauti") { return "5" } // voice/sound
if str_eq(noun, "thamani") { return "5" } // value
// Class 1/3 (M- prefix): disambiguate by lexical lookup
if str_eq(p1, "m") {
if sw_is_class1_noun(noun) { return "1" }
return "3" // class 3: trees, plants, inanimate m- nouns
}
if str_eq(p2, "mw") {
if sw_is_class1_noun(noun) { return "1" }
return "3"
}
// Class 9/10 (N): nasal prefix or zero prefix catch-all for most
// loanwords and animal names
// Many class 9 nouns start with: n, ny, ng, mb, nd, nj, nz, or bare vowel
if str_eq(p2, "ny") { return "9" }
if str_eq(p2, "ng") { return "9" }
if str_eq(p2, "mb") { return "9" }
if str_eq(p2, "nd") { return "9" }
if str_eq(p2, "nj") { return "9" }
if str_eq(p2, "nz") { return "9" }
if str_eq(p1, "n") { return "9" }
// Common lexical class 9 nouns (animals, loanwords, etc.)
if str_eq(noun, "paka") { return "9" } // cat
if str_eq(noun, "mbwa") { return "9" } // dog
if str_eq(noun, "simba") { return "9" } // lion
if str_eq(noun, "tembo") { return "9" } // elephant
if str_eq(noun, "nyoka") { return "9" } // snake
if str_eq(noun, "samaki") { return "9" } // fish
if str_eq(noun, "rafiki") { return "9" } // friend
if str_eq(noun, "daktari") { return "9" } // doctor
if str_eq(noun, "serikali") { return "9" } // government
if str_eq(noun, "hospitali") { return "9" } // hospital
if str_eq(noun, "shule") { return "9" } // school
if str_eq(noun, "kanisa") { return "9" } // church
if str_eq(noun, "ofisi") { return "9" } // office
if str_eq(noun, "picha") { return "9" } // picture/photo
if str_eq(noun, "sehemu") { return "9" } // part/section
if str_eq(noun, "habari") { return "9" } // news
if str_eq(noun, "nchi") { return "9" } // country/land
if str_eq(noun, "bahari") { return "9" } // sea/ocean
if str_eq(noun, "dunia") { return "9" } // world
if str_eq(noun, "ardhi") { return "9" } // ground/earth
// Default to class 9 for unknown nouns (most common catch-all)
return "9"
}
// Subject agreement prefixes
//
// Returns the subject prefix for verb conjugation.
//
// For personal pronouns: person "1","2","3" + number "sg","pl"
// For noun class agreement: person "3" + the noun's class
//
// Personal:
// 1sg (mimi) ni-
// 2sg (wewe) u-
// 3sg class 1 a-
// 1pl (sisi) tu-
// 2pl (nyinyi) m-
// 3pl class 2 wa-
//
// Noun class 3sg:
// cl.1 a- cl.2 wa-
// cl.3 u- cl.4 i-
// cl.5 li- cl.6 ya-
// cl.7 ki- cl.8 vi-
// cl.9 i- cl.10 zi-
// cl.11 u- cl.15 ku-
fn sw_subj_prefix(person: String, number: String, noun_class: String) -> String {
// First and second person always use personal prefixes regardless of noun class
if str_eq(person, "1") {
if str_eq(number, "sg") { return "ni" }
return "tu"
}
if str_eq(person, "2") {
if str_eq(number, "sg") { return "u" }
return "m"
}
// Third person: agree with noun class
// Plural classes
if str_eq(number, "pl") {
if str_eq(noun_class, "1") { return "wa" } // class 2 (wa-)
if str_eq(noun_class, "2") { return "wa" }
if str_eq(noun_class, "3") { return "i" } // class 4 (i-)
if str_eq(noun_class, "4") { return "i" }
if str_eq(noun_class, "5") { return "ya" } // class 6 (ya-)
if str_eq(noun_class, "6") { return "ya" }
if str_eq(noun_class, "7") { return "vi" } // class 8 (vi-)
if str_eq(noun_class, "8") { return "vi" }
if str_eq(noun_class, "9") { return "zi" } // class 10 (zi-)
if str_eq(noun_class, "10") { return "zi" }
if str_eq(noun_class, "11") { return "zi" } // class 11 pl uses cl.10
return "zi"
}
// Singular classes
if str_eq(noun_class, "1") { return "a" }
if str_eq(noun_class, "3") { return "u" }
if str_eq(noun_class, "4") { return "i" }
if str_eq(noun_class, "5") { return "li" }
if str_eq(noun_class, "6") { return "ya" }
if str_eq(noun_class, "7") { return "ki" }
if str_eq(noun_class, "8") { return "vi" }
if str_eq(noun_class, "9") { return "i" }
if str_eq(noun_class, "10") { return "zi" }
if str_eq(noun_class, "11") { return "u" }
if str_eq(noun_class, "15") { return "ku" }
// Default: third person animate sg
return "a"
}
// Object agreement prefixes
//
// Object infixes are inserted between tense marker and verb stem.
// They mirror the subject prefixes with minor differences.
//
// 1sg ni- 2sg ku- 3sg cl.1 m-/mw-
// 1pl tu- 2pl wa- 3pl cl.2 wa-
// cl.3 sg u- cl.4 pl i-
// cl.5 sg li- cl.6 pl ya-
// cl.7 sg ki- cl.8 pl vi-
// cl.9 sg i- cl.10 pl zi-
// cl.11 u- cl.15 ku-
fn sw_obj_prefix(person: String, number: String, noun_class: String) -> String {
if str_eq(person, "1") {
if str_eq(number, "sg") { return "ni" }
return "tu"
}
if str_eq(person, "2") {
if str_eq(number, "sg") { return "ku" }
return "wa"
}
// Third person object
if str_eq(number, "pl") {
if str_eq(noun_class, "1") { return "wa" }
if str_eq(noun_class, "2") { return "wa" }
if str_eq(noun_class, "3") { return "i" }
if str_eq(noun_class, "4") { return "i" }
if str_eq(noun_class, "5") { return "ya" }
if str_eq(noun_class, "6") { return "ya" }
if str_eq(noun_class, "7") { return "vi" }
if str_eq(noun_class, "8") { return "vi" }
if str_eq(noun_class, "9") { return "zi" }
if str_eq(noun_class, "10") { return "zi" }
return "wa"
}
// Singular
if str_eq(noun_class, "1") { return "m" }
if str_eq(noun_class, "3") { return "u" }
if str_eq(noun_class, "5") { return "li" }
if str_eq(noun_class, "7") { return "ki" }
if str_eq(noun_class, "9") { return "i" }
if str_eq(noun_class, "11") { return "u" }
if str_eq(noun_class, "15") { return "ku" }
return "m"
}
// Tense markers
//
// Tense markers are inserted between the subject prefix and the verb stem.
//
// "present" -a- (habitual/simple present; final vowel -a)
// "progressive" -na- (present progressive)
// "past" -li- (simple past)
// "future" -ta- (future)
// "perfect" -me- (present perfect)
// "remote_past" -li- (same marker; distinction via context or -ku-)
// "subjunctive" (no tense marker; final vowel changes to -e)
fn sw_tense_marker(tense: String) -> String {
if str_eq(tense, "present") { return "a" }
if str_eq(tense, "progressive") { return "na" }
if str_eq(tense, "past") { return "li" }
if str_eq(tense, "future") { return "ta" }
if str_eq(tense, "perfect") { return "me" }
if str_eq(tense, "subjunctive") { return "" }
if str_eq(tense, "remote_past") { return "li" }
// Default to progressive marker
return "na"
}
// Final vowel (verb ending)
//
// The final vowel of the verb stem changes based on tense and negation.
//
// Positive forms:
// present habitual / progressive / past / future / perfect: -a
// subjunctive: -e
//
// Negative forms:
// present: -i (the negative present uses -i instead of -a)
// past: -a (negative past: ha- prefix + -ku- tense + stem + -a)
// others: -e (negative subjunctive)
//
// Note: For most tenses the final vowel is simply -a. Only the negative present
// and subjunctive change it.
fn sw_verb_final(tense: String, negative: Bool) -> String {
if negative {
if str_eq(tense, "present") { return "i" }
if str_eq(tense, "progressive") { return "i" }
if str_eq(tense, "subjunctive") { return "e" }
return "a"
}
if str_eq(tense, "subjunctive") { return "e" }
return "a"
}
// Negative subject prefix
//
// Negation in Swahili is expressed by replacing the subject prefix with a
// negative counterpart (ha- for most; si- for 1sg).
//
// Positive Negative subject prefixes:
// ni- si- (1sg)
// u- hu- (2sg)
// a- ha- (3sg cl.1)
// tu- hatu- (1pl)
// m- ham- (2pl)
// wa- hawa- (3pl cl.2)
// other class 3sg prefixes: ha + prefix (e.g. ha+u = hau, ha+li = hali)
fn sw_neg_subj_prefix(person: String, number: String, noun_class: String) -> String {
if str_eq(person, "1") {
if str_eq(number, "sg") { return "si" }
return "hatu"
}
if str_eq(person, "2") {
if str_eq(number, "sg") { return "hu" }
return "ham"
}
// Third person negative = ha + positive subject prefix
let pos: String = sw_subj_prefix(person, number, noun_class)
return "ha" + pos
}
// Stem extraction
//
// Swahili infinitives begin with ku-. The verb stem is the infinitive minus ku-.
// For verbs where the stem itself starts with a vowel, the ku- is retained in
// some contexts (kuomba, kusoma etc.) but stripped for conjugation.
//
// Special cases:
// kula (to eat) stem: l- (kula la in conjugation: anakula)
// kuwa (to be) stem: wa (but equatorial forms are suppletive: ni/u/ni)
// kwenda (to go) stem: end (kw + end enda)
// kuja (to come) stem: ja
fn sw_verb_stem(infinitive: String) -> String {
if str_eq(infinitive, "kula") { return "l" } // eat irregular: anakula
if str_eq(infinitive, "kuwa") { return "wa" } // be
if str_eq(infinitive, "kwenda") { return "enda" }
if str_eq(infinitive, "kuja") { return "ja" }
if str_eq(infinitive, "kusoma") { return "soma" }
if str_eq(infinitive, "kusema") { return "sema" }
if str_eq(infinitive, "kuona") { return "ona" }
if str_eq(infinitive, "kufanya") { return "fanya" }
if str_eq(infinitive, "kutaka") { return "taka" }
if str_eq(infinitive, "kujua") { return "jua" }
if str_eq(infinitive, "kupata") { return "pata" }
if str_eq(infinitive, "kuambia") { return "ambia" }
if str_eq(infinitive, "kuleta") { return "leta" }
if str_eq(infinitive, "kuweka") { return "weka" }
if str_eq(infinitive, "kuingia") { return "ingia" }
if str_eq(infinitive, "kutoka") { return "toka" }
if str_eq(infinitive, "kupiga") { return "piga" }
if str_eq(infinitive, "kuimba") { return "imba" }
if str_eq(infinitive, "kucheza") { return "cheza" }
if str_eq(infinitive, "kulala") { return "lala" }
if str_eq(infinitive, "kuandika") { return "andika" }
if str_eq(infinitive, "kununua") { return "nunua" }
if str_eq(infinitive, "kuuza") { return "uza" }
if str_eq(infinitive, "kupenda") { return "penda" }
if str_eq(infinitive, "kuchukua") { return "chukua" }
if str_eq(infinitive, "kulipa") { return "lipa" }
if str_eq(infinitive, "kusikia") { return "sikia" }
if str_eq(infinitive, "kuamka") { return "amka" }
if str_eq(infinitive, "kukaa") { return "kaa" }
if str_eq(infinitive, "kurudi") { return "rudi" }
if str_eq(infinitive, "kushinda") { return "shinda" }
if str_eq(infinitive, "kusaidia") { return "saidia" }
if str_eq(infinitive, "kuzungumza") { return "zungumza" }
if str_eq(infinitive, "kupumzika") { return "pumzika" }
if str_eq(infinitive, "kufika") { return "fika" }
if str_eq(infinitive, "kuomba") { return "omba" }
if str_eq(infinitive, "kushukuru") { return "shukuru" }
// Generic: strip leading ku (2 chars) or kw (for kwenda etc.)
if sw_str_first2(infinitive) == "ku" {
return str_slice(infinitive, 2, str_len(infinitive))
}
if sw_str_first2(infinitive) == "kw" {
return str_slice(infinitive, 2, str_len(infinitive))
}
return infinitive
}
// Positive verb conjugation
//
// Structure: SUBJ_PREFIX + TENSE_MARKER + STEM + FINAL_VOWEL
//
// Examples:
// anasoma = a (3sg cl.1) + na (prog) + soma (read) + [stem ends -a]
// nilikula = ni (1sg) + li (past) + ku (special) + la (kula stem)
// tutasema = tu (1pl) + ta (future) + sema (say) + [stem ends -a]
// amesoma = a (3sg) + me (perfect) + soma + [final -a]
//
// Special case: kula the stem is bare "l", and the full conjugation inserts
// ku before the stem in most tenses: a-na-ku-la, a-li-ku-la, a-ta-ku-la
// but a-me-l-a is not standard standard is amekula.
fn sw_conjugate(verb_stem: String, person: String, number: String, noun_class: String, tense: String) -> String {
let subj: String = sw_subj_prefix(person, number, noun_class)
let tm: String = sw_tense_marker(tense)
let fv: String = sw_verb_final(tense, false)
// kula (eat) stem is "l" but conjugates as "kula" after prefix+tense
if str_eq(verb_stem, "l") {
if str_eq(tm, "") {
return subj + "kula"
}
return subj + tm + "kula"
}
// kuwa (be) present equational forms are suppletive
if str_eq(verb_stem, "wa") {
if str_eq(tense, "present") {
// Equational: ni/u/ni for 1sg/2sg/3sg; tu/m/ni for pl
if str_eq(person, "1") {
if str_eq(number, "sg") { return "ni" }
return "tu ni"
}
if str_eq(person, "2") {
if str_eq(number, "sg") { return "u" }
return "m ni"
}
// 3sg/3pl: use class-based copula
if str_eq(number, "sg") {
// Locative/existential forms: niko, uko, yuko etc. (class-based)
if str_eq(noun_class, "1") { return "yuko" }
if str_eq(noun_class, "3") { return "upo" }
if str_eq(noun_class, "5") { return "lipo" }
if str_eq(noun_class, "7") { return "kipo" }
if str_eq(noun_class, "9") { return "ipo" }
if str_eq(noun_class, "11") { return "upo" }
return "yuko"
}
// plural
if str_eq(noun_class, "1") { return "wako" }
if str_eq(noun_class, "3") { return "ipo" }
if str_eq(noun_class, "5") { return "yapo" }
if str_eq(noun_class, "7") { return "vipo" }
if str_eq(noun_class, "9") { return "zipo" }
return "wako"
}
// Progressive kuwa: niko/uko/yuko (existence/location)
if str_eq(tense, "progressive") {
if str_eq(person, "1") {
if str_eq(number, "sg") { return "niko" }
return "tuko"
}
if str_eq(person, "2") {
if str_eq(number, "sg") { return "uko" }
return "mko"
}
if str_eq(number, "sg") {
if str_eq(noun_class, "1") { return "yuko" }
return subj + "ko"
}
if str_eq(noun_class, "1") { return "wako" }
return subj + "ko"
}
// Past/future/perfect kuwa: regular
}
// Regular conjugation
let stem_final: String = sw_str_last_char(verb_stem)
// If stem already ends in the correct final vowel, don't double it
if str_eq(fv, "a") {
if str_eq(stem_final, "a") {
// Stem ends in -a: result is subj+tm+stem (final -a already present)
if str_eq(tm, "") {
return subj + verb_stem
}
return subj + tm + verb_stem
}
}
if str_eq(tm, "") {
return subj + verb_stem + fv
}
return subj + tm + verb_stem + fv
}
// Negative verb conjugation
//
// Negative structure: NEG_SUBJ_PREFIX + (tense) + STEM + FINAL_VOWEL_I
//
// Negative present:
// ha- prefix replaces subject prefix; final vowel -i
// e.g. hasomi (he/she doesn't read), sisomi (I don't read)
//
// Negative past:
// ha- prefix + -ku- tense infix + stem + -a
// e.g. hakusoma (he/she didn't read), sikusoma (I didn't read)
//
// Negative future:
// ha- prefix + -ta- + stem + -i (or sometimes -a; -i is more standard)
// e.g. hatasoma (he/she won't read)
//
// Negative perfect:
// ha- prefix + -ja- (not yet) + stem + -a
// e.g. hajaenda (he/she hasn't gone yet)
fn sw_negative(verb_stem: String, person: String, number: String, noun_class: String, tense: String) -> String {
let neg_subj: String = sw_neg_subj_prefix(person, number, noun_class)
// kula special gram_case
if str_eq(verb_stem, "l") {
if str_eq(tense, "past") { return neg_subj + "kukula" }
if str_eq(tense, "perfect") { return neg_subj + "jakula" }
return neg_subj + "kuli"
}
if str_eq(tense, "present") {
let fv: String = sw_verb_final("present", true) // -i
let stem_no_a: String = verb_stem
// If stem ends in -a, drop it before adding -i (some grammars)
// Standard: ha + stem-minus-final-a + i
let slen: Int = str_len(verb_stem)
if slen > 0 {
let last: String = sw_str_last_char(verb_stem)
if str_eq(last, "a") {
return neg_subj + sw_str_drop_last(verb_stem, 1) + fv
}
}
return neg_subj + verb_stem + fv
}
if str_eq(tense, "past") {
// Negative past: ha + ku + stem + a
return neg_subj + "ku" + verb_stem + "a"
}
if str_eq(tense, "future") {
// Negative future: ha + ta + stem + i (standard)
let fv: String = sw_verb_final("present", true) // -i
return neg_subj + "ta" + verb_stem + fv
}
if str_eq(tense, "perfect") {
// Negative perfect "not yet": ha + ja + stem + a
return neg_subj + "ja" + verb_stem + "a"
}
if str_eq(tense, "progressive") {
// Negative progressive: same as negative present
let fv: String = sw_verb_final("present", true)
let slen: Int = str_len(verb_stem)
if slen > 0 {
let last: String = sw_str_last_char(verb_stem)
if str_eq(last, "a") {
return neg_subj + sw_str_drop_last(verb_stem, 1) + fv
}
}
return neg_subj + verb_stem + fv
}
// Default negative: ha + stem + i
return neg_subj + verb_stem + "i"
}
// Noun plural formation
//
// Swahili plurals are formed by changing the noun class prefix.
//
// Class 1 (m-/mw-) Class 2 (wa-): mtu watu, mwalimu walimu
// Class 3 (m-/mw-) Class 4 (mi-): mti miti, mwamba miamba
// Class 5 (ji-/) Class 6 (ma-): jicho macho, tunda matunda
// Class 7 (ki-/ch-) Class 8 (vi-/vy-): kitu vitu, chakula vyakula
// Class 9 (n-/) Class 10 (n-/): same prefix no change in prefix;
// some class 9 nouns add ma- (ma- class) in plural
// Class 11 (u-) Class 10 (n-/): ubao mbao
//
// Known lexical exceptions are handled first.
fn sw_noun_plural(noun: String) -> String {
// Lexical irregulars
if str_eq(noun, "mtu") { return "watu" }
if str_eq(noun, "mtoto") { return "watoto" }
if str_eq(noun, "mke") { return "wake" }
if str_eq(noun, "mume") { return "waume" }
if str_eq(noun, "mwana") { return "wana" }
if str_eq(noun, "mwalimu") { return "walimu" }
if str_eq(noun, "mgeni") { return "wageni" }
if str_eq(noun, "mwanafunzi") { return "wanafunzi" }
if str_eq(noun, "mfanyakazi") { return "wafanyakazi" }
if str_eq(noun, "mkulima") { return "wakulima" }
if str_eq(noun, "mgonjwa") { return "wagonjwa" }
if str_eq(noun, "jicho") { return "macho" }
if str_eq(noun, "jino") { return "meno" }
if str_eq(noun, "bega") { return "mabega" }
if str_eq(noun, "tunda") { return "matunda" }
if str_eq(noun, "gari") { return "magari" }
if str_eq(noun, "embe") { return "maembe" }
if str_eq(noun, "wimbo") { return "nyimbo" } // class 11 10
if str_eq(noun, "ubao") { return "mbao" }
if str_eq(noun, "ugonjwa") { return "magonjwa" }
if str_eq(noun, "uso") { return "nyuso" }
if str_eq(noun, "ukuta") { return "kuta" }
if str_eq(noun, "ulimi") { return "ndimi" }
if str_eq(noun, "upande") { return "pande" }
if str_eq(noun, "uwezo") { return "nguvu" } // approximate
// Class 9/10 nouns: same form sg/pl (zero-change class)
if str_eq(noun, "paka") { return "paka" }
if str_eq(noun, "samaki") { return "samaki" }
if str_eq(noun, "rafiki") { return "rafiki" }
if str_eq(noun, "daktari") { return "madaktari" } // some use ma- pl
if str_eq(noun, "habari") { return "habari" }
if str_eq(noun, "nchi") { return "nchi" }
if str_eq(noun, "bahari") { return "bahari" }
if str_eq(noun, "shule") { return "shule" }
if str_eq(noun, "hospitali") { return "hospitali" }
if str_eq(noun, "ofisi") { return "ofisi" }
if str_eq(noun, "serikali") { return "serikali" }
// Productive rules
// Class 1 (m- people): m- wa-
if sw_is_class1_noun(noun) {
if str_eq(sw_str_first2(noun), "mw") {
return "wa" + str_slice(noun, 2, str_len(noun))
}
if str_eq(sw_str_first_char(noun), "m") {
return "wa" + str_slice(noun, 1, str_len(noun))
}
}
// Class 7 (ki-/ch-) Class 8 (vi-/vy-)
let p2: String = sw_str_first2(noun)
if str_eq(p2, "ki") {
return "vi" + str_slice(noun, 2, str_len(noun))
}
if str_eq(p2, "ch") {
return "vy" + str_slice(noun, 2, str_len(noun))
}
// Class 5 (ji-) Class 6 (ma-)
if str_eq(p2, "ji") {
return "ma" + str_slice(noun, 2, str_len(noun))
}
// Class 11 (u-/w-) Class 10 (n-/): drop u, may add n
let p1: String = sw_str_first_char(noun)
if str_eq(p1, "u") {
// Most u- class nouns: drop u bare stem (class 10 zero prefix)
return str_slice(noun, 1, str_len(noun))
}
// Class 3 (m-/mw- inanimate) Class 4 (mi-)
if str_eq(p1, "m") {
if str_eq(p2, "mw") {
return "mi" + str_slice(noun, 2, str_len(noun))
}
return "mi" + str_slice(noun, 1, str_len(noun))
}
// Class 9/10: no prefix change (same form)
return noun
}
// Adjective agreement
//
// Adjectives in Swahili take a class agreement prefix that matches the noun
// they modify. The prefix is placed before the adjective stem.
//
// Agreement prefixes (singular):
// cl.1 m-/mw- cl.2 wa-
// cl.3 m-/mw- cl.4 mi-
// cl.5 (j-)l- cl.6 ma-
// cl.7 ki-/ch- cl.8 vi-/vy-
// cl.9 (n-) cl.10 (n-)
// cl.11 mw- cl.15 ku-
//
// The adjective prefix for class 1/3 singular before a vowel-initial stem
// uses mw- instead of m-.
//
// number: "sg" | "pl"
// noun_class: singular class of the noun (the function applies the correct
// plural class internally when number == "pl")
fn sw_adj_prefix(noun_class: String, number: String) -> String {
// Plural classes
if str_eq(number, "pl") {
if str_eq(noun_class, "1") { return "wa" } // cl.2
if str_eq(noun_class, "2") { return "wa" }
if str_eq(noun_class, "3") { return "mi" } // cl.4
if str_eq(noun_class, "4") { return "mi" }
if str_eq(noun_class, "5") { return "ma" } // cl.6
if str_eq(noun_class, "6") { return "ma" }
if str_eq(noun_class, "7") { return "vi" } // cl.8
if str_eq(noun_class, "8") { return "vi" }
if str_eq(noun_class, "9") { return "n" } // cl.10 (nasal)
if str_eq(noun_class, "10") { return "n" }
if str_eq(noun_class, "11") { return "n" } // cl.10 in plural
return "wa"
}
// Singular classes
if str_eq(noun_class, "1") { return "m" }
if str_eq(noun_class, "3") { return "m" }
if str_eq(noun_class, "4") { return "mi" }
if str_eq(noun_class, "5") { return "j" } // l- before consonant, j- before vowel
if str_eq(noun_class, "6") { return "ma" }
if str_eq(noun_class, "7") { return "ki" }
if str_eq(noun_class, "8") { return "vi" }
if str_eq(noun_class, "9") { return "n" }
if str_eq(noun_class, "10") { return "n" }
if str_eq(noun_class, "11") { return "mw" }
if str_eq(noun_class, "15") { return "ku" }
return ""
}
// sw_agree_adj: apply the agreement prefix to an adjective stem.
//
// adj_stem: the adjective without any class prefix (e.g. "zuri", "kubwa", "dogo")
// noun_class: class of the noun being modified (singular class number as string)
// number: "sg" | "pl"
//
// Returns the fully prefixed adjective form.
//
// Note: many common adjectives in Swahili are Arabic/Bantu borrowings and do
// not take class prefixes (e.g. "nzuri" beautiful, "kubwa" big, "dogo" small).
// Prefix agreement applies primarily to native Bantu adjective stems and
// demonstratives. This function applies the prefix mechanically; for adjectives
// that are invariant loanwords the caller should use the bare form.
fn sw_agree_adj(adj_stem: String, noun_class: String, number: String) -> String {
// Invariant adjectives (no class prefix required in standard usage)
if str_eq(adj_stem, "nzuri") { return "nzuri" } // good/beautiful
if str_eq(adj_stem, "baya") { return "baya" } // bad
if str_eq(adj_stem, "safi") { return "safi" } // clean
if str_eq(adj_stem, "chafu") { return "chafu" } // dirty
if str_eq(adj_stem, "ghali") { return "ghali" } // expensive
if str_eq(adj_stem, "rahisi") { return "rahisi" } // cheap/easy
if str_eq(adj_stem, "mzuri") { return sw_adj_prefix(noun_class, number) + "zuri" }
// Apply class prefix to Bantu adjective stems
let prefix: String = sw_adj_prefix(noun_class, number)
if str_eq(prefix, "") {
return adj_stem
}
// cl.1/3 prefix m- before consonant, mw- before vowel
if str_eq(prefix, "m") {
let first: String = sw_str_first_char(adj_stem)
if str_eq(first, "a") { return "mw" + adj_stem }
if str_eq(first, "e") { return "mw" + adj_stem }
if str_eq(first, "i") { return "mw" + adj_stem }
if str_eq(first, "o") { return "mw" + adj_stem }
if str_eq(first, "u") { return "mw" + adj_stem }
return "m" + adj_stem
}
// cl.5 prefix j- before vowel, l- before consonant
if str_eq(prefix, "j") {
let first: String = sw_str_first_char(adj_stem)
if str_eq(first, "a") { return "j" + adj_stem }
if str_eq(first, "e") { return "j" + adj_stem }
if str_eq(first, "i") { return "j" + adj_stem }
if str_eq(first, "o") { return "j" + adj_stem }
if str_eq(first, "u") { return "j" + adj_stem }
return "l" + adj_stem
}
return prefix + adj_stem
}
// Demonstrative agreement
//
// Swahili demonstratives agree with the noun class.
// "this" (proximal): huyu (cl.1/2), huu (cl.3/11), hili (cl.5), hiki (cl.7),
// hii (cl.9), huu (cl.11)
// "that" (distal): yule (cl.1/2), ule (cl.3/11), lile (cl.5), kile (cl.7),
// ile (cl.9)
//
// proximity: "near" | "far"
fn sw_demonstrative(noun_class: String, number: String, proximity: String) -> String {
if str_eq(proximity, "near") {
if str_eq(number, "pl") {
if str_eq(noun_class, "1") { return "hawa" }
if str_eq(noun_class, "3") { return "hii" }
if str_eq(noun_class, "5") { return "haya" }
if str_eq(noun_class, "7") { return "hivi" }
if str_eq(noun_class, "9") { return "hizi" }
return "hawa"
}
if str_eq(noun_class, "1") { return "huyu" }
if str_eq(noun_class, "3") { return "huu" }
if str_eq(noun_class, "5") { return "hili" }
if str_eq(noun_class, "7") { return "hiki" }
if str_eq(noun_class, "9") { return "hii" }
if str_eq(noun_class, "11") { return "huu" }
if str_eq(noun_class, "15") { return "huku" }
return "hii"
}
// distal
if str_eq(number, "pl") {
if str_eq(noun_class, "1") { return "wale" }
if str_eq(noun_class, "3") { return "ile" }
if str_eq(noun_class, "5") { return "yale" }
if str_eq(noun_class, "7") { return "vile" }
if str_eq(noun_class, "9") { return "zile" }
return "wale"
}
if str_eq(noun_class, "1") { return "yule" }
if str_eq(noun_class, "3") { return "ule" }
if str_eq(noun_class, "5") { return "lile" }
if str_eq(noun_class, "7") { return "kile" }
if str_eq(noun_class, "9") { return "ile" }
if str_eq(noun_class, "11") { return "ule" }
if str_eq(noun_class, "15") { return "kule" }
return "ile"
}
// Swahili copula (kuwa to be)
//
// The copula in Swahili is complex:
//
// Equational ("X is Y", noun predicate):
// ni (is/am/are) invariant for present equational
// si negative present equational
//
// Locative/existential ("X is [somewhere]", with -ko/-po/-mo):
// niko/uko/yuko/tuko/mko/wako with -ko (definite location)
// nipo/upo/yupo/tupo/mpo/wapo with -po (near location)
// nimo/umo/yumo/tumo/mmo/wamo with -mo (inside)
//
// sw_copula_present: returns the present copula form.
// use_case: "equative" | "locative"
fn sw_copula_present(person: String, number: String, use_case: String) -> String {
if str_eq(use_case, "equative") {
if str_eq(person, "1") { return "ni" }
if str_eq(person, "2") { return "ni" }
return "ni"
}
// locative -ko (most common)
if str_eq(person, "1") {
if str_eq(number, "sg") { return "niko" }
return "tuko"
}
if str_eq(person, "2") {
if str_eq(number, "sg") { return "uko" }
return "mko"
}
if str_eq(number, "sg") { return "yuko" }
return "wako"
}
// sw_copula_neg_present: negative present copula.
// equative: si (invariant for all persons in negative equational)
fn sw_copula_neg_present(person: String, number: String) -> String {
if str_eq(person, "1") {
if str_eq(number, "sg") { return "si" }
return "si"
}
if str_eq(person, "2") {
if str_eq(number, "sg") { return "si" }
return "si"
}
return "si"
}