// 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" }